From e12b500144709be1e64ef6be893f1d85e69c9fe8 Mon Sep 17 00:00:00 2001 From: Koen J Date: Mon, 5 May 2025 12:00:04 +0200 Subject: [PATCH] Sync disabled by default. --- .../java/com/futo/platformplayer/Settings.kt | 2 +- .../activities/SyncHomeActivity.kt | 8 + .../futo/platformplayer/states/StateApp.kt | 24 +- .../futo/platformplayer/states/StateSync.kt | 289 ++++++++++-------- 4 files changed, 200 insertions(+), 123 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 81a2b791..3c52c2c2 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -926,7 +926,7 @@ class Settings : FragmentedStorageFileJson() { @Serializable class Synchronization { @FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1) - var enabled: Boolean = true; + var enabled: Boolean = false; @FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1) var broadcast: Boolean = false; diff --git a/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt index 2b7e3a72..859e6d4e 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/SyncHomeActivity.kt @@ -89,6 +89,14 @@ class SyncHomeActivity : AppCompatActivity() { updateEmptyVisibility() } } + + StateSync.instance.confirmStarted(this, { + StateSync.instance.showFailedToBindDialogIfNecessary(this@SyncHomeActivity) + }, { + finish() + }, { + StateSync.instance.showFailedToBindDialogIfNecessary(this@SyncHomeActivity) + }) } override fun onDestroy() { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index d15a604a..cdfb308d 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -29,6 +29,7 @@ import com.futo.platformplayer.activities.CaptchaActivity import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.SettingsActivity +import com.futo.platformplayer.activities.SettingsActivity.Companion.settingsActivityClosed import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.background.BackgroundWorker @@ -411,7 +412,27 @@ class StateApp { } if (Settings.instance.synchronization.enabled) { - StateSync.instance.start(context) + StateSync.instance.start(context, { + try { + UIDialogs.toast("Failed to start sync, port in use") + } catch (e: Throwable) { + //Ignored + } + }) + } + + settingsActivityClosed.subscribe { + if (Settings.instance.synchronization.enabled) { + StateSync.instance.start(context, { + try { + UIDialogs.toast("Failed to start sync, port in use") + } catch (e: Throwable) { + //Ignored + } + }) + } else { + StateSync.instance.stop() + } } Logger.onLogSubmitted.subscribe { @@ -707,6 +728,7 @@ class StateApp { StatePlayer.instance.closeMediaSession(); StateCasting.instance.stop(); + StateSync.instance.stop(); StatePlayer.dispose(); Companion.dispose(); _fileLogConsumer?.close(); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt index 811d89d8..f4c3ecf8 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -7,6 +7,7 @@ import android.os.Build import android.util.Log import com.futo.platformplayer.LittleEndianDataInputStream import com.futo.platformplayer.LittleEndianDataOutputStream +import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.MainActivity @@ -77,10 +78,11 @@ class StateSync { private var _serverSocket: ServerSocket? = null private var _thread: Thread? = null private var _connectThread: Thread? = null - private var _started = false + @Volatile private var _started = false private val _sessions: MutableMap = mutableMapOf() private val _lastConnectTimesMdns: MutableMap = mutableMapOf() private val _lastConnectTimesIp: MutableMap = mutableMapOf() + private var _serverStarted = false //TODO: Should sync mdns and casting mdns be merged? //TODO: Decrease interval that devices are updated //TODO: Send less data @@ -91,6 +93,117 @@ class StateSync { private var _threadRelay: Thread? = null private val _remotePendingStatusUpdate = mutableMapOf Unit>() private var _nsdManager: NsdManager? = null + private var _discoveryListener: NsdManager.DiscoveryListener = object : NsdManager.DiscoveryListener { + override fun onDiscoveryStarted(regType: String) { + Log.d(TAG, "Service discovery started for $regType") + } + + override fun onDiscoveryStopped(serviceType: String) { + Log.i(TAG, "Discovery stopped: $serviceType") + } + + override fun onServiceLost(service: NsdServiceInfo) { + Log.e(TAG, "service lost: $service") + // TODO: Handle service lost, e.g., remove device + } + + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode") + try { + _nsdManager?.stopServiceDiscovery(this) + } catch (e: Throwable) { + Logger.w(TAG, "Failed to stop service discovery", e) + } + } + + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode") + try { + _nsdManager?.stopServiceDiscovery(this) + } catch (e: Throwable) { + Logger.w(TAG, "Failed to stop service discovery", e) + } + } + + fun addOrUpdate(name: String, adrs: Array, port: Int, attributes: Map) { + if (!Settings.instance.synchronization.connectDiscovered) { + return + } + + val urlSafePkey = attributes.get("pk")?.decodeToString() ?: return + val pkey = Base64.getEncoder().encodeToString(Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/'))) + val syncDeviceInfo = SyncDeviceInfo(pkey, adrs.map { it.hostAddress }.toTypedArray(), port, null) + val authorized = isAuthorized(pkey) + + if (authorized && !isConnected(pkey)) { + val now = System.currentTimeMillis() + val lastConnectTime = synchronized(_lastConnectTimesMdns) { + _lastConnectTimesMdns[pkey] ?: 0 + } + + //Connect once every 30 seconds, max + if (now - lastConnectTime > 30000) { + synchronized(_lastConnectTimesMdns) { + _lastConnectTimesMdns[pkey] = now + } + + Logger.i(TAG, "Found device authorized device '${name}' with pkey=$pkey, attempting to connect") + + try { + connect(syncDeviceInfo) + } catch (e: Throwable) { + Logger.i(TAG, "Failed to connect to $pkey", e) + } + } + } + } + + override fun onServiceFound(service: NsdServiceInfo) { + Log.v(TAG, "Service discovery success for ${service.serviceType}: $service") + addOrUpdate(service.serviceName, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + service.hostAddresses.toTypedArray() + } else { + if(service.host != null) + arrayOf(service.host); + else + arrayOf(); + }, service.port, service.attributes) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + _nsdManager?.registerServiceInfoCallback(service, { it.run() }, object : NsdManager.ServiceInfoCallback { + override fun onServiceUpdated(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "onServiceUpdated: $serviceInfo") + addOrUpdate(serviceInfo.serviceName, serviceInfo.hostAddresses.toTypedArray(), serviceInfo.port, serviceInfo.attributes) + } + + override fun onServiceLost() { + Log.v(TAG, "onServiceLost: $service") + // TODO: Handle service lost + } + + override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { + Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode") + } + + override fun onServiceInfoCallbackUnregistered() { + Log.v(TAG, "onServiceInfoCallbackUnregistered") + } + }) + } else { + _nsdManager?.resolveService(service, object : NsdManager.ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.v(TAG, "Resolve failed: $errorCode") + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "Resolve Succeeded: $serviceInfo") + addOrUpdate(serviceInfo.serviceName, arrayOf(serviceInfo.host), serviceInfo.port, serviceInfo.attributes) + } + }) + } + } + } + private val _registrationListener = object : NsdManager.RegistrationListener { override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { Log.v(TAG, "onServiceRegistered: ${serviceInfo.serviceName}") @@ -122,7 +235,7 @@ class StateSync { } } - fun start(context: Context) { + fun start(context: Context, onServerBindFail: () -> Unit) { if (_started) { Logger.i(TAG, "Already started.") return @@ -132,116 +245,7 @@ class StateSync { if (Settings.instance.synchronization.connectDiscovered) { _nsdManager?.apply { - discoverServices("_gsync._tcp", NsdManager.PROTOCOL_DNS_SD, object : NsdManager.DiscoveryListener { - override fun onDiscoveryStarted(regType: String) { - Log.d(TAG, "Service discovery started for $regType") - } - - override fun onDiscoveryStopped(serviceType: String) { - Log.i(TAG, "Discovery stopped: $serviceType") - } - - override fun onServiceLost(service: NsdServiceInfo) { - Log.e(TAG, "service lost: $service") - // TODO: Handle service lost, e.g., remove device - } - - override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { - Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode") - try { - _nsdManager?.stopServiceDiscovery(this) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to stop service discovery", e) - } - } - - override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { - Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode") - try { - _nsdManager?.stopServiceDiscovery(this) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to stop service discovery", e) - } - } - - fun addOrUpdate(name: String, adrs: Array, port: Int, attributes: Map) { - if (!Settings.instance.synchronization.connectDiscovered) { - return - } - - val urlSafePkey = attributes.get("pk")?.decodeToString() ?: return - val pkey = Base64.getEncoder().encodeToString(Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/'))) - val syncDeviceInfo = SyncDeviceInfo(pkey, adrs.map { it.hostAddress }.toTypedArray(), port, null) - val authorized = isAuthorized(pkey) - - if (authorized && !isConnected(pkey)) { - val now = System.currentTimeMillis() - val lastConnectTime = synchronized(_lastConnectTimesMdns) { - _lastConnectTimesMdns[pkey] ?: 0 - } - - //Connect once every 30 seconds, max - if (now - lastConnectTime > 30000) { - synchronized(_lastConnectTimesMdns) { - _lastConnectTimesMdns[pkey] = now - } - - Logger.i(TAG, "Found device authorized device '${name}' with pkey=$pkey, attempting to connect") - - try { - connect(syncDeviceInfo) - } catch (e: Throwable) { - Logger.i(TAG, "Failed to connect to $pkey", e) - } - } - } - } - - override fun onServiceFound(service: NsdServiceInfo) { - Log.v(TAG, "Service discovery success for ${service.serviceType}: $service") - addOrUpdate(service.serviceName, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - service.hostAddresses.toTypedArray() - } else { - if(service.host != null) - arrayOf(service.host); - else - arrayOf(); - }, service.port, service.attributes) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - _nsdManager?.registerServiceInfoCallback(service, { it.run() }, object : NsdManager.ServiceInfoCallback { - override fun onServiceUpdated(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "onServiceUpdated: $serviceInfo") - addOrUpdate(serviceInfo.serviceName, serviceInfo.hostAddresses.toTypedArray(), serviceInfo.port, serviceInfo.attributes) - } - - override fun onServiceLost() { - Log.v(TAG, "onServiceLost: $service") - // TODO: Handle service lost - } - - override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { - Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode") - } - - override fun onServiceInfoCallbackUnregistered() { - Log.v(TAG, "onServiceInfoCallbackUnregistered") - } - }) - } else { - _nsdManager?.resolveService(service, object : NsdManager.ResolveListener { - override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { - Log.v(TAG, "Resolve failed: $errorCode") - } - - override fun onServiceResolved(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "Resolve Succeeded: $serviceInfo") - addOrUpdate(serviceInfo.serviceName, arrayOf(serviceInfo.host), serviceInfo.port, serviceInfo.attributes) - } - }) - } - } - }) + discoverServices("_gsync._tcp", NsdManager.PROTOCOL_DNS_SD, _discoveryListener) } } @@ -292,6 +296,7 @@ class StateSync { Logger.i(TAG, "Sync key pair initialized (public key = ${publicKey})") + _serverStarted = true _thread = Thread { try { val serverSocket = ServerSocket(PORT) @@ -305,8 +310,13 @@ class StateSync { session.startAsResponder() } } catch (e: Throwable) { + _serverStarted = false Logger.e(TAG, "Failed to bind server socket to port ${PORT}", e) - UIDialogs.toast("Failed to start sync, port in use") + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + onServerBindFail.invoke() + } + } finally { + _serverStarted = false } }.apply { start() } @@ -479,6 +489,31 @@ class StateSync { } } + fun showFailedToBindDialogIfNecessary(context: Context) { + if (!_serverStarted) { + try { + UIDialogs.showDialogOk(context, R.drawable.ic_warning, "Local discovery unavailable, port was in use") + } catch (e: Throwable) { + //Ignored + } + } + } + + fun confirmStarted(context: Context, onStarted: () -> Unit, onNotStarted: () -> Unit, onServerBindFail: () -> Unit) { + if (!_started) { + UIDialogs.showConfirmationDialog(context, "Sync has not been enabled yet, would you like to enable sync?", { + Settings.instance.synchronization.enabled = true + StateSync.instance.start(context, onServerBindFail) + Settings.instance.save() + onStarted.invoke() + }, { + onNotStarted.invoke() + }) + } else { + onStarted.invoke() + } + } + private fun getDeviceName(): String { val manufacturer = Build.MANUFACTURER.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } val model = Build.MODEL @@ -1034,19 +1069,31 @@ class StateSync { fun stop() { _started = false - _nsdManager?.unregisterService(_registrationListener) + try { + _nsdManager?.stopServiceDiscovery(_discoveryListener) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to stop discovery listener", e) + } + + try { + _nsdManager?.unregisterService(_registrationListener) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to unregister service", e) + } + + _relaySession?.stop() _serverSocket?.close() _serverSocket = null - _thread?.interrupt() - _thread = null - _connectThread?.interrupt() - _connectThread = null - _threadRelay?.interrupt() - _threadRelay = null + synchronized(_sessions) { + _sessions.values.forEach { it.close() } + _sessions.clear() + } - _relaySession?.stop() + _thread = null + _connectThread = null + _threadRelay = null _relaySession = null }