Skip to content

Commit 8ac65b2

Browse files
committed
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.
1 parent 5c2b8f0 commit 8ac65b2

File tree

9 files changed

+256
-4
lines changed

9 files changed

+256
-4
lines changed

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ android {
1313
minSdk 21
1414
targetSdk 34
1515
multiDexEnabled true
16-
versionCode 33
17-
versionName "6.3.2"
16+
versionCode 34
17+
versionName "6.4.0"
1818

1919

2020
}

app/src/main/java/github/umer0586/sensorserver/fragments/ServerFragment.kt

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
11
package github.umer0586.sensorserver.fragments
22

3+
import android.annotation.SuppressLint
34
import android.content.Context
45
import android.content.Intent
6+
import android.graphics.drawable.Drawable
7+
import android.graphics.drawable.Icon
8+
import android.net.nsd.NsdServiceInfo
59
import android.net.wifi.WifiManager
610
import android.os.Build
711
import android.os.Bundle
812
import android.util.Log
913
import android.view.LayoutInflater
1014
import android.view.View
1115
import android.view.ViewGroup
16+
import android.widget.TextView
1217
import android.widget.Toast
1318
import androidx.core.content.ContextCompat
1419
import androidx.fragment.app.Fragment
1520
import androidx.lifecycle.lifecycleScope
21+
import com.google.android.material.bottomsheet.BottomSheetDialog
22+
import com.google.android.material.progressindicator.LinearProgressIndicator
1623
import com.google.android.material.snackbar.Snackbar
1724
import com.permissionx.guolindev.PermissionX
1825
import github.umer0586.sensorserver.R
19-
import github.umer0586.sensorserver.customextensions.isHotSpotEnabled
2026
import github.umer0586.sensorserver.databinding.FragmentServerBinding
2127
import github.umer0586.sensorserver.service.WebsocketService
2228
import github.umer0586.sensorserver.service.WebsocketService.LocalBinder
2329
import github.umer0586.sensorserver.service.ServerStateListener
2430
import github.umer0586.sensorserver.service.ServiceBindHelper
31+
import github.umer0586.sensorserver.service.ServiceRegistrationState
2532
import github.umer0586.sensorserver.setting.AppSettings
2633
import github.umer0586.sensorserver.websocketserver.ServerInfo
2734
import kotlinx.coroutines.Dispatchers
35+
import kotlinx.coroutines.delay
2836
import kotlinx.coroutines.launch
2937
import java.net.BindException
3038
import java.net.UnknownHostException
@@ -36,6 +44,9 @@ class ServerFragment : Fragment(), ServerStateListener
3644
private lateinit var serviceBindHelper: ServiceBindHelper
3745
private lateinit var appSettings: AppSettings
3846

47+
private lateinit var bottomSheetDialog: BottomSheetDialog
48+
private lateinit var bottomSheetContent: View
49+
3950
private var _binding : FragmentServerBinding? = null
4051
// This property is only valid between onCreateView and
4152
// onDestroyView.
@@ -54,6 +65,10 @@ class ServerFragment : Fragment(), ServerStateListener
5465
super.onViewCreated(view, savedInstanceState)
5566
Log.i(TAG, "onViewCreated: ")
5667

68+
bottomSheetDialog = BottomSheetDialog(requireContext())
69+
bottomSheetContent = layoutInflater.inflate(R.layout.bottom_sheet_dialog, null)
70+
bottomSheetDialog.setContentView(bottomSheetContent)
71+
5772

5873
appSettings = AppSettings(requireContext())
5974

@@ -70,6 +85,7 @@ class ServerFragment : Fragment(), ServerStateListener
7085
websocketService = localBinder.service
7186

7287
websocketService?.setServerStateListener(this)
88+
websocketService?.setServiceRegistrationCallBack(this::onServiceRegistrationStateChanged)
7389
websocketService?.checkState() // this callbacks onServerAlreadyRunning()
7490
}
7591

@@ -150,6 +166,7 @@ class ServerFragment : Fragment(), ServerStateListener
150166

151167
// To prevent memory leak
152168
websocketService?.setServerStateListener(null)
169+
websocketService?.setServiceRegistrationCallBack(null)
153170
}
154171

155172
override fun onServerStarted(serverInfo: ServerInfo)
@@ -252,6 +269,91 @@ class ServerFragment : Fragment(), ServerStateListener
252269

253270
}
254271

272+
@SuppressLint("SetTextI18n")
273+
private fun onServiceRegistrationStateChanged(
274+
serviceRegistrationState: ServiceRegistrationState,
275+
serivceInfo: NsdServiceInfo?,
276+
errorCode: Int?
277+
) {
278+
279+
lifecycleScope.launch(Dispatchers.Main) {
280+
281+
val messageText = bottomSheetContent.findViewById<TextView>(R.id.message)
282+
val progressIndicator =
283+
bottomSheetContent.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
284+
285+
286+
when (serviceRegistrationState) {
287+
ServiceRegistrationState.REGISTERING -> {
288+
messageText.text = "Registering..."
289+
bottomSheetDialog.show()
290+
progressIndicator.show()
291+
292+
}
293+
294+
ServiceRegistrationState.REGISTRATION_SUCCESS -> {
295+
296+
messageText.text = "Successfully registered"
297+
delay(2000L)
298+
showToastMessage("Service Discoverable !")
299+
bottomSheetDialog.dismiss()
300+
progressIndicator.hide()
301+
302+
}
303+
304+
ServiceRegistrationState.UNREGISTERING -> {
305+
306+
messageText.text = "Unregistering..."
307+
bottomSheetDialog.show()
308+
progressIndicator.show()
309+
310+
}
311+
312+
ServiceRegistrationState.REGISTRATION_FAIL -> {
313+
314+
messageText.text = "Registration Failed"
315+
delay(2000L)
316+
showToastMessage("Service Not Discoverable")
317+
bottomSheetDialog.show()
318+
progressIndicator.hide()
319+
320+
}
321+
322+
ServiceRegistrationState.UNREGISTRATION_SUCCESS -> {
323+
324+
messageText.text = "Successfully unregistered"
325+
delay(2000L)
326+
showToastMessage("Service Not Discoverable")
327+
bottomSheetDialog.dismiss()
328+
progressIndicator.hide()
329+
330+
331+
}
332+
333+
ServiceRegistrationState.UNREGISTRATION_FAIL -> {
334+
335+
messageText.text = "Unregistration Failed"
336+
delay(2000L)
337+
showToastMessage("Failed to unregister service")
338+
bottomSheetDialog.show()
339+
progressIndicator.hide()
340+
341+
}
342+
}
343+
}
344+
345+
}
346+
347+
private fun showToastMessage(message: String){
348+
lifecycleScope.launch(Dispatchers.Main) {
349+
Toast.makeText(
350+
requireContext(),
351+
message,
352+
Toast.LENGTH_SHORT
353+
).show()
354+
}
355+
}
356+
255357
companion object
256358
{
257359
private val TAG: String = ServerFragment::class.java.simpleName

app/src/main/java/github/umer0586/sensorserver/fragments/SettingsFragment.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class SettingsFragment : PreferenceFragmentCompat()
2626
private var hotspotPref : SwitchPreferenceCompat? = null
2727
private var localHostPref : SwitchPreferenceCompat? = null
2828
private var allInterfacesPref : SwitchPreferenceCompat? = null
29+
private var discoverablePref : SwitchPreferenceCompat? = null
2930

3031

3132
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?)
@@ -39,9 +40,29 @@ class SettingsFragment : PreferenceFragmentCompat()
3940
handleAllInterfacesPreference()
4041
handleSamplingRatePreference()
4142
handleHotspotPref()
43+
handleDiscoverablePref()
4244

4345
}
4446

47+
private fun handleDiscoverablePref() {
48+
discoverablePref = findPreference(getString(R.string.pref_key_discoverable))
49+
discoverablePref?.isChecked = appSettings.isDiscoverableEnabled()
50+
51+
discoverablePref?.setOnPreferenceChangeListener { _, newValue ->
52+
appSettings.saveDiscoverable(newValue as Boolean)
53+
54+
if(newValue == true)
55+
{
56+
localHostPref?.apply {
57+
isChecked = false
58+
appSettings.enableLocalHostOption(false)
59+
}
60+
}
61+
62+
return@setOnPreferenceChangeListener true
63+
}
64+
}
65+
4566
private fun handleHttpPortPreference()
4667
{
4768
val httpPortNoPref = findPreference<EditTextPreference>(getString(R.string.pref_key_http_port_no))
@@ -147,7 +168,7 @@ class SettingsFragment : PreferenceFragmentCompat()
147168
{
148169
localHostPref = findPreference(getString(R.string.pref_key_localhost))
149170

150-
localHostPref?.setOnPreferenceChangeListener { preference, newValue ->
171+
localHostPref?.setOnPreferenceChangeListener { _, newValue ->
151172
val newState = newValue as Boolean
152173
appSettings.enableLocalHostOption(newState)
153174

@@ -162,6 +183,11 @@ class SettingsFragment : PreferenceFragmentCompat()
162183
isChecked = false
163184
appSettings.listenOnAllInterfaces(false)
164185
}
186+
187+
discoverablePref?.apply {
188+
isChecked = false
189+
appSettings.saveDiscoverable(false)
190+
}
165191
}
166192

167193

app/src/main/java/github/umer0586/sensorserver/service/WebsocketService.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import android.app.Service
77
import android.content.Context
88
import android.content.Intent
99
import android.content.IntentFilter
10+
import android.net.nsd.NsdManager
11+
import android.net.nsd.NsdServiceInfo
1012
import android.net.wifi.WifiManager
1113
import android.os.Binder
1214
import android.os.Build
@@ -36,6 +38,15 @@ interface ServerStateListener
3638
fun onServerAlreadyRunning(serverInfo: ServerInfo)
3739
}
3840

41+
enum class ServiceRegistrationState{
42+
REGISTERING,
43+
REGISTRATION_SUCCESS,
44+
REGISTRATION_FAIL,
45+
UNREGISTERING,
46+
UNREGISTRATION_SUCCESS,
47+
UNREGISTRATION_FAIL
48+
}
49+
3950
class WebsocketService : Service()
4051
{
4152

@@ -46,6 +57,9 @@ class WebsocketService : Service()
4657
private var connectionsChangeCallBack: ((List<WebSocket>) -> Unit)? = null
4758
private var connectionsCountChangeCallBack: ((Int) -> Unit)? = null
4859

60+
private lateinit var nsdManager : NsdManager
61+
private var serviceRegistrationCallBack: ((ServiceRegistrationState, NsdServiceInfo?, Int?) -> Unit)? = null
62+
4963
private lateinit var appSettings: AppSettings
5064

5165
// Binder given to clients
@@ -73,6 +87,7 @@ class WebsocketService : Service()
7387
{
7488
super.onCreate()
7589
Log.d(TAG, "onCreate()")
90+
nsdManager = (getSystemService(Context.NSD_SERVICE) as NsdManager)
7691
createNotificationChannel()
7792
appSettings = AppSettings(applicationContext)
7893
broadcastMessageReceiver = BroadcastMessageReceiver(applicationContext)
@@ -188,6 +203,9 @@ class WebsocketService : Service()
188203
val notification = notificationBuilder.build()
189204
startForeground(ON_GOING_NOTIFICATION_ID, notification)
190205

206+
if(appSettings.isDiscoverableEnabled())
207+
makeServiceDiscoverable(serverInfo.port)
208+
191209
}
192210
sensorWebSocketServer?.onStop {
193211

@@ -196,6 +214,9 @@ class WebsocketService : Service()
196214
//remove the service from foreground but don't stop (destroy) the service
197215
//stopForeground(true)
198216
stopForeground()
217+
218+
if(appSettings.isDiscoverableEnabled())
219+
makeServiceNotDiscoverable()
199220
}
200221

201222
sensorWebSocketServer?.onError { exception ->
@@ -363,6 +384,47 @@ class WebsocketService : Service()
363384

364385
}
365386

387+
private val serviceRegistrationListener = object : NsdManager.RegistrationListener {
388+
389+
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
390+
serviceRegistrationCallBack?.invoke(ServiceRegistrationState.REGISTRATION_FAIL,serviceInfo,errorCode)
391+
}
392+
393+
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
394+
serviceRegistrationCallBack?.invoke(ServiceRegistrationState.UNREGISTRATION_FAIL,serviceInfo,errorCode)
395+
}
396+
397+
override fun onServiceRegistered(serviceInfo: NsdServiceInfo?) {
398+
serviceRegistrationCallBack?.invoke(ServiceRegistrationState.REGISTRATION_SUCCESS,serviceInfo,null)
399+
}
400+
401+
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo?) {
402+
serviceRegistrationCallBack?.invoke(ServiceRegistrationState.UNREGISTRATION_SUCCESS,serviceInfo,null)
403+
}
404+
405+
}
406+
407+
private fun makeServiceDiscoverable(portNo : Int){
408+
val serviceInfo = NsdServiceInfo().apply {
409+
serviceName = "SensorServer"
410+
serviceType = "_websocket._tcp"
411+
port = portNo
412+
}
413+
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, serviceRegistrationListener)
414+
serviceRegistrationCallBack?.invoke(ServiceRegistrationState.REGISTERING,serviceInfo,null)
415+
}
416+
417+
private fun makeServiceNotDiscoverable(){
418+
nsdManager.unregisterService(serviceRegistrationListener)
419+
serviceRegistrationCallBack?.invoke(ServiceRegistrationState.UNREGISTERING,null,null)
420+
}
421+
422+
423+
fun setServiceRegistrationCallBack(callBack: ((ServiceRegistrationState, NsdServiceInfo?, Int?) -> Unit)?){
424+
serviceRegistrationCallBack = callBack
425+
}
426+
427+
366428

367429

368430
}

app/src/main/java/github/umer0586/sensorserver/setting/AppSettings.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,26 @@ class AppSettings(context: Context)
101101
return sharedPreferences.getBoolean(context.getString(R.string.pref_key_all_interface), false)
102102
}
103103

104+
fun saveDiscoverable(state: Boolean)
105+
{
106+
sharedPreferences.edit()
107+
.putBoolean(context.getString(R.string.pref_key_discoverable), state)
108+
.apply()
109+
110+
}
111+
112+
fun isDiscoverableEnabled() : Boolean
113+
{
114+
return sharedPreferences.getBoolean(context.getString(R.string.pref_key_discoverable), DEFAULT_DISCOVERABLE)
115+
}
116+
104117
companion object
105118
{
106119

107120

108121
private const val DEFAULT_WEBSOCKET_PORT_NO = 8080
109122
private const val DEFAULT_HTTP_PORT_NO = 9090
110123
private const val DEFAULT_SAMPLING_RATE = 200000
124+
private const val DEFAULT_DISCOVERABLE = false
111125
}
112126
}

0 commit comments

Comments
 (0)