From 9838daea2f262bd7358e3429a01eaf8bdfd4eabb Mon Sep 17 00:00:00 2001 From: ua1zbe Date: Mon, 17 Aug 2026 10:39:15 +0300 Subject: [PATCH] Add multiple connection profiles (v1.0.6) --- .../java/ru/ua1zbe/svxremote/MainActivity.kt | 78 +++++++++++ .../ru/ua1zbe/svxremote/SettingsActivity.kt | 52 +++++-- .../java/ru/ua1zbe/svxremote/prefs/Prefs.kt | 130 ++++++++++++++---- .../ua1zbe/svxremote/proto/ReflectorClient.kt | 7 +- app/src/main/res/layout/activity_main.xml | 1 + app/src/main/res/layout/activity_settings.xml | 17 +++ app/src/main/res/values/strings.xml | 6 + 7 files changed, 251 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt index 6aabb88..2c374af 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt @@ -20,6 +20,7 @@ import android.os.Looper import android.provider.Settings import android.util.Log import android.view.Gravity +import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.widget.ImageButton @@ -44,6 +45,7 @@ class MainActivity : Activity(), SvxService.Listener { } private lateinit var statusChip: TextView + private lateinit var lblAppTitle: TextView private lateinit var lblSubtitle: TextView private lateinit var lblCallsign: TextView private lateinit var lblTg: TextView @@ -62,9 +64,11 @@ class MainActivity : Activity(), SvxService.Listener { private lateinit var btnMonitor: ImageButton private val mainHandler = Handler(Looper.getMainLooper()) + private var titleHoldRunnable: Runnable? = null private var connected = false private var monitoring = false + private var connectedSettings: ConnectionSettings? = null private var priorityActive = false private var settings: ConnectionSettings = ConnectionSettings("", 5300, "", "", 11) @@ -91,6 +95,7 @@ class MainActivity : Activity(), SvxService.Listener { checkForUpdates() statusChip = findViewById(R.id.status_chip) + lblAppTitle = findViewById(R.id.lbl_app_title) lblSubtitle = findViewById(R.id.lbl_subtitle) lblTalkerTitle = findViewById(R.id.lbl_talker_title) lblTalker = findViewById(R.id.lbl_talker) @@ -131,6 +136,24 @@ class MainActivity : Activity(), SvxService.Listener { lblCallsign.setOnClickListener { showNodesDialog() } + // Удержание на названии программы (> 2 с) — выбор профиля настроек + lblAppTitle.setOnTouchListener { _, event -> + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + val r = Runnable { showProfileDialog() } + titleHoldRunnable = r + mainHandler.postDelayed(r, 2000) + true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + titleHoldRunnable?.let { mainHandler.removeCallbacks(it) } + titleHoldRunnable = null + true + } + else -> true + } + } + connectZone.setOnClickListener { if (connected) { svc?.disconnect() @@ -176,7 +199,22 @@ class MainActivity : Activity(), SvxService.Listener { override fun onResume() { super.onResume() + val prev = settings settings = Prefs.load(this) + if (prev != settings && connectedSettings != null) { + if (settings.host.isBlank() || settings.callsign.isBlank()) { + monitoring = false + priorityActive = false + updateMonitorButton() + updatePttEnabled() + svc?.disconnect() + } else if (connected) { + svc?.disconnect() + mainHandler.postDelayed({ connect() }, 500) + } else { + connect() + } + } refreshUi() if (!autoConnectAttempted && settings.host.isNotBlank() && @@ -414,6 +452,7 @@ class MainActivity : Activity(), SvxService.Listener { /** Запустить сервис и подключиться с текущими настройками. */ private fun connect() { + connectedSettings = settings startForegroundService(Intent(this, SvxService::class.java)) bindService(Intent(this, SvxService::class.java), svcConn, Context.BIND_AUTO_CREATE) mainHandler.postDelayed({ @@ -459,6 +498,45 @@ class MainActivity : Activity(), SvxService.Listener { } } + /** Список профилей: выбор сразу подключается с новыми настройками. */ + private fun showProfileDialog() { + val profiles = Prefs.loadProfiles(this) + val active = Prefs.activeProfileIndex(this) + val items = profiles.mapIndexed { i, p -> + val s = p.settings + val detail = if (s.callsign.isBlank()) { + getString(R.string.profile_empty) + } else { + "${s.callsign} @ ${s.host}:${s.port}" + } + val marker = if (i == active) getString(R.string.profile_current) else "" + "${p.name} — $detail$marker" + }.toTypedArray() + AlertDialog.Builder(this) + .setTitle(getString(R.string.profiles_title)) + .setItems(items) { _, which -> + if (which != active) { + Prefs.setActiveProfile(this, which) + settings = Prefs.load(this) + monitoring = false + priorityActive = false + updateMonitorButton() + updatePttEnabled() + refreshUi() + if (settings.host.isBlank() || settings.callsign.isBlank()) { + toast(getString(R.string.toast_need_settings)) + } else if (connected) { + svc?.disconnect() + mainHandler.postDelayed({ connect() }, 500) + } else { + connect() + } + } + } + .setNegativeButton(getString(R.string.cancel), null) + .show() + } + private fun updateMonitorButton() { val active = monitoring btnMonitor.imageTintList = ColorStateList.valueOf( diff --git a/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt index 6510a42..c91333c 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt @@ -2,26 +2,34 @@ package ru.ua1zbe.svxremote import android.app.Activity import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter import android.widget.Button import android.widget.CheckBox import android.widget.EditText +import android.widget.Spinner import android.widget.Toast import ru.ua1zbe.svxremote.prefs.ConnectionSettings import ru.ua1zbe.svxremote.prefs.Prefs +import ru.ua1zbe.svxremote.prefs.Profile class SettingsActivity : Activity() { + private lateinit var spinnerProfile: Spinner private lateinit var inputHost: EditText private lateinit var inputPort: EditText private lateinit var inputCallsign: EditText private lateinit var inputAuthKey: EditText private lateinit var inputTg: EditText private lateinit var inputGroups: EditText + private var currentProfile = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) + spinnerProfile = findViewById(R.id.spinner_profile) inputHost = findViewById(R.id.input_host) inputPort = findViewById(R.id.input_port) inputCallsign = findViewById(R.id.input_callsign) @@ -31,13 +39,24 @@ class SettingsActivity : Activity() { val chkNodeToasts = findViewById(R.id.chk_node_toasts) chkNodeToasts.isChecked = Prefs.nodeToastsEnabled(this) - val s = Prefs.load(this) - inputHost.setText(s.host) - inputPort.setText(s.port.toString()) - inputCallsign.setText(s.callsign) - inputAuthKey.setText(s.authKey) - inputTg.setText(s.talkGroup.toString()) - inputGroups.setText(s.groups.joinToString(", ")) + val profileNames = (1..Prefs.PROFILE_COUNT) + .map { getString(R.string.profile_name, it) } + spinnerProfile.adapter = ArrayAdapter( + this, android.R.layout.simple_spinner_dropdown_item, profileNames + ) + currentProfile = Prefs.activeProfileIndex(this) + spinnerProfile.setSelection(currentProfile) + spinnerProfile.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + if (position != currentProfile) { + currentProfile = position + loadFields(Prefs.loadProfile(this@SettingsActivity, position).settings) + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + loadFields(Prefs.loadProfile(this, currentProfile).settings) findViewById