Compare commits

..
5 Commits
Author SHA1 Message Date
Mygod 7922be5653 Fix signatures are being deprecated 2018-03-08 23:50:49 -08:00
Mygod a134cba08a Merge branch 'master' into beta 2018-03-08 23:44:44 -08:00
Mygod da2490b1b8 Use ContextCompat.getSystemService 2018-03-08 23:31:29 -08:00
Mygod 3a8f863bd1 Implement network capabilities reporting
This commit should fix the problem where VPN connection is viewed as metered connections on Android P DP1. However as I only have an emulator to test, I can't tell if it really works but it should.

An interesting observation is that the developer can call setUnderlyingNetworks(arrayOfNulls(1)) to make the VPN connection always unmetered. This is more of a quick hack instead of a real fix. Source: https://android.googlesource.com/platform/frameworks/base/+/adbf1d0/services/core/java/com/android/server/connectivity/Vpn.java#312

One may ask whether it's possible to fix this issue for Android 8.1. The short answer is not that I know of.
After some intensive source code reading, I found out that this is the commit that "breaks" whether VPN is metered for Android 8.1: https://android.googlesource.com/platform/frameworks/base/+/43d2a1700b6eb1d804924c6a1e5e0161a13a5348%5E%21/
The key is that isActiveNetworkMetered() used to return getActiveNetworkInfo().isMetered() and now it uses the more up-to-date API getActiveNetwork(). However, getActiveNetworkInfo() ignores VPN connections while getActiveNetwork() prefers VPN connection. And since NetworkCapabilities for VPN Network is always metered by default, this ultimately causes the bug. One might be able to get around this bug by somehow calling NetworkAgent.sendNetworkCapabilities in the correct context.

Fix #1666.
2018-03-08 22:47:58 -08:00
Mygod 8a273ab762 Update to Android P DP1 2018-03-08 22:45:23 -08:00
76 changed files with 3638 additions and 7943 deletions
+1 -3
View File
@@ -29,12 +29,10 @@ 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)
* Fixes for EMUI: [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Fixes for Huawei: [#1091 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
* Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
* 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)
### How to pause Shadowsocks service?
+7 -9
View File
@@ -12,6 +12,11 @@
Использйте интеграцию с [Tasker](http://tasker.dinglisch.net/).
### Как добавить QR код из локальной галереи?
Отсканируйте его при помощи стороннего сканера, например [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) и нажмите на `ss` ссылку.
### Почему режим NAT более не поддерживается?
1. Требуются ROOT права;
@@ -29,12 +34,10 @@
3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки";
4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
* Исправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
* Исправления для EMUI: [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Иправления для Huawei: [#1091 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
* Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung и/или Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
* Ещё Samsung: [#1712](https://github.com/shadowsocks/shadowsocks-android/issues/1712)
* Не устанавливайте это приложение на SD карту, из-за проблем с разрешениями: [#1124 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453)
### Как приостановить службу Shadowsocks?
@@ -55,12 +58,7 @@
Разрешите этому приложению передачу данных в фоновом режиме в настройках приложения.
Если у вас оперератор Yota, попробуйте следующее:
* измените точку доступа APN с `yota.ru` на `client.yota.ru`
* погоняйте программы: Speedtest, Telegram (скачивание файлов)
Я уже уведомил оператора о проблеме и вместе мы стараемся решить её.
Если у вас оперератор Yota, то измените точку доступа APN с `yota.ru` на `client.yota.ru`
### Почему требуется разрешение камере на устройствах младше Android 6.0?
+1 -4
View File
@@ -1,4 +1 @@
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).
Please check the [FAQ](https://github.com/shadowsocks/shadowsocks-android/wiki/FAQ) before submitting new issues.
+1 -5
View File
@@ -20,14 +20,10 @@ 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`
* Clone the repo using `git clone --recurse-submodules <repo>` or update submodules using `git submodule update --init --recursive`
* Fetch submodules using `$ git submodule update --init --recursive`
* Build it using Android Studio or gradle script
### TRANSLATE
+9 -8
View File
@@ -4,13 +4,14 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
kotlinVersion = '1.2.31'
kotlinVersion = '1.2.30'
minSdkVersion = 21
sdkVersion = 27
buildToolsVersion = '27.0.3'
supportLibraryVersion = '27.1.1'
takisoftFixVersion = '27.1.1.0'
playServicesVersion = '12.0.1'
sdkVersion = 'P'
compileSdkVersion = 'android-P'
buildToolsVersion = '28-rc1'
supportLibraryVersion = '28.0.0-alpha1'
takisoftFixVersion = '27.1.0.0'
playServicesVersion = '11.8.0'
junitVersion = '4.12'
androidTestVersion = '1.0.1'
androidEspressoVersion = '3.0.1'
@@ -22,9 +23,9 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.1'
classpath 'com.android.tools.build:gradle:3.2.0-alpha05'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'com.google.gms:google-services:3.2.1'
classpath 'com.google.gms:google-services:3.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ apply plugin: 'com.android.library'
android {
buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.minSdkVersion
@@ -38,7 +38,7 @@ android {
task goBuild(type: Exec) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
executable "cmd.exe"
args "/c", file("src/overture/make.bat").absolutePath , minSdkVersion
args "/c", "src\\overture\\make.bat " + 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", file("src/overture/clean.bat").absolutePath
args "/c", "src\\overture\\clean.bat"
} else {
executable "sh"
args "-c", "src/overture/clean.bash"
-121
View File
@@ -1,121 +0,0 @@
#!/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()
+4 -18
View File
@@ -28,35 +28,21 @@ 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
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 %ANDROID_ARM_CC% (
ECHO "Make standalone toolchain for ARM arch"
python.exe %ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm ^
%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"
python.exe %ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm64 ^
%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"
python.exe %ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch x86 ^
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch x86 ^
--api %MIN_API% --install-dir %ANDROID_X86_TOOLCHAIN%
)
@@ -132,4 +118,4 @@ IF %BUILD% == 1 (
)
ECHO "Successfully build overture"
ENDLOCAL
ENDLOCAL
+16 -26
View File
@@ -1,4 +1,4 @@
# https://github.com/arturbosch/detekt/blob/RC6-4/detekt-cli/src/main/resources/default-detekt-config.yml
# https://github.com/arturbosch/detekt/blob/801994bd60cc759b181649356cea045b8125301b/detekt-cli/src/main/resources/default-detekt-config.yml
comments:
active: false
@@ -7,7 +7,7 @@ complexity:
active: true
ComplexCondition:
active: true
threshold: 4
threshold: 3
ComplexInterface:
active: true
threshold: 10
@@ -15,7 +15,6 @@ complexity:
ComplexMethod:
active: true
threshold: 10
ignoreSingleWhenExpression: false
LabeledExpression:
active: true
LargeClass:
@@ -26,32 +25,31 @@ complexity:
threshold: 20
LongParameterList:
active: true
threshold: 6
threshold: 5
ignoreDefaultParameters: true
MethodOverloading:
active: false
NestedBlockDepth:
active: true
threshold: 4
threshold: 3
StringLiteralDuplication:
active: true
threshold: 3
threshold: 2
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
thresholdInFiles: 10
thresholdInClasses: 10
thresholdInInterfaces: 10
thresholdInObjects: 10
thresholdInEnums: 10
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: "^(_|ignore|expected).*"
active: false
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
@@ -66,7 +64,6 @@ empty-blocks:
active: true
EmptyFunctionBlock:
active: true
ignoreOverriddenFunctions: false
EmptyIfBlock:
active: true
EmptyInitBlock:
@@ -108,7 +105,7 @@ exceptions:
active: true
TooGenericExceptionCaught:
active: true
exceptionNames:
exceptions:
- ArrayIndexOutOfBoundsException
- Error
- Exception
@@ -119,9 +116,10 @@ exceptions:
- Throwable
TooGenericExceptionThrown:
active: true
exceptionNames:
exceptions:
- Error
- Exception
- NullPointerException
- Throwable
- RuntimeException
@@ -145,7 +143,6 @@ naming:
FunctionNaming:
active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^'
MatchingDeclarationName:
active: true
MemberNameEqualsClassName:
@@ -170,7 +167,6 @@ naming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
performance:
active: true
@@ -243,18 +239,16 @@ style:
maxLineLength: 120
excludePackageStatements: false
excludeImportStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: true
OptionalAbstractKeyword:
active: true
OptionalReturnKeyword:
active: true
OptionalUnit:
active: true
OptionalWhenBraces:
@@ -274,8 +268,6 @@ style:
ThrowsCount:
active: true
max: 2
TrailingWhitespace:
active: true
UnnecessaryAbstractClass:
active: true
UnnecessaryInheritance:
@@ -286,8 +278,6 @@ style:
active: true
UnusedImports:
active: true
UnusedPrivateMember:
active: true
UseDataClass:
active: false
UtilityClassWithPublicConstructor:
+1 -1
View File
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+5 -5
View File
@@ -16,13 +16,13 @@ def getCurrentFlavor() {
android {
buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 4050600
versionName "4.5.6"
versionCode 4050100
versionName "4.5.1"
testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
@@ -58,13 +58,13 @@ dependencies {
implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:gridlayout-v7:$supportLibraryVersion"
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
implementation 'com.evernote:android-job:1.2.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.7'
implementation 'com.mikepenz:materialdrawer:6.0.6'
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'
+1 -1
View File
@@ -72,7 +72,7 @@
<activity
android:name=".VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:theme="@style/Theme.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,3 @@
0.0.0.0/8
10.0.0.0/8
100.64.0.0/10
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.0.0.0/29
192.0.2.0/24
192.88.99.0/24
192.168.0.0/16
198.18.0.0/15
198.51.100.0/24
203.0.113.0/24
224.0.0.0/3
1.0.1.0/24
1.0.2.0/23
1.0.8.0/21
@@ -0,0 +1,27 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package android.support.design.animation
import android.animation.TimeInterpolator
object AnimationConsts {
val FAST_OUT_SLOW_IN_INTERPOLATOR: TimeInterpolator get() = AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR
}
@@ -1,8 +0,0 @@
package android.support.design.widget
import android.view.animation.Interpolator
object SnackbarAnimation {
val FAST_OUT_SLOW_IN_INTERPOLATOR: Interpolator get() = AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR
const val ANIMATION_DURATION = BaseTransientBottomBar.ANIMATION_DURATION.toLong()
}
@@ -0,0 +1,25 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package android.support.design.widget
object SnackbarConsts {
const val ANIMATION_DURATION = BaseTransientBottomBar.ANIMATION_DURATION.toLong()
}
@@ -23,7 +23,6 @@ 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
@@ -34,8 +33,9 @@ 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
@@ -72,15 +72,10 @@ 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, PackageManager.GET_SIGNATURES)!!
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName, if (BuildCompat.isAtLeastP())
PackageManager.GET_SIGNING_CERTIFICATES else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() {
val intent = Intent(this, BaseService.serviceClass.java)
@@ -107,7 +102,7 @@ class App : Application() {
fun track(t: Throwable) = track(Thread.currentThread(), t)
fun track(thread: Thread, t: Throwable) {
tracker.send(HitBuilders.ExceptionBuilder()
.setDescription("${exceptionParser.getDescription(thread.name, t)} - ${t.message}")
.setDescription(StandardExceptionParser(this, null).getDescription(thread.name, t))
.setFatal(false)
.build())
t.printStackTrace()
@@ -144,15 +139,14 @@ class App : Application() {
app.track(e)
}
// handle data restored/crash
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
(getSystemService(Context.USER_SERVICE) as UserManager).isUserUnlocked) DirectBoot.flushTrafficStats()
// handle data restored
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update()
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) {
@@ -27,7 +27,6 @@ import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
@@ -49,10 +48,7 @@ import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.thread
import com.github.shadowsocks.utils.*
import java.util.concurrent.atomic.AtomicBoolean
class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
@@ -62,21 +58,23 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
cachedApps = 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()
}
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) }
// 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,
@@ -140,7 +138,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
private lateinit var loadingView: View
private val appsLoading = AtomicBoolean()
private val handler = Handler()
private val clipboard by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private val clipboard by lazy { systemService<ClipboardManager>() }
private fun initProxiedApps(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet()
@@ -153,7 +151,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
appListView.visibility = View.GONE
fastScroller.visibility = View.GONE
loadingView.visibility = View.VISIBLE
thread("AppManager-loader") {
thread {
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,17 +39,20 @@ 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
true
directBootAwareListener.onPreferenceChange(null, DataStore.directBootAware)
}
boot.isChecked = BootReceiver.enabled
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 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 tfo = findPreference(Key.tfo) as SwitchPreference
tfo.isChecked = TcpFastOpen.sendEnabled
@@ -266,20 +266,18 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
++testCount
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
thread("ConnectionTest") { testConnection(id) }
thread { testConnection(id) }
}
}
fab = findViewById(R.id.fab)
fab.setOnClickListener {
when {
state == BaseService.CONNECTED -> app.stopService()
BaseService.usingVpnMode -> {
if (state == BaseService.CONNECTED) app.stopService() else thread {
if (BaseService.usingVpnMode) {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
}
else -> app.startService()
else app.handler.post { onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null) }
} else app.startService()
}
}
@@ -56,7 +56,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
}
private var profileId = -1
private lateinit var profile: Profile
private lateinit var isProxyApps: SwitchPreference
private lateinit var plugin: IconListPreference
private lateinit var pluginConfigure: EditTextPreference
@@ -66,7 +66,13 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity()
profileId = activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)
val profile = ProfileManager.getProfile(activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1))
if (profile == null) {
activity.finish()
return
}
this.profile = profile
profile.serialize()
addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) {
findPreference(Key.host).summary = "shadowsocks.example.org"
@@ -124,12 +130,10 @@ 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(profileId)
if (DataStore.profileId == profileId && DataStore.directBootAware) DirectBoot.update()
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profile.id)
if (DataStore.profileId == profile.id && DataStore.directBootAware) DirectBoot.update()
requireActivity().finish()
}
@@ -183,7 +187,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, { _, _ ->
ProfileManager.delProfile(profileId)
ProfileManager.delProfile(profile.id)
activity.finish()
})
.setNegativeButton(R.string.no, null)
@@ -45,6 +45,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.systemService
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
@@ -123,10 +124,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private var adView: AdView? = null
init {
edit.setOnClickListener {
item = ProfileManager.getProfile(item.id)!!
startConfig(item)
}
edit.setOnClickListener { startConfig(item.id) }
TooltipCompat.setTooltipText(edit, edit.contentDescription)
itemView.setOnClickListener(this)
val share = itemView.findViewById<View>(R.id.share)
@@ -308,12 +306,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private var txTotal: Long = 0L
private var rxTotal: Long = 0L
private val clipboard by lazy { requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private val clipboard by lazy { requireContext().systemService<ClipboardManager>() }
private fun startConfig(profile: Profile) {
profile.serialize()
startActivity(Intent(context, ProfileConfigActivity::class.java).putExtra(Action.EXTRA_PROFILE_ID, profile.id))
}
private fun startConfig(id: Int) = startActivity(Intent(context, ProfileConfigActivity::class.java)
.putExtra(Action.EXTRA_PROFILE_ID, id))
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_list, container, false)
@@ -371,20 +367,20 @@ 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 (_: Exception) { }
} catch (_: IndexOutOfBoundsException) { }
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())
startConfig(ProfileManager.createProfile().id)
true
}
R.id.action_export -> {
@@ -21,7 +21,6 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutManager
import android.graphics.BitmapFactory
@@ -40,6 +39,7 @@ import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.systemService
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
@@ -65,7 +65,7 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
if (try {
(getSystemService(Context.CAMERA_SERVICE) as CameraManager).cameraIdList.isEmpty()
systemService<CameraManager>().cameraIdList.isEmpty()
} catch (_: CameraAccessException) {
true
}) {
@@ -22,7 +22,6 @@ package com.github.shadowsocks
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
@@ -33,6 +32,7 @@ import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.systemService
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
companion object {
@@ -48,8 +48,7 @@ class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface
finish()
return
}
val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (km.inKeyguardRestrictedInputMode()) {
if (systemService<KeyguardManager>().inKeyguardRestrictedInputMode()) {
receiver = broadcastReceiver { _, _ -> connection.connect() }
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
} else connection.connect()
@@ -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,7 +22,6 @@ package com.github.shadowsocks.acl
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.support.design.widget.Snackbar
@@ -48,6 +47,7 @@ import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.systemService
import com.github.shadowsocks.widget.UndoSnackbarManager
import java.net.IDN
import java.net.MalformedURLException
@@ -360,7 +360,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
private lateinit var list: RecyclerView
private var mode: ActionMode? = null
private lateinit var undoManager: UndoSnackbarManager<Any>
private val clipboard by lazy { requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private val clipboard by lazy { requireContext().systemService<ClipboardManager>() }
private fun onSelectedItemsUpdated() {
if (selectedItems.isEmpty()) mode?.finish() else if (mode == null) mode = toolbar.startActionMode(this)
@@ -438,7 +438,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
}
R.id.action_import -> {
try {
check(adapter.addToProxy(clipboard.primaryClip!!.getItemAt(0).text.toString()) != null)
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()
@@ -49,7 +49,6 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.net.UnknownHostException
import java.security.MessageDigest
import java.util.*
@@ -76,7 +75,7 @@ object BaseService {
@Volatile var state = STOPPED
@Volatile var plugin = PluginOptions()
@Volatile var pluginPath: String? = null
val processes = GuardedProcessPool()
var sslocalProcess: GuardedProcess? = null
var timer: Timer? = null
var trafficMonitorThread: TrafficMonitorThread? = null
@@ -117,10 +116,7 @@ object BaseService {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder()))
item.trafficUpdated(profile!!.id, txRate, rxRate, txTotal, rxTotal)
} catch (e: Exception) {
e.printStackTrace()
app.track(e)
}
} catch (_: Exception) { } // ignore
callbacks.finishBroadcast()
}
}
@@ -149,35 +145,22 @@ object BaseService {
}
internal fun updateTrafficTotal(tx: Long, rx: Long) {
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()
// 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
}
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()
}
}
@@ -217,10 +200,7 @@ object BaseService {
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg)
} catch (e: Exception) {
e.printStackTrace()
app.track(e)
}
} catch (_: Exception) { } // ignore
callbacks.finishBroadcast()
}
state = s
@@ -255,7 +235,6 @@ 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",
@@ -270,11 +249,9 @@ object BaseService {
cmd += acl.absolutePath
}
if (profile.udpdns) cmd += "-D"
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
data.processes.start(cmd)
data.sslocalProcess = GuardedProcess(cmd).start()
}
fun createNotification(profileName: String): ServiceNotification
@@ -285,7 +262,11 @@ object BaseService {
else startService(Intent(this, javaClass))
}
fun killProcesses() = data.processes.killAll()
fun killProcesses() {
val data = data
data.sslocalProcess?.destroy()
data.sslocalProcess = null
}
fun stopRunner(stopService: Boolean, msg: String? = null) {
// channge the state
@@ -360,7 +341,7 @@ object BaseService {
data.changeState(CONNECTING)
thread("$tag-Connecting") {
thread {
try {
if (profile.host == "198.199.101.152") {
val client = OkHttpClient.Builder()
@@ -373,7 +354,7 @@ object BaseService {
.readTimeout(30, TimeUnit.SECONDS)
.build()
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signatures[0].toByteArray())
mdg.update(app.info.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0)))
.build()
@@ -28,37 +28,32 @@ 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.*
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.Semaphore
class GuardedProcessPool {
class GuardedProcess(private val cmd: List<String>) {
companion object {
private const val TAG = "GuardedProcessPool"
private val dummy = IOException()
private const val TAG = "GuardedProcess"
}
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 lateinit var guardThread: Thread
@Volatile
private var isDestroyed = false
@Volatile
private lateinit var process: Process
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
}
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = thread {
try {
input.bufferedReader().useLines { it.forEach { logger(TAG, it) } }
} catch (_: IOException) { } // ignore
}
fun looper() {
var process: Process? = null
fun start(onRestartCallback: (() -> Unit)? = null): GuardedProcess {
val semaphore = Semaphore(1)
semaphore.acquire()
var ioException: IOException? = null
guardThread = thread(name = "GuardThread-" + cmd.first()) {
try {
var callback: (() -> Unit)? = null
while (!isDestroyed) {
@@ -75,54 +70,44 @@ class GuardedProcessPool {
if (callback == null) callback = onRestartCallback else callback()
pushException(null)
semaphore.release()
process.waitFor()
synchronized(this) {
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Log.w(TAG, "process exit too fast, stop guard: $cmdName")
Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd))
isDestroyed = true
}
}
}
} catch (_: InterruptedException) {
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: $cmdName")
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd))
destroyProcess()
} catch (e: IOException) {
pushException(e)
ioException = e
} finally {
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.release()
}
}
}
private val guardThreads = HashSet<Thread>()
@Volatile
private var isDestroyed = false
fun start(cmd: List<String>, onRestartCallback: (() -> Unit)? = null): GuardedProcessPool {
val guard = Guard(cmd, onRestartCallback)
guardThreads.add(thread("GuardThread-${guard.cmdName}", block = guard::looper))
val ioException = guard.excQueue.take()
if (ioException !== dummy) throw ioException
semaphore.acquire()
if (ioException != null) throw ioException!!
return this
}
fun killAll() {
fun destroy() {
isDestroyed = true
guardThreads.forEach { it.interrupt() }
guardThread.interrupt()
destroyProcess()
try {
guardThreads.forEach { it.join() }
guardThread.join()
} catch (_: InterruptedException) { }
}
guardThreads.clear()
isDestroyed = false
private fun destroyProcess() {
if (Build.VERSION.SDK_INT < 24) @Suppress("DEPRECATION") {
JniHelper.sigtermCompat(process)
JniHelper.waitForCompat(process, 500)
}
process.destroy()
}
}
@@ -28,9 +28,19 @@ 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
@@ -62,11 +72,12 @@ 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", 9) })
val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "208.67.222.222:443", 3, false),
makeDns("Primary-2", "119.29.29.29:53", 3, false),
makeDns("Primary-3", "114.114.114.114:53", 3, false)
makeDns("Primary-1", "119.29.29.29:53", 3, false),
makeDns("Primary-2", "114.114.114.114:53", 3, false),
makeDns("Primary-3", "208.67.222.222:443", 3, false)
))
when (profile.route) {
@@ -74,6 +85,7 @@ 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)
@@ -86,10 +98,18 @@ object LocalDnsService {
return file
}
if (!profile.udpdns) data.processes.start(buildAdditionalArguments(arrayListOf(
if (!profile.udpdns) overtureProcess = GuardedProcess(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(tag) {
abstract class LocalSocketListener(protected val tag: String) : Thread() {
init {
setUncaughtExceptionHandler(app::track)
}
@@ -36,6 +36,7 @@ import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.systemService
import java.util.*
/**
@@ -46,7 +47,7 @@ import java.util.*
*/
class ServiceNotification(private val service: BaseService.Interface, profileName: String,
channel: String, private val visible: Boolean = false) {
private val keyGuard = (service as Context).getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
private val keyGuard = (service as Context).systemService<KeyguardManager>()
private val nm by lazy {
(service as Context).getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
@@ -82,7 +83,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
service as Context
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
val power = service.getSystemService(Context.POWER_SERVICE) as PowerManager
val power = service.systemService<PowerManager>()
update(if (power.isInteractive) Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
val screenFilter = IntentFilter()
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
@@ -21,7 +21,6 @@
package com.github.shadowsocks.bg
import android.app.KeyguardManager
import android.content.Context
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
import android.support.annotation.RequiresApi
@@ -31,6 +30,7 @@ import com.github.shadowsocks.ShadowsocksConnection
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.systemService
import android.service.quicksettings.TileService as BaseTileService
@RequiresApi(24)
@@ -38,7 +38,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle) }
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_service_active) }
private val keyguard by lazy { getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager }
private val keyguard by lazy { systemService<KeyguardManager>() }
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
@RequiresApi(24)
@@ -82,7 +82,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
}
override fun onClick() {
if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()
if (isLocked && !DataStore.directBootAware) unlockAndRun(this::toggle) else toggle()
}
private fun toggle() {
@@ -40,14 +40,18 @@ 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() {
data.processes.start(listOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
val cmd = arrayListOf(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
"-c", data.shadowsocksConfigFile!!.absolutePath) // config is already built by BaseService.Interface
sstunnelProcess = GuardedProcess(cmd).start()
}
private fun startRedsocksDaemon() {
@@ -66,8 +70,10 @@ redsocks {
type = socks5;
}
""")
data.processes.start(listOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
redsocksProcess = GuardedProcess(arrayListOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
"-c", "redsocks.conf")
).start()
}
override fun startNativeProcesses() {
@@ -75,4 +81,12 @@ redsocks {
super.startNativeProcesses()
if (data.profile!!.udpdns) startDNSTunnel()
}
override fun killProcesses() {
super.killProcesses()
sstunnelProcess?.destroy()
sstunnelProcess = null
redsocksProcess?.destroy()
redsocksProcess = null
}
}
@@ -22,7 +22,6 @@ package com.github.shadowsocks.bg
import android.annotation.TargetApi
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.*
@@ -40,6 +39,7 @@ import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.systemService
import java.io.File
import java.io.FileDescriptor
import java.io.IOException
@@ -56,8 +56,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$")
/**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: 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
@@ -78,22 +77,13 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
var success = false
try {
socket.inputStream.read()
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
}
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
} catch (e: Exception) {
Log.e(tag, "Error when receiving ancillary fd", e)
Log.e(tag, "Error when protect socket", e)
app.track(e)
}
try {
@@ -116,25 +106,20 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var conn: ParcelFileDescriptor? = null
private var worker: ProtectWorker? = null
private var underlyingNetwork: Network? = null
@TargetApi(28)
set(value) {
setUnderlyingNetworks(if (value == null) null else arrayOf(value))
field = value
}
private var tun2socksProcess: GuardedProcess? = null
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
@TargetApi(28)
private val connectivity by lazy { systemService<ConnectivityManager>() }
@TargetApi(Build.VERSION_CODES.P)
private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
underlyingNetwork = network
setUnderlyingNetworks(arrayOf(network))
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
// it's a good idea to refresh capabilities
underlyingNetwork = network
setUnderlyingNetworks(arrayOf(network))
}
override fun onLost(network: Network) {
underlyingNetwork = null
setUnderlyingNetworks(null)
}
}
private var listeningForDefaultNetwork = false
@@ -147,13 +132,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun onRevoke() = stopRunner(true)
override fun killProcesses() {
if (listeningForDefaultNetwork) {
if (BuildCompat.isAtLeastP() && listeningForDefaultNetwork) {
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
listeningForDefaultNetwork = false
}
worker?.stopThread()
worker = null
super.killProcesses()
tun2socksProcess?.destroy()
tun2socksProcess = null
conn?.close()
conn = null
}
@@ -230,6 +217,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()) {
// we want REQUEST here instead of LISTEN
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
@@ -253,7 +241,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}"
}
data.processes.start(cmd) { sendFd(fd) }
tun2socksProcess = GuardedProcess(cmd).start { sendFd(fd) }
return fd
}
@@ -31,7 +31,6 @@ 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.*
class Profile : Serializable {
@@ -44,51 +43,41 @@ class Profile : Serializable {
fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map {
val uri = Uri.parse(it.value)
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
}
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 {
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
}
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
}
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid base64 detected: ${it.value}")
null
}
}.filterNotNull()
}
@@ -147,9 +136,6 @@ class Profile : Serializable {
@DatabaseField
var plugin: String? = null
// 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,13 +20,11 @@
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
/**
@@ -64,11 +62,9 @@ object ProfileManager {
@Throws(SQLException::class)
fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.update(profile)
@Throws(IOException::class)
fun getProfile(id: Int): Profile? = try {
PrivateDatabase.profileDao.queryForId(id)
} catch (ex: SQLException) {
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "getProfile", ex)
app.track(ex)
null
@@ -81,21 +77,17 @@ object ProfileManager {
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
}
@Throws(IOException::class)
fun getFirstProfile(): Profile? = try {
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull()
} catch (ex: SQLException) {
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "getFirstProfile", ex)
app.track(ex)
null
}
@Throws(IOException::class)
fun getAllProfiles(): List<Profile>? = try {
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
} catch (ex: SQLException) {
if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex)
Log.e(TAG, "getAllProfiles", ex)
app.track(ex)
null
@@ -25,7 +25,7 @@ import android.os.Bundle
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init {
check(resolveInfo.providerInfo != null)
assert(resolveInfo.providerInfo != null)
}
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
@@ -32,6 +32,7 @@ import android.util.Base64
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.signaturesCompat
import eu.chainfire.libsuperuser.Shell
import java.io.File
import java.io.FileNotFoundException
@@ -47,7 +48,7 @@ object PluginManager {
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/
val trustedSignatures by lazy {
app.info.signatures.toSet() +
app.info.signaturesCompat.toSet() +
Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
@@ -87,19 +88,21 @@ object PluginManager {
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = 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 }
}
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()
@@ -132,10 +135,10 @@ object PluginManager {
private fun initNative(options: PluginOptions): String? {
val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
if (providers.isEmpty()) return null
assert(providers.size == 1)
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers.single().providerInfo.authority)
.authority(providers[0].providerInfo.authority)
.build()
val cr = app.contentResolver
return try {
@@ -152,7 +155,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)
check(File(result).canExecute())
assert(File(result).canExecute())
return result
}
@@ -171,7 +174,7 @@ object PluginManager {
do {
val path = cursor.getString(0)
val file = File(pluginDir, path)
check(file.absolutePath.startsWith(pluginDirPath))
assert(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
@@ -24,6 +24,7 @@ import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.Bundle
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.signaturesCompat
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
protected abstract val metaData: Bundle
@@ -34,6 +35,6 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin()
override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
override val packageName: String get() = resolveInfo.resolvePackageName
override val trusted by lazy {
app.getPackageInfo(packageName).signatures.any(PluginManager.trustedSignatures::contains)
app.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
}
}
@@ -21,7 +21,7 @@
package com.github.shadowsocks.preference
import android.os.Binder
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase
import com.github.shadowsocks.utils.DirectBoot
@@ -49,8 +49,10 @@ object DataStore {
publicStore.putInt(Key.id, value)
if (DataStore.directBootAware) DirectBoot.update()
}
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
val directBootAware: Boolean get() = app.directBootSupported && canToggleLocked
/**
* Setter is defined in MainActivity.onPreferenceDataStoreChanged.
*/
val directBootAware: Boolean get() = BootReceiver.enabled && publicStore.getBoolean(Key.directBootAware) == true
var serviceMode: String
get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
set(value) = publicStore.putString(Key.serviceMode, value)
@@ -29,8 +29,8 @@ import com.twofortyfouram.locale.api.Intent as ApiIntent
class Settings(bundle: Bundle?) {
companion object {
private const val KEY_SWITCH_ON = "switch_on"
private const val KEY_PROFILE_ID = "profile_id"
private val KEY_SWITCH_ON = "switch_on"
private val KEY_PROFILE_ID = "profile_id"
fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
}
@@ -1,10 +1,6 @@
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
@@ -16,9 +12,8 @@ import java.io.ObjectInputStream
import java.io.ObjectOutputStream
@TargetApi(24)
object DirectBoot : BroadcastReceiver() {
object DirectBoot {
private val file = File(app.deviceContext.noBackupFilesDir, "directBootProfile")
private var registered = false
fun getDeviceProfile(): Profile? = try {
ObjectInputStream(file.inputStream()).use { it.readObject() as Profile }
@@ -29,26 +24,8 @@ object DirectBoot : BroadcastReceiver() {
File(app.deviceContext.noBackupFilesDir, BaseService.CONFIG_FILE).delete()
}
/**
* 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
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) }
}
}
@@ -52,5 +52,5 @@ object TcpFastOpen {
"else",
" echo Failed.",
"fi"), null, true)?.joinToString("\n")
fun enabledAsync(value: Boolean) = thread("TcpFastOpen") { enabled(value) }.join(1000)
fun enabledAsync(value: Boolean) = thread { enabled(value) }.join(1000)
}
@@ -3,11 +3,14 @@ package com.github.shadowsocks.utils
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.res.Resources
import android.os.Build
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
@@ -39,8 +42,8 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
/**
* Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions.
*/
fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null, priority: Int = -1, block: () -> Unit): Thread {
fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null,
name: String? = 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()
@@ -50,6 +53,12 @@ fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = fals
val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
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()
/**
* Based on: https://stackoverflow.com/a/15656428/2245107
*/
@@ -22,18 +22,20 @@ package com.github.shadowsocks.widget
import android.animation.ValueAnimator
import android.content.Context
import android.support.design.animation.AnimationConsts
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.Snackbar
import android.support.design.widget.SnackbarAnimation
import android.support.design.widget.SnackbarConsts
import android.util.AttributeSet
import android.view.View
import android.view.accessibility.AccessibilityManager
import com.github.shadowsocks.utils.systemService
/**
* Full credits go to: https://stackoverflow.com/a/35904421/2245107
*/
class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val accessibility = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
private val accessibility = context.systemService<AccessibilityManager>()
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean =
dependency is Snackbar.SnackbarLayout
@@ -52,8 +54,8 @@ class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorL
val animator = ValueAnimator()
val start = child.height
animator.setIntValues(start, parent.height)
animator.interpolator = SnackbarAnimation.FAST_OUT_SLOW_IN_INTERPOLATOR
animator.duration = SnackbarAnimation.ANIMATION_DURATION
animator.interpolator = AnimationConsts.FAST_OUT_SLOW_IN_INTERPOLATOR
animator.duration = SnackbarConsts.ANIMATION_DURATION
@Suppress("NAME_SHADOWING")
animator.addUpdateListener { animator ->
child.layoutParams.height = animator.animatedValue as Int
@@ -1,10 +0,0 @@
<?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>
@@ -1,10 +0,0 @@
<?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>
@@ -1,21 +0,0 @@
<?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>
@@ -1,12 +0,0 @@
<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>
@@ -1,18 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<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>
<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>
@@ -1,11 +0,0 @@
<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>
@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<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>
<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>
+1 -1
View File
@@ -41,6 +41,7 @@
<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>
@@ -54,5 +55,4 @@
<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>
-50
View File
@@ -1,85 +1,35 @@
<?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>
+14 -2
View File
@@ -15,6 +15,9 @@
<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>
@@ -22,6 +25,9 @@
<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>
@@ -49,6 +55,10 @@
<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>
@@ -74,6 +84,8 @@
<!-- 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>
@@ -135,7 +147,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_summary">"選択されたプロファイル情報はより少ない保護を受けることになります"</string>
<string name="direct_boot_aware">"ダイレクトブート"</string>
<string name="direct_boot_aware_summary">"デバイスがロック解除される前にShadowsocksの自動起動を許可(選択されたプロファイル情報はより少ない保護を受けることになります)"</string>
<string name="acl_rule_online_config">"オンライン設定のURL"</string>
<string name="action_import_file">"ファイルからのインポート"</string>
</resources>
+12
View File
@@ -18,6 +18,9 @@
<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,6 +28,9 @@
<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>
@@ -52,6 +58,10 @@
<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>
@@ -76,6 +86,8 @@
<!-- 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>
+14 -2
View File
@@ -21,6 +21,9 @@
<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,6 +31,9 @@
<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>
@@ -54,6 +60,10 @@
<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>
@@ -81,6 +91,8 @@
<!-- 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>
@@ -145,7 +157,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_summary">"данные выбранного профиля будут хуже защищены"</string>
<string name="direct_boot_aware">"Непосредственная загрузка"</string>
<string name="direct_boot_aware_summary">"Позволить Shadowsocks запускаться до того, как устройство будет разблокировано (данные выбранного профиля будут хуже защищены)"</string>
<string name="acl_rule_online_config">"URL конфигурации"</string>
<string name="action_import_file">"Импортировать из файла..."</string>
</resources>
+15 -3
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"影梭"</string>
<string name="quick_toggle">"开关"</string>
<!-- misc -->
@@ -15,6 +16,9 @@
<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>
@@ -22,6 +26,9 @@
<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>
@@ -49,6 +56,10 @@
<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>
@@ -75,6 +86,8 @@
<!-- 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>
@@ -135,8 +148,7 @@
<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>
@@ -19,6 +19,9 @@
<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,6 +29,9 @@
<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>
@@ -52,6 +58,10 @@
<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>
@@ -78,6 +88,8 @@
<!-- 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>
+3 -2
View File
@@ -57,8 +57,9 @@
<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">Allow Toggling in Lock Screen</string>
<string name="direct_boot_aware_summary">Your selected profile information will be less protected</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="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>
+10
View File
@@ -1,5 +1,15 @@
<?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>
<style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
+1
View File
@@ -7,6 +7,7 @@
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"
-5
View File
@@ -1,8 +1,3 @@
* 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.
+3 -3
View File
@@ -5,13 +5,13 @@ apply from: 'gradle-mvn-push.gradle'
android {
buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 6
versionName "0.1.1"
versionCode 5
versionName "0.1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
+1 -2
View File
@@ -153,8 +153,7 @@ 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"
tools:ignore="ExportedContentProvider">
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER">
<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 {
check(selection == null && selectionArgs == null && sortOrder == null)
assert(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 {
check(mode == "r")
assert(mode == "r")
return openFile(uri)
}
@@ -1,8 +1,3 @@
<?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>
+7 -1
View File
@@ -1,9 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- proxy category -->
<string name="proxy_cat">"서버 설정"</string>
<!-- feature category -->
<string name="feature_cat">"기능 설정"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<!-- alert category -->
<string name="yes">"예"</string>
<string name="no">"아니오"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="apply">"적용"</string>
</resources>
@@ -1,9 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- proxy category -->
<string name="proxy_cat">"伺服器設定"</string>
<!-- feature category -->
<string name="feature_cat">"功能設定"</string>
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<!-- alert category -->
<string name="yes">"是"</string>
<string name="no">"否"</string>
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
<string name="apply">"套用"</string>
</resources>
-2
View File
@@ -7,12 +7,10 @@
<color name="material_blue_grey_100">#CFD8DC</color>
<color name="material_blue_grey_300">#90A4AE</color>
<color name="material_blue_grey_500">#607D8B</color>
<color name="material_blue_grey_600">#546E7A</color>
<color name="material_blue_grey_700">#455A64</color>
<color name="material_primary_100">@color/material_blue_grey_100</color>
<color name="material_primary_300">@color/material_blue_grey_300</color>
<color name="material_primary_500">@color/material_blue_grey_500</color>
<color name="material_primary_600">@color/material_blue_grey_600</color>
<color name="material_primary_700">@color/material_blue_grey_700</color>
<color name="material_accent_200">@color/material_green_a700</color>
-11
View File
@@ -11,15 +11,4 @@
<item name="preferenceCategory_marginBottom">8dp</item>
<item name="windowActionModeOverlay">true</item>
</style>
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowCloseOnTouchOutside">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>