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
912eac6c
Commit
912eac6c
authored
Jul 27, 2018
by
Mygod
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add import/export to JSON
Fix #1761.
parent
d6373926
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
281 additions
and
74 deletions
+281
-74
mobile/src/androidTest/java/com/github/shadowsocks/database/ProfileTest.kt
...dTest/java/com/github/shadowsocks/database/ProfileTest.kt
+1
-1
mobile/src/main/java/com/github/shadowsocks/AppManager.kt
mobile/src/main/java/com/github/shadowsocks/AppManager.kt
+2
-2
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
+3
-2
mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt
.../src/main/java/com/github/shadowsocks/ProfilesFragment.kt
+63
-5
mobile/src/main/java/com/github/shadowsocks/ScannerActivity.kt
...e/src/main/java/com/github/shadowsocks/ScannerActivity.kt
+19
-16
mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
...in/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
+1
-1
mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt
...le/src/main/java/com/github/shadowsocks/bg/BaseService.kt
+6
-11
mobile/src/main/java/com/github/shadowsocks/database/Profile.kt
.../src/main/java/com/github/shadowsocks/database/Profile.kt
+93
-1
mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt
...in/java/com/github/shadowsocks/database/ProfileManager.kt
+1
-13
mobile/src/main/java/com/github/shadowsocks/utils/ArrayIterator.kt
...c/main/java/com/github/shadowsocks/utils/ArrayIterator.kt
+51
-0
mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt
mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt
+1
-10
mobile/src/main/res/drawable/ic_file_file_upload.xml
mobile/src/main/res/drawable/ic_file_file_upload.xml
+9
-0
mobile/src/main/res/menu/app_manager_menu.xml
mobile/src/main/res/menu/app_manager_menu.xml
+2
-2
mobile/src/main/res/menu/custom_rules_menu.xml
mobile/src/main/res/menu/custom_rules_menu.xml
+1
-1
mobile/src/main/res/menu/profile_manager_menu.xml
mobile/src/main/res/menu/profile_manager_menu.xml
+24
-7
mobile/src/main/res/menu/profile_share_popup.xml
mobile/src/main/res/menu/profile_share_popup.xml
+1
-1
mobile/src/main/res/menu/scanner_menu.xml
mobile/src/main/res/menu/scanner_menu.xml
+1
-1
mobile/src/main/res/values/strings.xml
mobile/src/main/res/values/strings.xml
+2
-0
No files found.
mobile/src/androidTest/java/com/github/shadowsocks/database/ProfileTest.kt
View file @
912eac6c
...
...
@@ -27,7 +27,7 @@ import org.junit.Test
class
ProfileTest
{
@Test
fun
parsing
()
{
val
results
=
Profile
.
findAll
(
"garble ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4#example-server garble"
)
val
results
=
Profile
.
findAll
Urls
(
"garble ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4#example-server garble"
)
.
toList
()
Assert
.
assertEquals
(
1
,
results
.
size
)
Assert
.
assertEquals
(
"ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888#example-server"
.
toUri
(),
...
...
mobile/src/main/java/com/github/shadowsocks/AppManager.kt
View file @
912eac6c
...
...
@@ -223,13 +223,13 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
}
else
Snackbar
.
make
(
appListView
,
R
.
string
.
action_export_err
,
Snackbar
.
LENGTH_LONG
).
show
()
return
true
}
R
.
id
.
action_export
->
{
R
.
id
.
action_export
_clipboard
->
{
clipboard
.
primaryClip
=
ClipData
.
newPlainText
(
Key
.
individual
,
"${DataStore.bypass}\n${DataStore.individual}"
)
Snackbar
.
make
(
appListView
,
R
.
string
.
action_export_msg
,
Snackbar
.
LENGTH_LONG
).
show
()
return
true
}
R
.
id
.
action_import
->
{
R
.
id
.
action_import
_clipboard
->
{
val
proxiedAppString
=
clipboard
.
primaryClip
?.
getItemAt
(
0
)
?.
text
?.
toString
()
if
(!
proxiedAppString
.
isNullOrEmpty
())
{
val
i
=
proxiedAppString
!!
.
indexOf
(
'\n'
)
...
...
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
View file @
912eac6c
...
...
@@ -200,7 +200,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
}
override
fun
onActivityResult
(
requestCode
:
Int
,
resultCode
:
Int
,
data
:
Intent
?)
{
if
(
resultCode
==
Activity
.
RESULT_OK
)
app
.
startService
()
else
{
if
(
requestCode
!=
REQUEST_CONNECT
)
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
)
else
if
(
resultCode
==
Activity
.
RESULT_OK
)
app
.
startService
()
else
{
snackbar
().
setText
(
R
.
string
.
vpn_permission_denied
).
show
()
Crashlytics
.
log
(
Log
.
ERROR
,
TAG
,
"Failed to start VpnService from onActivityResult: $data"
)
}
...
...
@@ -272,7 +273,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
else
->
null
}
if
(
sharedStr
.
isNullOrEmpty
())
return
val
profiles
=
Profile
.
findAll
(
sharedStr
).
toList
()
val
profiles
=
Profile
.
findAll
Urls
(
sharedStr
,
app
.
currentProfile
).
toList
()
if
(
profiles
.
isEmpty
())
{
snackbar
().
setText
(
R
.
string
.
profile_invalid_input
).
show
()
return
...
...
mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt
View file @
912eac6c
...
...
@@ -21,6 +21,7 @@
package
com.github.shadowsocks
import
android.annotation.SuppressLint
import
android.app.Activity
import
android.content.ClipData
import
android.content.ClipboardManager
import
android.content.Context
...
...
@@ -48,11 +49,15 @@ import com.github.shadowsocks.database.ProfileManager
import
com.github.shadowsocks.plugin.PluginConfiguration
import
com.github.shadowsocks.preference.DataStore
import
com.github.shadowsocks.utils.Action
import
com.github.shadowsocks.utils.datas
import
com.github.shadowsocks.utils.printLog
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
import
java.io.IOException
class
ProfilesFragment
:
ToolbarFragment
(),
Toolbar
.
OnMenuItemClickListener
{
companion
object
{
...
...
@@ -62,6 +67,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
var
instance
:
ProfilesFragment
?
=
null
private
const
val
KEY_URL
=
"com.github.shadowsocks.QRCodeDialog.KEY_URL"
private
const
val
REQUEST_IMPORT_PROFILES
=
1
private
const
val
REQUEST_EXPORT_PROFILES
=
2
}
/**
...
...
@@ -210,7 +217,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
.
commitAllowingStateLoss
()
true
}
R
.
id
.
action_export
->
{
R
.
id
.
action_export
_clipboard
->
{
clipboard
.
primaryClip
=
ClipData
.
newPlainText
(
null
,
this
.
item
.
toString
())
true
}
...
...
@@ -363,9 +370,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
startActivity
(
Intent
(
context
,
ScannerActivity
::
class
.
java
))
true
}
R
.
id
.
action_import
->
{
R
.
id
.
action_import
_clipboard
->
{
try
{
val
profiles
=
Profile
.
findAll
(
clipboard
.
primaryClip
!!
.
getItemAt
(
0
).
text
).
toList
()
val
profiles
=
Profile
.
findAllUrls
(
clipboard
.
primaryClip
!!
.
getItemAt
(
0
).
text
,
app
.
currentProfile
)
.
toList
()
if
(
profiles
.
isNotEmpty
())
{
profiles
.
forEach
{
ProfileManager
.
createProfile
(
it
)
}
(
activity
as
MainActivity
).
snackbar
().
setText
(
R
.
string
.
action_import_msg
).
show
()
...
...
@@ -377,11 +385,19 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
(
activity
as
MainActivity
).
snackbar
().
setText
(
R
.
string
.
action_import_err
).
show
()
true
}
R
.
id
.
action_import_file
->
{
startActivityForResult
(
Intent
(
Intent
.
ACTION_GET_CONTENT
).
apply
{
addCategory
(
Intent
.
CATEGORY_OPENABLE
)
type
=
"application/json"
putExtra
(
Intent
.
EXTRA_ALLOW_MULTIPLE
,
true
)
},
REQUEST_IMPORT_PROFILES
)
true
}
R
.
id
.
action_manual_settings
->
{
startConfig
(
ProfileManager
.
createProfile
())
startConfig
(
ProfileManager
.
createProfile
(
Profile
().
also
{
app
.
currentProfile
?.
copyFeatureSettingsTo
(
it
)
}
))
true
}
R
.
id
.
action_export
->
{
R
.
id
.
action_export
_clipboard
->
{
val
profiles
=
ProfileManager
.
getAllProfiles
()
(
activity
as
MainActivity
).
snackbar
().
setText
(
if
(
profiles
!=
null
)
{
clipboard
.
primaryClip
=
ClipData
.
newPlainText
(
null
,
profiles
.
joinToString
(
"\n"
))
...
...
@@ -389,10 +405,52 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
else
R
.
string
.
action_export_err
).
show
()
true
}
R
.
id
.
action_export_file
->
{
startActivityForResult
(
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
)
true
}
else
->
false
}
}
override
fun
onActivityResult
(
requestCode
:
Int
,
resultCode
:
Int
,
data
:
Intent
?)
{
if
(
resultCode
!=
Activity
.
RESULT_OK
)
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
)
else
when
(
requestCode
)
{
REQUEST_IMPORT_PROFILES
->
{
val
feature
=
app
.
currentProfile
var
success
=
false
val
activity
=
activity
as
MainActivity
for
(
uri
in
data
!!
.
datas
)
try
{
Profile
.
parseJson
(
activity
.
contentResolver
.
openInputStream
(
uri
).
bufferedReader
().
readText
(),
feature
).
forEach
{
ProfileManager
.
createProfile
(
it
)
success
=
true
}
}
catch
(
e
:
IOException
)
{
printLog
(
e
)
}
activity
.
snackbar
().
setText
(
if
(
success
)
R
.
string
.
action_import_msg
else
R
.
string
.
action_import_err
)
.
show
()
}
REQUEST_EXPORT_PROFILES
->
{
val
profiles
=
ProfileManager
.
getAllProfiles
()
if
(
profiles
!=
null
)
try
{
requireContext
().
contentResolver
.
openOutputStream
(
data
!!
.
data
).
bufferedWriter
().
use
{
it
.
write
(
JSONArray
(
profiles
.
map
{
it
.
toJson
()
}.
toTypedArray
()).
toString
(
2
))
}
}
catch
(
e
:
Exception
)
{
printLog
(
e
)
(
activity
as
MainActivity
).
snackbar
(
e
.
localizedMessage
).
show
()
}
}
else
->
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
)
}
}
override
fun
onTrafficUpdated
(
profileId
:
Long
,
txRate
:
Long
,
rxRate
:
Long
,
txTotal
:
Long
,
rxTotal
:
Long
)
{
if
(
profileId
!=
-
1L
)
{
// ignore resets from MainActivity
if
(
bandwidthProfile
!=
profileId
)
{
...
...
mobile/src/main/java/com/github/shadowsocks/ScannerActivity.kt
View file @
912eac6c
...
...
@@ -35,9 +35,12 @@ import android.util.SparseArray
import
android.view.MenuItem
import
android.widget.Toast
import
androidx.core.content.getSystemService
import
androidx.core.util.forEach
import
com.crashlytics.android.Crashlytics
import
com.github.shadowsocks.App.Companion.app
import
com.github.shadowsocks.database.Profile
import
com.github.shadowsocks.database.ProfileManager
import
com.github.shadowsocks.utils.datas
import
com.github.shadowsocks.utils.openBitmap
import
com.github.shadowsocks.utils.printLog
import
com.github.shadowsocks.utils.resolveResourceId
...
...
@@ -107,7 +110,7 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
}
override
fun
onRetrieved
(
barcode
:
Barcode
)
=
runOnUiThread
{
Profile
.
findAll
(
barcode
.
rawValu
e
).
forEach
{
ProfileManager
.
createProfile
(
it
)
}
Profile
.
findAll
Urls
(
barcode
.
rawValue
,
app
.
currentProfil
e
).
forEach
{
ProfileManager
.
createProfile
(
it
)
}
navigateUp
()
}
override
fun
onRetrievedMultiple
(
closetToClick
:
Barcode
?,
barcode
:
MutableList
<
BarcodeGraphic
>?)
=
check
(
false
)
...
...
@@ -119,31 +122,31 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
}
override
fun
onMenuItemClick
(
item
:
MenuItem
)
=
when
(
item
.
itemId
)
{
R
.
id
.
action_import
->
{
R
.
id
.
action_import
_clipboard
->
{
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
)
private
fun
startImport
(
manual
:
Boolean
=
false
)
=
startActivityForResult
(
Intent
(
Intent
.
ACTION_GET_CONTENT
).
apply
{
addCategory
(
Intent
.
CATEGORY_OPENABLE
)
type
=
"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
)
{
val
feature
=
app
.
currentProfile
var
success
=
false
var
list
=
listOfNotNull
(
data
?.
data
)
val
clipData
=
data
?.
clipData
if
(
clipData
!=
null
)
list
+=
(
0
until
clipData
.
itemCount
).
map
{
clipData
.
getItemAt
(
it
).
uri
}
for
(
uri
in
list
)
try
{
val
barcodes
=
detector
.
detect
(
Frame
.
Builder
()
.
setBitmap
(
contentResolver
.
openBitmap
(
uri
)).
build
())
for
(
i
in
0
until
barcodes
.
size
())
Profile
.
findAll
(
barcodes
.
valueAt
(
i
).
rawValue
).
forEach
{
for
(
uri
in
data
!!
.
datas
)
try
{
detector
.
detect
(
Frame
.
Builder
().
setBitmap
(
contentResolver
.
openBitmap
(
uri
)).
build
())
.
forEach
{
_
,
barcode
->
Profile
.
findAllUrls
(
barcode
.
rawValue
,
feature
).
forEach
{
ProfileManager
.
createProfile
(
it
)
success
=
true
}
}
}
catch
(
e
:
Exception
)
{
printLog
(
e
)
}
...
...
mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
View file @
912eac6c
...
...
@@ -420,7 +420,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
dialog
.
show
()
true
}
R
.
id
.
action_import
->
{
R
.
id
.
action_import
_clipboard
->
{
try
{
check
(
adapter
.
addToProxy
(
clipboard
.
primaryClip
!!
.
getItemAt
(
0
).
text
.
toString
())
!=
null
)
}
catch
(
exc
:
Exception
)
{
...
...
mobile/src/main/java/com/github/shadowsocks/bg/BaseService.kt
View file @
912eac6c
...
...
@@ -50,7 +50,6 @@ import com.google.firebase.analytics.FirebaseAnalytics
import
okhttp3.FormBody
import
okhttp3.OkHttpClient
import
okhttp3.Request
import
org.json.JSONObject
import
java.io.File
import
java.io.IOException
import
java.net.InetAddress
...
...
@@ -186,11 +185,7 @@ object BaseService {
internal
var
shadowsocksConfigFile
:
File
?
=
null
internal
fun
buildShadowsocksConfig
():
File
{
val
profile
=
profile
!!
val
config
=
JSONObject
()
.
put
(
"server"
,
profile
.
host
)
.
put
(
"server_port"
,
profile
.
remotePort
)
.
put
(
"password"
,
profile
.
password
)
.
put
(
"method"
,
profile
.
method
)
val
config
=
profile
.
toJson
(
true
)
val
pluginPath
=
pluginPath
if
(
pluginPath
!=
null
)
{
val
pluginCmd
=
arrayListOf
(
pluginPath
)
...
...
@@ -200,12 +195,12 @@ 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
)
{
return
File
(
if
(
UserManagerCompat
.
isUserUnlocked
(
app
))
app
.
filesDir
else
@TargetApi
(
24
)
{
app
.
deviceStorage
.
noBackupFilesDir
// only API 24+ will be in locked state
},
CONFIG_FILE
)
shadowsocksConfigFile
=
file
file
.
writeText
(
config
.
toString
())
return
file
},
CONFIG_FILE
)
.
apply
{
shadowsocksConfigFile
=
this
writeText
(
config
.
toString
())
}
}
val
aclFile
:
File
?
get
()
{
...
...
mobile/src/main/java/com/github/shadowsocks/database/Profile.kt
View file @
912eac6c
...
...
@@ -26,13 +26,19 @@ import android.util.Base64
import
android.util.Log
import
androidx.core.net.toUri
import
com.github.shadowsocks.plugin.PluginConfiguration
import
com.github.shadowsocks.plugin.PluginOptions
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
org.json.JSONArray
import
org.json.JSONObject
import
org.json.JSONTokener
import
java.io.Serializable
import
java.net.URI
import
java.net.URISyntaxException
import
java.util.*
import
kotlin.collections.ArrayList
@Entity
class
Profile
:
Serializable
{
...
...
@@ -43,13 +49,14 @@ class Profile : Serializable {
private
val
userInfoPattern
=
"^(.+?):(.*)$"
.
toRegex
()
private
val
legacyPattern
=
"^(.+?):(.*)@(.+?):(\\d+?)$"
.
toRegex
()
fun
findAll
(
data
:
CharSequence
?
)
=
pattern
.
findAll
(
data
?:
""
).
map
{
fun
findAll
Urls
(
data
:
CharSequence
?,
feature
:
Profile
?
=
null
)
=
pattern
.
findAll
(
data
?:
""
).
map
{
val
uri
=
it
.
value
.
toUri
()
try
{
if
(
uri
.
userInfo
==
null
)
{
val
match
=
legacyPattern
.
matchEntire
(
String
(
Base64
.
decode
(
uri
.
host
,
Base64
.
NO_PADDING
)))
if
(
match
!=
null
)
{
val
profile
=
Profile
()
feature
?.
copyFeatureSettingsTo
(
profile
)
profile
.
method
=
match
.
groupValues
[
1
].
toLowerCase
()
profile
.
password
=
match
.
groupValues
[
2
]
profile
.
host
=
match
.
groupValues
[
3
]
...
...
@@ -66,6 +73,7 @@ class Profile : Serializable {
Base64
.
NO_PADDING
or
Base64
.
NO_WRAP
or
Base64
.
URL_SAFE
)))
if
(
match
!=
null
)
{
val
profile
=
Profile
()
feature
?.
copyFeatureSettingsTo
(
profile
)
profile
.
method
=
match
.
groupValues
[
1
]
profile
.
password
=
match
.
groupValues
[
2
]
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
...
...
@@ -92,6 +100,54 @@ class Profile : Serializable {
null
}
}.
filterNotNull
()
private
class
JsonParser
(
private
val
feature
:
Profile
?
=
null
)
:
ArrayList
<
Profile
>()
{
private
fun
tryAdd
(
json
:
JSONObject
)
{
val
host
=
json
.
optString
(
"server"
)
if
(
host
.
isNullOrEmpty
())
return
val
remotePort
=
json
.
optInt
(
"server_port"
)
if
(
remotePort
<=
0
)
return
val
password
=
json
.
optString
(
"password"
)
if
(
password
.
isNullOrEmpty
())
return
val
method
=
json
.
optString
(
"method"
)
if
(
method
.
isNullOrEmpty
())
return
add
(
Profile
().
also
{
it
.
host
=
host
it
.
remotePort
=
remotePort
it
.
password
=
password
it
.
method
=
method
}.
apply
{
feature
?.
copyFeatureSettingsTo
(
this
)
val
id
=
json
.
optString
(
"plugin"
)
if
(!
id
.
isNullOrEmpty
())
{
plugin
=
PluginOptions
(
id
,
json
.
optString
(
"plugin_opts"
)).
toString
(
false
)
}
name
=
json
.
optString
(
"remarks"
)
route
=
json
.
optString
(
"route"
,
route
)
remoteDns
=
json
.
optString
(
"remote_dns"
,
remoteDns
)
ipv6
=
json
.
optBoolean
(
"ipv6"
,
ipv6
)
json
.
optJSONObject
(
"proxy_apps"
)
?.
also
{
proxyApps
=
it
.
optBoolean
(
"enabled"
,
proxyApps
)
bypass
=
it
.
optBoolean
(
"bypass"
,
bypass
)
individual
=
it
.
optJSONArray
(
"android_list"
)
?.
asIterable
()
?.
joinToString
(
"\n"
)
?:
individual
}
udpdns
=
json
.
optBoolean
(
"udpdns"
,
udpdns
)
})
}
fun
process
(
json
:
Any
)
{
when
(
json
)
{
is
JSONObject
->
{
tryAdd
(
json
)
for
(
key
in
json
.
keys
())
process
(
json
.
get
(
key
))
}
is
JSONArray
->
json
.
asIterable
().
forEach
(
this
::
process
)
// ignore other types
}
}
}
fun
parseJson
(
json
:
String
,
feature
:
Profile
?
=
null
)
:
List
<
Profile
>
=
JsonParser
(
feature
).
apply
{
process
(
JSONTokener
(
json
).
nextValue
())
}
}
@
androidx
.
room
.
Dao
...
...
@@ -143,6 +199,15 @@ class Profile : Serializable {
val
formattedAddress
get
()
=
(
if
(
host
.
contains
(
":"
))
"[%s]:%d"
else
"%s:%d"
).
format
(
host
,
remotePort
)
val
formattedName
get
()
=
if
(
name
.
isNullOrEmpty
())
formattedAddress
else
name
!!
fun
copyFeatureSettingsTo
(
profile
:
Profile
)
{
profile
.
route
=
route
profile
.
ipv6
=
ipv6
profile
.
proxyApps
=
proxyApps
profile
.
bypass
=
bypass
profile
.
individual
=
individual
profile
.
udpdns
=
udpdns
}
fun
toUri
():
Uri
{
val
builder
=
Uri
.
Builder
()
.
scheme
(
"ss"
)
...
...
@@ -158,6 +223,33 @@ class Profile : Serializable {
}
override
fun
toString
()
=
toUri
().
toString
()
fun
toJson
(
compat
:
Boolean
=
false
)
=
JSONObject
().
apply
{
put
(
"server"
,
host
)
put
(
"server_port"
,
remotePort
)
put
(
"password"
,
password
)
put
(
"method"
,
method
)
if
(
compat
)
return
@apply
PluginConfiguration
(
plugin
?:
""
).
selectedOptions
.
apply
{
if
(
id
.
isNotEmpty
())
{
put
(
"plugin"
,
id
)
put
(
"plugin_opts"
,
toString
())
}
}
put
(
"remarks"
,
name
)
put
(
"route"
,
route
)
put
(
"remote_dns"
,
remoteDns
)
put
(
"ipv6"
,
ipv6
)
put
(
"proxy_apps"
,
JSONObject
().
apply
{
put
(
"enabled"
,
proxyApps
)
if
(
proxyApps
)
{
put
(
"bypass"
,
bypass
)
// android_ prefix is used because package names are Android specific
put
(
"android_list"
,
JSONArray
(
individual
.
split
(
"\n"
)))
}
})
put
(
"udpdns"
,
udpdns
)
}
fun
serialize
()
{
DataStore
.
privateStore
.
putString
(
Key
.
name
,
name
)
DataStore
.
privateStore
.
putString
(
Key
.
host
,
host
)
...
...
mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt
View file @
912eac6c
...
...
@@ -21,7 +21,6 @@
package
com.github.shadowsocks.database
import
android.database.sqlite.SQLiteCantOpenDatabaseException
import
com.github.shadowsocks.App.Companion.app
import
com.github.shadowsocks.ProfilesFragment
import
com.github.shadowsocks.preference.DataStore
import
com.github.shadowsocks.utils.DirectBoot
...
...
@@ -35,19 +34,8 @@ import java.sql.SQLException
*/
object
ProfileManager
{
@Throws
(
SQLException
::
class
)
fun
createProfile
(
p
:
Profile
?
=
null
):
Profile
{
val
profile
=
p
?:
Profile
()
fun
createProfile
(
profile
:
Profile
=
Profile
()):
Profile
{
profile
.
id
=
0
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
}
profile
.
userOrder
=
PrivateDatabase
.
profileDao
.
nextOrder
()
?:
0
profile
.
id
=
PrivateDatabase
.
profileDao
.
create
(
profile
)
ProfilesFragment
.
instance
?.
profilesAdapter
?.
add
(
profile
)
...
...
mobile/src/main/java/com/github/shadowsocks/utils/ArrayIterator.kt
0 → 100644
View file @
912eac6c
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.utils
import
android.content.ClipData
import
androidx.recyclerview.widget.SortedList
import
org.json.JSONArray
private
sealed
class
ArrayIterator
<
out
T
>
:
Iterator
<
T
>
{
abstract
val
size
:
Int
abstract
operator
fun
get
(
index
:
Int
):
T
private
var
count
=
0
override
fun
hasNext
()
=
count
<
size
override
fun
next
():
T
=
if
(
hasNext
())
this
[
count
++]
else
throw
NoSuchElementException
()
}
private
class
ClipDataIterator
(
private
val
data
:
ClipData
)
:
ArrayIterator
<
ClipData
.
Item
>()
{
override
val
size
get
()
=
data
.
itemCount
override
fun
get
(
index
:
Int
)
=
data
.
getItemAt
(
index
)
}
fun
ClipData
.
asIterable
()
=
Iterable
{
ClipDataIterator
(
this
)
}
private
class
JSONArrayIterator
(
private
val
arr
:
JSONArray
)
:
ArrayIterator
<
Any
>()
{
override
val
size
get
()
=
arr
.
length
()
override
fun
get
(
index
:
Int
)
=
arr
.
get
(
index
)
}
fun
JSONArray
.
asIterable
()
=
Iterable
{
JSONArrayIterator
(
this
)
}
private
class
SortedListIterator
<
out
T
>(
private
val
list
:
SortedList
<
T
>)
:
ArrayIterator
<
T
>()
{
override
val
size
get
()
=
list
.
size
()
override
fun
get
(
index
:
Int
)
=
list
[
index
]
}
fun
<
T
>
SortedList
<
T
>.
asIterable
()
=
Iterable
{
SortedListIterator
(
this
)
}
mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt
View file @
912eac6c
...
...
@@ -12,7 +12,6 @@ import android.net.Uri
import
android.os.Build
import
android.util.TypedValue
import
androidx.annotation.AttrRes
import
androidx.recyclerview.widget.SortedList
import
com.crashlytics.android.Crashlytics
import
com.github.shadowsocks.JniHelper
import
java.net.InetAddress
...
...
@@ -63,15 +62,7 @@ fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
return
typedValue
.
resourceId
}
private
class
SortedListIterable
<
out
T
>(
private
val
list
:
SortedList
<
T
>)
:
Iterable
<
T
>
{
override
fun
iterator
():
Iterator
<
T
>
=
SortedListIterator
(
list
)
}
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
=
if
(
hasNext
())
list
[
count
++]
else
throw
NoSuchElementException
()
}
fun
<
T
>
SortedList
<
T
>.
asIterable
():
Iterable
<
T
>
=
SortedListIterable
(
this
)
val
Intent
.
datas
get
()
=
listOfNotNull
(
data
)
+
(
clipData
?.
asIterable
()
?.
map
{
it
.
uri
}
?.
filterNotNull
()
?:
emptyList
())
fun
printLog
(
t
:
Throwable
)
{
Crashlytics
.
logException
(
t
)
...
...
mobile/src/main/res/drawable/ic_file_file_upload.xml
0 → 100644
View file @
912eac6c
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:width=
"24dp"
android:height=
"24dp"
android:viewportWidth=
"24.0"
android:viewportHeight=
"24.0"
>
<path
android:fillColor=
"#FFF"
android:pathData=
"M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"
/>
</vector>
mobile/src/main/res/menu/app_manager_menu.xml
View file @
912eac6c
...
...
@@ -7,14 +7,14 @@
android:numericShortcut=
"1"
app:showAsAction=
"never"
/>
<item
android:id=
"@+id/action_export"
android:id=
"@+id/action_export
_clipboard
"
android:alphabeticShortcut=
"e"
android:icon=
"?attr/actionModeCopyDrawable"
android:numericShortcut=
"2"
android:title=
"@string/action_export"
app:showAsAction=
"ifRoom"
/>
<item
android:id=
"@+id/action_import"
android:id=
"@+id/action_import
_clipboard
"
android:alphabeticShortcut=
"i"
android:icon=
"?attr/actionModePasteDrawable"
android:numericShortcut=
"3"
...
...
mobile/src/main/res/menu/custom_rules_menu.xml
View file @
912eac6c
...
...
@@ -11,7 +11,7 @@
android:title=
"@string/add_profile_methods_manual_settings"
android:alphabeticShortcut=
"m"
android:numericShortcut=
"1"
/>
<item
android:id=
"@+id/action_import"
<item
android:id=
"@+id/action_import
_clipboard
"
android:title=
"@string/action_import"
android:alphabeticShortcut=
"i"
android:numericShortcut=
"2"
/>
...
...
mobile/src/main/res/menu/profile_manager_menu.xml
View file @
912eac6c
...
...
@@ -14,22 +14,39 @@
android:numericShortcut=
"1"
android:title=
"@string/add_profile_methods_scan_qr_code"
/>
<item
android:id=
"@+id/action_import"
android:alphabeticShortcut=
"
i
"
android:id=
"@+id/action_import
_clipboard
"
android:alphabeticShortcut=
"
c
"
android:numericShortcut=
"2"
android:title=
"@string/action_import"
/>
<item
android:id=
"@+id/action_import_file"
android:alphabeticShortcut=
"f"
android:numericShortcut=
"3"
android:title=
"@string/action_import_file"
/>
<item
android:id=
"@+id/action_manual_settings"
android:alphabeticShortcut=
"m"
android:numericShortcut=
"
3
"
android:numericShortcut=
"
4
"
android:title=
"@string/add_profile_methods_manual_settings"
/>
</menu>
</item>
<item
android:id=
"@+id/action_export"
android:alphabeticShortcut=
"e"
android:icon=
"
?attr/actionModeCopyDrawable
"
android:icon=
"
@drawable/ic_file_file_upload
"
android:numericShortcut=
"1"
android:title=
"@string/action_export"
app:showAsAction=
"always"
/>
android:title=
"@string/action_export_more"
app:showAsAction=
"always"
>
<menu>
<item
android:id=
"@+id/action_export_clipboard"
android:alphabeticShortcut=
"c"
android:numericShortcut=
"1"
android:title=
"@string/action_export"
/>
<item
android:id=
"@+id/action_export_file"
android:alphabeticShortcut=
"f"
android:numericShortcut=
"2"
android:title=
"@string/action_export_file"
/>
</menu>
</item>
</menu>
mobile/src/main/res/menu/profile_share_popup.xml
View file @
912eac6c
...
...
@@ -4,7 +4,7 @@
android:title=
"@string/share_qr_nfc"
android:alphabeticShortcut=
"q"
android:numericShortcut=
"1"
/>
<item
android:id=
"@+id/action_export"
<item
android:id=
"@+id/action_export
_clipboard
"
android:title=
"@string/action_export"
android:alphabeticShortcut=
"e"
android:numericShortcut=
"2"
/>
...
...
mobile/src/main/res/menu/scanner_menu.xml
View file @
912eac6c
<?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"
<item
android:id=
"@+id/action_import
_clipboard
"
android:title=
"@string/action_import_file"
android:icon=
"@drawable/ic_image_photo"
android:alphabeticShortcut=
"p"
...
...
mobile/src/main/res/values/strings.xml
View file @
912eac6c
...
...
@@ -93,6 +93,8 @@
<string
name=
"share"
>
Share
</string>
<string
name=
"add_profile"
>
Add Profile
</string>
<string
name=
"action_apply_all"
>
Apply Settings to All Profiles
</string>
<string
name=
"action_export_more"
>
Export…
</string>
<string
name=
"action_export_file"
>
Export to file…
</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>
...
...
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