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
f0141317
Unverified
Commit
f0141317
authored
Feb 21, 2019
by
Max Lv
Committed by
GitHub
Feb 21, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2123 from shadowsocks/plugin-1.2.0
Plugin library 1.2.0
parents
1c1a8ed9
f4621f3f
Changes
10
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
257 additions
and
124 deletions
+257
-124
core/build.gradle
core/build.gradle
+5
-0
core/src/main/java/com/github/shadowsocks/database/Profile.kt
.../src/main/java/com/github/shadowsocks/database/Profile.kt
+27
-24
mobile/build.gradle
mobile/build.gradle
+5
-0
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
+18
-11
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
+34
-22
mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
...in/java/com/github/shadowsocks/acl/CustomRulesFragment.kt
+74
-58
plugin/CHANGES.md
plugin/CHANGES.md
+3
-0
plugin/build.gradle
plugin/build.gradle
+7
-2
plugin/src/main/java/com/github/shadowsocks/plugin/AlertDialogFragment.kt
...java/com/github/shadowsocks/plugin/AlertDialogFragment.kt
+70
-0
No files found.
core/build.gradle
View file @
f0141317
apply
plugin:
'com.android.library'
apply
plugin:
'kotlin-android'
apply
plugin:
'kotlin-android-extensions'
apply
plugin:
'kotlin-kapt'
android
{
...
...
@@ -38,6 +39,10 @@ android {
}
}
androidExtensions
{
experimental
=
true
}
def
lifecycleVersion
=
'2.0.0'
def
roomVersion
=
'2.0.0'
def
workVersion
=
'1.0.0-rc01'
...
...
core/src/main/java/com/github/shadowsocks/database/Profile.kt
View file @
f0141317
...
...
@@ -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
!!
...
...
mobile/build.gradle
View file @
f0141317
...
...
@@ -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/MainActivity.kt
View file @
f0141317
...
...
@@ -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
{
...
...
@@ -68,6 +73,17 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
var
stateListener
:
((
Int
)
->
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
private
lateinit
var
fab
:
ServiceButton
private
lateinit
var
stats
:
StatsBar
...
...
@@ -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 @
f0141317
...
...
@@ -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 @
f0141317
...
...
@@ -22,8 +22,10 @@ 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.core.os.bundleOf
...
...
@@ -33,10 +35,7 @@ import androidx.preference.SwitchPreference
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.DataStore
import
com.github.shadowsocks.preference.IconListPreference
import
com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
...
...
@@ -47,11 +46,26 @@ import com.github.shadowsocks.utils.Key
import
com.google.android.material.snackbar.Snackbar
import
com.takisoft.preferencex.EditTextPreference
import
com.takisoft.preferencex.PreferenceFragmentCompat
import
kotlinx.android.parcel.Parcelize
class
ProfileConfigFragment
:
PreferenceFragmentCompat
(),
Preference
.
OnPreferenceChangeListener
,
OnPreferenceDataStoreChangeListener
{
companion
object
{
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
...
...
@@ -116,7 +130,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
bundleOf
(
Pair
(
"key"
,
Key
.
pluginConfigure
),
Pair
(
PluginConfigurationDialogFragment
.
PLUGIN_ID_FRAGMENT_TAG
,
pluginConfiguration
.
selected
)))
fun
saveAndExit
()
{
private
fun
saveAndExit
()
{
val
profile
=
ProfileManager
.
getProfile
(
profileId
)
?:
Profile
()
profile
.
id
=
profileId
profile
.
deserialize
()
...
...
@@ -161,28 +175,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/acl/CustomRulesFragment.kt
View file @
f0141317
This diff is collapsed.
Click to expand it.
plugin/CHANGES.md
View file @
f0141317
*
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 @
f0141317
apply
plugin:
'com.android.library'
apply
plugin:
'kotlin-android'
apply
plugin:
'kotlin-android-extensions'
apply
from:
'gradle-mvn-push.gradle'
...
...
@@ -10,8 +11,8 @@ android {
defaultConfig
{
minSdkVersion
rootProject
.
minSdkVersion
targetSdkVersion
rootProject
.
sdkVersion
versionCode
8
versionName
"1.
1
.0"
versionCode
9
versionName
"1.
2
.0"
testInstrumentationRunner
"androidx.test.runner.AndroidJUnitRunner"
}
...
...
@@ -23,6 +24,10 @@ android {
}
}
androidExtensions
{
experimental
=
true
}
dependencies
{
api
"androidx.core:core-ktx:1.0.1"
api
"com.google.android.material:material:1.1.0-alpha03"
...
...
plugin/src/main/java/com/github/shadowsocks/plugin/AlertDialogFragment.kt
0 → 100644
View file @
f0141317
/*******************************************************************************
* *
* 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
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