moltmobile-android/app/src/main/java/com/inou/moltmobile/ui/MainActivity.kt

282 lines
9.2 KiB
Kotlin

package com.inou.moltmobile.ui
import android.Manifest
import android.app.role.RoleManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.provider.Settings
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.inou.moltmobile.MoltMobileApp
import com.inou.moltmobile.databinding.ActivityMainBinding
import com.inou.moltmobile.service.NodeService
/**
* Main setup and status activity.
* - Configure Gateway connection
* - Grant required permissions
* - Show connection status
* - View audit log
*/
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var nodeService: NodeService? = null
private var serviceBound = false
private val logLines = mutableListOf<String>()
private val maxLogLines = 50
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
nodeService = (service as NodeService.LocalBinder).getService()
serviceBound = true
updateUI()
nodeService?.onConnectionChange = { connected ->
runOnUiThread { updateConnectionStatus(connected) }
}
nodeService?.onLogMessage = { message ->
appendLog(message)
}
}
override fun onServiceDisconnected(name: ComponentName?) {
nodeService = null
serviceBound = false
}
}
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
updatePermissionStatus()
}
private val callScreeningRoleLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
updatePermissionStatus()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
startAndBindService()
}
override fun onResume() {
super.onResume()
updateUI()
}
override fun onDestroy() {
super.onDestroy()
if (serviceBound) {
unbindService(serviceConnection)
}
}
private fun setupUI() {
// Gateway configuration
binding.btnSaveGateway.setOnClickListener {
saveGatewayConfig()
}
// Load existing config
val tokenStore = MoltMobileApp.instance.tokenStore
binding.etGatewayUrl.setText(tokenStore.gatewayUrl ?: "")
binding.etGatewayToken.setText(tokenStore.gatewayToken ?: "")
// Permission buttons
binding.btnGrantNotifications.setOnClickListener {
openNotificationListenerSettings()
}
binding.btnGrantCallScreening.setOnClickListener {
requestCallScreeningRole()
}
binding.btnGrantPermissions.setOnClickListener {
requestRuntimePermissions()
}
// Connection control
binding.btnConnect.setOnClickListener {
nodeService?.connect()
}
binding.btnDisconnect.setOnClickListener {
nodeService?.disconnect()
}
// Audit log
binding.btnViewAuditLog.setOnClickListener {
showAuditLog()
}
}
private fun startAndBindService() {
// Start foreground service
val intent = Intent(this, NodeService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
// Bind to service
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
private fun saveGatewayConfig() {
val url = binding.etGatewayUrl.text.toString().trim()
val token = binding.etGatewayToken.text.toString().trim()
if (url.isEmpty() || token.isEmpty()) {
Toast.makeText(this, "Please enter Gateway URL and token", Toast.LENGTH_SHORT).show()
return
}
val tokenStore = MoltMobileApp.instance.tokenStore
tokenStore.gatewayUrl = url
tokenStore.gatewayToken = token
MoltMobileApp.instance.auditLog.log(
"CONFIG_SAVED",
"Gateway configuration updated",
mapOf("url" to url)
)
Toast.makeText(this, "Configuration saved", Toast.LENGTH_SHORT).show()
// Reconnect with new config
nodeService?.disconnect()
nodeService?.connect()
}
private fun updateUI() {
updatePermissionStatus()
updateConnectionStatus(nodeService?.isConnected() ?: false)
}
private fun updateConnectionStatus(connected: Boolean) {
binding.tvConnectionStatus.text = if (connected) {
"✓ Connected to Gateway"
} else {
"✗ Disconnected"
}
binding.tvConnectionStatus.setTextColor(
ContextCompat.getColor(this,
if (connected) android.R.color.holo_green_dark
else android.R.color.holo_red_dark
)
)
}
private fun updatePermissionStatus() {
// Notification listener
val notificationEnabled = isNotificationListenerEnabled()
binding.tvNotificationStatus.text = if (notificationEnabled) "✓ Granted" else "✗ Not granted"
// Call screening
val callScreeningEnabled = isCallScreeningRoleHeld()
binding.tvCallScreeningStatus.text = if (callScreeningEnabled) "✓ Granted" else "✗ Not granted"
// Runtime permissions
val permissionsGranted = areRuntimePermissionsGranted()
binding.tvPermissionsStatus.text = if (permissionsGranted) "✓ All granted" else "✗ Some missing"
}
// ========================================
// PERMISSIONS
// ========================================
private fun isNotificationListenerEnabled(): Boolean {
val flat = Settings.Secure.getString(contentResolver, "enabled_notification_listeners")
return flat?.contains(packageName) == true
}
private fun openNotificationListenerSettings() {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
}
private fun isCallScreeningRoleHeld(): Boolean {
val roleManager = getSystemService(RoleManager::class.java)
return roleManager.isRoleHeld(RoleManager.ROLE_CALL_SCREENING)
}
private fun requestCallScreeningRole() {
val roleManager = getSystemService(RoleManager::class.java)
if (roleManager.isRoleAvailable(RoleManager.ROLE_CALL_SCREENING)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_CALL_SCREENING)
callScreeningRoleLauncher.launch(intent)
} else {
Toast.makeText(this, "Call screening not available", Toast.LENGTH_SHORT).show()
}
}
private fun areRuntimePermissionsGranted(): Boolean {
return REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
}
private fun requestRuntimePermissions() {
val missing = REQUIRED_PERMISSIONS.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (missing.isNotEmpty()) {
permissionLauncher.launch(missing.toTypedArray())
}
}
private fun showAuditLog() {
val entries = MoltMobileApp.instance.auditLog.getRecentEntries(50)
val text = entries.joinToString("\n\n") { entry ->
"${entry.timestamp}\n${entry.action}: ${entry.details}"
}
AlertDialog.Builder(this)
.setTitle("Audit Log (last 50)")
.setMessage(text.ifEmpty { "No entries yet" })
.setPositiveButton("OK", null)
.show()
}
fun appendLog(message: String) {
runOnUiThread {
val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.US).format(java.util.Date())
logLines.add("[$timestamp] $message")
while (logLines.size > maxLogLines) {
logLines.removeAt(0)
}
binding.tvLiveLog.text = logLines.joinToString("\n")
}
}
companion object {
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_CALL_LOG,
Manifest.permission.ANSWER_PHONE_CALLS,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.READ_CONTACTS,
Manifest.permission.POST_NOTIFICATIONS
)
}
}