package pro.nnnteam.nnnet import android.os.Build import android.os.Bundle import android.widget.EditText import android.widget.ImageButton import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButton import com.google.android.material.switchmaterial.SwitchMaterial import kotlinx.coroutines.launch import pro.nnnteam.nnnet.data.MeshDatabase import pro.nnnteam.nnnet.data.MeshRepository import pro.nnnteam.nnnet.data.ProfileEntity import pro.nnnteam.nnnet.mesh.MeshServiceContract import pro.nnnteam.nnnet.update.UpdateInfo import pro.nnnteam.nnnet.update.UpdateManager import android.content.BroadcastReceiver import android.content.Intent import android.content.IntentFilter class SettingsActivity : AppCompatActivity() { private lateinit var repository: MeshRepository private lateinit var firstNameInput: EditText private lateinit var lastNameInput: EditText private lateinit var usernameInput: EditText private lateinit var descriptionInput: EditText private lateinit var searchInput: EditText private lateinit var profileResultCard: android.view.View private lateinit var resultNameText: TextView private lateinit var resultUsernameText: TextView private lateinit var resultDescriptionText: TextView private lateinit var resultPeerIdText: TextView private var receiverRegistered = false private val prefs by lazy { getSharedPreferences(UpdateManager.PREFS_NAME, MODE_PRIVATE) } private val meshEventReceiver = object : BroadcastReceiver() { override fun onReceive(context: android.content.Context?, intent: Intent?) { if (intent?.action != MeshServiceContract.ACTION_EVENT) return val eventType = intent.getStringExtra(MeshServiceContract.EXTRA_EVENT_TYPE) ?: return if (eventType == MeshServiceContract.EVENT_PROFILES_CHANGED) { val query = searchInput.text.toString().trim() if (query.isNotEmpty()) { lookupProfile(query) } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) val database = MeshDatabase.getInstance(applicationContext) repository = MeshRepository( database.messageDao(), database.outboundQueueDao(), database.profileDao(), database.packetTraceDao() ) findViewById(R.id.backButton).setOnClickListener { finish() } firstNameInput = findViewById(R.id.firstNameInput) lastNameInput = findViewById(R.id.lastNameInput) usernameInput = findViewById(R.id.usernameInput) descriptionInput = findViewById(R.id.descriptionInput) searchInput = findViewById(R.id.searchInput) profileResultCard = findViewById(R.id.profileResultCard) resultNameText = findViewById(R.id.resultNameText) resultUsernameText = findViewById(R.id.resultUsernameText) resultDescriptionText = findViewById(R.id.resultDescriptionText) resultPeerIdText = findViewById(R.id.resultPeerIdText) val autoUpdateSwitch = findViewById(R.id.autoUpdateSwitch) val versionText = findViewById(R.id.versionText) autoUpdateSwitch.isChecked = prefs.getBoolean(UpdateManager.KEY_AUTO_UPDATE, false) autoUpdateSwitch.setOnCheckedChangeListener { _, isChecked -> prefs.edit().putBoolean(UpdateManager.KEY_AUTO_UPDATE, isChecked).apply() } versionText.text = getString( R.string.current_version, packageManager.getPackageInfo(packageName, 0).versionName, currentVersionCode() ) findViewById(R.id.saveProfileButton).setOnClickListener { saveProfile() } findViewById(R.id.searchButton).setOnClickListener { val query = searchInput.text.toString().trim() if (query.isEmpty()) { Toast.makeText(this, R.string.enter_username_to_search, Toast.LENGTH_SHORT).show() } else { lookupProfile(query) } } findViewById(R.id.openMapButton).setOnClickListener { startActivity(Intent(this, PacketMapActivity::class.java)) } findViewById(R.id.openPacketLogButton).setOnClickListener { startActivity(Intent(this, PacketLogActivity::class.java)) } findViewById(R.id.checkUpdatesButton).setOnClickListener { checkForUpdates() } loadLocalProfile() } override fun onStart() { super.onStart() registerMeshReceiver() } override fun onStop() { if (receiverRegistered) { unregisterReceiver(meshEventReceiver) receiverRegistered = false } super.onStop() } private fun registerMeshReceiver() { if (receiverRegistered) return val filter = IntentFilter(MeshServiceContract.ACTION_EVENT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(meshEventReceiver, filter, RECEIVER_NOT_EXPORTED) } else { @Suppress("DEPRECATION") registerReceiver(meshEventReceiver, filter) } receiverRegistered = true } private fun loadLocalProfile() { lifecycleScope.launch { val localProfile = repository.localProfile() if (localProfile != null) { firstNameInput.setText(localProfile.firstName) lastNameInput.setText(localProfile.lastName) usernameInput.setText(localProfile.username) descriptionInput.setText(localProfile.description) } } } private fun saveProfile() { val firstName = firstNameInput.text.toString().trim() val lastName = lastNameInput.text.toString().trim() val username = usernameInput.text.toString().trim().removePrefix("@") val description = descriptionInput.text.toString().trim() if (username.isBlank()) { Toast.makeText(this, R.string.username_required, Toast.LENGTH_SHORT).show() return } lifecycleScope.launch { repository.saveLocalProfile( firstName = firstName, lastName = lastName, username = username, description = description ) Toast.makeText(this@SettingsActivity, R.string.profile_saved, Toast.LENGTH_SHORT).show() } } private fun lookupProfile(username: String) { lifecycleScope.launch { val profile = repository.profileByUsername(username.removePrefix("@")) renderSearchResult(profile) if (profile == null) { Toast.makeText(this@SettingsActivity, R.string.profile_not_found_locally, Toast.LENGTH_SHORT).show() } } } private fun renderSearchResult(profile: ProfileEntity?) { if (profile == null) { profileResultCard.visibility = android.view.View.GONE return } profileResultCard.visibility = android.view.View.VISIBLE resultNameText.text = profile.displayName() resultUsernameText.text = "@${profile.username}" resultDescriptionText.text = profile.description.ifBlank { getString(R.string.no_profile_description) } resultPeerIdText.text = if (profile.peerId.isBlank()) { getString(R.string.peer_id_unknown) } else { getString(R.string.peer_id_value, profile.peerId) } } private fun checkForUpdates() { lifecycleScope.launch { val updateInfo = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { UpdateManager.fetchUpdateInfo() } if (updateInfo == null) { Toast.makeText(this@SettingsActivity, R.string.update_check_failed, Toast.LENGTH_SHORT).show() return@launch } if (updateInfo.versionCode > currentVersionCode()) { showUpdateDialog(updateInfo) } else { Toast.makeText(this@SettingsActivity, R.string.latest_version_installed, Toast.LENGTH_SHORT).show() } } } private fun showUpdateDialog(updateInfo: UpdateInfo) { lifecycleScope.launch { val releaseNotes = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { UpdateManager.fetchReleaseNotes(updateInfo.releaseNotesPath) } androidx.appcompat.app.AlertDialog.Builder(this@SettingsActivity) .setTitle(updateInfo.releaseNotesTitle) .setMessage( buildString { append(getString(R.string.update_available_message, updateInfo.versionName)) if (!releaseNotes.isNullOrBlank()) { append("\n\n") append(releaseNotes.trim()) } } ) .setPositiveButton(R.string.download_update) { _, _ -> val url = UpdateManager.buildDownloadUrl(updateInfo.apkPath) startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse(url))) } .setNegativeButton(R.string.later, null) .show() } } private fun currentVersionCode(): Int { val packageInfo = packageManager.getPackageInfo(packageName, 0) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { packageInfo.longVersionCode.toInt() } else { @Suppress("DEPRECATION") packageInfo.versionCode } } }