282 lines
9.2 KiB
Kotlin
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
|
|
)
|
|
}
|
|
}
|