From 9790f9d69c201c7a727d226fe53c8537a6ae9cbb Mon Sep 17 00:00:00 2001 From: ua1zbe Date: Fri, 14 Aug 2026 13:28:55 +0300 Subject: [PATCH] Show centered node join/leave overlay for 5s (v1.0.4) - Replaces bottom toast: dark rounded card centered on screen, auto-hides after 5 seconds - Optional test trigger via node_toast_test intent extra --- .../java/ru/ua1zbe/svxremote/MainActivity.kt | 56 +++++++++++++++++++ .../ru/ua1zbe/svxremote/SettingsActivity.kt | 4 ++ .../ru/ua1zbe/svxremote/SplashActivity.kt | 2 + .../java/ru/ua1zbe/svxremote/SvxService.kt | 8 ++- .../java/ru/ua1zbe/svxremote/prefs/Prefs.kt | 10 ++++ .../ua1zbe/svxremote/proto/ReflectorClient.kt | 23 ++++++-- app/src/main/res/drawable/bg_node_toast.xml | 7 +++ app/src/main/res/layout/activity_settings.xml | 9 +++ app/src/main/res/layout/view_node_toast.xml | 20 +++++++ app/src/main/res/values/strings.xml | 4 ++ 10 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/bg_node_toast.xml create mode 100644 app/src/main/res/layout/view_node_toast.xml diff --git a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt index 826db76..38ca800 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt @@ -9,6 +9,7 @@ import android.content.Intent import android.content.ServiceConnection import android.content.pm.PackageManager import android.net.Uri +import android.graphics.PixelFormat import android.os.Build import android.os.Bundle import android.os.Handler @@ -16,6 +17,7 @@ import android.os.IBinder import android.os.Looper import android.provider.Settings import android.util.Log +import android.view.Gravity import android.view.View import android.view.WindowManager import android.widget.ImageButton @@ -101,6 +103,11 @@ class MainActivity : Activity(), SvxService.Listener { btnSettings = findViewById(R.id.btn_settings) pttButton.enabled2 = false + val testText = intent?.getStringExtra("node_toast_test") + if (testText != null) { + mainHandler.postDelayed({ showNodeOverlay(testText) }, 1500) + } + btnSettings.setOnClickListener { startActivity(Intent(this, SettingsActivity::class.java)) } @@ -170,6 +177,8 @@ class MainActivity : Activity(), SvxService.Listener { override fun onDestroy() { super.onDestroy() + hideNodeOverlay() + mainHandler.removeCallbacksAndMessages(null) SplashActivity.mainAlive = false svc?.listener = null unbindService(svcConn) @@ -273,6 +282,53 @@ class MainActivity : Activity(), SvxService.Listener { // последний говоривший остаётся на экране } + override fun onNodeJoined(callsign: String, tg: Long?) { + showNodeToast(R.string.toast_node_joined, callsign, tg) + } + + override fun onNodeLeft(callsign: String, tg: Long?) { + showNodeToast(R.string.toast_node_left, callsign, tg) + } + + private fun showNodeToast(tpl: Int, callsign: String, tg: Long?) { + if (!Prefs.nodeToastsEnabled(this)) return + if (callsign.equals(settings.callsign, ignoreCase = true)) return + val suffix = if (tg != null) getString(R.string.node_tg_suffix, tg) else "" + mainHandler.post { showNodeOverlay(getString(tpl, callsign, suffix)) } + } + + private var nodeOverlay: View? = null + + private fun showNodeOverlay(text: String) { + hideNodeOverlay() + val view = layoutInflater.inflate(R.layout.view_node_toast, null, false) + view.findViewById(R.id.lbl_node_toast).text = text + try { + windowManager.addView( + view, + WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSLUCENT + ).apply { gravity = Gravity.CENTER } + ) + nodeOverlay = view + mainHandler.postDelayed({ hideNodeOverlay() }, 5000) + } catch (_: Exception) { + toast(text) + } + } + + private fun hideNodeOverlay() { + nodeOverlay?.let { v -> + try { windowManager.removeView(v) } catch (_: Exception) { } + nodeOverlay = null + } + } + override fun onAudioPacket(packet: ByteArray, seq: Int) { } diff --git a/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt index 9d8d753..6510a42 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/SettingsActivity.kt @@ -3,6 +3,7 @@ package ru.ua1zbe.svxremote import android.app.Activity import android.os.Bundle import android.widget.Button +import android.widget.CheckBox import android.widget.EditText import android.widget.Toast import ru.ua1zbe.svxremote.prefs.ConnectionSettings @@ -27,6 +28,8 @@ class SettingsActivity : Activity() { inputAuthKey = findViewById(R.id.input_auth_key) inputTg = findViewById(R.id.input_tg) inputGroups = findViewById(R.id.input_groups) + val chkNodeToasts = findViewById(R.id.chk_node_toasts) + chkNodeToasts.isChecked = Prefs.nodeToastsEnabled(this) val s = Prefs.load(this) inputHost.setText(s.host) @@ -63,6 +66,7 @@ class SettingsActivity : Activity() { groups = groups ) ) + Prefs.setNodeToastsEnabled(this, chkNodeToasts.isChecked) toast(getString(R.string.saved)) finish() } diff --git a/app/src/main/java/ru/ua1zbe/svxremote/SplashActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/SplashActivity.kt index fc43e36..0cd2b2e 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/SplashActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/SplashActivity.kt @@ -63,6 +63,8 @@ class SplashActivity : Activity() { Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP ) + val testText = this.intent?.getStringExtra("node_toast_test") + if (testText != null) intent.putExtra("node_toast_test", testText) startActivity(intent) overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) finish() diff --git a/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt b/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt index 302684d..21798e6 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/SvxService.kt @@ -23,6 +23,8 @@ class SvxService : Service(), ReflectorClient.Callbacks { fun onServerInfo(info: MsgParser.ServerInfo) fun onTalkerStart(tg: Long, callsign: String) fun onTalkerStop(tg: Long, callsign: String) + fun onNodeJoined(callsign: String, tg: Long?) + fun onNodeLeft(callsign: String, tg: Long?) fun onAudioPacket(packet: ByteArray, seq: Int) fun onAudioFlush() fun onError(detail: String) @@ -123,12 +125,14 @@ class SvxService : Service(), ReflectorClient.Callbacks { /** Текущий список подключенных узлов (копия для UI). */ fun nodeList(): List = synchronized(nodeSet) { nodeSet.toList() } - override fun onNodeJoined(callsign: String) { + override fun onNodeJoined(callsign: String, tg: Long?) { synchronized(nodeSet) { nodeSet.add(callsign) } + listener?.onNodeJoined(callsign, tg) } - override fun onNodeLeft(callsign: String) { + override fun onNodeLeft(callsign: String, tg: Long?) { synchronized(nodeSet) { nodeSet.remove(callsign) } + listener?.onNodeLeft(callsign, tg) } override fun onTalkerStart(tg: Long, callsign: String) { diff --git a/app/src/main/java/ru/ua1zbe/svxremote/prefs/Prefs.kt b/app/src/main/java/ru/ua1zbe/svxremote/prefs/Prefs.kt index 937ef3b..9a0a1d6 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/prefs/Prefs.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/prefs/Prefs.kt @@ -42,4 +42,14 @@ object Prefs { .putString("groups", s.groups.distinct().joinToString(",")) .apply() } + + fun nodeToastsEnabled(context: Context): Boolean { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getBoolean("node_toasts", true) + } + + fun setNodeToastsEnabled(context: Context, enabled: Boolean) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit().putBoolean("node_toasts", enabled).apply() + } } \ No newline at end of file diff --git a/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt b/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt index c87fb5e..65f339b 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/proto/ReflectorClient.kt @@ -23,8 +23,8 @@ class ReflectorClient( fun onServerInfo(info: MsgParser.ServerInfo) fun onTalkerStart(tg: Long, callsign: String) fun onTalkerStop(tg: Long, callsign: String) - fun onNodeJoined(callsign: String) - fun onNodeLeft(callsign: String) + fun onNodeJoined(callsign: String, tg: Long?) + fun onNodeLeft(callsign: String, tg: Long?) /** Входящий аудиопакет Opus. */ fun onAudioPacket(packet: ByteArray, seq: Int) fun onAudioFlush() @@ -367,13 +367,13 @@ class ReflectorClient( } ReflectorProtocol.MSG_NODE_JOINED -> { - val cs = parseSimpleString(msg.fields) - callbacks.onNodeJoined(cs) + val (cs, tg) = parseNodeEvent(msg.fields) + callbacks.onNodeJoined(cs, tg) } ReflectorProtocol.MSG_NODE_LEFT -> { - val cs = parseSimpleString(msg.fields) - callbacks.onNodeLeft(cs) + val (cs, tg) = parseNodeEvent(msg.fields) + callbacks.onNodeLeft(cs, tg) } ReflectorProtocol.MSG_TALKER_START -> { @@ -404,6 +404,17 @@ class ReflectorClient( return true } + private fun parseNodeEvent(fields: ByteArray): Pair { + return try { + val r = BinReader(fields) + val cs = r.str().ifEmpty { "?" } + val tg = try { r.str().toLongOrNull() } catch (_: Exception) { null } + cs to tg + } catch (_: Exception) { + "?" to null + } + } + private fun parseSimpleString(fields: ByteArray): String { return try { BinReader(fields).str().ifEmpty { "?" } diff --git a/app/src/main/res/drawable/bg_node_toast.xml b/app/src/main/res/drawable/bg_node_toast.xml new file mode 100644 index 0000000..742f0a6 --- /dev/null +++ b/app/src/main/res/drawable/bg_node_toast.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 8fe2949..ff1204f 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -139,6 +139,15 @@ android:padding="12dp" android:hint="9, 11, 12, 13" /> + +