From 8ac65b2174defa63276201b11720025b48d21121 Mon Sep 17 00:00:00 2001 From: umer0586 Date: Mon, 4 Nov 2024 14:48:14 +0500 Subject: [PATCH] New Feature : Service Discovery Support The app now supports Zero-configuration networking (Zeroconf/mDNS), enabling automatic server discovery on local networks. This feature eliminates the need for clients to hardcode IP addresses and port numbers when connecting to the WebSocket server. When enabled by the app user, the server broadcasts its presence on the network using the service type "_websocket._tcp", allowing clients to discover the server automatically. Clients can now implement service discovery to locate the server dynamically, rather than relying on hardcoded network configurations. --- app/build.gradle | 4 +- .../sensorserver/fragments/ServerFragment.kt | 104 +++++++++++++++++- .../fragments/SettingsFragment.kt | 28 ++++- .../sensorserver/service/WebsocketService.kt | 62 +++++++++++ .../sensorserver/setting/AppSettings.kt | 14 +++ .../main/res/layout/bottom_sheet_dialog.xml | 39 +++++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_preference.xml | 6 + .../metadata/android/en-US/changelogs/34.txt | 2 + 9 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/layout/bottom_sheet_dialog.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/34.txt diff --git a/app/build.gradle b/app/build.gradle index f7a8522..facfd7c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { minSdk 21 targetSdk 34 multiDexEnabled true - versionCode 33 - versionName "6.3.2" + versionCode 34 + versionName "6.4.0" } diff --git a/app/src/main/java/github/umer0586/sensorserver/fragments/ServerFragment.kt b/app/src/main/java/github/umer0586/sensorserver/fragments/ServerFragment.kt index d41acc4..7d70f96 100644 --- a/app/src/main/java/github/umer0586/sensorserver/fragments/ServerFragment.kt +++ b/app/src/main/java/github/umer0586/sensorserver/fragments/ServerFragment.kt @@ -1,7 +1,11 @@ package github.umer0586.sensorserver.fragments +import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.net.nsd.NsdServiceInfo import android.net.wifi.WifiManager import android.os.Build import android.os.Bundle @@ -9,22 +13,26 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.TextView import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.snackbar.Snackbar import com.permissionx.guolindev.PermissionX import github.umer0586.sensorserver.R -import github.umer0586.sensorserver.customextensions.isHotSpotEnabled import github.umer0586.sensorserver.databinding.FragmentServerBinding import github.umer0586.sensorserver.service.WebsocketService import github.umer0586.sensorserver.service.WebsocketService.LocalBinder import github.umer0586.sensorserver.service.ServerStateListener import github.umer0586.sensorserver.service.ServiceBindHelper +import github.umer0586.sensorserver.service.ServiceRegistrationState import github.umer0586.sensorserver.setting.AppSettings import github.umer0586.sensorserver.websocketserver.ServerInfo import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.net.BindException import java.net.UnknownHostException @@ -36,6 +44,9 @@ class ServerFragment : Fragment(), ServerStateListener private lateinit var serviceBindHelper: ServiceBindHelper private lateinit var appSettings: AppSettings + private lateinit var bottomSheetDialog: BottomSheetDialog + private lateinit var bottomSheetContent: View + private var _binding : FragmentServerBinding? = null // This property is only valid between onCreateView and // onDestroyView. @@ -54,6 +65,10 @@ class ServerFragment : Fragment(), ServerStateListener super.onViewCreated(view, savedInstanceState) Log.i(TAG, "onViewCreated: ") + bottomSheetDialog = BottomSheetDialog(requireContext()) + bottomSheetContent = layoutInflater.inflate(R.layout.bottom_sheet_dialog, null) + bottomSheetDialog.setContentView(bottomSheetContent) + appSettings = AppSettings(requireContext()) @@ -70,6 +85,7 @@ class ServerFragment : Fragment(), ServerStateListener websocketService = localBinder.service websocketService?.setServerStateListener(this) + websocketService?.setServiceRegistrationCallBack(this::onServiceRegistrationStateChanged) websocketService?.checkState() // this callbacks onServerAlreadyRunning() } @@ -150,6 +166,7 @@ class ServerFragment : Fragment(), ServerStateListener // To prevent memory leak websocketService?.setServerStateListener(null) + websocketService?.setServiceRegistrationCallBack(null) } override fun onServerStarted(serverInfo: ServerInfo) @@ -252,6 +269,91 @@ class ServerFragment : Fragment(), ServerStateListener } + @SuppressLint("SetTextI18n") + private fun onServiceRegistrationStateChanged( + serviceRegistrationState: ServiceRegistrationState, + serivceInfo: NsdServiceInfo?, + errorCode: Int? + ) { + + lifecycleScope.launch(Dispatchers.Main) { + + val messageText = bottomSheetContent.findViewById(R.id.message) + val progressIndicator = + bottomSheetContent.findViewById(R.id.progress_indicator) + + + when (serviceRegistrationState) { + ServiceRegistrationState.REGISTERING -> { + messageText.text = "Registering..." + bottomSheetDialog.show() + progressIndicator.show() + + } + + ServiceRegistrationState.REGISTRATION_SUCCESS -> { + + messageText.text = "Successfully registered" + delay(2000L) + showToastMessage("Service Discoverable !") + bottomSheetDialog.dismiss() + progressIndicator.hide() + + } + + ServiceRegistrationState.UNREGISTERING -> { + + messageText.text = "Unregistering..." + bottomSheetDialog.show() + progressIndicator.show() + + } + + ServiceRegistrationState.REGISTRATION_FAIL -> { + + messageText.text = "Registration Failed" + delay(2000L) + showToastMessage("Service Not Discoverable") + bottomSheetDialog.show() + progressIndicator.hide() + + } + + ServiceRegistrationState.UNREGISTRATION_SUCCESS -> { + + messageText.text = "Successfully unregistered" + delay(2000L) + showToastMessage("Service Not Discoverable") + bottomSheetDialog.dismiss() + progressIndicator.hide() + + + } + + ServiceRegistrationState.UNREGISTRATION_FAIL -> { + + messageText.text = "Unregistration Failed" + delay(2000L) + showToastMessage("Failed to unregister service") + bottomSheetDialog.show() + progressIndicator.hide() + + } + } + } + + } + + private fun showToastMessage(message: String){ + lifecycleScope.launch(Dispatchers.Main) { + Toast.makeText( + requireContext(), + message, + Toast.LENGTH_SHORT + ).show() + } + } + companion object { private val TAG: String = ServerFragment::class.java.simpleName diff --git a/app/src/main/java/github/umer0586/sensorserver/fragments/SettingsFragment.kt b/app/src/main/java/github/umer0586/sensorserver/fragments/SettingsFragment.kt index ef9c4cf..b378c69 100644 --- a/app/src/main/java/github/umer0586/sensorserver/fragments/SettingsFragment.kt +++ b/app/src/main/java/github/umer0586/sensorserver/fragments/SettingsFragment.kt @@ -26,6 +26,7 @@ class SettingsFragment : PreferenceFragmentCompat() private var hotspotPref : SwitchPreferenceCompat? = null private var localHostPref : SwitchPreferenceCompat? = null private var allInterfacesPref : SwitchPreferenceCompat? = null + private var discoverablePref : SwitchPreferenceCompat? = null override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) @@ -39,9 +40,29 @@ class SettingsFragment : PreferenceFragmentCompat() handleAllInterfacesPreference() handleSamplingRatePreference() handleHotspotPref() + handleDiscoverablePref() } + private fun handleDiscoverablePref() { + discoverablePref = findPreference(getString(R.string.pref_key_discoverable)) + discoverablePref?.isChecked = appSettings.isDiscoverableEnabled() + + discoverablePref?.setOnPreferenceChangeListener { _, newValue -> + appSettings.saveDiscoverable(newValue as Boolean) + + if(newValue == true) + { + localHostPref?.apply { + isChecked = false + appSettings.enableLocalHostOption(false) + } + } + + return@setOnPreferenceChangeListener true + } + } + private fun handleHttpPortPreference() { val httpPortNoPref = findPreference(getString(R.string.pref_key_http_port_no)) @@ -147,7 +168,7 @@ class SettingsFragment : PreferenceFragmentCompat() { localHostPref = findPreference(getString(R.string.pref_key_localhost)) - localHostPref?.setOnPreferenceChangeListener { preference, newValue -> + localHostPref?.setOnPreferenceChangeListener { _, newValue -> val newState = newValue as Boolean appSettings.enableLocalHostOption(newState) @@ -162,6 +183,11 @@ class SettingsFragment : PreferenceFragmentCompat() isChecked = false appSettings.listenOnAllInterfaces(false) } + + discoverablePref?.apply { + isChecked = false + appSettings.saveDiscoverable(false) + } } diff --git a/app/src/main/java/github/umer0586/sensorserver/service/WebsocketService.kt b/app/src/main/java/github/umer0586/sensorserver/service/WebsocketService.kt index a7af9f1..4ffcdce 100644 --- a/app/src/main/java/github/umer0586/sensorserver/service/WebsocketService.kt +++ b/app/src/main/java/github/umer0586/sensorserver/service/WebsocketService.kt @@ -7,6 +7,8 @@ import android.app.Service import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo import android.net.wifi.WifiManager import android.os.Binder import android.os.Build @@ -36,6 +38,15 @@ interface ServerStateListener fun onServerAlreadyRunning(serverInfo: ServerInfo) } +enum class ServiceRegistrationState{ + REGISTERING, + REGISTRATION_SUCCESS, + REGISTRATION_FAIL, + UNREGISTERING, + UNREGISTRATION_SUCCESS, + UNREGISTRATION_FAIL +} + class WebsocketService : Service() { @@ -46,6 +57,9 @@ class WebsocketService : Service() private var connectionsChangeCallBack: ((List) -> Unit)? = null private var connectionsCountChangeCallBack: ((Int) -> Unit)? = null + private lateinit var nsdManager : NsdManager + private var serviceRegistrationCallBack: ((ServiceRegistrationState, NsdServiceInfo?, Int?) -> Unit)? = null + private lateinit var appSettings: AppSettings // Binder given to clients @@ -73,6 +87,7 @@ class WebsocketService : Service() { super.onCreate() Log.d(TAG, "onCreate()") + nsdManager = (getSystemService(Context.NSD_SERVICE) as NsdManager) createNotificationChannel() appSettings = AppSettings(applicationContext) broadcastMessageReceiver = BroadcastMessageReceiver(applicationContext) @@ -188,6 +203,9 @@ class WebsocketService : Service() val notification = notificationBuilder.build() startForeground(ON_GOING_NOTIFICATION_ID, notification) + if(appSettings.isDiscoverableEnabled()) + makeServiceDiscoverable(serverInfo.port) + } sensorWebSocketServer?.onStop { @@ -196,6 +214,9 @@ class WebsocketService : Service() //remove the service from foreground but don't stop (destroy) the service //stopForeground(true) stopForeground() + + if(appSettings.isDiscoverableEnabled()) + makeServiceNotDiscoverable() } sensorWebSocketServer?.onError { exception -> @@ -363,6 +384,47 @@ class WebsocketService : Service() } + private val serviceRegistrationListener = object : NsdManager.RegistrationListener { + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + serviceRegistrationCallBack?.invoke(ServiceRegistrationState.REGISTRATION_FAIL,serviceInfo,errorCode) + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + serviceRegistrationCallBack?.invoke(ServiceRegistrationState.UNREGISTRATION_FAIL,serviceInfo,errorCode) + } + + override fun onServiceRegistered(serviceInfo: NsdServiceInfo?) { + serviceRegistrationCallBack?.invoke(ServiceRegistrationState.REGISTRATION_SUCCESS,serviceInfo,null) + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo?) { + serviceRegistrationCallBack?.invoke(ServiceRegistrationState.UNREGISTRATION_SUCCESS,serviceInfo,null) + } + + } + + private fun makeServiceDiscoverable(portNo : Int){ + val serviceInfo = NsdServiceInfo().apply { + serviceName = "SensorServer" + serviceType = "_websocket._tcp" + port = portNo + } + nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, serviceRegistrationListener) + serviceRegistrationCallBack?.invoke(ServiceRegistrationState.REGISTERING,serviceInfo,null) + } + + private fun makeServiceNotDiscoverable(){ + nsdManager.unregisterService(serviceRegistrationListener) + serviceRegistrationCallBack?.invoke(ServiceRegistrationState.UNREGISTERING,null,null) + } + + + fun setServiceRegistrationCallBack(callBack: ((ServiceRegistrationState, NsdServiceInfo?, Int?) -> Unit)?){ + serviceRegistrationCallBack = callBack + } + + } \ No newline at end of file diff --git a/app/src/main/java/github/umer0586/sensorserver/setting/AppSettings.kt b/app/src/main/java/github/umer0586/sensorserver/setting/AppSettings.kt index 6b79c2e..edc042c 100644 --- a/app/src/main/java/github/umer0586/sensorserver/setting/AppSettings.kt +++ b/app/src/main/java/github/umer0586/sensorserver/setting/AppSettings.kt @@ -101,6 +101,19 @@ class AppSettings(context: Context) return sharedPreferences.getBoolean(context.getString(R.string.pref_key_all_interface), false) } + fun saveDiscoverable(state: Boolean) + { + sharedPreferences.edit() + .putBoolean(context.getString(R.string.pref_key_discoverable), state) + .apply() + + } + + fun isDiscoverableEnabled() : Boolean + { + return sharedPreferences.getBoolean(context.getString(R.string.pref_key_discoverable), DEFAULT_DISCOVERABLE) + } + companion object { @@ -108,5 +121,6 @@ class AppSettings(context: Context) private const val DEFAULT_WEBSOCKET_PORT_NO = 8080 private const val DEFAULT_HTTP_PORT_NO = 9090 private const val DEFAULT_SAMPLING_RATE = 200000 + private const val DEFAULT_DISCOVERABLE = false } } \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_dialog.xml b/app/src/main/res/layout/bottom_sheet_dialog.xml new file mode 100644 index 0000000..42f2a35 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_dialog.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 853e5b0..a151a6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ pref_key_localhost pref_key_all_interface pref_key_hotspot + pref_key_discoverable Open diff --git a/app/src/main/res/xml/settings_preference.xml b/app/src/main/res/xml/settings_preference.xml index da76c81..436e31c 100644 --- a/app/src/main/res/xml/settings_preference.xml +++ b/app/src/main/res/xml/settings_preference.xml @@ -57,6 +57,12 @@ app:summaryOff="Use device's Hotspot" app:icon="@drawable/ic_wifi_tethering" /> + \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/34.txt b/fastlane/metadata/android/en-US/changelogs/34.txt new file mode 100644 index 0000000..cf59569 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/34.txt @@ -0,0 +1,2 @@ +New Feature : Service Discovery Support +The app now supports Zero-configuration networking (Zeroconf/mDNS), enabling automatic server discovery on local networks. This feature eliminates the need for clients to hardcode IP addresses and port numbers when connecting to the WebSocket server. When enabled by the app user, the server broadcasts its presence on the network using the service type "_websocket._tcp", allowing clients to discover the server automatically. Clients can now implement service discovery to locate the server dynamically, rather than relying on hardcoded network configurations. \ No newline at end of file