Skip to content

Commit

Permalink
New Feature : Service Discovery Support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
umer0586 committed Nov 4, 2024
1 parent 5c2b8f0 commit 8ac65b2
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 4 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ android {
minSdk 21
targetSdk 34
multiDexEnabled true
versionCode 33
versionName "6.3.2"
versionCode 34
versionName "6.4.0"


}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
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
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
Expand All @@ -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.
Expand All @@ -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())

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

websocketService?.setServerStateListener(this)
websocketService?.setServiceRegistrationCallBack(this::onServiceRegistrationStateChanged)
websocketService?.checkState() // this callbacks onServerAlreadyRunning()
}

Expand Down Expand Up @@ -150,6 +166,7 @@ class ServerFragment : Fragment(), ServerStateListener

// To prevent memory leak
websocketService?.setServerStateListener(null)
websocketService?.setServiceRegistrationCallBack(null)
}

override fun onServerStarted(serverInfo: ServerInfo)
Expand Down Expand Up @@ -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<TextView>(R.id.message)
val progressIndicator =
bottomSheetContent.findViewById<LinearProgressIndicator>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
Expand All @@ -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<EditTextPreference>(getString(R.string.pref_key_http_port_no))
Expand Down Expand Up @@ -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)

Expand All @@ -162,6 +183,11 @@ class SettingsFragment : PreferenceFragmentCompat()
isChecked = false
appSettings.listenOnAllInterfaces(false)
}

discoverablePref?.apply {
isChecked = false
appSettings.saveDiscoverable(false)
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
{

Expand All @@ -46,6 +57,9 @@ class WebsocketService : Service()
private var connectionsChangeCallBack: ((List<WebSocket>) -> 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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {

Expand All @@ -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 ->
Expand Down Expand Up @@ -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
}




}
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,26 @@ 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
{


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
}
}
Loading

0 comments on commit 8ac65b2

Please sign in to comment.