package {{PACKAGE_NAME}}
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import echo.Echo.EchoRequest
import echo.Echo.EchoResponse
import io.actor_rtc.actr.PayloadType
import io.actor_rtc.actr.dsl.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import {{PACKAGE_NAME}}.R
/**
* {{PROJECT_NAME_PASCAL}} Echo Client Main Activity
*
* This activity provides a simple UI to:
* 1. Connect to the Echo server via Actor-RTC
* 2. Send messages and receive echo responses
* 3. Display connection status and message logs
*/
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
private lateinit var statusText: TextView
private lateinit var connectButton: Button
private lateinit var disconnectButton: Button
private lateinit var messageInput: EditText
private lateinit var sendButton: Button
private lateinit var logText: TextView
private lateinit var scrollView: ScrollView
// Actor-RTC components
private var clientRef: ActrRef? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initViews()
setupClickListeners()
log("Ready to connect to Echo server")
log("Signaling URL: {{SIGNALING_URL}}")
}
private fun initViews() {
statusText = findViewById(R.id.statusText)
connectButton = findViewById(R.id.connectButton)
disconnectButton = findViewById(R.id.disconnectButton)
messageInput = findViewById(R.id.messageInput)
sendButton = findViewById(R.id.sendButton)
logText = findViewById(R.id.logText)
scrollView = findViewById(R.id.scrollView)
}
private fun setupClickListeners() {
connectButton.setOnClickListener {
connect()
}
disconnectButton.setOnClickListener {
disconnect()
}
sendButton.setOnClickListener {
sendMessage()
}
}
private fun copyAssetToInternalStorage(assetName: String): String {
val inputStream = assets.open(assetName)
val outputFile = File(filesDir, assetName)
outputFile.parentFile?.mkdirs()
inputStream.use { input ->
outputFile.outputStream().use { output -> input.copyTo(output) }
}
return outputFile.absolutePath
}
private fun connect() {
updateStatus("Connecting...")
connectButton.isEnabled = false
lifecycleScope.launch {
try {
// Copy config file from assets to internal storage
val configPath = copyAssetToInternalStorage("manifest.toml")
Log.i(TAG, "Config path: $configPath")
// Also copy lock file - required by ActrNode
copyAssetToInternalStorage("manifest.lock.toml")
// Create ActrNode
val clientSystem = createActrNode(configPath)
// Create and start UnifiedWorkload (generated by actr gen)
Log.i(TAG, "🚀 Starting EchoClient...")
val clientWorkload = UnifiedWorkload()
val clientNode = clientSystem.attach(clientWorkload)
clientRef = clientNode.start()
Log.i(TAG, "✅ Client started: ${clientRef?.actorId()?.serialNumber}")
// Wait for client to discover the server
delay(2000)
withContext(Dispatchers.Main) {
updateStatus("Connected")
disconnectButton.isEnabled = true
messageInput.isEnabled = true
sendButton.isEnabled = true
log("Connected to Echo server")
log("Client ID: ${clientRef?.actorId()?.serialNumber}")
}
} catch (e: Exception) {
Log.e(TAG, "Connection failed", e)
withContext(Dispatchers.Main) {
updateStatus("Connection failed")
connectButton.isEnabled = true
log("Error: ${e.message}")
}
}
}
}
private fun disconnect() {
updateStatus("Disconnecting...")
disconnectButton.isEnabled = false
messageInput.isEnabled = false
sendButton.isEnabled = false
lifecycleScope.launch {
try {
// Shutdown the client
clientRef?.shutdown()
clientRef?.awaitShutdown()
clientRef = null
withContext(Dispatchers.Main) {
updateStatus("Disconnected")
connectButton.isEnabled = true
log("Disconnected from Echo server")
}
} catch (e: Exception) {
Log.e(TAG, "Disconnect error", e)
withContext(Dispatchers.Main) {
updateStatus("Disconnected")
connectButton.isEnabled = true
clientRef = null
log("Disconnect error: ${e.message}")
}
}
}
}
private fun sendMessage() {
val message = messageInput.text.toString().trim()
if (message.isEmpty()) return
val ref = clientRef
if (ref == null) {
log("Error: Not connected")
return
}
messageInput.text.clear()
log("📤 Sending: $message")
lifecycleScope.launch {
try {
// Create EchoRequest using generated protobuf class
val request = EchoRequest.newBuilder()
.setMessage(message)
.build()
// Send RPC via ActrRef.call()
Log.i(TAG, "📞 Sending RPC via ActrRef.call()...")
val responsePayload = ref.call(
"echo.EchoService.Echo",
PayloadType.RPC_RELIABLE,
request.toByteArray(),
30000L
)
// Parse response using generated protobuf class
val response = EchoResponse.parseFrom(responsePayload)
Log.i(TAG, "📬 Response: ${response.reply}")
withContext(Dispatchers.Main) {
log("📥 Received: ${response.reply}")
}
} catch (e: Exception) {
Log.e(TAG, "Send error", e)
withContext(Dispatchers.Main) {
log("❌ Send error: ${e.message}")
}
}
}
}
private fun updateStatus(status: String) {
statusText.text = "Status: $status"
}
private fun log(message: String) {
val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault())
.format(java.util.Date())
val logEntry = "[$timestamp] $message\n"
logText.append(logEntry)
scrollView.post { scrollView.fullScroll(ScrollView.FOCUS_DOWN) }
}
override fun onDestroy() {
super.onDestroy()
// Clean up ActrRef
lifecycleScope.launch {
try {
clientRef?.shutdown()
clientRef?.awaitShutdown()
} catch (e: Exception) {
Log.w(TAG, "Error during cleanup: ${e.message}")
}
}
}
}