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() 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 ) } }