Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
com.ccwangluo.accelerator
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
sheteng
com.ccwangluo.accelerator
Commits
be6cf63d
Commit
be6cf63d
authored
Feb 23, 2019
by
Mygod
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into preference-1.1
parents
f4171711
dca0da09
Changes
39
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
39 changed files
with
942 additions
and
478 deletions
+942
-478
build.gradle
build.gradle
+3
-5
core/build.gradle
core/build.gradle
+9
-1
core/src/main/assets/acl/gfwlist.acl
core/src/main/assets/acl/gfwlist.acl
+372
-18
core/src/main/java/com/github/shadowsocks/acl/Acl.kt
core/src/main/java/com/github/shadowsocks/acl/Acl.kt
+3
-2
core/src/main/java/com/github/shadowsocks/aidl/ShadowsocksConnection.kt
...java/com/github/shadowsocks/aidl/ShadowsocksConnection.kt
+10
-6
core/src/main/java/com/github/shadowsocks/bg/BaseService.kt
core/src/main/java/com/github/shadowsocks/bg/BaseService.kt
+35
-29
core/src/main/java/com/github/shadowsocks/bg/Executable.kt
core/src/main/java/com/github/shadowsocks/bg/Executable.kt
+3
-4
core/src/main/java/com/github/shadowsocks/bg/ProxyInstance.kt
.../src/main/java/com/github/shadowsocks/bg/ProxyInstance.kt
+4
-2
core/src/main/java/com/github/shadowsocks/bg/ServiceNotification.kt
...ain/java/com/github/shadowsocks/bg/ServiceNotification.kt
+1
-1
core/src/main/java/com/github/shadowsocks/database/Profile.kt
.../src/main/java/com/github/shadowsocks/database/Profile.kt
+27
-24
core/src/main/java/com/github/shadowsocks/database/ProfileManager.kt
...in/java/com/github/shadowsocks/database/ProfileManager.kt
+35
-0
core/src/main/java/com/github/shadowsocks/net/ChannelMonitor.kt
...rc/main/java/com/github/shadowsocks/net/ChannelMonitor.kt
+12
-3
core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt
...rc/main/java/com/github/shadowsocks/net/LocalDnsServer.kt
+11
-11
core/src/main/java/com/github/shadowsocks/net/LocalSocketListener.kt
...in/java/com/github/shadowsocks/net/LocalSocketListener.kt
+1
-1
core/src/main/java/com/github/shadowsocks/net/TcpFastOpen.kt
core/src/main/java/com/github/shadowsocks/net/TcpFastOpen.kt
+2
-1
core/src/main/java/com/github/shadowsocks/utils/DeviceStorageApp.kt
...ain/java/com/github/shadowsocks/utils/DeviceStorageApp.kt
+1
-1
core/src/main/java/com/github/shadowsocks/utils/Utils.kt
core/src/main/java/com/github/shadowsocks/utils/Utils.kt
+2
-0
core/src/main/jni/shadowsocks-libev
core/src/main/jni/shadowsocks-libev
+1
-1
core/src/main/res/values/strings.xml
core/src/main/res/values/strings.xml
+1
-0
mobile/build.gradle
mobile/build.gradle
+5
-0
mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt
...om/github/shadowsocks/GlobalSettingsPreferenceFragment.kt
+2
-2
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
+29
-22
mobile/src/main/java/com/github/shadowsocks/ProfileConfigActivity.kt
...main/java/com/github/shadowsocks/ProfileConfigActivity.kt
+14
-7
mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt
...main/java/com/github/shadowsocks/ProfileConfigFragment.kt
+36
-23
mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt
.../src/main/java/com/github/shadowsocks/ProfilesFragment.kt
+45
-36
mobile/src/main/java/com/github/shadowsocks/QuickToggleShortcut.kt
...c/main/java/com/github/shadowsocks/QuickToggleShortcut.kt
+5
-4
mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
...in/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
+80
-66
mobile/src/main/java/com/github/shadowsocks/bg/TileService.kt
...le/src/main/java/com/github/shadowsocks/bg/TileService.kt
+18
-11
mobile/src/main/java/com/github/shadowsocks/widget/ServiceButton.kt
.../main/java/com/github/shadowsocks/widget/ServiceButton.kt
+6
-6
mobile/src/main/java/com/github/shadowsocks/widget/StatsBar.kt
...e/src/main/java/com/github/shadowsocks/widget/StatsBar.kt
+5
-5
mobile/src/main/res/menu/profile_manager_menu.xml
mobile/src/main/res/menu/profile_manager_menu.xml
+5
-0
plugin/CHANGES.md
plugin/CHANGES.md
+3
-0
plugin/build.gradle
plugin/build.gradle
+29
-4
plugin/gradle-mvn-push.gradle
plugin/gradle-mvn-push.gradle
+0
-133
plugin/gradle.properties
plugin/gradle.properties
+22
-0
plugin/src/main/java/com/github/shadowsocks/plugin/AlertDialogFragment.kt
...java/com/github/shadowsocks/plugin/AlertDialogFragment.kt
+70
-0
tv/src/main/java/com/github/shadowsocks/tv/MainFragment.kt
tv/src/main/java/com/github/shadowsocks/tv/MainFragment.kt
+1
-1
tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt
.../java/com/github/shadowsocks/tv/MainPreferenceFragment.kt
+33
-47
tv/src/main/res/xml/pref_main.xml
tv/src/main/res/xml/pref_main.xml
+1
-1
No files found.
build.gradle
View file @
be6cf63d
...
...
@@ -9,14 +9,11 @@ buildscript {
sdkVersion
=
28
compileSdkVersion
=
28
buildToolsVersion
=
'28.0.3'
lifecycleVersion
=
'2.0.0'
roomVersion
=
'2.0.0'
workVersion
=
'1.0.0-beta05'
junitVersion
=
'4.12'
androidTestVersion
=
'1.1.1'
androidEspressoVersion
=
'3.1.1'
versionCode
=
4070
0
50
versionName
=
'4.7.
0
-nightly'
versionCode
=
4070
1
50
versionName
=
'4.7.
1
-nightly'
}
repositories
{
...
...
@@ -28,6 +25,7 @@ buildscript {
}
dependencies
{
classpath
'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
classpath
'com.android.tools.build:gradle:3.3.1'
classpath
'com.github.ben-manes:gradle-versions-plugin:0.20.0'
classpath
'com.google.gms:google-services:4.2.0'
...
...
core/build.gradle
View file @
be6cf63d
apply
plugin:
'com.android.library'
apply
plugin:
'kotlin-android'
apply
plugin:
'kotlin-android-extensions'
apply
plugin:
'kotlin-kapt'
android
{
...
...
@@ -38,6 +39,13 @@ android {
}
}
androidExtensions
{
experimental
=
true
}
def
lifecycleVersion
=
'2.0.0'
def
roomVersion
=
'2.0.0'
def
workVersion
=
'1.0.0-rc02'
dependencies
{
api
project
(
':plugin'
)
api
"android.arch.work:work-runtime-ktx:$workVersion"
...
...
@@ -46,7 +54,7 @@ dependencies {
api
'androidx.preference:preference:1.1.0-alpha03'
api
"androidx.room:room-runtime:$roomVersion"
api
'com.crashlytics.sdk.android:crashlytics:2.9.9'
api
'com.google.firebase:firebase-config:16.
3.0
'
api
'com.google.firebase:firebase-config:16.
1.3
'
api
'com.google.firebase:firebase-core:16.0.7'
api
'dnsjava:dnsjava:2.1.8'
api
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
...
...
core/src/main/assets/acl/gfwlist.acl
View file @
be6cf63d
This diff is collapsed.
Click to expand it.
core/src/main/java/com/github/shadowsocks/acl/Acl.kt
View file @
be6cf63d
...
...
@@ -32,6 +32,7 @@ import java.io.File
import
java.io.IOException
import
java.io.Reader
import
java.net.URL
import
java.net.URLConnection
class
Acl
{
companion
object
{
...
...
@@ -151,11 +152,11 @@ class Acl {
fromReader
(
getFile
(
id
).
bufferedReader
())
}
catch
(
_
:
IOException
)
{
this
}
fun
flatten
(
depth
:
Int
):
Acl
{
suspend
fun
flatten
(
depth
:
Int
,
connect
:
suspend
(
URL
)
->
URLConnection
):
Acl
{
if
(
depth
>
0
)
for
(
url
in
urls
.
asIterable
())
{
val
child
=
Acl
()
try
{
child
.
fromReader
(
url
.
openStream
().
bufferedReader
(),
bypass
).
flatten
(
depth
-
1
)
child
.
fromReader
(
connect
(
url
).
getInputStream
().
bufferedReader
(),
bypass
).
flatten
(
depth
-
1
,
connect
)
}
catch
(
e
:
IOException
)
{
e
.
printStackTrace
()
continue
...
...
core/src/main/java/com/github/shadowsocks/aidl/ShadowsocksConnection.kt
View file @
be6cf63d
...
...
@@ -28,6 +28,7 @@ import android.os.DeadObjectException
import
android.os.Handler
import
android.os.IBinder
import
android.os.RemoteException
import
com.github.shadowsocks.bg.BaseService
import
com.github.shadowsocks.bg.ProxyService
import
com.github.shadowsocks.bg.TransproxyService
import
com.github.shadowsocks.bg.VpnService
...
...
@@ -51,7 +52,7 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
}
interface
Callback
{
fun
stateChanged
(
state
:
Int
,
profileName
:
String
?,
msg
:
String
?)
fun
stateChanged
(
state
:
BaseService
.
State
,
profileName
:
String
?,
msg
:
String
?)
fun
trafficUpdated
(
profileId
:
Long
,
stats
:
TrafficStats
)
{
}
fun
trafficPersisted
(
profileId
:
Long
)
{
}
...
...
@@ -68,13 +69,16 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
private
var
callback
:
Callback
?
=
null
private
val
serviceCallback
=
object
:
IShadowsocksServiceCallback
.
Stub
()
{
override
fun
stateChanged
(
state
:
Int
,
profileName
:
String
?,
msg
:
String
?)
{
handler
.
post
{
callback
!!
.
stateChanged
(
state
,
profileName
,
msg
)
}
val
callback
=
callback
?:
return
handler
.
post
{
callback
.
stateChanged
(
BaseService
.
State
.
values
()[
state
],
profileName
,
msg
)
}
}
override
fun
trafficUpdated
(
profileId
:
Long
,
stats
:
TrafficStats
)
{
handler
.
post
{
callback
!!
.
trafficUpdated
(
profileId
,
stats
)
}
val
callback
=
callback
?:
return
handler
.
post
{
callback
.
trafficUpdated
(
profileId
,
stats
)
}
}
override
fun
trafficPersisted
(
profileId
:
Long
)
{
handler
.
post
{
callback
!!
.
trafficPersisted
(
profileId
)
}
val
callback
=
callback
?:
return
handler
.
post
{
callback
.
trafficPersisted
(
profileId
)
}
}
}
private
var
binder
:
IBinder
?
=
null
...
...
@@ -105,14 +109,14 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
override
fun
onServiceDisconnected
(
name
:
ComponentName
?)
{
unregisterCallback
()
callback
!!
.
onServiceDisconnected
()
callback
?
.
onServiceDisconnected
()
service
=
null
binder
=
null
}
override
fun
binderDied
()
{
service
=
null
handler
.
post
(
callback
!!::
onBinderDied
)
callback
?.
also
{
handler
.
post
(
it
::
onBinderDied
)
}
}
private
fun
unregisterCallback
()
{
...
...
core/src/main/java/com/github/shadowsocks/bg/BaseService.kt
View file @
be6cf63d
...
...
@@ -39,9 +39,11 @@ import com.github.shadowsocks.plugin.PluginManager
import
com.github.shadowsocks.utils.Action
import
com.github.shadowsocks.utils.broadcastReceiver
import
com.github.shadowsocks.utils.printLog
import
com.github.shadowsocks.utils.readableMessage
import
com.google.firebase.analytics.FirebaseAnalytics
import
kotlinx.coroutines.*
import
java.io.File
import
java.net.BindException
import
java.net.InetAddress
import
java.net.URL
import
java.net.UnknownHostException
...
...
@@ -51,20 +53,22 @@ import java.util.*
* This object uses WeakMap to simulate the effects of multi-inheritance.
*/
object
BaseService
{
/**
* IDLE state is only used by UI and will never be returned by BaseService.
*/
const
val
IDLE
=
0
const
val
CONNECTING
=
1
const
val
CONNECTED
=
2
const
val
STOPPING
=
3
const
val
STOPPED
=
4
enum
class
State
(
val
canStop
:
Boolean
=
false
)
{
/**
* Idle state is only used by UI and will never be returned by BaseService.
*/
Idle
,
Connecting
(
true
),
Connected
(
true
),
Stopping
,
Stopped
,
}
const
val
CONFIG_FILE
=
"shadowsocks.conf"
const
val
CONFIG_FILE_UDP
=
"shadowsocks-udp.conf"
class
Data
internal
constructor
(
private
val
service
:
Interface
)
{
var
state
=
S
TOPPED
var
state
=
S
tate
.
Stopped
var
processes
:
GuardedProcessPool
?
=
null
var
proxy
:
ProxyInstance
?
=
null
var
udpFallback
:
ProxyInstance
?
=
null
...
...
@@ -81,7 +85,7 @@ object BaseService {
val
binder
=
Binder
(
this
)
var
connectingJob
:
Job
?
=
null
fun
changeState
(
s
:
Int
,
msg
:
String
?
=
null
)
{
fun
changeState
(
s
:
State
,
msg
:
String
?
=
null
)
{
if
(
state
==
s
&&
msg
==
null
)
return
binder
.
stateChanged
(
s
,
msg
)
state
=
s
...
...
@@ -102,7 +106,7 @@ object BaseService {
private
val
bandwidthListeners
=
mutableMapOf
<
IBinder
,
Long
>()
// the binder is the real identifier
private
val
handler
=
Handler
()
override
fun
getState
():
Int
=
data
!!
.
state
override
fun
getState
():
Int
=
data
!!
.
state
.
ordinal
override
fun
getProfileName
():
String
=
data
!!
.
proxy
?.
profile
?.
name
?:
"Idle"
override
fun
registerCallback
(
cb
:
IShadowsocksServiceCallback
)
{
...
...
@@ -124,12 +128,12 @@ object BaseService {
handler
.
postDelayed
(
this
::
onTimeout
,
bandwidthListeners
.
values
.
min
()
?:
return
)
}
private
fun
onTimeout
()
{
val
proxies
=
listOfNotNull
(
data
!!
.
proxy
,
data
!!
.
udpFallback
)
val
proxies
=
listOfNotNull
(
data
?.
proxy
,
data
?
.
udpFallback
)
val
stats
=
proxies
.
map
{
Pair
(
it
.
profile
.
id
,
it
.
trafficMonitor
?.
requestUpdate
())
}
.
filter
{
it
.
second
!=
null
}
.
map
{
Triple
(
it
.
first
,
it
.
second
!!
.
first
,
it
.
second
!!
.
second
)
}
if
(
stats
.
any
{
it
.
third
}
&&
state
==
CONNECTED
&&
bandwidthListeners
.
isNotEmpty
())
{
if
(
stats
.
any
{
it
.
third
}
&&
data
!!
.
state
==
State
.
Connected
&&
bandwidthListeners
.
isNotEmpty
())
{
val
sum
=
stats
.
fold
(
TrafficStats
())
{
a
,
b
->
a
+
b
.
second
}
broadcast
{
item
->
if
(
bandwidthListeners
.
contains
(
item
.
asBinder
()))
{
...
...
@@ -145,7 +149,7 @@ object BaseService {
val
wasEmpty
=
bandwidthListeners
.
isEmpty
()
if
(
bandwidthListeners
.
put
(
cb
.
asBinder
(),
timeout
)
==
null
)
{
if
(
wasEmpty
)
registerTimeout
()
if
(
state
!=
CONNECTED
)
return
if
(
data
!!
.
state
!=
State
.
Connected
)
return
var
sum
=
TrafficStats
()
val
proxy
=
data
!!
.
proxy
?:
return
proxy
.
trafficMonitor
?.
out
.
also
{
stats
->
...
...
@@ -177,9 +181,9 @@ object BaseService {
callbacks
.
unregister
(
cb
)
}
fun
stateChanged
(
s
:
Int
,
msg
:
String
?)
{
fun
stateChanged
(
s
:
State
,
msg
:
String
?)
{
val
profileName
=
profileName
broadcast
{
it
.
stateChanged
(
s
,
profileName
,
msg
)
}
broadcast
{
it
.
stateChanged
(
s
.
ordinal
,
profileName
,
msg
)
}
}
fun
trafficPersisted
(
ids
:
List
<
Long
>)
{
...
...
@@ -211,9 +215,9 @@ object BaseService {
return
}
val
s
=
data
.
state
when
(
s
)
{
STOPPED
->
startRunner
()
CONNECTED
->
stopRunner
(
true
)
when
{
s
==
State
.
Stopped
->
startRunner
()
s
.
canStop
->
stopRunner
(
true
)
else
->
Crashlytics
.
log
(
Log
.
WARN
,
tag
,
"Illegal state when invoking use: $s"
)
}
}
...
...
@@ -249,11 +253,12 @@ object BaseService {
}
fun
stopRunner
(
restart
:
Boolean
=
false
,
msg
:
String
?
=
null
)
{
if
(
data
.
state
==
S
TOPPING
)
return
if
(
data
.
state
==
S
tate
.
Stopping
)
return
// channge the state
data
.
changeState
(
S
TOPPING
)
data
.
changeState
(
S
tate
.
Stopping
)
GlobalScope
.
launch
(
Dispatchers
.
Main
,
CoroutineStart
.
UNDISPATCHED
)
{
Core
.
analytics
.
logEvent
(
"stop"
,
bundleOf
(
Pair
(
FirebaseAnalytics
.
Param
.
METHOD
,
tag
)))
data
.
connectingJob
?.
cancelAndJoin
()
// ensure stop connecting first
this
@Interface
as
Service
// we use a coroutineScope here to allow clean-up in parallel
coroutineScope
{
...
...
@@ -278,7 +283,7 @@ object BaseService {
}
// change the state
data
.
changeState
(
S
TOPPED
,
msg
)
data
.
changeState
(
S
tate
.
Stopped
,
msg
)
// stop the service if nothing has bound to it
if
(
restart
)
startRunner
()
else
stopSelf
()
...
...
@@ -291,7 +296,7 @@ object BaseService {
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
,
startId
:
Int
):
Int
{
val
data
=
data
if
(
data
.
state
!=
S
TOPPED
)
return
Service
.
START_NOT_STICKY
if
(
data
.
state
!=
S
tate
.
Stopped
)
return
Service
.
START_NOT_STICKY
val
profilePair
=
Core
.
currentProfile
this
as
Context
if
(
profilePair
==
null
)
{
...
...
@@ -318,7 +323,7 @@ object BaseService {
data
.
notification
=
createNotification
(
profile
.
formattedName
)
Core
.
analytics
.
logEvent
(
"start"
,
bundleOf
(
Pair
(
FirebaseAnalytics
.
Param
.
METHOD
,
tag
)))
data
.
changeState
(
CONNECTING
)
data
.
changeState
(
State
.
Connecting
)
data
.
connectingJob
=
GlobalScope
.
launch
(
Dispatchers
.
Main
)
{
try
{
Executable
.
killAll
()
// clean up old processes
...
...
@@ -328,8 +333,7 @@ object BaseService {
data
.
processes
=
GuardedProcessPool
{
printLog
(
it
)
data
.
connectingJob
?.
cancelAndJoin
()
stopRunner
(
false
,
it
.
localizedMessage
)
stopRunner
(
false
,
it
.
readableMessage
)
}
startProcesses
()
...
...
@@ -337,16 +341,18 @@ object BaseService {
data
.
udpFallback
?.
scheduleUpdate
()
RemoteConfig
.
fetch
()
data
.
changeState
(
CONNECTED
)
data
.
changeState
(
State
.
Connected
)
}
catch
(
_
:
CancellationException
)
{
// if the job was cancelled, it is canceller's responsibility to call stopRunner
}
catch
(
_
:
UnknownHostException
)
{
stopRunner
(
false
,
getString
(
R
.
string
.
invalid_server
))
}
catch
(
exc
:
Throwable
)
{
if
(
exc
!
is
PluginManager
.
PluginNotFoundException
&&
exc
!
is
VpnService
.
NullConnectionException
)
{
if
(
exc
!
is
PluginManager
.
PluginNotFoundException
&&
exc
!
is
BindException
&&
exc
!
is
VpnService
.
NullConnectionException
)
{
printLog
(
exc
)
}
stopRunner
(
false
,
"${getString(R.string.service_failed)}: ${exc.
localized
Message}"
)
stopRunner
(
false
,
"${getString(R.string.service_failed)}: ${exc.
readable
Message}"
)
}
finally
{
data
.
connectingJob
=
null
}
...
...
core/src/main/java/com/github/shadowsocks/bg/Executable.kt
View file @
be6cf63d
...
...
@@ -26,7 +26,6 @@ import android.system.OsConstants
import
android.text.TextUtils
import
android.util.Log
import
com.crashlytics.android.Crashlytics
import
com.github.shadowsocks.Core.app
import
java.io.File
import
java.io.FileNotFoundException
...
...
@@ -40,11 +39,11 @@ object Executable {
fun
killAll
()
{
for
(
process
in
File
(
"/proc"
).
listFiles
{
_
,
name
->
TextUtils
.
isDigitsOnly
(
name
)
})
{
val
exe
=
File
(
try
{
File
(
process
,
"cmdline"
).
readText
()
}
catch
(
ignore
:
FileNotFoundException
)
{
File
(
process
,
"cmdline"
).
inputStream
().
bufferedReader
().
readText
()
}
catch
(
_
:
FileNotFoundException
)
{
continue
}.
split
(
Character
.
MIN_VALUE
,
limit
=
2
).
first
())
if
(
exe
.
parent
==
app
.
applicationInfo
.
nativeLibraryDir
&&
EXECUTABLES
.
contains
(
exe
.
name
))
try
{
if
(
EXECUTABLES
.
contains
(
exe
.
name
))
try
{
Os
.
kill
(
process
.
name
.
toInt
(),
OsConstants
.
SIGKILL
)
}
catch
(
e
:
ErrnoException
)
{
if
(
e
.
errno
!=
OsConstants
.
ESRCH
)
{
...
...
core/src/main/java/com/github/shadowsocks/bg/ProxyInstance.kt
View file @
be6cf63d
...
...
@@ -61,7 +61,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn
.
doOutput
=
true
val
proxies
=
try
{
withTimeoutOrNull
(
3
0_000
)
{
withTimeoutOrNull
(
1
0_000
)
{
withContext
(
Dispatchers
.
IO
)
{
conn
.
outputStream
.
bufferedWriter
().
use
{
it
.
write
(
"sig="
+
Base64
.
encodeToString
(
mdg
.
digest
(),
Base64
.
DEFAULT
))
...
...
@@ -80,7 +80,9 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
profile
.
method
=
proxy
[
3
].
trim
()
}
if
(
route
==
Acl
.
CUSTOM_RULES
)
Acl
.
save
(
Acl
.
CUSTOM_RULES
,
Acl
.
customRules
.
flatten
(
10
))
if
(
route
==
Acl
.
CUSTOM_RULES
)
withContext
(
Dispatchers
.
IO
)
{
Acl
.
save
(
Acl
.
CUSTOM_RULES
,
Acl
.
customRules
.
flatten
(
10
,
service
::
openConnection
))
}
// it's hard to resolve DNS on a specific interface so we'll do it here
if
(
profile
.
host
.
parseNumericAddress
()
==
null
)
profile
.
host
=
withTimeoutOrNull
(
10_000
)
{
...
...
core/src/main/java/com/github/shadowsocks/bg/ServiceNotification.kt
View file @
be6cf63d
...
...
@@ -94,7 +94,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
}
private
fun
update
(
action
:
String
?,
forceShow
:
Boolean
=
false
)
{
if
(
forceShow
||
service
.
data
.
state
==
BaseService
.
CONNECTED
)
when
(
action
)
{
if
(
forceShow
||
service
.
data
.
state
==
BaseService
.
State
.
Connected
)
when
(
action
)
{
Intent
.
ACTION_SCREEN_OFF
->
{
setVisible
(
false
,
forceShow
)
unregisterCallback
()
// unregister callback to save battery
...
...
core/src/main/java/com/github/shadowsocks/database/Profile.kt
View file @
be6cf63d
...
...
@@ -21,6 +21,7 @@
package
com.github.shadowsocks.database
import
android.net.Uri
import
android.os.Parcelable
import
android.util.Base64
import
android.util.Log
import
android.util.LongSparseArray
...
...
@@ -32,6 +33,7 @@ import com.github.shadowsocks.preference.DataStore
import
com.github.shadowsocks.utils.Key
import
com.github.shadowsocks.utils.asIterable
import
com.github.shadowsocks.utils.parsePort
import
kotlinx.android.parcel.Parcelize
import
org.json.JSONArray
import
org.json.JSONObject
import
org.json.JSONTokener
...
...
@@ -41,7 +43,31 @@ import java.net.URISyntaxException
import
java.util.*
@Entity
class
Profile
:
Serializable
{
@Parcelize
data class
Profile
(
@PrimaryKey
(
autoGenerate
=
true
)
var
id
:
Long
=
0
,
var
name
:
String
?
=
""
,
var
host
:
String
=
"198.199.101.152"
,
var
remotePort
:
Int
=
8388
,
var
password
:
String
=
"u1rRWTssNv0p"
,
var
method
:
String
=
"aes-256-cfb"
,
var
route
:
String
=
"all"
,
var
remoteDns
:
String
=
"8.8.8.8"
,
var
proxyApps
:
Boolean
=
false
,
var
bypass
:
Boolean
=
false
,
var
udpdns
:
Boolean
=
false
,
var
ipv6
:
Boolean
=
true
,
var
individual
:
String
=
""
,
var
tx
:
Long
=
0
,
var
rx
:
Long
=
0
,
var
userOrder
:
Long
=
0
,
var
plugin
:
String
?
=
null
,
var
udpFallback
:
Long
?
=
null
,
@Ignore
// not persisted in db, only used by direct boot
var
dirty
:
Boolean
=
false
)
:
Parcelable
,
Serializable
{
companion
object
{
private
const
val
TAG
=
"ShadowParser"
private
const
val
serialVersionUID
=
0L
...
...
@@ -200,29 +226,6 @@ class Profile : Serializable {
fun
deleteAll
():
Int
}
@PrimaryKey
(
autoGenerate
=
true
)
var
id
:
Long
=
0
var
name
:
String
?
=
""
var
host
:
String
=
"198.199.101.152"
var
remotePort
:
Int
=
8388
var
password
:
String
=
"u1rRWTssNv0p"
var
method
:
String
=
"aes-256-cfb"
var
route
:
String
=
"all"
var
remoteDns
:
String
=
"8.8.8.8"
var
proxyApps
:
Boolean
=
false
var
bypass
:
Boolean
=
false
var
udpdns
:
Boolean
=
false
var
ipv6
:
Boolean
=
true
var
individual
:
String
=
""
var
tx
:
Long
=
0
var
rx
:
Long
=
0
var
userOrder
:
Long
=
0
var
plugin
:
String
?
=
null
var
udpFallback
:
Long
?
=
null
@Ignore
// not persisted in db, only used by direct boot
var
dirty
:
Boolean
=
false
val
formattedAddress
get
()
=
(
if
(
host
.
contains
(
":"
))
"[%s]:%d"
else
"%s:%d"
).
format
(
host
,
remotePort
)
val
formattedName
get
()
=
if
(
name
.
isNullOrEmpty
())
formattedAddress
else
name
!!
...
...
core/src/main/java/com/github/shadowsocks/database/ProfileManager.kt
View file @
be6cf63d
...
...
@@ -21,11 +21,14 @@
package
com.github.shadowsocks.database
import
android.database.sqlite.SQLiteCantOpenDatabaseException
import
android.util.LongSparseArray
import
com.github.shadowsocks.Core
import
com.github.shadowsocks.preference.DataStore
import
com.github.shadowsocks.utils.DirectBoot
import
com.github.shadowsocks.utils.printLog
import
org.json.JSONArray
import
java.io.IOException
import
java.io.InputStream
import
java.sql.SQLException
/**
...
...
@@ -36,6 +39,7 @@ object ProfileManager {
interface
Listener
{
fun
onAdd
(
profile
:
Profile
)
fun
onRemove
(
profileId
:
Long
)
fun
onCleared
()
}
var
listener
:
Listener
?
=
null
...
...
@@ -48,6 +52,36 @@ object ProfileManager {
return
profile
}
fun
createProfilesFromJson
(
jsons
:
Sequence
<
InputStream
>,
replace
:
Boolean
=
false
)
{
val
profiles
=
if
(
replace
)
getAllProfiles
()
?.
associateBy
{
it
.
formattedAddress
}
else
null
val
feature
=
if
(
replace
)
{
profiles
?.
values
?.
singleOrNull
{
it
.
id
==
DataStore
.
profileId
}
}
else
Core
.
currentProfile
?.
first
val
lazyClear
=
lazy
{
clear
()
}
var
result
:
Exception
?
=
null
for
(
json
in
jsons
)
try
{
Profile
.
parseJson
(
json
.
bufferedReader
().
readText
(),
feature
)
{
if
(
replace
)
{
lazyClear
.
value
// if two profiles has the same address, treat them as the same profile and copy stats over
profiles
?.
get
(
it
.
formattedAddress
)
?.
apply
{
it
.
tx
=
tx
it
.
rx
=
rx
}
}
createProfile
(
it
)
}
}
catch
(
e
:
Exception
)
{
if
(
result
==
null
)
result
=
e
else
result
.
addSuppressed
(
e
)
}
if
(
result
!=
null
)
throw
result
}
fun
serializeToJson
(
profiles
:
List
<
Profile
>?
=
getAllProfiles
()):
JSONArray
?
{
if
(
profiles
==
null
)
return
null
val
lookup
=
LongSparseArray
<
Profile
>(
profiles
.
size
).
apply
{
profiles
.
forEach
{
put
(
it
.
id
,
it
)
}
}
return
JSONArray
(
profiles
.
map
{
it
.
toJson
(
lookup
)
}.
toTypedArray
())
}
/**
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
*/
...
...
@@ -78,6 +112,7 @@ object ProfileManager {
fun
clear
()
=
PrivateDatabase
.
profileDao
.
deleteAll
().
also
{
// listener is not called since this won't be used in mobile submodule
DirectBoot
.
clean
()
listener
?.
onCleared
()
}
@Throws
(
IOException
::
class
)
...
...
core/src/main/java/com/github/shadowsocks/net/ChannelMonitor.kt
View file @
be6cf63d
...
...
@@ -20,6 +20,7 @@
package
com.github.shadowsocks.net
import
android.os.Build
import
com.github.shadowsocks.utils.printLog
import
kotlinx.coroutines.*
import
kotlinx.coroutines.channels.Channel
...
...
@@ -65,23 +66,31 @@ class ChannelMonitor : Thread("ChannelMonitor") {
start
()
}
/**
* Prevent NetworkOnMainThreadException because people enable strict mode for no reasons.
*/
private
suspend
fun
WritableByteChannel
.
writeCompat
(
src
:
ByteBuffer
)
=
if
(
Build
.
VERSION
.
SDK_INT
<=
23
)
withContext
(
Dispatchers
.
Default
)
{
write
(
src
)
}
else
write
(
src
)
suspend
fun
register
(
channel
:
SelectableChannel
,
ops
:
Int
,
block
:
(
SelectionKey
)
->
Unit
):
SelectionKey
{
val
registration
=
Registration
(
channel
,
ops
,
block
)
pendingRegistrations
.
send
(
registration
)
ByteBuffer
.
allocateDirect
(
1
).
also
{
junk
->
loop@
while
(
running
)
when
(
registrationPipe
.
sink
().
write
(
junk
))
{
loop@
while
(
running
)
when
(
registrationPipe
.
sink
().
write
Compat
(
junk
))
{
0
->
kotlinx
.
coroutines
.
yield
()
1
->
break
@loop
else
->
throw
IOException
(
"Failed to register in the channel"
)
}
}
if
(!
running
)
throw
C
losedChannel
Exception
()
if
(!
running
)
throw
C
ancellation
Exception
()
return
registration
.
result
.
await
()
}
suspend
fun
wait
(
channel
:
SelectableChannel
,
ops
:
Int
)
=
CompletableDeferred
<
SelectionKey
>().
run
{
register
(
channel
,
ops
)
{
if
(
it
.
isValid
)
it
.
interestOps
(
0
)
// stop listening
if
(
it
.
isValid
)
try
{
it
.
interestOps
(
0
)
// stop listening
}
catch
(
_
:
CancelledKeyException
)
{
}
complete
(
it
)
}
await
()
...
...
core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt
View file @
be6cf63d
...
...
@@ -68,7 +68,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
private
fun
prepareDnsResponse
(
request
:
Message
)
=
Message
(
request
.
header
.
id
).
apply
{
header
.
setFlag
(
Flags
.
QR
.
toInt
())
// this is a response
if
(
request
.
header
.
getFlag
(
Flags
.
RD
.
toInt
()))
header
.
setFlag
(
Flags
.
RD
.
toInt
())
addRecord
(
request
.
question
,
Section
.
QUESTION
)
request
.
question
?.
also
{
addRecord
(
it
,
Section
.
QUESTION
)
}
}
}
private
val
monitor
=
ChannelMonitor
()
...
...
@@ -99,30 +99,30 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
printLog
(
e
)
return
forward
(
packet
)
}
return
coroutine
Scope
{
return
super
visor
Scope
{
val
remote
=
async
{
withTimeout
(
TIMEOUT
)
{
forward
(
packet
)
}
}
try
{
if
(
forwardOnly
||
request
.
header
.
opcode
!=
Opcode
.
QUERY
)
return
@
coroutine
Scope
remote
.
await
()
if
(
forwardOnly
||
request
.
header
.
opcode
!=
Opcode
.
QUERY
)
return
@
supervisor
Scope
remote
.
await
()
val
question
=
request
.
question
if
(
question
?.
type
!=
Type
.
A
)
return
@
coroutine
Scope
remote
.
await
()
if
(
question
?.
type
!=
Type
.
A
)
return
@
supervisor
Scope
remote
.
await
()
val
host
=
question
.
name
.
toString
(
true
)
if
(
remoteDomainMatcher
?.
containsMatchIn
(
host
)
==
true
)
return
@
coroutine
Scope
remote
.
await
()
if
(
remoteDomainMatcher
?.
containsMatchIn
(
host
)
==
true
)
return
@
supervisor
Scope
remote
.
await
()
val
localResults
=
try
{
withTimeout
(
TIMEOUT
)
{
GlobalScope
.
async
(
Dispatchers
.
IO
)
{
localResolver
(
host
)
}.
await
()
}
}
catch
(
_
:
TimeoutCancellationException
)
{
Crashlytics
.
log
(
Log
.
WARN
,
TAG
,
"Local resolving timed out, falling back to remote resolving"
)
return
@
coroutine
Scope
remote
.
await
()
return
@
supervisor
Scope
remote
.
await
()
}
catch
(
_
:
UnknownHostException
)
{
return
@
coroutine
Scope
remote
.
await
()
return
@
supervisor
Scope
remote
.
await
()
}
if
(
localResults
.
isEmpty
())
return
@
coroutine
Scope
remote
.
await
()
if
(
localResults
.
isEmpty
())
return
@
supervisor
Scope
remote
.
await
()
if
(
localIpMatcher
.
isEmpty
()
||
localIpMatcher
.
any
{
subnet
->
localResults
.
any
(
subnet
::
matches
)
})
{
remote
.
cancel
()
ByteBuffer
.
wrap
(
prepareDnsResponse
(
request
).
apply
{
header
.
setFlag
(
Flags
.
RA
.
toInt
())
// recursion available
for
(
address
in
localResults
)
addRecord
(
when
(
address
)
{
is
Inet4Address
->
ARecord
(
request
.
question
.
name
,
DClass
.
IN
,
TTL
,
address
)
is
Inet6Address
->
AAAARecord
(
request
.
question
.
name
,
DClass
.
IN
,
TTL
,
address
)
is
Inet4Address
->
ARecord
(
question
.
name
,
DClass
.
IN
,
TTL
,
address
)
is
Inet6Address
->
AAAARecord
(
question
.
name
,
DClass
.
IN
,
TTL
,
address
)
else
->
throw
IllegalStateException
(
"Unsupported address $address"
)
},
Section
.
ANSWER
)
}.
toWire
())
...
...
@@ -159,7 +159,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
check
(
channel
.
send
(
remoteDns
.
udpWrap
(
packet
),
proxy
)
>
0
)
monitor
.
wait
(
channel
,
SelectionKey
.
OP_READ
)
val
result
=
remoteDns
.
udpReceiveBuffer
(
UDP_PACKET_SIZE
)
check
(
channel
.
receive
(
result
)
==
proxy
)
while
(
channel
.
receive
(
result
)
!=
proxy
)
result
.
clear
(
)
result
.
flip
()
remoteDns
.
udpUnwrap
(
result
)
result
...
...
core/src/main/java/com/github/shadowsocks/net/LocalSocketListener.kt
View file @
be6cf63d
...
...
@@ -65,7 +65,7 @@ abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name
open
fun
shutdown
(
scope
:
CoroutineScope
)
{
running
=
false
localSocket
.
fileDescriptor
.
apply
{
localSocket
.
fileDescriptor
?
.
apply
{
// see also: https://issuetracker.google.com/issues/36945762#comment15
if
(
valid
())
try
{
Os
.
shutdown
(
this
,
OsConstants
.
SHUT_RDWR
)
...
...
core/src/main/java/com/github/shadowsocks/net/TcpFastOpen.kt
View file @
be6cf63d
...
...
@@ -20,6 +20,7 @@
package
com.github.shadowsocks.net
import
com.github.shadowsocks.utils.readableMessage
import
kotlinx.coroutines.runBlocking
import
kotlinx.coroutines.withTimeoutOrNull
import
java.io.File
...
...
@@ -57,7 +58,7 @@ object TcpFastOpen {
ProcessBuilder
(
"su"
,
"-c"
,
"echo 3 > $PATH"
).
redirectErrorStream
(
true
).
start
()
.
inputStream
.
bufferedReader
().
readText
()
}
catch
(
e
:
IOException
)
{
e
.
localized
Message
e
.
readable
Message
}
}
fun
enableTimeout
()
=
runBlocking
{
withTimeoutOrNull
(
1000
)
{
enable
()
}
}
...
...
core/src/main/java/com/github/shadowsocks/utils/DeviceStorageApp.kt
View file @
be6cf63d
...
...
@@ -36,5 +36,5 @@ class DeviceStorageApp(context: Context) : Application() {
* Thou shalt not get the REAL underlying application context which would no longer be operating under device
* protected storage.
*/
override
fun
getApplicationContext
()
:
Context
=
this
override
fun
getApplicationContext
()
=
this
}
core/src/main/java/com/github/shadowsocks/utils/Utils.kt
View file @
be6cf63d
...
...
@@ -42,6 +42,8 @@ import kotlinx.coroutines.launch
import
java.net.HttpURLConnection
import
java.net.InetAddress
val
Throwable
.
readableMessage
get
()
=
localizedMessage
?:
javaClass
.
name
private
val
parseNumericAddress
by
lazy
{
InetAddress
::
class
.
java
.
getDeclaredMethod
(
"parseNumericAddress"
,
String
::
class
.
java
).
apply
{
isAccessible
=
true
...
...
shadowsocks-libev
@
b430124e
Subproject commit
7ad2df961505ecddb44984ce34ea86874cd2ecc
4
Subproject commit
b430124ed82973d4edf345dfadbe556f9337748
4
core/src/main/res/values/strings.xml
View file @
be6cf63d
...
...
@@ -97,6 +97,7 @@
<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_replace_file"
>
Replace 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>
...
...
mobile/build.gradle
View file @
be6cf63d
...
...
@@ -5,6 +5,7 @@ import java.util.regex.Pattern
apply
plugin:
'com.android.application'
apply
plugin:
'io.fabric'
apply
plugin:
'kotlin-android'
apply
plugin:
'kotlin-android-extensions'
def
getCurrentFlavor
()
{
String
task
=
getGradle
().
getStartParameter
().
getTaskRequests
().
toString
()
...
...
@@ -49,6 +50,10 @@ android {
new
File
(
project
(
':core'
).
buildDir
,
"intermediates/bundles/${getCurrentFlavor()}/jni"
)
}
androidExtensions
{
experimental
=
true
}
dependencies
{
implementation
project
(
':core'
)
implementation
'androidx.browser:browser:1.0.0'
...
...
mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt
View file @
be6cf63d
...
...
@@ -88,8 +88,8 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
portTransproxy
.
isEnabled
=
enabledTransproxy
true
}
val
listener
:
(
Int
)
->
Unit
=
{
if
(
it
==
BaseService
.
S
TOPPED
)
{
val
listener
:
(
BaseService
.
State
)
->
Unit
=
{
if
(
it
==
BaseService
.
S
tate
.
Stopped
)
{
tfo
.
isEnabled
=
true
serviceMode
.
isEnabled
=
true
portProxy
.
isEnabled
=
true
...
...
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
View file @
be6cf63d
...
...
@@ -23,6 +23,7 @@ package com.github.shadowsocks
import
android.app.Activity
import
android.app.backup.BackupManager
import
android.content.ActivityNotFoundException
import
android.content.DialogInterface
import
android.content.Intent
import
android.net.VpnService
import
android.nfc.NdefMessage
...
...
@@ -30,6 +31,7 @@ import android.nfc.NfcAdapter
import
android.os.Bundle
import
android.os.DeadObjectException
import
android.os.Handler
import
android.os.Parcelable
import
android.util.Log
import
android.view.KeyCharacterMap
import
android.view.KeyEvent
...
...
@@ -51,6 +53,8 @@ import com.github.shadowsocks.aidl.TrafficStats
import
com.github.shadowsocks.bg.BaseService
import
com.github.shadowsocks.database.Profile
import
com.github.shadowsocks.database.ProfileManager
import
com.github.shadowsocks.plugin.AlertDialogFragment
import
com.github.shadowsocks.plugin.Empty
import
com.github.shadowsocks.preference.DataStore
import
com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import
com.github.shadowsocks.utils.Key
...
...
@@ -58,6 +62,7 @@ import com.github.shadowsocks.widget.ServiceButton
import
com.github.shadowsocks.widget.StatsBar
import
com.google.android.material.navigation.NavigationView
import
com.google.android.material.snackbar.Snackbar
import
kotlinx.android.parcel.Parcelize
class
MainActivity
:
AppCompatActivity
(),
ShadowsocksConnection
.
Callback
,
OnPreferenceDataStoreChangeListener
,
NavigationView
.
OnNavigationItemSelectedListener
{
...
...
@@ -65,7 +70,18 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
private
const
val
TAG
=
"ShadowsocksMainActivity"
private
const
val
REQUEST_CONNECT
=
1
var
stateListener
:
((
Int
)
->
Unit
)?
=
null
var
stateListener
:
((
BaseService
.
State
)
->
Unit
)?
=
null
}
@Parcelize
data class
ProfilesArg
(
val
profiles
:
List
<
Profile
>)
:
Parcelable
class
ImportProfilesDialogFragment
:
AlertDialogFragment
<
ProfilesArg
,
Empty
>()
{
override
fun
AlertDialog
.
Builder
.
prepare
(
listener
:
DialogInterface
.
OnClickListener
)
{
setTitle
(
R
.
string
.
add_profile_dialog
)
setPositiveButton
(
R
.
string
.
yes
)
{
_
,
_
->
arg
.
profiles
.
forEach
{
ProfileManager
.
createProfile
(
it
)
}
}
setNegativeButton
(
R
.
string
.
no
,
null
)
setMessage
(
arg
.
profiles
.
joinToString
(
"\n"
))
}
}
// UI
...
...
@@ -91,12 +107,12 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
// service
var
state
=
BaseService
.
IDLE
override
fun
stateChanged
(
state
:
Int
,
profileName
:
String
?,
msg
:
String
?)
=
changeState
(
state
,
msg
,
true
)
var
state
=
BaseService
.
State
.
Idle
override
fun
stateChanged
(
state
:
BaseService
.
State
,
profileName
:
String
?,
msg
:
String
?)
=
changeState
(
state
,
msg
,
true
)
override
fun
trafficUpdated
(
profileId
:
Long
,
stats
:
TrafficStats
)
{
if
(
profileId
==
0L
)
this
@MainActivity
.
stats
.
updateTraffic
(
stats
.
txRate
,
stats
.
rxRate
,
stats
.
txTotal
,
stats
.
rxTotal
)
if
(
state
!=
BaseService
.
S
TOPPING
)
{
if
(
state
!=
BaseService
.
S
tate
.
Stopping
)
{
(
supportFragmentManager
.
findFragmentById
(
R
.
id
.
fragment_holder
)
as
?
ToolbarFragment
)
?.
onTrafficUpdated
(
profileId
,
stats
)
}
...
...
@@ -105,7 +121,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
ProfilesFragment
.
instance
?.
onTrafficPersisted
(
profileId
)
}
private
fun
changeState
(
state
:
Int
,
msg
:
String
?
=
null
,
animate
:
Boolean
=
false
)
{
private
fun
changeState
(
state
:
BaseService
.
State
,
msg
:
String
?
=
null
,
animate
:
Boolean
=
false
)
{
fab
.
changeState
(
state
,
animate
)
stats
.
changeState
(
state
)
if
(
msg
!=
null
)
snackbar
(
getString
(
R
.
string
.
vpn_error
,
msg
)).
show
()
...
...
@@ -115,7 +131,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
private
fun
toggle
()
=
when
{
state
==
BaseService
.
CONNECTED
->
Core
.
stopService
()
state
.
canStop
->
Core
.
stopService
()
DataStore
.
serviceMode
==
Key
.
modeVpn
->
{
val
intent
=
VpnService
.
prepare
(
this
)
if
(
intent
!=
null
)
startActivityForResult
(
intent
,
REQUEST_CONNECT
)
...
...
@@ -127,11 +143,11 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
private
val
handler
=
Handler
()
private
val
connection
=
ShadowsocksConnection
(
handler
,
true
)
override
fun
onServiceConnected
(
service
:
IShadowsocksService
)
=
changeState
(
try
{
service
.
state
BaseService
.
State
.
values
()[
service
.
state
]
}
catch
(
_
:
DeadObjectException
)
{
BaseService
.
IDLE
BaseService
.
State
.
Idle
})
override
fun
onServiceDisconnected
()
=
changeState
(
BaseService
.
IDLE
)
override
fun
onServiceDisconnected
()
=
changeState
(
BaseService
.
State
.
Idle
)
override
fun
onBinderDied
()
{
connection
.
disconnect
(
this
)
connection
.
connect
(
this
,
this
)
...
...
@@ -152,7 +168,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
super
.
onCreate
(
savedInstanceState
)
setContentView
(
R
.
layout
.
layout_main
)
stats
=
findViewById
(
R
.
id
.
stats
)
stats
.
setOnClickListener
{
if
(
state
==
BaseService
.
CONNECTED
)
stats
.
testConnection
()
}
stats
.
setOnClickListener
{
if
(
state
==
BaseService
.
State
.
Connected
)
stats
.
testConnection
()
}
drawer
=
findViewById
(
R
.
id
.
drawer
)
navigation
=
findViewById
(
R
.
id
.
navigation
)
navigation
.
setNavigationItemSelectedListener
(
this
)
...
...
@@ -164,7 +180,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
fab
=
findViewById
(
R
.
id
.
fab
)
fab
.
setOnClickListener
{
toggle
()
}
changeState
(
BaseService
.
IDLE
)
// reset everything to init state
changeState
(
BaseService
.
State
.
Idle
)
// reset everything to init state
connection
.
connect
(
this
,
this
)
DataStore
.
publicStore
.
registerChangeListener
(
this
)
...
...
@@ -188,17 +204,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
if
(
sharedStr
.
isNullOrEmpty
())
return
val
profiles
=
Profile
.
findAllUrls
(
sharedStr
,
Core
.
currentProfile
?.
first
).
toList
()
if
(
profiles
.
isEmpty
())
{
snackbar
().
setText
(
R
.
string
.
profile_invalid_input
).
show
()
return
}
AlertDialog
.
Builder
(
this
)
.
setTitle
(
R
.
string
.
add_profile_dialog
)
.
setPositiveButton
(
R
.
string
.
yes
)
{
_
,
_
->
profiles
.
forEach
{
ProfileManager
.
createProfile
(
it
)
}
}
.
setNegativeButton
(
R
.
string
.
no
,
null
)
.
setMessage
(
profiles
.
joinToString
(
"\n"
))
.
create
()
.
show
()
if
(
profiles
.
isEmpty
())
snackbar
().
setText
(
R
.
string
.
profile_invalid_input
).
show
()
else
ImportProfilesDialogFragment
().
withArg
(
ProfilesArg
(
profiles
)).
show
(
supportFragmentManager
,
null
)
}
override
fun
onPreferenceDataStoreChanged
(
store
:
PreferenceDataStore
,
key
:
String
?)
{
...
...
mobile/src/main/java/com/github/shadowsocks/ProfileConfigActivity.kt
View file @
be6cf63d
...
...
@@ -21,12 +21,15 @@
package
com.github.shadowsocks
import
android.app.Activity
import
android.content.DialogInterface
import
android.content.Intent
import
android.os.Bundle
import
android.view.Menu
import
android.view.MenuItem
import
androidx.appcompat.app.AlertDialog
import
androidx.appcompat.app.AppCompatActivity
import
com.github.shadowsocks.plugin.AlertDialogFragment
import
com.github.shadowsocks.plugin.Empty
import
com.github.shadowsocks.plugin.PluginContract
import
com.github.shadowsocks.preference.DataStore
...
...
@@ -35,6 +38,15 @@ class ProfileConfigActivity : AppCompatActivity() {
const
val
REQUEST_CODE_PLUGIN_HELP
=
1
}
class
UnsavedChangesDialogFragment
:
AlertDialogFragment
<
Empty
,
Empty
>()
{
override
fun
AlertDialog
.
Builder
.
prepare
(
listener
:
DialogInterface
.
OnClickListener
)
{
setTitle
(
R
.
string
.
unsaved_changes_prompt
)
setPositiveButton
(
R
.
string
.
yes
,
listener
)
setNegativeButton
(
R
.
string
.
no
,
listener
)
setNeutralButton
(
android
.
R
.
string
.
cancel
,
null
)
}
}
private
val
child
by
lazy
{
supportFragmentManager
.
findFragmentById
(
R
.
id
.
content
)
as
ProfileConfigFragment
}
override
fun
onCreate
(
savedInstanceState
:
Bundle
?)
{
...
...
@@ -59,13 +71,8 @@ class ProfileConfigActivity : AppCompatActivity() {
override
fun
onOptionsItemSelected
(
item
:
MenuItem
)
=
child
.
onOptionsItemSelected
(
item
)
override
fun
onBackPressed
()
{
if
(
DataStore
.
dirty
)
AlertDialog
.
Builder
(
this
)
.
setTitle
(
R
.
string
.
unsaved_changes_prompt
)
.
setPositiveButton
(
R
.
string
.
yes
)
{
_
,
_
->
child
.
saveAndExit
()
}
.
setNegativeButton
(
R
.
string
.
no
)
{
_
,
_
->
finish
()
}
.
setNeutralButton
(
android
.
R
.
string
.
cancel
,
null
)
.
create
()
.
show
()
else
super
.
onBackPressed
()
if
(
DataStore
.
dirty
)
UnsavedChangesDialogFragment
().
show
(
child
,
ProfileConfigFragment
.
REQUEST_UNSAVED_CHANGES
)
else
super
.
onBackPressed
()
}
override
fun
onActivityResult
(
requestCode
:
Int
,
resultCode
:
Int
,
data
:
Intent
?)
{
...
...
mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt
View file @
be6cf63d
...
...
@@ -22,23 +22,24 @@ package com.github.shadowsocks
import
android.app.Activity
import
android.content.BroadcastReceiver
import
android.content.DialogInterface
import
android.content.Intent
import
android.os.Bundle
import
android.os.Parcelable
import
android.view.MenuItem
import
androidx.appcompat.app.AlertDialog
import
androidx.preference.*
import
com.github.shadowsocks.Core.app
import
com.github.shadowsocks.database.Profile
import
com.github.shadowsocks.database.ProfileManager
import
com.github.shadowsocks.plugin.PluginConfiguration
import
com.github.shadowsocks.plugin.PluginContract
import
com.github.shadowsocks.plugin.PluginManager
import
com.github.shadowsocks.plugin.PluginOptions
import
com.github.shadowsocks.plugin.*
import
com.github.shadowsocks.preference.*
import
com.github.shadowsocks.utils.Action
import
com.github.shadowsocks.utils.DirectBoot
import
com.github.shadowsocks.utils.Key
import
com.github.shadowsocks.utils.readableMessage
import
com.google.android.material.snackbar.Snackbar
import
kotlinx.android.parcel.Parcelize
class
ProfileConfigFragment
:
PreferenceFragmentCompat
(),
Preference
.
OnPreferenceChangeListener
,
OnPreferenceDataStoreChangeListener
{
...
...
@@ -46,6 +47,20 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
override
fun
provideSummary
(
preference
:
EditTextPreference
?)
=
"\u2022"
.
repeat
(
preference
?.
text
?.
length
?:
0
)
private
const
val
REQUEST_CODE_PLUGIN_CONFIGURE
=
1
const
val
REQUEST_UNSAVED_CHANGES
=
2
}
@Parcelize
data class
ProfileIdArg
(
val
profileId
:
Long
)
:
Parcelable
class
DeleteConfirmationDialogFragment
:
AlertDialogFragment
<
ProfileIdArg
,
Empty
>()
{
override
fun
AlertDialog
.
Builder
.
prepare
(
listener
:
DialogInterface
.
OnClickListener
)
{
setTitle
(
R
.
string
.
delete_confirm_prompt
)
setPositiveButton
(
R
.
string
.
yes
)
{
_
,
_
->
ProfileManager
.
delProfile
(
arg
.
profileId
)
requireActivity
().
finish
()
}
setNegativeButton
(
R
.
string
.
no
,
null
)
}
}
private
var
profileId
=
-
1L
...
...
@@ -114,7 +129,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
}.
show
(
fragmentManager
?:
return
,
Key
.
pluginConfigure
)
}
fun
saveAndExit
()
{
private
fun
saveAndExit
()
{
val
profile
=
ProfileManager
.
getProfile
(
profileId
)
?:
Profile
()
profile
.
id
=
profileId
profile
.
deserialize
()
...
...
@@ -140,7 +155,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
DataStore
.
dirty
=
true
true
}
catch
(
exc
:
RuntimeException
)
{
Snackbar
.
make
(
view
!!
,
exc
.
localized
Message
,
Snackbar
.
LENGTH_LONG
).
show
()
Snackbar
.
make
(
view
!!
,
exc
.
readable
Message
,
Snackbar
.
LENGTH_LONG
).
show
()
false
}
...
...
@@ -167,28 +182,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
}
override
fun
onActivityResult
(
requestCode
:
Int
,
resultCode
:
Int
,
data
:
Intent
?)
{
if
(
requestCode
==
REQUEST_CODE_PLUGIN_CONFIGURE
)
when
(
resultCode
)
{
Activity
.
RESULT_OK
->
{
val
options
=
data
?.
getStringExtra
(
PluginContract
.
EXTRA_OPTIONS
)
pluginConfigure
.
text
=
options
onPreferenceChange
(
null
,
options
)
when
(
requestCode
)
{
REQUEST_CODE_PLUGIN_CONFIGURE
->
when
(
resultCode
)
{
Activity
.
RESULT_OK
->
{
val
options
=
data
?.
getStringExtra
(
PluginContract
.
EXTRA_OPTIONS
)
pluginConfigure
.
text
=
options
onPreferenceChange
(
null
,
options
)
}
PluginContract
.
RESULT_FALLBACK
->
showPluginEditor
()
}
REQUEST_UNSAVED_CHANGES
->
when
(
resultCode
)
{
DialogInterface
.
BUTTON_POSITIVE
->
saveAndExit
()
DialogInterface
.
BUTTON_NEGATIVE
->
requireActivity
().
finish
()
}
PluginContract
.
RESULT_FALLBACK
->
showPluginEditor
(
)
}
else
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
)
else
->
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
)
}
}
override
fun
onOptionsItemSelected
(
item
:
MenuItem
)
=
when
(
item
.
itemId
)
{
R
.
id
.
action_delete
->
{
val
activity
=
requireActivity
()
AlertDialog
.
Builder
(
activity
)
.
setTitle
(
R
.
string
.
delete_confirm_prompt
)
.
setPositiveButton
(
R
.
string
.
yes
)
{
_
,
_
->
ProfileManager
.
delProfile
(
profileId
)
activity
.
finish
()
}
.
setNegativeButton
(
R
.
string
.
no
,
null
)
.
create
()
.
show
()
DeleteConfirmationDialogFragment
().
withArg
(
ProfileIdArg
(
profileId
)).
show
(
this
)
true
}
R
.
id
.
action_apply
->
{
...
...
mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt
View file @
be6cf63d
...
...
@@ -49,12 +49,12 @@ import com.github.shadowsocks.preference.DataStore
import
com.github.shadowsocks.utils.Action
import
com.github.shadowsocks.utils.datas
import
com.github.shadowsocks.utils.printLog
import
com.github.shadowsocks.utils.readableMessage
import
com.github.shadowsocks.widget.UndoSnackbarManager
import
com.google.android.gms.ads.AdRequest
import
com.google.android.gms.ads.AdSize
import
com.google.android.gms.ads.AdView
import
net.glxn.qrgen.android.QRCode
import
org.json.JSONArray
class
ProfilesFragment
:
ToolbarFragment
(),
Toolbar
.
OnMenuItemClickListener
{
companion
object
{
...
...
@@ -65,21 +65,16 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private
const
val
KEY_URL
=
"com.github.shadowsocks.QRCodeDialog.KEY_URL"
private
const
val
REQUEST_IMPORT_PROFILES
=
1
private
const
val
REQUEST_REPLACE_PROFILES
=
3
private
const
val
REQUEST_EXPORT_PROFILES
=
2
}
/**
* Is ProfilesFragment editable at all.
*/
private
val
isEnabled
get
()
=
when
((
activity
as
MainActivity
).
state
)
{
BaseService
.
CONNECTED
,
BaseService
.
STOPPED
->
true
else
->
false
}
private
fun
isProfileEditable
(
id
:
Long
)
=
when
((
activity
as
MainActivity
).
state
)
{
BaseService
.
CONNECTED
->
id
!
in
Core
.
activeProfileIds
BaseService
.
STOPPED
->
true
else
->
false
}
private
val
isEnabled
get
()
=
(
activity
as
MainActivity
).
state
.
let
{
it
.
canStop
||
it
==
BaseService
.
State
.
Stopped
}
private
fun
isProfileEditable
(
id
:
Long
)
=
(
activity
as
MainActivity
).
state
==
BaseService
.
State
.
Stopped
||
id
!
in
Core
.
activeProfileIds
@SuppressLint
(
"ValidFragment"
)
class
QRCodeDialog
()
:
DialogFragment
()
{
...
...
@@ -175,21 +170,20 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
var
adView
=
adView
if
(
item
.
host
==
"198.199.101.152"
)
{
if
(
adView
==
null
)
{
val
params
=
LinearLayout
.
LayoutParams
(
ViewGroup
.
LayoutParams
.
WRAP_CONT
ENT
,
ViewGroup
.
LayoutParams
.
WRAP_CONTENT
)
val
params
=
LinearLayout
.
LayoutParams
(
ViewGroup
.
LayoutParams
.
MATCH_PAR
ENT
,
AdSize
.
SMART_BANNER
.
getHeightInPixels
(
context
)
)
params
.
gravity
=
Gravity
.
CENTER_HORIZONTAL
adView
=
AdView
(
context
)
adView
.
layoutParams
=
params
adView
.
adUnitId
=
"ca-app-pub-9097031975646651/7760346322"
adView
.
adSize
=
AdSize
.
FLUID
val
padding
=
context
.
resources
.
getDimensionPixelOffset
(
R
.
dimen
.
profile_padding
)
adView
.
setPadding
(
padding
,
0
,
0
,
padding
)
adView
.
adSize
=
AdSize
.
SMART_BANNER
itemView
.
findViewById
<
LinearLayout
>(
R
.
id
.
content
).
addView
(
adView
)
// Load Ad
val
adBuilder
=
AdRequest
.
Builder
()
adBuilder
.
addTestDevice
(
"B08FC1764A7B250E91EA9D0D5EBEB208"
)
adBuilder
.
addTestDevice
(
"7509D18EB8AF82F915874FEF53877A64"
)
adView
.
loadAd
(
adBuilder
.
build
())
this
.
adView
=
adView
}
else
adView
.
visibility
=
View
.
VISIBLE
...
...
@@ -203,7 +197,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
Core
.
switchProfile
(
item
.
id
)
profilesAdapter
.
refreshId
(
old
)
itemView
.
isSelected
=
true
if
(
activity
.
state
==
BaseService
.
CONNECTED
)
Core
.
reloadService
()
if
(
activity
.
state
.
canStop
)
Core
.
reloadService
()
}
}
...
...
@@ -296,6 +290,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
notifyItemRemoved
(
index
)
if
(
profileId
==
DataStore
.
profileId
)
DataStore
.
profileId
=
0
// switch to null profile
}
override
fun
onCleared
()
{
profiles
.
clear
()
notifyDataSetChanged
()
}
}
private
var
selectedItem
:
ProfileViewHolder
?
=
null
...
...
@@ -383,12 +382,20 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R
.
id
.
action_import_file
->
{
startFilesForResult
(
Intent
(
Intent
.
ACTION_GET_CONTENT
).
apply
{
addCategory
(
Intent
.
CATEGORY_OPENABLE
)
type
=
"application/json"
type
=
"application/*"
putExtra
(
Intent
.
EXTRA_ALLOW_MULTIPLE
,
true
)
putExtra
(
Intent
.
EXTRA_MIME_TYPES
,
arrayOf
(
"application/*"
,
"text/*"
))
},
REQUEST_IMPORT_PROFILES
)
true
}
R
.
id
.
action_replace_file
->
{
startFilesForResult
(
Intent
(
Intent
.
ACTION_GET_CONTENT
).
apply
{
type
=
"application/*"
putExtra
(
Intent
.
EXTRA_ALLOW_MULTIPLE
,
true
)
putExtra
(
Intent
.
EXTRA_MIME_TYPES
,
arrayOf
(
"application/*"
,
"text/*"
))
},
REQUEST_REPLACE_PROFILES
)
true
}
R
.
id
.
action_manual_settings
->
{
startConfig
(
ProfileManager
.
createProfile
(
Profile
().
also
{
Core
.
currentProfile
?.
first
?.
copyFeatureSettingsTo
(
it
)
}))
...
...
@@ -404,7 +411,6 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
R
.
id
.
action_export_file
->
{
startFilesForResult
(
Intent
(
Intent
.
ACTION_CREATE_DOCUMENT
).
apply
{
addCategory
(
Intent
.
CATEGORY_OPENABLE
)
type
=
"application/json"
putExtra
(
Intent
.
EXTRA_TITLE
,
"profiles.json"
)
// optional title that can be edited
},
REQUEST_EXPORT_PROFILES
)
...
...
@@ -414,9 +420,9 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
}
private
fun
startFilesForResult
(
intent
:
Intent
?
,
requestCode
:
Int
)
{
private
fun
startFilesForResult
(
intent
:
Intent
,
requestCode
:
Int
)
{
try
{
startActivityForResult
(
intent
,
requestCode
)
startActivityForResult
(
intent
.
addCategory
(
Intent
.
CATEGORY_OPENABLE
)
,
requestCode
)
return
}
catch
(
_
:
ActivityNotFoundException
)
{
}
catch
(
_
:
SecurityException
)
{
}
(
activity
as
MainActivity
).
snackbar
(
getString
(
R
.
string
.
file_manager_missing
)).
show
()
...
...
@@ -426,31 +432,34 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
if
(
resultCode
!=
Activity
.
RESULT_OK
)
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
)
else
when
(
requestCode
)
{
REQUEST_IMPORT_PROFILES
->
{
val
feature
=
Core
.
currentProfile
?.
first
var
success
=
false
val
activity
=
activity
as
MainActivity
for
(
uri
in
data
!!
.
datas
)
try
{
Profile
.
parseJson
(
activity
.
contentResolver
.
openInputStream
(
uri
)
!!
.
bufferedReader
().
readText
(),
feature
)
{
ProfileManager
.
createProfile
(
it
)
success
=
true
}
try
{
ProfileManager
.
createProfilesFromJson
(
data
!!
.
datas
.
asSequence
().
map
{
activity
.
contentResolver
.
openInputStream
(
it
)
})
}
catch
(
e
:
Exception
)
{
printLog
(
e
)
activity
.
snackbar
(
e
.
readableMessage
).
show
()
}
}
REQUEST_REPLACE_PROFILES
->
{
val
activity
=
activity
as
MainActivity
try
{
ProfileManager
.
createProfilesFromJson
(
data
!!
.
datas
.
asSequence
().
map
{
activity
.
contentResolver
.
openInputStream
(
it
)
},
true
)
}
catch
(
e
:
Exception
)
{
activity
.
snackbar
(
e
.
readableMessage
).
show
()
}
activity
.
snackbar
().
setText
(
if
(
success
)
R
.
string
.
action_import_msg
else
R
.
string
.
action_import_err
)
.
show
()
}
REQUEST_EXPORT_PROFILES
->
{
val
profiles
=
ProfileManager
.
getAllProfiles
()
val
profiles
=
ProfileManager
.
serializeToJson
()
if
(
profiles
!=
null
)
try
{
val
lookup
=
LongSparseArray
<
Profile
>(
profiles
.
size
).
apply
{
profiles
.
forEach
{
put
(
it
.
id
,
it
)
}
}
requireContext
().
contentResolver
.
openOutputStream
(
data
?.
data
!!
)
!!
.
bufferedWriter
().
use
{
it
.
write
(
JSONArray
(
profiles
.
map
{
it
.
toJson
(
lookup
)
}.
toTypedArray
())
.
toString
(
2
))
it
.
write
(
profiles
.
toString
(
2
))
}
}
catch
(
e
:
Exception
)
{
printLog
(
e
)
(
activity
as
MainActivity
).
snackbar
(
e
.
localized
Message
).
show
()
(
activity
as
MainActivity
).
snackbar
(
e
.
readable
Message
).
show
()
}
}
else
->
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
)
...
...
mobile/src/main/java/com/github/shadowsocks/QuickToggleShortcut.kt
View file @
be6cf63d
...
...
@@ -55,14 +55,15 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Callback {
}
override
fun
onServiceConnected
(
service
:
IShadowsocksService
)
{
when
(
service
.
state
)
{
BaseService
.
STOPPED
->
Core
.
startService
()
BaseService
.
CONNECTED
->
Core
.
stopService
()
val
state
=
BaseService
.
State
.
values
()[
service
.
state
]
when
{
state
.
canStop
->
Core
.
stopService
()
state
==
BaseService
.
State
.
Stopped
->
Core
.
startService
()
}
finish
()
}
override
fun
stateChanged
(
state
:
Int
,
profileName
:
String
?,
msg
:
String
?)
{
}
override
fun
stateChanged
(
state
:
BaseService
.
State
,
profileName
:
String
?,
msg
:
String
?)
{
}
override
fun
onDestroy
()
{
connection
.
disconnect
(
this
)
...
...
mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
View file @
be6cf63d
This diff is collapsed.
Click to expand it.
mobile/src/main/java/com/github/shadowsocks/bg/TileService.kt
View file @
be6cf63d
...
...
@@ -41,9 +41,9 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private
var
tapPending
=
false
private
val
connection
=
ShadowsocksConnection
()
override
fun
stateChanged
(
state
:
Int
,
profileName
:
String
?,
msg
:
String
?)
=
updateTile
(
state
)
{
profileName
}
override
fun
stateChanged
(
state
:
BaseService
.
State
,
profileName
:
String
?,
msg
:
String
?)
=
updateTile
(
state
)
{
profileName
}
override
fun
onServiceConnected
(
service
:
IShadowsocksService
)
{
updateTile
(
service
.
state
)
{
service
.
profileName
}
updateTile
(
BaseService
.
State
.
values
()[
service
.
state
]
)
{
service
.
profileName
}
if
(
tapPending
)
{
tapPending
=
false
onClick
()
...
...
@@ -63,23 +63,28 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
if
(
isLocked
&&
!
DataStore
.
canToggleLocked
)
unlockAndRun
(
this
::
toggle
)
else
toggle
()
}
private
fun
updateTile
(
serviceState
:
Int
,
profileName
:
()
->
String
?)
{
private
fun
updateTile
(
serviceState
:
BaseService
.
State
,
profileName
:
()
->
String
?)
{
qsTile
?.
apply
{
label
=
null
when
(
serviceState
)
{
BaseService
.
STOPPED
->
{
icon
=
iconIdle
state
=
Tile
.
STATE_INACTIVE
BaseService
.
State
.
Idle
->
throw
IllegalStateException
(
"serviceState"
)
BaseService
.
State
.
Connecting
->
{
icon
=
iconBusy
state
=
Tile
.
STATE_ACTIVE
}
BaseService
.
CONNECTED
->
{
BaseService
.
State
.
Connected
->
{
icon
=
iconConnected
if
(!
keyguard
.
isDeviceLocked
)
label
=
profileName
()
state
=
Tile
.
STATE_ACTIVE
}
else
->
{
BaseService
.
State
.
Stopping
->
{
icon
=
iconBusy
state
=
Tile
.
STATE_UNAVAILABLE
}
BaseService
.
State
.
Stopped
->
{
icon
=
iconIdle
state
=
Tile
.
STATE_INACTIVE
}
}
label
=
label
?:
getString
(
R
.
string
.
app_name
)
updateTile
()
...
...
@@ -88,9 +93,11 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private
fun
toggle
()
{
val
service
=
connection
.
service
if
(
service
==
null
)
tapPending
=
true
else
when
(
service
.
state
)
{
BaseService
.
STOPPED
->
Core
.
startService
()
BaseService
.
CONNECTED
->
Core
.
stopService
()
if
(
service
==
null
)
tapPending
=
true
else
BaseService
.
State
.
values
()[
service
.
state
].
let
{
state
->
when
{
state
.
canStop
->
Core
.
stopService
()
state
==
BaseService
.
State
.
Stopped
->
Core
.
startService
()
}
}
}
}
mobile/src/main/java/com/github/shadowsocks/widget/ServiceButton.kt
View file @
be6cf63d
...
...
@@ -68,14 +68,14 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
return
drawableState
}
fun
changeState
(
state
:
Int
,
animate
:
Boolean
)
{
fun
changeState
(
state
:
BaseService
.
State
,
animate
:
Boolean
)
{
when
(
state
)
{
BaseService
.
CONNECTING
->
changeState
(
iconConnecting
,
animate
)
BaseService
.
CONNECTED
->
changeState
(
iconConnected
,
animate
)
BaseService
.
S
TOPPING
->
changeState
(
iconStopping
,
animate
)
BaseService
.
State
.
Connecting
->
changeState
(
iconConnecting
,
animate
)
BaseService
.
State
.
Connected
->
changeState
(
iconConnected
,
animate
)
BaseService
.
S
tate
.
Stopping
->
changeState
(
iconStopping
,
animate
)
else
->
changeState
(
iconStopped
,
animate
)
}
if
(
state
==
BaseService
.
CONNECTED
)
{
if
(
state
==
BaseService
.
State
.
Connected
)
{
checked
=
true
TooltipCompat
.
setTooltipText
(
this
,
context
.
getString
(
R
.
string
.
stop
))
}
else
{
...
...
@@ -83,7 +83,7 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
TooltipCompat
.
setTooltipText
(
this
,
context
.
getString
(
R
.
string
.
connect
))
}
refreshDrawableState
()
isEnabled
=
state
==
BaseService
.
CONNECTED
||
state
==
BaseService
.
STOPPED
isEnabled
=
state
.
canStop
||
state
==
BaseService
.
State
.
Stopped
}
private
fun
counters
(
a
:
AnimatedVectorDrawableCompat
,
b
:
AnimatedVectorDrawableCompat
):
Boolean
=
...
...
mobile/src/main/java/com/github/shadowsocks/widget/StatsBar.kt
View file @
be6cf63d
...
...
@@ -65,15 +65,15 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
super
.
setOnClickListener
(
l
)
}
fun
changeState
(
state
:
Int
)
{
fun
changeState
(
state
:
BaseService
.
State
)
{
val
activity
=
context
as
MainActivity
if
(
state
!=
BaseService
.
CONNECTED
)
{
if
(
state
!=
BaseService
.
State
.
Connected
)
{
updateTraffic
(
0
,
0
,
0
,
0
)
tester
.
status
.
removeObservers
(
activity
)
if
(
state
!=
BaseService
.
IDLE
)
tester
.
invalidate
()
if
(
state
!=
BaseService
.
State
.
Idle
)
tester
.
invalidate
()
statusText
.
setText
(
when
(
state
)
{
BaseService
.
CONNECTING
->
R
.
string
.
connecting
BaseService
.
S
TOPPING
->
R
.
string
.
stopping
BaseService
.
State
.
Connecting
->
R
.
string
.
connecting
BaseService
.
S
tate
.
Stopping
->
R
.
string
.
stopping
else
->
R
.
string
.
not_connected
})
}
else
{
...
...
mobile/src/main/res/menu/profile_manager_menu.xml
View file @
be6cf63d
...
...
@@ -18,6 +18,11 @@
android:id=
"@+id/action_import_file"
android:alphabeticShortcut=
"o"
android:title=
"@string/action_import_file"
/>
<item
android:id=
"@+id/action_replace_file"
android:alphabeticShortcut=
"o"
android:title=
"@string/action_replace_file"
app:alphabeticModifiers=
"CTRL|SHIFT"
/>
<item
android:id=
"@+id/action_manual_settings"
android:title=
"@string/add_profile_methods_manual_settings"
...
...
plugin/CHANGES.md
View file @
be6cf63d
*
1.2.0:
*
New helper class
`AlertDialogFragment`
for creating
`AlertDialog`
that persists through configuration changes;
*
Dependency update:
`com.google.android.material:material:1.1.0-alpha03`
.
*
1.1.0:
*
Having control characters in plugin options is no longer allowed.
If this breaks your plugin, you are doing it wrong.
...
...
plugin/build.gradle
View file @
be6cf63d
apply
plugin:
'com.android.library'
apply
plugin:
'kotlin-android'
apply
plugin:
'kotlin-android-extensions'
apply
plugin:
'com.vanniktech.maven.publish'
apply
from:
'gradle-mvn-push.gradle'
@SuppressWarnings
(
"GroovyUnusedDeclaration"
)
def
getRepositoryUsername
()
{
return
hasProperty
(
'NEXUS_USERNAME'
)
?
NEXUS_USERNAME
:
""
}
@SuppressWarnings
(
"GroovyUnusedDeclaration"
)
def
getRepositoryPassword
()
{
return
hasProperty
(
'NEXUS_PASSWORD'
)
?
NEXUS_PASSWORD
:
""
}
android
{
buildToolsVersion
rootProject
.
buildToolsVersion
...
...
@@ -10,10 +20,10 @@ android {
defaultConfig
{
minSdkVersion
rootProject
.
minSdkVersion
targetSdkVersion
rootProject
.
sdkVersion
versionCode
8
versionName
"1.1.0"
versionCode
Integer
.
parseInt
(
VERSION_CODE
)
versionName
VERSION_NAME
testInstrumentationRunner
"androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner
"androidx.test
getRepositoryPassword()
.runner.AndroidJUnitRunner"
}
buildTypes
{
...
...
@@ -23,6 +33,21 @@ android {
}
}
androidExtensions
{
experimental
=
true
}
mavenPublish
{
targets
{
uploadArchives
{
releaseRepositoryUrl
=
"https://oss.sonatype.org/service/local/staging/deploy/maven2/"
snapshotRepositoryUrl
=
"https://oss.sonatype.org/content/repositories/snapshots/"
repositoryUsername
=
getRepositoryUsername
()
repositoryPassword
=
getRepositoryPassword
()
}
}
}
dependencies
{
api
'androidx.core:core-ktx:1.0.1'
api
'com.google.android.material:material:1.1.0-alpha03'
...
...
plugin/gradle-mvn-push.gradle
deleted
100644 → 0
View file @
f4171711
/*
* Copyright 2013 Chris Banes
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply
plugin:
'maven'
apply
plugin:
'signing'
@SuppressWarnings
([
"GroovyUnusedDeclaration"
,
"GrMethodMayBeStatic"
])
def
isReleaseBuild
()
{
return
!
android
.
defaultConfig
.
versionName
.
contains
(
"SNAPSHOT"
)
}
@SuppressWarnings
(
"GroovyUnusedDeclaration"
)
def
getReleaseRepositoryUrl
()
{
return
hasProperty
(
'RELEASE_REPOSITORY_URL'
)
?
RELEASE_REPOSITORY_URL
:
"https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
@SuppressWarnings
(
"GroovyUnusedDeclaration"
)
def
getSnapshotRepositoryUrl
()
{
return
hasProperty
(
'SNAPSHOT_REPOSITORY_URL'
)
?
SNAPSHOT_REPOSITORY_URL
:
"https://oss.sonatype.org/content/repositories/snapshots/"
}
@SuppressWarnings
(
"GroovyUnusedDeclaration"
)
def
getRepositoryUsername
()
{
return
hasProperty
(
'NEXUS_USERNAME'
)
?
NEXUS_USERNAME
:
""
}
@SuppressWarnings
(
"GroovyUnusedDeclaration"
)
def
getRepositoryPassword
()
{
return
hasProperty
(
'NEXUS_PASSWORD'
)
?
NEXUS_PASSWORD
:
""
}
afterEvaluate
{
project
->
uploadArchives
{
repositories
{
mavenDeployer
{
beforeDeployment
{
MavenDeployment
deployment
->
signing
.
signPom
(
deployment
)
}
pom
.
groupId
=
'com.github.shadowsocks'
pom
.
artifactId
=
'plugin'
pom
.
version
=
android
.
defaultConfig
.
versionName
repository
(
url:
getReleaseRepositoryUrl
())
{
authentication
(
userName:
getRepositoryUsername
(),
password:
getRepositoryPassword
())
}
snapshotRepository
(
url:
getSnapshotRepositoryUrl
())
{
authentication
(
userName:
getRepositoryUsername
(),
password:
getRepositoryPassword
())
}
pom
.
project
{
name
'Shadowsocks Plugin'
packaging
'aar'
description
'Shadowsocks Plugin'
url
'https://github.com/shadowsocks/shadowsocks-android'
scm
{
url
'https://github.com/shadowsocks/shadowsocks-android.git'
connection
'scm:git:git://github.com/shadowsocks/shadowsocks-android.git'
developerConnection
'scm:git:git@github.com:shadowsocks/shadowsocks-android.git'
}
licenses
{
license
{
name
'GPLv3'
url
'https://www.gnu.org/licenses/gpl-3.0.html'
distribution
'repo'
}
}
developers
{
developer
{
id
'madeye'
name
'Max Lv'
url
'https://github.com/madeye'
}
developer
{
id
'Mygod'
name
'Mygod Studio'
url
'https://github.com/Mygod'
}
}
}
}
}
}
signing
{
required
{
isReleaseBuild
()
&&
gradle
.
taskGraph
.
hasTask
(
"uploadArchives"
)
}
sign
configurations
.
archives
}
android
.
libraryVariants
.
all
{
variant
->
def
javadocTask
=
task
(
"generate${variant.name.capitalize()}Javadoc"
,
type:
Javadoc
)
{
description
"Generates Javadoc for $variant.name."
source
=
android
.
sourceSets
.
main
.
java
.
srcDirs
classpath
+=
project
.
files
(
android
.
getBootClasspath
().
join
(
File
.
pathSeparator
))
exclude
'**/*.kt'
exclude
'**/BuildConfig.java'
exclude
'**/R.java'
}
def
jarJavadocTask
=
task
(
"jar${variant.name.capitalize()}Javadoc"
,
type:
Jar
)
{
description
"Generate Javadoc Jar for $variant.name"
classifier
=
'javadoc'
from
javadocTask
.
destinationDir
}
jarJavadocTask
.
dependsOn
javadocTask
artifacts
.
add
(
'archives'
,
jarJavadocTask
)
def
jarSourceTask
=
task
(
"jar${variant.name.capitalize()}Sources"
,
type:
Jar
)
{
description
"Generates Java Sources for $variant.name."
classifier
=
'sources'
from
android
.
sourceSets
.
main
.
java
.
srcDirs
}
artifacts
.
add
(
'archives'
,
jarSourceTask
)
}
}
plugin/gradle.properties
0 → 100644
View file @
be6cf63d
GROUP
=
com.github.shadowsocks
VERSION_NAME
=
1.2.0
VERSION_CODE
=
9
POM_ARTIFACT_ID
=
plugin
POM_NAME
=
Shadowsocks Plugin
POM_PACKAGING
=
aar
POM_DESCRIPTION
=
SIP003 plugin for Shadowsocks
POM_INCEPTION_YEAR
=
2018
POM_URL
=
https://github.com/shadowsocks/shadowsocks-android
POM_SCM_URL
=
https://github.com/shadowsocks/shadowsocks-android
POM_SCM_CONNECTION
=
scm:git:git://github.com/shadowsocks/shadowsocks-android.git
POM_SCM_DEV_CONNECTION
=
scm:git:ssh://git@github.com/shadowsocks/shadowsocks-android.git
POM_LICENCE_NAME
=
The GNU General Public License v3.0
POM_LICENCE_URL
=
https://www.gnu.org/licenses/gpl-3.0.html
POM_LICENCE_DIST
=
repo
POM_DEVELOPER_ID
=
Mygod
POM_DEVELOPER_NAME
=
Mygod Studio
plugin/src/main/java/com/github/shadowsocks/plugin/AlertDialogFragment.kt
0 → 100644
View file @
be6cf63d
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package
com.github.shadowsocks.plugin
import
android.app.Activity
import
android.content.DialogInterface
import
android.content.Intent
import
android.os.Bundle
import
android.os.Parcelable
import
androidx.appcompat.app.AlertDialog
import
androidx.appcompat.app.AppCompatDialogFragment
import
androidx.fragment.app.Fragment
import
kotlinx.android.parcel.Parcelize
/**
* Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
*/
abstract
class
AlertDialogFragment
<
Arg
:
Parcelable
,
Ret
:
Parcelable
>
:
AppCompatDialogFragment
(),
DialogInterface
.
OnClickListener
{
companion
object
{
private
const
val
KEY_ARG
=
"arg"
private
const
val
KEY_RET
=
"ret"
fun
<
T
:
Parcelable
>
getRet
(
data
:
Intent
)
=
data
.
extras
!!
.
getParcelable
<
T
>(
KEY_RET
)
!!
}
protected
abstract
fun
AlertDialog
.
Builder
.
prepare
(
listener
:
DialogInterface
.
OnClickListener
)
protected
val
arg
by
lazy
{
arguments
!!
.
getParcelable
<
Arg
>(
KEY_ARG
)
!!
}
protected
open
val
ret
:
Ret
?
get
()
=
null
fun
withArg
(
arg
:
Arg
)
=
apply
{
arguments
=
Bundle
().
apply
{
putParcelable
(
KEY_ARG
,
arg
)
}
}
override
fun
onCreateDialog
(
savedInstanceState
:
Bundle
?):
AlertDialog
=
AlertDialog
.
Builder
(
requireContext
()).
also
{
it
.
prepare
(
this
)
}.
create
()
override
fun
onClick
(
dialog
:
DialogInterface
?,
which
:
Int
)
{
targetFragment
?.
onActivityResult
(
targetRequestCode
,
which
,
ret
?.
let
{
Intent
().
replaceExtras
(
Bundle
().
apply
{
putParcelable
(
KEY_RET
,
it
)
})
})
}
override
fun
onDismiss
(
dialog
:
DialogInterface
)
{
super
.
onDismiss
(
dialog
)
targetFragment
?.
onActivityResult
(
targetRequestCode
,
Activity
.
RESULT_CANCELED
,
null
)
}
fun
show
(
target
:
Fragment
,
requestCode
:
Int
=
0
,
tag
:
String
=
javaClass
.
simpleName
)
{
setTargetFragment
(
target
,
requestCode
)
show
(
target
.
fragmentManager
?:
return
,
tag
)
}
}
@Parcelize
class
Empty
:
Parcelable
tv/src/main/java/com/github/shadowsocks/tv/MainFragment.kt
View file @
be6cf63d
...
...
@@ -38,7 +38,7 @@ class MainFragment : LeanbackSettingsFragmentCompat() {
override
fun
onPreferenceDisplayDialog
(
caller
:
PreferenceFragmentCompat
,
pref
:
Preference
?):
Boolean
{
if
(
pref
?.
key
==
Key
.
id
)
{
if
((
childFragmentManager
.
findFragmentById
(
R
.
id
.
settings_preference_fragment_container
)
as
MainPreferenceFragment
).
state
==
BaseService
.
S
TOPPED
)
{
as
MainPreferenceFragment
).
state
==
BaseService
.
S
tate
.
Stopped
)
{
startPreferenceFragment
(
ProfilesDialogFragment
().
apply
{
arguments
=
bundleOf
(
Pair
(
LeanbackPreferenceDialogFragmentCompat
.
ARG_KEY
,
Key
.
id
))
setTargetFragment
(
caller
,
0
)
...
...
tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt
View file @
be6cf63d
This diff is collapsed.
Click to expand it.
tv/src/main/res/xml/pref_main.xml
View file @
be6cf63d
...
...
@@ -13,7 +13,7 @@
app:summary=
"@string/stat_summary"
/>
<Preference
app:key=
"control.import"
app:title=
"@string/action_
import
_file"
/>
app:title=
"@string/action_
replace
_file"
/>
<Preference
app:key=
"control.export"
app:title=
"@string/action_export_file"
/>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment