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
3. Some ROM like [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) has **very** broken direct boot support;
4. If you have Xposed framework and/or battery saver apps, it's likely that this app wouldn't work well with these either.
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#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)
* 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)
* 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)
* `INTERACT_ACROSS_USERS` permission missing: [#1184](https://github.com/shadowsocks/shadowsocks-android/issues/1184)
### How to pause Shadowsocks service?
......
......@@ -12,11 +12,6 @@
Использйте интеграцию с [Tasker](http://tasker.dinglisch.net/).
### Как добавить QR код из локальной галереи?
Отсканируйте его при помощи стороннего сканера, например [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) и нажмите на `ss` ссылку.
### Почему режим NAT более не поддерживается?
1. Требуются ROOT права;
......@@ -34,10 +29,12 @@
3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки";
4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
* Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Исправления для 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)
* Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* 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)
### Как приостановить службу Shadowsocks?
......@@ -58,7 +55,12 @@
Разрешите этому приложению передачу данных в фоновом режиме в настройках приложения.
Если у вас оперератор Yota, то измените точку доступа APN с `yota.ru` на `client.yota.ru`
Если у вас оперератор Yota, попробуйте следующее:
* измените точку доступа APN с `yota.ru` на `client.yota.ru`
* погоняйте программы: Speedtest, Telegram (скачивание файлов)
Я уже уведомил оператора о проблеме и вместе мы стараемся решить её.
### Почему требуется разрешение камере на устройствах младше 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.
### 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`
* (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`
* 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
### TRANSLATE
......
......@@ -4,17 +4,17 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
kotlinVersion = '1.2.30'
kotlinVersion = '1.2.41'
minSdkVersion = 21
sdkVersion = 'P'
compileSdkVersion = 'android-P'
buildToolsVersion = '28-rc1'
supportLibraryVersion = '28.0.0-alpha1'
takisoftFixVersion = '27.1.0.0'
playServicesVersion = '11.8.0'
sdkVersion = 28
compileSdkVersion = 28
buildToolsVersion = '28.0.0'
supportLibraryVersion = '28.0.0-alpha3'
takisoftFixVersion = '27.1.1.1'
roomVersion = '1.1.0'
junitVersion = '4.12'
androidTestVersion = '1.0.1'
androidEspressoVersion = '3.0.1'
androidTestVersion = '1.0.2'
androidEspressoVersion = '3.0.2'
}
repositories {
......@@ -23,9 +23,9 @@ buildscript {
}
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.google.gms:google-services:3.2.0'
classpath 'com.google.gms:google-services:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
......
......@@ -38,7 +38,7 @@ android {
task goBuild(type: Exec) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
executable "cmd.exe"
args "/c", "src\\overture\\make.bat " + minSdkVersion
args "/c", file("src/overture/make.bat").absolutePath , minSdkVersion
} else {
executable "sh"
args "-c", "src/overture/make.bash " + minSdkVersion
......@@ -48,7 +48,7 @@ task goBuild(type: Exec) {
task goClean(type: Exec) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
executable "cmd.exe"
args "/c", "src\\overture\\clean.bat"
args "/c", file("src/overture/clean.bat").absolutePath
} else {
executable "sh"
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
MKDIR %TARGET%\x86>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% (
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%
)
IF NOT EXIST %ANDROID_ARM64_CC% (
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%
)
IF NOT EXIST %ANDROID_X86_CC% (
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%
)
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 (
ECHO "Build the custom go"
......@@ -118,4 +132,4 @@ IF %BUILD% == 1 (
)
ECHO "Successfully build overture"
ENDLOCAL
\ No newline at end of file
ENDLOCAL
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:
active: false
......@@ -7,7 +7,7 @@ complexity:
active: true
ComplexCondition:
active: true
threshold: 3
threshold: 4
ComplexInterface:
active: true
threshold: 10
......@@ -15,6 +15,7 @@ complexity:
ComplexMethod:
active: true
threshold: 10
ignoreSingleWhenExpression: false
LabeledExpression:
active: true
LargeClass:
......@@ -25,31 +26,32 @@ complexity:
threshold: 20
LongParameterList:
active: true
threshold: 5
threshold: 6
ignoreDefaultParameters: true
MethodOverloading:
active: false
NestedBlockDepth:
active: true
threshold: 3
threshold: 4
StringLiteralDuplication:
active: true
threshold: 2
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
thresholdInFiles: 10
thresholdInClasses: 10
thresholdInInterfaces: 10
thresholdInObjects: 10
thresholdInEnums: 10
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
empty-blocks:
active: true
EmptyCatchBlock:
active: false
active: true
allowedExceptionNameRegex: "^(_|ignore|expected).*"
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
......@@ -64,6 +66,7 @@ empty-blocks:
active: true
EmptyFunctionBlock:
active: true
ignoreOverriddenFunctions: false
EmptyIfBlock:
active: true
EmptyInitBlock:
......@@ -105,7 +108,7 @@ exceptions:
active: true
TooGenericExceptionCaught:
active: true
exceptions:
exceptionNames:
- ArrayIndexOutOfBoundsException
- Error
- Exception
......@@ -116,10 +119,9 @@ exceptions:
- Throwable
TooGenericExceptionThrown:
active: true
exceptions:
exceptionNames:
- Error
- Exception
- NullPointerException
- Throwable
- RuntimeException
......@@ -143,6 +145,7 @@ naming:
FunctionNaming:
active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^'
MatchingDeclarationName:
active: true
MemberNameEqualsClassName:
......@@ -167,6 +170,7 @@ naming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
performance:
active: true
......@@ -239,15 +243,17 @@ style:
maxLineLength: 120
excludePackageStatements: false
excludeImportStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
OptionalAbstractKeyword:
NoTabs:
active: true
OptionalReturnKeyword:
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: true
......@@ -268,6 +274,8 @@ style:
ThrowsCount:
active: true
max: 2
TrailingWhitespace:
active: true
UnnecessaryAbstractClass:
active: true
UnnecessaryInheritance:
......@@ -278,6 +286,8 @@ style:
active: true
UnusedImports:
active: true
UnusedPrivateMember:
active: true
UseDataClass:
active: false
UtilityClassWithPublicConstructor:
......
......@@ -4,6 +4,7 @@ import java.util.regex.Pattern
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
def getCurrentFlavor() {
String task = getGradle().getStartParameter().getTaskRequests().toString()
......@@ -21,11 +22,16 @@ android {
applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 4050100
versionName "4.5.1"
versionCode 4060000
versionName "4.6.0"
testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
debug {
......@@ -54,25 +60,27 @@ android {
dependencies {
implementation project(':core')
implementation "android.arch.persistence.room:runtime:$roomVersion"
implementation "com.android.support:customtabs:$supportLibraryVersion"
implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:gridlayout-v7:$supportLibraryVersion"
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
implementation 'com.evernote:android-job:1.2.4'
implementation "com.google.android.gms:play-services-ads:$playServicesVersion"
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
implementation "com.google.android.gms:play-services-vision:$playServicesVersion"
implementation "com.google.firebase:firebase-config:$playServicesVersion"
implementation 'com.j256.ormlite:ormlite-android:5.1'
implementation 'com.mikepenz:materialdrawer:6.0.6'
implementation 'com.evernote:android-job:1.2.6'
implementation "com.google.android.gms:play-services-ads:15.0.1"
implementation "com.google.android.gms:play-services-analytics:16.0.0"
implementation "com.google.android.gms:play-services-vision:15.0.2"
implementation "com.google.firebase:firebase-config:16.0.0"
implementation 'com.google.firebase:firebase-core:16.0.0'
implementation 'com.mikepenz:materialdrawer:6.0.7'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation "com.takisoft.fix:preference-v7-simplemenu:$takisoftFixVersion"
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2'
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'eu.chainfire:libsuperuser:1.0.0.201704021214'
implementation 'net.glxn.qrgen:android:2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
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"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
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 @@
android:fullBackupContent="@xml/backup_descriptor"
android:fullBackupOnly="true"
android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.App"
android:supportsRtl="true"
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"
android:value="true" />
......@@ -72,7 +74,7 @@
<activity
android:name=".VpnRequestActivity"
android:theme="@style/Theme.Translucent"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
......
......@@ -2,7 +2,7 @@ package com.github.shadowsocks.aidl;
interface IShadowsocksServiceCallback {
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
oneway void trafficPersisted(int profileId);
oneway void trafficPersisted(long profileId);
}
This diff is collapsed.
This diff is collapsed.
<!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 @@
package com.github.shadowsocks
import android.annotation.TargetApi
import android.os.Build
import android.os.Bundle
import android.support.v4.content.ContextCompat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
......@@ -31,14 +30,20 @@ import android.webkit.WebView
import android.webkit.WebViewClient
class AboutFragment : ToolbarFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_about, container, false)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// 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?) {
super.onViewCreated(view, savedInstanceState)
toolbar.title = getString(R.string.about_title, BuildConfig.VERSION_NAME)
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() {
@Suppress("OverridingDeprecatedMember")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
......@@ -46,7 +51,6 @@ class AboutFragment : ToolbarFragment() {
return true
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest): Boolean {
(activity as MainActivity).launchUrl(request.url)
return true
......
......@@ -23,6 +23,7 @@ package com.github.shadowsocks
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.admin.DevicePolicyManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
......@@ -33,9 +34,8 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.UserManager
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.util.Log
import android.widget.Toast
......@@ -57,7 +57,6 @@ import com.google.android.gms.analytics.StandardExceptionParser
import com.google.android.gms.analytics.Tracker
import com.google.firebase.FirebaseApp
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.j256.ormlite.logger.LocalLog
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat
import java.io.File
import java.io.IOException
......@@ -72,10 +71,16 @@ class App : Application() {
val deviceContext: Context by lazy { if (Build.VERSION.SDK_INT < 24) this else DeviceContext(this) }
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
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 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())
PackageManager.GET_SIGNING_CERTIFICATES else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName,
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() {
val intent = Intent(this, BaseService.serviceClass.java)
......@@ -87,7 +92,7 @@ class App : Application() {
val currentProfile: Profile? get() =
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()
DataStore.profileId = result.id
return result
......@@ -102,7 +107,7 @@ class App : Application() {
fun track(t: Throwable) = track(Thread.currentThread(), t)
fun track(thread: Thread, t: Throwable) {
tracker.send(HitBuilders.ExceptionBuilder()
.setDescription(StandardExceptionParser(this, null).getDescription(thread.name, t))
.setDescription("${exceptionParser.getDescription(thread.name, t)} - ${t.message}")
.setFatal(false)
.build())
t.printStackTrace()
......@@ -111,8 +116,8 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
app = this
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java)
......@@ -139,14 +144,15 @@ class App : Application() {
app.track(e)
}
// handle data restored
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update()
// handle data restored/crash
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))
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
val assetManager = assets
for (dir in arrayOf("acl", "overture"))
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) }
}
} catch (e: IOException) {
......@@ -155,7 +161,7 @@ class App : Application() {
}
DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
}
AppCompatDelegate.setDefaultNightMode(DataStore.nightMode)
updateNotificationChannels()
}
......
......@@ -58,23 +58,21 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager): List<ProxiedApp> {
return synchronized(AppManager) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
cachedApps = null
}
AppManager.instance?.reloadApps()
private fun getApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
cachedApps = null
}
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter { it.packageName != app.packageName &&
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
this.cachedApps = cachedApps
cachedApps
}.map { ProxiedApp(pm, it.applicationInfo, it.packageName) }
}
AppManager.instance?.reloadApps()
}
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter { it.packageName != app.packageName &&
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
this.cachedApps = cachedApps
cachedApps
}.map { ProxiedApp(pm, it.applicationInfo, it.packageName) }
}
private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo,
......@@ -151,7 +149,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
appListView.visibility = View.GONE
fastScroller.visibility = View.GONE
loadingView.visibility = View.VISIBLE
thread {
thread("AppManager-loader") {
val adapter = appListView.adapter as AppsAdapter
do {
appsLoading.set(true)
......
......@@ -20,12 +20,12 @@
package com.github.shadowsocks
import android.app.admin.DevicePolicyManager
import android.os.Build
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.Preference
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
......@@ -39,20 +39,17 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompatDividers() {
DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global)
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 ->
BootReceiver.enabled = value as Boolean
directBootAwareListener.onPreferenceChange(null, DataStore.directBootAware)
true
}
boot.isChecked = BootReceiver.enabled
val dba = findPreference(Key.directBootAware)
if (Build.VERSION.SDK_INT >= 24 && requireContext().getSystemService(DevicePolicyManager::class.java)
.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER)
dba.onPreferenceChangeListener = directBootAwareListener else dba.parent!!.removePreference(dba)
val canToggleLocked = findPreference(Key.directBootAware)
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
if (app.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
true
} else canToggleLocked.parent!!.removePreference(canToggleLocked)
val tfo = findPreference(Key.tfo) as SwitchPreference
tfo.isChecked = TcpFastOpen.sendEnabled
......
......@@ -37,8 +37,10 @@ import android.support.design.widget.Snackbar
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.support.v7.app.AppCompatDelegate
import android.support.v7.content.res.AppCompatResources
import android.support.v7.preference.PreferenceDataStore
import android.text.format.Formatter
import android.util.Log
import android.view.View
import android.widget.TextView
......@@ -49,7 +51,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.Executable
import com.github.shadowsocks.bg.TrafficMonitor
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
......@@ -101,7 +102,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
private val customTabsIntent by lazy {
CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.material_primary_500))
.setToolbarColor(ContextCompat.getColor(this, R.color.color_primary))
.build()
}
fun launchUrl(uri: Uri) = try {
......@@ -116,10 +117,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
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) }
}
override fun trafficPersisted(profileId: Int) {
override fun trafficPersisted(profileId: Long) {
app.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) }
}
}
......@@ -148,11 +149,11 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
stateListener?.invoke(state)
}
fun updateTraffic(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.text = TrafficMonitor.formatTraffic(txTotal)
rxText.text = TrafficMonitor.formatTraffic(rxTotal)
txRateText.text = getString(R.string.speed, TrafficMonitor.formatTraffic(txRate))
rxRateText.text = getString(R.string.speed, TrafficMonitor.formatTraffic(rxRate))
fun updateTraffic(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.text = Formatter.formatFileSize(this, txTotal)
rxText.text = Formatter.formatFileSize(this, rxTotal)
txRateText.text = getString(R.string.speed, Formatter.formatFileSize(this, txRate))
rxRateText.text = getString(R.string.speed, Formatter.formatFileSize(this, rxRate))
val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment?
if (state != BaseService.STOPPING)
child?.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal)
......@@ -266,18 +267,20 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
++testCount
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
thread { testConnection(id) }
thread("ConnectionTest") { testConnection(id) }
}
}
fab = findViewById(R.id.fab)
fab.setOnClickListener {
if (state == BaseService.CONNECTED) app.stopService() else thread {
if (BaseService.usingVpnMode) {
when {
state == BaseService.CONNECTED -> app.stopService()
BaseService.usingVpnMode -> {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else app.handler.post { onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null) }
} else app.startService()
else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
}
else -> app.startService()
}
}
......@@ -319,9 +322,15 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
if (key == Key.serviceMode) app.handler.post {
connection.disconnect()
connection.connect()
when (key) {
Key.serviceMode -> app.handler.post {
connection.disconnect()
connection.connect()
}
Key.nightMode -> {
AppCompatDelegate.setDefaultNightMode(DataStore.nightMode)
recreate()
}
}
}
......
......@@ -56,7 +56,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
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 plugin: IconListPreference
private lateinit var pluginConfigure: EditTextPreference
......@@ -66,13 +66,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity()
val profile = ProfileManager.getProfile(activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1))
if (profile == null) {
activity.finish()
return
}
this.profile = profile
profile.serialize()
profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L)
addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) {
findPreference(Key.host).summary = "shadowsocks.example.org"
......@@ -130,10 +124,12 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
}
fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile()
profile.id = profileId
profile.deserialize()
ProfileManager.updateProfile(profile)
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profile.id)
if (DataStore.profileId == profile.id && DataStore.directBootAware) DirectBoot.update()
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profileId)
if (DataStore.profileId == profileId && DataStore.directBootAware) DirectBoot.update()
requireActivity().finish()
}
......@@ -161,11 +157,11 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference.key == Key.pluginConfigure) {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) != null)
startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS,
pluginConfiguration.selectedOptions.toString()), REQUEST_CODE_PLUGIN_CONFIGURE) else {
showPluginEditor()
}
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else
startActivityForResult(intent
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString())
.putExtra(PluginContract.EXTRA_NIGHT_MODE, DataStore.nightMode),
REQUEST_CODE_PLUGIN_CONFIGURE)
} else super.onDisplayPreferenceDialog(preference)
}
......@@ -187,7 +183,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, { _, _ ->
ProfileManager.delProfile(profile.id)
ProfileManager.delProfile(profileId)
activity.finish()
})
.setNegativeButton(R.string.no, null)
......
......@@ -33,13 +33,13 @@ import android.support.design.widget.Snackbar
import android.support.v4.app.DialogFragment
import android.support.v7.widget.*
import android.support.v7.widget.helper.ItemTouchHelper
import android.text.format.Formatter
import android.view.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.TrafficMonitor
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
......@@ -69,7 +69,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
BaseService.CONNECTED, BaseService.STOPPED -> true
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.STOPPED -> true
else -> false
......@@ -124,7 +124,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private var adView: AdView? = null
init {
edit.setOnClickListener { startConfig(item.id) }
edit.setOnClickListener {
item = ProfileManager.getProfile(item.id)!!
startConfig(item)
}
TooltipCompat.setTooltipText(edit, edit.contentDescription)
itemView.setOnClickListener(this)
val share = itemView.findViewById<View>(R.id.share)
......@@ -157,10 +160,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
text2.visibility = View.VISIBLE
text2.text = t2.joinToString("\n")
}
val context = requireContext()
if (tx <= 0 && rx <= 0) traffic.visibility = View.GONE else {
traffic.visibility = View.VISIBLE
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) {
......@@ -177,7 +181,6 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
val params = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER_HORIZONTAL
val context = requireContext()
adView = AdView(context)
adView.layoutParams = params
adView.adUnitId = "ca-app-pub-9097031975646651/7760346322"
......@@ -279,17 +282,17 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
for ((_, item) in actions) ProfileManager.delProfile(item.id)
}
fun refreshId(id: Int) {
fun refreshId(id: Long) {
val index = profiles.indexOfFirst { it.id == id }
if (index >= 0) notifyItemChanged(index)
}
fun deepRefreshId(id: Int) {
fun deepRefreshId(id: Long) {
val index = profiles.indexOfFirst { it.id == id }
if (index < 0) return
profiles[index] = ProfileManager.getProfile(id)!!
notifyItemChanged(index)
}
fun removeId(id: Int) {
fun removeId(id: Long) {
val index = profiles.indexOfFirst { it.id == id }
if (index < 0) return
profiles.removeAt(index)
......@@ -302,14 +305,16 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
val profilesAdapter by lazy { ProfilesAdapter() }
private lateinit var undoManager: UndoSnackbarManager<Profile>
private var bandwidthProfile: Int = 0
private var bandwidthProfile = 0L
private var txTotal: Long = 0L
private var rxTotal: Long = 0L
private val clipboard by lazy { requireContext().systemService<ClipboardManager>() }
private fun startConfig(id: Int) = startActivity(Intent(context, ProfileConfigActivity::class.java)
.putExtra(Action.EXTRA_PROFILE_ID, id))
private fun startConfig(profile: Profile) {
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? =
inflater.inflate(R.layout.layout_list, container, false)
......@@ -320,8 +325,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (ProfileManager.getFirstProfile() == null)
DataStore.profileId = ProfileManager.createProfile().id
if (!ProfileManager.isNotEmpty()) DataStore.profileId = ProfileManager.createProfile().id
val profilesList = view.findViewById<RecyclerView>(R.id.list)
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
profilesList.layoutManager = layoutManager
......@@ -367,20 +371,22 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R.id.action_import -> {
try {
val profiles = Profile.findAll(clipboard.primaryClip.getItemAt(0).text).toList()
val profiles = Profile.findAll(clipboard.primaryClip!!.getItemAt(0).text).toList()
if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) }
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_msg,
Snackbar.LENGTH_LONG).show()
return true
}
} catch (_: IndexOutOfBoundsException) { }
} catch (exc: Exception) {
exc.printStackTrace()
}
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err,
Snackbar.LENGTH_LONG).show()
true
}
R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile().id)
startConfig(ProfileManager.createProfile())
true
}
R.id.action_export -> {
......@@ -395,8 +401,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
}
override fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
if (profileId != -1) { // ignore resets from MainActivity
override fun onTrafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
if (profileId != -1L) { // ignore resets from MainActivity
if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId
......@@ -406,7 +412,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
profilesAdapter.refreshId(profileId)
}
}
fun onTrafficPersisted(profileId: Int) {
fun onTrafficPersisted(profileId: Long) {
txTotal = 0
rxTotal = 0
if (bandwidthProfile != profileId) {
......
......@@ -40,6 +40,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.resolveResourceId
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.BarcodeGraphic
import com.google.android.gms.vision.Frame
......@@ -52,8 +53,11 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
private const val TAG = "ScannerActivity"
private const val REQUEST_IMPORT = 2
private const val REQUEST_IMPORT_OR_FINISH = 3
private const val REQUEST_GOOGLE_API = 4
}
private lateinit var detector: BarcodeDetector
private fun navigateUp() {
val intent = parentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
......@@ -63,6 +67,23 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
override fun onCreate(savedInstanceState: Bundle?) {
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 (try {
systemService<CameraManager>().cameraIdList.isEmpty()
......@@ -79,7 +100,9 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
toolbar.setNavigationOnClickListener { navigateUp() }
toolbar.inflateMenu(R.menu.scanner_menu)
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 {
......@@ -112,25 +135,20 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
var success = false
val detector = BarcodeDetector.Builder(this)
.setBarcodeFormats(Barcode.QR_CODE)
.build()
if (detector.isOperational) {
var list = listOfNotNull(data?.data)
val clipData = data?.clipData
if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri }
val resolver = contentResolver
for (uri in list) try {
val barcodes = detector.detect(Frame.Builder()
.setBitmap(BitmapFactory.decodeStream(resolver.openInputStream(uri))).build())
for (i in 0 until barcodes.size()) Profile.findAll(barcodes.valueAt(i).rawValue).forEach {
ProfileManager.createProfile(it)
success = true
}
} catch (e: Exception) {
app.track(e)
var list = listOfNotNull(data?.data)
val clipData = data?.clipData
if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri }
val resolver = contentResolver
for (uri in list) try {
val barcodes = detector.detect(Frame.Builder()
.setBitmap(BitmapFactory.decodeStream(resolver.openInputStream(uri))).build())
for (i in 0 until barcodes.size()) Profile.findAll(barcodes.valueAt(i).rawValue).forEach {
ProfileManager.createProfile(it)
success = true
}
} 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.LENGTH_SHORT).show()
navigateUp()
......
......@@ -38,7 +38,7 @@ open class ToolbarFragment : Fragment() {
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
}
......@@ -38,13 +38,13 @@ class AclSyncJob(private val route: String) : Job() {
return when (parts[0]) {
TAG -> AclSyncJob(parts[1])
else -> {
Log.w(TAG, "Unknown job tag: " + tag)
Log.w(TAG, "Unknown job tag: $tag")
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))
.setRequirementsEnforced(true)
.setRequiredNetworkType(JobRequest.NetworkType.UNMETERED)
......
......@@ -22,6 +22,7 @@ package com.github.shadowsocks.acl
import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.support.design.widget.Snackbar
......@@ -438,7 +439,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
}
R.id.action_import -> {
try {
adapter.addToProxy(clipboard.primaryClip.getItemAt(0).text.toString()) != null
check(adapter.addToProxy(clipboard.primaryClip!!.getItemAt(0).text.toString()) != null)
} catch (exc: Exception) {
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err,
Snackbar.LENGTH_LONG).show()
......@@ -466,10 +467,16 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
val activity = requireActivity()
val window = activity.window
// In the end material_grey_100 is used for background, see AppCompatDrawableManager (very complicated)
if (Build.VERSION.SDK_INT >= 23) {
window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_300)
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_600)
// for dark mode, it's roughly 850? (#303030)
window.statusBarColor = ContextCompat.getColor(activity, when {
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES ->
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)
toolbar.touchscreenBlocksFocus = true
return true
......
......@@ -49,7 +49,8 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.File
import java.net.UnknownHostException
import java.io.IOException
import java.net.*
import java.security.MessageDigest
import java.util.*
import java.util.concurrent.TimeUnit
......@@ -75,7 +76,7 @@ object BaseService {
@Volatile var state = STOPPED
@Volatile var plugin = PluginOptions()
@Volatile var pluginPath: String? = null
var sslocalProcess: GuardedProcess? = null
val processes = GuardedProcessPool()
var timer: Timer? = null
var trafficMonitorThread: TrafficMonitorThread? = null
......@@ -105,6 +106,7 @@ object BaseService {
val t = Timer(true)
t.schedule(object : TimerTask() {
override fun run() {
val profile = profile ?: return
if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val txRate = TrafficMonitor.txRate
......@@ -115,8 +117,11 @@ object BaseService {
for (i in 0 until n) try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder()))
item.trafficUpdated(profile!!.id, txRate, rxRate, txTotal, rxTotal)
} catch (_: Exception) { } // ignore
item.trafficUpdated(profile.id, txRate, rxRate, txTotal, rxTotal)
} catch (e: Exception) {
e.printStackTrace()
app.track(e)
}
callbacks.finishBroadcast()
}
}
......@@ -145,22 +150,35 @@ object BaseService {
}
internal fun updateTrafficTotal(tx: Long, rx: Long) {
// this.profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
val profile = ProfileManager.getProfile((profile ?: return).id) ?: return
profile.tx += tx
profile.rx += rx
ProfileManager.updateProfile(profile)
app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(profile.id)
} catch (_: Exception) { } // ignore
try {
// this.profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
val profile = ProfileManager.getProfile((profile ?: return).id) ?: return
profile.tx += tx
profile.rx += rx
ProfileManager.updateProfile(profile)
app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(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 {
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg)
} catch (_: Exception) { } // ignore
} catch (e: Exception) {
e.printStackTrace()
app.track(e)
}
callbacks.finishBroadcast()
}
state = s
......@@ -235,6 +256,7 @@ object BaseService {
fun startNativeProcesses() {
val data = data
val profile = data.profile!!
val cmd = buildAdditionalArguments(arrayListOf(
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
"-u",
......@@ -249,9 +271,11 @@ object BaseService {
cmd += acl.absolutePath
}
if (profile.udpdns) cmd += "-D"
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
data.sslocalProcess = GuardedProcess(cmd).start()
data.processes.start(cmd)
}
fun createNotification(profileName: String): ServiceNotification
......@@ -262,11 +286,7 @@ object BaseService {
else startService(Intent(this, javaClass))
}
fun killProcesses() {
val data = data
data.sslocalProcess?.destroy()
data.sslocalProcess = null
}
fun killProcesses() = data.processes.killAll()
fun stopRunner(stopService: Boolean, msg: String? = null) {
// channge the state
......@@ -341,14 +361,10 @@ object BaseService {
data.changeState(CONNECTING)
thread {
thread("$tag-Connecting") {
try {
if (profile.host == "198.199.101.152") {
val client = OkHttpClient.Builder()
.dns {
listOf((Dns.resolve(it, false) ?: throw UnknownHostException())
.parseNumericAddress())
}
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
......@@ -383,7 +399,7 @@ object BaseService {
killProcesses()
if (!profile.host.isNumericAddress())
profile.host = Dns.resolve(profile.host, true) ?: throw UnknownHostException()
profile.host = InetAddress.getByName(profile.host).hostAddress ?: throw UnknownHostException()
startNativeProcesses()
......
......@@ -28,35 +28,40 @@ import com.github.shadowsocks.BuildConfig
import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.thread
import java.io.File
import java.io.IOException
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 {
private const val TAG = "GuardedProcess"
private const val TAG = "GuardedProcessPool"
private val dummy = IOException()
}
private lateinit var guardThread: Thread
@Volatile
private var isDestroyed = false
@Volatile
private lateinit var process: Process
private inner class Guard(private val cmd: List<String>, private val onRestartCallback: (() -> Unit)?) {
val cmdName = File(cmd.first()).nameWithoutExtension
val excQueue = ArrayBlockingQueue<IOException>(1) // ArrayBlockingQueue doesn't want null
private var pushed = false
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = thread {
try {
input.bufferedReader().useLines { it.forEach { logger(TAG, it) } }
} catch (_: IOException) { } // ignore
}
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) =
thread("StreamLogger-$cmdName") {
try {
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 {
val semaphore = Semaphore(1)
semaphore.acquire()
var ioException: IOException? = null
guardThread = thread(name = "GuardThread-" + cmd.first()) {
fun looper(host: HashSet<Thread>) {
var process: Process? = null
try {
var callback: (() -> Unit)? = null
while (!isDestroyed) {
while (guardThreads.get() === host) {
if (BuildConfig.DEBUG) Log.d(TAG, "start process: " + Commandline.toString(cmd))
val startTime = SystemClock.elapsedRealtime()
......@@ -70,44 +75,57 @@ class GuardedProcess(private val cmd: List<String>) {
if (callback == null) callback = onRestartCallback else callback()
semaphore.release()
pushException(null)
process.waitFor()
synchronized(this) {
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd))
isDestroyed = true
}
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Log.w(TAG, "process exit too fast, stop guard: $cmdName")
}
}
} catch (_: InterruptedException) {
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd))
destroyProcess()
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: $cmdName")
} catch (e: IOException) {
ioException = e
pushException(e)
} 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
guardThread.interrupt()
destroyProcess()
try {
guardThread.join()
} catch (_: InterruptedException) { }
/**
* This is an indication of which thread pool is being active.
* Reading/writing this collection still needs an additional lock to prevent concurrent modification.
*/
private val guardThreads = AtomicReference<HashSet<Thread>>(HashSet())
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() {
if (Build.VERSION.SDK_INT < 24) @Suppress("DEPRECATION") {
JniHelper.sigtermCompat(process)
JniHelper.waitForCompat(process, 500)
fun killAll() {
val guardThreads = guardThreads.getAndSet(HashSet())
synchronized(guardThreads) {
guardThreads.forEach { it.interrupt() }
try {
guardThreads.forEach { it.join() }
} catch (_: InterruptedException) { }
}
process.destroy()
}
}
......@@ -28,19 +28,9 @@ import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.net.Inet6Address
import java.util.*
/**
* This object also uses WeakMap to simulate the effects of multi-inheritance, but more lightweight.
*/
object LocalDnsService {
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() {
super.startNativeProcesses()
val data = data
......@@ -72,12 +62,11 @@ object LocalDnsService {
.put("MinimumTTL", 120)
.put("CacheSize", 4096)
val remoteDns = JSONArray(profile.remoteDns.split(",")
.mapIndexed { i, dns -> makeDns("UserDef-" + i,
dns.trim() + ":53", 9) })
.mapIndexed { i, dns -> makeDns("UserDef-$i", dns.trim() + ":53", 12) })
val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "119.29.29.29:53", 3, false),
makeDns("Primary-2", "114.114.114.114:53", 3, false),
makeDns("Primary-3", "208.67.222.222:443", 3, false)
makeDns("Primary-1", "208.67.222.222:443", 9, false),
makeDns("Primary-2", "119.29.29.29:53", 9, false),
makeDns("Primary-3", "114.114.114.114:53", 9, false)
))
when (profile.route) {
......@@ -85,7 +74,6 @@ object LocalDnsService {
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
.put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", data.aclFile!!.absolutePath)
Acl.CHINALIST -> config
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
......@@ -98,18 +86,10 @@ object LocalDnsService {
return file
}
if (!profile.udpdns) overtureProcess = GuardedProcess(buildAdditionalArguments(arrayListOf(
if (!profile.udpdns) data.processes.start(buildAdditionalArguments(arrayListOf(
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath,
"-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
import java.io.File
import java.io.IOException
abstract class LocalSocketListener(protected val tag: String) : Thread() {
abstract class LocalSocketListener(protected val tag: String) : Thread(tag) {
init {
setUncaughtExceptionHandler(app::track)
}
......
......@@ -31,6 +31,7 @@ import android.os.Build
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
import android.text.format.Formatter
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
......@@ -54,16 +55,16 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
private val callback by lazy {
object : IShadowsocksServiceCallback.Stub() {
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
val txr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(txRate))
val rxr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(rxRate))
val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, txRate))
val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, rxRate))
builder.setContentText("$txr↑\t$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()
}
override fun trafficPersisted(profileId: Int) { }
override fun trafficPersisted(profileId: Long) { }
}
}
private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
......
......@@ -64,8 +64,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
tile.label = label ?: getString(R.string.app_name)
tile.updateTile()
}
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
override fun trafficPersisted(profileId: Int) { }
override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
override fun trafficPersisted(profileId: Long) { }
}
}
......@@ -82,7 +82,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
}
override fun onClick() {
if (isLocked && !DataStore.directBootAware) unlockAndRun(this::toggle) else toggle()
if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()
}
private fun toggle() {
......
......@@ -21,9 +21,6 @@
package com.github.shadowsocks.bg
import android.os.SystemClock
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import java.text.DecimalFormat
object TrafficMonitor {
// Bytes per second
......@@ -41,19 +38,6 @@ object TrafficMonitor {
@Volatile
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 {
val now = SystemClock.elapsedRealtime()
val delta = now - timestampLast
......
......@@ -40,18 +40,14 @@ class TransproxyService : Service(), LocalDnsService.Interface {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
private var sstunnelProcess: GuardedProcess? = null
private var redsocksProcess: GuardedProcess? = null
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",
"-b", "127.0.0.1",
"-u",
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
"-L", data.profile!!.remoteDns.split(",").first().trim() + ":53",
"-c", data.shadowsocksConfigFile!!.absolutePath) // config is already built by BaseService.Interface
sstunnelProcess = GuardedProcess(cmd).start()
"-c", data.shadowsocksConfigFile!!.absolutePath)) // config is already built by BaseService.Interface
}
private fun startRedsocksDaemon() {
......@@ -70,10 +66,8 @@ redsocks {
type = socks5;
}
""")
redsocksProcess = GuardedProcess(arrayListOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
"-c", "redsocks.conf")
).start()
data.processes.start(listOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
}
override fun startNativeProcesses() {
......@@ -81,12 +75,4 @@ redsocks {
super.startNativeProcesses()
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.*
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.support.v4.os.BuildCompat
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper
......@@ -50,13 +49,14 @@ import android.net.VpnService as BaseVpnService
class VpnService : BaseVpnService(), LocalDnsService.Interface {
companion object {
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 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
* satisfies default network capabilities but only THE default network. Unfortunately we need to have
......@@ -77,13 +77,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
var success = false
try {
socket.inputStream.read()
val fds = socket.ancillaryFileDescriptors
if (fds.isEmpty()) return
val fd = getInt.invoke(fds.first()) as Int
success = protect(fd)
JniHelper.close(fd) // Trick to close file decriptor
val fd = socket.ancillaryFileDescriptors!!.single()!!
val fdInt = getInt.invoke(fd) as Int
try {
val network = underlyingNetwork
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) {
Log.e(tag, "Error when protect socket", e)
Log.e(tag, "Error when receiving ancillary fd", e)
app.track(e)
}
try {
......@@ -106,10 +115,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var conn: ParcelFileDescriptor? = 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>() }
@TargetApi(Build.VERSION_CODES.P)
@TargetApi(28)
private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
setUnderlyingNetworks(arrayOf(network))
......@@ -132,15 +146,13 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun onRevoke() = stopRunner(true)
override fun killProcesses() {
if (BuildCompat.isAtLeastP() && listeningForDefaultNetwork) {
if (listeningForDefaultNetwork) {
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
listeningForDefaultNetwork = false
}
worker?.stopThread()
worker = null
super.killProcesses()
tun2socksProcess?.destroy()
tun2socksProcess = null
conn?.close()
conn = null
}
......@@ -217,8 +229,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
this.conn = conn
val fd = conn.fd
// We only need to update since Android P: https://android.googlesource.com/platform/frameworks/base/+/72f9c42b9e59761a28d6b32c42f65de57c98daed
if (BuildCompat.isAtLeastP()) {
if (Build.VERSION.SDK_INT >= 28) {
// we want REQUEST here instead of LISTEN
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
listeningForDefaultNetwork = true
......@@ -241,7 +252,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}"
}
tun2socksProcess = GuardedProcess(cmd).start { sendFd(fd) }
data.processes.start(cmd) { sendFd(fd) }
return fd
}
......
......@@ -20,37 +20,54 @@
package com.github.shadowsocks.database
import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField
import android.arch.persistence.room.*
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
@Entity
class KeyValuePair() {
companion object {
const val TYPE_UNINITIALIZED = 0
const val TYPE_BOOLEAN = 1
const val TYPE_FLOAT = 2
@Deprecated("Use TYPE_LONG.")
const val TYPE_INT = 3
const val TYPE_LONG = 4
const val TYPE_STRING = 5
const val TYPE_STRING_SET = 6
}
@DatabaseField(id = true)
var key: String? = ""
@DatabaseField
@android.arch.persistence.room.Dao
interface Dao {
@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
@DatabaseField(dataType = DataType.BYTE_ARRAY)
var value: ByteArray = ByteArray(0)
val boolean: Boolean?
get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null
val float: Float?
get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null
@Suppress("DEPRECATION")
@Deprecated("Use long.", ReplaceWith("long"))
val int: Int?
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
val long: Long?
get() = if (valueType == TYPE_LONG) ByteBuffer.wrap(value).long else null
val long: Long? get() = when (valueType) {
@Suppress("DEPRECATION")
TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
TYPE_LONG -> ByteBuffer.wrap(value).long
else -> null
}
val string: String?
get() = if (valueType == TYPE_STRING) String(value) else null
val stringSet: Set<String>?
......@@ -65,7 +82,8 @@ class KeyValuePair() {
result
} else null
constructor(key: String?) : this() {
@Ignore
constructor(key: String) : this() {
this.key = key
}
......@@ -80,6 +98,8 @@ class KeyValuePair() {
this.value = ByteBuffer.allocate(4).putFloat(value).array()
return this
}
@Suppress("DEPRECATION")
@Deprecated("Use long.")
fun put(value: Int): KeyValuePair {
valueType = TYPE_INT
this.value = ByteBuffer.allocate(4).putInt(value).array()
......
......@@ -20,137 +20,39 @@
package com.github.shadowsocks.database
import android.database.sqlite.SQLiteDatabase
import android.support.v7.preference.PreferenceManager
import android.text.TextUtils
import android.arch.persistence.db.SupportSQLiteDatabase
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.database.migration.RecreateSchemaMigration
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) {
@Suppress("UNCHECKED_CAST")
val profileDao: Dao<Profile, Int> by lazy { getDao(Profile::class.java) as Dao<Profile, Int> }
@Suppress("UNCHECKED_CAST")
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> }
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
TableUtils.createTable(connectionSource, Profile::class.java)
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
}
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
@Database(entities = [Profile::class, KeyValuePair::class], version = 26)
abstract class PrivateDatabase : RoomDatabase() {
companion object {
private val instance by lazy {
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
.addMigrations(
Migration26
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
try {
if (oldVersion < 8) {
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)
}
val profileDao get() = instance.profileDao()
val kvPairDao get() = instance.keyValuePairDao()
}
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
val connection = AndroidDatabaseConnection(db, true)
connectionSource.saveSpecialConnection(connection)
recreate(db, connectionSource)
connectionSource.clearSpecialConnection(connection)
abstract fun profileDao(): Profile.Dao
abstract fun keyValuePairDao(): KeyValuePair.Dao
private object Migration26 : RecreateSchemaMigration(25, 26, "Profile",
"(`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)",
"`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 @@
package com.github.shadowsocks.database
import android.arch.persistence.room.*
import android.net.Uri
import android.util.Base64
import android.util.Log
......@@ -27,12 +28,12 @@ import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort
import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField
import java.io.Serializable
import java.net.URI
import java.net.URISyntaxException
import java.util.*
@Entity
class Profile : Serializable {
companion object {
private const val TAG = "ShadowParser"
......@@ -43,99 +44,101 @@ class Profile : Serializable {
fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map {
val uri = Uri.parse(it.value)
if (uri.userInfo == null) {
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
if (match != null) {
val profile = Profile()
profile.method = match.groupValues[1].toLowerCase()
profile.password = match.groupValues[2]
profile.host = match.groupValues[3]
profile.remotePort = match.groupValues[4].toInt()
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment
profile
try {
if (uri.userInfo == null) {
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
if (match != null) {
val profile = Profile()
profile.method = match.groupValues[1].toLowerCase()
profile.password = match.groupValues[2]
profile.host = match.groupValues[3]
profile.remotePort = match.groupValues[4].toInt()
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment
profile
} else {
Log.e(TAG, "Unrecognized URI: ${it.value}")
null
}
} else {
Log.e(TAG, "Unrecognized URI: ${it.value}")
null
}
} else {
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
if (match != null) {
val profile = Profile()
profile.method = match.groupValues[1]
profile.password = match.groupValues[2]
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
val javaURI = URI(it.value)
profile.host = javaURI.host
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
profile.host = profile.host.substring(1, profile.host.length - 1)
profile.remotePort = javaURI.port
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment ?: ""
profile
} else {
Log.e(TAG, "Unknown user info: ${it.value}")
null
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
if (match != null) {
val profile = Profile()
profile.method = match.groupValues[1]
profile.password = match.groupValues[2]
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
try {
val javaURI = URI(it.value)
profile.host = javaURI.host ?: ""
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
profile.host = profile.host.substring(1, profile.host.length - 1)
profile.remotePort = javaURI.port
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment ?: ""
profile
} catch (e: URISyntaxException) {
Log.e(TAG, "Invalid URI: ${it.value}")
null
}
} else {
Log.e(TAG, "Unknown user info: ${it.value}")
null
}
}
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid base64 detected: ${it.value}")
null
}
}.filterNotNull()
}
@DatabaseField(generatedId = true)
var id: Int = 0
@android.arch.persistence.room.Dao
interface Dao {
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
operator fun get(id: Long): Profile?
@DatabaseField
var name: String? = ""
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
fun list(): List<Profile>
@DatabaseField
var host: String = "198.199.101.152"
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
fun nextOrder(): Long?
@DatabaseField
var remotePort: Int = 8388
@Query("SELECT 1 FROM `Profile` LIMIT 1")
fun isNotEmpty(): Boolean
@DatabaseField
var password: String = "u1rRWTssNv0p"
@Insert
fun create(value: Profile): Long
@DatabaseField
var method: String = "aes-256-cfb"
@Update
fun update(value: Profile): Int
@DatabaseField
var route: String = "all"
@Query("DELETE FROM `Profile` WHERE `id` = :id")
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"
@DatabaseField
var proxyApps: Boolean = false
@DatabaseField
var bypass: Boolean = false
@DatabaseField
var udpdns: Boolean = false
@DatabaseField
var ipv6: Boolean = true
@DatabaseField(dataType = DataType.LONG_STRING)
var individual: String = ""
@DatabaseField
var tx: Long = 0
@DatabaseField
var rx: Long = 0
@DatabaseField
val date: Date = Date()
@DatabaseField
var userOrder: Long = 0
@DatabaseField
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 formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
......
......@@ -20,11 +20,13 @@
package com.github.shadowsocks.database
import android.database.sqlite.SQLiteCantOpenDatabaseException
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import java.io.IOException
import java.sql.SQLException
/**
......@@ -48,10 +50,8 @@ object ProfileManager {
profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns
}
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
PrivateDatabase.profileDao.createOrUpdate(profile)
profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0
profile.id = PrivateDatabase.profileDao.create(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile)
return profile
}
......@@ -60,34 +60,40 @@ object ProfileManager {
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
*/
@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 {
PrivateDatabase.profileDao.queryForId(id)
@Throws(IOException::class)
fun getProfile(id: Long): Profile? = try {
PrivateDatabase.profileDao[id]
} catch (ex: SQLException) {
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "getProfile", ex)
app.track(ex)
null
}
@Throws(SQLException::class)
fun delProfile(id: Int) {
PrivateDatabase.profileDao.deleteById(id)
fun delProfile(id: Long) {
check(PrivateDatabase.profileDao.delete(id) == 1)
ProfilesFragment.instance?.profilesAdapter?.removeId(id)
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
}
fun getFirstProfile(): Profile? = try {
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull()
@Throws(IOException::class)
fun isNotEmpty(): Boolean = try {
PrivateDatabase.profileDao.isNotEmpty()
} catch (ex: SQLException) {
Log.e(TAG, "getFirstProfile", ex)
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "isNotEmpty", ex)
app.track(ex)
null
false
}
@Throws(IOException::class)
fun getAllProfiles(): List<Profile>? = try {
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
PrivateDatabase.profileDao.list()
} catch (ex: SQLException) {
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "getAllProfiles", ex)
app.track(ex)
null
......
......@@ -20,42 +20,31 @@
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.acl.Acl
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
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) {
@Suppress("UNCHECKED_CAST")
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> }
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
}
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?,
oldVersion: Int, newVersion: Int) {
if (oldVersion < 1) {
PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key",
Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query()
.forEach { kvPairDao.createOrUpdate(it) }
@Database(entities = [(KeyValuePair::class)], version = 3)
abstract class PublicDatabase : RoomDatabase() {
companion object {
private val instance by lazy {
Room.databaseBuilder(app.deviceContext, PublicDatabase::class.java, Key.DB_PUBLIC)
.allowMainThreadQueries()
.addMigrations(
Migration3
)
.fallbackToDestructiveMigration()
.build()
}
if (oldVersion < 2) {
kvPairDao.createOrUpdate(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString()))
}
val kvPairDao get() = instance.keyValuePairDao()
}
abstract fun keyValuePairDao(): KeyValuePair.Dao
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
val connection = AndroidDatabaseConnection(db, true)
connectionSource.saveSpecialConnection(connection)
TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true)
onCreate(db, connectionSource)
connectionSource.clearSpecialConnection(connection)
}
internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair",
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"`key`, `valueType`, `value`")
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
......@@ -18,55 +18,18 @@
* *
*******************************************************************************/
package com.github.shadowsocks.bg
package com.github.shadowsocks.database.migration
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.BuildConfig
import org.xbill.DNS.*
import java.io.IOException
import java.net.*
import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.room.migration.Migration
object Dns {
private const val TAG = "Dns"
private val hasIPv6Support get() = try {
val result = NetworkInterface.getNetworkInterfaces().asSequence().flatMap { it.inetAddresses.asSequence() }
.any { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress }
if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected")
result
} 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
open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
private val schema: String, private val keys: String) :
Migration(oldVersion, newVersion) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `tmp` $schema")
database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`")
database.execSQL("DROP TABLE `$table`")
database.execSQL("ALTER TABLE `tmp` RENAME TO `$table`")
}
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
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init {
assert(resolveInfo.providerInfo != null)
check(resolveInfo.providerInfo != null)
}
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
......
......@@ -88,21 +88,19 @@ object PluginManager {
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> {
return synchronized(this) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
}
}
if (cachedPlugins == null) {
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
}
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()
......@@ -135,10 +133,10 @@ object PluginManager {
private fun initNative(options: PluginOptions): String? {
val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
assert(providers.size == 1)
if (providers.isEmpty()) return null
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers[0].providerInfo.authority)
.authority(providers.single().providerInfo.authority)
.build()
val cr = app.contentResolver
return try {
......@@ -155,7 +153,7 @@ object PluginManager {
out.putString(PluginContract.EXTRA_OPTIONS, options.id)
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out)
.getString(PluginContract.EXTRA_ENTRY)
assert(File(result).canExecute())
check(File(result).canExecute())
return result
}
......@@ -174,7 +172,7 @@ object PluginManager {
do {
val path = cursor.getString(0)
val file = File(pluginDir, path)
assert(file.absolutePath.startsWith(pluginDirPath))
check(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
......
......@@ -21,7 +21,8 @@
package com.github.shadowsocks.preference
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.PublicDatabase
import com.github.shadowsocks.utils.DirectBoot
......@@ -29,9 +30,9 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort
object DataStore {
val publicStore = OrmLitePreferenceDataStore(PublicDatabase.kvPairDao)
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
// 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
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
......@@ -43,16 +44,24 @@ object DataStore {
} else parsePort(publicStore.getString(key), default + userIndex)
}
var profileId: Int
get() = publicStore.getInt(Key.id) ?: 0
var profileId: Long
get() = publicStore.getLong(Key.id) ?: 0
set(value) {
publicStore.putInt(Key.id, value)
publicStore.putLong(Key.id, value)
if (DataStore.directBootAware) DirectBoot.update()
}
/**
* Setter is defined in MainActivity.onPreferenceDataStoreChanged.
*/
val directBootAware: Boolean get() = BootReceiver.enabled && publicStore.getBoolean(Key.directBootAware) == true
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
val directBootAware: Boolean get() = app.directBootSupported && canToggleLocked
private var nightModeString: String
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
get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
set(value) = publicStore.putString(Key.serviceMode, value)
......@@ -68,6 +77,7 @@ object DataStore {
fun initGlobal() {
// 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.portProxy) == null) portProxy = portProxy
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
......
......@@ -22,61 +22,60 @@ package com.github.shadowsocks.preference
import android.support.v7.preference.PreferenceDataStore
import com.github.shadowsocks.database.KeyValuePair
import com.j256.ormlite.dao.Dao
import java.util.HashSet
@Suppress("MemberVisibilityCanPrivate", "unused")
open class OrmLitePreferenceDataStore(private val kvPairDao: Dao<KeyValuePair, String?>) : PreferenceDataStore() {
fun getBoolean(key: String?) = kvPairDao.queryForId(key)?.boolean
fun getFloat(key: String?) = kvPairDao.queryForId(key)?.float
fun getInt(key: String?) = kvPairDao.queryForId(key)?.int
fun getLong(key: String?) = kvPairDao.queryForId(key)?.long
fun getString(key: String?) = kvPairDao.queryForId(key)?.string
fun getStringSet(key: String?) = kvPairDao.queryForId(key)?.stringSet
@Suppress("MemberVisibilityCanBePrivate", "unused")
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : PreferenceDataStore() {
fun getBoolean(key: String) = kvPairDao[key]?.boolean
fun getFloat(key: String) = kvPairDao[key]?.float
fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
fun getLong(key: String) = kvPairDao[key]?.long
fun getString(key: String) = kvPairDao[key]?.string
fun getStringSet(key: String) = kvPairDao[key]?.stringSet
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue
override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue
override fun getLong(key: String?, defValue: Long) = getLong(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 getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue
override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue
override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
override fun getString(key: String, defValue: String?) = getString(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 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 putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value)
override fun putBoolean(key: String?, value: Boolean) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(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 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)
override fun putBoolean(key: String, value: Boolean) {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putFloat(key: String?, value: Float) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
override fun putFloat(key: String, value: Float) {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putInt(key: String?, value: Int) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
override fun putInt(key: String, value: Int) {
kvPairDao.put(KeyValuePair(key).put(value.toLong()))
fireChangeListener(key)
}
override fun putLong(key: String?, value: Long) {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
override fun putLong(key: String, value: Long) {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else {
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else {
kvPairDao.createOrUpdate(KeyValuePair(key).put(values))
override fun putStringSet(key: String, values: MutableSet<String>?) = if (values == null) remove(key) else {
kvPairDao.put(KeyValuePair(key).put(values))
fireChangeListener(key)
}
fun remove(key: String?) {
kvPairDao.deleteById(key)
fun remove(key: String) {
kvPairDao.delete(key)
fireChangeListener(key)
}
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 unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
}
......@@ -29,19 +29,24 @@ import com.twofortyfouram.locale.api.Intent as ApiIntent
class Settings(bundle: Bundle?) {
companion object {
private val KEY_SWITCH_ON = "switch_on"
private val KEY_PROFILE_ID = "profile_id"
private const val KEY_SWITCH_ON = "switch_on"
private const val KEY_PROFILE_ID = "profile_id"
fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
}
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 {
val bundle = Bundle()
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)
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,
......
......@@ -32,6 +32,12 @@ object Key {
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 modeProxy = "proxy"
const val modeVpn = "vpn"
......
package com.github.shadowsocks.utils
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.bg.BaseService
import com.github.shadowsocks.database.Profile
......@@ -12,8 +16,9 @@ import java.io.ObjectInputStream
import java.io.ObjectOutputStream
@TargetApi(24)
object DirectBoot {
object DirectBoot : BroadcastReceiver() {
private val file = File(app.deviceContext.noBackupFilesDir, "directBootProfile")
private var registered = false
fun getDeviceProfile(): Profile? = try {
ObjectInputStream(file.inputStream()).use { it.readObject() as Profile }
......@@ -24,8 +29,26 @@ object DirectBoot {
File(app.deviceContext.noBackupFilesDir, BaseService.CONFIG_FILE).delete()
}
fun update() {
val profile = ProfileManager.getProfile(DataStore.profileId) // app.currentProfile will call this
if (profile == null) clean() else ObjectOutputStream(file.outputStream()).use { it.writeObject(profile) }
/**
* app.currentProfile will call this.
*/
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 {
"else",
" echo Failed.",
"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
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.content.ContextCompat
import android.support.v4.os.BuildCompat
import android.support.v7.util.SortedList
import android.util.TypedValue
import com.github.shadowsocks.App.Companion.app
......@@ -42,8 +41,8 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
/**
* Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions.
*/
fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null,
name: String? = null, priority: Int = -1, block: () -> Unit): Thread {
fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null, priority: Int = -1, block: () -> Unit): Thread {
val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
thread.setUncaughtExceptionHandler(app::track)
if (start) thread.start()
......@@ -56,8 +55,7 @@ val URLConnection.responseLength: Long
inline fun <reified T> Context.systemService() = ContextCompat.getSystemService(this, T::class.java)!!
val PackageInfo.signaturesCompat get() =
if (BuildCompat.isAtLeastP()) signingCertificateHistory.flatten()
else @Suppress("DEPRECATION") signatures.asIterable()
if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
/**
* 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 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" />
<solid android:color="@color/background_stat" />
</shape>
</item>
<item>
......@@ -18,7 +18,7 @@
<!-- android:start not available until API 23 -->
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<solid android:color="@color/background" />
</shape>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
......
......@@ -4,7 +4,7 @@
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<color android:color="@color/material_primary_100"/>
<color android:color="@color/background_selected"/>
</item>
</selector>
</item>
......
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/material_primary_300"/>
<color android:color="@color/background_stat"/>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
</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 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<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"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#f5f5f5"
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0Z" />
<path
android:fillColor="#546e7a"
android:pathData="M24,21.8 C25.7673,21.8,27.2,23.2327,27.2,25 C27.2,26.7673,25.7673,28.2,24,28.2
C22.2327,28.2,20.8,26.7673,20.8,25 C20.8,23.2327,22.2327,21.8,24,21.8 Z" />
<path
android:fillColor="#546e7a"
android:pathData="M21,15 L19.17,17 L16,17 A2,2,0,0,0,14,19 L14,31 A2,2,0,0,0,16,33 L32,33
A2,2,0,0,0,34,31 L34,19 A2,2,0,0,0,32,17 L28.83,17 L27,15 Z M24,30
A5,5,0,1,1,29,25 A5,5,0,0,1,24,30 Z" />
</vector>
\ No newline at end of file
<item
android:drawable="@drawable/ic_app_shortcut_background"
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp" />
<item
android:drawable="@drawable/ic_image_camera_alt"
android:left="12dp"
android:top="12dp"
android:right="12dp"
android:bottom="12dp" />
</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"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#f5f5f5"
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0Z" />
<path
android:fillColor="#546e7a"
android:pathData="M34,14.72 L30.52,30.04 L22.72,27.53 L29.61,19.31 L20.43,27.04 L14,24.62
L34,14.72 Z" />
<path
android:fillColor="#546e7a"
android:pathData="M22.89,29.1 L25.38,29.9 L22.9,33.28 L22.89,29.1 Z" />
</vector>
<item
android:drawable="@drawable/ic_app_shortcut_background"
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp" />
<item
android:drawable="@drawable/ic_qu_shadowsocks_foreground"
android:left="12dp"
android:top="12dp"
android:right="12dp"
android:bottom="12dp" />
</layer-list>
\ No newline at end of file
......@@ -5,6 +5,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<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"/>
</vector>
......@@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:padding="16dp">
<Spinner android:id="@+id/template_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
......
......@@ -4,7 +4,8 @@
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="12dp"
android:paddingEnd="12dp">
android:paddingEnd="12dp"
android:focusable="true">
<ImageView android:id="@android:id/icon"
android:layout_width="52dp"
android:layout_height="52dp"/>
......@@ -17,7 +18,6 @@
android:layout_gravity="center_vertical">
<TextView android:id="@android:id/text1"
android:textAppearance="?android:attr/textAppearance"
android:textColor="@android:color/primary_text_light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
......
......@@ -11,7 +11,7 @@
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:elevation="1dp"
android:background="@android:color/white"
android:background="@color/background"
android:orientation="vertical"
tools:ignore="RtlSymmetry">
......
......@@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/snackbar"
android:background="@color/material_primary_700"
android:background="@color/color_primary_dark"
android:clipChildren="false"
android:layout_width="match_parent"
android:layout_height="match_parent">
......@@ -60,6 +60,7 @@
android:ellipsize="marquee"
android:gravity="end"
android:text="@string/sent"
android:textColor="?android:attr/textColorPrimary"
app:layout_column="0"/>
<TextView
......@@ -102,6 +103,7 @@
android:ellipsize="marquee"
android:gravity="end"
android:text="@string/received"
android:textColor="?android:attr/textColorPrimary"
app:layout_column="0"
app:layout_row="1"/>
......
......@@ -11,6 +11,5 @@
android:name="com.google.android.gms.samples.vision.barcodereader.BarcodeCapture"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:gvb_auto_focus="true"
app:gvb_code_format="qr_code"/>
app:gvb_auto_focus="true"/>
</LinearLayout>
......@@ -41,7 +41,6 @@
<string name="delete_confirm_prompt">"آیا مطمئن هستید که می‌خواهید این پروفایل را حذف کنید؟"</string>
<string name="share_qr_nfc">"NFC یا QR Code"</string>
<string name="add_profile_methods_scan_qr_code">"اسکن‌کردن QR Code"</string>
<string name="add_profile_methods_manual_settings">"تنظیمات دستی"</string>
<!-- tasker -->
<string name="toggle_service_state">"فعال‌کردن سرویس"</string>
......@@ -55,4 +54,5 @@
<string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string>
<string name="not_connected">"اتصال برقرار نیست!"</string>
<string name="route_entry_all">"همه"</string>
<string name="add_profile_methods_manual_settings">"تنظیمات دستی"</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="quick_toggle">"Basculer"</string>
<!-- misc -->
<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_fail">"Internet Indisponible"</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="remote_port">"Port Distant"</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="reboot_required">"Impossible de démarrer le service VPN. Vous devrez peut-être redémarrer votre appareil."</string>
<!-- alert category -->
<string name="close">"Fermer"</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 -->
<string name="profiles">"Profils"</string>
<string name="settings">"Paramètres"</string>
<string name="about">"À propos"</string>
<string name="edit">"Éditer"</string>
<string name="share">"Partager"</string>
<string name="add_profile">"Ajouter un Profil"</string>
<string name="action_apply_all">"Appliquer les Paramètres à tous les Profils"</string>
<string name="action_export">"Exporter vers le presse-papiers"</string>
<string name="action_import">"Importer depuis le presse-papiers"</string>
<string name="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_confirm_prompt">"Êtes-vous sûr de vouloir supprimer ce profil?"</string>
<string name="share_qr_nfc">"QR Code/NFC"</string>
<string name="add_profile_dialog">"Ajouter ce profil Shadowsocks?"</string>
<string name="add_profile_methods_scan_qr_code">"Scanner le QR Code"</string>
<plurals name="removed">
<item quantity="one">"Supprimé"</item>
<item quantity="other">"%d éléments supprimés"</item>
</plurals>
<string name="undo">"Annuler"</string>
<!-- 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>
\ No newline at end of file
......@@ -15,9 +15,6 @@
<string name="connection_test_fail">"インターネット利用不可"</string>
<string name="connection_test_error_status_code">"ステータスコード無効: #%d"</string>
<!-- proxy category -->
<string name="proxy_cat">"サーバー設定"</string>
<!-- proxy category -->
<string name="profile_name">"サーバー名"</string>
<string name="proxy">"サーバーアドレス"</string>
......@@ -25,9 +22,6 @@
<string name="sitekey">"パスワード"</string>
<string name="enc_method">"暗号化方式"</string>
<!-- feature category -->
<string name="feature_cat">"ファンクション設定"</string>
<!-- feature category -->
<string name="ipv6">"IPv6 プロキシ"</string>
<string name="ipv6_summary">"リモートサーバーに IPv6 パケットを転送"</string>
......@@ -55,10 +49,6 @@
<string name="reboot_required">"VPN サービスの起動に失敗しました、デバイスの再起動を試みて下さい"</string>
<string name="profile_invalid_input">"有効なプロファイルが見つかりません"</string>
<!-- alert category -->
<string name="yes">"はい"</string>
<string name="no">"いいえ"</string>
<!-- alert category -->
<string name="close">"閉じる"</string>
<string name="profile_empty">"プロファイルを選択して下さい"</string>
......@@ -84,8 +74,6 @@
<!-- profile -->
<string name="profile_config">"プロファイル編集"</string>
<string name="unsaved_changes_prompt">"変更は適応されておりません、保存しますか?"</string>
<string name="apply">"適用"</string>
<string name="delete">"削除"</string>
<string name="delete_confirm_prompt">"このプロファイルを削除しますか"</string>
<string name="share_qr_nfc">"QR コード / NFC"</string>
......@@ -147,7 +135,7 @@
<string name="service_transproxy">"トランスプロキシサービス"</string>
<string name="vpn_permission_denied">"VPNサービス作成のアクセス許可が拒否されました"</string>
<string name="auto_connect_summary_v24">"起動時にShadowsockを有効。 VPN常時接続の使用をお勧めします"</string>
<string name="direct_boot_aware">"ダイレクトブート"</string>
<string name="direct_boot_aware_summary">"デバイスがロック解除される前にShadowsocksの自動起動を許可(選択されたプロファイル情報はより少ない保護を受けることになります)"</string>
<string name="direct_boot_aware_summary">"選択されたプロファイル情報はより少ない保護を受けることになります"</string>
<string name="acl_rule_online_config">"オンライン設定のURL"</string>
<string name="action_import_file">"ファイルからのインポート"</string>
</resources>
\ No newline at end of file
......@@ -18,9 +18,6 @@
<string name="connection_test_fail">"인터넷에 연결할 수 없습니다"</string>
<string name="connection_test_error_status_code">"오류 코드: #%d"</string>
<!-- proxy category -->
<string name="proxy_cat">"서버 설정"</string>
<!-- proxy category -->
<string name="profile_name">"프로필 이름"</string>
<string name="proxy">"서버 주소"</string>
......@@ -28,9 +25,6 @@
<string name="sitekey">"비밀번호"</string>
<string name="enc_method">"암호화 방법"</string>
<!-- feature category -->
<string name="feature_cat">"기능 설정"</string>
<!-- feature category -->
<string name="ipv6">"IPv6 라우팅"</string>
<string name="ipv6_summary">"IPv6 트래픽도 원격으로 리다이렉트 합니다"</string>
......@@ -58,10 +52,6 @@
<string name="reboot_required">"VPN 서비스를 시작하는 데 실패했습니다. 장치를 재시작해 보세요."</string>
<string name="profile_invalid_input">"올바른 프로필 데이터를 찾을 수 없습니다"</string>
<!-- alert category -->
<string name="yes">"예"</string>
<string name="no">"아니오"</string>
<!-- alert category -->
<string name="close">"닫기"</string>
<string name="profile_empty">"프로필을 선택해 주세요"</string>
......@@ -86,8 +76,6 @@
<!-- profile -->
<string name="profile_config">"프로필 설정"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="apply">"적용"</string>
<string name="delete">"삭제"</string>
<string name="delete_confirm_prompt">"정말 이 프로필을 삭제하시겠습니까?"</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 @@
<string name="connection_test_fail">"Интернет недоступен"</string>
<string name="connection_test_error_status_code">"Код ошибки: #%d"</string>
<!-- proxy category -->
<string name="proxy_cat">"Настройки Сервера"</string>
<!-- proxy category -->
<string name="profile_name">"Имя профиля"</string>
<string name="proxy">"Сервер"</string>
......@@ -31,9 +28,6 @@
<string name="sitekey">"Пароль"</string>
<string name="enc_method">"Метод шифрования"</string>
<!-- feature category -->
<string name="feature_cat">"Дополнительные Настройки"</string>
<!-- feature category -->
<string name="ipv6">"IPv6 Маршрут"</string>
<string name="ipv6_summary">"Перенаправлять трафик IPv6 на удалённый сервер"</string>
......@@ -60,10 +54,6 @@
<string name="reboot_required">"Не удалось запустить службу VPN. Возможно, требуется перезагрузить ваше устройство."</string>
<string name="profile_invalid_input">"Не найдено действительных данных профиля."</string>
<!-- alert category -->
<string name="yes">"Да"</string>
<string name="no">"Нет"</string>
<!-- alert category -->
<string name="close">"Закрыть"</string>
<string name="profile_empty">"Пожалуйста, выберите профиль"</string>
......@@ -91,8 +81,6 @@
<!-- profile -->
<string name="profile_config">"Настройка профиля"</string>
<string name="unsaved_changes_prompt">"Изменения не сохранены. Сохранить?"</string>
<string name="apply">"Применить"</string>
<string name="delete">"Удалить"</string>
<string name="delete_confirm_prompt">"Вы уверены, что хотите удалить этот профиль?"</string>
<string name="share_qr_nfc">"QR-код/NFC"</string>
......@@ -157,7 +145,7 @@
<string name="vpn_permission_denied">"Нет разрешения на создание VPN соединения"</string>
<string name="auto_connect_summary_v24">"Включить Shadowsocks во время запуска. Лучше использовать режим постоянной VPN."</string>
<string name="speed">"%s/с"</string>
<string name="direct_boot_aware">"Непосредственная загрузка"</string>
<string name="direct_boot_aware_summary">"Позволить Shadowsocks запускаться до того, как устройство будет разблокировано (данные выбранного профиля будут хуже защищены)"</string>
<string name="direct_boot_aware_summary">"данные выбранного профиля будут хуже защищены"</string>
<string name="acl_rule_online_config">"URL конфигурации"</string>
<string name="action_import_file">"Импортировать из файла..."</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"影梭"</string>
<string name="quick_toggle">"开关"</string>
<!-- misc -->
......@@ -16,9 +15,6 @@
<string name="connection_test_fail">"无互联网连接"</string>
<string name="connection_test_error_status_code">"状态码无效(#%d)"</string>
<!-- proxy category -->
<string name="proxy_cat">"服务器设置"</string>
<!-- proxy category -->
<string name="profile_name">"配置名称"</string>
<string name="proxy">"服务器"</string>
......@@ -26,9 +22,6 @@
<string name="sitekey">"密码"</string>
<string name="enc_method">"加密方式"</string>
<!-- feature category -->
<string name="feature_cat">"功能设置"</string>
<!-- feature category -->
<string name="ipv6">"IPv6 路由"</string>
<string name="ipv6_summary">"转发 IPv6 流量到远程服务器"</string>
......@@ -45,6 +38,11 @@
<string name="tcp_fastopen_summary_unsupported">"不支持的内核版本: %s &lt; 3.7.1"</string>
<string name="udp_dns">"DNS 转发"</string>
<string name="udp_dns_summary">"转发所有 DNS 请求到远程服务器"</string>
<string name="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 -->
<string name="forward_success">"后台服务已开始运行。"</string>
......@@ -56,10 +54,6 @@
<string name="reboot_required">"VPN 服务启动失败。你可能需要重启设备。"</string>
<string name="profile_invalid_input">"未找到有效的配置文件。"</string>
<!-- alert category -->
<string name="yes">"是"</string>
<string name="no">"否"</string>
<!-- alert category -->
<string name="close">"关闭"</string>
<string name="profile_empty">"请选择配置文件"</string>
......@@ -86,8 +80,6 @@
<!-- profile -->
<string name="profile_config">"配置文件设置"</string>
<string name="unsaved_changes_prompt">"保存修改吗?"</string>
<string name="apply">"应用"</string>
<string name="delete">"删除"</string>
<string name="delete_confirm_prompt">"您确定要删除此配置文件?"</string>
<string name="share_qr_nfc">"二维码 / NFC"</string>
......@@ -148,7 +140,8 @@
<string name="service_transproxy">"透明代理模式"</string>
<string name="vpn_permission_denied">"创建 VPN 服务权限不足"</string>
<string name="auto_connect_summary_v24">"允许 Shadowsocks 随系统启动,建议使用始终开启的 VPN"</string>
<string name="direct_boot_aware">"直接启动"</string>
<string name="direct_boot_aware_summary">"允许在设备解锁前启动服务(选中的配置信息会不那么安全)"</string>
<string name="direct_boot_aware">"允许锁屏下切换"</string>
<string name="direct_boot_aware_summary">"选中的配置信息会不那么安全"</string>
<string name="acl_rule_online_config">"在线规则文件 URL"</string>
<string name="action_import_file">"从文件导入…"</string>
</resources>
\ No newline at end of file
......@@ -19,9 +19,6 @@
<string name="connection_test_fail">"無法使用網際網路"</string>
<string name="connection_test_error_status_code">"錯誤碼: (#%d)"</string>
<!-- proxy category -->
<string name="proxy_cat">"伺服器設定"</string>
<!-- proxy category -->
<string name="profile_name">"設定檔名稱"</string>
<string name="proxy">"伺服器"</string>
......@@ -29,9 +26,6 @@
<string name="sitekey">"密碼"</string>
<string name="enc_method">"加密方法"</string>
<!-- feature category -->
<string name="feature_cat">"功能設定"</string>
<!-- feature category -->
<string name="ipv6">"IPv6 路由"</string>
<string name="ipv6_summary">"向遠端重新導向 IPv6 流量"</string>
......@@ -58,10 +52,6 @@
<string name="reboot_required">"VPN 服務啟動失敗。您或許需要重新啟動您的裝置。"</string>
<string name="profile_invalid_input">"未找到有效的設定檔資料。"</string>
<!-- alert category -->
<string name="yes">"是"</string>
<string name="no">"否"</string>
<!-- alert category -->
<string name="close">"關閉"</string>
<string name="profile_empty">"請選擇設定檔"</string>
......@@ -88,8 +78,6 @@
<!-- profile -->
<string name="profile_config">"設定檔設定"</string>
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<string name="apply">"套用"</string>
<string name="delete">"刪除"</string>
<string name="delete_confirm_prompt">"您確定要移除這個設定檔嗎?"</string>
<string name="share_qr_nfc">"QR 碼 / NFC"</string>
......
......@@ -237,4 +237,16 @@
<item>vpn</item>
<item>transproxy</item>
</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>
......@@ -2,4 +2,19 @@
<resources>
<color name="ic_launcher_background">#7488A1</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>
......@@ -3,6 +3,7 @@
<string name="app_name">Shadowsocks</string>
<string name="quick_toggle">Toggle</string>
<string name="about_url" translatable="false">file:///android_asset/pages/about.html</string>
<!-- misc -->
<string name="profile">Profile</string>
......@@ -20,10 +21,6 @@
<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="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_testing">Testing…</string>
<string name="connection_test_available">Success: %dms latency</string>
......@@ -57,13 +54,17 @@
<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
instead</string>
<string name="direct_boot_aware">Direct Boot Aware</string>
<string name="direct_boot_aware_summary">Allow Shadowsocks to start before your device is unlocked (your selected
profile information will be less protected)</string>
<string name="direct_boot_aware">Allow Toggling in Lock Screen</string>
<string name="direct_boot_aware_summary">Your selected profile information will be less protected</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="udp_dns">DNS Forwarding</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 -->
<string name="service_vpn">VPN Service</string>
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.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>
<style name="Theme.Shadowsocks.App">
<item name="bottomSheetDialogTheme">@style/Theme.Design.DayNight.BottomSheetDialog</item>
<!-- MaterialDrawer specific values -->
<item name="material_drawer_background">@color/drawer_background</item>
<item name="material_drawer_primary_text">@color/drawer_primary_text</item>
<item name="material_drawer_primary_icon">@color/drawer_primary_icon</item>
<item name="material_drawer_secondary_text">@color/drawer_secondary_text</item>
<item name="material_drawer_hint_text">@color/drawer_hint_text</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 name="Theme.Design.DayNight.BottomSheetDialog" parent="@style/Theme.Design.Light.BottomSheetDialog"/>
<style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
......
......@@ -7,12 +7,18 @@
android:title="@string/auto_connect"/>
<!-- Direct Boot Aware alone doesn't do anything without auto connect -->
<SwitchPreference android:key="directBootAware"
android:dependency="isAutoConnect"
android:summary="@string/direct_boot_aware_summary"
android:title="@string/direct_boot_aware"/>
<SwitchPreference android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
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
android:title="@string/advanced">
<SimpleMenuPreference
......
......@@ -29,7 +29,8 @@ class KeyValuePairTest {
val kvp = KeyValuePair()
Assert.assertEquals(true, kvp.put(true).boolean)
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("3", kvp.put("3").string)
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:
* Enlarge text size of number pickers;
* Update support library version to 26.0.0.
......
......@@ -10,8 +10,8 @@ android {
defaultConfig {
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 5
versionName "0.1.0"
versionCode 6
versionName "0.1.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
......@@ -26,7 +26,7 @@ android {
dependencies {
api "com.android.support:preference-v14:$supportLibraryVersion"
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"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
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
* 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
relevant information;
* SHOULD parse `@NightMode` int extra `com.github.shadowsocks.plugin.EXTRA_NIGHT_MODE` and act
accordingly;
* SHOULD either:
- 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
......@@ -153,7 +155,8 @@ This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the
...
<provider android:name=".BinaryProvider"
android:exported="true"
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER">
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER"
tools:ignore="ExportedContentProvider">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
</intent-filter>
......
......@@ -61,7 +61,7 @@ abstract class NativePluginProvider : ContentProvider() {
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
sortOrder: String?): Cursor {
assert(selection == null && selectionArgs == null && sortOrder == null)
check(selection == null && selectionArgs == null && sortOrder == null)
val result = MatrixCursor(projection)
populateFiles(PathProvider(uri, result))
return result
......@@ -79,7 +79,7 @@ abstract class NativePluginProvider : ContentProvider() {
abstract fun openFile(uri: Uri?): ParcelFileDescriptor
override fun openFile(uri: Uri?, mode: String?): ParcelFileDescriptor {
assert(mode == "r")
check(mode == "r")
return openFile(uri)
}
......
......@@ -23,6 +23,7 @@ package com.github.shadowsocks.plugin
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.app.AppCompatDelegate
import android.widget.Toast
/**
......@@ -43,6 +44,13 @@ abstract class OptionsCapableActivity : AppCompatActivity() {
*/
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?) {
super.onPostCreate(savedInstanceState)
if (savedInstanceState == null) onInitializePluginOptions()
......
......@@ -20,6 +20,8 @@
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.
*
......@@ -71,6 +73,13 @@ object PluginContract {
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
</host_name> */
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.
......
......@@ -8,5 +8,5 @@
android:elevation="4dp"
android:touchscreenBlocksFocus="false"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
android:id="@+id/toolbar" />
<?xml version="1.0" encoding="utf-8"?>
<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>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- proxy category -->
<string name="proxy_cat">"서버 설정"</string>
<!-- feature category -->
<string name="feature_cat">"기능 설정"</string>
<!-- alert category -->
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="yes">"예"</string>
<string name="no">"아니오"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="apply">"적용"</string>
</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"?>
<resources>
<!-- proxy category -->
<string name="proxy_cat">"伺服器設定"</string>
<!-- feature category -->
<string name="feature_cat">"功能設定"</string>
<!-- alert category -->
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<string name="yes">"是"</string>
<string name="no">"否"</string>
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<string name="apply">"套用"</string>
</resources>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
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