Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
550f06cf2a | ||
|
|
cad258da6e | ||
|
|
71cd80a634 | ||
|
|
f0e5681052 | ||
|
|
d10a406584 | ||
|
|
a99a07f586 | ||
|
|
39a0a61da2 | ||
|
|
cf118a1bbf | ||
|
|
510098911e | ||
|
|
201bb4fe58 | ||
|
|
026d1ecb2b | ||
|
|
7461d3bf30 | ||
|
|
4483765b2d | ||
|
|
62375a576c | ||
|
|
4904e9c06c | ||
|
|
f75b54699a | ||
|
|
22ffcc53bb | ||
|
|
b69c20d086 | ||
|
|
c71c6ffe02 | ||
|
|
439d56d1d7 | ||
|
|
81f35acf20 | ||
|
|
a755b669c8 | ||
|
|
9fb180956f | ||
|
|
39530a1a3b | ||
|
|
b91d9474d5 | ||
|
|
f0140c0070 | ||
|
|
d23be09cb0 | ||
|
|
4a97ff1d30 | ||
|
|
88fc092aa2 | ||
|
|
d715f7b4c6 | ||
|
|
7cc3c48240 | ||
|
|
26fb0e5e3c | ||
|
|
f4d0835e00 | ||
|
|
ebcf514323 | ||
|
|
3975f3f43c | ||
|
|
be79e6f80c | ||
|
|
61edb40dd1 | ||
|
|
0046ed87d5 | ||
|
|
4f44d119ee | ||
|
|
1e5aabf824 | ||
|
|
7a49ddc045 | ||
|
|
5ced42125c | ||
|
|
3f88b46353 | ||
|
|
34da03d6e3 | ||
|
|
16c0d27130 | ||
|
|
bff2615599 | ||
|
|
c63a659d88 | ||
|
|
cb3fa2d923 | ||
|
|
daea4b5f6c | ||
|
|
8e7ddaebbd | ||
|
|
aaf9f27d56 | ||
|
|
7898eef48d | ||
|
|
71979434d7 | ||
|
|
ad522572f0 |
+3
-7
@@ -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
@@ -30,10 +30,11 @@
|
||||
### Почему мой ROM не поддерживается?
|
||||
|
||||
1. Некоторые ROM используют неправильную реализацию службы VPN, особенно IPv6;
|
||||
2. Некоторые ROM используют агрессивную (или так называемую неправильную) фоновую службу с ошибочной политикой остановки;
|
||||
3. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
|
||||
2. Некоторые ROM используют агрессивную (или так называемую испорченную) фоновую службу с ошибочной политикой остановки;
|
||||
3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки";
|
||||
4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
|
||||
|
||||
* Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
|
||||
* Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
|
||||
* Иправления для Huawei: [#1091 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
|
||||
* Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
|
||||
* Samsung и/или Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://travis-ci.org/shadowsocks/shadowsocks-android)
|
||||
[](https://android-arsenal.com/api?level=21)
|
||||
[](https://github.com/shadowsocks/shadowsocks-android/releases)
|
||||
[](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
A [shadowsocks](http://shadowsocks.org) client for Android, written in Kotlin.
|
||||
|
||||
+7
-4
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/python
|
||||
# -*- encoding: utf8 -*-
|
||||
|
||||
import itertools
|
||||
import math
|
||||
import sys
|
||||
|
||||
import IPy
|
||||
|
||||
Submodule core/src/main/jni/shadowsocks-libev updated: 6040be9d0a...1a21989566
+1
-1
Submodule core/src/overture/go updated: b68d7f897c...486515fc98
Submodule core/src/overture/src/github.com/shadowsocks/overture updated: 466f9902c9...58ab31a9fd
+286
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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
-1
@@ -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"/>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user