Compare commits

...
76 Commits
Author SHA1 Message Date
Max Lv 550f06cf2a Bump version 2018-03-06 02:01:46 -08:00
Max Lv cad258da6e Fix the building error 2018-03-01 21:51:49 -08:00
Mygod 71cd80a634 Fix #1609 2018-02-28 13:56:06 -08:00
Max Lv f0e5681052 Bump version 2018-02-25 19:38:04 +08:00
Max Lv d10a406584 Refine overture memory usage 2018-02-25 11:23:10 +08:00
Max Lv a99a07f586 Update overture 2018-02-24 20:32:45 +08:00
Mygod 39a0a61da2 Allow using TileService in direct boot mode 2018-02-22 11:56:26 -08:00
Mygod cf118a1bbf Remove unnecessary crossfader dependency 2018-02-17 20:39:24 -08:00
Mygod 510098911e Fix domain name display in TileService 2018-02-17 20:35:09 -08:00
Mygod 201bb4fe58 Remove unnecessary tint in TileService 2018-02-17 14:13:08 -08:00
Mygod 026d1ecb2b Fix receiver leaking 2018-02-15 15:34:56 -08:00
Mygod 7461d3bf30 Fix listenForPackageChanges for upgrades again 2018-02-14 00:18:49 -08:00
Mygod 4483765b2d Fix listenForPackageChanges 2018-02-14 00:00:15 -08:00
Mygod 62375a576c Enable multi-dex for debug builds
This is not a big deal thanks to min API = 21 and instant run already uses multi-dex anyways.
2018-02-11 03:07:24 -08:00
Mygod 4904e9c06c Update dependencies 2018-02-11 02:31:44 -08:00
Mygod f75b54699a Use Google mobile vision API to scan QR codes as well
Interestingly even though multiple QR code isn't enabled, if there are two QR codes in the picture they will all get scanned.
2018-02-11 02:26:23 -08:00
Mygod 22ffcc53bb Refine QR code decoding 2018-02-11 01:55:14 -08:00
Max Lv b69c20d086 Use mobile vision APIs for QR code from files 2018-02-10 03:46:47 -08:00
Max Lv c71c6ffe02 Fix a crash in QuickToggle 2018-02-05 16:58:45 -08:00
Max Lv 439d56d1d7 Remove the return value in traffic IPC 2018-02-05 01:38:45 -08:00
Mygod 81f35acf20 Let Zxing try harder 2018-02-05 00:11:50 -08:00
Mygod a755b669c8 Fix code style issues 2018-02-04 15:07:38 -08:00
Mygod 9fb180956f Add detekt.yml 2018-02-04 13:38:53 -08:00
Mygod 39530a1a3b Fix a crash if you're trying to do weird thing 2018-02-04 05:41:06 -08:00
Mygod b91d9474d5 Support importing from file
Now scanning QR codes on camera-less devices actually do sth.
2018-02-04 05:35:03 -08:00
Mygod f0140c0070 Add back scanner shortcut 2018-02-04 04:45:51 -08:00
Mygod d23be09cb0 Add snackbars everywhere 2018-02-04 04:29:54 -08:00
Mygod 4a97ff1d30 Makes less noise while stopping service 2018-02-04 04:14:39 -08:00
Mygod 88fc092aa2 Get rid of postDelayeds
FASTER!
2018-02-04 04:12:17 -08:00
Mygod d715f7b4c6 Fix another crash 2018-02-04 04:08:46 -08:00
Mygod 7cc3c48240 Fix translucent style 2018-02-04 04:08:34 -08:00
Mygod 26fb0e5e3c Prevent ANR for TFO 2018-02-04 03:35:10 -08:00
Mygod f4d0835e00 Refine error message 2018-02-04 03:16:01 -08:00
Mygod ebcf514323 Handle errors in acl dialog more gracefully 2018-02-04 03:08:01 -08:00
Mygod 3975f3f43c Disconnect as soon as binder died 2018-02-03 22:24:42 -08:00
Mygod be79e6f80c Suppress crash if platform does not support jobs
This is apparently an issue for some HTC devices.
2018-02-03 21:07:48 -08:00
Mygod 61edb40dd1 Remove unnecessary APIs 2018-02-03 21:03:59 -08:00
Mygod 0046ed87d5 Fix a crash on Android 6.0 2018-02-03 20:56:55 -08:00
initrider 4f44d119ee Update translation of FAQ (#1626)
* Create faq.ru-RU.md

* Update faq.ru-RU.md

* Translate - FAQ - Russian

* Rename .github/faq.ru-RU.md to  .github/faq.ru.md

* Fixed translation mistakes + Fix #1565

* Update faq.ru.md

* Visual improvement

* Add #888

* Add #1589 and retranslate a bit of info

* Removed unnecessary word
2018-02-02 11:31:58 -08:00
Mygod 1e5aabf824 Add #888 to FAQ 2018-01-30 20:33:13 -08:00
Mygod 7a49ddc045 Fix #1612 2018-01-28 00:09:01 -08:00
Mygod 5ced42125c Implement ActionMode for CustomRulesFragment 2018-01-27 15:25:29 -08:00
Mygod 3f88b46353 Retire tablet layout
Google doesn't care for tablets either.
2018-01-27 15:09:07 -08:00
Mygod 34da03d6e3 Dependency updates 2018-01-27 13:32:32 -08:00
Mygod 16c0d27130 Update kotlin 2018-01-25 22:33:35 -08:00
Mygod bff2615599 Add #1589 to FAQ 2018-01-25 22:17:06 -08:00
Max Lv c63a659d88 Fix remote DNS 2018-01-24 18:21:32 +08:00
Mygod cb3fa2d923 Fix gradle build script
Now that we have max(versionCodes) = 211003, it's time to just change this versionCode scheme.
2018-01-24 00:14:21 -08:00
Mygod daea4b5f6c Refine connection tests 2018-01-24 00:14:21 -08:00
Max Lv 8e7ddaebbd Add OpenDNS into the primary list 2018-01-23 23:10:01 -08:00
Max Lv aaf9f27d56 Add universal apk 2018-01-23 23:10:01 -08:00
Mygod 7898eef48d Bump versionCode 2018-01-23 22:45:12 -08:00
Mygod 71979434d7 Revert "Fix ABI versions"
This reverts commit ad522572f0.
2018-01-23 22:44:53 -08:00
Max Lv ad522572f0 Fix ABI versions 2018-01-22 21:57:23 -08:00
Max Lv d9fa4481a2 Bump version 2018-01-22 19:12:23 -08:00
Max Lv e2c29594f2 Set different timeouts 2018-01-22 19:04:00 -08:00
Max Lv 2ef042d56b Merge pull request #1593 from chon219/fix-windows-script
fixed windows build script for overture
2018-01-22 12:58:36 +08:00
Zichuan Ma 0fa97f064f fixed windows build script for overture 2018-01-21 18:57:20 +08:00
Max Lv d7ecc34836 Merge pull request #1491 from Mygod/split-abi
Split ABIs for smaller apk size
2018-01-19 15:38:44 +08:00
Mygod 168c0588a6 Fix debug builds 2018-01-18 23:10:12 -08:00
Mygod 4f5ee68f40 Add universalApk 2018-01-18 22:58:26 -08:00
Mygod 0c3352935a Merge branch 'master' into split-abi 2018-01-18 22:34:22 -08:00
Mygod 0d02a39b61 Update Application.mk
Update min api.
2018-01-18 21:54:01 -08:00
Mygod 090cc35bff Fix links in build-shared-executable 2018-01-18 20:58:15 -08:00
Max Lv 042ee55421 Merge pull request #1585 from Mygod/min21
Min API 19 -> 21
2018-01-19 12:33:15 +08:00
Mygod ac96e6fc9c Min API 19 -> 21
Fix #1579.
2018-01-18 19:57:30 -08:00
Mygod c6ff9b3d89 Add license badge 2018-01-18 11:51:10 -08:00
Mygod 77af5f6e3c Refine #1579 2018-01-18 11:46:53 -08:00
Reker 95db29bd13 Fix Windows build script of overture (#1582)
make.bat not detect Windows go binary due to missing '.exe' of filename
when the script convert from bash to bat
2018-01-18 16:12:40 +08:00
Max Lv 7297a2664c Avoid problematic local DNS resolver 2018-01-16 23:17:00 -08:00
Mygod 195e2afbeb Fix keyboard navigations 2018-01-15 22:24:08 -08:00
Mygod 3dbc5f4147 Import translations from POEditor 2018-01-15 21:32:56 -08:00
Mygod 580ce14809 Refine animations 2018-01-15 21:01:29 -08:00
Mygod d4213e8ee8 Remove checkChineseLocale
This was introduced by commit 396d31a0a7. I just did some further testing and:

* zh_rCN and zh_rTW are the only zh locales you can pick before Android 7;
* Since Android 7, user can use locale list to specify their fallback preferences and there's no need to enforce it.

Therefore this method isn't really necessary and will be thus removed.
2018-01-15 19:58:16 -08:00
Mygod cd1f94f4a0 Change version code 2017-12-14 12:26:32 -08:00
Mygod 6dace959d2 Split ABIs for smaller apk size 2017-12-06 23:58:11 -08:00
111 changed files with 1123 additions and 936 deletions
+3 -7
View File
@@ -12,11 +12,6 @@ Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issu
Use [Tasker](http://tasker.dinglisch.net/) integration.
### How to add QR code from local gallery?
Scan it with a third-party scanner like [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) and click the `ss` url.
### Why is NAT mode deprecated?
1. Requiring ROOT permission;
@@ -31,9 +26,10 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails
1. Some ROM has broken VPNService implementation, especially for IPv6;
2. Some ROM has aggressive (or called broken) background service killing policy;
3. If you have Xposed framework and/or battery saver apps, it's likely that this app wouldn't work well with these either.
3. Some ROM like [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) has **very** broken direct boot support;
4. If you have Xposed framework and/or battery saver apps, it's likely that this app wouldn't work well with these either.
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Fixes for Huawei: [#1091 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
* Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
+4 -3
View File
@@ -30,10 +30,11 @@
### Почему мой ROM не поддерживается?
1. Некоторые ROM используют неправильную реализацию службы VPN, особенно IPv6;
2. Некоторые ROM используют агрессивную (или так называемую неправильную) фоновую службу с ошибочной политикой остановки;
3. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
2. Некоторые ROM используют агрессивную (или так называемую испорченную) фоновую службу с ошибочной политикой остановки;
3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки";
4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
* Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
* Иправления для 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)
+3
View File
@@ -24,3 +24,6 @@ core/src/overture/src/gopkg.in
# work in progress
tv/
things/
# release apks
*.apk
+3
View File
@@ -1,7 +1,10 @@
## Shadowsocks for Android
[![Build Status](https://api.travis-ci.org/shadowsocks/shadowsocks-android.svg)](https://travis-ci.org/shadowsocks/shadowsocks-android)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)
[![Releases](https://img.shields.io/github/downloads/shadowsocks/shadowsocks-android/total.svg)](https://github.com/shadowsocks/shadowsocks-android/releases)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1a21d48d466644cdbcb57a1889abea5b)](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
A [shadowsocks](http://shadowsocks.org) client for Android, written in Kotlin.
<a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a>
+8 -5
View File
@@ -1,13 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
kotlinVersion = '1.2.10'
minSdkVersion = 19
kotlinVersion = '1.2.21'
minSdkVersion = 21
sdkVersion = 27
buildToolsVersion = '27.0.3'
supportLibraryVersion = '27.0.2'
takisoftFixVersion = '27.0.2.0'
supportLibraryVersion = '27.1.0'
takisoftFixVersion = '27.1.0.0'
playServicesVersion = '11.8.0'
junitVersion = '4.12'
androidTestVersion = '1.0.1'
@@ -21,7 +23,8 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.google.gms:google-services:3.1.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'com.google.gms:google-services:3.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
+2 -2
View File
@@ -38,7 +38,7 @@ android {
task goBuild(type: Exec) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
executable "cmd.exe"
args "/c", "src/overture/make.bat " + minSdkVersion
args "/c", "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", "src/overture/clean.bat"
args "/c", "src\\overture\\clean.bat"
} else {
executable "sh"
args "-c", "src/overture/clean.bash"
-2
View File
@@ -1,8 +1,6 @@
#!/usr/bin/python
# -*- encoding: utf8 -*-
import itertools
import math
import sys
import IPy
@@ -43,8 +43,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.system.ErrnoException;
import java.net.InetAddress;
public class JniHelper {
static {
System.loadLibrary("jni-helper");
@@ -54,8 +52,7 @@ public class JniHelper {
public static void sigtermCompat(@NonNull Process process) throws Exception {
if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!");
int errno = sigterm(process);
if (errno != 0) throw Build.VERSION.SDK_INT >= 21
? new ErrnoException("kill", errno) : new Exception("kill failed: " + errno);
if (errno != 0) throw new ErrnoException("kill", errno);
}
@Deprecated // only implemented for before API 24
+1 -1
View File
@@ -1,4 +1,4 @@
APP_ABI := armeabi-v7a arm64-v8a x86
APP_PLATFORM := android-19
APP_PLATFORM := android-21
APP_STL := c++_static
NDK_TOOLCHAIN_VERSION := clang
+2 -2
View File
@@ -16,8 +16,8 @@
# executable program
#
# Modified by @Mygod, based on:
# https://android.googlesource.com/platform/ndk/+/f2e98f8c066aed59caf61163d4b87c2b858f9814/build/core/build-shared-library.mk
# https://android.googlesource.com/platform/ndk/+/f2e98f8c066aed59caf61163d4b87c2b858f9814/build/core/build-executable.mk
# https://android.googlesource.com/platform/ndk/+/a355a4e/build/core/build-shared-library.mk
# https://android.googlesource.com/platform/ndk/+/a355a4e/build/core/build-executable.mk
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
+4 -4
View File
@@ -1,10 +1,10 @@
@ECHO OFF
SET DIR=%CD%
SET DIR=%~dp0
SET DEPS=%DIR%\.deps
RD /SQ %DEPS%
RD /SQ %DIR%\go\bin
RD /SQ %DIR%\bin
RD /S /Q %DEPS%
RD /S /Q %DIR%\go\bin
RD /S /Q %DIR%\bin
ECHO "Successfully clean overture"
+2 -2
View File
@@ -12,7 +12,7 @@ TARGET=$DIR/bin
DEPS=$DIR/.deps
ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-arm
ANDROID_ARM64_TOOLCHAIN=$DEPS/android-toolchain-21-arm64
ANDROID_ARM64_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-arm64
ANDROID_X86_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-x86
ANDROID_ARM_CC=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-clang
@@ -35,7 +35,7 @@ fi
if [ ! -f "$ANDROID_ARM64_CC" ]; then
echo "Make standalone toolchain for ARM64 arch"
$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm64 \
--api 21 --install-dir $ANDROID_ARM64_TOOLCHAIN
--api $MIN_API --install-dir $ANDROID_ARM64_TOOLCHAIN
fi
if [ ! -f "$ANDROID_X86_CC" ]; then
+4 -4
View File
@@ -5,13 +5,13 @@ IF NOT DEFINED ANDROID_NDK_HOME (
SET ANDROID_NDK_HOME=%ANDROID_HOME%\ndk-bundle
)
SET DIR=%CD%
SET DIR=%~dp0
SET MIN_API=%1%
SET TARGET=%DIR%\bin
SET DEPS=%DIR%\.deps
SET ANDROID_ARM_TOOLCHAIN=%DEPS%\android-toolchain-%MIN_API%-arm
SET ANDROID_ARM64_TOOLCHAIN=%DEPS%\android-toolchain-21-arm64
SET ANDROID_ARM64_TOOLCHAIN=%DEPS%\android-toolchain-%MIN_API%-arm64
SET ANDROID_X86_TOOLCHAIN=%DEPS%\android-toolchain-%MIN_API%-x86
SET ANDROID_ARM_CC=%ANDROID_ARM_TOOLCHAIN%\bin\arm-linux-androideabi-clang
@@ -37,7 +37,7 @@ IF NOT EXIST %ANDROID_ARM_CC% (
IF NOT EXIST %ANDROID_ARM64_CC% (
ECHO "Make standalone toolchain for ARM64 arch"
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm64 ^
--api 21 --install-dir %ANDROID_ARM64_TOOLCHAIN%
--api %MIN_API% --install-dir %ANDROID_ARM64_TOOLCHAIN%
)
IF NOT EXIST %ANDROID_X86_CC% (
@@ -46,7 +46,7 @@ IF NOT EXIST %ANDROID_X86_CC% (
--api %MIN_API% --install-dir %ANDROID_X86_TOOLCHAIN%
)
IF NOT EXIST %DIR%\go\bin\go (
IF NOT EXIST %DIR%\go\bin\go.exe (
ECHO "Build the custom go"
PUSHD %DIR%\go\src
+286
View File
@@ -0,0 +1,286 @@
# https://github.com/arturbosch/detekt/blob/801994bd60cc759b181649356cea045b8125301b/detekt-cli/src/main/resources/default-detekt-config.yml
comments:
active: false
complexity:
active: true
ComplexCondition:
active: true
threshold: 3
ComplexInterface:
active: true
threshold: 10
includeStaticDeclarations: false
ComplexMethod:
active: true
threshold: 10
LabeledExpression:
active: true
LargeClass:
active: true
threshold: 150
LongMethod:
active: true
threshold: 20
LongParameterList:
active: true
threshold: 5
ignoreDefaultParameters: true
MethodOverloading:
active: false
NestedBlockDepth:
active: true
threshold: 3
StringLiteralDuplication:
active: true
threshold: 2
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
thresholdInFiles: 10
thresholdInClasses: 10
thresholdInInterfaces: 10
thresholdInObjects: 10
thresholdInEnums: 10
empty-blocks:
active: true
EmptyCatchBlock:
active: false
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames: 'toString,hashCode,equals,finalize'
InstanceOfCheckForException:
active: true
NotImplementedDeclaration:
active: true
PrintStackTrace:
active: false
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
SwallowedException:
active: true
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: true
ThrowingExceptionsWithoutMessageOrCause:
active: true
exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: true
exceptions:
- ArrayIndexOutOfBoundsException
- Error
- Exception
- IllegalMonitorStateException
- NullPointerException
- IndexOutOfBoundsException
- RuntimeException
- Throwable
TooGenericExceptionThrown:
active: true
exceptions:
- Error
- Exception
- NullPointerException
- Throwable
- RuntimeException
naming:
active: true
ClassNaming:
active: true
classPattern: '[A-Z$][a-zA-Z0-9$]*'
EnumNaming:
active: true
enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: true
forbiddenName: ''
FunctionMaxLength:
active: true
maximumFunctionNameLength: 30
FunctionMinLength:
active: true
minimumFunctionNameLength: 3
FunctionNaming:
active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
MatchingDeclarationName:
active: true
MemberNameEqualsClassName:
active: false
ObjectPropertyNaming:
active: true
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[a-z][A-Za-z\d]*'
privatePropertyPattern: '(_)?[a-z][A-Za-z0-9]*'
VariableMaxLength:
active: true
maximumVariableNameLength: 64
VariableMinLength:
active: false
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
performance:
active: true
ForEachOnRange:
active: true
SpreadOperator:
active: true
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExplicitGarbageCollectionCall:
active: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
UnconditionalJumpStatementInLoop:
active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: false
UnsafeCast:
active: false
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
active: true
style:
active: true
CollapsibleIfStatements:
active: true
DataClassContainsFunctions:
active: true
conversionFunctionPrefix: 'to'
EqualsNullCall:
active: true
ExpressionBodySyntax:
active: true
ForbiddenComment:
active: true
values: 'TODO:,FIXME:,STOPSHIP:'
ForbiddenImport:
active: true
imports: ''
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
excludedFunctions: 'describeContents'
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: false
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: false
excludeImportStatements: false
ModifierOrder:
active: true
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
OptionalAbstractKeyword:
active: true
OptionalReturnKeyword:
active: true
OptionalUnit:
active: true
OptionalWhenBraces:
active: true
ProtectedMemberInFinalClass:
active: true
RedundantVisibilityModifierRule:
active: true
ReturnCount:
active: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: true
ThrowsCount:
active: true
max: 2
UnnecessaryAbstractClass:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryParentheses:
active: true
UntilInsteadOfRangeTo:
active: true
UnusedImports:
active: true
UseDataClass:
active: false
UtilityClassWithPublicConstructor:
active: true
WildcardImport:
active: false
+26 -12
View File
@@ -1,3 +1,4 @@
import com.android.build.OutputFile
import java.util.regex.Matcher
import java.util.regex.Pattern
@@ -7,7 +8,7 @@ apply plugin: 'kotlin-android'
def getCurrentFlavor() {
String task = getGradle().getStartParameter().getTaskRequests().toString()
Matcher matcher = Pattern.compile("(assemble|generate)\\w*(Release|Debug)").matcher(task)
if (matcher.find()) return matcher.group(1).toLowerCase() else {
if (matcher.find()) return matcher.group(2).toLowerCase() else {
println "Warning: No match found for $task"
return "debug"
}
@@ -20,15 +21,15 @@ android {
applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 205
versionName "4.4.5"
versionCode 4050100
versionName "4.5.1"
testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary true
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
}
buildTypes {
debug {
multiDexEnabled true
pseudoLocalesEnabled true
}
release {
@@ -37,6 +38,15 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions.checkReleaseBuilds false
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86'
universalApk true
}
}
sourceSets.main.jniLibs.srcDirs +=
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
sourceSets.main.jniLibs.srcDirs += new File(project(':core').projectDir, "src/overture/bin")
@@ -48,25 +58,21 @@ dependencies {
implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.android.support:gridlayout-v7:$supportLibraryVersion"
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
implementation 'com.evernote:android-job:1.2.1'
implementation 'com.evernote:android-job:1.2.4'
implementation "com.google.android.gms:play-services-ads:$playServicesVersion"
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
implementation "com.google.android.gms:play-services-gcm:$playServicesVersion"
implementation "com.google.android.gms:play-services-vision:$playServicesVersion"
implementation "com.google.firebase:firebase-config:$playServicesVersion"
implementation 'com.j256.ormlite:ormlite-android:5.0'
implementation 'com.mikepenz:crossfader:1.5.1'
implementation 'com.mikepenz:fastadapter:3.0.4'
implementation 'com.mikepenz:iconics-core:3.0.0'
implementation 'com.mikepenz:materialdrawer:6.0.2'
implementation 'com.mikepenz:materialize:1.1.2'
implementation 'com.mikepenz:materialdrawer:6.0.6'
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
implementation "com.takisoft.fix:preference-v7-simplemenu:$takisoftFixVersion"
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2'
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'eu.chainfire:libsuperuser:1.0.0.201704021214'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation 'net.glxn.qrgen:android:2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion"
@@ -76,3 +82,11 @@ repositories {
}
apply plugin: 'com.google.gms.google-services'
ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3]
if (getCurrentFlavor() == 'release') android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def offset = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (offset != null) output.versionCodeOverride = variant.versionCode + offset
}
}
+6 -7
View File
@@ -71,7 +71,7 @@
<activity
android:name=".VpnRequestActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:theme="@style/Theme.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
@@ -169,17 +169,16 @@
android:process=":bg"
android:directBootAware="true"/>
<service android:name="com.evernote.android.job.v14.PlatformAlarmService"
android:process=":bg"
android:directBootAware="true"/>
tools:node="remove"/>
<service android:name="com.evernote.android.job.v14.PlatformAlarmServiceExact"
tools:node="remove"/>
<service android:name="com.evernote.android.job.gcm.PlatformGcmService"
android:process=":bg"
android:directBootAware="true"/>
tools:node="remove"/>
<service android:name="com.evernote.android.job.JobRescheduleService"
android:process=":bg"
android:directBootAware="true"/>
<receiver android:name="com.evernote.android.job.v14.PlatformAlarmReceiver"
android:process=":bg"
android:directBootAware="true"/>
tools:node="remove"/>
<receiver android:name="com.evernote.android.job.JobBootReceiver"
android:process=":bg"
android:directBootAware="true"/>
@@ -32,14 +32,15 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Handler
import android.os.LocaleList
import android.os.Looper
import android.support.annotation.RequiresApi
import android.support.v4.os.UserManagerCompat
import android.support.v7.app.AppCompatDelegate
import android.util.Log
import android.widget.Toast
import com.evernote.android.job.JobConstants
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobManagerCreateException
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncJob
import com.github.shadowsocks.bg.BaseService
@@ -59,20 +60,11 @@ import com.j256.ormlite.logger.LocalLog
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat
import java.io.File
import java.io.IOException
import java.util.*
class App : Application() {
companion object {
lateinit var app: App
private const val TAG = "ShadowsocksApplication"
// The ones in Locale doesn't have script included
private val SIMPLIFIED_CHINESE by lazy {
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hans-CN") else Locale.SIMPLIFIED_CHINESE
}
private val TRADITIONAL_CHINESE by lazy {
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hant-TW") else Locale.TRADITIONAL_CHINESE
}
}
val handler by lazy { Handler(Looper.getMainLooper()) }
@@ -112,59 +104,11 @@ class App : Application() {
t.printStackTrace()
}
private fun checkChineseLocale(config: Configuration) {
fun check(locale: Locale): Locale? {
if (locale.language != "zh") return null
when (locale.country) { "CN", "TW" -> return null }
if (Build.VERSION.SDK_INT >= 21) when (locale.script) {
"Hans" -> return SIMPLIFIED_CHINESE
"Hant" -> return TRADITIONAL_CHINESE
else -> Log.w(TAG, "Unknown zh locale script: ${locale.script}. Falling back to trying countries...")
}
when (locale.country) {
"SG" -> return SIMPLIFIED_CHINESE
"HK", "MO" -> return TRADITIONAL_CHINESE
}
Log.w(TAG, "Unknown zh locale: %s. Falling back to zh-Hans-CN..."
.format(Locale.ENGLISH, if (Build.VERSION.SDK_INT >= 21) locale.toLanguageTag() else locale))
return SIMPLIFIED_CHINESE
}
if (Build.VERSION.SDK_INT >= 24) @RequiresApi(24) {
val localeList = config.locales
var changed = false
val newList = Array<Locale>(localeList.size(), { i ->
val locale = localeList[i]
val newLocale = check(locale)
if (newLocale == null) locale else {
changed = true
newLocale
}
})
if (changed) {
val newConfig = Configuration(config)
newConfig.locales = LocaleList(*(newList.distinct().toTypedArray()))
val res = resources
res.updateConfiguration(newConfig, res.displayMetrics)
}
} else {
@Suppress("DEPRECATION")
val newLocale = check(config.locale)
if (newLocale != null) {
val newConfig = Configuration(config)
@Suppress("DEPRECATION")
newConfig.locale = newLocale
val res = resources
res.updateConfiguration(newConfig, res.displayMetrics)
}
}
}
override fun onCreate() {
super.onCreate()
app = this
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
checkChineseLocale(resources.configuration)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java)
@@ -184,11 +128,16 @@ class App : Application() {
remoteConfig.fetch().addOnCompleteListener {
if (it.isSuccessful) remoteConfig.activateFetched() else Log.e(TAG, "Failed to fetch config")
}
JobManager.create(deviceContext).addJobCreator(AclSyncJob)
try {
JobManager.create(deviceContext).addJobCreator(AclSyncJob)
} catch (e: JobManagerCreateException) {
Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()
app.track(e)
}
// handle data restored
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update()
TcpFastOpen.enabled(DataStore.publicStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
TcpFastOpen.enabledAsync(DataStore.publicStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
val assetManager = assets
for (dir in arrayOf("acl", "overture"))
@@ -208,7 +157,6 @@ class App : Application() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
checkChineseLocale(newConfig)
updateNotificationChannels()
}
@@ -226,13 +174,16 @@ class App : Application() {
}
}
fun listenForPackageChanges(callback: () -> Unit): BroadcastReceiver {
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit): BroadcastReceiver {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
val result = broadcastReceiver { _, intent ->
if (intent.action != Intent.ACTION_PACKAGE_REMOVED ||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) callback()
val result = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
callback()
if (onetime) app.unregisterReceiver(this)
}
}
app.registerReceiver(result, filter)
return result
@@ -34,6 +34,7 @@ import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.support.design.widget.Snackbar
import android.support.v4.app.TaskStackBuilder
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DefaultItemAnimator
@@ -43,7 +44,6 @@ import android.support.v7.widget.Toolbar
import android.view.*
import android.widget.ImageView
import android.widget.Switch
import android.widget.Toast
import com.futuremind.recyclerviewfastscroll.FastScroller
import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
import com.github.shadowsocks.App.Companion.app
@@ -63,11 +63,14 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager): List<ProxiedApp> {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) { cachedApps = null }
AppManager.instance?.reloadApps()
}
return synchronized(AppManager) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
cachedApps = null
}
AppManager.instance?.reloadApps()
}
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter { it.packageName != app.packageName &&
@@ -231,14 +234,14 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
ProfileManager.updateProfile(it)
}
if (DataStore.directBootAware) DirectBoot.update()
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show()
} else Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
Snackbar.make(appListView, R.string.action_apply_all, Snackbar.LENGTH_LONG).show()
} else Snackbar.make(appListView, R.string.action_export_err, Snackbar.LENGTH_LONG).show()
return true
}
R.id.action_export -> {
clipboard.primaryClip = ClipData.newPlainText(Key.individual,
"${DataStore.bypass}\n${DataStore.individual}")
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
Snackbar.make(appListView, R.string.action_export_msg, Snackbar.LENGTH_LONG).show()
return true
}
R.id.action_import -> {
@@ -251,23 +254,21 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
bypassSwitch.isChecked = enabled.toBoolean()
DataStore.individual = apps
DataStore.dirty = true
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
Snackbar.make(appListView, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedApps(apps)
reloadApps()
return true
} catch (_: IllegalArgumentException) { }
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
Snackbar.make(appListView, R.string.action_import_err, Snackbar.LENGTH_LONG).show()
}
}
return false
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return if (keyCode == KeyEvent.KEYCODE_MENU)
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu()
else super.onKeyUp(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?) = if (keyCode == KeyEvent.KEYCODE_MENU)
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu()
else super.onKeyUp(keyCode, event)
override fun onDestroy() {
instance = null
@@ -26,7 +26,6 @@ import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.Preference
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
@@ -21,8 +21,10 @@
package com.github.shadowsocks
import android.app.Activity
import android.app.PendingIntent
import android.app.backup.BackupManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.net.VpnService
@@ -56,13 +58,11 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.responseLength
import com.github.shadowsocks.utils.thread
import com.github.shadowsocks.widget.ServiceButton
import com.mikepenz.crossfader.Crossfader
import com.mikepenz.crossfader.view.CrossFadeSlidingPaneLayout
import com.mikepenz.materialdrawer.Drawer
import com.mikepenz.materialdrawer.DrawerBuilder
import com.mikepenz.materialdrawer.interfaces.ICrossfader
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
@@ -81,12 +81,14 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
private const val DRAWER_FAQ = 4L
private const val DRAWER_CUSTOM_RULES = 5L
fun pendingIntent(context: Context) = PendingIntent.getActivity(context, 0,
Intent(context, MainActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
var stateListener: ((Int) -> Unit)? = null
}
// UI
private lateinit var fab: ServiceButton
internal var crossfader: Crossfader<CrossFadeSlidingPaneLayout>? = null
internal lateinit var drawer: Drawer
private var previousSelectedDrawer: Long = 0 // it's actually lateinit
@@ -168,9 +170,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
url.openConnection(Proxy(Proxy.Type.SOCKS,
InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.connectTimeout = 10000
conn.readTimeout = 10000
conn.useCaches = false
val (success, result) = try {
val start = SystemClock.elapsedRealtime()
@@ -178,8 +179,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L)
Pair(true, getString(R.string.connection_test_available, elapsed))
else throw Exception(getString(R.string.connection_test_error_status_code, code))
} catch (e: Exception) {
else throw IOException(getString(R.string.connection_test_error_status_code, code))
} catch (e: IOException) {
Pair(false, getString(R.string.connection_test_error, e.message))
} finally {
conn.disconnect()
@@ -196,6 +197,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
override fun onServiceConnected(service: IShadowsocksService) = changeState(service.state)
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun binderDied() {
super.binderDied()
app.handler.post {
connection.disconnect()
Executable.killAll()
@@ -213,7 +215,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
val drawerBuilder = DrawerBuilder()
drawer = DrawerBuilder()
.withActivity(this)
.withTranslucentStatusBar(true)
.withHeader(R.layout.layout_header)
@@ -250,28 +252,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
.withOnDrawerItemClickListener(this)
.withActionBarDrawerToggle(true)
.withSavedInstance(savedInstanceState)
val miniDrawerWidth = resources.getDimension(R.dimen.material_mini_drawer_item)
if (resources.displayMetrics.widthPixels >=
resources.getDimension(R.dimen.profile_item_max_width) + miniDrawerWidth) {
drawer = drawerBuilder.withGenerateMiniDrawer(true).buildView()
val crossfader = Crossfader<CrossFadeSlidingPaneLayout>()
this.crossfader = crossfader
crossfader
.withContent(findViewById(android.R.id.content))
.withFirst(drawer.slider, resources.getDimensionPixelSize(R.dimen.material_drawer_width))
.withSecond(drawer.miniDrawer.build(this), miniDrawerWidth.toInt())
.withSavedInstance(savedInstanceState)
.build()
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL)
crossfader.crossFadeSlidingPaneLayout.setShadowDrawableRight(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_right))
else crossfader.crossFadeSlidingPaneLayout.setShadowDrawableLeft(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_left))
drawer.miniDrawer.withCrossFader(object : ICrossfader { // a wrapper is needed
override fun isCrossfaded(): Boolean = crossfader.isCrossFaded
override fun crossfade() = crossfader.crossFade()
})
} else drawer = drawerBuilder.build()
.build()
if (savedInstanceState == null) displayFragment(ProfilesFragment())
previousSelectedDrawer = drawer.currentSelection
@@ -395,7 +376,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
drawer.saveInstanceState(outState)
crossfader?.saveInstanceState(outState)
}
override fun onDestroy() {
@@ -21,6 +21,7 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Intent
import android.os.Build
import android.os.Bundle
@@ -60,6 +61,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
private lateinit var plugin: IconListPreference
private lateinit var pluginConfigure: EditTextPreference
private lateinit var pluginConfiguration: PluginConfiguration
private lateinit var receiver: BroadcastReceiver
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore
@@ -80,13 +82,11 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
val serviceMode = DataStore.serviceMode
findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference
if (Build.VERSION.SDK_INT < 21) isProxyApps.parent!!.removePreference(isProxyApps) else {
isProxyApps.isEnabled = serviceMode == Key.modeVpn
isProxyApps.setOnPreferenceClickListener {
startActivity(Intent(activity, AppManager::class.java))
isProxyApps.isChecked = true
false
}
isProxyApps.isEnabled = serviceMode == Key.modeVpn
isProxyApps.setOnPreferenceClickListener {
startActivity(Intent(activity, AppManager::class.java))
isProxyApps.isChecked = true
false
}
findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy
plugin = findPreference(Key.plugin) as IconListPreference
@@ -104,7 +104,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
}
pluginConfigure.onPreferenceChangeListener = this
initPlugins()
app.listenForPackageChanges { initPlugins() }
receiver = app.listenForPackageChanges(false) { initPlugins() }
DataStore.privateStore.registerChangeListener(this)
}
@@ -181,31 +181,30 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
} else super.onActivityResult(requestCode, resultCode, data)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_delete -> {
val activity = activity!!
AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, { _, _ ->
ProfileManager.delProfile(profile.id)
activity.finish()
})
.setNegativeButton(R.string.no, null)
.create()
.show()
true
}
R.id.action_apply -> {
saveAndExit()
true
}
else -> false
override fun onMenuItemClick(item: MenuItem) = when (item.itemId) {
R.id.action_delete -> {
val activity = activity!!
AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, { _, _ ->
ProfileManager.delProfile(profile.id)
activity.finish()
})
.setNegativeButton(R.string.no, null)
.create()
.show()
true
}
R.id.action_apply -> {
saveAndExit()
true
}
else -> false
}
override fun onDestroy() {
DataStore.privateStore.unregisterChangeListener(this)
app.unregisterReceiver(receiver)
super.onDestroy()
}
}
@@ -37,7 +37,6 @@ import android.view.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.TrafficMonitor
@@ -230,7 +229,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) = holder.bind(profiles[position])
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ProfileViewHolder = ProfileViewHolder(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder = ProfileViewHolder(
LayoutInflater.from(parent!!.context).inflate(R.layout.layout_profile, parent, false))
override fun getItemCount(): Int = profiles.size
override fun getItemId(position: Int): Long = profiles[position].id.toLong()
@@ -370,12 +369,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
val profiles = Profile.findAll(clipboard.primaryClip.getItemAt(0).text).toList()
if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) }
Toast.makeText(activity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_msg,
Snackbar.LENGTH_LONG).show()
return true
}
} catch (exc: Exception) {
app.track(exc)
}
} catch (_: IndexOutOfBoundsException) { }
Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
.show()
true
@@ -386,10 +384,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R.id.action_export -> {
val profiles = ProfileManager.getAllProfiles()
if (profiles != null) {
Snackbar.make(activity!!.findViewById(R.id.snackbar), if (profiles != null) {
clipboard.primaryClip = ClipData.newPlainText(null, profiles.joinToString("\n"))
Toast.makeText(activity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
} else Toast.makeText(activity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
R.string.action_export_msg
} else R.string.action_export_err, Snackbar.LENGTH_LONG).show()
true
}
else -> false
@@ -20,29 +20,40 @@
package com.github.shadowsocks
import android.content.pm.PackageManager
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutManager
import android.graphics.BitmapFactory
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.app.TaskStackBuilder
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.util.Log
import android.util.SparseArray
import android.view.MenuItem
import android.widget.Toast
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.resolveResourceId
import com.google.zxing.Result
import me.dm7.barcodescanner.zxing.ZXingScannerView
import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture
import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic
import com.google.android.gms.vision.Frame
import com.google.android.gms.vision.barcode.Barcode
import com.google.android.gms.vision.barcode.BarcodeDetector
import xyz.belvi.mobilevisionbarcodescanner.BarcodeRetriever
class ScannerActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, BarcodeRetriever {
companion object {
private const val MY_PERMISSIONS_REQUEST_CAMERA = 1
private const val TAG = "ScannerActivity"
private const val REQUEST_IMPORT = 2
private const val REQUEST_IMPORT_OR_FINISH = 3
}
private lateinit var scannerView: ZXingScannerView
private fun navigateUp() {
val intent = parentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
@@ -52,45 +63,79 @@ class ScannerActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
if (try {
(getSystemService(Context.CAMERA_SERVICE) as CameraManager).cameraIdList.isEmpty()
} catch (_: CameraAccessException) {
true
}) {
startImport()
return
}
setContentView(R.layout.layout_scanner)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = title
toolbar.setNavigationIcon(theme.resolveResourceId(R.attr.homeAsUpIndicator))
toolbar.setNavigationOnClickListener { navigateUp() }
scannerView = findViewById(R.id.scanner)
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
toolbar.inflateMenu(R.menu.scanner_menu)
toolbar.setOnMenuItemClickListener(this)
(supportFragmentManager.findFragmentById(R.id.barcode) as BarcodeCapture).setRetrieval(this)
}
override fun onResume() {
super.onResume()
val permissionCheck = ContextCompat.checkSelfPermission(this,
android.Manifest.permission.CAMERA)
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
scannerView.startCamera() // Start camera on resume
} else ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CAMERA),
MY_PERMISSIONS_REQUEST_CAMERA)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA)
if (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED) {
scannerView.setResultHandler(this)
scannerView.startCamera()
} else {
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
finish()
}
else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onPause() {
super.onPause()
scannerView.stopCamera() // Stop camera on pause
}
override fun handleResult(rawResult: Result?) {
Profile.findAll(rawResult?.text).forEach { ProfileManager.createProfile(it) }
override fun onRetrieved(barcode: Barcode) = runOnUiThread {
Profile.findAll(barcode.rawValue).forEach { ProfileManager.createProfile(it) }
navigateUp()
}
override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false)
override fun onBitmapScanned(sparseArray: SparseArray<Barcode>?) { }
override fun onRetrievedFailed(reason: String?) {
Log.w(TAG, reason)
}
override fun onPermissionRequestDenied() {
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
startImport()
}
override fun onMenuItemClick(item: MenuItem) = when (item.itemId) {
R.id.action_import -> {
startImport(true)
true
}
else -> false
}
private fun startImport(manual: Boolean = false) = startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("image/*")
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true), if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
var success = false
val detector = BarcodeDetector.Builder(this)
.setBarcodeFormats(Barcode.QR_CODE)
.build()
if (detector.isOperational) {
var list = listOfNotNull(data?.data)
val clipData = data?.clipData
if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri }
val resolver = contentResolver
for (uri in list) try {
val barcodes = detector.detect(Frame.Builder()
.setBitmap(BitmapFactory.decodeStream(resolver.openInputStream(uri))).build())
for (i in 0 until barcodes.size()) Profile.findAll(barcodes.valueAt(i).rawValue).forEach {
ProfileManager.createProfile(it)
success = true
}
} catch (e: Exception) {
app.track(e)
}
} else Log.w(TAG, "Google vision isn't operational.")
Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err,
Toast.LENGTH_SHORT).show()
navigateUp()
} else if (requestCode == REQUEST_IMPORT_OR_FINISH) navigateUp()
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
}
@@ -48,7 +48,9 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
* Different from Android framework, this method will be called even when you call `detachService`.
*/
fun onServiceDisconnected() { }
override fun binderDied() { }
override fun binderDied() {
connection.service = null
}
}
private var connectionActive = false
@@ -109,6 +111,8 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
connectionActive = false
if (instance.listenForDeath) binder?.unlinkToDeath(instance, 0)
binder = null
if (instance.serviceCallback != null)
service?.stopListeningForBandwidth(instance.serviceCallback)
service = null
}
}
@@ -35,7 +35,7 @@ open class ToolbarFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar)
val activity = activity as MainActivity
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
activity.drawer.setToolbar(activity, toolbar, true)
}
open fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
@@ -20,7 +20,6 @@
package com.github.shadowsocks
import android.app.Activity
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
@@ -28,13 +27,14 @@ import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.broadcastReceiver
class VpnRequestActivity : Activity(), ShadowsocksConnection.Interface {
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
companion object {
private const val TAG = "VpnRequestActivity"
private const val REQUEST_CONNECT = 1
@@ -56,11 +56,9 @@ class VpnRequestActivity : Activity(), ShadowsocksConnection.Interface {
}
override fun onServiceConnected(service: IShadowsocksService) {
app.handler.postDelayed({
val intent = VpnService.prepare(this)
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
else startActivityForResult(intent, REQUEST_CONNECT)
}, 1000)
val intent = VpnService.prepare(this)
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
else startActivityForResult(intent, REQUEST_CONNECT)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -23,22 +23,21 @@ package com.github.shadowsocks.acl
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.design.widget.TextInputLayout
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.support.v7.widget.helper.ItemTouchHelper
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import android.text.Editable
import android.text.TextWatcher
import android.view.*
import android.widget.*
import com.futuremind.recyclerviewfastscroll.FastScroller
import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
import com.github.shadowsocks.App.Companion.app
@@ -48,12 +47,14 @@ import com.github.shadowsocks.ToolbarFragment
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.widget.UndoSnackbarManager
import java.net.IDN
import java.net.MalformedURLException
import java.net.URL
import java.util.*
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ActionMode.Callback {
companion object {
private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
@@ -70,6 +71,92 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
Domain,
Url;
}
private inner class AclRuleDialog(item: Any = "") : TextWatcher, AdapterView.OnItemSelectedListener {
val builder: AlertDialog.Builder
val templateSelector: Spinner
val editText: EditText
private val inputLayout: TextInputLayout
private lateinit var dialog: AlertDialog
private lateinit var positive: Button
init {
val activity = activity!!
val view = activity.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
templateSelector = view.findViewById(R.id.template_selector)
editText = view.findViewById(R.id.content)
inputLayout = view.findViewById(R.id.content_layout)
when (item) {
is String -> {
val match = PATTERN_DOMAIN.find(item)
if (match != null) {
templateSelector.setSelection(Template.Domain.ordinal)
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
} else {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item)
}
}
is URL -> {
templateSelector.setSelection(Template.Url.ordinal)
editText.setText(item.toString())
}
else -> {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item.toString())
}
}
templateSelector.onItemSelectedListener = this
editText.addTextChangedListener(this)
builder = AlertDialog.Builder(activity)
.setTitle(R.string.edit_rule)
.setNegativeButton(android.R.string.cancel, null)
.setView(view)
}
fun show() {
dialog = builder.create()
dialog.show()
positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
validate()
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(s: Editable) = validate(value = s)
override fun onNothingSelected(parent: AdapterView<*>?) = check(false)
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = validate(position)
private fun validate(template: Int = templateSelector.selectedItemPosition, value: Editable = editText.text) {
val error = when (Template.values()[template]) {
Template.Generic -> if (value.isEmpty()) "" else null
Template.Domain -> try {
IDN.toASCII(value.toString(), IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
null
} catch (e: IllegalArgumentException) {
e.cause?.message ?: e.message
}
Template.Url -> try {
URL(value.toString())
null
} catch (e: MalformedURLException) {
e.message
}
}
inputLayout.error = error
positive.isEnabled = error == null
}
fun add(): Int? {
val text = editText.text.toString()
return when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> adapter.addToProxy(text)
Template.Domain -> adapter.addHostname(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
Template.Url -> adapter.addURL(URL(text))
}
}
}
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, View.OnLongClickListener {
@@ -103,19 +190,18 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
override fun onClick(v: View?) {
if (selectedItems.isNotEmpty()) onLongClick(v) else {
val (templateSelector, editText, dialog) = createAclRuleDialog(item)
dialog
val dialog = AclRuleDialog(item)
dialog.builder
.setNeutralButton(R.string.delete, { _, _ ->
adapter.remove(item)
undoManager.remove(Pair(-1, item))
})
.setPositiveButton(android.R.string.ok, { _, _ ->
adapter.remove(item)
val index = adapter.addFromTemplate(templateSelector.selectedItemPosition,
editText.text.toString()) ?: adapter.add(item)
val index = dialog.add() ?: adapter.add(item)
if (index != null) list.post { list.scrollToPosition(index) }
})
.create().show()
dialog.show()
}
}
override fun onLongClick(p0: View?): Boolean {
@@ -142,16 +228,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
override fun getItemCount(): Int = acl.subnets.size() + acl.hostnames.size() + acl.urls.size()
override fun getSectionTitle(i: Int): String {
val j = i - acl.subnets.size()
return try {
(if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else {
val k = j - acl.hostnames.size()
if (k < 0) {
val hostname = acl.hostnames[j]
// don't convert IDN yet
PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname
} else acl.urls[k].host
}).substring(0, 1)
} catch (_: IndexOutOfBoundsException) { " " }
return (if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else {
val k = j - acl.hostnames.size()
if (k < 0) {
val hostname = acl.hostnames[j]
// don't convert IDN yet
PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname
} else acl.urls[k].host
}).firstOrNull()?.toString() ?: " "
}
private fun apply() {
@@ -207,20 +291,6 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
.forEach { if (result == null) result = it }
return result
}
fun addFromTemplate(templateValue: Int, text: String): Int? {
val template = Template.values()[templateValue]
return when (template) {
Template.Generic -> addToProxy(text)
Template.Domain -> try {
addHostname(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
} catch (exc: IllegalArgumentException) {
Toast.makeText(activity, exc.message, Toast.LENGTH_SHORT).show()
null
}
Template.Url -> addURL(URL(text))
}
}
fun remove(i: Int) {
val j = i - acl.subnets.size()
@@ -288,44 +358,12 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private val selectedItems = HashSet<Any>()
private val adapter by lazy { AclRulesAdapter() }
private lateinit var list: RecyclerView
private var selectionItem: MenuItem? = null
private var mode: ActionMode? = null
private lateinit var undoManager: UndoSnackbarManager<Any>
private val clipboard by lazy { activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
private fun onSelectedItemsUpdated() {
val selectionItem = selectionItem
if (selectionItem != null) selectionItem.isVisible = selectedItems.isNotEmpty()
}
private fun createAclRuleDialog(item: Any = ""): Triple<Spinner, EditText, AlertDialog.Builder> {
val view = activity!!.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById<Spinner>(R.id.template_selector)
val editText = view.findViewById<EditText>(R.id.content)
when (item) {
is String -> {
val match = PATTERN_DOMAIN.find(item)
if (match != null) {
templateSelector.setSelection(Template.Domain.ordinal)
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
} else {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item)
}
}
is URL -> {
templateSelector.setSelection(Template.Url.ordinal)
editText.setText(item.toString())
}
else -> {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item.toString())
}
}
return Triple(templateSelector, editText, AlertDialog.Builder(activity!!)
.setTitle(R.string.edit_rule)
.setNegativeButton(android.R.string.cancel, null)
.setView(view))
if (selectedItems.isEmpty()) mode?.finish() else if (mode == null) mode = toolbar.startActionMode(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
@@ -345,9 +383,6 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
toolbar.setTitle(R.string.custom_rules)
toolbar.inflateMenu(R.menu.custom_rules_menu)
toolbar.setOnMenuItemClickListener(this)
val selectionItem = toolbar.menu.findItem(R.id.selection)
selectionItem.isVisible = selectedItems.isNotEmpty()
this.selectionItem = selectionItem
list = view.findViewById(R.id.list)
list.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
@@ -365,10 +400,9 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
override fun onBackPressed(): Boolean {
return if (selectedItems.isNotEmpty()) {
selectedItems.clear()
onSelectedItemsUpdated()
adapter.notifyDataSetChanged()
val mode = mode
return if (mode != null) {
mode.finish()
true
} else super.onBackPressed()
}
@@ -395,29 +429,10 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_select_all -> {
adapter.selectAll()
true
}
R.id.action_cut -> {
copySelected()
adapter.removeSelected()
true
}
R.id.action_copy -> {
copySelected()
true
}
R.id.action_delete -> {
adapter.removeSelected()
true
}
R.id.action_manual_settings -> {
val (templateSelector, editText, dialog) = createAclRuleDialog()
dialog.setPositiveButton(android.R.string.ok, { _, _ ->
adapter.addFromTemplate(templateSelector.selectedItemPosition, editText.text.toString())
}).create().show()
val dialog = AclRuleDialog()
dialog.builder.setPositiveButton(android.R.string.ok, { _, _ -> dialog.add() })
dialog.show()
true
}
R.id.action_import -> {
@@ -442,6 +457,53 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
override fun onDetach() {
undoManager.flush()
mode?.finish()
super.onDetach()
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val activity = activity!!
val window = activity.window
// In the end material_grey_100 is used for background, see AppCompatDrawableManager (very complicated)
if (Build.VERSION.SDK_INT >= 23) {
window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_300)
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_600)
activity.menuInflater.inflate(R.menu.custom_rules_selection, menu)
toolbar.touchscreenBlocksFocus = true
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = when (item.itemId) {
R.id.action_select_all -> {
adapter.selectAll()
true
}
R.id.action_cut -> {
copySelected()
adapter.removeSelected()
true
}
R.id.action_copy -> {
copySelected()
true
}
R.id.action_delete -> {
adapter.removeSelected()
true
}
else -> false
}
override fun onDestroyActionMode(mode: ActionMode) {
val activity = activity!!
val window = activity.window
window.statusBarColor = ContextCompat.getColor(activity,
activity.theme.resolveResourceId(android.R.attr.statusBarColor))
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
toolbar.touchscreenBlocksFocus = false
selectedItems.clear()
onSelectedItemsUpdated()
adapter.notifyDataSetChanged()
this.mode = null
}
}
@@ -31,7 +31,6 @@ import android.os.RemoteCallbackList
import android.support.v4.os.UserManagerCompat
import android.util.Base64
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import com.github.shadowsocks.acl.Acl
@@ -88,17 +87,14 @@ object BaseService {
val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) {
Action.RELOAD -> service.forceLoad()
else -> {
Toast.makeText(service as Context, R.string.stopping, Toast.LENGTH_SHORT).show()
service.stopRunner(true)
}
else -> service.stopRunner(true)
}
}
var closeReceiverRegistered = false
val binder = object : IShadowsocksService.Stub() {
override fun getState(): Int = this@Data.state
override fun getProfileName(): String = profile?.formattedName ?: "Idle"
override fun getProfileName(): String = profile?.name ?: "Idle"
override fun registerCallback(cb: IShadowsocksServiceCallback) {
callbacks.register(cb)
@@ -149,18 +145,18 @@ object BaseService {
}
internal fun updateTrafficTotal(tx: Long, rx: Long) {
val profile = profile ?: return
val p = ProfileManager.getProfile(profile.id) ?: return // profile may have host, etc. modified
p.tx += tx
p.rx += rx
ProfileManager.updateProfile(p)
// this.profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
val profile = ProfileManager.getProfile((profile ?: return).id) ?: return
profile.tx += tx
profile.rx += rx
ProfileManager.updateProfile(profile)
app.handler.post {
if (bandwidthListeners.isNotEmpty()) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(p.id)
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(profile.id)
} catch (_: Exception) { } // ignore
}
callbacks.finishBroadcast()
@@ -185,9 +181,9 @@ object BaseService {
.put("plugin_opts", plugin.toString())
}
// sensitive Shadowsocks config is stored in
val file = File((if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
val file = File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
app.deviceContext.noBackupFilesDir // only API 24+ will be in locked state
}), CONFIG_FILE)
}, CONFIG_FILE)
shadowsocksConfigFile = file
file.writeText(config.toString())
return file
@@ -221,9 +217,9 @@ object BaseService {
false
} else true
fun forceLoad() {
val p = app.currentProfile
val profile = app.currentProfile
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
if (!checkProfile(p)) return
if (!checkProfile(profile)) return
val s = data.state
when (s) {
STOPPED -> startRunner()
@@ -322,6 +318,7 @@ object BaseService {
stopRunner(true, getString(R.string.profile_empty))
return Service.START_NOT_STICKY
}
profile.name = profile.formattedName // save name for later queries
data.profile = profile
TrafficMonitor.reset()
@@ -20,56 +20,23 @@
package com.github.shadowsocks.bg
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.os.Build
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.BuildConfig
import okhttp3.Dns as Okdns
import org.xbill.DNS.*
import java.net.Inet6Address
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.UnknownHostException
import java.util.*
import java.io.IOException
import java.net.*
object Dns {
private const val TAG = "Dns"
@Throws(Exception::class)
fun getDnsResolver(context: Context): String {
val dnsResolvers = getDnsResolvers(context)
if (dnsResolvers.isEmpty()) throw Exception("Couldn't find an active DNS resolver")
val dnsResolver = dnsResolvers.iterator().next().toString()
if (dnsResolver.startsWith("/")) return dnsResolver.substring(1)
return dnsResolver
}
@Throws(Exception::class)
private fun getDnsResolvers(context: Context): Collection<InetAddress> {
val addresses = ArrayList<InetAddress>()
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val classLinkProperties = Class.forName("android.net.LinkProperties")
val getActiveLinkPropertiesMethod = ConnectivityManager::class.java.getMethod("getActiveLinkProperties")
val linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager)
if (linkProperties != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val dnses = classLinkProperties.getMethod("getDnses").invoke(linkProperties) as Collection<*>
dnses.mapTo(addresses) { it as InetAddress }
} else addresses.addAll((linkProperties as LinkProperties).dnsServers)
}
return addresses
}
private val hasIPv6Support get() = try {
val result = NetworkInterface.getNetworkInterfaces().asSequence().flatMap { it.inetAddresses.asSequence() }
.count { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } > 0
.any { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress }
if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected")
result
} catch (ex: Exception) {
Log.e(TAG, "Failed to get interfaces' addresses.")
} catch (ex: SocketException) {
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
app.track(ex)
false
}
@@ -90,7 +57,7 @@ object Dns {
Type.AAAA -> return (r as AAAARecord).address.hostAddress
}
}
} catch (_: Exception) { }
} catch (_: IOException) { }
return null
}
private fun resolve(host: String): String? = try {
@@ -25,6 +25,7 @@ import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper
import java.io.File
import java.io.FileNotFoundException
object Executable {
const val REDSOCKS = "libredsocks.so"
@@ -37,7 +38,11 @@ object Executable {
fun killAll() {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
val exe = File(File(process, "cmdline").readText().split(Character.MIN_VALUE, limit = 2).first())
val exe = File(try {
File(process, "cmdline").readText()
} catch (ignore: FileNotFoundException) {
continue
}.split(Character.MIN_VALUE, limit = 2).first())
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) {
val errno = JniHelper.sigkill(process.name.toInt())
if (errno != 0) Log.w("kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed with $errno")
@@ -20,7 +20,6 @@
package com.github.shadowsocks.bg
import android.content.Context
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore
@@ -47,14 +46,14 @@ object LocalDnsService {
val data = data
val profile = data.profile!!
fun makeDns(name: String, address: String, edns: Boolean = true): JSONObject {
fun makeDns(name: String, address: String, timeout: Int, edns: Boolean = true): JSONObject {
val dns = JSONObject()
.put("Name", name)
.put("Address", (when (address.parseNumericAddress()) {
.put("Address", when (address.parseNumericAddress()) {
is Inet6Address -> "[$address]"
else -> address
}) + ":53")
.put("Timeout", 3)
})
.put("Timeout", timeout)
.put("EDNSClientSubnet", JSONObject().put("Policy", "disable"))
if (edns) dns
.put("Protocol", "tcp")
@@ -73,18 +72,14 @@ object LocalDnsService {
.put("MinimumTTL", 120)
.put("CacheSize", 4096)
val remoteDns = JSONArray(profile.remoteDns.split(",")
.mapIndexed { i, dns -> makeDns("UserDef-" + i, dns.trim()) })
.mapIndexed { i, dns -> makeDns("UserDef-" + i,
dns.trim() + ":53", 9) })
val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "119.29.29.29", false),
makeDns("Primary-2", "114.114.114.114", false)
makeDns("Primary-1", "119.29.29.29:53", 3, false),
makeDns("Primary-2", "114.114.114.114:53", 3, false),
makeDns("Primary-3", "208.67.222.222:443", 3, false)
))
this as Context
try {
val localLinkDns = Dns.getDnsResolver(this)
localDns.put(makeDns("Primary-3", localLinkDns, false))
} catch (_: Exception) { } // ignore
when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> config
.put("PrimaryDNS", localDns)
@@ -41,7 +41,7 @@ abstract class LocalSocketListener(protected val tag: String) : Thread() {
* Inherited class do not need to close input/output streams as they will be closed automatically.
*/
protected abstract fun accept(socket: LocalSocket)
override final fun run() {
final override fun run() {
socketFile.delete() // It's a must-have to close and reuse previous local socket.
LocalSocket().use { localSocket ->
val serverSocket = try {
@@ -23,7 +23,6 @@ package com.github.shadowsocks.bg
import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.github.shadowsocks.database.Profile
/**
* Shadowsocks service at its minimum.
@@ -40,8 +40,7 @@ import java.util.*
/**
* Android < 8 VPN: always invisible because of VPN notification/icon
* Android 4.x other: always visible
* Android 5-7 other: only invisible in (possibly unsecure) lockscreen
* Android < 8 other: only invisible in (possibly unsecure) lockscreen
* Android 8+: always visible due to system limitations
* (user can choose to hide the notification in secure lockscreen or anywhere)
*/
@@ -74,8 +73,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName)
.setContentIntent(PendingIntent.getActivity(service, 0, Intent(service, MainActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
.setContentIntent(MainActivity.pendingIntent(service))
.setSmallIcon(R.drawable.ic_service_active)
private val style = NotificationCompat.BigTextStyle(builder)
private var isVisible = true
@@ -85,24 +83,22 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
val power = service.getSystemService(Context.POWER_SERVICE) as PowerManager
update(if (if (Build.VERSION.SDK_INT >= 20) power.isInteractive else @Suppress("DEPRECATION") power.isScreenOn)
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
update(if (power.isInteractive) Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
val screenFilter = IntentFilter()
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
if (visible && Build.VERSION.SDK_INT in 21 until 26) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
if (visible && Build.VERSION.SDK_INT < 26) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
service.registerReceiver(lockReceiver, screenFilter)
}
private fun update(action: String, forceShow: Boolean = false) {
if (forceShow || service.data.state == BaseService.CONNECTED) when (action) {
Intent.ACTION_SCREEN_OFF -> {
setVisible(visible && Build.VERSION.SDK_INT < 21, forceShow)
setVisible(false, forceShow)
unregisterCallback() // unregister callback to save battery
}
Intent.ACTION_SCREEN_ON -> {
setVisible(visible && (Build.VERSION.SDK_INT < 21 || !keyGuard.inKeyguardRestrictedInputMode()),
forceShow)
setVisible(visible && !keyGuard.inKeyguardRestrictedInputMode(), forceShow)
service.data.binder.registerCallback(callback)
service.data.binder.startListeningForBandwidth(callback)
callbackRegistered = true
@@ -30,11 +30,12 @@ import com.github.shadowsocks.R
import com.github.shadowsocks.ShadowsocksConnection
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.preference.DataStore
import android.service.quicksettings.TileService as BaseTileService
@RequiresApi(24)
class TileService : BaseTileService(), ShadowsocksConnection.Interface {
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle).setTint(0x79ffffff) }
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle) }
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_service_active) }
private val keyguard by lazy { getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager }
@@ -81,7 +82,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
}
override fun onClick() {
if (isLocked) unlockAndRun(this::toggle) else toggle()
if (isLocked && !DataStore.directBootAware) unlockAndRun(this::toggle) else toggle()
}
private fun toggle() {
@@ -44,7 +44,7 @@ object TrafficMonitor {
private val units = arrayOf("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
private val numberFormat = DecimalFormat("@@@")
fun formatTraffic(size: Long): String {
var n: Double = size.toDouble()
var n = size.toDouble()
var i = -1
while (n >= 999.5) {
n /= 1024
@@ -37,8 +37,7 @@ class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") {
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
TrafficMonitor.update(stat.getLong(0), stat.getLong(8))
socket.outputStream.write(0)
} catch (e: Exception) {
} catch (e: IOException) {
Log.e(tag, "Error when recv traffic stat", e)
app.track(e)
}
@@ -24,12 +24,12 @@ import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.net.LocalSocket
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.VpnRequestActivity
import com.github.shadowsocks.acl.Acl
@@ -38,6 +38,7 @@ import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress
import java.io.File
import java.io.FileDescriptor
import java.io.IOException
import java.lang.reflect.Method
import java.util.*
import android.net.VpnService as BaseVpnService
@@ -55,18 +56,24 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override val socketFile: File = File(app.deviceContext.filesDir, "protect_path")
override fun accept(socket: LocalSocket) {
var success = false
try {
socket.inputStream.read()
val fds = socket.ancillaryFileDescriptors
if (fds.isEmpty()) return
val fd = getInt.invoke(fds.first()) as Int
val ret = protect(fd)
success = protect(fd)
JniHelper.close(fd) // Trick to close file decriptor
socket.outputStream.write(if (ret) 0 else 1)
} catch (e: Exception) {
Log.e(tag, "Error when protect socket", e)
app.track(e)
}
try {
socket.outputStream.write(if (success) 0 else 1)
} catch (e: IOException) {
Log.e(tag, "Error when returning result in protect", e)
app.track(e)
}
}
}
class NullConnectionException : NullPointerException()
@@ -118,7 +125,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
super.startNativeProcesses()
val fd = startVpn()
if (!sendFd(fd)) throw Exception("sendFd failed")
if (!sendFd(fd)) throw IOException("sendFd failed")
}
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
@@ -129,6 +136,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private fun startVpn(): Int {
val profile = data.profile!!
val builder = Builder()
.setConfigureIntent(MainActivity.pendingIntent(this))
.setSession(profile.formattedName)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24)
@@ -140,7 +148,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
builder.addRoute("::", 0)
}
if (Build.VERSION.SDK_INT >= 21 && profile.proxyApps) {
if (profile.proxyApps) {
val me = packageName
profile.individual.split('\n')
.filter { it != me }
@@ -72,10 +72,10 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25)
profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
TableUtils.createTable(connectionSource, Profile::class.java)
profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, proxyApps, bypass," +
" udpdns, ipv6, individual) " +
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global, bypass, udpdns, ipv6," +
" individual FROM `tmp`;")
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route," +
" proxyApps, bypass, udpdns, ipv6, individual) " +
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global," +
" bypass, udpdns, ipv6, individual FROM `tmp`;")
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
} else if (oldVersion < 13) {
@@ -36,6 +36,7 @@ import java.util.*
class Profile : Serializable {
companion object {
private const val TAG = "ShadowParser"
private const val serialVersionUID = 0L
private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=~_|\[\]]""".toRegex()
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
@@ -25,79 +25,69 @@ import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import java.sql.SQLException
/**
* SQLExceptions are not caught (and therefore will cause crash) for insert/update transactions
* to ensure we are in a consistent state.
*/
object ProfileManager {
private const val TAG = "ProfileManager"
@Throws(SQLException::class)
fun createProfile(p: Profile? = null): Profile {
val profile = p ?: Profile()
profile.id = 0
try {
val oldProfile = app.currentProfile
if (oldProfile != null) {
// Copy Feature Settings from old profile
profile.route = oldProfile.route
profile.ipv6 = oldProfile.ipv6
profile.proxyApps = oldProfile.proxyApps
profile.bypass = oldProfile.bypass
profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns
}
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
PrivateDatabase.profileDao.createOrUpdate(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile)
} catch (ex: Exception) {
Log.e(TAG, "addProfile", ex)
app.track(ex)
val oldProfile = app.currentProfile
if (oldProfile != null) {
// Copy Feature Settings from old profile
profile.route = oldProfile.route
profile.ipv6 = oldProfile.ipv6
profile.proxyApps = oldProfile.proxyApps
profile.bypass = oldProfile.bypass
profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns
}
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
PrivateDatabase.profileDao.createOrUpdate(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile)
return profile
}
/**
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
*/
fun updateProfile(profile: Profile): Boolean = try {
PrivateDatabase.profileDao.update(profile)
true
} catch (ex: Exception) {
Log.e(TAG, "updateProfile", ex)
app.track(ex)
false
}
@Throws(SQLException::class)
fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.update(profile)
fun getProfile(id: Int): Profile? = try {
PrivateDatabase.profileDao.queryForId(id)
} catch (ex: Exception) {
} catch (ex: SQLException) {
Log.e(TAG, "getProfile", ex)
app.track(ex)
null
}
fun delProfile(id: Int): Boolean = try {
@Throws(SQLException::class)
fun delProfile(id: Int) {
PrivateDatabase.profileDao.deleteById(id)
ProfilesFragment.instance?.profilesAdapter?.removeId(id)
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
true
} catch (ex: Exception) {
Log.e(TAG, "delProfile", ex)
app.track(ex)
false
}
fun getFirstProfile(): Profile? = try {
val result = PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare())
if (result.size == 1) result[0] else null
} catch (ex: Exception) {
Log.e(TAG, "getAllProfiles", ex)
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull()
} catch (ex: SQLException) {
Log.e(TAG, "getFirstProfile", ex)
app.track(ex)
null
}
fun getAllProfiles(): List<Profile>? = try {
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
} catch (ex: Exception) {
} catch (ex: SQLException) {
Log.e(TAG, "getAllProfiles", ex)
app.track(ex)
null
@@ -21,6 +21,7 @@
package com.github.shadowsocks.plugin
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
@@ -84,11 +85,16 @@ object PluginManager {
""", Base64.DEFAULT))
}
private val receiver by lazy { app.listenForPackageChanges { synchronized(this) { cachedPlugins = null } } }
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> {
receiver
return synchronized(this) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
}
}
if (cachedPlugins == null) {
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
@@ -29,6 +29,7 @@ import android.os.Bundle
import android.provider.Settings
import android.support.design.widget.BottomSheetDialog
import android.support.v7.preference.PreferenceDialogFragmentCompat
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
@@ -107,10 +108,12 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
recycler.setPadding(0, padding, 0, padding)
recycler.setHasFixedSize(true)
recycler.layoutManager = LinearLayoutManager(activity)
recycler.itemAnimator = DefaultItemAnimator()
recycler.adapter = IconListAdapter(dialog)
recycler.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dialog.setContentView(recycler)
dialog.findViewById<View>(R.id.touch_outside)!!.isFocusable = false
return dialog
}
@@ -52,7 +52,7 @@ object DataStore {
/**
* Setter is defined in MainActivity.onPreferenceDataStoreChanged.
*/
val directBootAware: Boolean get() = BootReceiver.enabled && (publicStore.getBoolean(Key.directBootAware) ?: false)
val directBootAware: Boolean get() = BootReceiver.enabled && publicStore.getBoolean(Key.directBootAware) == true
var serviceMode: String
get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
set(value) = publicStore.putString(Key.serviceMode, value)
@@ -28,11 +28,7 @@ import android.util.AttributeSet
class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) {
var entryIcons: Array<Drawable?>? = null
val selectedEntry: Int get() = entryValues.indexOf(value)
val entryIcon: Drawable? get() = try {
entryIcons?.get(selectedEntry)
} catch (_: ArrayIndexOutOfBoundsException) {
null
}
val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry)
// fun setEntryIcons(@ArrayRes entryIconsResId: Int) {
// val array = getContext().getResources().obtainTypedArray(entryIconsResId)
// entryIcons = Array(array.length(), { i -> array.getDrawable(i) })
@@ -49,7 +45,7 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
}
init {
super.setOnPreferenceChangeListener({ preference, newValue ->
super.setOnPreferenceChangeListener { preference, newValue ->
val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString()
@@ -57,7 +53,7 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
if (entryIcons != null) icon = entryIcon
true
} else false
})
}
// val a = context.obtainStyledAttributes(attrs, R.styleable.IconListPreference)
// val entryIconsResId: Int = a.getResourceId(R.styleable.IconListPreference_entryIcons, -1)
// if (entryIconsResId != -1) entryIcons = entryIconsResId
@@ -22,7 +22,6 @@ package com.github.shadowsocks.tasker
import android.app.Activity
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DefaultItemAnimator
@@ -71,13 +70,12 @@ class ConfigActivity : AppCompatActivity() {
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
private val name = "select_dialog_singlechoice_" + (if (Build.VERSION.SDK_INT >= 21) "material" else "holo")
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
if (position == 0) holder.bindDefault() else holder.bind(profiles[position - 1])
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder = ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(Resources.getSystem()
.getIdentifier(name, "layout", "android"), parent, false))
.getIdentifier("select_dialog_singlechoice_material", "layout", "android"), parent, false))
override fun getItemCount(): Int = 1 + profiles.size
}
@@ -87,12 +85,12 @@ class ConfigActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
taskerOption = Settings.fromIntent(intent!!)
} catch (_: Exception) {
val intent = intent
if (intent == null) {
finish()
return
}
taskerOption = Settings.fromIntent(intent)
setContentView(R.layout.layout_tasker)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
@@ -44,8 +44,8 @@ class Settings(bundle: Bundle?) {
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
val profile = ProfileManager.getProfile(profileId)
return Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
if (profile != null)
context.getString(if (switchOn) R.string.start_service else R.string.stop_service, profile.formattedName)
if (profile != null) context.getString(if (switchOn) R.string.start_service else R.string.stop_service,
profile.formattedName)
else context.getString(if (switchOn) R.string.start_service_default else R.string.stop))
}
}
@@ -20,9 +20,7 @@
package com.github.shadowsocks.utils
import java.util.ArrayList
import java.util.Arrays
import java.util.StringTokenizer
import java.util.*
/**
* Commandline objects help handling command lines specifying processes to
@@ -86,9 +84,7 @@ object Commandline {
* @return empty string for null or no command, else every argument split
* by spaces and quoted by quoting rules.
*/
fun toString(args: Array<String>): String {
return toString(Arrays.asList(*args)) // thanks to Java, arrays aren't iterable
}
fun toString(args: Array<String>) = toString(args.asIterable()) // thanks to Java, arrays aren't iterable
/**
* Crack a command line.
@@ -7,7 +7,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
@@ -17,7 +17,7 @@ object DirectBoot {
fun getDeviceProfile(): Profile? = try {
ObjectInputStream(file.inputStream()).use { it.readObject() as Profile }
} catch (_: FileNotFoundException) { null }
} catch (_: IOException) { null }
fun clean() {
file.delete()
@@ -52,4 +52,5 @@ object TcpFastOpen {
"else",
" echo Failed.",
"fi"), null, true)?.joinToString("\n")
fun enabledAsync(value: Boolean) = thread { enabled(value) }.join(1000)
}
@@ -9,7 +9,6 @@ import android.support.annotation.AttrRes
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v7.util.SortedList
import android.view.accessibility.AccessibilityManager
import android.util.TypedValue
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper
@@ -22,11 +21,6 @@ private val fieldChildFragmentManager by lazy {
field
}
fun isAccessibilityEnabled(context: Context): Boolean {
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
return am.isEnabled()
}
fun String.isNumericAddress() = JniHelper.parseNumericAddress(this) != null
fun String.parseNumericAddress(): InetAddress? {
val addr = JniHelper.parseNumericAddress(this)
@@ -34,8 +28,8 @@ fun String.parseNumericAddress(): InetAddress? {
}
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
val x = str?.toIntOrNull() ?: default
return if (x < min || x > 65535) default else x
val value = str?.toIntOrNull() ?: default
return if (value < min || value > 65535) default else value
}
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
@@ -78,6 +72,6 @@ private class SortedListIterable<out T>(private val list: SortedList<T>) : Itera
private class SortedListIterator<out T>(private val list: SortedList<T>) : Iterator<T> {
private var count = 0
override fun hasNext() = count < list.size()
override fun next(): T = list[count++]
override fun next(): T = if (hasNext()) list[count++] else throw NoSuchElementException()
}
fun <T> SortedList<T>.asIterable(): Iterable<T> = SortedListIterable(this)
@@ -1,40 +0,0 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.support.design.widget.CoordinatorLayout
import android.util.AttributeSet
import android.view.View
import com.github.shadowsocks.R
class BottomMarginBehavior : CoordinatorLayout.Behavior<View> {
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean =
dependency.id == R.id.stat // sorry I'm too lazy to write an attribute
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
(child.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = (parent.height - dependency.y).toInt()
return true
}
}
@@ -29,7 +29,6 @@ import android.support.graphics.drawable.AnimatedVectorDrawableCompat
import android.support.v7.widget.TooltipCompat
import android.util.AttributeSet
import android.view.View
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.R
import com.github.shadowsocks.bg.BaseService
import java.util.*
@@ -84,9 +83,7 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
TooltipCompat.setTooltipText(this, context.getString(R.string.connect))
}
refreshDrawableState()
isEnabled = false
if (state == BaseService.CONNECTED || state == BaseService.STOPPED) app.handler.postDelayed(
{ isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED }, 1000)
isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED
}
private fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
@@ -20,34 +20,27 @@
package com.github.shadowsocks.widget
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.os.Build
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.SnackbarAnimation
import android.support.design.widget.Snackbar
import android.support.v4.view.ViewCompat
import android.support.design.widget.SnackbarAnimation
import android.util.AttributeSet
import android.view.View
import com.github.shadowsocks.utils.isAccessibilityEnabled
import android.view.accessibility.AccessibilityManager
/**
* Full credits go to: https://stackoverflow.com/a/35904421/2245107
*/
class MoveUpwardBehavior : CoordinatorLayout.Behavior<View> {
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val accessibility = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean =
dependency is Snackbar.SnackbarLayout
dependency is Snackbar.SnackbarLayout
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
val params = child.layoutParams
params.height = parent.height - dependency.height
child.layoutParams = params
child.layoutParams.height = dependency.y.toInt()
child.requestLayout()
return true
}
@@ -55,25 +48,18 @@ class MoveUpwardBehavior : CoordinatorLayout.Behavior<View> {
* Based on BaseTransientBottomBar.animateViewOut (support lib 27.0.2).
*/
override fun onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) {
if (!isAccessibilityEnabled(parent.getContext())) {
if (accessibility.isEnabled) child.layoutParams.height = parent.height else {
val animator = ValueAnimator()
val start = child.height
animator.setIntValues(start, parent.height)
animator.interpolator = SnackbarAnimation.FAST_OUT_SLOW_IN_INTERPOLATOR
animator.duration = SnackbarAnimation.ANIMATION_DURATION
animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
override fun onAnimationUpdate(animator: ValueAnimator) {
val currentValue = animator.animatedValue as Int
val params = child.layoutParams
params.height = currentValue
child.layoutParams = params
}
})
@Suppress("NAME_SHADOWING")
animator.addUpdateListener { animator ->
child.layoutParams.height = animator.animatedValue as Int
child.requestLayout()
}
animator.start()
} else {
val params = child.layoutParams
params.height = parent.height
child.layoutParams = params
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 B

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" />
</shape>
</item>
<item>
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/material_green_700" />
</shape>
</item>
</selector>
</item>
<!-- android:start not available until API 23 -->
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
</shape>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
<item>
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<color android:color="@color/material_primary_100"/>
</item>
</selector>
</item>
</ripple>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
<item>
<color android:color="@color/material_primary_300"/>
</item>
</ripple>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

@@ -15,18 +15,11 @@
</item>
</selector>
</item>
<item android:left="@dimen/profile_padding">
<!-- android:start not available until API 23 -->
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
</shape>
</item>
<item>
<selector>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#12000000" />
</shape>
</item>
</selector>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list>
@@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<color android:color="@color/material_primary_100"/>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<color android:color="@color/material_primary_100"/>
</item>
</selector>
</item>
</selector>
<item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list>
@@ -1,17 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" />
</shape>
</item>
<item>
<selector>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#12000000" />
</shape>
</item>
</selector>
<color android:color="@color/material_primary_300"/>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list>
@@ -2,7 +2,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#f5f5f5"
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0Z" />
<path
android:fillColor="#546e7a"
android:pathData="M24,21.8 C25.7673,21.8,27.2,23.2327,27.2,25 C27.2,26.7673,25.7673,28.2,24,28.2
C22.2327,28.2,20.8,26.7673,20.8,25 C20.8,23.2327,22.2327,21.8,24,21.8 Z" />
<path
android:fillColor="#546e7a"
android:pathData="M21,15 L19.17,17 L16,17 A2,2,0,0,0,14,19 L14,31 A2,2,0,0,0,16,33 L32,33
A2,2,0,0,0,34,31 L34,19 A2,2,0,0,0,32,17 L28.83,17 L27,15 Z M24,30
A5,5,0,1,1,29,25 A5,5,0,0,1,24,30 Z" />
</vector>
@@ -2,9 +2,7 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
android:drawable="@drawable/ic_service_busy"
tools:ignore="NewApi">
android:drawable="@drawable/ic_service_busy">
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
@@ -2,9 +2,7 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
android:drawable="@drawable/ic_service_idle"
tools:ignore="NewApi">
android:drawable="@drawable/ic_service_idle">
<target android:name="strike_thru_path">
<aapt:attr name="android:animation">
<objectAnimator
@@ -2,9 +2,7 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
android:drawable="@drawable/ic_service_idle"
tools:ignore="NewApi">
android:drawable="@drawable/ic_service_idle">
<target android:name="strike_thru_path">
<aapt:attr name="android:animation">
<objectAnimator
@@ -2,9 +2,7 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
android:drawable="@drawable/ic_service_busy"
tools:ignore="NewApi">
android:drawable="@drawable/ic_service_busy">
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
+12 -4
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -8,8 +9,15 @@
android:layout_height="wrap_content"
android:labelFor="@+id/content"
android:entries="@array/acl_rule_templates"/>
<EditText android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions|textMultiLine"/>
<android.support.design.widget.TextInputLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions|textMultiLine"/>
</android.support.design.widget.TextInputLayout>
</LinearLayout>
+34 -26
View File
@@ -3,8 +3,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:duplicateParentState="false">
<include layout="@layout/toolbar_light_dark"/>
@@ -41,28 +41,36 @@
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"/>
</LinearLayout>
<FrameLayout android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ProgressBar android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<android.support.v7.widget.RecyclerView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/list"
tools:listitem="@layout/layout_apps_item"/>
<com.futuremind.recyclerviewfastscroll.FastScroller
android:id="@+id/fastscroller"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:fastscroll__handleColor="@color/material_primary_500"
app:fastscroll__bubbleColor="@color/material_primary_500"
app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/>
</FrameLayout>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/snackbar"
android:background="@color/material_primary_700"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground"
app:layout_behavior="com.github.shadowsocks.widget.ShrinkUpwardBehavior">
<ProgressBar android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<android.support.v7.widget.RecyclerView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/list"
tools:listitem="@layout/layout_apps_item"/>
<com.futuremind.recyclerviewfastscroll.FastScroller
android:id="@+id/fastscroller"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:fastscroll__handleColor="@color/material_primary_500"
app:fastscroll__bubbleColor="@color/material_primary_500"
app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/>
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
+2 -2
View File
@@ -12,7 +12,7 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.github.shadowsocks.widget.MoveUpwardBehavior">
app:layout_behavior="com.github.shadowsocks.widget.ShrinkUpwardBehavior">
<FrameLayout
android:id="@+id/fragment_holder"
@@ -148,7 +148,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginTop="@dimen/fab_margin_top"
android:layout_marginTop="-28dp"
android:elevation="6dp"
app:layout_anchor="@id/stat"
app:layout_anchorGravity="top|end">
@@ -1,12 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" />
<me.dm7.barcodescanner.zxing.ZXingScannerView android:id="@+id/scanner"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<fragment
android:id="@+id/barcode"
android:name="com.google.android.gms.samples.vision.barcodereader.BarcodeCapture"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:gvb_auto_focus="true"
app:gvb_code_format="qr_code"/>
</LinearLayout>
@@ -1,6 +1,6 @@
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_apply_all"
android:title="@string/action_apply_all"
android:alphabeticShortcut="a"
@@ -1,41 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/selection"
android:alphabeticShortcut="s"
android:icon="?attr/actionModeSelectAllDrawable"
android:numericShortcut="0"
android:title="@string/action_selection"
android:visible="false"
app:showAsAction="always">
<menu>
<item
android:id="@+id/action_select_all"
android:alphabeticShortcut="a"
android:numericShortcut="1"
android:title="@android:string/selectAll"
app:showAsAction="always"/>
<item
android:id="@+id/action_cut"
android:alphabeticShortcut="t"
android:numericShortcut="2"
android:title="@android:string/cut"
app:showAsAction="always"/>
<item
android:id="@+id/action_copy"
android:alphabeticShortcut="c"
android:numericShortcut="3"
android:title="@android:string/copy"
app:showAsAction="always"/>
<item
android:id="@+id/action_delete"
android:alphabeticShortcut="d"
android:numericShortcut="4"
android:title="@string/delete"
app:showAsAction="always"/>
</menu>
</item>
<item android:title="@string/action_add_rule"
android:icon="@drawable/ic_av_playlist_add"
android:alphabeticShortcut="a"
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource">
<item
android:id="@+id/action_select_all"
android:alphabeticShortcut="a"
android:icon="?attr/actionModeSelectAllDrawable"
android:numericShortcut="1"
android:title="@android:string/selectAll"
android:showAsAction="always"/>
<item
android:id="@+id/action_cut"
android:alphabeticShortcut="t"
android:icon="?attr/actionModeCutDrawable"
android:numericShortcut="2"
android:title="@android:string/cut"
android:showAsAction="always"/>
<item
android:id="@+id/action_copy"
android:alphabeticShortcut="c"
android:icon="?attr/actionModeCopyDrawable"
android:numericShortcut="3"
android:title="@android:string/copy"
android:showAsAction="always"/>
<item
android:id="@+id/action_delete"
android:alphabeticShortcut="d"
android:icon="@drawable/ic_action_delete"
android:numericShortcut="4"
android:title="@string/delete"
android:showAsAction="always"/>
</menu>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_import"
android:title="@string/action_import_file"
android:icon="@drawable/ic_image_photo"
android:alphabeticShortcut="p"
android:numericShortcut="1"
app:showAsAction="always"/>
</menu>
+3 -5
View File
@@ -4,7 +4,7 @@
<!-- misc -->
<string name="profile">"پروفایل"</string>
<string name="connection_test_testing">"در حال آزمایش..."</string>
<string name="connection_test_testing">"در حال آزمایش…"</string>
<!-- proxy category -->
<string name="profile_name">"نام پروفایل"</string>
@@ -20,7 +20,7 @@
<string name="invalid_server">"آدرس سرور نامعتبر است"</string>
<string name="service_failed">"اتصال به سرور ناموفق بود!"</string>
<string name="stop">"غیرفعال‌کردن"</string>
<string name="stopping">"در حال قطع‌کردن اتصال..."</string>
<string name="stopping">"در حال قطع‌کردن اتصال…"</string>
<string name="profile_invalid_input">"هیچ پروفایل معتبری یافت نشد!"</string>
<!-- alert category -->
@@ -33,7 +33,6 @@
<string name="profiles">"پروفایل‌ها"</string>
<string name="settings">"تنظیمات"</string>
<string name="faq">"سوالات مداول"</string>
<string name="recovery">"ریست"</string>
<string name="about">"درباره ما"</string>
<string name="edit">"ویرایش"</string>
<string name="share">"به اشتراک گذاری"</string>
@@ -52,9 +51,8 @@
<!-- status -->
<string name="sent">"فرستاده‌شده:"</string>
<string name="received">"دریافت‌شده:"</string>
<string name="connecting">"در حال اتصال..."</string>
<string name="connecting">"در حال اتصال…"</string>
<string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string>
<string name="nat_connected">"اتصال برقرار است."</string>
<string name="not_connected">"اتصال برقرار نیست!"</string>
<string name="route_entry_all">"همه"</string>
</resources>
+32
View File
@@ -1,3 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- misc -->
<string name="profile">"Profil"</string>
<string name="connection_test_error">"Impossible de détecter la connexion Internet :%s"</string>
<string name="connection_test_fail">"Internet Indisponible"</string>
<string name="connection_test_error_status_code">"Code de l'Erreur : #%d"</string>
<string name="proxy">"Serveur"</string>
<string name="sitekey">"Mot de passe"</string>
<string name="service_failed">"Échec de la connexion au serveur distant"</string>
<!-- alert category -->
<string name="close">"Fermer"</string>
<string name="profile_empty">"Veuillez sélectionner un profil"</string>
<!-- menu category -->
<string name="profiles">"Profils"</string>
<string name="settings">"Paramètres"</string>
<string name="about">"À propos"</string>
<string name="share">"Partager"</string>
<string name="add_profile">"Ajouter un Profil"</string>
<string name="action_apply_all">"Appliquer les Paramètres à tous les Profils"</string>
<string name="action_export">"Exporter vers le presse-papiers"</string>
<string name="action_import">"Importer depuis le presse-papiers"</string>
<string name="delete">"Supprimer"</string>
<string name="delete_confirm_prompt">"Êtes-vous sûr de vouloir supprimer ce profil?"</string>
<string name="share_qr_nfc">"QR Code/NFC"</string>
<string name="add_profile_methods_scan_qr_code">"Scanner le QR Code"</string>
<plurals name="removed">
<item quantity="one">"Supprimé"</item>
<item quantity="other">"%d éléments supprimés"</item>
</plurals>
<string name="undo">"Annuler"</string>
</resources>
+7 -14
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"Shadowsocks"</string>
<string name="quick_toggle">"ON/OFF"</string>
<!-- misc -->
@@ -9,7 +8,6 @@
<string name="remote_dns">"リモートDNS"</string>
<string name="stat_summary">"送信済み: \t%3$s\t↑\t%1$s/s
受信済み: \t%4$s\t↓\t%2$s/s"</string>
<string name="connection_test_pending">"接続状況確認"</string>
<string name="connection_test_testing">"テスト中…"</string>
<string name="connection_test_available">"成功: %dmsの遅延"</string>
@@ -35,10 +33,8 @@
<string name="ipv6_summary">"リモートサーバーに IPv6 パケットを転送"</string>
<string name="route_list">"プロキシ方式"</string>
<string name="route_entry_gfwlist">"中国本土からアクセス不可なアドレス以外を迂回する"</string>
<string name="proxied_apps">"アプリ別のプロキシを使用"</string>
<string name="proxied_apps_summary">"アプリ別でプロキシを指定、Android 4.x では必ずNATモードを使用して下さい"</string>
<string name="proxied_apps">"アプリ別のVPNモードを使用"</string>
<string name="proxied_apps_summary">"アプリ別のVPNモードを設定"</string>
<string name="on">"ON"</string>
<string name="bypass_apps">"バイパスモード(迂回モード)"</string>
<string name="bypass_apps_summary">"このモードを選択した場合、指定したアプリのパケットはプロキシを経由しない"</string>
@@ -74,9 +70,7 @@
<string name="profiles">"プロファイル"</string>
<string name="settings">"オプション設定"</string>
<string name="faq">"よくある質問"</string>
<string name="faq_url">"https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md"</string>
<string name="about">"本アプリについて"</string>
<string name="about_title">"Shadowsocks %s"</string>
<string name="edit">"編集"</string>
<string name="share">"共有"</string>
<string name="add_profile">"プロファイルを追加"</string>
@@ -127,10 +121,7 @@
<string name="route_entry_bypass_chn">"中国本土のアドレスを迂回する"</string>
<string name="route_entry_bypass_lan_chn">"LAN 及び中国本土のアドレスを迂回する"</string>
<string name="route_entry_chinalist">"中国本土のアドレス以外を迂回する"</string>
<string name="proxied_apps_summary_v21">"選択したアプリにプロキシを設定する"</string>
<string name="acl_rule_templates_generic">"URL/サブネット/ホスト名 PCRE パターン"</string>
<string name="acl_rule_templates_generic">"サブネットまたはホスト名のPCREパターン"</string>
<string name="acl_rule_templates_domain">"ドメイン及び全てのサブドメイン"</string>
<!-- plugin -->
@@ -148,7 +139,6 @@
<string name="advanced">"アドバンス"</string>
<string name="service_mode">"サービスモード"</string>
<string name="service_mode_proxy">"プロキシのみ"</string>
<string name="service_mode_vpn">"VPN"</string>
<string name="service_mode_transproxy">"トランスプロキシ"</string>
<string name="port_proxy">"SOCKS5プロキシポート"</string>
<string name="port_local_dns">"ローカルDNSポート"</string>
@@ -157,4 +147,7 @@
<string name="service_transproxy">"トランスプロキシサービス"</string>
<string name="vpn_permission_denied">"VPNサービス作成のアクセス許可が拒否されました"</string>
<string name="auto_connect_summary_v24">"起動時にShadowsockを有効。 VPN常時接続の使用をお勧めします"</string>
</resources>
<string name="direct_boot_aware">"ダイレクトブート"</string>
<string name="direct_boot_aware_summary">"デバイスがロック解除される前にShadowsocksの自動起動を許可(選択されたプロファイル情報はより少ない保護を受けることになります)"</string>
<string name="acl_rule_online_config">"オンライン設定のURL"</string>
</resources>
+7 -47
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"Shadowsocks"</string>
<string name="quick_toggle">"켜기/끄기"</string>
<!-- misc -->
@@ -9,7 +8,9 @@
<string name="remote_dns">"원격 DNS"</string>
<string name="stat_summary">"송신: \t%3$s\t↑\t%1$s/s
수신: \t%4$s\t↓\t%2$s/s"</string>
<plurals name="bytes">
<item quantity="other">"바이트"</item>
</plurals>
<string name="connection_test_pending">"인터넷 연결 상태 검사하기"</string>
<string name="connection_test_testing">"검사 중…"</string>
<string name="connection_test_available">"성공: 지연시간 %dms"</string>
@@ -34,11 +35,10 @@
<string name="ipv6">"IPv6 라우팅"</string>
<string name="ipv6_summary">"IPv6 트래픽도 원격으로 리다이렉트 합니다"</string>
<string name="route_list">"라우팅 대상"</string>
<string name="route_entry_gfwlist">"GFW List"</string>
<!-- Fuzzy -->
<string name="proxied_apps">"원하는 앱만 프락시 적용하기"</string>
<string name="proxied_apps_summary">"선택한 앱에만 프락시를 적용합니다. Android 4.x 이하에서는 NAT 모드를 써야 합니다."</string>
<string name="proxied_apps_summary">"선택한 앱에만 프락시를 적용합니다"</string>
<string name="on">"활성화"</string>
<string name="bypass_apps">"선택된 앱들만 프록시 적용 제외하기"</string>
<string name="bypass_apps_summary">"이 옵션을 활성화하면 선택된 앱들은 프락시를 사용하지 않습니다"</string>
@@ -55,7 +55,6 @@
<string name="service_failed">"원격 서버에 접속하는 데 실패했습니다"</string>
<string name="stop">"중지"</string>
<string name="stopping">"종료 중…"</string>
<string name="vpn_error">"%s"</string>
<string name="reboot_required">"VPN 서비스를 시작하는 데 실패했습니다. 장치를 재시작해 보세요."</string>
<string name="profile_invalid_input">"올바른 프로필 데이터를 찾을 수 없습니다"</string>
@@ -74,13 +73,10 @@
<string name="profiles">"프로필"</string>
<string name="settings">"설정"</string>
<string name="faq">"자주 묻는 질문"</string>
<string name="faq_url">"https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md"</string>
<string name="about">"이 앱에 대하여"</string>
<string name="about_title">"Shadowsocks %s"</string>
<string name="edit">"수정"</string>
<string name="share">"공유"</string>
<string name="add_profile">"프로필 추가"</string>
<string name="action_apply_all">"Apply Settings to All Profiles"</string>
<string name="action_export">"클립보드로 내보내기"</string>
<string name="action_import">"클립보드에서 불러오기"</string>
<string name="action_export_msg">"성공적으로 내보냈습니다"</string>
@@ -102,34 +98,15 @@
</plurals>
<string name="undo">"실행 취소"</string>
<!-- tasker -->
<string name="toggle_service_state">"Start the service"</string>
<string name="start_service_default">"Connect to the current server"</string>
<string name="start_service">"Connect to %s"</string>
<string name="stop_service">"Switch to %s"</string>
<string name="profile_default">"Use the current profile"</string>
<!-- status -->
<string name="sent">"송신:"</string>
<string name="received">"수신:"</string>
<string name="connecting">"연결 중…"</string>
<string name="vpn_connected">"연결 완료. 탭 하면 연결 상태를 검사합니다."</string>
<string name="not_connected">"Not connected"</string>
<!-- acl -->
<string name="custom_rules">"사용자 정의 규칙"</string>
<string name="action_selection">"Selection…"</string>
<string name="action_add_rule">"Add rule(s)…"</string>
<string name="edit_rule">"Edit rule"</string>
<string name="route_entry_all">"All"</string>
<string name="route_entry_bypass_lan">"Bypass LAN"</string>
<string name="route_entry_bypass_chn">"Bypass mainland China"</string>
<string name="route_entry_bypass_lan_chn">"Bypass LAN &amp; mainland China"</string>
<string name="route_entry_chinalist">"China List"</string>
<string name="proxied_apps_summary_v21">"Configure VPN mode for selected apps"</string>
<string name="acl_rule_templates_generic">"URL, 서브넷 혹은 호스트 이름 PCRE 패턴"</string>
<string name="acl_rule_templates_domain">"Domain name and all its subdomain names"</string>
<string name="acl_rule_templates_generic">"서브넷 혹은 호스트 이름 PCRE 패턴"</string>
<!-- plugin -->
<string name="plugin">"플러그인"</string>
@@ -139,21 +116,4 @@
<string name="plugin_untrusted">"경고: 이 플러그인은 신뢰할 수 있는 출처에서 온 것이 아닌 것 같습니다"</string>
<string name="profile_plugin">"플러그인: %s"</string>
<string name="add_profile_scanner_permission_required">"QR 코드를 읽어 들이려면 카메라 권한이 필요합니다"</string>
<!-- notification category -->
<string name="service_vpn">"VPN Service"</string>
<string name="add_profile_methods_manual_settings">"Manual Settings"</string>
<string name="advanced">"Advanced"</string>
<string name="service_mode">"Service mode"</string>
<string name="service_mode_proxy">"Proxy only"</string>
<string name="service_mode_vpn">"VPN"</string>
<string name="service_mode_transproxy">"Transproxy"</string>
<string name="port_proxy">"SOCKS5 proxy port"</string>
<string name="port_local_dns">"Local DNS port"</string>
<string name="port_transproxy">"Transproxy port"</string>
<string name="service_proxy">"Proxy Service"</string>
<string name="service_transproxy">"Transproxy Service"</string>
<string name="vpn_permission_denied">"Permission denied to create a VPN service"</string>
<string name="auto_connect_summary_v24">"Enable Shadowsocks on startup. Recommended to use always-on VPN
instead"</string>
</resources>
</resources>
+31 -37
View File
@@ -1,15 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"Shadowsocks"</string>
<string name="quick_toggle">"Подключение"</string>
<!-- misc -->
<string name="profile">"Профиль"</string>
<string name="profile_summary">"Переключить на другой профиль или добавить новые"</string>
<string name="remote_dns">"Удалённый DNS"</string>
<string name="stat_summary">"Отправлено: \t%3$s\t↑\t%1$s/s
Получено: \t%4$s\t↓\t%2$s/s"</string>
<string name="stat_summary">"Отправлено: \t%3$s\t↑\t%1$s
Получено: \t\t\t%4$s\t↓\t%2$s"</string>
<plurals name="bytes">
<item quantity="one">"байт"</item>
<item quantity="few">"байта"</item>
@@ -27,11 +25,11 @@
<string name="proxy_cat">"Настройки Сервера"</string>
<!-- proxy category -->
<string name="profile_name">"Имя Профиля"</string>
<string name="profile_name">"Имя профиля"</string>
<string name="proxy">"Сервер"</string>
<string name="remote_port">"Удалённый порт"</string>
<string name="sitekey">"Пароль"</string>
<string name="enc_method">"Метод Шифрования"</string>
<string name="enc_method">"Метод шифрования"</string>
<!-- feature category -->
<string name="feature_cat">"Дополнительные Настройки"</string>
@@ -41,14 +39,12 @@
<string name="ipv6_summary">"Перенаправлять трафик IPv6 на удалённый сервер"</string>
<string name="route_list">"Маршрут"</string>
<string name="route_entry_gfwlist">"Список GFW"</string>
<string name="proxied_apps">"Прокси для выбранных приложений"</string>
<string name="proxied_apps_summary">"Установить прокси для выбранных приложений, требует режим NAT под Android 4.x"</string>
<string name="proxied_apps">"Режим VPN для выбранных приложений"</string>
<string name="proxied_apps_summary">"Выбрать приложения, для которых нужно использовать режим VPN"</string>
<string name="on">"Вкл"</string>
<string name="bypass_apps">"В обход прокси"</string>
<string name="bypass_apps_summary">"Включите эту опцию для работы выбранных приложений в обход прокси"</string>
<string name="auto_connect">"Авто Подключение"</string>
<string name="auto_connect">"Авто-подключение"</string>
<string name="auto_connect_summary">"Запускать Shadowsocks при включении"</string>
<string name="tcp_fastopen_summary">"Переключение требует наличия ROOT прав"</string>
<string name="tcp_fastopen_summary_unsupported">"Неподдерживаемая версия ядра: %s &lt; 3.7.1"</string>
@@ -61,9 +57,8 @@
<string name="service_failed">"Ошибка при подключении к удалённому серверу"</string>
<string name="stop">"Остановить"</string>
<string name="stopping">"Останавливается…"</string>
<string name="vpn_error">"%s"</string>
<string name="reboot_required">"Не удалось запустить службу VPN. Возможно, требуется перезагрузить ваше устройство."</string>
<string name="profile_invalid_input">"No valid profile data found."</string>
<string name="profile_invalid_input">"Не найдено действительных данных профиля."</string>
<!-- alert category -->
<string name="yes">"Да"</string>
@@ -79,13 +74,13 @@
<!-- menu category -->
<string name="profiles">"Профили"</string>
<string name="settings">"Настройки"</string>
<string name="faq">"FAQ"</string>
<string name="faq_url">"https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md"</string>
<string name="faq">"ЧаВо"</string>
<string name="faq_url">"https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.ru.md"</string>
<string name="about">"О приложении"</string>
<string name="about_title">"Shadowsocks %s"</string>
<string name="edit">"Изменить"</string>
<string name="share">"Поделиться"</string>
<string name="add_profile">"Добавить Профиль"</string>
<string name="add_profile">"Добавить профиль"</string>
<string name="action_apply_all">"Применить настройки для всех профилей"</string>
<string name="action_export">"Экспортировать в буфер обмена"</string>
<string name="action_import">"Импортировать из буфера обмена"</string>
@@ -101,7 +96,7 @@
<string name="delete">"Удалить"</string>
<string name="delete_confirm_prompt">"Вы уверены, что хотите удалить этот профиль?"</string>
<string name="share_qr_nfc">"QR-код/NFC"</string>
<string name="add_profile_dialog">"Добавить этот Профиль Shadowsocks?"</string>
<string name="add_profile_dialog">"Добавить этот профиль Shadowsocks?"</string>
<string name="add_profile_methods_scan_qr_code">"Сканировать QR-код"</string>
<plurals name="removed">
<item quantity="one">"Удалено %d элементов"</item>
@@ -135,10 +130,7 @@
<string name="route_entry_bypass_chn">"Все, кроме Китая"</string>
<string name="route_entry_bypass_lan_chn">"Все, кроме LAN и Китая"</string>
<string name="route_entry_chinalist">"Список Китай"</string>
<string name="proxied_apps_summary_v21">"Установить прокси для выбранных приложений"</string>
<string name="acl_rule_templates_generic">"URL/Подсеть/Регулярное выражение (PCRE) имени хоста"</string>
<string name="acl_rule_templates_generic">"Подсеть или имя хоста, записанное регулярным выражением (PCRE)"</string>
<string name="acl_rule_templates_domain">"Доменное имя и все его поддомены"</string>
<!-- plugin -->
@@ -151,19 +143,21 @@
<string name="add_profile_scanner_permission_required">"Разрешение камеры требуется для сканирования QR код."</string>
<!-- notification category -->
<string name="service_vpn">"VPN Service"</string>
<string name="add_profile_methods_manual_settings">"Manual Settings"</string>
<string name="advanced">"Advanced"</string>
<string name="service_mode">"Service mode"</string>
<string name="service_mode_proxy">"Proxy only"</string>
<string name="service_mode_vpn">"VPN"</string>
<string name="service_mode_transproxy">"Transproxy"</string>
<string name="port_proxy">"SOCKS5 proxy port"</string>
<string name="port_local_dns">"Local DNS port"</string>
<string name="port_transproxy">"Transproxy port"</string>
<string name="service_proxy">"Proxy Service"</string>
<string name="service_transproxy">"Transproxy Service"</string>
<string name="vpn_permission_denied">"Permission denied to create a VPN service"</string>
<string name="auto_connect_summary_v24">"Enable Shadowsocks on startup. Recommended to use always-on VPN
instead"</string>
</resources>
<string name="service_vpn">"Служба VPN"</string>
<string name="add_profile_methods_manual_settings">"Ручные настройки"</string>
<string name="advanced">"Дополнительно"</string>
<string name="service_mode">"Режим"</string>
<string name="service_mode_proxy">"Только прокси"</string>
<string name="service_mode_transproxy">"Прозрачный прокси"</string>
<string name="port_proxy">"Порт SOCKS5 прокси"</string>
<string name="port_local_dns">"Локальный порт DNS"</string>
<string name="port_transproxy">"Порт прозрачного прокси"</string>
<string name="service_proxy">"Служба прокси"</string>
<string name="service_transproxy">"Служба прозрачный прокси"</string>
<string name="vpn_permission_denied">"Нет разрешения на создание VPN соединения"</string>
<string name="auto_connect_summary_v24">"Включить Shadowsocks во время запуска. Лучше использовать режим постоянной VPN."</string>
<string name="speed">"%s/с"</string>
<string name="direct_boot_aware">"Непосредственная загрузка"</string>
<string name="direct_boot_aware_summary">"Позволить Shadowsocks запускаться до того, как устройство будет разблокировано (данные выбранного профиля будут хуже защищены)"</string>
<string name="acl_rule_online_config">"URL конфигурации"</string>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin_top">-28dp</dimen>
</resources>
+7 -20
View File
@@ -1,27 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"影梭"</string>
<string name="quick_toggle">"开关"</string>
<!-- misc -->
<string name="profile">"配置文件"</string>
<string name="profile_summary">"新建或切换到其他配置文件"</string>
<string name="remote_dns">"远程 DNS"</string>
<string name="stat_summary">"上传: \t%3$s\t↑\t%1$s/s
下载: \t%4$s\t↓\t%2$s/s"</string>
<string name="stat_summary">"上传: \t%3$s\t↑\t%1$s
下载: \t%4$s\t↓\t%2$s"</string>
<string name="connection_test_pending">"检查网络连接"</string>
<string name="connection_test_testing">"测试中…"</string>
<string name="connection_test_available">"连接成功:延时 %d 毫秒"</string>
<string name="connection_test_error">"失败:%s"</string>
<string name="connection_test_fail">"无互联网连接"</string>
<string name="connection_test_error_status_code">"状态码无效(#%d)"</string>
<!-- proxy category -->
@@ -29,7 +21,6 @@
<!-- proxy category -->
<string name="profile_name">"配置名称"</string>
<string name="proxy">"服务器"</string>
<string name="remote_port">"远程端口"</string>
<string name="sitekey">"密码"</string>
@@ -44,7 +35,6 @@
<string name="route_list">"路由"</string>
<string name="route_entry_gfwlist">"GFW 列表"</string>
<string name="proxied_apps">"分应用 VPN"</string>
<string name="proxied_apps_summary">"允许部分应用绕过 VPN"</string>
<string name="on">"启用"</string>
<string name="bypass_apps">"绕行模式"</string>
@@ -81,7 +71,6 @@
<string name="profiles">"配置文件"</string>
<string name="settings">"设置选项"</string>
<string name="faq">"常见问题"</string>
<string name="faq_url">"https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md"</string>
<string name="about">"关于"</string>
<string name="about_title">"影梭 (Shadowsocks) %s"</string>
<string name="edit">"编辑"</string>
@@ -133,12 +122,8 @@
<string name="route_entry_bypass_chn">"绕过中国大陆地址"</string>
<string name="route_entry_bypass_lan_chn">"绕过局域网及中国大陆地址"</string>
<string name="route_entry_chinalist">"仅代理中国大陆地址"</string>
<string name="proxied_apps_summary_v21">"为应用程序分别设置代理"</string>
<string name="acl_rule_templates_generic">"URL/子网/域名 PCRE 正则表达式"</string>
<string name="acl_rule_templates_generic">"子网/域名 PCRE 正则表达式"</string>
<string name="acl_rule_templates_domain">"域名及其子域名"</string>
<string name="acl_rule_online_config">配置文件地址</string>
<!-- plugin -->
<string name="plugin">"插件"</string>
@@ -155,7 +140,6 @@
<string name="advanced">"高级选项"</string>
<string name="service_mode">"服务模式"</string>
<string name="service_mode_proxy">"仅代理"</string>
<string name="service_mode_vpn">"VPN"</string>
<string name="service_mode_transproxy">"透明代理"</string>
<string name="port_proxy">"SOCKS5 代理端口"</string>
<string name="port_local_dns">"本地 DNS 端口"</string>
@@ -164,4 +148,7 @@
<string name="service_transproxy">"透明代理模式"</string>
<string name="vpn_permission_denied">"创建 VPN 服务权限不足"</string>
<string name="auto_connect_summary_v24">"允许 Shadowsocks 随系统启动,建议使用始终开启的 VPN"</string>
</resources>
<string name="direct_boot_aware">"直接启动"</string>
<string name="direct_boot_aware_summary">"允许在设备解锁前启动服务(选中的配置信息会不那么安全)"</string>
<string name="acl_rule_online_config">"在线规则文件 URL"</string>
</resources>
+4 -13
View File
@@ -37,10 +37,8 @@
<string name="ipv6_summary">"向遠端重新導向 IPv6 流量"</string>
<string name="route_list">"路由"</string>
<string name="route_entry_gfwlist">"GFW List"</string>
<string name="proxied_apps">"個別應用程式的 Proxy"</string>
<string name="proxied_apps_summary">"為已選擇的應用程式設定 Proxy,在 Android 4.X 以下需要開啟 NAT 模式"</string>
<string name="proxied_apps">"個別應用程式的 VPN"</string>
<string name="proxied_apps_summary">"為已選擇的應用程式設定 VPN"</string>
<string name="on">"開"</string>
<string name="bypass_apps">"略過模式"</string>
<string name="bypass_apps_summary">"啟用此選項,會略過已選擇的應用程式"</string>
@@ -57,7 +55,6 @@
<string name="service_failed">"連線至遠端伺服器失敗"</string>
<string name="stop">"停止"</string>
<string name="stopping">"關閉中…"</string>
<string name="vpn_error">"%s"</string>
<string name="reboot_required">"VPN 服務啟動失敗。您或許需要重新啟動您的裝置。"</string>
<string name="profile_invalid_input">"未找到有效的設定檔資料。"</string>
@@ -76,7 +73,6 @@
<string name="profiles">"設定檔"</string>
<string name="settings">"設定"</string>
<string name="faq">"常見問題"</string>
<string name="faq_url">"https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md"</string>
<string name="about">"關於"</string>
<string name="about_title">"Shadowsocks %s"</string>
<string name="edit">"編輯"</string>
@@ -128,12 +124,8 @@
<string name="route_entry_bypass_chn">"略過中國大陸"</string>
<string name="route_entry_bypass_lan_chn">"略過區域網路及中國大陸"</string>
<string name="route_entry_chinalist">"China List"</string>
<string name="proxied_apps_summary_v21">"為已選擇的應用程式設定 Proxy"</string>
<string name="acl_rule_templates_generic">"URL/子網路/主機名稱 PCRE 模式"</string>
<string name="acl_rule_templates_generic">"子網路/主機名稱 PCRE 模式"</string>
<string name="acl_rule_templates_domain">"網域及其所有子網域"</string>
<string name="acl_rule_online_config">配置文件地址</string>
<!-- plugin -->
<string name="plugin">"外掛程式"</string>
@@ -150,7 +142,6 @@
<string name="advanced">"高級"</string>
<string name="service_mode">"服務模式"</string>
<string name="service_mode_proxy">"仅代理"</string>
<string name="service_mode_vpn">"VPN"</string>
<string name="service_mode_transproxy">"透明代理"</string>
<string name="port_proxy">"SOCKS5 代理連接埠"</string>
<string name="port_local_dns">"本地 DNS 連接埠"</string>
@@ -159,4 +150,4 @@
<string name="service_transproxy">"透明代理服務"</string>
<string name="vpn_permission_denied">"没有權限創建 VPN 服務"</string>
<string name="auto_connect_summary_v24">"允許 Shadowsocks 隨系統啟動,建議使用始終開啟的 VPN"</string>
</resources>
</resources>
-2
View File
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="profile_item_max_width">480dp</dimen>
<dimen name="qr_code_size">250dp</dimen>
<dimen name="profile_padding">8dp</dimen>
<dimen name="bottom_sheet_padding">8dp</dimen>
<dimen name="fab_margin_top">-44dp</dimen>
</resources>
+1
View File
@@ -99,6 +99,7 @@
<string name="action_apply_all">Apply Settings to All Profiles</string>
<string name="action_export">Export to Clipboard</string>
<string name="action_import">Import from Clipboard</string>
<string name="action_import_file">Import from file…</string>
<string name="action_export_msg">Successfully export!</string>
<string name="action_export_err">Failed to export.</string>
<string name="action_import_msg">Successfully import!</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>
+2
View File
@@ -13,6 +13,7 @@
<AutoSummaryEditTextPreference
android:key="proxy"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/proxy"/>
<AutoSummaryEditTextPreference
android:key="remotePortNum"
@@ -25,6 +26,7 @@
app:pref_summaryPasswordSubstituteLength="0"
android:key="sitekey"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/sitekey"/>
<SimpleMenuPreference
android:key="encMethod"

Some files were not shown because too many files have changed in this diff Show More