Commit e33adc4f authored by Mygod's avatar Mygod

Seriously clean up Shadowsocks MainActivity

This activity contains lots of obsolete code and it's time to do a cleanup.
parent 418ae215
...@@ -75,11 +75,13 @@ ...@@ -75,11 +75,13 @@
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginTop="@dimen/fab_margin_top"> android:layout_marginTop="@dimen/fab_margin_top">
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" <android.support.design.widget.FloatingActionButton android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:backgroundTint="@color/material_primary_500" app:backgroundTint="@color/material_primary_500"
app:srcCompat="@drawable/ic_start_idle" app:srcCompat="@drawable/ic_start_idle"
app:pressedTranslationZ="6dp" app:borderWidth="0dp" /> app:pressedTranslationZ="6dp"
app:borderWidth="0dp"/>
</com.github.jorgecastilloprz.FABProgressCircle> </com.github.jorgecastilloprz.FABProgressCircle>
<android.support.design.widget.CoordinatorLayout <android.support.design.widget.CoordinatorLayout
android:id="@+id/snackbar" android:id="@+id/snackbar"
......
...@@ -43,7 +43,6 @@ import android.widget.{TextView, Toast} ...@@ -43,7 +43,6 @@ import android.widget.{TextView, Toast}
import com.github.jorgecastilloprz.FABProgressCircle import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
...@@ -65,46 +64,56 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -65,46 +64,56 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
with OnSharedPreferenceChangeListener { with OnSharedPreferenceChangeListener {
import MainActivity._ import MainActivity._
// Variables // UI
var serviceStarted: Boolean = _ private val handler = new Handler()
var fab: FloatingActionButton = _ private var fab: FloatingActionButton = _
var fabProgressCircle: FABProgressCircle = _ private var fabProgressCircle: FABProgressCircle = _
var state = State.STOPPED
var currentProfile = new Profile
var drawer: Drawer = _ var drawer: Drawer = _
private var progressDialog: ProgressDialog = _ private var testCount: Int = _
private var statusText: TextView = _
private var txText: TextView = _
private var rxText: TextView = _
private var txRateText: TextView = _
private var rxRateText: TextView = _
private var currentFragment: ToolbarFragment = _ private var currentFragment: ToolbarFragment = _
private lazy val profilesFragment = new ProfilesFragment() private lazy val profilesFragment = new ProfilesFragment()
private lazy val globalSettingsFragment = new GlobalSettingsFragment() private lazy val globalSettingsFragment = new GlobalSettingsFragment()
private lazy val aboutFragment = new AboutFragment() private lazy val aboutFragment = new AboutFragment()
// Services // Services
var state: Int = _
private val callback = new IShadowsocksServiceCallback.Stub { private val callback = new IShadowsocksServiceCallback.Stub {
def stateChanged(s: Int, profileName: String, m: String) { def stateChanged(s: Int, profileName: String, m: String): Unit = handler.post(() => changeState(s, profileName, m))
handler.post(() => { def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
handler.post(() => updateTraffic(txRate, rxRate, txTotal, rxTotal))
}
private lazy val greyTint = ContextCompat.getColorStateList(MainActivity.this, R.color.material_primary_500)
private lazy val greenTint = ContextCompat.getColorStateList(MainActivity.this, R.color.material_green_700)
private def hideCircle() = try fabProgressCircle.hide() catch {
case _: NullPointerException =>
}
private def changeState(s: Int, profileName: String = null, m: String = null) {
// TODO: localize texts for statusText in this method
s match { s match {
case State.CONNECTING => case State.CONNECTING =>
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_start_busy) fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false)
fabProgressCircle.show() fabProgressCircle.show()
statusText.setText("Connecting...") statusText.setText("Connecting...")
case State.CONNECTED => case State.CONNECTED =>
fab.setBackgroundTintList(greenTint) if (state == State.CONNECTING) fabProgressCircle.beginFinalAnimation()
if (state == State.CONNECTING) { else fabProgressCircle.postDelayed(hideCircle, 1000)
fabProgressCircle.beginFinalAnimation() fab.setImageResource(R.drawable.ic_start_connected)
} else {
fabProgressCircle.postDelayed(hideCircle, 1000)
}
fab.setEnabled(true)
changeSwitch(checked = true)
statusText.setText(if (app.isNatEnabled) "Connected" else "Connected, tap to check connection") statusText.setText(if (app.isNatEnabled) "Connected" else "Connected, tap to check connection")
case State.STOPPED => case State.STOPPING =>
fab.setBackgroundTintList(greyTint) fab.setImageResource(R.drawable.ic_start_busy)
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
statusText.setText("Shutting down...")
case _ =>
fab.setImageResource(R.drawable.ic_start_idle)
fabProgressCircle.postDelayed(hideCircle, 1000) fabProgressCircle.postDelayed(hideCircle, 1000)
fab.setEnabled(true)
changeSwitch(checked = false)
if (m != null) { if (m != null) {
val snackbar = Snackbar.make(findViewById(R.id.snackbar), val snackbar = Snackbar.make(findViewById(R.id.snackbar),
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG) getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG)
...@@ -113,21 +122,16 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -113,21 +122,16 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
Log.e(TAG, "Error to start VPN service: " + m) Log.e(TAG, "Error to start VPN service: " + m)
} }
statusText.setText("Not connected") statusText.setText("Not connected")
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false)
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
statusText.setText("Shutting down...")
} }
state = s state = s
}) if (state == State.CONNECTED) fab.setBackgroundTintList(greenTint) else {
} fab.setBackgroundTintList(greyTint)
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { updateTraffic(0, 0, 0, 0)
handler.post(() => updateTraffic(txRate, rxRate, txTotal, rxTotal))
} }
fab.setEnabled(false)
if (state == State.CONNECTED || state == State.STOPPED)
handler.postDelayed(() => fab.setEnabled(state == State.CONNECTED || state == State.STOPPED), 1000)
} }
def updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { def updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.setText(TrafficMonitor.formatTraffic(txTotal)) txText.setText(TrafficMonitor.formatTraffic(txTotal))
rxText.setText(TrafficMonitor.formatTraffic(rxTotal)) rxText.setText(TrafficMonitor.formatTraffic(rxTotal))
...@@ -137,67 +141,28 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -137,67 +141,28 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
if (child != null) child.onTrafficUpdated(txRate, rxRate, txTotal, rxTotal) if (child != null) child.onTrafficUpdated(txRate, rxRate, txTotal, rxTotal)
} }
def attachServiceCallback(): Unit = attachService(callback)
override def onServiceConnected() { override def onServiceConnected() {
// Update the UI changeState(bgService.getState)
if (fab != null) fab.setEnabled(true)
Log.d(TAG, "bgService " + bgService.getState)
callback.stateChanged(bgService.getState, null, null)
state = bgService.getState
if (Build.VERSION.SDK_INT >= 21 && app.isNatEnabled) { if (Build.VERSION.SDK_INT >= 21 && app.isNatEnabled) {
val snackbar = Snackbar.make(findViewById(R.id.snackbar), R.string.nat_deprecated, Snackbar.LENGTH_LONG) val snackbar = Snackbar.make(findViewById(R.id.snackbar), R.string.nat_deprecated, Snackbar.LENGTH_LONG)
addDisableNatToSnackbar(snackbar) addDisableNatToSnackbar(snackbar)
snackbar.show() snackbar.show()
} }
} }
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ => private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false)): View.OnClickListener) if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false)): View.OnClickListener)
override def onServiceDisconnected() {
if (fab != null) fab.setEnabled(false)
}
override def binderDied() { override def binderDied() {
detachService() detachService()
app.crashRecovery() app.crashRecovery()
attachServiceCallback() attachService(callback)
}
private var testCount: Int = _
private var statusText: TextView = _ // TODO: localize texts for statusText in this file
private var txText: TextView = _
private var rxText: TextView = _
private var txRateText: TextView = _
private var rxRateText: TextView = _
private lazy val greyTint = ContextCompat.getColorStateList(this, R.color.material_primary_500)
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
val handler = new Handler()
private val connectedListener: BroadcastReceiver = (_, _) =>
statusText.setText(if (app.isNatEnabled) "Connected" else "Connected, tap to check connection")
private def changeSwitch(checked: Boolean) {
serviceStarted = checked
fab.setImageResource(if (checked) R.drawable.ic_start_connected else R.drawable.ic_start_idle)
if (fab.isEnabled) {
fab.setEnabled(false)
handler.postDelayed(() => fab.setEnabled(true), 1000)
}
} }
def prepareStartService() { override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
Utils.ThrowableFuture { case Activity.RESULT_OK => bgService.use(app.profileId)
if (app.isNatEnabled) serviceLoad() else { case _ => Log.e(TAG, "Failed to start VpnService")
val intent = VpnService.prepare(this)
if (intent != null) {
startActivityForResult(intent, REQUEST_CONNECT)
} else {
handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
}
}
}
} }
override def onCreate(savedInstanceState: Bundle) { override def onCreate(savedInstanceState: Bundle) {
...@@ -276,7 +241,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -276,7 +241,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
success = false success = false
result = getString(R.string.connection_test_error, e.getMessage) result = getString(R.string.connection_test_error, e.getMessage)
} }
synchronized(if (serviceStarted && testCount == id && app.isVpnEnabled) handler.post(() => synchronized(if (state == State.CONNECTED && testCount == id && app.isVpnEnabled) handler.post(() =>
if (success) statusText.setText(result) else { if (success) statusText.setText(result) else {
statusText.setText(R.string.connection_test_fail) statusText.setText(R.string.connection_test_fail)
Snackbar.make(findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show() Snackbar.make(findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show()
...@@ -288,19 +253,22 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -288,19 +253,22 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton] fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle] fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener(_ => if (serviceStarted) serviceStop() fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture {
else if (bgService != null) prepareStartService() if (app.isNatEnabled) bgService.use(app.profileId) else {
else changeSwitch(checked = false)) val intent = VpnService.prepare(this)
fab.setOnLongClickListener((v: View) => { if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
Utils.positionToast(Toast.makeText(this, if (serviceStarted) R.string.stop else R.string.connect, else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
}
})
fab.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(this, if (state == State.CONNECTED) R.string.stop else R.string.connect,
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show() Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show()
true true
}) })
updateTraffic(0, 0, 0, 0)
handler.post(attachServiceCallback) changeState(State.IDLE) // reset everything to init state
handler.post(() => attachService(callback))
app.settings.registerOnSharedPreferenceChangeListener(this) app.settings.registerOnSharedPreferenceChangeListener(this)
registerReceiver(connectedListener, new IntentFilter(Action.CONNECTED))
val intent = getIntent val intent = getIntent
if (intent != null) handleShareIntent(intent) if (intent != null) handleShareIntent(intent)
...@@ -342,7 +310,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -342,7 +310,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match { def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT => handler.post(() => { case Key.isNAT => handler.post(() => {
detachService() detachService()
attachServiceCallback() attachService(callback)
}) })
case _ => case _ =>
} }
...@@ -358,8 +326,11 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -358,8 +326,11 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
case DRAWER_PROFILES => displayFragment(profilesFragment) case DRAWER_PROFILES => displayFragment(profilesFragment)
case DRAWER_RECOVERY => case DRAWER_RECOVERY =>
app.track("GlobalConfigFragment", "reset") app.track("GlobalConfigFragment", "reset")
serviceStop() if (bgService != null) bgService.use(-1)
val handler = showProgress(R.string.recovering) val dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false)
val handler = new Handler {
override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss()
}
Utils.ThrowableFuture { Utils.ThrowableFuture {
app.copyAssets() app.copyAssets()
handler.sendEmptyMessage(0) handler.sendEmptyMessage(0)
...@@ -372,29 +343,6 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -372,29 +343,6 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
true // unexpected cases will throw exception true // unexpected cases will throw exception
} }
private def showProgress(msg: Int): Handler = {
clearDialog()
progressDialog = ProgressDialog.show(this, "", getString(msg), true, false)
new Handler {
override def handleMessage(msg: Message): Unit = clearDialog()
}
}
private def clearDialog() {
if (progressDialog != null && progressDialog.isShowing) {
if (!isDestroyed) progressDialog.dismiss()
progressDialog = null
}
}
private def hideCircle() {
try {
fabProgressCircle.hide()
} catch {
case _: java.lang.NullPointerException => // Ignore
}
}
protected override def onResume() { protected override def onResume() {
super.onResume() super.onResume()
app.refreshContainerHolder() app.refreshContainerHolder()
...@@ -418,31 +366,9 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe ...@@ -418,31 +366,9 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onDestroy() { override def onDestroy() {
super.onDestroy() super.onDestroy()
unregisterReceiver(connectedListener)
app.settings.unregisterOnSharedPreferenceChangeListener(this) app.settings.unregisterOnSharedPreferenceChangeListener(this)
detachService() detachService()
new BackupManager(this).dataChanged() new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
} }
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK =>
serviceLoad()
case _ =>
changeSwitch(checked = false)
Log.e(TAG, "Failed to start VpnService")
}
def serviceStop() {
if (bgService != null) bgService.use(-1)
}
/** Called when connect button is clicked. */
def serviceLoad() {
bgService.use(app.profileId)
if (app.isVpnEnabled) {
changeSwitch(checked = false)
}
}
} }
...@@ -138,7 +138,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic ...@@ -138,7 +138,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
app.switchProfile(item.id) app.switchProfile(item.id)
profilesAdapter.refreshId(old) profilesAdapter.refreshId(old)
bind(item) bind(item)
if (state == State.CONNECTED) activity.serviceLoad() if (state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile
} }
} }
......
...@@ -30,7 +30,7 @@ import com.github.shadowsocks.ShadowsocksApplication.app ...@@ -30,7 +30,7 @@ import com.github.shadowsocks.ShadowsocksApplication.app
* @author Mygod * @author Mygod
*/ */
trait ServiceBoundContext extends Context with IBinder.DeathRecipient { trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
class ShadowsocksServiceConnection extends ServiceConnection { private class ShadowsocksServiceConnection extends ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) { override def onServiceConnected(name: ComponentName, service: IBinder) {
binder = service binder = service
service.linkToDeath(ServiceBoundContext.this, 0) service.linkToDeath(ServiceBoundContext.this, 0)
...@@ -52,7 +52,7 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient { ...@@ -52,7 +52,7 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
} }
} }
def setListeningForBandwidth(value: Boolean) { protected def setListeningForBandwidth(value: Boolean) {
if (listeningForBandwidth != value && bgService != null && callback != null) if (listeningForBandwidth != value && bgService != null && callback != null)
if (value) bgService.startListeningForBandwidth(callback) else bgService.stopListeningForBandwidth(callback) if (value) bgService.startListeningForBandwidth(callback) else bgService.stopListeningForBandwidth(callback)
listeningForBandwidth = value listeningForBandwidth = value
...@@ -66,8 +66,11 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient { ...@@ -66,8 +66,11 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
callbackRegistered = false callbackRegistered = false
} }
def onServiceConnected(): Unit = () protected def onServiceConnected(): Unit = ()
def onServiceDisconnected(): Unit = () /**
* Different from Android framework, this method will be called even when you call `detachService`.
*/
protected def onServiceDisconnected(): Unit = ()
override def binderDied(): Unit = () override def binderDied(): Unit = ()
private var callback: IShadowsocksServiceCallback.Stub = _ private var callback: IShadowsocksServiceCallback.Stub = _
...@@ -76,10 +79,10 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient { ...@@ -76,10 +79,10 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
private var listeningForBandwidth: Boolean = _ private var listeningForBandwidth: Boolean = _
// Variables // Variables
var binder: IBinder = _ private var binder: IBinder = _
var bgService: IShadowsocksService = _ var bgService: IShadowsocksService = _
def attachService(callback: IShadowsocksServiceCallback.Stub = null) { protected def attachService(callback: IShadowsocksServiceCallback.Stub = null) {
this.callback = callback this.callback = callback
if (bgService == null) { if (bgService == null) {
val s = if (app.isNatEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService] val s = if (app.isNatEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
...@@ -92,8 +95,9 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient { ...@@ -92,8 +95,9 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
} }
} }
def detachService() { protected def detachService() {
unregisterCallback() unregisterCallback()
onServiceDisconnected()
callback = null callback = null
if (connection != null) { if (connection != null) {
try unbindService(connection) catch { try unbindService(connection) catch {
......
...@@ -162,17 +162,20 @@ object Key { ...@@ -162,17 +162,20 @@ object Key {
} }
object State { object State {
/**
* This state will never be broadcast by the service. This state is only used to indicate that the current context
* hasn't bound to any context.
*/
val IDLE = 0
val CONNECTING = 1 val CONNECTING = 1
val CONNECTED = 2 val CONNECTED = 2
val STOPPING = 3 val STOPPING = 3
val STOPPED = 4 val STOPPED = 4
def isAvailable(state: Int): Boolean = state != CONNECTED && state != CONNECTING
} }
object Action { object Action {
final val SERVICE = "com.github.shadowsocks.SERVICE" final val SERVICE = "com.github.shadowsocks.SERVICE"
final val CLOSE = "com.github.shadowsocks.CLOSE" final val CLOSE = "com.github.shadowsocks.CLOSE"
final val CONNECTED = "com.github.shadowsocks.CONNECTED" // TODO
final val PROFILE_CHANGED = "com.github.shadowsocks.PROFILE_CHANGED" final val PROFILE_CHANGED = "com.github.shadowsocks.PROFILE_CHANGED"
final val PROFILE_REMOVED = "com.github.shadowsocks.PROFILE_REMOVED" final val PROFILE_REMOVED = "com.github.shadowsocks.PROFILE_REMOVED"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment