Commit 1d49b2ff authored by Mygod's avatar Mygod

Merge branch 'master' into beta

parents 7922be56 28ee28ce
...@@ -29,11 +29,14 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails ...@@ -29,11 +29,14 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails
3. Some ROM like [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) has **very** broken direct boot support; 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. 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) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888) * Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
* Fixes for EMUI: [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Fixes for Huawei: [#1091 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836) * 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) * 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) * Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
* Another Samsung: [#1712](https://github.com/shadowsocks/shadowsocks-android/issues/1712)
* Don't install this app on SD card because of permission issues: [#1124 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453) * Don't install this app on SD card because of permission issues: [#1124 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453)
* `INTERACT_ACROSS_USERS` permission missing: [#1184](https://github.com/shadowsocks/shadowsocks-android/issues/1184)
### How to pause Shadowsocks service? ### How to pause Shadowsocks service?
......
...@@ -12,11 +12,6 @@ ...@@ -12,11 +12,6 @@
Использйте интеграцию с [Tasker](http://tasker.dinglisch.net/). Использйте интеграцию с [Tasker](http://tasker.dinglisch.net/).
### Как добавить QR код из локальной галереи?
Отсканируйте его при помощи стороннего сканера, например [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) и нажмите на `ss` ссылку.
### Почему режим NAT более не поддерживается? ### Почему режим NAT более не поддерживается?
1. Требуются ROOT права; 1. Требуются ROOT права;
...@@ -34,10 +29,12 @@ ...@@ -34,10 +29,12 @@
3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки"; 3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки";
4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними. 4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
* Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888) * Исправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
* Исправления для EMUI: [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Иправления для Huawei: [#1091 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836) * Иправления для Huawei: [#1091 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
* Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414) * Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung и/или Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410) * Samsung и/или Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
* Ещё Samsung: [#1712](https://github.com/shadowsocks/shadowsocks-android/issues/1712)
* Не устанавливайте это приложение на SD карту, из-за проблем с разрешениями: [#1124 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453) * Не устанавливайте это приложение на SD карту, из-за проблем с разрешениями: [#1124 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453)
### Как приостановить службу Shadowsocks? ### Как приостановить службу Shadowsocks?
...@@ -58,7 +55,12 @@ ...@@ -58,7 +55,12 @@
Разрешите этому приложению передачу данных в фоновом режиме в настройках приложения. Разрешите этому приложению передачу данных в фоновом режиме в настройках приложения.
Если у вас оперератор Yota, то измените точку доступа APN с `yota.ru` на `client.yota.ru` Если у вас оперератор Yota, попробуйте следующее:
* измените точку доступа APN с `yota.ru` на `client.yota.ru`
* погоняйте программы: Speedtest, Telegram (скачивание файлов)
Я уже уведомил оператора о проблеме и вместе мы стараемся решить её.
### Почему требуется разрешение камере на устройствах младше Android 6.0? ### Почему требуется разрешение камере на устройствах младше Android 6.0?
......
Please check the [FAQ](https://github.com/shadowsocks/shadowsocks-android/wiki/FAQ) before submitting new issues. Please check [FAQ](.github/faq.md) before submitting new issues.
And just in case you don't know,
[don't ask questions on issues](https://medium.com/@methane/why-you-must-not-ask-questions-on-github-issues-51d741d83fde).
...@@ -20,10 +20,14 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Kotlin. ...@@ -20,10 +20,14 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Kotlin.
### BUILD ### BUILD
You can check whether the latest commit builds under UNIX environment by checking Travis status.
Building on Windows is also possible since [#1570](https://github.com/shadowsocks/shadowsocks-android/pull/1570),
but probably painful. Further contributions regarding building on Windows are also welcome.
* Set environment variable `ANDROID_HOME` to `/path/to/android-sdk` * Set environment variable `ANDROID_HOME` to `/path/to/android-sdk`
* (optional) Set environment variable `ANDROID_NDK_HOME` to `/path/to/android-ndk` (default: `$ANDROID_HOME/ndk-bundle`) * (optional) Set environment variable `ANDROID_NDK_HOME` to `/path/to/android-ndk` (default: `$ANDROID_HOME/ndk-bundle`)
* Set environment variable `GOROOT_BOOTSTRAP` to `/path/to/go` * Set environment variable `GOROOT_BOOTSTRAP` to `/path/to/go`
* Fetch submodules using `$ git submodule update --init --recursive` * Clone the repo using `git clone --recurse-submodules <repo>` or update submodules using `git submodule update --init --recursive`
* Build it using Android Studio or gradle script * Build it using Android Studio or gradle script
### TRANSLATE ### TRANSLATE
......
...@@ -4,17 +4,17 @@ apply plugin: 'com.github.ben-manes.versions' ...@@ -4,17 +4,17 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext { ext {
kotlinVersion = '1.2.30' kotlinVersion = '1.2.41'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 'P' sdkVersion = 28
compileSdkVersion = 'android-P' compileSdkVersion = 28
buildToolsVersion = '28-rc1' buildToolsVersion = '28.0.0'
supportLibraryVersion = '28.0.0-alpha1' supportLibraryVersion = '28.0.0-alpha3'
takisoftFixVersion = '27.1.0.0' takisoftFixVersion = '27.1.1.1'
playServicesVersion = '11.8.0' roomVersion = '1.1.0'
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.0.1' androidTestVersion = '1.0.2'
androidEspressoVersion = '3.0.1' androidEspressoVersion = '3.0.2'
} }
repositories { repositories {
...@@ -23,9 +23,9 @@ buildscript { ...@@ -23,9 +23,9 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-alpha05' classpath 'com.android.tools.build:gradle:3.1.3'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'com.google.gms:google-services:3.2.0' classpath 'com.google.gms:google-services:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
} }
} }
......
...@@ -38,7 +38,7 @@ android { ...@@ -38,7 +38,7 @@ android {
task goBuild(type: Exec) { task goBuild(type: Exec) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) { if (Os.isFamily(Os.FAMILY_WINDOWS)) {
executable "cmd.exe" executable "cmd.exe"
args "/c", "src\\overture\\make.bat " + minSdkVersion args "/c", file("src/overture/make.bat").absolutePath , minSdkVersion
} else { } else {
executable "sh" executable "sh"
args "-c", "src/overture/make.bash " + minSdkVersion args "-c", "src/overture/make.bash " + minSdkVersion
...@@ -48,7 +48,7 @@ task goBuild(type: Exec) { ...@@ -48,7 +48,7 @@ task goBuild(type: Exec) {
task goClean(type: Exec) { task goClean(type: Exec) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) { if (Os.isFamily(Os.FAMILY_WINDOWS)) {
executable "cmd.exe" executable "cmd.exe"
args "/c", "src\\overture\\clean.bat" args "/c", file("src/overture/clean.bat").absolutePath
} else { } else {
executable "sh" executable "sh"
args "-c", "src/overture/clean.bash" args "-c", "src/overture/clean.bash"
......
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pkgutil
import urlparse
import socket
import logging
from argparse import ArgumentParser
from datetime import date
__all__ = ['main']
def parse_args():
parser = ArgumentParser()
parser.add_argument('-i', '--input', dest='input', required=True,
help='path to gfwlist', metavar='GFWLIST')
parser.add_argument('-f', '--file', dest='output', required=True,
help='path to output acl', metavar='ACL')
return parser.parse_args()
def decode_gfwlist(content):
# decode base64 if have to
try:
return content.decode('base64')
except:
return content
def get_hostname(something):
try:
# quite enough for GFW
if not something.startswith('http:'):
something = 'http://' + something
r = urlparse.urlparse(something)
return r.hostname
except Exception as e:
logging.error(e)
return None
def add_domain_to_set(s, something):
hostname = get_hostname(something)
if hostname is not None:
if hostname.startswith('.'):
hostname = hostname.lstrip('.')
if hostname.endswith('/'):
hostname = hostname.rstrip('/')
if hostname:
s.add(hostname)
def parse_gfwlist(content):
gfwlist = content.splitlines(False)
domains = set()
for line in gfwlist:
if line.find('.*') >= 0:
continue
elif line.find('*') >= 0:
line = line.replace('*', '/')
if line.startswith('!'):
continue
elif line.startswith('['):
continue
elif line.startswith('@'):
# ignore white list
continue
elif line.startswith('||'):
add_domain_to_set(domains, line.lstrip('||'))
elif line.startswith('|'):
add_domain_to_set(domains, line.lstrip('|'))
elif line.startswith('.'):
add_domain_to_set(domains, line.lstrip('.'))
else:
add_domain_to_set(domains, line)
# TODO: reduce ['www.google.com', 'google.com'] to ['google.com']
return domains
def generate_acl(domains):
header ="""#
# GFW list from https://github.com/gfwlist/gfwlist/blob/master/gfwlist.txt
# updated on DATE
#
[bypass_all]
[proxy_list]
"""
header = header.replace('DATE', str(date.today()))
proxy_content = ""
ip_content = ""
for domain in sorted(domains):
try:
socket.inet_aton(domain)
ip_content += (domain + "\n")
except socket.error:
domain = domain.replace('.', '\.')
proxy_content += ('(^|\.)' + domain + '$\n')
proxy_content = header + ip_content + proxy_content
return proxy_content
def main():
args = parse_args()
with open(args.input, 'rb') as f:
content = f.read()
content = decode_gfwlist(content)
domains = parse_gfwlist(content)
acl_content = generate_acl(domains)
with open(args.output, 'wb') as f:
f.write(acl_content)
if __name__ == '__main__':
main()
Subproject commit 1a21989566e778139c489d5f8972979bda9b16c1 Subproject commit a9d56518bb3e0662e76f60e0ce087a35d6af8323
...@@ -28,24 +28,38 @@ MKDIR %TARGET%\armeabi-v7a>nul 2>nul ...@@ -28,24 +28,38 @@ MKDIR %TARGET%\armeabi-v7a>nul 2>nul
MKDIR %TARGET%\x86>nul 2>nul MKDIR %TARGET%\x86>nul 2>nul
MKDIR %TARGET%\arm64-v8a>nul 2>nul MKDIR %TARGET%\arm64-v8a>nul 2>nul
SET CC=%ANDROID_ARM_TOOLCHAIN%\bin\arm-linux-androideabi-gcc.exe
IF NOT EXIST %ANDROID_ARM_CC% ( IF NOT EXIST %ANDROID_ARM_CC% (
ECHO "Make standalone toolchain for ARM arch" ECHO "Make standalone toolchain for ARM arch"
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm ^ python.exe %ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm ^
--api %MIN_API% --install-dir %ANDROID_ARM_TOOLCHAIN% --api %MIN_API% --install-dir %ANDROID_ARM_TOOLCHAIN%
) )
IF NOT EXIST %ANDROID_ARM64_CC% ( IF NOT EXIST %ANDROID_ARM64_CC% (
ECHO "Make standalone toolchain for ARM64 arch" ECHO "Make standalone toolchain for ARM64 arch"
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm64 ^ python.exe %ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm64 ^
--api %MIN_API% --install-dir %ANDROID_ARM64_TOOLCHAIN% --api %MIN_API% --install-dir %ANDROID_ARM64_TOOLCHAIN%
) )
IF NOT EXIST %ANDROID_X86_CC% ( IF NOT EXIST %ANDROID_X86_CC% (
ECHO "Make standalone toolchain for X86 arch" ECHO "Make standalone toolchain for X86 arch"
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch x86 ^ python.exe %ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch x86 ^
--api %MIN_API% --install-dir %ANDROID_X86_TOOLCHAIN% --api %MIN_API% --install-dir %ANDROID_X86_TOOLCHAIN%
) )
REM Check environment availability
IF NOT EXIST %CC% (
ECHO "gcc not found"
EXIT 1
)
WHERE python.exe
IF "%ERRORLEVEL%" == 1 (
ECHO "python not found"
EXIT 1
)
IF NOT EXIST %DIR%\go\bin\go.exe ( IF NOT EXIST %DIR%\go\bin\go.exe (
ECHO "Build the custom go" ECHO "Build the custom go"
...@@ -118,4 +132,4 @@ IF %BUILD% == 1 ( ...@@ -118,4 +132,4 @@ IF %BUILD% == 1 (
) )
ECHO "Successfully build overture" ECHO "Successfully build overture"
ENDLOCAL ENDLOCAL
\ No newline at end of file
Subproject commit 58ab31a9fdfcc7f62f00c0b093ff6a37efcc3ff2 Subproject commit afe34f99d4914ab35f6e423c042ed072f5697e05
# https://github.com/arturbosch/detekt/blob/801994bd60cc759b181649356cea045b8125301b/detekt-cli/src/main/resources/default-detekt-config.yml # https://github.com/arturbosch/detekt/blob/RC6-4/detekt-cli/src/main/resources/default-detekt-config.yml
comments: comments:
active: false active: false
...@@ -7,7 +7,7 @@ complexity: ...@@ -7,7 +7,7 @@ complexity:
active: true active: true
ComplexCondition: ComplexCondition:
active: true active: true
threshold: 3 threshold: 4
ComplexInterface: ComplexInterface:
active: true active: true
threshold: 10 threshold: 10
...@@ -15,6 +15,7 @@ complexity: ...@@ -15,6 +15,7 @@ complexity:
ComplexMethod: ComplexMethod:
active: true active: true
threshold: 10 threshold: 10
ignoreSingleWhenExpression: false
LabeledExpression: LabeledExpression:
active: true active: true
LargeClass: LargeClass:
...@@ -25,31 +26,32 @@ complexity: ...@@ -25,31 +26,32 @@ complexity:
threshold: 20 threshold: 20
LongParameterList: LongParameterList:
active: true active: true
threshold: 5 threshold: 6
ignoreDefaultParameters: true ignoreDefaultParameters: true
MethodOverloading: MethodOverloading:
active: false active: false
NestedBlockDepth: NestedBlockDepth:
active: true active: true
threshold: 3 threshold: 4
StringLiteralDuplication: StringLiteralDuplication:
active: true active: true
threshold: 2 threshold: 3
ignoreAnnotation: true ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^' ignoreStringsRegex: '$^'
TooManyFunctions: TooManyFunctions:
active: true active: true
thresholdInFiles: 10 thresholdInFiles: 11
thresholdInClasses: 10 thresholdInClasses: 11
thresholdInInterfaces: 10 thresholdInInterfaces: 11
thresholdInObjects: 10 thresholdInObjects: 11
thresholdInEnums: 10 thresholdInEnums: 11
empty-blocks: empty-blocks:
active: true active: true
EmptyCatchBlock: EmptyCatchBlock:
active: false active: true
allowedExceptionNameRegex: "^(_|ignore|expected).*"
EmptyClassBlock: EmptyClassBlock:
active: true active: true
EmptyDefaultConstructor: EmptyDefaultConstructor:
...@@ -64,6 +66,7 @@ empty-blocks: ...@@ -64,6 +66,7 @@ empty-blocks:
active: true active: true
EmptyFunctionBlock: EmptyFunctionBlock:
active: true active: true
ignoreOverriddenFunctions: false
EmptyIfBlock: EmptyIfBlock:
active: true active: true
EmptyInitBlock: EmptyInitBlock:
...@@ -105,7 +108,7 @@ exceptions: ...@@ -105,7 +108,7 @@ exceptions:
active: true active: true
TooGenericExceptionCaught: TooGenericExceptionCaught:
active: true active: true
exceptions: exceptionNames:
- ArrayIndexOutOfBoundsException - ArrayIndexOutOfBoundsException
- Error - Error
- Exception - Exception
...@@ -116,10 +119,9 @@ exceptions: ...@@ -116,10 +119,9 @@ exceptions:
- Throwable - Throwable
TooGenericExceptionThrown: TooGenericExceptionThrown:
active: true active: true
exceptions: exceptionNames:
- Error - Error
- Exception - Exception
- NullPointerException
- Throwable - Throwable
- RuntimeException - RuntimeException
...@@ -143,6 +145,7 @@ naming: ...@@ -143,6 +145,7 @@ naming:
FunctionNaming: FunctionNaming:
active: true active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^'
MatchingDeclarationName: MatchingDeclarationName:
active: true active: true
MemberNameEqualsClassName: MemberNameEqualsClassName:
...@@ -167,6 +170,7 @@ naming: ...@@ -167,6 +170,7 @@ naming:
active: true active: true
variablePattern: '[a-z][A-Za-z0-9]*' variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
performance: performance:
active: true active: true
...@@ -239,15 +243,17 @@ style: ...@@ -239,15 +243,17 @@ style:
maxLineLength: 120 maxLineLength: 120
excludePackageStatements: false excludePackageStatements: false
excludeImportStatements: false excludeImportStatements: false
MayBeConst:
active: true
ModifierOrder: ModifierOrder:
active: true active: true
NestedClassesVisibility: NestedClassesVisibility:
active: true active: true
NewLineAtEndOfFile: NewLineAtEndOfFile:
active: true active: true
OptionalAbstractKeyword: NoTabs:
active: true active: true
OptionalReturnKeyword: OptionalAbstractKeyword:
active: true active: true
OptionalUnit: OptionalUnit:
active: true active: true
...@@ -268,6 +274,8 @@ style: ...@@ -268,6 +274,8 @@ style:
ThrowsCount: ThrowsCount:
active: true active: true
max: 2 max: 2
TrailingWhitespace:
active: true
UnnecessaryAbstractClass: UnnecessaryAbstractClass:
active: true active: true
UnnecessaryInheritance: UnnecessaryInheritance:
...@@ -278,6 +286,8 @@ style: ...@@ -278,6 +286,8 @@ style:
active: true active: true
UnusedImports: UnusedImports:
active: true active: true
UnusedPrivateMember:
active: true
UseDataClass: UseDataClass:
active: false active: false
UtilityClassWithPublicConstructor: UtilityClassWithPublicConstructor:
......
...@@ -4,6 +4,7 @@ import java.util.regex.Pattern ...@@ -4,6 +4,7 @@ import java.util.regex.Pattern
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
def getCurrentFlavor() { def getCurrentFlavor() {
String task = getGradle().getStartParameter().getTaskRequests().toString() String task = getGradle().getStartParameter().getTaskRequests().toString()
...@@ -21,11 +22,16 @@ android { ...@@ -21,11 +22,16 @@ android {
applicationId "com.github.shadowsocks" applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion targetSdkVersion rootProject.sdkVersion
versionCode 4050100 versionCode 4060000
versionName "4.5.1" versionName "4.6.0"
testApplicationId "com.github.shadowsocks.test" testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW" resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
buildTypes { buildTypes {
debug { debug {
...@@ -54,25 +60,27 @@ android { ...@@ -54,25 +60,27 @@ android {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation "android.arch.persistence.room:runtime:$roomVersion"
implementation "com.android.support:customtabs:$supportLibraryVersion" implementation "com.android.support:customtabs:$supportLibraryVersion"
implementation "com.android.support:design:$supportLibraryVersion" implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:gridlayout-v7:$supportLibraryVersion" implementation "com.android.support:gridlayout-v7:$supportLibraryVersion"
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5' implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
implementation 'com.evernote:android-job:1.2.4' implementation 'com.evernote:android-job:1.2.6'
implementation "com.google.android.gms:play-services-ads:$playServicesVersion" implementation "com.google.android.gms:play-services-ads:15.0.1"
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion" implementation "com.google.android.gms:play-services-analytics:16.0.0"
implementation "com.google.android.gms:play-services-vision:$playServicesVersion" implementation "com.google.android.gms:play-services-vision:15.0.2"
implementation "com.google.firebase:firebase-config:$playServicesVersion" implementation "com.google.firebase:firebase-config:16.0.0"
implementation 'com.j256.ormlite:ormlite-android:5.1' implementation 'com.google.firebase:firebase-core:16.0.0'
implementation 'com.mikepenz:materialdrawer:6.0.6' implementation 'com.mikepenz:materialdrawer:6.0.7'
implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation "com.takisoft.fix:preference-v7-simplemenu:$takisoftFixVersion" implementation "com.takisoft.fix:preference-v7-simplemenu:$takisoftFixVersion"
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2' 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 'eu.chainfire:libsuperuser:1.0.0.201704021214'
implementation 'net.glxn.qrgen:android:2.0' implementation 'net.glxn.qrgen:android:2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3' implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
kapt "android.arch.persistence.room:compiler:$roomVersion"
testImplementation "android.arch.persistence.room:testing:$roomVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion" androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion" androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion"
......
{
"formatVersion": 1,
"database": {
"version": 26,
"identityHash": "a69accf94393d71abdb60873d4ea74a2",
"entities": [
{
"tableName": "Profile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `individual` TEXT NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePort",
"columnName": "remotePort",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "method",
"columnName": "method",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "route",
"columnName": "route",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remoteDns",
"columnName": "remoteDns",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "proxyApps",
"columnName": "proxyApps",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypass",
"columnName": "bypass",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "udpdns",
"columnName": "udpdns",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ipv6",
"columnName": "ipv6",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "individual",
"columnName": "individual",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tx",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "rx",
"columnName": "rx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userOrder",
"columnName": "userOrder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "plugin",
"columnName": "plugin",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "KeyValuePair",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "valueType",
"columnName": "valueType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"key"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a69accf94393d71abdb60873d4ea74a2\")"
]
}
}
\ No newline at end of file
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "f1aab1fb633378621635c344dbc8ac7b",
"entities": [
{
"tableName": "KeyValuePair",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "valueType",
"columnName": "valueType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"key"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"f1aab1fb633378621635c344dbc8ac7b\")"
]
}
}
\ No newline at end of file
...@@ -26,9 +26,11 @@ ...@@ -26,9 +26,11 @@
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
android:fullBackupOnly="true" android:fullBackupOnly="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.App"
android:supportsRtl="true" android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:banner="@drawable/ic_service_active"> android:banner="@drawable/ic_service_active"
tools:replace="android:theme">
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" /> android:value="true" />
...@@ -72,7 +74,7 @@ ...@@ -72,7 +74,7 @@
<activity <activity
android:name=".VpnRequestActivity" android:name=".VpnRequestActivity"
android:theme="@style/Theme.Translucent" android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
......
...@@ -2,7 +2,7 @@ package com.github.shadowsocks.aidl; ...@@ -2,7 +2,7 @@ package com.github.shadowsocks.aidl;
interface IShadowsocksServiceCallback { interface IShadowsocksServiceCallback {
oneway void stateChanged(int state, String profileName, String msg); oneway void stateChanged(int state, String profileName, String msg);
oneway void trafficUpdated(int profileId, long txRate, long rxRate, long txTotal, long rxTotal); oneway void trafficUpdated(long profileId, long txRate, long rxRate, long txTotal, long rxTotal);
// Traffic data has persisted to database, listener should refetch their data from database // Traffic data has persisted to database, listener should refetch their data from database
oneway void trafficPersisted(int profileId); oneway void trafficPersisted(long profileId);
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>MarkdownPad Document</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
/* Author: Nicolas Hery - http://nicolashery.com */
/* Version: 29d1c5bc36da364ad5aa86946d420b7bbc54a253 */
/* Source: https://github.com/nicolahery/markdownpad-github */
/* RESET
=============================================================================*/
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
}
/* BODY
=============================================================================*/
body {
font-family: Helvetica, arial, freesans, clean, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
background-color: #303030;
padding: 20px;
max-width: 960px;
margin: 0 auto;
}
body>*:first-child {
margin-top: 0 !important;
}
body>*:last-child {
margin-bottom: 0 !important;
}
/* BLOCKS
=============================================================================*/
p, blockquote, ul, ol, dl, table, pre {
margin: 15px 0;
color: white;
}
/* HEADERS
=============================================================================*/
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
}
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
font-size: inherit;
}
h1 {
font-size: 28px;
color: #000;
}
h2 {
font-size: 24px;
border-bottom: 1px solid #ccc;
color: #000;
}
h3 {
font-size: 18px;
color: white;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
color: #777;
font-size: 14px;
}
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
margin-top: 0;
padding-top: 0;
}
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
margin-top: 0;
padding-top: 0;
}
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
margin-top: 10px;
}
/* LINKS
=============================================================================*/
a {
color: #4183C4;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* LISTS
=============================================================================*/
ul, ol {
padding-left: 30px;
}
ul li > :first-child,
ol li > :first-child,
ul li ul:first-of-type,
ol li ol:first-of-type,
ul li ol:first-of-type,
ol li ul:first-of-type {
margin-top: 0px;
}
ul ul, ul ol, ol ol, ol ul {
margin-bottom: 0;
}
dl {
padding: 0;
}
dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px;
}
dl dt:first-child {
padding: 0;
}
dl dt>:first-child {
margin-top: 0px;
}
dl dt>:last-child {
margin-bottom: 0px;
}
dl dd {
margin: 0 0 15px;
padding: 0 15px;
}
dl dd>:first-child {
margin-top: 0px;
}
dl dd>:last-child {
margin-bottom: 0px;
}
/* CODE
=============================================================================*/
pre, code, tt {
font-size: 12px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
}
code, tt {
margin: 0 0px;
padding: 0px 0px;
white-space: nowrap;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius: 3px;
}
pre>code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent;
}
pre {
background-color: #f8f8f8;
border: 1px solid #ccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px;
}
pre code, pre tt {
background-color: transparent;
border: none;
}
/* QUOTES
=============================================================================*/
blockquote {
border-left: 4px solid #DDD;
padding: 0 15px;
color: #777;
}
blockquote>:first-child {
margin-top: 0px;
}
blockquote>:last-child {
margin-bottom: 0px;
}
/* HORIZONTAL RULES
=============================================================================*/
hr {
clear: both;
margin: 15px 0;
height: 0px;
overflow: hidden;
border: none;
background: transparent;
border-bottom: 4px solid #ddd;
padding: 0;
}
/* TABLES
=============================================================================*/
table th {
font-weight: bold;
}
table th, table td {
border: 1px solid #ccc;
padding: 6px 13px;
}
table tr {
border-top: 1px solid #ccc;
background-color: #303030;
}
table tr:nth-child(2n) {
background-color: #f8f8f8;
}
/* IMAGES
=============================================================================*/
img {
max-width: 100%
}
</style>
</head>
<body>
<p>A <a href="http://shadowsocks.org">Shadowsocks</a> client for Android, written in Kotlin.</p>
<p>Copyright (C) 2017 by Max Lv <a href="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#x3a;&#x6d;&#97;x&#46;&#99;.&#108;&#118;&#x40;&#103;&#109;&#97;&#x69;&#108;&#46;&#x63;&#x6f;&#109;">&#x6d;&#97;&#x78;.&#x63;&#x2e;&#108;v&#64;&#x67;&#109;&#x61;&#x69;&#108;&#x2e;&#99;&#111;&#109;</a></p>
<p>Copyright (C) 2017 by Mygod Studio <a href="mailto:contact-shadowsocks-android@mygod.be">contact-shadowsocks-android@mygod.be</a></p>
<p>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.</p>
<p>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.</p>
<p>You should have received a copy of the GNU General Public License
along with this program. If not, see <a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.</p>
<h3>Open Source Licenses</h3>
<ul>
<li>redsocks: <a href="https://github.com/shadowsocks/redsocks/blob/shadowsocks-android/README">APL 2.0</a></li>
<li>mbed TLS: <a href="https://github.com/ARMmbed/mbedtls/blob/development/LICENSE">APL 2.0</a></li>
<li>libevent: <a href="https://github.com/shadowsocks/libevent/blob/master/LICENSE">BSD</a></li>
<li>tun2socks: <a href="https://github.com/shadowsocks/badvpn/blob/shadowsocks-android/COPYING">BSD</a></li>
<li>pcre: <a href="https://android.googlesource.com/platform/external/pcre/+/master/dist2/LICENCE">BSD</a></li>
<li>libancillary: <a href="https://github.com/shadowsocks/libancillary/blob/shadowsocks-android/COPYING">BSD</a></li>
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a></li>
<li>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li>
<li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
</ul>
</body>
</html>
<!-- This document was created with MarkdownPad, the Markdown editor for Windows (http://markdownpad.com) -->
...@@ -20,9 +20,8 @@ ...@@ -20,9 +20,8 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.annotation.TargetApi
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v4.content.ContextCompat
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
...@@ -31,14 +30,20 @@ import android.webkit.WebView ...@@ -31,14 +30,20 @@ import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
class AboutFragment : ToolbarFragment() { class AboutFragment : ToolbarFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
inflater.inflate(R.layout.layout_about, container, false) // workaround for weird night mode bug
val configuration = resources.configuration
val result = inflater.inflate(R.layout.layout_about, container, false)
if (resources.configuration !== configuration) requireActivity().recreate()
return result
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
toolbar.title = getString(R.string.about_title, BuildConfig.VERSION_NAME) toolbar.title = getString(R.string.about_title, BuildConfig.VERSION_NAME)
val web = view.findViewById<WebView>(R.id.web_view) val web = view.findViewById<WebView>(R.id.web_view)
web.loadUrl("file:///android_asset/pages/about.html") web.setBackgroundColor(ContextCompat.getColor(requireActivity(), R.color.background))
web.loadUrl(getString(R.string.about_url))
web.webViewClient = object : WebViewClient() { web.webViewClient = object : WebViewClient() {
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
...@@ -46,7 +51,6 @@ class AboutFragment : ToolbarFragment() { ...@@ -46,7 +51,6 @@ class AboutFragment : ToolbarFragment() {
return true return true
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest): Boolean { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest): Boolean {
(activity as MainActivity).launchUrl(request.url) (activity as MainActivity).launchUrl(request.url)
return true return true
......
...@@ -23,6 +23,7 @@ package com.github.shadowsocks ...@@ -23,6 +23,7 @@ package com.github.shadowsocks
import android.app.Application import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
...@@ -33,9 +34,8 @@ import android.content.res.Configuration ...@@ -33,9 +34,8 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.UserManager
import android.support.annotation.RequiresApi import android.support.annotation.RequiresApi
import android.support.v4.os.BuildCompat
import android.support.v4.os.UserManagerCompat
import android.support.v7.app.AppCompatDelegate import android.support.v7.app.AppCompatDelegate
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
...@@ -57,7 +57,6 @@ import com.google.android.gms.analytics.StandardExceptionParser ...@@ -57,7 +57,6 @@ import com.google.android.gms.analytics.StandardExceptionParser
import com.google.android.gms.analytics.Tracker import com.google.android.gms.analytics.Tracker
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.j256.ormlite.logger.LocalLog
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
...@@ -72,10 +71,16 @@ class App : Application() { ...@@ -72,10 +71,16 @@ class App : Application() {
val deviceContext: Context by lazy { if (Build.VERSION.SDK_INT < 24) this else DeviceContext(this) } val deviceContext: Context by lazy { if (Build.VERSION.SDK_INT < 24) this else DeviceContext(this) }
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() } val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) } private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) }
private val exceptionParser by lazy { StandardExceptionParser(this, null) }
val info: PackageInfo by lazy { getPackageInfo(packageName) } val info: PackageInfo by lazy { getPackageInfo(packageName) }
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && getSystemService(DevicePolicyManager::class.java)
.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
}
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName, if (BuildCompat.isAtLeastP()) fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName,
PackageManager.GET_SIGNING_CERTIFICATES else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!! if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() { fun startService() {
val intent = Intent(this, BaseService.serviceClass.java) val intent = Intent(this, BaseService.serviceClass.java)
...@@ -87,7 +92,7 @@ class App : Application() { ...@@ -87,7 +92,7 @@ class App : Application() {
val currentProfile: Profile? get() = val currentProfile: Profile? get() =
if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId) if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId)
fun switchProfile(id: Int): Profile { fun switchProfile(id: Long): Profile {
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile() val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
DataStore.profileId = result.id DataStore.profileId = result.id
return result return result
...@@ -102,7 +107,7 @@ class App : Application() { ...@@ -102,7 +107,7 @@ class App : Application() {
fun track(t: Throwable) = track(Thread.currentThread(), t) fun track(t: Throwable) = track(Thread.currentThread(), t)
fun track(thread: Thread, t: Throwable) { fun track(thread: Thread, t: Throwable) {
tracker.send(HitBuilders.ExceptionBuilder() tracker.send(HitBuilders.ExceptionBuilder()
.setDescription(StandardExceptionParser(this, null).getDescription(thread.name, t)) .setDescription("${exceptionParser.getDescription(thread.name, t)} - ${t.message}")
.setFatal(false) .setFatal(false)
.build()) .build())
t.printStackTrace() t.printStackTrace()
...@@ -111,8 +116,8 @@ class App : Application() { ...@@ -111,8 +116,8 @@ class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
app = this app = this
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java, PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java) BottomSheetPreferenceDialogFragment::class.java)
...@@ -139,14 +144,15 @@ class App : Application() { ...@@ -139,14 +144,15 @@ class App : Application() {
app.track(e) app.track(e)
} }
// handle data restored // handle data restored/crash
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update() if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
(getSystemService(Context.USER_SERVICE) as UserManager).isUserUnlocked) DirectBoot.flushTrafficStats()
TcpFastOpen.enabledAsync(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) { if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
val assetManager = assets val assetManager = assets
for (dir in arrayOf("acl", "overture")) for (dir in arrayOf("acl", "overture"))
try { try {
for (file in assetManager.list(dir)) assetManager.open(dir + '/' + file).use { input -> for (file in assetManager.list(dir)) assetManager.open("$dir/$file").use { input ->
File(deviceContext.filesDir, file).outputStream().use { output -> input.copyTo(output) } File(deviceContext.filesDir, file).outputStream().use { output -> input.copyTo(output) }
} }
} catch (e: IOException) { } catch (e: IOException) {
...@@ -155,7 +161,7 @@ class App : Application() { ...@@ -155,7 +161,7 @@ class App : Application() {
} }
DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime) DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
} }
AppCompatDelegate.setDefaultNightMode(DataStore.nightMode)
updateNotificationChannels() updateNotificationChannels()
} }
......
...@@ -58,23 +58,21 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener { ...@@ -58,23 +58,21 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager): List<ProxiedApp> { private fun getApps(pm: PackageManager) = synchronized(AppManager) {
return synchronized(AppManager) { if (receiver == null) receiver = app.listenForPackageChanges {
if (receiver == null) receiver = app.listenForPackageChanges { synchronized(AppManager) {
synchronized(AppManager) { receiver = null
receiver = null cachedApps = null
cachedApps = null
}
AppManager.instance?.reloadApps()
} }
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached. AppManager.instance?.reloadApps()
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS) }
.filter { it.packageName != app.packageName && // Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false } val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
this.cachedApps = cachedApps .filter { it.packageName != app.packageName &&
cachedApps it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
}.map { ProxiedApp(pm, it.applicationInfo, it.packageName) } this.cachedApps = cachedApps
} cachedApps
}.map { ProxiedApp(pm, it.applicationInfo, it.packageName) }
} }
private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo, private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo,
...@@ -151,7 +149,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener { ...@@ -151,7 +149,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
appListView.visibility = View.GONE appListView.visibility = View.GONE
fastScroller.visibility = View.GONE fastScroller.visibility = View.GONE
loadingView.visibility = View.VISIBLE loadingView.visibility = View.VISIBLE
thread { thread("AppManager-loader") {
val adapter = appListView.adapter as AppsAdapter val adapter = appListView.adapter as AppsAdapter
do { do {
appsLoading.set(true) appsLoading.set(true)
......
...@@ -20,12 +20,12 @@ ...@@ -20,12 +20,12 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.admin.DevicePolicyManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.Preference import android.support.v7.preference.Preference
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
...@@ -39,20 +39,17 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompatDividers() { ...@@ -39,20 +39,17 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompatDividers() {
DataStore.initGlobal() DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global) addPreferencesFromResource(R.xml.pref_global)
val boot = findPreference(Key.isAutoConnect) as SwitchPreference val boot = findPreference(Key.isAutoConnect) as SwitchPreference
val directBootAwareListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
true
}
boot.setOnPreferenceChangeListener { _, value -> boot.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean BootReceiver.enabled = value as Boolean
directBootAwareListener.onPreferenceChange(null, DataStore.directBootAware) true
} }
boot.isChecked = BootReceiver.enabled boot.isChecked = BootReceiver.enabled
val dba = findPreference(Key.directBootAware) val canToggleLocked = findPreference(Key.directBootAware)
if (Build.VERSION.SDK_INT >= 24 && requireContext().getSystemService(DevicePolicyManager::class.java) if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER) if (app.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
dba.onPreferenceChangeListener = directBootAwareListener else dba.parent!!.removePreference(dba) true
} else canToggleLocked.parent!!.removePreference(canToggleLocked)
val tfo = findPreference(Key.tfo) as SwitchPreference val tfo = findPreference(Key.tfo) as SwitchPreference
tfo.isChecked = TcpFastOpen.sendEnabled tfo.isChecked = TcpFastOpen.sendEnabled
......
...@@ -37,8 +37,10 @@ import android.support.design.widget.Snackbar ...@@ -37,8 +37,10 @@ import android.support.design.widget.Snackbar
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.app.AppCompatDelegate
import android.support.v7.content.res.AppCompatResources import android.support.v7.content.res.AppCompatResources
import android.support.v7.preference.PreferenceDataStore import android.support.v7.preference.PreferenceDataStore
import android.text.format.Formatter
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
...@@ -49,7 +51,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService ...@@ -49,7 +51,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.Executable import com.github.shadowsocks.bg.Executable
import com.github.shadowsocks.bg.TrafficMonitor
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
...@@ -101,7 +102,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe ...@@ -101,7 +102,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
private val customTabsIntent by lazy { private val customTabsIntent by lazy {
CustomTabsIntent.Builder() CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.material_primary_500)) .setToolbarColor(ContextCompat.getColor(this, R.color.color_primary))
.build() .build()
} }
fun launchUrl(uri: Uri) = try { fun launchUrl(uri: Uri) = try {
...@@ -116,10 +117,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe ...@@ -116,10 +117,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
override fun stateChanged(state: Int, profileName: String?, msg: String?) { override fun stateChanged(state: Int, profileName: String?, msg: String?) {
app.handler.post { changeState(state, msg, true) } app.handler.post { changeState(state, msg, true) }
} }
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
app.handler.post { updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal) } app.handler.post { updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal) }
} }
override fun trafficPersisted(profileId: Int) { override fun trafficPersisted(profileId: Long) {
app.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) } app.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) }
} }
} }
...@@ -148,11 +149,11 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe ...@@ -148,11 +149,11 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
stateListener?.invoke(state) stateListener?.invoke(state)
} }
fun updateTraffic(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { fun updateTraffic(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.text = TrafficMonitor.formatTraffic(txTotal) txText.text = Formatter.formatFileSize(this, txTotal)
rxText.text = TrafficMonitor.formatTraffic(rxTotal) rxText.text = Formatter.formatFileSize(this, rxTotal)
txRateText.text = getString(R.string.speed, TrafficMonitor.formatTraffic(txRate)) txRateText.text = getString(R.string.speed, Formatter.formatFileSize(this, txRate))
rxRateText.text = getString(R.string.speed, TrafficMonitor.formatTraffic(rxRate)) rxRateText.text = getString(R.string.speed, Formatter.formatFileSize(this, rxRate))
val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment? val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment?
if (state != BaseService.STOPPING) if (state != BaseService.STOPPING)
child?.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal) child?.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal)
...@@ -266,18 +267,20 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe ...@@ -266,18 +267,20 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
++testCount ++testCount
statusText.setText(R.string.connection_test_testing) statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code val id = testCount // it would change by other code
thread { testConnection(id) } thread("ConnectionTest") { testConnection(id) }
} }
} }
fab = findViewById(R.id.fab) fab = findViewById(R.id.fab)
fab.setOnClickListener { fab.setOnClickListener {
if (state == BaseService.CONNECTED) app.stopService() else thread { when {
if (BaseService.usingVpnMode) { state == BaseService.CONNECTED -> app.stopService()
BaseService.usingVpnMode -> {
val intent = VpnService.prepare(this) val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT) if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else app.handler.post { onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null) } else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
} else app.startService() }
else -> app.startService()
} }
} }
...@@ -319,9 +322,15 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe ...@@ -319,9 +322,15 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
if (key == Key.serviceMode) app.handler.post { when (key) {
connection.disconnect() Key.serviceMode -> app.handler.post {
connection.connect() connection.disconnect()
connection.connect()
}
Key.nightMode -> {
AppCompatDelegate.setDefaultNightMode(DataStore.nightMode)
recreate()
}
} }
} }
......
...@@ -56,7 +56,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu ...@@ -56,7 +56,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
} }
private lateinit var profile: Profile private var profileId = -1L
private lateinit var isProxyApps: SwitchPreference private lateinit var isProxyApps: SwitchPreference
private lateinit var plugin: IconListPreference private lateinit var plugin: IconListPreference
private lateinit var pluginConfigure: EditTextPreference private lateinit var pluginConfigure: EditTextPreference
...@@ -66,13 +66,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu ...@@ -66,13 +66,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity() val activity = requireActivity()
val profile = ProfileManager.getProfile(activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L)
if (profile == null) {
activity.finish()
return
}
this.profile = profile
profile.serialize()
addPreferencesFromResource(R.xml.pref_profile) addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) { if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) {
findPreference(Key.host).summary = "shadowsocks.example.org" findPreference(Key.host).summary = "shadowsocks.example.org"
...@@ -130,10 +124,12 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu ...@@ -130,10 +124,12 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
} }
fun saveAndExit() { fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile()
profile.id = profileId
profile.deserialize() profile.deserialize()
ProfileManager.updateProfile(profile) ProfileManager.updateProfile(profile)
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profile.id) ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profileId)
if (DataStore.profileId == profile.id && DataStore.directBootAware) DirectBoot.update() if (DataStore.profileId == profileId && DataStore.directBootAware) DirectBoot.update()
requireActivity().finish() requireActivity().finish()
} }
...@@ -161,11 +157,11 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu ...@@ -161,11 +157,11 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference.key == Key.pluginConfigure) { if (preference.key == Key.pluginConfigure) {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE) val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) != null) if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else
startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS, startActivityForResult(intent
pluginConfiguration.selectedOptions.toString()), REQUEST_CODE_PLUGIN_CONFIGURE) else { .putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString())
showPluginEditor() .putExtra(PluginContract.EXTRA_NIGHT_MODE, DataStore.nightMode),
} REQUEST_CODE_PLUGIN_CONFIGURE)
} else super.onDisplayPreferenceDialog(preference) } else super.onDisplayPreferenceDialog(preference)
} }
...@@ -187,7 +183,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu ...@@ -187,7 +183,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt) .setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, { _, _ -> .setPositiveButton(R.string.yes, { _, _ ->
ProfileManager.delProfile(profile.id) ProfileManager.delProfile(profileId)
activity.finish() activity.finish()
}) })
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
......
...@@ -33,13 +33,13 @@ import android.support.design.widget.Snackbar ...@@ -33,13 +33,13 @@ import android.support.design.widget.Snackbar
import android.support.v4.app.DialogFragment import android.support.v4.app.DialogFragment
import android.support.v7.widget.* import android.support.v7.widget.*
import android.support.v7.widget.helper.ItemTouchHelper import android.support.v7.widget.helper.ItemTouchHelper
import android.text.format.Formatter
import android.view.* import android.view.*
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.TrafficMonitor
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
...@@ -69,7 +69,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -69,7 +69,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
BaseService.CONNECTED, BaseService.STOPPED -> true BaseService.CONNECTED, BaseService.STOPPED -> true
else -> false else -> false
} }
private fun isProfileEditable(id: Int) = when ((activity as MainActivity).state) { private fun isProfileEditable(id: Long) = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> id != DataStore.profileId BaseService.CONNECTED -> id != DataStore.profileId
BaseService.STOPPED -> true BaseService.STOPPED -> true
else -> false else -> false
...@@ -124,7 +124,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -124,7 +124,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private var adView: AdView? = null private var adView: AdView? = null
init { init {
edit.setOnClickListener { startConfig(item.id) } edit.setOnClickListener {
item = ProfileManager.getProfile(item.id)!!
startConfig(item)
}
TooltipCompat.setTooltipText(edit, edit.contentDescription) TooltipCompat.setTooltipText(edit, edit.contentDescription)
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
val share = itemView.findViewById<View>(R.id.share) val share = itemView.findViewById<View>(R.id.share)
...@@ -157,10 +160,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -157,10 +160,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
text2.visibility = View.VISIBLE text2.visibility = View.VISIBLE
text2.text = t2.joinToString("\n") text2.text = t2.joinToString("\n")
} }
val context = requireContext()
if (tx <= 0 && rx <= 0) traffic.visibility = View.GONE else { if (tx <= 0 && rx <= 0) traffic.visibility = View.GONE else {
traffic.visibility = View.VISIBLE traffic.visibility = View.VISIBLE
traffic.text = getString(R.string.traffic, traffic.text = getString(R.string.traffic,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)) Formatter.formatFileSize(context, tx), Formatter.formatFileSize(context, rx))
} }
if (item.id == DataStore.profileId) { if (item.id == DataStore.profileId) {
...@@ -177,7 +181,6 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -177,7 +181,6 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
val params = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, val params = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT) ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER_HORIZONTAL params.gravity = Gravity.CENTER_HORIZONTAL
val context = requireContext()
adView = AdView(context) adView = AdView(context)
adView.layoutParams = params adView.layoutParams = params
adView.adUnitId = "ca-app-pub-9097031975646651/7760346322" adView.adUnitId = "ca-app-pub-9097031975646651/7760346322"
...@@ -279,17 +282,17 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -279,17 +282,17 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
for ((_, item) in actions) ProfileManager.delProfile(item.id) for ((_, item) in actions) ProfileManager.delProfile(item.id)
} }
fun refreshId(id: Int) { fun refreshId(id: Long) {
val index = profiles.indexOfFirst { it.id == id } val index = profiles.indexOfFirst { it.id == id }
if (index >= 0) notifyItemChanged(index) if (index >= 0) notifyItemChanged(index)
} }
fun deepRefreshId(id: Int) { fun deepRefreshId(id: Long) {
val index = profiles.indexOfFirst { it.id == id } val index = profiles.indexOfFirst { it.id == id }
if (index < 0) return if (index < 0) return
profiles[index] = ProfileManager.getProfile(id)!! profiles[index] = ProfileManager.getProfile(id)!!
notifyItemChanged(index) notifyItemChanged(index)
} }
fun removeId(id: Int) { fun removeId(id: Long) {
val index = profiles.indexOfFirst { it.id == id } val index = profiles.indexOfFirst { it.id == id }
if (index < 0) return if (index < 0) return
profiles.removeAt(index) profiles.removeAt(index)
...@@ -302,14 +305,16 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -302,14 +305,16 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
val profilesAdapter by lazy { ProfilesAdapter() } val profilesAdapter by lazy { ProfilesAdapter() }
private lateinit var undoManager: UndoSnackbarManager<Profile> private lateinit var undoManager: UndoSnackbarManager<Profile>
private var bandwidthProfile: Int = 0 private var bandwidthProfile = 0L
private var txTotal: Long = 0L private var txTotal: Long = 0L
private var rxTotal: Long = 0L private var rxTotal: Long = 0L
private val clipboard by lazy { requireContext().systemService<ClipboardManager>() } private val clipboard by lazy { requireContext().systemService<ClipboardManager>() }
private fun startConfig(id: Int) = startActivity(Intent(context, ProfileConfigActivity::class.java) private fun startConfig(profile: Profile) {
.putExtra(Action.EXTRA_PROFILE_ID, id)) profile.serialize()
startActivity(Intent(context, ProfileConfigActivity::class.java).putExtra(Action.EXTRA_PROFILE_ID, profile.id))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_list, container, false) inflater.inflate(R.layout.layout_list, container, false)
...@@ -320,8 +325,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -320,8 +325,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.profile_manager_menu) toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
if (ProfileManager.getFirstProfile() == null) if (!ProfileManager.isNotEmpty()) DataStore.profileId = ProfileManager.createProfile().id
DataStore.profileId = ProfileManager.createProfile().id
val profilesList = view.findViewById<RecyclerView>(R.id.list) val profilesList = view.findViewById<RecyclerView>(R.id.list)
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
profilesList.layoutManager = layoutManager profilesList.layoutManager = layoutManager
...@@ -367,20 +371,22 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -367,20 +371,22 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
R.id.action_import -> { R.id.action_import -> {
try { try {
val profiles = Profile.findAll(clipboard.primaryClip.getItemAt(0).text).toList() val profiles = Profile.findAll(clipboard.primaryClip!!.getItemAt(0).text).toList()
if (profiles.isNotEmpty()) { if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) } profiles.forEach { ProfileManager.createProfile(it) }
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_msg, Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_msg,
Snackbar.LENGTH_LONG).show() Snackbar.LENGTH_LONG).show()
return true return true
} }
} catch (_: IndexOutOfBoundsException) { } } catch (exc: Exception) {
exc.printStackTrace()
}
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err,
Snackbar.LENGTH_LONG).show() Snackbar.LENGTH_LONG).show()
true true
} }
R.id.action_manual_settings -> { R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile().id) startConfig(ProfileManager.createProfile())
true true
} }
R.id.action_export -> { R.id.action_export -> {
...@@ -395,8 +401,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -395,8 +401,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
} }
override fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { override fun onTrafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
if (profileId != -1) { // ignore resets from MainActivity if (profileId != -1L) { // ignore resets from MainActivity
if (bandwidthProfile != profileId) { if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile) onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId bandwidthProfile = profileId
...@@ -406,7 +412,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -406,7 +412,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
profilesAdapter.refreshId(profileId) profilesAdapter.refreshId(profileId)
} }
} }
fun onTrafficPersisted(profileId: Int) { fun onTrafficPersisted(profileId: Long) {
txTotal = 0 txTotal = 0
rxTotal = 0 rxTotal = 0
if (bandwidthProfile != profileId) { if (bandwidthProfile != profileId) {
......
...@@ -40,6 +40,7 @@ import com.github.shadowsocks.database.Profile ...@@ -40,6 +40,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.systemService import com.github.shadowsocks.utils.systemService
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture
import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic
import com.google.android.gms.vision.Frame import com.google.android.gms.vision.Frame
...@@ -52,8 +53,11 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba ...@@ -52,8 +53,11 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
private const val TAG = "ScannerActivity" private const val TAG = "ScannerActivity"
private const val REQUEST_IMPORT = 2 private const val REQUEST_IMPORT = 2
private const val REQUEST_IMPORT_OR_FINISH = 3 private const val REQUEST_IMPORT_OR_FINISH = 3
private const val REQUEST_GOOGLE_API = 4
} }
private lateinit var detector: BarcodeDetector
private fun navigateUp() { private fun navigateUp() {
val intent = parentActivityIntent val intent = parentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot) if (shouldUpRecreateTask(intent) || isTaskRoot)
...@@ -63,6 +67,23 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba ...@@ -63,6 +67,23 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
detector = BarcodeDetector.Builder(this)
.setBarcodeFormats(Barcode.QR_CODE)
.build()
if (!detector.isOperational) {
val availability = GoogleApiAvailability.getInstance()
val dialog = availability.getErrorDialog(this, availability.isGooglePlayServicesAvailable(this),
REQUEST_GOOGLE_API)
if (dialog == null) {
Toast.makeText(this, R.string.common_google_play_services_notification_ticker, Toast.LENGTH_SHORT)
.show()
finish()
} else {
dialog.setOnDismissListener { finish() }
dialog.show()
}
return
}
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan") if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
if (try { if (try {
systemService<CameraManager>().cameraIdList.isEmpty() systemService<CameraManager>().cameraIdList.isEmpty()
...@@ -79,7 +100,9 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba ...@@ -79,7 +100,9 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
toolbar.setNavigationOnClickListener { navigateUp() } toolbar.setNavigationOnClickListener { navigateUp() }
toolbar.inflateMenu(R.menu.scanner_menu) toolbar.inflateMenu(R.menu.scanner_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
(supportFragmentManager.findFragmentById(R.id.barcode) as BarcodeCapture).setRetrieval(this) val capture = supportFragmentManager.findFragmentById(R.id.barcode) as BarcodeCapture
capture.setCustomDetector(detector)
capture.setRetrieval(this)
} }
override fun onRetrieved(barcode: Barcode) = runOnUiThread { override fun onRetrieved(barcode: Barcode) = runOnUiThread {
...@@ -112,25 +135,20 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba ...@@ -112,25 +135,20 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
when (requestCode) { when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) { REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
var success = false var success = false
val detector = BarcodeDetector.Builder(this) var list = listOfNotNull(data?.data)
.setBarcodeFormats(Barcode.QR_CODE) val clipData = data?.clipData
.build() if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri }
if (detector.isOperational) { val resolver = contentResolver
var list = listOfNotNull(data?.data) for (uri in list) try {
val clipData = data?.clipData val barcodes = detector.detect(Frame.Builder()
if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri } .setBitmap(BitmapFactory.decodeStream(resolver.openInputStream(uri))).build())
val resolver = contentResolver for (i in 0 until barcodes.size()) Profile.findAll(barcodes.valueAt(i).rawValue).forEach {
for (uri in list) try { ProfileManager.createProfile(it)
val barcodes = detector.detect(Frame.Builder() success = true
.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.") } catch (e: Exception) {
app.track(e)
}
Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err, Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err,
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT).show()
navigateUp() navigateUp()
......
...@@ -38,7 +38,7 @@ open class ToolbarFragment : Fragment() { ...@@ -38,7 +38,7 @@ open class ToolbarFragment : Fragment() {
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) { } open fun onTrafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
open fun onBackPressed(): Boolean = false open fun onBackPressed(): Boolean = false
} }
...@@ -38,13 +38,13 @@ class AclSyncJob(private val route: String) : Job() { ...@@ -38,13 +38,13 @@ class AclSyncJob(private val route: String) : Job() {
return when (parts[0]) { return when (parts[0]) {
TAG -> AclSyncJob(parts[1]) TAG -> AclSyncJob(parts[1])
else -> { else -> {
Log.w(TAG, "Unknown job tag: " + tag) Log.w(TAG, "Unknown job tag: $tag")
null null
} }
} }
} }
fun schedule(route: String): Int = JobRequest.Builder(TAG + ':' + route) fun schedule(route: String): Int = JobRequest.Builder("$TAG:$route")
.setExecutionWindow(TimeUnit.SECONDS.toMillis(10), TimeUnit.DAYS.toMillis(28)) .setExecutionWindow(TimeUnit.SECONDS.toMillis(10), TimeUnit.DAYS.toMillis(28))
.setRequirementsEnforced(true) .setRequirementsEnforced(true)
.setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED)
......
...@@ -22,6 +22,7 @@ package com.github.shadowsocks.acl ...@@ -22,6 +22,7 @@ package com.github.shadowsocks.acl
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
...@@ -438,7 +439,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -438,7 +439,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
} }
R.id.action_import -> { R.id.action_import -> {
try { try {
adapter.addToProxy(clipboard.primaryClip.getItemAt(0).text.toString()) != null check(adapter.addToProxy(clipboard.primaryClip!!.getItemAt(0).text.toString()) != null)
} catch (exc: Exception) { } catch (exc: Exception) {
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err,
Snackbar.LENGTH_LONG).show() Snackbar.LENGTH_LONG).show()
...@@ -466,10 +467,16 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -466,10 +467,16 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
val activity = requireActivity() val activity = requireActivity()
val window = activity.window val window = activity.window
// In the end material_grey_100 is used for background, see AppCompatDrawableManager (very complicated) // In the end material_grey_100 is used for background, see AppCompatDrawableManager (very complicated)
if (Build.VERSION.SDK_INT >= 23) { // for dark mode, it's roughly 850? (#303030)
window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_300) window.statusBarColor = ContextCompat.getColor(activity, when {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES ->
} else window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_600) R.color.md_black_1000
Build.VERSION.SDK_INT >= 23 -> {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
R.color.material_grey_300
}
else -> R.color.material_grey_600
})
activity.menuInflater.inflate(R.menu.custom_rules_selection, menu) activity.menuInflater.inflate(R.menu.custom_rules_selection, menu)
toolbar.touchscreenBlocksFocus = true toolbar.touchscreenBlocksFocus = true
return true return true
......
...@@ -49,7 +49,8 @@ import okhttp3.OkHttpClient ...@@ -49,7 +49,8 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.net.UnknownHostException import java.io.IOException
import java.net.*
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
...@@ -75,7 +76,7 @@ object BaseService { ...@@ -75,7 +76,7 @@ object BaseService {
@Volatile var state = STOPPED @Volatile var state = STOPPED
@Volatile var plugin = PluginOptions() @Volatile var plugin = PluginOptions()
@Volatile var pluginPath: String? = null @Volatile var pluginPath: String? = null
var sslocalProcess: GuardedProcess? = null val processes = GuardedProcessPool()
var timer: Timer? = null var timer: Timer? = null
var trafficMonitorThread: TrafficMonitorThread? = null var trafficMonitorThread: TrafficMonitorThread? = null
...@@ -105,6 +106,7 @@ object BaseService { ...@@ -105,6 +106,7 @@ object BaseService {
val t = Timer(true) val t = Timer(true)
t.schedule(object : TimerTask() { t.schedule(object : TimerTask() {
override fun run() { override fun run() {
val profile = profile ?: return
if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post { if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post {
if (bandwidthListeners.isNotEmpty()) { if (bandwidthListeners.isNotEmpty()) {
val txRate = TrafficMonitor.txRate val txRate = TrafficMonitor.txRate
...@@ -115,8 +117,11 @@ object BaseService { ...@@ -115,8 +117,11 @@ object BaseService {
for (i in 0 until n) try { for (i in 0 until n) try {
val item = callbacks.getBroadcastItem(i) val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder())) if (bandwidthListeners.contains(item.asBinder()))
item.trafficUpdated(profile!!.id, txRate, rxRate, txTotal, rxTotal) item.trafficUpdated(profile.id, txRate, rxRate, txTotal, rxTotal)
} catch (_: Exception) { } // ignore } catch (e: Exception) {
e.printStackTrace()
app.track(e)
}
callbacks.finishBroadcast() callbacks.finishBroadcast()
} }
} }
...@@ -145,22 +150,35 @@ object BaseService { ...@@ -145,22 +150,35 @@ object BaseService {
} }
internal fun updateTrafficTotal(tx: Long, rx: Long) { internal fun updateTrafficTotal(tx: Long, rx: Long) {
// this.profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition) try {
val profile = ProfileManager.getProfile((profile ?: return).id) ?: return // this.profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
profile.tx += tx val profile = ProfileManager.getProfile((profile ?: return).id) ?: return
profile.rx += rx profile.tx += tx
ProfileManager.updateProfile(profile) profile.rx += rx
app.handler.post { ProfileManager.updateProfile(profile)
if (bandwidthListeners.isNotEmpty()) { app.handler.post {
val n = callbacks.beginBroadcast() if (bandwidthListeners.isNotEmpty()) {
for (i in 0 until n) { val n = callbacks.beginBroadcast()
try { for (i in 0 until n) {
val item = callbacks.getBroadcastItem(i) try {
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(profile.id) val item = callbacks.getBroadcastItem(i)
} catch (_: Exception) { } // ignore if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(profile.id)
} catch (e: Exception) {
e.printStackTrace()
app.track(e)
}
}
callbacks.finishBroadcast()
} }
callbacks.finishBroadcast()
} }
} catch (e: IOException) {
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
val profile = DirectBoot.getDeviceProfile()!!
profile.tx += tx
profile.rx += rx
profile.dirty = true
DirectBoot.update(profile)
DirectBoot.listenForUnlock()
} }
} }
...@@ -200,7 +218,10 @@ object BaseService { ...@@ -200,7 +218,10 @@ object BaseService {
val n = callbacks.beginBroadcast() val n = callbacks.beginBroadcast()
for (i in 0 until n) try { for (i in 0 until n) try {
callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg) callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg)
} catch (_: Exception) { } // ignore } catch (e: Exception) {
e.printStackTrace()
app.track(e)
}
callbacks.finishBroadcast() callbacks.finishBroadcast()
} }
state = s state = s
...@@ -235,6 +256,7 @@ object BaseService { ...@@ -235,6 +256,7 @@ object BaseService {
fun startNativeProcesses() { fun startNativeProcesses() {
val data = data val data = data
val profile = data.profile!!
val cmd = buildAdditionalArguments(arrayListOf( val cmd = buildAdditionalArguments(arrayListOf(
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath, File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
"-u", "-u",
...@@ -249,9 +271,11 @@ object BaseService { ...@@ -249,9 +271,11 @@ object BaseService {
cmd += acl.absolutePath cmd += acl.absolutePath
} }
if (profile.udpdns) cmd += "-D"
if (TcpFastOpen.sendEnabled) cmd += "--fast-open" if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
data.sslocalProcess = GuardedProcess(cmd).start() data.processes.start(cmd)
} }
fun createNotification(profileName: String): ServiceNotification fun createNotification(profileName: String): ServiceNotification
...@@ -262,11 +286,7 @@ object BaseService { ...@@ -262,11 +286,7 @@ object BaseService {
else startService(Intent(this, javaClass)) else startService(Intent(this, javaClass))
} }
fun killProcesses() { fun killProcesses() = data.processes.killAll()
val data = data
data.sslocalProcess?.destroy()
data.sslocalProcess = null
}
fun stopRunner(stopService: Boolean, msg: String? = null) { fun stopRunner(stopService: Boolean, msg: String? = null) {
// channge the state // channge the state
...@@ -341,14 +361,10 @@ object BaseService { ...@@ -341,14 +361,10 @@ object BaseService {
data.changeState(CONNECTING) data.changeState(CONNECTING)
thread { thread("$tag-Connecting") {
try { try {
if (profile.host == "198.199.101.152") { if (profile.host == "198.199.101.152") {
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
.dns {
listOf((Dns.resolve(it, false) ?: throw UnknownHostException())
.parseNumericAddress())
}
.connectTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
...@@ -383,7 +399,7 @@ object BaseService { ...@@ -383,7 +399,7 @@ object BaseService {
killProcesses() killProcesses()
if (!profile.host.isNumericAddress()) if (!profile.host.isNumericAddress())
profile.host = Dns.resolve(profile.host, true) ?: throw UnknownHostException() profile.host = InetAddress.getByName(profile.host).hostAddress ?: throw UnknownHostException()
startNativeProcesses() startNativeProcesses()
......
...@@ -28,35 +28,40 @@ import com.github.shadowsocks.BuildConfig ...@@ -28,35 +28,40 @@ import com.github.shadowsocks.BuildConfig
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.utils.Commandline import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.thread import com.github.shadowsocks.utils.thread
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.Semaphore import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.atomic.AtomicReference
class GuardedProcess(private val cmd: List<String>) { class GuardedProcessPool {
companion object { companion object {
private const val TAG = "GuardedProcess" private const val TAG = "GuardedProcessPool"
private val dummy = IOException()
} }
private lateinit var guardThread: Thread private inner class Guard(private val cmd: List<String>, private val onRestartCallback: (() -> Unit)?) {
@Volatile val cmdName = File(cmd.first()).nameWithoutExtension
private var isDestroyed = false val excQueue = ArrayBlockingQueue<IOException>(1) // ArrayBlockingQueue doesn't want null
@Volatile private var pushed = false
private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = thread { private fun streamLogger(input: InputStream, logger: (String, String) -> Int) =
try { thread("StreamLogger-$cmdName") {
input.bufferedReader().useLines { it.forEach { logger(TAG, it) } } try {
} catch (_: IOException) { } // ignore input.bufferedReader().useLines { it.forEach { logger(TAG, it) } }
} } catch (_: IOException) { } // ignore
}
private fun pushException(ioException: IOException?) {
if (pushed) return
excQueue.put(ioException ?: dummy)
pushed = true
}
fun start(onRestartCallback: (() -> Unit)? = null): GuardedProcess { fun looper(host: HashSet<Thread>) {
val semaphore = Semaphore(1) var process: Process? = null
semaphore.acquire()
var ioException: IOException? = null
guardThread = thread(name = "GuardThread-" + cmd.first()) {
try { try {
var callback: (() -> Unit)? = null var callback: (() -> Unit)? = null
while (!isDestroyed) { while (guardThreads.get() === host) {
if (BuildConfig.DEBUG) Log.d(TAG, "start process: " + Commandline.toString(cmd)) if (BuildConfig.DEBUG) Log.d(TAG, "start process: " + Commandline.toString(cmd))
val startTime = SystemClock.elapsedRealtime() val startTime = SystemClock.elapsedRealtime()
...@@ -70,44 +75,57 @@ class GuardedProcess(private val cmd: List<String>) { ...@@ -70,44 +75,57 @@ class GuardedProcess(private val cmd: List<String>) {
if (callback == null) callback = onRestartCallback else callback() if (callback == null) callback = onRestartCallback else callback()
semaphore.release() pushException(null)
process.waitFor() process.waitFor()
synchronized(this) { if (SystemClock.elapsedRealtime() - startTime < 1000) {
if (SystemClock.elapsedRealtime() - startTime < 1000) { Log.w(TAG, "process exit too fast, stop guard: $cmdName")
Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd))
isDestroyed = true
}
} }
} }
} catch (_: InterruptedException) { } catch (_: InterruptedException) {
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd)) if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: $cmdName")
destroyProcess()
} catch (e: IOException) { } catch (e: IOException) {
ioException = e pushException(e)
} finally { } finally {
semaphore.release() if (process != null) {
if (Build.VERSION.SDK_INT < 24) @Suppress("DEPRECATION") {
JniHelper.sigtermCompat(process)
JniHelper.waitForCompat(process, 500)
}
process.destroy()
process.waitFor() // ensure the process is destroyed
}
pushException(null)
} }
} }
semaphore.acquire()
if (ioException != null) throw ioException!!
return this
} }
fun destroy() { /**
isDestroyed = true * This is an indication of which thread pool is being active.
guardThread.interrupt() * Reading/writing this collection still needs an additional lock to prevent concurrent modification.
destroyProcess() */
try { private val guardThreads = AtomicReference<HashSet<Thread>>(HashSet())
guardThread.join()
} catch (_: InterruptedException) { } fun start(cmd: List<String>, onRestartCallback: (() -> Unit)? = null): GuardedProcessPool {
val guard = Guard(cmd, onRestartCallback)
val guardThreads = guardThreads.get()
synchronized(guardThreads) {
guardThreads.add(thread("GuardThread-${guard.cmdName}") {
guard.looper(guardThreads)
})
}
val ioException = guard.excQueue.take()
if (ioException !== dummy) throw ioException
return this
} }
private fun destroyProcess() { fun killAll() {
if (Build.VERSION.SDK_INT < 24) @Suppress("DEPRECATION") { val guardThreads = guardThreads.getAndSet(HashSet())
JniHelper.sigtermCompat(process) synchronized(guardThreads) {
JniHelper.waitForCompat(process, 500) guardThreads.forEach { it.interrupt() }
try {
guardThreads.forEach { it.join() }
} catch (_: InterruptedException) { }
} }
process.destroy()
} }
} }
...@@ -28,19 +28,9 @@ import org.json.JSONArray ...@@ -28,19 +28,9 @@ import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.net.Inet6Address import java.net.Inet6Address
import java.util.*
/**
* This object also uses WeakMap to simulate the effects of multi-inheritance, but more lightweight.
*/
object LocalDnsService { object LocalDnsService {
interface Interface : BaseService.Interface { interface Interface : BaseService.Interface {
var overtureProcess: GuardedProcess?
get() = overtureProcesses[this]
set(value) {
if (value == null) overtureProcesses.remove(this) else overtureProcesses[this] = value
}
override fun startNativeProcesses() { override fun startNativeProcesses() {
super.startNativeProcesses() super.startNativeProcesses()
val data = data val data = data
...@@ -72,12 +62,11 @@ object LocalDnsService { ...@@ -72,12 +62,11 @@ object LocalDnsService {
.put("MinimumTTL", 120) .put("MinimumTTL", 120)
.put("CacheSize", 4096) .put("CacheSize", 4096)
val remoteDns = JSONArray(profile.remoteDns.split(",") val remoteDns = JSONArray(profile.remoteDns.split(",")
.mapIndexed { i, dns -> makeDns("UserDef-" + i, .mapIndexed { i, dns -> makeDns("UserDef-$i", dns.trim() + ":53", 12) })
dns.trim() + ":53", 9) })
val localDns = JSONArray(arrayOf( val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "119.29.29.29:53", 3, false), makeDns("Primary-1", "208.67.222.222:443", 9, false),
makeDns("Primary-2", "114.114.114.114:53", 3, false), makeDns("Primary-2", "119.29.29.29:53", 9, false),
makeDns("Primary-3", "208.67.222.222:443", 3, false) makeDns("Primary-3", "114.114.114.114:53", 9, false)
)) ))
when (profile.route) { when (profile.route) {
...@@ -85,7 +74,6 @@ object LocalDnsService { ...@@ -85,7 +74,6 @@ object LocalDnsService {
.put("PrimaryDNS", localDns) .put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns) .put("AlternativeDNS", remoteDns)
.put("IPNetworkFile", "china_ip_list.txt") .put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", data.aclFile!!.absolutePath)
Acl.CHINALIST -> config Acl.CHINALIST -> config
.put("PrimaryDNS", localDns) .put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns) .put("AlternativeDNS", remoteDns)
...@@ -98,18 +86,10 @@ object LocalDnsService { ...@@ -98,18 +86,10 @@ object LocalDnsService {
return file return file
} }
if (!profile.udpdns) overtureProcess = GuardedProcess(buildAdditionalArguments(arrayListOf( if (!profile.udpdns) data.processes.start(buildAdditionalArguments(arrayListOf(
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath, File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath,
"-c", buildOvertureConfig("overture.conf") "-c", buildOvertureConfig("overture.conf")
))).start() )))
}
override fun killProcesses() {
super.killProcesses()
overtureProcess?.destroy()
overtureProcess = null
} }
} }
private val overtureProcesses = WeakHashMap<Interface, GuardedProcess>()
} }
...@@ -28,7 +28,7 @@ import com.github.shadowsocks.App.Companion.app ...@@ -28,7 +28,7 @@ import com.github.shadowsocks.App.Companion.app
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
abstract class LocalSocketListener(protected val tag: String) : Thread() { abstract class LocalSocketListener(protected val tag: String) : Thread(tag) {
init { init {
setUncaughtExceptionHandler(app::track) setUncaughtExceptionHandler(app::track)
} }
......
...@@ -31,6 +31,7 @@ import android.os.Build ...@@ -31,6 +31,7 @@ import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.text.format.Formatter
import com.github.shadowsocks.MainActivity import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
...@@ -54,16 +55,16 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam ...@@ -54,16 +55,16 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
private val callback by lazy { private val callback by lazy {
object : IShadowsocksServiceCallback.Stub() { object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
service as Context service as Context
val txr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(txRate)) val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, txRate))
val rxr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(rxRate)) val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, rxRate))
builder.setContentText("$txr↑\t$rxr↓") builder.setContentText("$txr↑\t$rxr↓")
style.bigText(service.getString(R.string.stat_summary).format(Locale.ENGLISH, txr, rxr, style.bigText(service.getString(R.string.stat_summary).format(Locale.ENGLISH, txr, rxr,
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal))) Formatter.formatFileSize(service, txTotal), Formatter.formatFileSize(service, rxTotal)))
show() show()
} }
override fun trafficPersisted(profileId: Int) { } override fun trafficPersisted(profileId: Long) { }
} }
} }
private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) } private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
......
...@@ -64,8 +64,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface { ...@@ -64,8 +64,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
tile.label = label ?: getString(R.string.app_name) tile.label = label ?: getString(R.string.app_name)
tile.updateTile() tile.updateTile()
} }
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { } override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
override fun trafficPersisted(profileId: Int) { } override fun trafficPersisted(profileId: Long) { }
} }
} }
...@@ -82,7 +82,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface { ...@@ -82,7 +82,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
} }
override fun onClick() { override fun onClick() {
if (isLocked && !DataStore.directBootAware) unlockAndRun(this::toggle) else toggle() if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()
} }
private fun toggle() { private fun toggle() {
......
...@@ -21,9 +21,6 @@ ...@@ -21,9 +21,6 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import android.os.SystemClock import android.os.SystemClock
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import java.text.DecimalFormat
object TrafficMonitor { object TrafficMonitor {
// Bytes per second // Bytes per second
...@@ -41,19 +38,6 @@ object TrafficMonitor { ...@@ -41,19 +38,6 @@ object TrafficMonitor {
@Volatile @Volatile
private var dirty = true private var dirty = true
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 = size.toDouble()
var i = -1
while (n >= 999.5) {
n /= 1024
++i
}
return if (i < 0) "$size ${app.resources.getQuantityString(R.plurals.bytes, size.toInt())}"
else "${numberFormat.format(n)} ${units[i]}"
}
fun updateRate(): Boolean { fun updateRate(): Boolean {
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
val delta = now - timestampLast val delta = now - timestampLast
......
...@@ -40,18 +40,14 @@ class TransproxyService : Service(), LocalDnsService.Interface { ...@@ -40,18 +40,14 @@ class TransproxyService : Service(), LocalDnsService.Interface {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId) super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
private var sstunnelProcess: GuardedProcess? = null
private var redsocksProcess: GuardedProcess? = null
private fun startDNSTunnel() { private fun startDNSTunnel() {
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath, data.processes.start(listOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
"-t", "10", "-t", "10",
"-b", "127.0.0.1", "-b", "127.0.0.1",
"-u", "-u",
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture "-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
"-L", data.profile!!.remoteDns.split(",").first().trim() + ":53", "-L", data.profile!!.remoteDns.split(",").first().trim() + ":53",
"-c", data.shadowsocksConfigFile!!.absolutePath) // config is already built by BaseService.Interface "-c", data.shadowsocksConfigFile!!.absolutePath)) // config is already built by BaseService.Interface
sstunnelProcess = GuardedProcess(cmd).start()
} }
private fun startRedsocksDaemon() { private fun startRedsocksDaemon() {
...@@ -70,10 +66,8 @@ redsocks { ...@@ -70,10 +66,8 @@ redsocks {
type = socks5; type = socks5;
} }
""") """)
redsocksProcess = GuardedProcess(arrayListOf( data.processes.start(listOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
"-c", "redsocks.conf")
).start()
} }
override fun startNativeProcesses() { override fun startNativeProcesses() {
...@@ -81,12 +75,4 @@ redsocks { ...@@ -81,12 +75,4 @@ redsocks {
super.startNativeProcesses() super.startNativeProcesses()
if (data.profile!!.udpdns) startDNSTunnel() if (data.profile!!.udpdns) startDNSTunnel()
} }
override fun killProcesses() {
super.killProcesses()
sstunnelProcess?.destroy()
sstunnelProcess = null
redsocksProcess?.destroy()
redsocksProcess = null
}
} }
...@@ -28,7 +28,6 @@ import android.net.* ...@@ -28,7 +28,6 @@ import android.net.*
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.support.v4.os.BuildCompat
import android.util.Log import android.util.Log
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
...@@ -50,13 +49,14 @@ import android.net.VpnService as BaseVpnService ...@@ -50,13 +49,14 @@ import android.net.VpnService as BaseVpnService
class VpnService : BaseVpnService(), LocalDnsService.Interface { class VpnService : BaseVpnService(), LocalDnsService.Interface {
companion object { companion object {
private const val VPN_MTU = 1500 private const val VPN_MTU = 1500
private const val PRIVATE_VLAN = "26.26.26.%s" private const val PRIVATE_VLAN = "172.19.0.%s"
private const val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s" private const val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$") private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$")
/** /**
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
* *
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
* satisfies default network capabilities but only THE default network. Unfortunately we need to have * satisfies default network capabilities but only THE default network. Unfortunately we need to have
...@@ -77,13 +77,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -77,13 +77,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
var success = false var success = false
try { try {
socket.inputStream.read() socket.inputStream.read()
val fds = socket.ancillaryFileDescriptors val fd = socket.ancillaryFileDescriptors!!.single()!!
if (fds.isEmpty()) return val fdInt = getInt.invoke(fd) as Int
val fd = getInt.invoke(fds.first()) as Int try {
success = protect(fd) val network = underlyingNetwork
JniHelper.close(fd) // Trick to close file decriptor success = if (network != null && Build.VERSION.SDK_INT >= 23) {
network.bindSocket(fd)
true
} else protect(fdInt)
} catch (e: Exception) {
Log.e(tag, "Error when protect socket", e)
app.track(e)
} finally {
JniHelper.close(fdInt) // Trick to close file decriptor
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "Error when protect socket", e) Log.e(tag, "Error when receiving ancillary fd", e)
app.track(e) app.track(e)
} }
try { try {
...@@ -106,10 +115,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -106,10 +115,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var conn: ParcelFileDescriptor? = null private var conn: ParcelFileDescriptor? = null
private var worker: ProtectWorker? = null private var worker: ProtectWorker? = null
private var tun2socksProcess: GuardedProcess? = null private var underlyingNetwork: Network? = null
@TargetApi(28)
set(value) {
setUnderlyingNetworks(if (value == null) null else arrayOf(value))
field = value
}
private val connectivity by lazy { systemService<ConnectivityManager>() } private val connectivity by lazy { systemService<ConnectivityManager>() }
@TargetApi(Build.VERSION_CODES.P) @TargetApi(28)
private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() { private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
setUnderlyingNetworks(arrayOf(network)) setUnderlyingNetworks(arrayOf(network))
...@@ -132,15 +146,13 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -132,15 +146,13 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun onRevoke() = stopRunner(true) override fun onRevoke() = stopRunner(true)
override fun killProcesses() { override fun killProcesses() {
if (BuildCompat.isAtLeastP() && listeningForDefaultNetwork) { if (listeningForDefaultNetwork) {
connectivity.unregisterNetworkCallback(defaultNetworkCallback) connectivity.unregisterNetworkCallback(defaultNetworkCallback)
listeningForDefaultNetwork = false listeningForDefaultNetwork = false
} }
worker?.stopThread() worker?.stopThread()
worker = null worker = null
super.killProcesses() super.killProcesses()
tun2socksProcess?.destroy()
tun2socksProcess = null
conn?.close() conn?.close()
conn = null conn = null
} }
...@@ -217,8 +229,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -217,8 +229,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
this.conn = conn this.conn = conn
val fd = conn.fd val fd = conn.fd
// We only need to update since Android P: https://android.googlesource.com/platform/frameworks/base/+/72f9c42b9e59761a28d6b32c42f65de57c98daed if (Build.VERSION.SDK_INT >= 28) {
if (BuildCompat.isAtLeastP()) {
// we want REQUEST here instead of LISTEN // we want REQUEST here instead of LISTEN
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
listeningForDefaultNetwork = true listeningForDefaultNetwork = true
...@@ -241,7 +252,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -241,7 +252,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
cmd += "--dnsgw" cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}" cmd += "127.0.0.1:${DataStore.portLocalDns}"
} }
tun2socksProcess = GuardedProcess(cmd).start { sendFd(fd) } data.processes.start(cmd) { sendFd(fd) }
return fd return fd
} }
......
...@@ -20,37 +20,54 @@ ...@@ -20,37 +20,54 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import com.j256.ormlite.field.DataType import android.arch.persistence.room.*
import com.j256.ormlite.field.DatabaseField
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
@Entity
class KeyValuePair() { class KeyValuePair() {
companion object { companion object {
const val TYPE_UNINITIALIZED = 0 const val TYPE_UNINITIALIZED = 0
const val TYPE_BOOLEAN = 1 const val TYPE_BOOLEAN = 1
const val TYPE_FLOAT = 2 const val TYPE_FLOAT = 2
@Deprecated("Use TYPE_LONG.")
const val TYPE_INT = 3 const val TYPE_INT = 3
const val TYPE_LONG = 4 const val TYPE_LONG = 4
const val TYPE_STRING = 5 const val TYPE_STRING = 5
const val TYPE_STRING_SET = 6 const val TYPE_STRING_SET = 6
} }
@DatabaseField(id = true) @android.arch.persistence.room.Dao
var key: String? = "" interface Dao {
@DatabaseField @Query("SELECT * FROM `KeyValuePair` WHERE `key` = :key")
operator fun get(key: String): KeyValuePair?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun put(value: KeyValuePair): Long
@Query("DELETE FROM `KeyValuePair` WHERE `key` = :key")
fun delete(key: String): Int
}
@PrimaryKey
var key: String = ""
var valueType: Int = TYPE_UNINITIALIZED var valueType: Int = TYPE_UNINITIALIZED
@DatabaseField(dataType = DataType.BYTE_ARRAY)
var value: ByteArray = ByteArray(0) var value: ByteArray = ByteArray(0)
val boolean: Boolean? val boolean: Boolean?
get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null
val float: Float? val float: Float?
get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null
@Suppress("DEPRECATION")
@Deprecated("Use long.", ReplaceWith("long"))
val int: Int? val int: Int?
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
val long: Long? val long: Long? get() = when (valueType) {
get() = if (valueType == TYPE_LONG) ByteBuffer.wrap(value).long else null @Suppress("DEPRECATION")
TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
TYPE_LONG -> ByteBuffer.wrap(value).long
else -> null
}
val string: String? val string: String?
get() = if (valueType == TYPE_STRING) String(value) else null get() = if (valueType == TYPE_STRING) String(value) else null
val stringSet: Set<String>? val stringSet: Set<String>?
...@@ -65,7 +82,8 @@ class KeyValuePair() { ...@@ -65,7 +82,8 @@ class KeyValuePair() {
result result
} else null } else null
constructor(key: String?) : this() { @Ignore
constructor(key: String) : this() {
this.key = key this.key = key
} }
...@@ -80,6 +98,8 @@ class KeyValuePair() { ...@@ -80,6 +98,8 @@ class KeyValuePair() {
this.value = ByteBuffer.allocate(4).putFloat(value).array() this.value = ByteBuffer.allocate(4).putFloat(value).array()
return this return this
} }
@Suppress("DEPRECATION")
@Deprecated("Use long.")
fun put(value: Int): KeyValuePair { fun put(value: Int): KeyValuePair {
valueType = TYPE_INT valueType = TYPE_INT
this.value = ByteBuffer.allocate(4).putInt(value).array() this.value = ByteBuffer.allocate(4).putInt(value).array()
......
...@@ -20,137 +20,39 @@ ...@@ -20,137 +20,39 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.database.sqlite.SQLiteDatabase import android.arch.persistence.db.SupportSQLiteDatabase
import android.support.v7.preference.PreferenceManager import android.arch.persistence.room.Database
import android.text.TextUtils import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.j256.ormlite.android.AndroidDatabaseConnection
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
import com.j256.ormlite.table.TableUtils
object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) { @Database(entities = [Profile::class, KeyValuePair::class], version = 26)
@Suppress("UNCHECKED_CAST") abstract class PrivateDatabase : RoomDatabase() {
val profileDao: Dao<Profile, Int> by lazy { getDao(Profile::class.java) as Dao<Profile, Int> } companion object {
@Suppress("UNCHECKED_CAST") private val instance by lazy {
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> } Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
.addMigrations(
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) { Migration26
TableUtils.createTable(connectionSource, Profile::class.java) )
TableUtils.createTable(connectionSource, KeyValuePair::class.java) .fallbackToDestructiveMigration()
} .allowMainThreadQueries()
.build()
private fun recreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
TableUtils.dropTable<Profile, Int>(connectionSource, Profile::class.java, true)
TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true)
onCreate(database, connectionSource)
}
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?,
oldVersion: Int, newVersion: Int) {
if (oldVersion < 7) {
recreate(database, connectionSource)
return
} }
try { val profileDao get() = instance.profileDao()
if (oldVersion < 8) { val kvPairDao get() = instance.keyValuePairDao()
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;")
}
if (oldVersion < 9) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';")
} else if (oldVersion < 19) {
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'all' WHERE route IS NULL;")
}
if (oldVersion < 11) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;")
}
if (oldVersion < 12) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
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`;")
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
} else if (oldVersion < 13) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN tx LONG;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;")
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;")
}
if (oldVersion < 15) {
if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;")
var i = 0L
val apps by lazy { app.packageManager.getInstalledApplications(0) }
for (profile in profileDao.queryForAll()) {
if (oldVersion < 14) {
val uidSet = profile.individual.split('|').filter(TextUtils::isDigitsOnly)
.map(String::toInt).toSet()
profile.individual = apps.filter { uidSet.contains(it.uid) }
.joinToString("\n") { it.packageName }
}
profile.userOrder = i
profileDao.update(profile)
i += 1
}
}
if (oldVersion < 16) {
profileDao.executeRawNoArgs(
"UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'")
}
if (oldVersion < 21) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN remoteDns VARCHAR DEFAULT '8.8.8.8';")
}
if (oldVersion < 17) {
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN plugin VARCHAR;")
} else if (oldVersion < 22) {
// upgrade kcptun to SIP003 plugin
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
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, " +
"remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " +
"plugin) " +
"SELECT id, name, host, localPort, " +
"CASE WHEN kcp = 1 THEN kcpPort ELSE remotePort END, password, method, route, " +
"remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " +
"CASE WHEN kcp = 1 THEN 'kcptun ' || kcpcli ELSE NULL END FROM `tmp`;")
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
}
if (oldVersion < 23) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
profileDao.executeRawNoArgs("COMMIT;")
val old = PreferenceManager.getDefaultSharedPreferences(app)
PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0)))
PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false)))
}
if (oldVersion < 25) {
PublicDatabase.onUpgrade(database, 0, -1)
}
} catch (ex: Exception) {
app.track(ex)
recreate(database, connectionSource)
}
} }
abstract fun profileDao(): Profile.Dao
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { abstract fun keyValuePairDao(): KeyValuePair.Dao
val connection = AndroidDatabaseConnection(db, true)
connectionSource.saveSpecialConnection(connection) private object Migration26 : RecreateSchemaMigration(25, 26, "Profile",
recreate(db, connectionSource) "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `individual` TEXT NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT)",
connectionSource.clearSpecialConnection(connection) "`id`, `name`, `host`, `remotePort`, `password`, `method`, `route`, `remoteDns`, `proxyApps`, `bypass`, `udpdns`, `ipv6`, `individual`, `tx`, `rx`, `userOrder`, `plugin`") {
override fun migrate(database: SupportSQLiteDatabase) {
super.migrate(database)
PublicDatabase.Migration3.migrate(database)
}
} }
} }
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.arch.persistence.room.*
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
...@@ -27,12 +28,12 @@ import com.github.shadowsocks.plugin.PluginConfiguration ...@@ -27,12 +28,12 @@ import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort import com.github.shadowsocks.utils.parsePort
import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField
import java.io.Serializable import java.io.Serializable
import java.net.URI import java.net.URI
import java.net.URISyntaxException
import java.util.* import java.util.*
@Entity
class Profile : Serializable { class Profile : Serializable {
companion object { companion object {
private const val TAG = "ShadowParser" private const val TAG = "ShadowParser"
...@@ -43,99 +44,101 @@ class Profile : Serializable { ...@@ -43,99 +44,101 @@ class Profile : Serializable {
fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map { fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map {
val uri = Uri.parse(it.value) val uri = Uri.parse(it.value)
if (uri.userInfo == null) { try {
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING))) if (uri.userInfo == null) {
if (match != null) { val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
val profile = Profile() if (match != null) {
profile.method = match.groupValues[1].toLowerCase() val profile = Profile()
profile.password = match.groupValues[2] profile.method = match.groupValues[1].toLowerCase()
profile.host = match.groupValues[3] profile.password = match.groupValues[2]
profile.remotePort = match.groupValues[4].toInt() profile.host = match.groupValues[3]
profile.plugin = uri.getQueryParameter(Key.plugin) profile.remotePort = match.groupValues[4].toInt()
profile.name = uri.fragment profile.plugin = uri.getQueryParameter(Key.plugin)
profile profile.name = uri.fragment
profile
} else {
Log.e(TAG, "Unrecognized URI: ${it.value}")
null
}
} else { } else {
Log.e(TAG, "Unrecognized URI: ${it.value}") val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
null Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
} if (match != null) {
} else { val profile = Profile()
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo, profile.method = match.groupValues[1]
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE))) profile.password = match.groupValues[2]
if (match != null) { // bug in Android: https://code.google.com/p/android/issues/detail?id=192855
val profile = Profile() try {
profile.method = match.groupValues[1] val javaURI = URI(it.value)
profile.password = match.groupValues[2] profile.host = javaURI.host ?: ""
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855 if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
val javaURI = URI(it.value) profile.host = profile.host.substring(1, profile.host.length - 1)
profile.host = javaURI.host profile.remotePort = javaURI.port
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']') profile.plugin = uri.getQueryParameter(Key.plugin)
profile.host = profile.host.substring(1, profile.host.length - 1) profile.name = uri.fragment ?: ""
profile.remotePort = javaURI.port profile
profile.plugin = uri.getQueryParameter(Key.plugin) } catch (e: URISyntaxException) {
profile.name = uri.fragment ?: "" Log.e(TAG, "Invalid URI: ${it.value}")
profile null
} else { }
Log.e(TAG, "Unknown user info: ${it.value}") } else {
null Log.e(TAG, "Unknown user info: ${it.value}")
null
}
} }
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid base64 detected: ${it.value}")
null
} }
}.filterNotNull() }.filterNotNull()
} }
@DatabaseField(generatedId = true) @android.arch.persistence.room.Dao
var id: Int = 0 interface Dao {
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
operator fun get(id: Long): Profile?
@DatabaseField @Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
var name: String? = "" fun list(): List<Profile>
@DatabaseField @Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
var host: String = "198.199.101.152" fun nextOrder(): Long?
@DatabaseField @Query("SELECT 1 FROM `Profile` LIMIT 1")
var remotePort: Int = 8388 fun isNotEmpty(): Boolean
@DatabaseField @Insert
var password: String = "u1rRWTssNv0p" fun create(value: Profile): Long
@DatabaseField @Update
var method: String = "aes-256-cfb" fun update(value: Profile): Int
@DatabaseField @Query("DELETE FROM `Profile` WHERE `id` = :id")
var route: String = "all" fun delete(id: Long): Int
}
@DatabaseField @PrimaryKey(autoGenerate = true)
var id: Long = 0
var name: String? = ""
var host: String = "198.199.101.152"
var remotePort: Int = 8388
var password: String = "u1rRWTssNv0p"
var method: String = "aes-256-cfb"
var route: String = "all"
var remoteDns: String = "8.8.8.8" var remoteDns: String = "8.8.8.8"
@DatabaseField
var proxyApps: Boolean = false var proxyApps: Boolean = false
@DatabaseField
var bypass: Boolean = false var bypass: Boolean = false
@DatabaseField
var udpdns: Boolean = false var udpdns: Boolean = false
@DatabaseField
var ipv6: Boolean = true var ipv6: Boolean = true
@DatabaseField(dataType = DataType.LONG_STRING)
var individual: String = "" var individual: String = ""
@DatabaseField
var tx: Long = 0 var tx: Long = 0
@DatabaseField
var rx: Long = 0 var rx: Long = 0
@DatabaseField
val date: Date = Date()
@DatabaseField
var userOrder: Long = 0 var userOrder: Long = 0
@DatabaseField
var plugin: String? = null var plugin: String? = null
@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort) val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!! val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
......
...@@ -20,11 +20,13 @@ ...@@ -20,11 +20,13 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.database.sqlite.SQLiteCantOpenDatabaseException
import android.util.Log import android.util.Log
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.ProfilesFragment import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import java.io.IOException
import java.sql.SQLException import java.sql.SQLException
/** /**
...@@ -48,10 +50,8 @@ object ProfileManager { ...@@ -48,10 +50,8 @@ object ProfileManager {
profile.individual = oldProfile.individual profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns profile.udpdns = oldProfile.udpdns
} }
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder() profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult profile.id = PrivateDatabase.profileDao.create(profile)
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
PrivateDatabase.profileDao.createOrUpdate(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile) ProfilesFragment.instance?.profilesAdapter?.add(profile)
return profile return profile
} }
...@@ -60,34 +60,40 @@ object ProfileManager { ...@@ -60,34 +60,40 @@ object ProfileManager {
* Note: It's caller's responsibility to update DirectBoot profile if necessary. * Note: It's caller's responsibility to update DirectBoot profile if necessary.
*/ */
@Throws(SQLException::class) @Throws(SQLException::class)
fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.update(profile) fun updateProfile(profile: Profile) = check(PrivateDatabase.profileDao.update(profile) == 1)
fun getProfile(id: Int): Profile? = try { @Throws(IOException::class)
PrivateDatabase.profileDao.queryForId(id) fun getProfile(id: Long): Profile? = try {
PrivateDatabase.profileDao[id]
} catch (ex: SQLException) { } catch (ex: SQLException) {
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "getProfile", ex) Log.e(TAG, "getProfile", ex)
app.track(ex) app.track(ex)
null null
} }
@Throws(SQLException::class) @Throws(SQLException::class)
fun delProfile(id: Int) { fun delProfile(id: Long) {
PrivateDatabase.profileDao.deleteById(id) check(PrivateDatabase.profileDao.delete(id) == 1)
ProfilesFragment.instance?.profilesAdapter?.removeId(id) ProfilesFragment.instance?.profilesAdapter?.removeId(id)
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean() if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
} }
fun getFirstProfile(): Profile? = try { @Throws(IOException::class)
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull() fun isNotEmpty(): Boolean = try {
PrivateDatabase.profileDao.isNotEmpty()
} catch (ex: SQLException) { } catch (ex: SQLException) {
Log.e(TAG, "getFirstProfile", ex) if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "isNotEmpty", ex)
app.track(ex) app.track(ex)
null false
} }
@Throws(IOException::class)
fun getAllProfiles(): List<Profile>? = try { fun getAllProfiles(): List<Profile>? = try {
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare()) PrivateDatabase.profileDao.list()
} catch (ex: SQLException) { } catch (ex: SQLException) {
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "getAllProfiles", ex) Log.e(TAG, "getAllProfiles", ex)
app.track(ex) app.track(ex)
null null
......
...@@ -20,42 +20,31 @@ ...@@ -20,42 +20,31 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.database.sqlite.SQLiteDatabase import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.j256.ormlite.android.AndroidDatabaseConnection
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
import com.j256.ormlite.table.TableUtils
object PublicDatabase : OrmLiteSqliteOpenHelper(app.deviceContext, Key.DB_PUBLIC, null, 2) { @Database(entities = [(KeyValuePair::class)], version = 3)
@Suppress("UNCHECKED_CAST") abstract class PublicDatabase : RoomDatabase() {
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> } companion object {
private val instance by lazy {
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) { Room.databaseBuilder(app.deviceContext, PublicDatabase::class.java, Key.DB_PUBLIC)
TableUtils.createTable(connectionSource, KeyValuePair::class.java) .allowMainThreadQueries()
} .addMigrations(
Migration3
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?, )
oldVersion: Int, newVersion: Int) { .fallbackToDestructiveMigration()
if (oldVersion < 1) { .build()
PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key",
Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query()
.forEach { kvPairDao.createOrUpdate(it) }
} }
if (oldVersion < 2) { val kvPairDao get() = instance.keyValuePairDao()
kvPairDao.createOrUpdate(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString()))
}
} }
abstract fun keyValuePairDao(): KeyValuePair.Dao
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair",
val connection = AndroidDatabaseConnection(db, true) "(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
connectionSource.saveSpecialConnection(connection) "`key`, `valueType`, `value`")
TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true)
onCreate(db, connectionSource)
connectionSource.clearSpecialConnection(connection)
}
} }
/******************************************************************************* /*******************************************************************************
* * * *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> * * Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> * * Copyright (C) 2018 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* * * *
* This program is free software: you can redistribute it and/or modify * * 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 * * it under the terms of the GNU General Public License as published by *
...@@ -18,55 +18,18 @@ ...@@ -18,55 +18,18 @@
* * * *
*******************************************************************************/ *******************************************************************************/
package com.github.shadowsocks.bg package com.github.shadowsocks.database.migration
import android.util.Log import android.arch.persistence.db.SupportSQLiteDatabase
import com.github.shadowsocks.App.Companion.app import android.arch.persistence.room.migration.Migration
import com.github.shadowsocks.BuildConfig
import org.xbill.DNS.*
import java.io.IOException
import java.net.*
object Dns { open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
private const val TAG = "Dns" private val schema: String, private val keys: String) :
Migration(oldVersion, newVersion) {
private val hasIPv6Support get() = try { override fun migrate(database: SupportSQLiteDatabase) {
val result = NetworkInterface.getNetworkInterfaces().asSequence().flatMap { it.inetAddresses.asSequence() } database.execSQL("CREATE TABLE `tmp` $schema")
.any { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`")
if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected") database.execSQL("DROP TABLE `$table`")
result database.execSQL("ALTER TABLE `tmp` RENAME TO `$table`")
} catch (ex: SocketException) {
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
app.track(ex)
false
}
private fun resolve(host: String, addrType: Int): String? {
try {
val lookup = Lookup(host, addrType)
val resolver = SimpleResolver("208.67.220.220")
resolver.setTCP(true)
resolver.setPort(443)
resolver.setTimeout(5)
lookup.setResolver(resolver)
val records = (lookup.run() ?: return null).toMutableList()
records.shuffle()
for (r in records) {
when (addrType) {
Type.A -> return (r as ARecord).address.hostAddress
Type.AAAA -> return (r as AAAARecord).address.hostAddress
}
}
} catch (_: IOException) { }
return null
}
private fun resolve(host: String): String? = try {
InetAddress.getByName(host).hostAddress
} catch (_: UnknownHostException) {
null
} }
fun resolve(host: String, enableIPv6: Boolean): String? =
(if (enableIPv6 && hasIPv6Support) resolve(host, Type.AAAA) else null)
?: resolve(host, Type.A)
?: resolve(host)
} }
...@@ -25,7 +25,7 @@ import android.os.Bundle ...@@ -25,7 +25,7 @@ import android.os.Bundle
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) { class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init { init {
assert(resolveInfo.providerInfo != null) check(resolveInfo.providerInfo != null)
} }
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
......
...@@ -88,21 +88,19 @@ object PluginManager { ...@@ -88,21 +88,19 @@ object PluginManager {
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> { fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
return synchronized(this) { if (receiver == null) receiver = app.listenForPackageChanges {
if (receiver == null) receiver = app.listenForPackageChanges { synchronized(this) {
synchronized(this) { receiver = null
receiver = null cachedPlugins = null
cachedPlugins = null
}
}
if (cachedPlugins == null) {
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
} }
cachedPlugins!!
} }
if (cachedPlugins == null) {
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
}
cachedPlugins!!
} }
private fun buildUri(id: String) = Uri.Builder() private fun buildUri(id: String) = Uri.Builder()
...@@ -135,10 +133,10 @@ object PluginManager { ...@@ -135,10 +133,10 @@ object PluginManager {
private fun initNative(options: PluginOptions): String? { private fun initNative(options: PluginOptions): String? {
val providers = app.packageManager.queryIntentContentProviders( val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0) Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
assert(providers.size == 1) if (providers.isEmpty()) return null
val uri = Uri.Builder() val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT) .scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers[0].providerInfo.authority) .authority(providers.single().providerInfo.authority)
.build() .build()
val cr = app.contentResolver val cr = app.contentResolver
return try { return try {
...@@ -155,7 +153,7 @@ object PluginManager { ...@@ -155,7 +153,7 @@ object PluginManager {
out.putString(PluginContract.EXTRA_OPTIONS, options.id) out.putString(PluginContract.EXTRA_OPTIONS, options.id)
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out) val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out)
.getString(PluginContract.EXTRA_ENTRY) .getString(PluginContract.EXTRA_ENTRY)
assert(File(result).canExecute()) check(File(result).canExecute())
return result return result
} }
...@@ -174,7 +172,7 @@ object PluginManager { ...@@ -174,7 +172,7 @@ object PluginManager {
do { do {
val path = cursor.getString(0) val path = cursor.getString(0)
val file = File(pluginDir, path) val file = File(pluginDir, path)
assert(file.absolutePath.startsWith(pluginDirPath)) check(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream -> cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) } file.outputStream().use { outStream -> inStream.copyTo(outStream) }
} }
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
package com.github.shadowsocks.preference package com.github.shadowsocks.preference
import android.os.Binder import android.os.Binder
import com.github.shadowsocks.BootReceiver import android.support.v7.app.AppCompatDelegate
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.PrivateDatabase import com.github.shadowsocks.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase import com.github.shadowsocks.database.PublicDatabase
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
...@@ -29,9 +30,9 @@ import com.github.shadowsocks.utils.Key ...@@ -29,9 +30,9 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort import com.github.shadowsocks.utils.parsePort
object DataStore { object DataStore {
val publicStore = OrmLitePreferenceDataStore(PublicDatabase.kvPairDao) val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
// privateStore will only be used as temp storage for ProfileConfigFragment // privateStore will only be used as temp storage for ProfileConfigFragment
val privateStore = OrmLitePreferenceDataStore(PrivateDatabase.kvPairDao) val privateStore = RoomPreferenceDataStore(PrivateDatabase.kvPairDao)
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat // hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() } private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
...@@ -43,16 +44,24 @@ object DataStore { ...@@ -43,16 +44,24 @@ object DataStore {
} else parsePort(publicStore.getString(key), default + userIndex) } else parsePort(publicStore.getString(key), default + userIndex)
} }
var profileId: Int var profileId: Long
get() = publicStore.getInt(Key.id) ?: 0 get() = publicStore.getLong(Key.id) ?: 0
set(value) { set(value) {
publicStore.putInt(Key.id, value) publicStore.putLong(Key.id, value)
if (DataStore.directBootAware) DirectBoot.update() if (DataStore.directBootAware) DirectBoot.update()
} }
/** val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
* Setter is defined in MainActivity.onPreferenceDataStoreChanged. val directBootAware: Boolean get() = app.directBootSupported && canToggleLocked
*/ private var nightModeString: String
val directBootAware: Boolean get() = BootReceiver.enabled && publicStore.getBoolean(Key.directBootAware) == true get() = publicStore.getString(Key.nightMode) ?: Key.nightModeSystem
set(value) = publicStore.putString(Key.nightMode, value)
@AppCompatDelegate.NightMode
val nightMode: Int get() = when (nightModeString) {
Key.nightModeAuto -> AppCompatDelegate.MODE_NIGHT_AUTO
Key.nightModeOff -> AppCompatDelegate.MODE_NIGHT_NO
Key.nightModeOn -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
var serviceMode: String var serviceMode: String
get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
set(value) = publicStore.putString(Key.serviceMode, value) set(value) = publicStore.putString(Key.serviceMode, value)
...@@ -68,6 +77,7 @@ object DataStore { ...@@ -68,6 +77,7 @@ object DataStore {
fun initGlobal() { fun initGlobal() {
// temporary workaround for support lib bug // temporary workaround for support lib bug
if (publicStore.getString(Key.nightMode) == null) nightModeString = nightModeString
if (publicStore.getString(Key.serviceMode) == null) serviceMode = serviceMode if (publicStore.getString(Key.serviceMode) == null) serviceMode = serviceMode
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
......
...@@ -22,61 +22,60 @@ package com.github.shadowsocks.preference ...@@ -22,61 +22,60 @@ package com.github.shadowsocks.preference
import android.support.v7.preference.PreferenceDataStore import android.support.v7.preference.PreferenceDataStore
import com.github.shadowsocks.database.KeyValuePair import com.github.shadowsocks.database.KeyValuePair
import com.j256.ormlite.dao.Dao
import java.util.HashSet import java.util.HashSet
@Suppress("MemberVisibilityCanPrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
open class OrmLitePreferenceDataStore(private val kvPairDao: Dao<KeyValuePair, String?>) : PreferenceDataStore() { open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : PreferenceDataStore() {
fun getBoolean(key: String?) = kvPairDao.queryForId(key)?.boolean fun getBoolean(key: String) = kvPairDao[key]?.boolean
fun getFloat(key: String?) = kvPairDao.queryForId(key)?.float fun getFloat(key: String) = kvPairDao[key]?.float
fun getInt(key: String?) = kvPairDao.queryForId(key)?.int fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
fun getLong(key: String?) = kvPairDao.queryForId(key)?.long fun getLong(key: String) = kvPairDao[key]?.long
fun getString(key: String?) = kvPairDao.queryForId(key)?.string fun getString(key: String) = kvPairDao[key]?.string
fun getStringSet(key: String?) = kvPairDao.queryForId(key)?.stringSet fun getStringSet(key: String) = kvPairDao[key]?.stringSet
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue override fun getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue
override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
override fun getLong(key: String?, defValue: Long) = getLong(key) ?: defValue override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
override fun getString(key: String?, defValue: String?) = getString(key) ?: defValue override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
override fun getStringSet(key: String?, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue override fun getStringSet(key: String, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
fun putBoolean(key: String?, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value) fun putBoolean(key: String, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
fun putFloat(key: String?, value: Float?) = if (value == null) remove(key) else putFloat(key, value) fun putFloat(key: String, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value) fun putInt(key: String, value: Int?) = if (value == null) remove(key) else putLong(key, value.toLong())
fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value) fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String, value: Boolean) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putFloat(key: String?, value: Float) { override fun putFloat(key: String, value: Float) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putInt(key: String?, value: Int) { override fun putInt(key: String, value: Int) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value.toLong()))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putLong(key: String?, value: Long) { override fun putLong(key: String, value: Long) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else { override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else { override fun putStringSet(key: String, values: MutableSet<String>?) = if (values == null) remove(key) else {
kvPairDao.createOrUpdate(KeyValuePair(key).put(values)) kvPairDao.put(KeyValuePair(key).put(values))
fireChangeListener(key) fireChangeListener(key)
} }
fun remove(key: String?) { fun remove(key: String) {
kvPairDao.deleteById(key) kvPairDao.delete(key)
fireChangeListener(key) fireChangeListener(key)
} }
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>() private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
private fun fireChangeListener(key: String?) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) } private fun fireChangeListener(key: String) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener) fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener) fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
} }
...@@ -29,19 +29,24 @@ import com.twofortyfouram.locale.api.Intent as ApiIntent ...@@ -29,19 +29,24 @@ import com.twofortyfouram.locale.api.Intent as ApiIntent
class Settings(bundle: Bundle?) { class Settings(bundle: Bundle?) {
companion object { companion object {
private val KEY_SWITCH_ON = "switch_on" private const val KEY_SWITCH_ON = "switch_on"
private val KEY_PROFILE_ID = "profile_id" private const val KEY_PROFILE_ID = "profile_id"
fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE)) fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
} }
var switchOn: Boolean = bundle?.getBoolean(KEY_SWITCH_ON, true) ?: true var switchOn: Boolean = bundle?.getBoolean(KEY_SWITCH_ON, true) ?: true
var profileId: Int = bundle?.getInt(KEY_PROFILE_ID, -1) ?: -1 var profileId: Long
init {
profileId = bundle?.getLong(KEY_PROFILE_ID, -1L) ?: -1L
if (profileId < 0) profileId = (bundle?.getInt(KEY_PROFILE_ID, -1) ?: -1).toLong()
}
fun toIntent(context: Context): Intent { fun toIntent(context: Context): Intent {
val bundle = Bundle() val bundle = Bundle()
if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false) if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false)
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId) if (profileId >= 0) bundle.putLong(KEY_PROFILE_ID, profileId)
val profile = ProfileManager.getProfile(profileId) val profile = ProfileManager.getProfile(profileId)
return Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB, 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, if (profile != null) context.getString(if (switchOn) R.string.start_service else R.string.stop_service,
......
...@@ -32,6 +32,12 @@ object Key { ...@@ -32,6 +32,12 @@ object Key {
const val individual = "Proxyed" const val individual = "Proxyed"
const val nightMode = "nightMode"
const val nightModeSystem = "system"
const val nightModeAuto = "auto"
const val nightModeOff = "off"
const val nightModeOn = "on"
const val serviceMode = "serviceMode" const val serviceMode = "serviceMode"
const val modeProxy = "proxy" const val modeProxy = "proxy"
const val modeVpn = "vpn" const val modeVpn = "vpn"
......
package com.github.shadowsocks.utils package com.github.shadowsocks.utils
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
...@@ -12,8 +16,9 @@ import java.io.ObjectInputStream ...@@ -12,8 +16,9 @@ import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
@TargetApi(24) @TargetApi(24)
object DirectBoot { object DirectBoot : BroadcastReceiver() {
private val file = File(app.deviceContext.noBackupFilesDir, "directBootProfile") private val file = File(app.deviceContext.noBackupFilesDir, "directBootProfile")
private var registered = false
fun getDeviceProfile(): Profile? = try { fun getDeviceProfile(): Profile? = try {
ObjectInputStream(file.inputStream()).use { it.readObject() as Profile } ObjectInputStream(file.inputStream()).use { it.readObject() as Profile }
...@@ -24,8 +29,26 @@ object DirectBoot { ...@@ -24,8 +29,26 @@ object DirectBoot {
File(app.deviceContext.noBackupFilesDir, BaseService.CONFIG_FILE).delete() File(app.deviceContext.noBackupFilesDir, BaseService.CONFIG_FILE).delete()
} }
fun update() { /**
val profile = ProfileManager.getProfile(DataStore.profileId) // app.currentProfile will call this * app.currentProfile will call this.
if (profile == null) clean() else ObjectOutputStream(file.outputStream()).use { it.writeObject(profile) } */
fun update(profile: Profile? = ProfileManager.getProfile(DataStore.profileId)) =
if (profile == null) clean() else ObjectOutputStream(file.outputStream()).use { it.writeObject(profile) }
fun flushTrafficStats() {
val profile = getDeviceProfile()
if (profile?.dirty == true) ProfileManager.updateProfile(profile)
update()
}
fun listenForUnlock() {
if (registered) return
app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))
registered = true
}
override fun onReceive(context: Context, intent: Intent) {
flushTrafficStats()
app.unregisterReceiver(this)
registered = false
} }
} }
...@@ -52,5 +52,5 @@ object TcpFastOpen { ...@@ -52,5 +52,5 @@ object TcpFastOpen {
"else", "else",
" echo Failed.", " echo Failed.",
"fi"), null, true)?.joinToString("\n") "fi"), null, true)?.joinToString("\n")
fun enabledAsync(value: Boolean) = thread { enabled(value) }.join(1000) fun enabledAsync(value: Boolean) = thread("TcpFastOpen") { enabled(value) }.join(1000)
} }
...@@ -10,7 +10,6 @@ import android.support.annotation.AttrRes ...@@ -10,7 +10,6 @@ import android.support.annotation.AttrRes
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager import android.support.v4.app.FragmentManager
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v4.os.BuildCompat
import android.support.v7.util.SortedList import android.support.v7.util.SortedList
import android.util.TypedValue import android.util.TypedValue
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
...@@ -42,8 +41,8 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = ...@@ -42,8 +41,8 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
/** /**
* Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions. * Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions.
*/ */
fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = false,
name: String? = null, priority: Int = -1, block: () -> Unit): Thread { contextClassLoader: ClassLoader? = null, priority: Int = -1, block: () -> Unit): Thread {
val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block) val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
thread.setUncaughtExceptionHandler(app::track) thread.setUncaughtExceptionHandler(app::track)
if (start) thread.start() if (start) thread.start()
...@@ -56,8 +55,7 @@ val URLConnection.responseLength: Long ...@@ -56,8 +55,7 @@ val URLConnection.responseLength: Long
inline fun <reified T> Context.systemService() = ContextCompat.getSystemService(this, T::class.java)!! inline fun <reified T> Context.systemService() = ContextCompat.getSystemService(this, T::class.java)!!
val PackageInfo.signaturesCompat get() = val PackageInfo.signaturesCompat get() =
if (BuildCompat.isAtLeastP()) signingCertificateHistory.flatten() if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
else @Suppress("DEPRECATION") signatures.asIterable()
/** /**
* Based on: https://stackoverflow.com/a/15656428/2245107 * Based on: https://stackoverflow.com/a/15656428/2245107
......
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/material_grey_100"/>
<foreground>
<!-- 44dp icon scaled to 52dp in 72dp, padding = (1-52/44*24/72)/2 -->
<inset
android:drawable="@drawable/ic_image_camera_alt"
android:inset="30.303%"/>
</foreground>
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/material_grey_100"/>
<foreground>
<!-- 44dp icon scaled to 52dp in 72dp, padding = (1-52/44*24/72)/2 -->
<inset
android:drawable="@drawable/ic_qu_shadowsocks_foreground"
android:inset="30.303%"/>
</foreground>
</adaptive-icon>
\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" /> <solid android:color="@color/background_stat" />
</shape> </shape>
</item> </item>
<item> <item>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<!-- android:start not available until API 23 --> <!-- android:start not available until API 23 -->
<item android:left="8dp"> <item android:left="8dp">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@android:color/white" /> <solid android:color="@color/background" />
</shape> </shape>
</item> </item>
<item android:drawable="?android:attr/selectableItemBackground"/> <item android:drawable="?android:attr/selectableItemBackground"/>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime" <selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime"> android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true"> <item android:state_selected="true">
<color android:color="@color/material_primary_100"/> <color android:color="@color/background_selected"/>
</item> </item>
</selector> </selector>
</item> </item>
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<color android:color="@color/material_primary_300"/> <color android:color="@color/background_stat"/>
</item> </item>
<item android:drawable="?android:attr/selectableItemBackground"/> <item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list> </layer-list>
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing permissions and limitations under
the License.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/material_grey_100" />
<size android:width="44dp" android:height="44dp" />
</shape>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/material_primary_600"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
<path
android:fillColor="@color/material_primary_600"
android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
</vector>
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="@color/primary_drawable"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector> </vector>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path <item
android:fillColor="#f5f5f5" android:drawable="@drawable/ic_app_shortcut_background"
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0Z" /> android:left="2dp"
<path android:top="2dp"
android:fillColor="#546e7a" android:right="2dp"
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 android:bottom="2dp" />
C22.2327,28.2,20.8,26.7673,20.8,25 C20.8,23.2327,22.2327,21.8,24,21.8 Z" />
<path <item
android:fillColor="#546e7a" android:drawable="@drawable/ic_image_camera_alt"
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 android:left="12dp"
A2,2,0,0,0,34,31 L34,19 A2,2,0,0,0,32,17 L28.83,17 L27,15 Z M24,30 android:top="12dp"
A5,5,0,1,1,29,25 A5,5,0,0,1,24,30 Z" /> android:right="12dp"
</vector> android:bottom="12dp" />
\ No newline at end of file
</layer-list>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="@color/material_blue_grey_600"
android:pathData="M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 ZM 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path <item
android:fillColor="#f5f5f5" android:drawable="@drawable/ic_app_shortcut_background"
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0Z" /> android:left="2dp"
<path android:top="2dp"
android:fillColor="#546e7a" android:right="2dp"
android:pathData="M34,14.72 L30.52,30.04 L22.72,27.53 L29.61,19.31 L20.43,27.04 L14,24.62 android:bottom="2dp" />
L34,14.72 Z" />
<path <item
android:fillColor="#546e7a" android:drawable="@drawable/ic_qu_shadowsocks_foreground"
android:pathData="M22.89,29.1 L25.38,29.9 L22.9,33.28 L22.89,29.1 Z" /> android:left="12dp"
</vector> android:top="12dp"
android:right="12dp"
android:bottom="12dp" />
</layer-list>
\ No newline at end of file
...@@ -5,6 +5,6 @@ ...@@ -5,6 +5,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="@color/primary_drawable"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector> </vector>
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:padding="16dp">
<Spinner android:id="@+id/template_selector" <Spinner android:id="@+id/template_selector"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingEnd="12dp"> android:paddingEnd="12dp"
android:focusable="true">
<ImageView android:id="@android:id/icon" <ImageView android:id="@android:id/icon"
android:layout_width="52dp" android:layout_width="52dp"
android:layout_height="52dp"/> android:layout_height="52dp"/>
...@@ -17,7 +18,6 @@ ...@@ -17,7 +18,6 @@
android:layout_gravity="center_vertical"> android:layout_gravity="center_vertical">
<TextView android:id="@android:id/text1" <TextView android:id="@android:id/text1"
android:textAppearance="?android:attr/textAppearance" android:textAppearance="?android:attr/textAppearance"
android:textColor="@android:color/primary_text_light"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="2" android:maxLines="2"
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<LinearLayout android:layout_width="fill_parent" <LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:elevation="1dp" android:elevation="1dp"
android:background="@android:color/white" android:background="@color/background"
android:orientation="vertical" android:orientation="vertical"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/snackbar" android:id="@+id/snackbar"
android:background="@color/material_primary_700" android:background="@color/color_primary_dark"
android:clipChildren="false" android:clipChildren="false"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
android:ellipsize="marquee" android:ellipsize="marquee"
android:gravity="end" android:gravity="end"
android:text="@string/sent" android:text="@string/sent"
android:textColor="?android:attr/textColorPrimary"
app:layout_column="0"/> app:layout_column="0"/>
<TextView <TextView
...@@ -102,6 +103,7 @@ ...@@ -102,6 +103,7 @@
android:ellipsize="marquee" android:ellipsize="marquee"
android:gravity="end" android:gravity="end"
android:text="@string/received" android:text="@string/received"
android:textColor="?android:attr/textColorPrimary"
app:layout_column="0" app:layout_column="0"
app:layout_row="1"/> app:layout_row="1"/>
......
...@@ -11,6 +11,5 @@ ...@@ -11,6 +11,5 @@
android:name="com.google.android.gms.samples.vision.barcodereader.BarcodeCapture" android:name="com.google.android.gms.samples.vision.barcodereader.BarcodeCapture"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
app:gvb_auto_focus="true" app:gvb_auto_focus="true"/>
app:gvb_code_format="qr_code"/>
</LinearLayout> </LinearLayout>
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
<string name="delete_confirm_prompt">"آیا مطمئن هستید که می‌خواهید این پروفایل را حذف کنید؟"</string> <string name="delete_confirm_prompt">"آیا مطمئن هستید که می‌خواهید این پروفایل را حذف کنید؟"</string>
<string name="share_qr_nfc">"NFC یا QR Code"</string> <string name="share_qr_nfc">"NFC یا QR Code"</string>
<string name="add_profile_methods_scan_qr_code">"اسکن‌کردن QR Code"</string> <string name="add_profile_methods_scan_qr_code">"اسکن‌کردن QR Code"</string>
<string name="add_profile_methods_manual_settings">"تنظیمات دستی"</string>
<!-- tasker --> <!-- tasker -->
<string name="toggle_service_state">"فعال‌کردن سرویس"</string> <string name="toggle_service_state">"فعال‌کردن سرویس"</string>
...@@ -55,4 +54,5 @@ ...@@ -55,4 +54,5 @@
<string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string> <string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string>
<string name="not_connected">"اتصال برقرار نیست!"</string> <string name="not_connected">"اتصال برقرار نیست!"</string>
<string name="route_entry_all">"همه"</string> <string name="route_entry_all">"همه"</string>
<string name="add_profile_methods_manual_settings">"تنظیمات دستی"</string>
</resources> </resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="quick_toggle">"Basculer"</string>
<!-- misc --> <!-- misc -->
<string name="profile">"Profil"</string> <string name="profile">"Profil"</string>
<string name="profile_summary">"Passer à un autre profil ou ajouter de nouveaux profils"</string>
<string name="remote_dns">"DNS Distant"</string>
<string name="stat_summary">"Envoyé : \t%3$s\t↑\t%1$s
Reçu : \t\t\t%4$s\t↓\t%2$s"</string>
<string name="connection_test_testing">"Essai..."</string>
<string name="connection_test_error">"Impossible de détecter la connexion Internet :%s"</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_fail">"Internet Indisponible"</string>
<string name="connection_test_error_status_code">"Code de l'Erreur : #%d"</string> <string name="connection_test_error_status_code">"Code de l'Erreur : #%d"</string>
<!-- proxy category -->
<string name="profile_name">"Nom du Profil"</string>
<string name="proxy">"Serveur"</string> <string name="proxy">"Serveur"</string>
<string name="remote_port">"Port Distant"</string>
<string name="sitekey">"Mot de passe"</string> <string name="sitekey">"Mot de passe"</string>
<string name="enc_method">"Méthode d'Encryption"</string>
<string name="auto_connect_summary">"Activer Shadowsocks au démarrage"</string>
<string name="tcp_fastopen_summary_unsupported">"Version du noyau non supportée : %s &lt;3.7.1"</string>
<!-- notification category -->
<string name="forward_success">"Shadowsocks a démarré"</string>
<string name="invalid_server">"Nom de serveur invalide"</string>
<string name="service_failed">"Échec de la connexion au serveur distant"</string> <string name="service_failed">"Échec de la connexion au serveur distant"</string>
<string name="reboot_required">"Impossible de démarrer le service VPN. Vous devrez peut-être redémarrer votre appareil."</string>
<!-- alert category --> <!-- alert category -->
<string name="close">"Fermer"</string> <string name="close">"Fermer"</string>
<string name="profile_empty">"Veuillez sélectionner un profil"</string> <string name="profile_empty">"Veuillez sélectionner un profil"</string>
<string name="connect">"Connexion"</string>
<string name="remove_profile">"Supprimer ce profil %s ?"</string>
<!-- menu category --> <!-- menu category -->
<string name="profiles">"Profils"</string> <string name="profiles">"Profils"</string>
<string name="settings">"Paramètres"</string> <string name="settings">"Paramètres"</string>
<string name="about">"À propos"</string> <string name="about">"À propos"</string>
<string name="edit">"Éditer"</string>
<string name="share">"Partager"</string> <string name="share">"Partager"</string>
<string name="add_profile">"Ajouter un Profil"</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_apply_all">"Appliquer les Paramètres à tous les Profils"</string>
<string name="action_export">"Exporter vers le presse-papiers"</string> <string name="action_export">"Exporter vers le presse-papiers"</string>
<string name="action_import">"Importer depuis le presse-papiers"</string> <string name="action_import">"Importer depuis le presse-papiers"</string>
<string name="action_export_msg">"Exporter avec succès !"</string>
<string name="action_export_err">"Échec de l'exportation"</string>
<string name="action_import_msg">"Importer avec succès !"</string>
<string name="action_import_err">"Échec de l’importation."</string>
<string name="delete">"Supprimer"</string> <string name="delete">"Supprimer"</string>
<string name="delete_confirm_prompt">"Êtes-vous sûr de vouloir supprimer ce profil?"</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="share_qr_nfc">"QR Code/NFC"</string>
<string name="add_profile_dialog">"Ajouter ce profil Shadowsocks?"</string>
<string name="add_profile_methods_scan_qr_code">"Scanner le QR Code"</string> <string name="add_profile_methods_scan_qr_code">"Scanner le QR Code"</string>
<plurals name="removed"> <plurals name="removed">
<item quantity="one">"Supprimé"</item> <item quantity="one">"Supprimé"</item>
<item quantity="other">"%d éléments supprimés"</item> <item quantity="other">"%d éléments supprimés"</item>
</plurals> </plurals>
<string name="undo">"Annuler"</string> <string name="undo">"Annuler"</string>
<!-- tasker -->
<string name="toggle_service_state">"Démarrer le service"</string>
<string name="profile_default">"Utiliser le profil actuel"</string>
<!-- status -->
<string name="sent">"Envoyé :"</string>
<string name="received">"Reçu :"</string>
<string name="connecting">"Connexion..."</string>
<string name="not_connected">"Non connecté"</string>
<!-- acl -->
<string name="custom_rules">"Règles personnalisées"</string>
<string name="action_add_rule">"Ajouter une/des règle(s)"</string>
<string name="edit_rule">"Modifier une règle"</string>
<string name="route_entry_all">"Toutes"</string>
<string name="plugin_configure">"Configurer..."</string>
<string name="plugin_disabled">"Désactivé"</string>
<string name="plugin_unknown">"Plugin inconnu %s"</string>
<string name="plugin_untrusted">"Attention : ce plugin ne semble pas provenir d'une source fiable reconnue."</string>
<string name="profile_plugin">"Plugin : %s"</string>
<string name="advanced">"Avancé"</string>
<string name="auto_connect_summary_v24">"Activer Shadowsocks au démarrage. Il est plutôt recommandé d'utiliser le VPN en permanence"</string>
<string name="direct_boot_aware_summary">"Vos informations de profil sélectionnées seront moins protégées"</string>
</resources> </resources>
\ No newline at end of file
...@@ -15,9 +15,6 @@ ...@@ -15,9 +15,6 @@
<string name="connection_test_fail">"インターネット利用不可"</string> <string name="connection_test_fail">"インターネット利用不可"</string>
<string name="connection_test_error_status_code">"ステータスコード無効: #%d"</string> <string name="connection_test_error_status_code">"ステータスコード無効: #%d"</string>
<!-- proxy category -->
<string name="proxy_cat">"サーバー設定"</string>
<!-- proxy category --> <!-- proxy category -->
<string name="profile_name">"サーバー名"</string> <string name="profile_name">"サーバー名"</string>
<string name="proxy">"サーバーアドレス"</string> <string name="proxy">"サーバーアドレス"</string>
...@@ -25,9 +22,6 @@ ...@@ -25,9 +22,6 @@
<string name="sitekey">"パスワード"</string> <string name="sitekey">"パスワード"</string>
<string name="enc_method">"暗号化方式"</string> <string name="enc_method">"暗号化方式"</string>
<!-- feature category -->
<string name="feature_cat">"ファンクション設定"</string>
<!-- feature category --> <!-- feature category -->
<string name="ipv6">"IPv6 プロキシ"</string> <string name="ipv6">"IPv6 プロキシ"</string>
<string name="ipv6_summary">"リモートサーバーに IPv6 パケットを転送"</string> <string name="ipv6_summary">"リモートサーバーに IPv6 パケットを転送"</string>
...@@ -55,10 +49,6 @@ ...@@ -55,10 +49,6 @@
<string name="reboot_required">"VPN サービスの起動に失敗しました、デバイスの再起動を試みて下さい"</string> <string name="reboot_required">"VPN サービスの起動に失敗しました、デバイスの再起動を試みて下さい"</string>
<string name="profile_invalid_input">"有効なプロファイルが見つかりません"</string> <string name="profile_invalid_input">"有効なプロファイルが見つかりません"</string>
<!-- alert category -->
<string name="yes">"はい"</string>
<string name="no">"いいえ"</string>
<!-- alert category --> <!-- alert category -->
<string name="close">"閉じる"</string> <string name="close">"閉じる"</string>
<string name="profile_empty">"プロファイルを選択して下さい"</string> <string name="profile_empty">"プロファイルを選択して下さい"</string>
...@@ -84,8 +74,6 @@ ...@@ -84,8 +74,6 @@
<!-- profile --> <!-- profile -->
<string name="profile_config">"プロファイル編集"</string> <string name="profile_config">"プロファイル編集"</string>
<string name="unsaved_changes_prompt">"変更は適応されておりません、保存しますか?"</string>
<string name="apply">"適用"</string>
<string name="delete">"削除"</string> <string name="delete">"削除"</string>
<string name="delete_confirm_prompt">"このプロファイルを削除しますか"</string> <string name="delete_confirm_prompt">"このプロファイルを削除しますか"</string>
<string name="share_qr_nfc">"QR コード / NFC"</string> <string name="share_qr_nfc">"QR コード / NFC"</string>
...@@ -147,7 +135,7 @@ ...@@ -147,7 +135,7 @@
<string name="service_transproxy">"トランスプロキシサービス"</string> <string name="service_transproxy">"トランスプロキシサービス"</string>
<string name="vpn_permission_denied">"VPNサービス作成のアクセス許可が拒否されました"</string> <string name="vpn_permission_denied">"VPNサービス作成のアクセス許可が拒否されました"</string>
<string name="auto_connect_summary_v24">"起動時にShadowsockを有効。 VPN常時接続の使用をお勧めします"</string> <string name="auto_connect_summary_v24">"起動時にShadowsockを有効。 VPN常時接続の使用をお勧めします"</string>
<string name="direct_boot_aware">"ダイレクトブート"</string> <string name="direct_boot_aware_summary">"選択されたプロファイル情報はより少ない保護を受けることになります"</string>
<string name="direct_boot_aware_summary">"デバイスがロック解除される前にShadowsocksの自動起動を許可(選択されたプロファイル情報はより少ない保護を受けることになります)"</string>
<string name="acl_rule_online_config">"オンライン設定のURL"</string> <string name="acl_rule_online_config">"オンライン設定のURL"</string>
<string name="action_import_file">"ファイルからのインポート"</string>
</resources> </resources>
\ No newline at end of file
...@@ -18,9 +18,6 @@ ...@@ -18,9 +18,6 @@
<string name="connection_test_fail">"인터넷에 연결할 수 없습니다"</string> <string name="connection_test_fail">"인터넷에 연결할 수 없습니다"</string>
<string name="connection_test_error_status_code">"오류 코드: #%d"</string> <string name="connection_test_error_status_code">"오류 코드: #%d"</string>
<!-- proxy category -->
<string name="proxy_cat">"서버 설정"</string>
<!-- proxy category --> <!-- proxy category -->
<string name="profile_name">"프로필 이름"</string> <string name="profile_name">"프로필 이름"</string>
<string name="proxy">"서버 주소"</string> <string name="proxy">"서버 주소"</string>
...@@ -28,9 +25,6 @@ ...@@ -28,9 +25,6 @@
<string name="sitekey">"비밀번호"</string> <string name="sitekey">"비밀번호"</string>
<string name="enc_method">"암호화 방법"</string> <string name="enc_method">"암호화 방법"</string>
<!-- feature category -->
<string name="feature_cat">"기능 설정"</string>
<!-- feature category --> <!-- feature category -->
<string name="ipv6">"IPv6 라우팅"</string> <string name="ipv6">"IPv6 라우팅"</string>
<string name="ipv6_summary">"IPv6 트래픽도 원격으로 리다이렉트 합니다"</string> <string name="ipv6_summary">"IPv6 트래픽도 원격으로 리다이렉트 합니다"</string>
...@@ -58,10 +52,6 @@ ...@@ -58,10 +52,6 @@
<string name="reboot_required">"VPN 서비스를 시작하는 데 실패했습니다. 장치를 재시작해 보세요."</string> <string name="reboot_required">"VPN 서비스를 시작하는 데 실패했습니다. 장치를 재시작해 보세요."</string>
<string name="profile_invalid_input">"올바른 프로필 데이터를 찾을 수 없습니다"</string> <string name="profile_invalid_input">"올바른 프로필 데이터를 찾을 수 없습니다"</string>
<!-- alert category -->
<string name="yes">"예"</string>
<string name="no">"아니오"</string>
<!-- alert category --> <!-- alert category -->
<string name="close">"닫기"</string> <string name="close">"닫기"</string>
<string name="profile_empty">"프로필을 선택해 주세요"</string> <string name="profile_empty">"프로필을 선택해 주세요"</string>
...@@ -86,8 +76,6 @@ ...@@ -86,8 +76,6 @@
<!-- profile --> <!-- profile -->
<string name="profile_config">"프로필 설정"</string> <string name="profile_config">"프로필 설정"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="apply">"적용"</string>
<string name="delete">"삭제"</string> <string name="delete">"삭제"</string>
<string name="delete_confirm_prompt">"정말 이 프로필을 삭제하시겠습니까?"</string> <string name="delete_confirm_prompt">"정말 이 프로필을 삭제하시겠습니까?"</string>
<string name="share_qr_nfc">"QR 코드/NFC"</string> <string name="share_qr_nfc">"QR 코드/NFC"</string>
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background">@color/md_dark_background</color>
<color name="background_selected">@color/material_primary_800</color>
<color name="primary_drawable">@android:color/white</color>
<color name="background_stat">@color/material_primary_600</color>
<!-- MaterialDrawer colors -->
<color name="drawer_background">@color/material_drawer_dark_background</color>
<color name="drawer_primary_text">@color/material_drawer_dark_primary_text</color>
<color name="drawer_primary_icon">@color/material_drawer_dark_primary_icon</color>
<color name="drawer_secondary_text">@color/material_drawer_dark_secondary_text</color>
<color name="drawer_hint_text">@color/material_drawer_dark_hint_text</color>
<color name="drawer_divider">@color/material_drawer_dark_divider</color>
<color name="drawer_selected">@color/material_drawer_dark_selected</color>
<color name="drawer_selected_text">@color/material_accent_200</color>
<color name="drawer_header_selection_text">@color/material_drawer_dark_header_selection_text</color>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_url" translatable="false">file:///android_asset/pages/about_night.html</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Design.DayNight.BottomSheetDialog" parent="@style/Theme.Design.BottomSheetDialog"/>
</resources>
...@@ -21,9 +21,6 @@ ...@@ -21,9 +21,6 @@
<string name="connection_test_fail">"Интернет недоступен"</string> <string name="connection_test_fail">"Интернет недоступен"</string>
<string name="connection_test_error_status_code">"Код ошибки: #%d"</string> <string name="connection_test_error_status_code">"Код ошибки: #%d"</string>
<!-- proxy category -->
<string name="proxy_cat">"Настройки Сервера"</string>
<!-- proxy category --> <!-- proxy category -->
<string name="profile_name">"Имя профиля"</string> <string name="profile_name">"Имя профиля"</string>
<string name="proxy">"Сервер"</string> <string name="proxy">"Сервер"</string>
...@@ -31,9 +28,6 @@ ...@@ -31,9 +28,6 @@
<string name="sitekey">"Пароль"</string> <string name="sitekey">"Пароль"</string>
<string name="enc_method">"Метод шифрования"</string> <string name="enc_method">"Метод шифрования"</string>
<!-- feature category -->
<string name="feature_cat">"Дополнительные Настройки"</string>
<!-- feature category --> <!-- feature category -->
<string name="ipv6">"IPv6 Маршрут"</string> <string name="ipv6">"IPv6 Маршрут"</string>
<string name="ipv6_summary">"Перенаправлять трафик IPv6 на удалённый сервер"</string> <string name="ipv6_summary">"Перенаправлять трафик IPv6 на удалённый сервер"</string>
...@@ -60,10 +54,6 @@ ...@@ -60,10 +54,6 @@
<string name="reboot_required">"Не удалось запустить службу VPN. Возможно, требуется перезагрузить ваше устройство."</string> <string name="reboot_required">"Не удалось запустить службу VPN. Возможно, требуется перезагрузить ваше устройство."</string>
<string name="profile_invalid_input">"Не найдено действительных данных профиля."</string> <string name="profile_invalid_input">"Не найдено действительных данных профиля."</string>
<!-- alert category -->
<string name="yes">"Да"</string>
<string name="no">"Нет"</string>
<!-- alert category --> <!-- alert category -->
<string name="close">"Закрыть"</string> <string name="close">"Закрыть"</string>
<string name="profile_empty">"Пожалуйста, выберите профиль"</string> <string name="profile_empty">"Пожалуйста, выберите профиль"</string>
...@@ -91,8 +81,6 @@ ...@@ -91,8 +81,6 @@
<!-- profile --> <!-- profile -->
<string name="profile_config">"Настройка профиля"</string> <string name="profile_config">"Настройка профиля"</string>
<string name="unsaved_changes_prompt">"Изменения не сохранены. Сохранить?"</string>
<string name="apply">"Применить"</string>
<string name="delete">"Удалить"</string> <string name="delete">"Удалить"</string>
<string name="delete_confirm_prompt">"Вы уверены, что хотите удалить этот профиль?"</string> <string name="delete_confirm_prompt">"Вы уверены, что хотите удалить этот профиль?"</string>
<string name="share_qr_nfc">"QR-код/NFC"</string> <string name="share_qr_nfc">"QR-код/NFC"</string>
...@@ -157,7 +145,7 @@ ...@@ -157,7 +145,7 @@
<string name="vpn_permission_denied">"Нет разрешения на создание VPN соединения"</string> <string name="vpn_permission_denied">"Нет разрешения на создание VPN соединения"</string>
<string name="auto_connect_summary_v24">"Включить Shadowsocks во время запуска. Лучше использовать режим постоянной VPN."</string> <string name="auto_connect_summary_v24">"Включить Shadowsocks во время запуска. Лучше использовать режим постоянной VPN."</string>
<string name="speed">"%s/с"</string> <string name="speed">"%s/с"</string>
<string name="direct_boot_aware">"Непосредственная загрузка"</string> <string name="direct_boot_aware_summary">"данные выбранного профиля будут хуже защищены"</string>
<string name="direct_boot_aware_summary">"Позволить Shadowsocks запускаться до того, как устройство будет разблокировано (данные выбранного профиля будут хуже защищены)"</string>
<string name="acl_rule_online_config">"URL конфигурации"</string> <string name="acl_rule_online_config">"URL конфигурации"</string>
<string name="action_import_file">"Импортировать из файла..."</string>
</resources> </resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">"影梭"</string>
<string name="quick_toggle">"开关"</string> <string name="quick_toggle">"开关"</string>
<!-- misc --> <!-- misc -->
...@@ -16,9 +15,6 @@ ...@@ -16,9 +15,6 @@
<string name="connection_test_fail">"无互联网连接"</string> <string name="connection_test_fail">"无互联网连接"</string>
<string name="connection_test_error_status_code">"状态码无效(#%d)"</string> <string name="connection_test_error_status_code">"状态码无效(#%d)"</string>
<!-- proxy category -->
<string name="proxy_cat">"服务器设置"</string>
<!-- proxy category --> <!-- proxy category -->
<string name="profile_name">"配置名称"</string> <string name="profile_name">"配置名称"</string>
<string name="proxy">"服务器"</string> <string name="proxy">"服务器"</string>
...@@ -26,9 +22,6 @@ ...@@ -26,9 +22,6 @@
<string name="sitekey">"密码"</string> <string name="sitekey">"密码"</string>
<string name="enc_method">"加密方式"</string> <string name="enc_method">"加密方式"</string>
<!-- feature category -->
<string name="feature_cat">"功能设置"</string>
<!-- feature category --> <!-- feature category -->
<string name="ipv6">"IPv6 路由"</string> <string name="ipv6">"IPv6 路由"</string>
<string name="ipv6_summary">"转发 IPv6 流量到远程服务器"</string> <string name="ipv6_summary">"转发 IPv6 流量到远程服务器"</string>
...@@ -45,6 +38,11 @@ ...@@ -45,6 +38,11 @@
<string name="tcp_fastopen_summary_unsupported">"不支持的内核版本: %s &lt; 3.7.1"</string> <string name="tcp_fastopen_summary_unsupported">"不支持的内核版本: %s &lt; 3.7.1"</string>
<string name="udp_dns">"DNS 转发"</string> <string name="udp_dns">"DNS 转发"</string>
<string name="udp_dns_summary">"转发所有 DNS 请求到远程服务器"</string> <string name="udp_dns_summary">"转发所有 DNS 请求到远程服务器"</string>
<string name="night_mode">夜间模式</string>
<string name="night_mode_system">跟随系统</string>
<string name="night_mode_auto">自动</string>
<string name="night_mode_on">开启</string>
<string name="night_mode_off">关闭</string>
<!-- notification category --> <!-- notification category -->
<string name="forward_success">"后台服务已开始运行。"</string> <string name="forward_success">"后台服务已开始运行。"</string>
...@@ -56,10 +54,6 @@ ...@@ -56,10 +54,6 @@
<string name="reboot_required">"VPN 服务启动失败。你可能需要重启设备。"</string> <string name="reboot_required">"VPN 服务启动失败。你可能需要重启设备。"</string>
<string name="profile_invalid_input">"未找到有效的配置文件。"</string> <string name="profile_invalid_input">"未找到有效的配置文件。"</string>
<!-- alert category -->
<string name="yes">"是"</string>
<string name="no">"否"</string>
<!-- alert category --> <!-- alert category -->
<string name="close">"关闭"</string> <string name="close">"关闭"</string>
<string name="profile_empty">"请选择配置文件"</string> <string name="profile_empty">"请选择配置文件"</string>
...@@ -86,8 +80,6 @@ ...@@ -86,8 +80,6 @@
<!-- profile --> <!-- profile -->
<string name="profile_config">"配置文件设置"</string> <string name="profile_config">"配置文件设置"</string>
<string name="unsaved_changes_prompt">"保存修改吗?"</string>
<string name="apply">"应用"</string>
<string name="delete">"删除"</string> <string name="delete">"删除"</string>
<string name="delete_confirm_prompt">"您确定要删除此配置文件?"</string> <string name="delete_confirm_prompt">"您确定要删除此配置文件?"</string>
<string name="share_qr_nfc">"二维码 / NFC"</string> <string name="share_qr_nfc">"二维码 / NFC"</string>
...@@ -148,7 +140,8 @@ ...@@ -148,7 +140,8 @@
<string name="service_transproxy">"透明代理模式"</string> <string name="service_transproxy">"透明代理模式"</string>
<string name="vpn_permission_denied">"创建 VPN 服务权限不足"</string> <string name="vpn_permission_denied">"创建 VPN 服务权限不足"</string>
<string name="auto_connect_summary_v24">"允许 Shadowsocks 随系统启动,建议使用始终开启的 VPN"</string> <string name="auto_connect_summary_v24">"允许 Shadowsocks 随系统启动,建议使用始终开启的 VPN"</string>
<string name="direct_boot_aware">"直接启动"</string> <string name="direct_boot_aware">"允许锁屏下切换"</string>
<string name="direct_boot_aware_summary">"允许在设备解锁前启动服务(选中的配置信息会不那么安全)"</string> <string name="direct_boot_aware_summary">"选中的配置信息会不那么安全"</string>
<string name="acl_rule_online_config">"在线规则文件 URL"</string> <string name="acl_rule_online_config">"在线规则文件 URL"</string>
<string name="action_import_file">"从文件导入…"</string>
</resources> </resources>
\ No newline at end of file
...@@ -19,9 +19,6 @@ ...@@ -19,9 +19,6 @@
<string name="connection_test_fail">"無法使用網際網路"</string> <string name="connection_test_fail">"無法使用網際網路"</string>
<string name="connection_test_error_status_code">"錯誤碼: (#%d)"</string> <string name="connection_test_error_status_code">"錯誤碼: (#%d)"</string>
<!-- proxy category -->
<string name="proxy_cat">"伺服器設定"</string>
<!-- proxy category --> <!-- proxy category -->
<string name="profile_name">"設定檔名稱"</string> <string name="profile_name">"設定檔名稱"</string>
<string name="proxy">"伺服器"</string> <string name="proxy">"伺服器"</string>
...@@ -29,9 +26,6 @@ ...@@ -29,9 +26,6 @@
<string name="sitekey">"密碼"</string> <string name="sitekey">"密碼"</string>
<string name="enc_method">"加密方法"</string> <string name="enc_method">"加密方法"</string>
<!-- feature category -->
<string name="feature_cat">"功能設定"</string>
<!-- feature category --> <!-- feature category -->
<string name="ipv6">"IPv6 路由"</string> <string name="ipv6">"IPv6 路由"</string>
<string name="ipv6_summary">"向遠端重新導向 IPv6 流量"</string> <string name="ipv6_summary">"向遠端重新導向 IPv6 流量"</string>
...@@ -58,10 +52,6 @@ ...@@ -58,10 +52,6 @@
<string name="reboot_required">"VPN 服務啟動失敗。您或許需要重新啟動您的裝置。"</string> <string name="reboot_required">"VPN 服務啟動失敗。您或許需要重新啟動您的裝置。"</string>
<string name="profile_invalid_input">"未找到有效的設定檔資料。"</string> <string name="profile_invalid_input">"未找到有效的設定檔資料。"</string>
<!-- alert category -->
<string name="yes">"是"</string>
<string name="no">"否"</string>
<!-- alert category --> <!-- alert category -->
<string name="close">"關閉"</string> <string name="close">"關閉"</string>
<string name="profile_empty">"請選擇設定檔"</string> <string name="profile_empty">"請選擇設定檔"</string>
...@@ -88,8 +78,6 @@ ...@@ -88,8 +78,6 @@
<!-- profile --> <!-- profile -->
<string name="profile_config">"設定檔設定"</string> <string name="profile_config">"設定檔設定"</string>
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<string name="apply">"套用"</string>
<string name="delete">"刪除"</string> <string name="delete">"刪除"</string>
<string name="delete_confirm_prompt">"您確定要移除這個設定檔嗎?"</string> <string name="delete_confirm_prompt">"您確定要移除這個設定檔嗎?"</string>
<string name="share_qr_nfc">"QR 碼 / NFC"</string> <string name="share_qr_nfc">"QR 碼 / NFC"</string>
......
...@@ -237,4 +237,16 @@ ...@@ -237,4 +237,16 @@
<item>vpn</item> <item>vpn</item>
<item>transproxy</item> <item>transproxy</item>
</string-array> </string-array>
<string-array name="night_modes">
<item>@string/night_mode_system</item>
<item>@string/night_mode_auto</item>
<item>@string/night_mode_off</item>
<item>@string/night_mode_on</item>
</string-array>
<string-array name="night_mode_values" translatable="false">
<item>system</item>
<item>auto</item>
<item>off</item>
<item>on</item>
</string-array>
</resources> </resources>
...@@ -2,4 +2,19 @@ ...@@ -2,4 +2,19 @@
<resources> <resources>
<color name="ic_launcher_background">#7488A1</color> <color name="ic_launcher_background">#7488A1</color>
<color name="material_drawer_selected_text">@color/material_accent_200</color> <color name="material_drawer_selected_text">@color/material_accent_200</color>
<color name="background">@color/md_light_background</color>
<color name="background_selected">@color/material_primary_100</color>
<color name="primary_drawable">@android:color/black</color>
<color name="background_stat">@color/material_primary_300</color>
<!-- MaterialDrawer colors -->
<color name="drawer_background">@color/material_drawer_background</color>
<color name="drawer_primary_text">@color/material_drawer_primary_text</color>
<color name="drawer_primary_icon">@color/material_drawer_primary_icon</color>
<color name="drawer_secondary_text">@color/material_drawer_secondary_text</color>
<color name="drawer_hint_text">@color/material_drawer_hint_text</color>
<color name="drawer_divider">@color/material_drawer_divider</color>
<color name="drawer_selected">@color/material_drawer_selected</color>
<color name="drawer_selected_text">@color/material_accent_200</color>
<color name="drawer_header_selection_text">@color/material_drawer_header_selection_text</color>
</resources> </resources>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<string name="app_name">Shadowsocks</string> <string name="app_name">Shadowsocks</string>
<string name="quick_toggle">Toggle</string> <string name="quick_toggle">Toggle</string>
<string name="about_url" translatable="false">file:///android_asset/pages/about.html</string>
<!-- misc --> <!-- misc -->
<string name="profile">Profile</string> <string name="profile">Profile</string>
...@@ -20,10 +21,6 @@ ...@@ -20,10 +21,6 @@
<string name="traffic">%s↑\t%s↓</string> <string name="traffic">%s↑\t%s↓</string>
<string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s\nReceived: \t%4$s\t↓\t%2$s</string> <string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s\nReceived: \t%4$s\t↓\t%2$s</string>
<string name="speed">%s/s</string> <string name="speed">%s/s</string>
<plurals name="bytes">
<item quantity="one">Byte</item>
<item quantity="other">Bytes</item>
</plurals>
<string name="connection_test_pending">Check Connectivity</string> <string name="connection_test_pending">Check Connectivity</string>
<string name="connection_test_testing">Testing…</string> <string name="connection_test_testing">Testing…</string>
<string name="connection_test_available">Success: %dms latency</string> <string name="connection_test_available">Success: %dms latency</string>
...@@ -57,13 +54,17 @@ ...@@ -57,13 +54,17 @@
<string name="auto_connect_summary">Enable Shadowsocks on startup</string> <string name="auto_connect_summary">Enable Shadowsocks on startup</string>
<string name="auto_connect_summary_v24">Enable Shadowsocks on startup. Recommended to use always-on VPN <string name="auto_connect_summary_v24">Enable Shadowsocks on startup. Recommended to use always-on VPN
instead</string> instead</string>
<string name="direct_boot_aware">Direct Boot Aware</string> <string name="direct_boot_aware">Allow Toggling in Lock Screen</string>
<string name="direct_boot_aware_summary">Allow Shadowsocks to start before your device is unlocked (your selected <string name="direct_boot_aware_summary">Your selected profile information will be less protected</string>
profile information will be less protected)</string>
<string name="tcp_fastopen_summary">Toggling requires ROOT permission</string> <string name="tcp_fastopen_summary">Toggling requires ROOT permission</string>
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string> <string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string>
<string name="udp_dns">DNS Forwarding</string> <string name="udp_dns">DNS Forwarding</string>
<string name="udp_dns_summary">Forward all DNS requests to remote</string> <string name="udp_dns_summary">Forward all DNS requests to remote</string>
<string name="night_mode">Night mode</string>
<string name="night_mode_system">Follow system</string>
<string name="night_mode_auto">Auto</string>
<string name="night_mode_on">On</string>
<string name="night_mode_off">Off</string>
<!-- notification category --> <!-- notification category -->
<string name="service_vpn">VPN Service</string> <string name="service_vpn">VPN Service</string>
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Translucent" parent="Theme.AppCompat.Dialog"> <style name="Theme.Shadowsocks.App">
<item name="android:windowBackground">@android:color/transparent</item> <item name="bottomSheetDialogTheme">@style/Theme.Design.DayNight.BottomSheetDialog</item>
<item name="android:windowFrame">@null</item> <!-- MaterialDrawer specific values -->
<item name="android:windowContentOverlay">@null</item> <item name="material_drawer_background">@color/drawer_background</item>
<item name="android:windowAnimationStyle">@null</item> <item name="material_drawer_primary_text">@color/drawer_primary_text</item>
<item name="android:backgroundDimEnabled">false</item> <item name="material_drawer_primary_icon">@color/drawer_primary_icon</item>
<item name="android:windowIsTranslucent">true</item> <item name="material_drawer_secondary_text">@color/drawer_secondary_text</item>
<item name="android:windowNoTitle">true</item> <item name="material_drawer_hint_text">@color/drawer_hint_text</item>
<item name="android:windowCloseOnTouchOutside">false</item> <item name="material_drawer_divider">@color/drawer_divider</item>
<item name="material_drawer_selected">@color/drawer_selected</item>
<item name="material_drawer_selected_text">@color/drawer_selected_text</item>
<item name="material_drawer_header_selection_text">@color/drawer_header_selection_text</item>
</style> </style>
<style name="Theme.Design.DayNight.BottomSheetDialog" parent="@style/Theme.Design.Light.BottomSheetDialog"/>
<style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance"> <style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">24sp</item> <item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
......
...@@ -7,12 +7,18 @@ ...@@ -7,12 +7,18 @@
android:title="@string/auto_connect"/> android:title="@string/auto_connect"/>
<!-- Direct Boot Aware alone doesn't do anything without auto connect --> <!-- Direct Boot Aware alone doesn't do anything without auto connect -->
<SwitchPreference android:key="directBootAware" <SwitchPreference android:key="directBootAware"
android:dependency="isAutoConnect"
android:summary="@string/direct_boot_aware_summary" android:summary="@string/direct_boot_aware_summary"
android:title="@string/direct_boot_aware"/> android:title="@string/direct_boot_aware"/>
<SwitchPreference android:key="tcp_fastopen" <SwitchPreference android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary" android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/> android:title="TCP Fast Open"/>
<SimpleMenuPreference
android:key="nightMode"
android:entries="@array/night_modes"
android:entryValues="@array/night_mode_values"
android:defaultValue="system"
android:summary="%s"
android:title="@string/night_mode"/>
<PreferenceCategory <PreferenceCategory
android:title="@string/advanced"> android:title="@string/advanced">
<SimpleMenuPreference <SimpleMenuPreference
......
...@@ -29,7 +29,8 @@ class KeyValuePairTest { ...@@ -29,7 +29,8 @@ class KeyValuePairTest {
val kvp = KeyValuePair() val kvp = KeyValuePair()
Assert.assertEquals(true, kvp.put(true).boolean) Assert.assertEquals(true, kvp.put(true).boolean)
Assert.assertEquals(3f, kvp.put(3f).float) Assert.assertEquals(3f, kvp.put(3f).float)
Assert.assertEquals(3, kvp.put(3).int) @Suppress("DEPRECATION")
Assert.assertEquals(3L, kvp.put(3).long)
Assert.assertEquals(3L, kvp.put(3L).long) Assert.assertEquals(3L, kvp.put(3L).long)
Assert.assertEquals("3", kvp.put("3").string) Assert.assertEquals("3", kvp.put("3").string)
val set = (0 until 3).map(Int::toString).toSet() val set = (0 until 3).map(Int::toString).toSet()
......
* 0.1.1:
* Rewritten in Kotlin;
* Fix assert not working;
* Min API 21;
* Update support library version to 27.1.1.
* 0.0.4: * 0.0.4:
* Enlarge text size of number pickers; * Enlarge text size of number pickers;
* Update support library version to 26.0.0. * Update support library version to 26.0.0.
......
...@@ -10,8 +10,8 @@ android { ...@@ -10,8 +10,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion rootProject.minSdkVersion minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion targetSdkVersion rootProject.sdkVersion
versionCode 5 versionCode 6
versionName "0.1.0" versionName "0.1.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
...@@ -26,7 +26,7 @@ android { ...@@ -26,7 +26,7 @@ android {
dependencies { dependencies {
api "com.android.support:preference-v14:$supportLibraryVersion" api "com.android.support:preference-v14:$supportLibraryVersion"
api "com.takisoft.fix:preference-v7:$takisoftFixVersion" api "com.takisoft.fix:preference-v7:$takisoftFixVersion"
api "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion" api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion" androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion" androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion"
......
...@@ -80,6 +80,8 @@ If the plugin doesn't provide a configuration activity, it's highly recommended ...@@ -80,6 +80,8 @@ If the plugin doesn't provide a configuration activity, it's highly recommended
* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`; * MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`;
* CAN parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` and display some more * CAN parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` and display some more
relevant information; relevant information;
* SHOULD parse `@NightMode` int extra `com.github.shadowsocks.plugin.EXTRA_NIGHT_MODE` and act
accordingly;
* SHOULD either: * SHOULD either:
- Be invisible and return help message with CharSequence extra - Be invisible and return help message with CharSequence extra
`com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE` in the data intent with `RESULT_OK`; (in this `com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE` in the data intent with `RESULT_OK`; (in this
...@@ -153,7 +155,8 @@ This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the ...@@ -153,7 +155,8 @@ This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the
... ...
<provider android:name=".BinaryProvider" <provider android:name=".BinaryProvider"
android:exported="true" android:exported="true"
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER"> android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER"
tools:ignore="ExportedContentProvider">
<intent-filter> <intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/> <action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
</intent-filter> </intent-filter>
......
...@@ -61,7 +61,7 @@ abstract class NativePluginProvider : ContentProvider() { ...@@ -61,7 +61,7 @@ abstract class NativePluginProvider : ContentProvider() {
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
sortOrder: String?): Cursor { sortOrder: String?): Cursor {
assert(selection == null && selectionArgs == null && sortOrder == null) check(selection == null && selectionArgs == null && sortOrder == null)
val result = MatrixCursor(projection) val result = MatrixCursor(projection)
populateFiles(PathProvider(uri, result)) populateFiles(PathProvider(uri, result))
return result return result
...@@ -79,7 +79,7 @@ abstract class NativePluginProvider : ContentProvider() { ...@@ -79,7 +79,7 @@ abstract class NativePluginProvider : ContentProvider() {
abstract fun openFile(uri: Uri?): ParcelFileDescriptor abstract fun openFile(uri: Uri?): ParcelFileDescriptor
override fun openFile(uri: Uri?, mode: String?): ParcelFileDescriptor { override fun openFile(uri: Uri?, mode: String?): ParcelFileDescriptor {
assert(mode == "r") check(mode == "r")
return openFile(uri) return openFile(uri)
} }
......
...@@ -23,6 +23,7 @@ package com.github.shadowsocks.plugin ...@@ -23,6 +23,7 @@ package com.github.shadowsocks.plugin
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.app.AppCompatDelegate
import android.widget.Toast import android.widget.Toast
/** /**
...@@ -43,6 +44,13 @@ abstract class OptionsCapableActivity : AppCompatActivity() { ...@@ -43,6 +44,13 @@ abstract class OptionsCapableActivity : AppCompatActivity() {
*/ */
protected abstract fun onInitializePluginOptions(options: PluginOptions = pluginOptions()) protected abstract fun onInitializePluginOptions(options: PluginOptions = pluginOptions())
override fun onCreate(savedInstanceState: Bundle?) {
val nightMode = intent.getIntExtra(PluginContract.EXTRA_NIGHT_MODE, -100) // MODE_NIGHT_UNSPECIFIED
if (nightMode >= AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && nightMode <= AppCompatDelegate.MODE_NIGHT_YES)
AppCompatDelegate.setDefaultNightMode(nightMode)
super.onCreate(savedInstanceState) // applyDayNight is called in AppCompatActivity.onCreate
}
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
if (savedInstanceState == null) onInitializePluginOptions() if (savedInstanceState == null) onInitializePluginOptions()
......
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
package com.github.shadowsocks.plugin package com.github.shadowsocks.plugin
import android.support.v7.app.AppCompatDelegate
/** /**
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc. * The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
* *
...@@ -71,6 +73,13 @@ object PluginContract { ...@@ -71,6 +73,13 @@ object PluginContract {
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE" * Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
</host_name> */ </host_name> */
const val EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE" const val EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
/**
* The lookup key for an @NightMode int that suggests a night mode that is being used in the main app.
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_NIGHT_MODE"
*/
@AppCompatDelegate.NightMode
const val EXTRA_NIGHT_MODE = "com.github.shadowsocks.plugin.EXTRA_NIGHT_MODE"
/** /**
* The metadata key to retrieve plugin id. Required for plugins. * The metadata key to retrieve plugin id. Required for plugins.
......
...@@ -8,5 +8,5 @@ ...@@ -8,5 +8,5 @@
android:elevation="4dp" android:elevation="4dp"
android:touchscreenBlocksFocus="false" android:touchscreenBlocksFocus="false"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
android:id="@+id/toolbar" /> android:id="@+id/toolbar" />
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="proxy_cat">"Paramètres du Serveur"</string>
<string name="feature_cat">"Paramètres des Fonctionnalités"</string>
<string name="yes">"Oui"</string>
<string name="no">"Non"</string>
<string name="apply">"Appliquer"</string>
</resources> </resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- proxy category -->
<string name="proxy_cat">"서버 설정"</string> <string name="proxy_cat">"서버 설정"</string>
<!-- feature category -->
<string name="feature_cat">"기능 설정"</string> <string name="feature_cat">"기능 설정"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<!-- alert category -->
<string name="yes">"예"</string> <string name="yes">"예"</string>
<string name="no">"아니오"</string> <string name="no">"아니오"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="apply">"적용"</string> <string name="apply">"적용"</string>
</resources> </resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="color_primary">@color/material_primary_800</color>
<color name="color_primary_dark">@color/material_primary_900</color>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ThemeOverlay.AppCompat.DayNight" parent="ThemeOverlay.AppCompat.Dark"/>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- proxy category -->
<string name="proxy_cat">"伺服器設定"</string> <string name="proxy_cat">"伺服器設定"</string>
<!-- feature category -->
<string name="feature_cat">"功能設定"</string> <string name="feature_cat">"功能設定"</string>
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<!-- alert category -->
<string name="yes">"是"</string> <string name="yes">"是"</string>
<string name="no">"否"</string> <string name="no">"否"</string>
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<string name="apply">"套用"</string> <string name="apply">"套用"</string>
</resources> </resources>
\ No newline at end of file
...@@ -7,11 +7,18 @@ ...@@ -7,11 +7,18 @@
<color name="material_blue_grey_100">#CFD8DC</color> <color name="material_blue_grey_100">#CFD8DC</color>
<color name="material_blue_grey_300">#90A4AE</color> <color name="material_blue_grey_300">#90A4AE</color>
<color name="material_blue_grey_500">#607D8B</color> <color name="material_blue_grey_500">#607D8B</color>
<color name="material_blue_grey_600">#546E7A</color>
<color name="material_blue_grey_700">#455A64</color> <color name="material_blue_grey_700">#455A64</color>
<color name="material_primary_100">@color/material_blue_grey_100</color> <color name="material_primary_100">@color/material_blue_grey_100</color>
<color name="material_primary_300">@color/material_blue_grey_300</color> <color name="material_primary_300">@color/material_blue_grey_300</color>
<color name="material_primary_500">@color/material_blue_grey_500</color> <color name="material_primary_500">@color/material_blue_grey_500</color>
<color name="material_primary_600">@color/material_blue_grey_600</color>
<color name="material_primary_700">@color/material_blue_grey_700</color> <color name="material_primary_700">@color/material_blue_grey_700</color>
<color name="material_primary_800">@color/material_blue_grey_800</color>
<color name="material_primary_900">@color/material_blue_grey_900</color>
<color name="material_accent_200">@color/material_green_a700</color> <color name="material_accent_200">@color/material_green_a700</color>
<color name="color_primary">@color/material_blue_grey_500</color>
<color name="color_primary_dark">@color/material_blue_grey_700</color>
</resources> </resources>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Shadowsocks" parent="PreferenceFixTheme.Light.NoActionBar"> <style name="Theme.Shadowsocks" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:navigationBarColor">@color/material_primary_700</item> <item name="android:navigationBarColor">@color/color_primary_dark</item>
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item> <item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
<item name="colorAccent">@color/material_accent_200</item> <item name="colorAccent">@color/material_accent_200</item>
<item name="colorButtonNormal">@color/material_accent_200</item> <item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item> <item name="colorPrimary">@color/color_primary</item>
<item name="colorPrimaryDark">@color/material_primary_700</item> <item name="colorPrimaryDark">@color/color_primary_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.Fix</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.Fix</item>
<item name="preferenceCategory_marginBottom">8dp</item> <item name="preferenceCategory_marginBottom">8dp</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
</style> </style>
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowCloseOnTouchOutside">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="ThemeOverlay.AppCompat.DayNight" parent="ThemeOverlay.AppCompat.Light"/>
</resources> </resources>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment