diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt new file mode 100644 index 0000000..739a105 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt @@ -0,0 +1,171 @@ +package com.casic.br.operationsite.utils + +import android.util.Log +import com.casic.br.operationsite.base.BaseApplication +import com.pengxh.kt.lite.extensions.show +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class TcpClient(private val listener: OnTcpConnectStateListener) { + + private val kTag = "TcpClient" + private val reconnectDelay = 15L + private var maxRetryTimes = 10 // 设置最大重连次数 + private var needReconnect = false + private var bootStrap: Bootstrap = Bootstrap() + private var loopGroup: EventLoopGroup = NioEventLoopGroup() + private lateinit var host: String + private var port: Int = 0 + private var channel: Channel? = null + private var scope: CoroutineScope? = null + + @Volatile + private var isRunning = AtomicBoolean(false) + + @Volatile + private var retryTimes = AtomicInteger(0) + + init { + bootStrap.group(loopGroup) + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler(SimpleChannelInitializer()) + } + + /** + * TcpClient 是否正在运行 + * */ + fun isRunning(): Boolean { + return isRunning.get() + } + + fun start(host: String, port: Int) { + this.host = host + this.port = port + if (isRunning.get()) { + return + } + connect() + } + + private inner class SimpleChannelInitializer : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val channelPipeline = ch.pipeline() + channelPipeline + .addLast(ByteArrayDecoder()) + .addLast(ByteArrayEncoder()) + .addLast(IdleStateHandler(15, 15, 60, TimeUnit.SECONDS))//如果连接没有接收或发送数据超过60秒钟就发送一次心跳 + .addLast(object : SimpleChannelInboundHandler() { + override fun channelActive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已连接") + listener.onConnected() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已断开") + listener.onDisconnected() + if (needReconnect) { + reconnect() + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteArray?) { + listener.onMessageReceived(msg) + } + + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + Log.d(kTag, "exceptionCaught: ${cause.message}") + listener.onConnectFailed() + ctx.close() + } + }) + } + } + + @Synchronized + private fun connect() { + if (channel != null && channel!!.isActive) { + return + } + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope?.launch(Dispatchers.IO) { + try { + Log.d(kTag, "start connect: ${host}:${port}") + val channelFuture = bootStrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + isRunning.set(true) + retryTimes.set(0) + channel = channelFuture.channel() + } + } + }).sync() + channelFuture.channel().closeFuture().sync() + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + reconnect() + } + } + } + + private fun reconnect() { + val currentRetryTimes = retryTimes.incrementAndGet() + if (currentRetryTimes <= maxRetryTimes) { + scope?.launch(Dispatchers.Main) { + "开始第 $currentRetryTimes 次重连".show(BaseApplication.get()) + } + loopGroup.schedule({ connect() }, reconnectDelay, TimeUnit.SECONDS) + } else { + Log.e(kTag, "达到最大重连次数,停止重连") + listener.onConnectFailed() + } + } + + fun stop(needReconnect: Boolean) { + this.needReconnect = needReconnect + isRunning.set(false) + channel?.close() + scope?.cancel() + } + + fun sendMessage(bytes: ByteArray) { + if (!isRunning.get()) { + return + } + channel?.writeAndFlush(bytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt new file mode 100644 index 0000000..739a105 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt @@ -0,0 +1,171 @@ +package com.casic.br.operationsite.utils + +import android.util.Log +import com.casic.br.operationsite.base.BaseApplication +import com.pengxh.kt.lite.extensions.show +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class TcpClient(private val listener: OnTcpConnectStateListener) { + + private val kTag = "TcpClient" + private val reconnectDelay = 15L + private var maxRetryTimes = 10 // 设置最大重连次数 + private var needReconnect = false + private var bootStrap: Bootstrap = Bootstrap() + private var loopGroup: EventLoopGroup = NioEventLoopGroup() + private lateinit var host: String + private var port: Int = 0 + private var channel: Channel? = null + private var scope: CoroutineScope? = null + + @Volatile + private var isRunning = AtomicBoolean(false) + + @Volatile + private var retryTimes = AtomicInteger(0) + + init { + bootStrap.group(loopGroup) + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler(SimpleChannelInitializer()) + } + + /** + * TcpClient 是否正在运行 + * */ + fun isRunning(): Boolean { + return isRunning.get() + } + + fun start(host: String, port: Int) { + this.host = host + this.port = port + if (isRunning.get()) { + return + } + connect() + } + + private inner class SimpleChannelInitializer : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val channelPipeline = ch.pipeline() + channelPipeline + .addLast(ByteArrayDecoder()) + .addLast(ByteArrayEncoder()) + .addLast(IdleStateHandler(15, 15, 60, TimeUnit.SECONDS))//如果连接没有接收或发送数据超过60秒钟就发送一次心跳 + .addLast(object : SimpleChannelInboundHandler() { + override fun channelActive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已连接") + listener.onConnected() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已断开") + listener.onDisconnected() + if (needReconnect) { + reconnect() + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteArray?) { + listener.onMessageReceived(msg) + } + + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + Log.d(kTag, "exceptionCaught: ${cause.message}") + listener.onConnectFailed() + ctx.close() + } + }) + } + } + + @Synchronized + private fun connect() { + if (channel != null && channel!!.isActive) { + return + } + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope?.launch(Dispatchers.IO) { + try { + Log.d(kTag, "start connect: ${host}:${port}") + val channelFuture = bootStrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + isRunning.set(true) + retryTimes.set(0) + channel = channelFuture.channel() + } + } + }).sync() + channelFuture.channel().closeFuture().sync() + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + reconnect() + } + } + } + + private fun reconnect() { + val currentRetryTimes = retryTimes.incrementAndGet() + if (currentRetryTimes <= maxRetryTimes) { + scope?.launch(Dispatchers.Main) { + "开始第 $currentRetryTimes 次重连".show(BaseApplication.get()) + } + loopGroup.schedule({ connect() }, reconnectDelay, TimeUnit.SECONDS) + } else { + Log.e(kTag, "达到最大重连次数,停止重连") + listener.onConnectFailed() + } + } + + fun stop(needReconnect: Boolean) { + this.needReconnect = needReconnect + isRunning.set(false) + channel?.close() + scope?.cancel() + } + + fun sendMessage(bytes: ByteArray) { + if (!isRunning.get()) { + return + } + channel?.writeAndFlush(bytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt index 99b5c4e..d7fd789 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt @@ -4,14 +4,10 @@ import android.os.Bundle import android.os.Handler import android.os.Message -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.AppCompatEditText import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.TabPageViewAdapter import com.casic.br.operationsite.databinding.ActivityDeviceControlBinding @@ -25,17 +21,11 @@ import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.VideoPlayerManager import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.convertColor import com.pengxh.kt.lite.extensions.getScreenWidth -import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.TitleBarView -import com.pengxh.kt.lite.widget.dialog.BottomActionSheet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch class DeviceControlActivity : KotlinBaseActivity(), Handler.Callback { @@ -43,11 +33,9 @@ lateinit var weakReferenceHandler: WeakReferenceHandler } - private val context = this private val fragmentPages by lazy { ArrayList() } private val socketService by lazy { Intent(this, SocketConnectionService::class.java) } private val pageTitles = arrayOf("相机控制", "激光监测", "语音设置", "基础配置") - private val options = arrayListOf("修改安全树视频相机IP地址", "修改安全树数据通信IP地址") private var isRtspPlaying = false init { @@ -118,18 +106,7 @@ } override fun onRightClick() { - BottomActionSheet.Builder() - .setContext(context) - .setActionItemTitle(options) - .setItemTextColor(R.color.mainThemeColor.convertColor(context)) - .setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener { - override fun onActionItemClick(position: Int) { - when (position) { - 0 -> modifyCameraIp() - 1 -> modifySocketIp() - } - } - }).build().show() + } }) } @@ -137,83 +114,4 @@ override fun initEvent() { } - - /** - * rtsp流IP地址,默认是192.168.10.137 - * */ - private fun modifyCameraIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_camera_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, value) - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 板子tcp通信IP地址,默认是192.168.10.51 - * */ - private fun modifySocketIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_socket_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, value) - //设置通信IP之后需要重启服务 - lifecycleScope.launch(Dispatchers.IO) { - stopService(socketService) - - delay(1000) - - startService(socketService) - } - disconnectCurrentWiFi() - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 跳转WiFi连接界面 - * */ - private fun disconnectCurrentWiFi() { - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setMessage("数据通信IP地址已更改,请手动切换到相应WiFi") - .setIcon(R.mipmap.ic_launcher) - .setCancelable(false) - .setPositiveButton("好的") { _, _ -> - startActivity(Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)) - }.create().show() - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt new file mode 100644 index 0000000..739a105 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt @@ -0,0 +1,171 @@ +package com.casic.br.operationsite.utils + +import android.util.Log +import com.casic.br.operationsite.base.BaseApplication +import com.pengxh.kt.lite.extensions.show +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class TcpClient(private val listener: OnTcpConnectStateListener) { + + private val kTag = "TcpClient" + private val reconnectDelay = 15L + private var maxRetryTimes = 10 // 设置最大重连次数 + private var needReconnect = false + private var bootStrap: Bootstrap = Bootstrap() + private var loopGroup: EventLoopGroup = NioEventLoopGroup() + private lateinit var host: String + private var port: Int = 0 + private var channel: Channel? = null + private var scope: CoroutineScope? = null + + @Volatile + private var isRunning = AtomicBoolean(false) + + @Volatile + private var retryTimes = AtomicInteger(0) + + init { + bootStrap.group(loopGroup) + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler(SimpleChannelInitializer()) + } + + /** + * TcpClient 是否正在运行 + * */ + fun isRunning(): Boolean { + return isRunning.get() + } + + fun start(host: String, port: Int) { + this.host = host + this.port = port + if (isRunning.get()) { + return + } + connect() + } + + private inner class SimpleChannelInitializer : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val channelPipeline = ch.pipeline() + channelPipeline + .addLast(ByteArrayDecoder()) + .addLast(ByteArrayEncoder()) + .addLast(IdleStateHandler(15, 15, 60, TimeUnit.SECONDS))//如果连接没有接收或发送数据超过60秒钟就发送一次心跳 + .addLast(object : SimpleChannelInboundHandler() { + override fun channelActive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已连接") + listener.onConnected() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已断开") + listener.onDisconnected() + if (needReconnect) { + reconnect() + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteArray?) { + listener.onMessageReceived(msg) + } + + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + Log.d(kTag, "exceptionCaught: ${cause.message}") + listener.onConnectFailed() + ctx.close() + } + }) + } + } + + @Synchronized + private fun connect() { + if (channel != null && channel!!.isActive) { + return + } + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope?.launch(Dispatchers.IO) { + try { + Log.d(kTag, "start connect: ${host}:${port}") + val channelFuture = bootStrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + isRunning.set(true) + retryTimes.set(0) + channel = channelFuture.channel() + } + } + }).sync() + channelFuture.channel().closeFuture().sync() + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + reconnect() + } + } + } + + private fun reconnect() { + val currentRetryTimes = retryTimes.incrementAndGet() + if (currentRetryTimes <= maxRetryTimes) { + scope?.launch(Dispatchers.Main) { + "开始第 $currentRetryTimes 次重连".show(BaseApplication.get()) + } + loopGroup.schedule({ connect() }, reconnectDelay, TimeUnit.SECONDS) + } else { + Log.e(kTag, "达到最大重连次数,停止重连") + listener.onConnectFailed() + } + } + + fun stop(needReconnect: Boolean) { + this.needReconnect = needReconnect + isRunning.set(false) + channel?.close() + scope?.cancel() + } + + fun sendMessage(bytes: ByteArray) { + if (!isRunning.get()) { + return + } + channel?.writeAndFlush(bytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt index 99b5c4e..d7fd789 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt @@ -4,14 +4,10 @@ import android.os.Bundle import android.os.Handler import android.os.Message -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.AppCompatEditText import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.TabPageViewAdapter import com.casic.br.operationsite.databinding.ActivityDeviceControlBinding @@ -25,17 +21,11 @@ import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.VideoPlayerManager import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.convertColor import com.pengxh.kt.lite.extensions.getScreenWidth -import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.TitleBarView -import com.pengxh.kt.lite.widget.dialog.BottomActionSheet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch class DeviceControlActivity : KotlinBaseActivity(), Handler.Callback { @@ -43,11 +33,9 @@ lateinit var weakReferenceHandler: WeakReferenceHandler } - private val context = this private val fragmentPages by lazy { ArrayList() } private val socketService by lazy { Intent(this, SocketConnectionService::class.java) } private val pageTitles = arrayOf("相机控制", "激光监测", "语音设置", "基础配置") - private val options = arrayListOf("修改安全树视频相机IP地址", "修改安全树数据通信IP地址") private var isRtspPlaying = false init { @@ -118,18 +106,7 @@ } override fun onRightClick() { - BottomActionSheet.Builder() - .setContext(context) - .setActionItemTitle(options) - .setItemTextColor(R.color.mainThemeColor.convertColor(context)) - .setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener { - override fun onActionItemClick(position: Int) { - when (position) { - 0 -> modifyCameraIp() - 1 -> modifySocketIp() - } - } - }).build().show() + } }) } @@ -137,83 +114,4 @@ override fun initEvent() { } - - /** - * rtsp流IP地址,默认是192.168.10.137 - * */ - private fun modifyCameraIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_camera_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, value) - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 板子tcp通信IP地址,默认是192.168.10.51 - * */ - private fun modifySocketIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_socket_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, value) - //设置通信IP之后需要重启服务 - lifecycleScope.launch(Dispatchers.IO) { - stopService(socketService) - - delay(1000) - - startService(socketService) - } - disconnectCurrentWiFi() - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 跳转WiFi连接界面 - * */ - private fun disconnectCurrentWiFi() { - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setMessage("数据通信IP地址已更改,请手动切换到相应WiFi") - .setIcon(R.mipmap.ic_launcher) - .setCancelable(false) - .setPositiveButton("好的") { _, _ -> - startActivity(Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)) - }.create().show() - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt index 5c6aafd..cf9708b 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt @@ -8,6 +8,8 @@ import android.view.View import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.ViewModelProvider import com.amap.api.maps.AMap import com.amap.api.maps.CameraUpdateFactory @@ -24,6 +26,7 @@ import com.casic.br.operationsite.model.WorkSiteDeviceModel import com.casic.br.operationsite.model.WorkSiteWorkerModel import com.casic.br.operationsite.utils.AuthenticationHelper +import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.RuntimeCache import com.casic.br.operationsite.vm.LoginViewModel import com.casic.br.operationsite.vm.TubeViewModel @@ -43,6 +46,7 @@ import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.LoadState import com.pengxh.kt.lite.utils.LoadingDialog +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.widget.TitleBarView import com.pengxh.kt.lite.widget.dialog.AlertControlDialog @@ -466,7 +470,37 @@ } binding.deviceMenuItem.setOnClickListener { - navigatePageTo() + val cameraOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String + val socketOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String + + val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_config, null) + val cameraIpView = view.findViewById(R.id.cameraIpView) + val socketIpView = view.findViewById(R.id.socketIpView) + cameraIpView.setText(cameraOldIp) + socketIpView.setText(socketOldIp) + + AlertDialog.Builder(this) + .setTitle(getString(R.string.app_name)) + .setMessage("请确定设备控制相关配置") + .setIcon(R.mipmap.ic_launcher) + .setView(view) + .setCancelable(false) + .setNegativeButton("取消") { _, _ -> } + .setPositiveButton("确定") { _, _ -> + val cameraIp = cameraIpView.text.toString() + val socketIp = socketIpView.text.toString() + if (cameraIp.isBlank() || socketIp.isBlank()) { + "什么都还没输入呢!".show(context) + return@setPositiveButton + } + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, cameraIp) + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, socketIp) + + //设置好了之后才跳转设备控制,确保设备能连上最新的配置 + navigatePageTo() + }.create().show() } binding.installMenuItem.setOnClickListener { diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt new file mode 100644 index 0000000..739a105 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt @@ -0,0 +1,171 @@ +package com.casic.br.operationsite.utils + +import android.util.Log +import com.casic.br.operationsite.base.BaseApplication +import com.pengxh.kt.lite.extensions.show +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class TcpClient(private val listener: OnTcpConnectStateListener) { + + private val kTag = "TcpClient" + private val reconnectDelay = 15L + private var maxRetryTimes = 10 // 设置最大重连次数 + private var needReconnect = false + private var bootStrap: Bootstrap = Bootstrap() + private var loopGroup: EventLoopGroup = NioEventLoopGroup() + private lateinit var host: String + private var port: Int = 0 + private var channel: Channel? = null + private var scope: CoroutineScope? = null + + @Volatile + private var isRunning = AtomicBoolean(false) + + @Volatile + private var retryTimes = AtomicInteger(0) + + init { + bootStrap.group(loopGroup) + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler(SimpleChannelInitializer()) + } + + /** + * TcpClient 是否正在运行 + * */ + fun isRunning(): Boolean { + return isRunning.get() + } + + fun start(host: String, port: Int) { + this.host = host + this.port = port + if (isRunning.get()) { + return + } + connect() + } + + private inner class SimpleChannelInitializer : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val channelPipeline = ch.pipeline() + channelPipeline + .addLast(ByteArrayDecoder()) + .addLast(ByteArrayEncoder()) + .addLast(IdleStateHandler(15, 15, 60, TimeUnit.SECONDS))//如果连接没有接收或发送数据超过60秒钟就发送一次心跳 + .addLast(object : SimpleChannelInboundHandler() { + override fun channelActive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已连接") + listener.onConnected() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已断开") + listener.onDisconnected() + if (needReconnect) { + reconnect() + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteArray?) { + listener.onMessageReceived(msg) + } + + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + Log.d(kTag, "exceptionCaught: ${cause.message}") + listener.onConnectFailed() + ctx.close() + } + }) + } + } + + @Synchronized + private fun connect() { + if (channel != null && channel!!.isActive) { + return + } + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope?.launch(Dispatchers.IO) { + try { + Log.d(kTag, "start connect: ${host}:${port}") + val channelFuture = bootStrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + isRunning.set(true) + retryTimes.set(0) + channel = channelFuture.channel() + } + } + }).sync() + channelFuture.channel().closeFuture().sync() + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + reconnect() + } + } + } + + private fun reconnect() { + val currentRetryTimes = retryTimes.incrementAndGet() + if (currentRetryTimes <= maxRetryTimes) { + scope?.launch(Dispatchers.Main) { + "开始第 $currentRetryTimes 次重连".show(BaseApplication.get()) + } + loopGroup.schedule({ connect() }, reconnectDelay, TimeUnit.SECONDS) + } else { + Log.e(kTag, "达到最大重连次数,停止重连") + listener.onConnectFailed() + } + } + + fun stop(needReconnect: Boolean) { + this.needReconnect = needReconnect + isRunning.set(false) + channel?.close() + scope?.cancel() + } + + fun sendMessage(bytes: ByteArray) { + if (!isRunning.get()) { + return + } + channel?.writeAndFlush(bytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt index 99b5c4e..d7fd789 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt @@ -4,14 +4,10 @@ import android.os.Bundle import android.os.Handler import android.os.Message -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.AppCompatEditText import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.TabPageViewAdapter import com.casic.br.operationsite.databinding.ActivityDeviceControlBinding @@ -25,17 +21,11 @@ import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.VideoPlayerManager import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.convertColor import com.pengxh.kt.lite.extensions.getScreenWidth -import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.TitleBarView -import com.pengxh.kt.lite.widget.dialog.BottomActionSheet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch class DeviceControlActivity : KotlinBaseActivity(), Handler.Callback { @@ -43,11 +33,9 @@ lateinit var weakReferenceHandler: WeakReferenceHandler } - private val context = this private val fragmentPages by lazy { ArrayList() } private val socketService by lazy { Intent(this, SocketConnectionService::class.java) } private val pageTitles = arrayOf("相机控制", "激光监测", "语音设置", "基础配置") - private val options = arrayListOf("修改安全树视频相机IP地址", "修改安全树数据通信IP地址") private var isRtspPlaying = false init { @@ -118,18 +106,7 @@ } override fun onRightClick() { - BottomActionSheet.Builder() - .setContext(context) - .setActionItemTitle(options) - .setItemTextColor(R.color.mainThemeColor.convertColor(context)) - .setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener { - override fun onActionItemClick(position: Int) { - when (position) { - 0 -> modifyCameraIp() - 1 -> modifySocketIp() - } - } - }).build().show() + } }) } @@ -137,83 +114,4 @@ override fun initEvent() { } - - /** - * rtsp流IP地址,默认是192.168.10.137 - * */ - private fun modifyCameraIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_camera_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, value) - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 板子tcp通信IP地址,默认是192.168.10.51 - * */ - private fun modifySocketIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_socket_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, value) - //设置通信IP之后需要重启服务 - lifecycleScope.launch(Dispatchers.IO) { - stopService(socketService) - - delay(1000) - - startService(socketService) - } - disconnectCurrentWiFi() - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 跳转WiFi连接界面 - * */ - private fun disconnectCurrentWiFi() { - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setMessage("数据通信IP地址已更改,请手动切换到相应WiFi") - .setIcon(R.mipmap.ic_launcher) - .setCancelable(false) - .setPositiveButton("好的") { _, _ -> - startActivity(Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)) - }.create().show() - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt index 5c6aafd..cf9708b 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt @@ -8,6 +8,8 @@ import android.view.View import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.ViewModelProvider import com.amap.api.maps.AMap import com.amap.api.maps.CameraUpdateFactory @@ -24,6 +26,7 @@ import com.casic.br.operationsite.model.WorkSiteDeviceModel import com.casic.br.operationsite.model.WorkSiteWorkerModel import com.casic.br.operationsite.utils.AuthenticationHelper +import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.RuntimeCache import com.casic.br.operationsite.vm.LoginViewModel import com.casic.br.operationsite.vm.TubeViewModel @@ -43,6 +46,7 @@ import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.LoadState import com.pengxh.kt.lite.utils.LoadingDialog +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.widget.TitleBarView import com.pengxh.kt.lite.widget.dialog.AlertControlDialog @@ -466,7 +470,37 @@ } binding.deviceMenuItem.setOnClickListener { - navigatePageTo() + val cameraOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String + val socketOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String + + val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_config, null) + val cameraIpView = view.findViewById(R.id.cameraIpView) + val socketIpView = view.findViewById(R.id.socketIpView) + cameraIpView.setText(cameraOldIp) + socketIpView.setText(socketOldIp) + + AlertDialog.Builder(this) + .setTitle(getString(R.string.app_name)) + .setMessage("请确定设备控制相关配置") + .setIcon(R.mipmap.ic_launcher) + .setView(view) + .setCancelable(false) + .setNegativeButton("取消") { _, _ -> } + .setPositiveButton("确定") { _, _ -> + val cameraIp = cameraIpView.text.toString() + val socketIp = socketIpView.text.toString() + if (cameraIp.isBlank() || socketIp.isBlank()) { + "什么都还没输入呢!".show(context) + return@setPositiveButton + } + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, cameraIp) + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, socketIp) + + //设置好了之后才跳转设备控制,确保设备能连上最新的配置 + navigatePageTo() + }.create().show() } binding.installMenuItem.setOnClickListener { diff --git a/app/src/main/res/layout/activity_device_control.xml b/app/src/main/res/layout/activity_device_control.xml index e309c27..50df033 100644 --- a/app/src/main/res/layout/activity_device_control.xml +++ b/app/src/main/res/layout/activity_device_control.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:background="@color/mainThemeColor" app:tbv_show_left_image="false" - app:tbv_show_right_image="true" + app:tbv_show_right_image="false" app:tbv_smaller_title="true" app:tbv_text="@string/app_name" /> diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt new file mode 100644 index 0000000..739a105 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt @@ -0,0 +1,171 @@ +package com.casic.br.operationsite.utils + +import android.util.Log +import com.casic.br.operationsite.base.BaseApplication +import com.pengxh.kt.lite.extensions.show +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class TcpClient(private val listener: OnTcpConnectStateListener) { + + private val kTag = "TcpClient" + private val reconnectDelay = 15L + private var maxRetryTimes = 10 // 设置最大重连次数 + private var needReconnect = false + private var bootStrap: Bootstrap = Bootstrap() + private var loopGroup: EventLoopGroup = NioEventLoopGroup() + private lateinit var host: String + private var port: Int = 0 + private var channel: Channel? = null + private var scope: CoroutineScope? = null + + @Volatile + private var isRunning = AtomicBoolean(false) + + @Volatile + private var retryTimes = AtomicInteger(0) + + init { + bootStrap.group(loopGroup) + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler(SimpleChannelInitializer()) + } + + /** + * TcpClient 是否正在运行 + * */ + fun isRunning(): Boolean { + return isRunning.get() + } + + fun start(host: String, port: Int) { + this.host = host + this.port = port + if (isRunning.get()) { + return + } + connect() + } + + private inner class SimpleChannelInitializer : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val channelPipeline = ch.pipeline() + channelPipeline + .addLast(ByteArrayDecoder()) + .addLast(ByteArrayEncoder()) + .addLast(IdleStateHandler(15, 15, 60, TimeUnit.SECONDS))//如果连接没有接收或发送数据超过60秒钟就发送一次心跳 + .addLast(object : SimpleChannelInboundHandler() { + override fun channelActive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已连接") + listener.onConnected() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已断开") + listener.onDisconnected() + if (needReconnect) { + reconnect() + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteArray?) { + listener.onMessageReceived(msg) + } + + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + Log.d(kTag, "exceptionCaught: ${cause.message}") + listener.onConnectFailed() + ctx.close() + } + }) + } + } + + @Synchronized + private fun connect() { + if (channel != null && channel!!.isActive) { + return + } + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope?.launch(Dispatchers.IO) { + try { + Log.d(kTag, "start connect: ${host}:${port}") + val channelFuture = bootStrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + isRunning.set(true) + retryTimes.set(0) + channel = channelFuture.channel() + } + } + }).sync() + channelFuture.channel().closeFuture().sync() + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + reconnect() + } + } + } + + private fun reconnect() { + val currentRetryTimes = retryTimes.incrementAndGet() + if (currentRetryTimes <= maxRetryTimes) { + scope?.launch(Dispatchers.Main) { + "开始第 $currentRetryTimes 次重连".show(BaseApplication.get()) + } + loopGroup.schedule({ connect() }, reconnectDelay, TimeUnit.SECONDS) + } else { + Log.e(kTag, "达到最大重连次数,停止重连") + listener.onConnectFailed() + } + } + + fun stop(needReconnect: Boolean) { + this.needReconnect = needReconnect + isRunning.set(false) + channel?.close() + scope?.cancel() + } + + fun sendMessage(bytes: ByteArray) { + if (!isRunning.get()) { + return + } + channel?.writeAndFlush(bytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt index 99b5c4e..d7fd789 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt @@ -4,14 +4,10 @@ import android.os.Bundle import android.os.Handler import android.os.Message -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.AppCompatEditText import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.TabPageViewAdapter import com.casic.br.operationsite.databinding.ActivityDeviceControlBinding @@ -25,17 +21,11 @@ import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.VideoPlayerManager import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.convertColor import com.pengxh.kt.lite.extensions.getScreenWidth -import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.TitleBarView -import com.pengxh.kt.lite.widget.dialog.BottomActionSheet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch class DeviceControlActivity : KotlinBaseActivity(), Handler.Callback { @@ -43,11 +33,9 @@ lateinit var weakReferenceHandler: WeakReferenceHandler } - private val context = this private val fragmentPages by lazy { ArrayList() } private val socketService by lazy { Intent(this, SocketConnectionService::class.java) } private val pageTitles = arrayOf("相机控制", "激光监测", "语音设置", "基础配置") - private val options = arrayListOf("修改安全树视频相机IP地址", "修改安全树数据通信IP地址") private var isRtspPlaying = false init { @@ -118,18 +106,7 @@ } override fun onRightClick() { - BottomActionSheet.Builder() - .setContext(context) - .setActionItemTitle(options) - .setItemTextColor(R.color.mainThemeColor.convertColor(context)) - .setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener { - override fun onActionItemClick(position: Int) { - when (position) { - 0 -> modifyCameraIp() - 1 -> modifySocketIp() - } - } - }).build().show() + } }) } @@ -137,83 +114,4 @@ override fun initEvent() { } - - /** - * rtsp流IP地址,默认是192.168.10.137 - * */ - private fun modifyCameraIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_camera_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, value) - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 板子tcp通信IP地址,默认是192.168.10.51 - * */ - private fun modifySocketIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_socket_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, value) - //设置通信IP之后需要重启服务 - lifecycleScope.launch(Dispatchers.IO) { - stopService(socketService) - - delay(1000) - - startService(socketService) - } - disconnectCurrentWiFi() - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 跳转WiFi连接界面 - * */ - private fun disconnectCurrentWiFi() { - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setMessage("数据通信IP地址已更改,请手动切换到相应WiFi") - .setIcon(R.mipmap.ic_launcher) - .setCancelable(false) - .setPositiveButton("好的") { _, _ -> - startActivity(Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)) - }.create().show() - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt index 5c6aafd..cf9708b 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt @@ -8,6 +8,8 @@ import android.view.View import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.ViewModelProvider import com.amap.api.maps.AMap import com.amap.api.maps.CameraUpdateFactory @@ -24,6 +26,7 @@ import com.casic.br.operationsite.model.WorkSiteDeviceModel import com.casic.br.operationsite.model.WorkSiteWorkerModel import com.casic.br.operationsite.utils.AuthenticationHelper +import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.RuntimeCache import com.casic.br.operationsite.vm.LoginViewModel import com.casic.br.operationsite.vm.TubeViewModel @@ -43,6 +46,7 @@ import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.LoadState import com.pengxh.kt.lite.utils.LoadingDialog +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.widget.TitleBarView import com.pengxh.kt.lite.widget.dialog.AlertControlDialog @@ -466,7 +470,37 @@ } binding.deviceMenuItem.setOnClickListener { - navigatePageTo() + val cameraOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String + val socketOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String + + val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_config, null) + val cameraIpView = view.findViewById(R.id.cameraIpView) + val socketIpView = view.findViewById(R.id.socketIpView) + cameraIpView.setText(cameraOldIp) + socketIpView.setText(socketOldIp) + + AlertDialog.Builder(this) + .setTitle(getString(R.string.app_name)) + .setMessage("请确定设备控制相关配置") + .setIcon(R.mipmap.ic_launcher) + .setView(view) + .setCancelable(false) + .setNegativeButton("取消") { _, _ -> } + .setPositiveButton("确定") { _, _ -> + val cameraIp = cameraIpView.text.toString() + val socketIp = socketIpView.text.toString() + if (cameraIp.isBlank() || socketIp.isBlank()) { + "什么都还没输入呢!".show(context) + return@setPositiveButton + } + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, cameraIp) + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, socketIp) + + //设置好了之后才跳转设备控制,确保设备能连上最新的配置 + navigatePageTo() + }.create().show() } binding.installMenuItem.setOnClickListener { diff --git a/app/src/main/res/layout/activity_device_control.xml b/app/src/main/res/layout/activity_device_control.xml index e309c27..50df033 100644 --- a/app/src/main/res/layout/activity_device_control.xml +++ b/app/src/main/res/layout/activity_device_control.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:background="@color/mainThemeColor" app:tbv_show_left_image="false" - app:tbv_show_right_image="true" + app:tbv_show_right_image="false" app:tbv_smaller_title="true" app:tbv_text="@string/app_name" /> diff --git a/app/src/main/res/layout/dialog_input_camera_ip.xml b/app/src/main/res/layout/dialog_input_camera_ip.xml deleted file mode 100644 index 6ccb7ae..0000000 --- a/app/src/main/res/layout/dialog_input_camera_ip.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt new file mode 100644 index 0000000..739a105 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt @@ -0,0 +1,171 @@ +package com.casic.br.operationsite.utils + +import android.util.Log +import com.casic.br.operationsite.base.BaseApplication +import com.pengxh.kt.lite.extensions.show +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class TcpClient(private val listener: OnTcpConnectStateListener) { + + private val kTag = "TcpClient" + private val reconnectDelay = 15L + private var maxRetryTimes = 10 // 设置最大重连次数 + private var needReconnect = false + private var bootStrap: Bootstrap = Bootstrap() + private var loopGroup: EventLoopGroup = NioEventLoopGroup() + private lateinit var host: String + private var port: Int = 0 + private var channel: Channel? = null + private var scope: CoroutineScope? = null + + @Volatile + private var isRunning = AtomicBoolean(false) + + @Volatile + private var retryTimes = AtomicInteger(0) + + init { + bootStrap.group(loopGroup) + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler(SimpleChannelInitializer()) + } + + /** + * TcpClient 是否正在运行 + * */ + fun isRunning(): Boolean { + return isRunning.get() + } + + fun start(host: String, port: Int) { + this.host = host + this.port = port + if (isRunning.get()) { + return + } + connect() + } + + private inner class SimpleChannelInitializer : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val channelPipeline = ch.pipeline() + channelPipeline + .addLast(ByteArrayDecoder()) + .addLast(ByteArrayEncoder()) + .addLast(IdleStateHandler(15, 15, 60, TimeUnit.SECONDS))//如果连接没有接收或发送数据超过60秒钟就发送一次心跳 + .addLast(object : SimpleChannelInboundHandler() { + override fun channelActive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已连接") + listener.onConnected() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已断开") + listener.onDisconnected() + if (needReconnect) { + reconnect() + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteArray?) { + listener.onMessageReceived(msg) + } + + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + Log.d(kTag, "exceptionCaught: ${cause.message}") + listener.onConnectFailed() + ctx.close() + } + }) + } + } + + @Synchronized + private fun connect() { + if (channel != null && channel!!.isActive) { + return + } + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope?.launch(Dispatchers.IO) { + try { + Log.d(kTag, "start connect: ${host}:${port}") + val channelFuture = bootStrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + isRunning.set(true) + retryTimes.set(0) + channel = channelFuture.channel() + } + } + }).sync() + channelFuture.channel().closeFuture().sync() + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + reconnect() + } + } + } + + private fun reconnect() { + val currentRetryTimes = retryTimes.incrementAndGet() + if (currentRetryTimes <= maxRetryTimes) { + scope?.launch(Dispatchers.Main) { + "开始第 $currentRetryTimes 次重连".show(BaseApplication.get()) + } + loopGroup.schedule({ connect() }, reconnectDelay, TimeUnit.SECONDS) + } else { + Log.e(kTag, "达到最大重连次数,停止重连") + listener.onConnectFailed() + } + } + + fun stop(needReconnect: Boolean) { + this.needReconnect = needReconnect + isRunning.set(false) + channel?.close() + scope?.cancel() + } + + fun sendMessage(bytes: ByteArray) { + if (!isRunning.get()) { + return + } + channel?.writeAndFlush(bytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt index 99b5c4e..d7fd789 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt @@ -4,14 +4,10 @@ import android.os.Bundle import android.os.Handler import android.os.Message -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.AppCompatEditText import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.TabPageViewAdapter import com.casic.br.operationsite.databinding.ActivityDeviceControlBinding @@ -25,17 +21,11 @@ import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.VideoPlayerManager import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.convertColor import com.pengxh.kt.lite.extensions.getScreenWidth -import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.TitleBarView -import com.pengxh.kt.lite.widget.dialog.BottomActionSheet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch class DeviceControlActivity : KotlinBaseActivity(), Handler.Callback { @@ -43,11 +33,9 @@ lateinit var weakReferenceHandler: WeakReferenceHandler } - private val context = this private val fragmentPages by lazy { ArrayList() } private val socketService by lazy { Intent(this, SocketConnectionService::class.java) } private val pageTitles = arrayOf("相机控制", "激光监测", "语音设置", "基础配置") - private val options = arrayListOf("修改安全树视频相机IP地址", "修改安全树数据通信IP地址") private var isRtspPlaying = false init { @@ -118,18 +106,7 @@ } override fun onRightClick() { - BottomActionSheet.Builder() - .setContext(context) - .setActionItemTitle(options) - .setItemTextColor(R.color.mainThemeColor.convertColor(context)) - .setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener { - override fun onActionItemClick(position: Int) { - when (position) { - 0 -> modifyCameraIp() - 1 -> modifySocketIp() - } - } - }).build().show() + } }) } @@ -137,83 +114,4 @@ override fun initEvent() { } - - /** - * rtsp流IP地址,默认是192.168.10.137 - * */ - private fun modifyCameraIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_camera_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, value) - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 板子tcp通信IP地址,默认是192.168.10.51 - * */ - private fun modifySocketIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_socket_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, value) - //设置通信IP之后需要重启服务 - lifecycleScope.launch(Dispatchers.IO) { - stopService(socketService) - - delay(1000) - - startService(socketService) - } - disconnectCurrentWiFi() - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 跳转WiFi连接界面 - * */ - private fun disconnectCurrentWiFi() { - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setMessage("数据通信IP地址已更改,请手动切换到相应WiFi") - .setIcon(R.mipmap.ic_launcher) - .setCancelable(false) - .setPositiveButton("好的") { _, _ -> - startActivity(Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)) - }.create().show() - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt index 5c6aafd..cf9708b 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt @@ -8,6 +8,8 @@ import android.view.View import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.ViewModelProvider import com.amap.api.maps.AMap import com.amap.api.maps.CameraUpdateFactory @@ -24,6 +26,7 @@ import com.casic.br.operationsite.model.WorkSiteDeviceModel import com.casic.br.operationsite.model.WorkSiteWorkerModel import com.casic.br.operationsite.utils.AuthenticationHelper +import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.RuntimeCache import com.casic.br.operationsite.vm.LoginViewModel import com.casic.br.operationsite.vm.TubeViewModel @@ -43,6 +46,7 @@ import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.LoadState import com.pengxh.kt.lite.utils.LoadingDialog +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.widget.TitleBarView import com.pengxh.kt.lite.widget.dialog.AlertControlDialog @@ -466,7 +470,37 @@ } binding.deviceMenuItem.setOnClickListener { - navigatePageTo() + val cameraOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String + val socketOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String + + val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_config, null) + val cameraIpView = view.findViewById(R.id.cameraIpView) + val socketIpView = view.findViewById(R.id.socketIpView) + cameraIpView.setText(cameraOldIp) + socketIpView.setText(socketOldIp) + + AlertDialog.Builder(this) + .setTitle(getString(R.string.app_name)) + .setMessage("请确定设备控制相关配置") + .setIcon(R.mipmap.ic_launcher) + .setView(view) + .setCancelable(false) + .setNegativeButton("取消") { _, _ -> } + .setPositiveButton("确定") { _, _ -> + val cameraIp = cameraIpView.text.toString() + val socketIp = socketIpView.text.toString() + if (cameraIp.isBlank() || socketIp.isBlank()) { + "什么都还没输入呢!".show(context) + return@setPositiveButton + } + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, cameraIp) + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, socketIp) + + //设置好了之后才跳转设备控制,确保设备能连上最新的配置 + navigatePageTo() + }.create().show() } binding.installMenuItem.setOnClickListener { diff --git a/app/src/main/res/layout/activity_device_control.xml b/app/src/main/res/layout/activity_device_control.xml index e309c27..50df033 100644 --- a/app/src/main/res/layout/activity_device_control.xml +++ b/app/src/main/res/layout/activity_device_control.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:background="@color/mainThemeColor" app:tbv_show_left_image="false" - app:tbv_show_right_image="true" + app:tbv_show_right_image="false" app:tbv_smaller_title="true" app:tbv_text="@string/app_name" /> diff --git a/app/src/main/res/layout/dialog_input_camera_ip.xml b/app/src/main/res/layout/dialog_input_camera_ip.xml deleted file mode 100644 index 6ccb7ae..0000000 --- a/app/src/main/res/layout/dialog_input_camera_ip.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_input_config.xml b/app/src/main/res/layout/dialog_input_config.xml new file mode 100644 index 0000000..5742abc --- /dev/null +++ b/app/src/main/res/layout/dialog_input_config.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt index 7985fa0..392f03b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/ForegroundRunningService.kt @@ -3,7 +3,6 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Handler import android.os.IBinder @@ -25,7 +24,7 @@ override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val name = "${resources.getString(R.string.app_name)}前台服务" val channel = NotificationChannel( "foreground_running_service_channel", name, NotificationManager.IMPORTANCE_HIGH diff --git a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt index 0e2034f..e30152b 100644 --- a/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt +++ b/app/src/main/java/com/casic/br/operationsite/service/SocketConnectionService.kt @@ -14,10 +14,11 @@ import com.casic.br.operationsite.fragments.VoiceSettingsFragment import com.casic.br.operationsite.utils.CommandCreator import com.casic.br.operationsite.utils.LocaleConstant +import com.casic.br.operationsite.utils.OnTcpConnectStateListener +import com.casic.br.operationsite.utils.TcpClient import com.pengxh.kt.lite.extensions.toAsciiCode +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler -import com.pengxh.kt.lite.utils.socket.tcp.OnTcpConnectStateListener -import com.pengxh.kt.lite.utils.socket.tcp.TcpClient class SocketConnectionService : Service(), OnTcpConnectStateListener, Handler.Callback { @@ -151,7 +152,10 @@ override fun onCreate() { super.onCreate() weakReferenceHandler = WeakReferenceHandler(this) - tcpClient.start(LocaleConstant.GAS_BASE_IP, LocaleConstant.TCP_PORT) + val socketOldIp = SaveKeyValues.getValue( + LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "" + ) as String + tcpClient.start(socketOldIp, LocaleConstant.TCP_PORT) Log.d(kTag, "onCreate: SocketConnectionService") } diff --git a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt index 8831760..78e3ace 100644 --- a/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/operationsite/utils/LocaleConstant.kt @@ -48,9 +48,6 @@ const val SERVER_BASE_URL = "http://111.198.10.15:22006" - //一体机DeviceMonitor程序TCP Server IP地址 - const val GAS_BASE_IP = "192.168.10.51" - const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" const val ACCOUNT = "account" const val PASSWORD = "password" diff --git a/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt new file mode 100644 index 0000000..496915d --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/OnTcpConnectStateListener.kt @@ -0,0 +1,8 @@ +package com.casic.br.operationsite.utils + +interface OnTcpConnectStateListener { + fun onConnected() + fun onDisconnected() + fun onConnectFailed() + fun onMessageReceived(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt new file mode 100644 index 0000000..739a105 --- /dev/null +++ b/app/src/main/java/com/casic/br/operationsite/utils/TcpClient.kt @@ -0,0 +1,171 @@ +package com.casic.br.operationsite.utils + +import android.util.Log +import com.casic.br.operationsite.base.BaseApplication +import com.pengxh.kt.lite.extensions.show +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class TcpClient(private val listener: OnTcpConnectStateListener) { + + private val kTag = "TcpClient" + private val reconnectDelay = 15L + private var maxRetryTimes = 10 // 设置最大重连次数 + private var needReconnect = false + private var bootStrap: Bootstrap = Bootstrap() + private var loopGroup: EventLoopGroup = NioEventLoopGroup() + private lateinit var host: String + private var port: Int = 0 + private var channel: Channel? = null + private var scope: CoroutineScope? = null + + @Volatile + private var isRunning = AtomicBoolean(false) + + @Volatile + private var retryTimes = AtomicInteger(0) + + init { + bootStrap.group(loopGroup) + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .handler(SimpleChannelInitializer()) + } + + /** + * TcpClient 是否正在运行 + * */ + fun isRunning(): Boolean { + return isRunning.get() + } + + fun start(host: String, port: Int) { + this.host = host + this.port = port + if (isRunning.get()) { + return + } + connect() + } + + private inner class SimpleChannelInitializer : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + val channelPipeline = ch.pipeline() + channelPipeline + .addLast(ByteArrayDecoder()) + .addLast(ByteArrayEncoder()) + .addLast(IdleStateHandler(15, 15, 60, TimeUnit.SECONDS))//如果连接没有接收或发送数据超过60秒钟就发送一次心跳 + .addLast(object : SimpleChannelInboundHandler() { + override fun channelActive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已连接") + listener.onConnected() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + val address = ctx.channel().remoteAddress() as InetSocketAddress + Log.d(kTag, "${address.address.hostAddress} 已断开") + listener.onDisconnected() + if (needReconnect) { + reconnect() + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteArray?) { + listener.onMessageReceived(msg) + } + + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + Log.d(kTag, "exceptionCaught: ${cause.message}") + listener.onConnectFailed() + ctx.close() + } + }) + } + } + + @Synchronized + private fun connect() { + if (channel != null && channel!!.isActive) { + return + } + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope?.launch(Dispatchers.IO) { + try { + Log.d(kTag, "start connect: ${host}:${port}") + val channelFuture = bootStrap.connect(host, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + isRunning.set(true) + retryTimes.set(0) + channel = channelFuture.channel() + } + } + }).sync() + channelFuture.channel().closeFuture().sync() + } catch (e: InterruptedException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + reconnect() + } + } + } + + private fun reconnect() { + val currentRetryTimes = retryTimes.incrementAndGet() + if (currentRetryTimes <= maxRetryTimes) { + scope?.launch(Dispatchers.Main) { + "开始第 $currentRetryTimes 次重连".show(BaseApplication.get()) + } + loopGroup.schedule({ connect() }, reconnectDelay, TimeUnit.SECONDS) + } else { + Log.e(kTag, "达到最大重连次数,停止重连") + listener.onConnectFailed() + } + } + + fun stop(needReconnect: Boolean) { + this.needReconnect = needReconnect + isRunning.set(false) + channel?.close() + scope?.cancel() + } + + fun sendMessage(bytes: ByteArray) { + if (!isRunning.get()) { + return + } + channel?.writeAndFlush(bytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt index 99b5c4e..d7fd789 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/DeviceControlActivity.kt @@ -4,14 +4,10 @@ import android.os.Bundle import android.os.Handler import android.os.Message -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.AppCompatEditText import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import com.casic.br.operationsite.R import com.casic.br.operationsite.adapter.TabPageViewAdapter import com.casic.br.operationsite.databinding.ActivityDeviceControlBinding @@ -25,17 +21,11 @@ import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.VideoPlayerManager import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.convertColor import com.pengxh.kt.lite.extensions.getScreenWidth -import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.utils.WeakReferenceHandler import com.pengxh.kt.lite.widget.TitleBarView -import com.pengxh.kt.lite.widget.dialog.BottomActionSheet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch class DeviceControlActivity : KotlinBaseActivity(), Handler.Callback { @@ -43,11 +33,9 @@ lateinit var weakReferenceHandler: WeakReferenceHandler } - private val context = this private val fragmentPages by lazy { ArrayList() } private val socketService by lazy { Intent(this, SocketConnectionService::class.java) } private val pageTitles = arrayOf("相机控制", "激光监测", "语音设置", "基础配置") - private val options = arrayListOf("修改安全树视频相机IP地址", "修改安全树数据通信IP地址") private var isRtspPlaying = false init { @@ -118,18 +106,7 @@ } override fun onRightClick() { - BottomActionSheet.Builder() - .setContext(context) - .setActionItemTitle(options) - .setItemTextColor(R.color.mainThemeColor.convertColor(context)) - .setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener { - override fun onActionItemClick(position: Int) { - when (position) { - 0 -> modifyCameraIp() - 1 -> modifySocketIp() - } - } - }).build().show() + } }) } @@ -137,83 +114,4 @@ override fun initEvent() { } - - /** - * rtsp流IP地址,默认是192.168.10.137 - * */ - private fun modifyCameraIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_camera_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, value) - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 板子tcp通信IP地址,默认是192.168.10.51 - * */ - private fun modifySocketIp() { - val oldValue = SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String - - val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_socket_ip, null) - val inputView = view.findViewById(R.id.inputView) - if (oldValue != "") { - inputView?.setText(oldValue) - } - - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setIcon(R.mipmap.ic_launcher) - .setView(view) - .setCancelable(false) - .setNegativeButton("取消") { _, _ -> } - .setPositiveButton("确定") { _, _ -> - val value = inputView?.text.toString() - if (value.isNotBlank()) { - SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, value) - //设置通信IP之后需要重启服务 - lifecycleScope.launch(Dispatchers.IO) { - stopService(socketService) - - delay(1000) - - startService(socketService) - } - disconnectCurrentWiFi() - } else { - "什么都还没输入呢!".show(context) - } - }.create().show() - } - - /** - * 跳转WiFi连接界面 - * */ - private fun disconnectCurrentWiFi() { - AlertDialog.Builder(this) - .setTitle(getString(R.string.app_name)) - .setMessage("数据通信IP地址已更改,请手动切换到相应WiFi") - .setIcon(R.mipmap.ic_launcher) - .setCancelable(false) - .setPositiveButton("好的") { _, _ -> - startActivity(Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)) - }.create().show() - } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt index 5c6aafd..cf9708b 100644 --- a/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt +++ b/app/src/main/java/com/casic/br/operationsite/view/WorkSiteTabActivity.kt @@ -8,6 +8,8 @@ import android.view.View import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.ViewModelProvider import com.amap.api.maps.AMap import com.amap.api.maps.CameraUpdateFactory @@ -24,6 +26,7 @@ import com.casic.br.operationsite.model.WorkSiteDeviceModel import com.casic.br.operationsite.model.WorkSiteWorkerModel import com.casic.br.operationsite.utils.AuthenticationHelper +import com.casic.br.operationsite.utils.LocaleConstant import com.casic.br.operationsite.utils.RuntimeCache import com.casic.br.operationsite.vm.LoginViewModel import com.casic.br.operationsite.vm.TubeViewModel @@ -43,6 +46,7 @@ import com.pengxh.kt.lite.utils.ActivityStackManager import com.pengxh.kt.lite.utils.LoadState import com.pengxh.kt.lite.utils.LoadingDialog +import com.pengxh.kt.lite.utils.SaveKeyValues import com.pengxh.kt.lite.widget.TitleBarView import com.pengxh.kt.lite.widget.dialog.AlertControlDialog @@ -466,7 +470,37 @@ } binding.deviceMenuItem.setOnClickListener { - navigatePageTo() + val cameraOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, "") as String + val socketOldIp = + SaveKeyValues.getValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, "") as String + + val view = LayoutInflater.from(this).inflate(R.layout.dialog_input_config, null) + val cameraIpView = view.findViewById(R.id.cameraIpView) + val socketIpView = view.findViewById(R.id.socketIpView) + cameraIpView.setText(cameraOldIp) + socketIpView.setText(socketOldIp) + + AlertDialog.Builder(this) + .setTitle(getString(R.string.app_name)) + .setMessage("请确定设备控制相关配置") + .setIcon(R.mipmap.ic_launcher) + .setView(view) + .setCancelable(false) + .setNegativeButton("取消") { _, _ -> } + .setPositiveButton("确定") { _, _ -> + val cameraIp = cameraIpView.text.toString() + val socketIp = socketIpView.text.toString() + if (cameraIp.isBlank() || socketIp.isBlank()) { + "什么都还没输入呢!".show(context) + return@setPositiveButton + } + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_CAMERA_IP_KEY, cameraIp) + SaveKeyValues.putValue(LocaleConstant.SAFE_TREE_SOCKET_IP_KEY, socketIp) + + //设置好了之后才跳转设备控制,确保设备能连上最新的配置 + navigatePageTo() + }.create().show() } binding.installMenuItem.setOnClickListener { diff --git a/app/src/main/res/layout/activity_device_control.xml b/app/src/main/res/layout/activity_device_control.xml index e309c27..50df033 100644 --- a/app/src/main/res/layout/activity_device_control.xml +++ b/app/src/main/res/layout/activity_device_control.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:background="@color/mainThemeColor" app:tbv_show_left_image="false" - app:tbv_show_right_image="true" + app:tbv_show_right_image="false" app:tbv_smaller_title="true" app:tbv_text="@string/app_name" /> diff --git a/app/src/main/res/layout/dialog_input_camera_ip.xml b/app/src/main/res/layout/dialog_input_camera_ip.xml deleted file mode 100644 index 6ccb7ae..0000000 --- a/app/src/main/res/layout/dialog_input_camera_ip.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_input_config.xml b/app/src/main/res/layout/dialog_input_config.xml new file mode 100644 index 0000000..5742abc --- /dev/null +++ b/app/src/main/res/layout/dialog_input_config.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_input_socket_ip.xml b/app/src/main/res/layout/dialog_input_socket_ip.xml deleted file mode 100644 index e97677f..0000000 --- a/app/src/main/res/layout/dialog_input_socket_ip.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file