Compare commits

..
54 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
69 changed files with 927 additions and 482 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)
+1
View File
@@ -3,6 +3,7 @@
[![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.
+7 -4
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.20'
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
View File
@@ -1,8 +1,6 @@
#!/usr/bin/python
# -*- encoding: utf8 -*-
import itertools
import math
import sys
import IPy
+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
+8 -7
View File
@@ -8,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"
}
@@ -21,14 +21,15 @@ android {
applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion
versionCode 211
versionName "4.4.6"
versionCode 4050100
versionName "4.5.1"
testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
}
buildTypes {
debug {
multiDexEnabled true
pseudoLocalesEnabled true
}
release {
@@ -57,21 +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.2'
implementation 'com.evernote:android-job:1.2.4'
implementation "com.google.android.gms:play-services-ads:$playServicesVersion"
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
implementation "com.google.android.gms:play-services-vision:$playServicesVersion"
implementation "com.google.firebase:firebase-config:$playServicesVersion"
implementation 'com.j256.ormlite:ormlite-android:5.0'
implementation 'com.mikepenz:crossfader:1.5.1'
implementation 'com.mikepenz:materialdrawer:6.0.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"
+4 -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,19 +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"
android:process=":bg"
android:directBootAware="true"/>
tools:node="remove"/>
<service android:name="com.evernote.android.job.gcm.PlatformGcmService"
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"/>
@@ -37,8 +37,10 @@ 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
@@ -126,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"))
@@ -167,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
@@ -102,7 +104,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
}
pluginConfigure.onPreferenceChangeListener = this
initPlugins()
app.listenForPackageChanges { initPlugins() }
receiver = app.listenForPackageChanges(false) { initPlugins() }
DataStore.privateStore.registerChangeListener(this)
}
@@ -179,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()
@@ -24,22 +24,19 @@ import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.BuildConfig
import org.xbill.DNS.*
import java.net.Inet6Address
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.UnknownHostException
import okhttp3.Dns as Okdns
import java.io.IOException
import java.net.*
object Dns {
private const val TAG = "Dns"
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
}
@@ -60,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
@@ -50,10 +49,10 @@ object LocalDnsService {
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", timeout)
.put("EDNSClientSubnet", JSONObject().put("Policy", "disable"))
if (edns) dns
@@ -73,10 +72,12 @@ object LocalDnsService {
.put("MinimumTTL", 120)
.put("CacheSize", 4096)
val remoteDns = JSONArray(profile.remoteDns.split(",")
.mapIndexed { i, dns -> makeDns("UserDef-" + i, dns.trim(), 9) })
.mapIndexed { i, dns -> makeDns("UserDef-" + i,
dns.trim() + ":53", 9) })
val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "119.29.29.29", 3, false),
makeDns("Primary-2", "114.114.114.114", 3, false)
makeDns("Primary-1", "119.29.29.29:53", 3, false),
makeDns("Primary-2", "114.114.114.114:53", 3, false),
makeDns("Primary-3", "208.67.222.222:443", 3, false)
))
when (profile.route) {
@@ -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.
@@ -73,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
@@ -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)
}
@@ -29,6 +29,7 @@ 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
@@ -37,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
@@ -54,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()
@@ -117,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> {
@@ -128,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)
@@ -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),
@@ -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
@@ -85,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)
}
@@ -28,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() {
@@ -72,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)
@@ -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 =
@@ -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>
@@ -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>
-1
View File
@@ -1,6 +1,5 @@
<?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>
+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>
+8
View File
@@ -8,4 +8,12 @@
android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.QuickToggleShortcut" />
</shortcut>
<shortcut android:shortcutId="scan"
android:icon="@drawable/ic_qu_camera_launcher"
android:shortcutShortLabel="@string/add_profile_methods_scan_qr_code"
android:shortcutLongLabel="@string/add_profile_methods_scan_qr_code">
<intent android:action="android.intent.action.MAIN"
android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.ScannerActivity" />
</shortcut>
</shortcuts>
@@ -70,8 +70,8 @@ abstract class NativePluginProvider : ContentProvider() {
/**
* Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
*
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the default
* behavior.
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
* default behavior.
*
* @return Absolute path for executable entry.
*/
@@ -29,13 +29,11 @@ import android.widget.Toast
* Activity that's capable of getting EXTRA_OPTIONS input.
*/
abstract class OptionsCapableActivity : AppCompatActivity() {
protected fun pluginOptions(intent: Intent = this.intent): PluginOptions {
return try {
PluginOptions(intent.getStringExtra(PluginContract.EXTRA_OPTIONS))
} catch (exc: IllegalArgumentException) {
Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show()
PluginOptions()
}
protected fun pluginOptions(intent: Intent = this.intent) = try {
PluginOptions(intent.getStringExtra(PluginContract.EXTRA_OPTIONS))
} catch (exc: IllegalArgumentException) {
Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show()
PluginOptions()
}
/**
@@ -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="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+2 -1
View File
@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Shadowsocks" parent="PreferenceFixTheme.Light.NoActionBar">
<!--suppress NewApi -->
<item name="android:navigationBarColor">@color/material_primary_700</item>
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
<item name="colorAccent">@color/material_accent_200</item>
<item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item>
<item name="colorPrimaryDark">@color/material_primary_700</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.Fix</item>
<item name="preferenceCategory_marginBottom">8dp</item>
<item name="windowActionModeOverlay">true</item>
</style>
</resources>
+1
View File
@@ -4,3 +4,4 @@ release=$1
cp mobile/build/outputs/apk/release/mobile-armeabi-v7a-release.apk shadowsocks-armeabi-v7a-${release}.apk
cp mobile/build/outputs/apk/release/mobile-arm64-v8a-release.apk shadowsocks-arm64-v8a-${release}.apk
cp mobile/build/outputs/apk/release/mobile-x86-release.apk shadowsocks-x86-${release}.apk
cp mobile/build/outputs/apk/release/mobile-universal-release.apk shadowsocks--universal-${release}.apk