diff --git a/app/src/main/java/com/sw/platecabinet/GlobalData.kt b/app/src/main/java/com/sw/platecabinet/GlobalData.kt index 01c2ba6..8a20c2c 100644 --- a/app/src/main/java/com/sw/platecabinet/GlobalData.kt +++ b/app/src/main/java/com/sw/platecabinet/GlobalData.kt @@ -28,7 +28,7 @@ object GlobalData { var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj" var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k" - var activeKey ="085F-118G-Q3J6-35UX" //"085F-118G-Q391-53YL" + var activeKey = "85Q1-1216-X3DH-QYTJ"//"085F-118G-Q3J6-35UX" //"085F-118G-Q391-53YL" /** * 具体业务baseurl diff --git a/app/src/main/java/com/sw/platecabinet/MyApp.kt b/app/src/main/java/com/sw/platecabinet/MyApp.kt index 4bbd37f..3215ee0 100644 --- a/app/src/main/java/com/sw/platecabinet/MyApp.kt +++ b/app/src/main/java/com/sw/platecabinet/MyApp.kt @@ -30,7 +30,7 @@ class MyApp : App() { */ private fun initGlobalData() { var deviceId = AppUtil.getUDID(this) - deviceId = "be154831-3466-3ba2-a2ea-57652c919fed" +// deviceId = "be154831-3466-3ba2-a2ea-57652c919fed" Log.d("MyApp", "initialize: deviceId=$deviceId") GlobalData.deviceId = deviceId diff --git a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt index 643b29b..529c4e3 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt @@ -464,7 +464,7 @@ abstract class BaseActivity : AppCompatActivity() { } } - private val recognizeViewModel by viewModels() + public val recognizeViewModel by viewModels() private fun updateFaceData(list: List) { Thread { val faceList = mutableListOf() diff --git a/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt index 66db5fe..3538400 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/DeviceInitActivity.kt @@ -4,8 +4,10 @@ import android.content.Intent import android.util.Log import androidx.activity.viewModels import com.sw.inbound.utils.DateTimeUtils +import com.sw.plate.App import com.sw.plate.utils.AppUtil import com.sw.plate.utils.ToastUtils +import com.sw.plate.utils.arcface.facedb.FaceDatabase import com.sw.platecabinet.GlobalData import com.sw.platecabinet.MyApp import com.sw.platecabinet.databinding.ActivityDeviceInitBinding @@ -51,6 +53,17 @@ class DeviceInitActivity : BaseActivity() { // }, 1000) initPlateList() showWaitingDialog("加载中……") + Thread { + val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao() + faceDao.deleteAll() + faceDao.resetId() +// runOnUiThread { +// viewModel.getUserFaceCache { +// recognizeViewModel.refreshFaceList() +// } +// } + }.start() + viewModel.getUserFaceCache() viewModel.getDeviceConfig { deviceConfig -> runOnUiThread { if (deviceConfig == null) { @@ -69,6 +82,7 @@ class DeviceInitActivity : BaseActivity() { }, 500) } } + // goLoginActivity() } @@ -95,8 +109,9 @@ class DeviceInitActivity : BaseActivity() { private fun initPlateList() { // SpTool.putString(SpTool.PLATE_BOX_DATA, "") - var list = SpTool.getPlateData() - if (list.isNullOrEmpty()) { +// var list = SpTool.getPlateData() + var list:List? = null; +// if (list.isNullOrEmpty()) { list = mutableListOf() repeat(22) { index -> list.add( @@ -108,7 +123,7 @@ class DeviceInitActivity : BaseActivity() { ) ) } - } +// } // TODO: 测试数据----------------------------⬇️ // else { // list[0].let { diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt index 9725944..c890f86 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt @@ -13,13 +13,12 @@ import android.view.ViewGroup import android.view.ViewTreeObserver import androidx.activity.viewModels import androidx.annotation.RequiresApi -import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.arcsoft.face.ErrorInfo import com.google.gson.Gson +import com.sw.platecabinet.socket.TcpClientListenerImpl import com.sw.inbound.utils.DateTimeUtils -import com.sw.inbound.utils.GsonUtils import com.sw.plate.App import com.sw.plate.utils.Base64 import com.sw.plate.utils.ToastUtils @@ -40,6 +39,7 @@ import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.model.UserFaceInfo import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY +import com.sw.platecabinet.GlobalData import com.sw.platecabinet.MyApp import com.sw.platecabinet.R import com.sw.platecabinet.databinding.ActivityLoginFaceBinding @@ -47,19 +47,21 @@ import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.ext.gone import com.sw.platecabinet.ext.invisible import com.sw.platecabinet.ext.visible -import com.sw.platecabinet.model.request.BindParam import com.sw.platecabinet.model.response.EquipmentUserInfo +import com.sw.platecabinet.socket.LanServer +import com.sw.platecabinet.socket.LanServerListenerImpl +import com.sw.platecabinet.socket.TcpClient import com.sw.platecabinet.utils.IntervalExecutor import com.sw.platecabinet.utils.PermissionHelper import com.sw.platecabinet.utils.PlateUtils import com.sw.platecabinet.utils.SpTool import com.sw.platecabinet.utils.countDownByFlow -import com.sw.platecabinet.viewmodel.SettingViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.json.JSONObject import timber.log.Timber /** @@ -67,7 +69,7 @@ import timber.log.Timber */ class LoginByFaceActivity : BaseActivity(), ViewTreeObserver.OnGlobalLayoutListener { - private val recognizeViewModel by viewModels() +// private val recognizeViewModel by viewModels() private var countDownTimer: CountDownTimer? = null private val CAMERA_PERMISSION_REQUEST_CODE = 100 @@ -97,6 +99,7 @@ class LoginByFaceActivity : BaseActivity(), override fun initialize() { instance = this viewModel.activeEngine() + addSocketListener() initCountTime() initArcViewModel() @@ -106,15 +109,6 @@ class LoginByFaceActivity : BaseActivity(), //viewModel.generateToken() // viewModel.activeEngine() - // TODO: 暂时注释人脸接口数据,测试本地采集 -// viewModel.getUserFaceCache() -// Thread { -// val faceDao = FaceDatabase.getInstance(App.getContext()).faceDao(); -// faceDao.deleteAll() -// faceDao.resetId() -// }.start() - - // binding.llToPwd.setOnClickListener { // val intent = Intent(this, LoginByPwdActivity::class.java) // startActivity(intent) @@ -124,7 +118,9 @@ class LoginByFaceActivity : BaseActivity(), EventBus.getDefault().register(this) - binding.btnCollectFace.setOnClickListener { collectFace() } +// binding.btnCollectFace.setOnClickListener { collectFace() } + + checkFaceState() } private fun checkCameraPermission() { @@ -310,7 +306,7 @@ class LoginByFaceActivity : BaseActivity(), EventBus.getDefault().unregister(this) countDownJob?.cancel() - + checkFaceJob?.cancel() super.onDestroy() } @@ -565,7 +561,8 @@ class LoginByFaceActivity : BaseActivity(), return } if (currentUserId != null) { - ToastUtils.showToast("您已采集过人脸信息") + currentUserId = null +// ToastUtils.showToast("您已采集过人脸信息") return } currentUserId = null @@ -574,16 +571,16 @@ class LoginByFaceActivity : BaseActivity(), //showWaitingDialog("人脸信息中……") // binding.tvFaceTip.gone() - binding.btnCollectFace.invisible() - binding.layoutState.visible() - binding.pbCollectLoading.visible() - binding.tvCollectState.text = "采集中......" +// binding.btnCollectFace.invisible() +// binding.layoutState.visible() +// binding.pbCollectLoading.visible() +// binding.tvCollectState.text = "采集中......" getFaceData { faceData -> Log.d("LoginByFaceActivity", "faceData: $faceData") runOnUiThread { //hideWaitingDialog() //toast("人脸信息已采集") - binding.pbCollectLoading.gone() +// binding.pbCollectLoading.gone() //binding.tvCollectState.text = "采集完成" userFaceInfo = null countDown() @@ -617,23 +614,23 @@ class LoginByFaceActivity : BaseActivity(), val executor = IntervalExecutor() private var currentUserId: String? = null private fun countDown() { - countDownJob = countDownByFlow( - total = 3, - scope = lifecycleScope, - onStart = { - binding.tvCollectState.text = "采集完成(3s)" - }, - onTick = { seconds -> - binding.tvCollectState.text = "采集完成(${seconds}s)" - }, - onFinish = { - //binding.tvCollectState.text = "采集完成" - currentUserId = null - binding.layoutState.invisible() -// binding.tvFaceTip.visible() - binding.btnCollectFace.visible() - } - ) +// countDownJob = countDownByFlow( +// total = 3, +// scope = lifecycleScope, +// onStart = { +// binding.tvCollectState.text = "采集完成(3s)" +// }, +// onTick = { seconds -> +// binding.tvCollectState.text = "采集完成(${seconds}s)" +// }, +// onFinish = { +// //binding.tvCollectState.text = "采集完成" +// currentUserId = null +// binding.layoutState.invisible() +//// binding.tvFaceTip.visible() +// binding.btnCollectFace.visible() +// } +// ) } private fun saveFaceInfo(faceData: String) { @@ -649,13 +646,91 @@ class LoginByFaceActivity : BaseActivity(), } FaceDatabase.getInstance(this).faceDao().insert(faceEntity) recognizeViewModel.refreshFaceList(); - runOnUiThread { - ToastUtils.showToast("你的人脸信息已采集") - } +// runOnUiThread { +// ToastUtils.showToast("你的人脸信息已采集") +// } } catch (e: Exception) { e.printStackTrace() } }.start() } + private var tcpClient: TcpClient? = null + + private fun addSocketListener() { + //val ipAddress = NetworkUtils.getIPAddress(true) + //Log.d(TAG, "addSocketListener,ipAddress: $ipAddress"); + tcpClient = TcpClient( + "192.168.1.95", + 5000, + GlobalData.deviceId, // clientId + 5000, // connectTimeoutMs + 10 * 1000, // heartbeatIntervalMs + 30 * 1000 // heartbeatTimeoutMs + ) + + tcpClient?.setListener(object : TcpClientListenerImpl() { + override fun onSendSuccess(json: JSONObject?) { + super.onSendSuccess(json) +// toast("发送成功") + } + + override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) { + super.onSendFailed(json, e) + ToastUtils.showToast("发送失败${e?.message}") + } + + override fun onMessage(json: JSONObject?) { + super.onMessage(json) + + val type = json?.getInt("type") + // 根据服务端消息类型处理删除新增临时用户人脸数据--------------------- + when (type) { + LanServer.TYPE_ADD_FACE_DATA -> { + //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据 + } + LanServer.TYPE_CLEAR_FACE_DATA -> { + clearFaceData() + } + else-> {} + } + } + }) + tcpClient?.start(); + + try { + LanServer.getInstance().let { + it.setListener(object : LanServerListenerImpl() { + override fun onMessageReceived(clientId: String?, message: JSONObject?) { + super.onMessageReceived(clientId, message) + ToastUtils.showToast("收到消息clientId=$clientId") + } + }) + it.start() + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun clearFaceData() { + Thread { + val faceDao = FaceDatabase.getInstance(this).faceDao() + faceDao.deleteTempUserFaceData() + recognizeViewModel.refreshFaceList(); + }.start() + } + private val WAITING_TIME = 5 * 1000L + private var checkStartTime = 0L + private var checkFaceJob: Job?=null + private fun checkFaceState() { + checkFaceJob = executor.startIntervalTask(5) { + if (System.currentTimeMillis() - checkStartTime >= WAITING_TIME && currentUserId.isNullOrBlank()) { + checkFaceJob?.cancel() + //超过5秒采集人脸并识别 + collectFace() + return@startIntervalTask + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt index 08ebdab..1b45925 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt @@ -39,7 +39,8 @@ class MainActivity : BaseActivity() { context: Context, pageType: PageType, equipmentUserInfo: EquipmentUserInfo? = null, - isAdmin: Boolean = false + isAdmin: Boolean = false, + block:()->Unit={} ) { val intent = Intent(context, MainActivity::class.java) intent.putExtra(PARAM_PAGE_TYPE, pageType.name) diff --git a/app/src/main/java/com/sw/platecabinet/fragment/PlateOpenFragment.kt b/app/src/main/java/com/sw/platecabinet/fragment/PlateOpenFragment.kt index 2a2a9ce..907c0e5 100644 --- a/app/src/main/java/com/sw/platecabinet/fragment/PlateOpenFragment.kt +++ b/app/src/main/java/com/sw/platecabinet/fragment/PlateOpenFragment.kt @@ -68,10 +68,11 @@ class PlateOpenFragment : override fun onFinish() { activity?.finish() - if (!isAdmin) { +// if (!isAdmin) { val intent = Intent(context, InitActivity::class.java) startActivity(intent) - } +// } + } }.start() } diff --git a/app/src/main/java/com/sw/platecabinet/socket/LanCommunicationManager.java b/app/src/main/java/com/sw/platecabinet/socket/LanCommunicationManager.java new file mode 100644 index 0000000..71200f5 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/socket/LanCommunicationManager.java @@ -0,0 +1,300 @@ +package com.sw.platecabinet.socket; + +import android.annotation.SuppressLint; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 局域网多客户端通信管理器:支持并发、心跳、认证、广播、点对点发送 + * 可用于 Android 和 JVM 程序。 + */ +public class LanCommunicationManager { + + // ============ 监听配置 ============ + private final int port; + private final long HEARTBEAT_TIMEOUT_MS; + private final int MAX_CLIENT_THREADS; + + // ============ 状态 ============ + private volatile boolean running = false; + private ServerSocket serverSocket; + + // ============ 线程池 ============ + private final ExecutorService acceptExecutor = Executors.newSingleThreadExecutor(); + private final ExecutorService clientExecutor; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + // ============ 客户端会话 ============ + private final ConcurrentHashMap clients = new ConcurrentHashMap<>(); + private final ConcurrentHashMap unAuthSessions = new ConcurrentHashMap<>(); + + // ============ 回调接口 ============ + public interface Listener { + // 新客户端完成 AUTH / 认证 + void onClientConnected(String clientId); + + // 客户端断开 + void onClientDisconnected(String clientId); + + // 收到业务消息(type != heartbeat/auth) + void onMessageReceived(String clientId, JSONObject message); + } + + private Listener listener; + + // ============ 构造 ============ + public LanCommunicationManager(int port, int maxClientThreads, long heartbeatTimeoutMs) { + this.port = port; + this.MAX_CLIENT_THREADS = maxClientThreads; + this.HEARTBEAT_TIMEOUT_MS = heartbeatTimeoutMs; + this.clientExecutor = Executors.newFixedThreadPool(Math.max(2, maxClientThreads)); + } + + public void setListener(Listener listener) { + this.listener = listener; + } + + // ============ 启动服务端 ============ + @SuppressLint("DiscouragedApi") + public void start() throws IOException { + if (running) return; + running = true; + + serverSocket = new ServerSocket(port); + serverSocket.setSoTimeout(2000); + + acceptExecutor.execute(this::acceptLoop); + scheduler.scheduleAtFixedRate(this::heartbeatCheck, + HEARTBEAT_TIMEOUT_MS, + HEARTBEAT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + + System.out.println("LanCommunicationManager started on port " + port); + } + + private void acceptLoop() { + while (running) { + try { + Socket socket = serverSocket.accept(); + socket.setSoTimeout((int) HEARTBEAT_TIMEOUT_MS * 2); + + ClientSession session = new ClientSession(socket); + unAuthSessions.put(socket, session); + + clientExecutor.execute(() -> clientReadLoop(session)); + + } catch (SocketTimeoutException ignore) { + } catch (Exception e) { + if (running) e.printStackTrace(); + } + } + } + + // ============ 处理客户端数据读取 ============ + private void clientReadLoop(ClientSession session) { + Socket socket = session.socket; + + try (DataInputStream in = new DataInputStream(socket.getInputStream())) { + while (running && !socket.isClosed()) { + + int len; + try { + len = in.readInt(); + } catch (SocketTimeoutException ste) { + continue; + } + + if (len <= 0 || len > 10 * 1024 * 1024) break; + + byte[] buf = new byte[len]; + in.readFully(buf); + session.updateLastSeen(); + + JSONObject msg = new JSONObject(new String(buf)); + handleMessage(session, msg); + } + } catch (Exception ignored) { + } finally { + closeSession(session); + } + } + + private void handleMessage(ClientSession session, JSONObject msg) { + String type = msg.optString("type", ""); + + switch (type) { + case "auth": + handleAuth(session, msg); + break; + case "heartbeat": + session.updateLastSeen(); + break; + default: + if (listener != null && session.clientId != null) { + listener.onMessageReceived(session.clientId, msg); + } + break; + } + } + + private void handleAuth(ClientSession session, JSONObject msg) { + String clientId = msg.optString("clientId", null); + if (clientId == null) return; + + session.clientId = clientId; + + // 移动到已认证 map + unAuthSessions.remove(session.socket); + clients.put(clientId, session); + + if (listener != null) listener.onClientConnected(clientId); + + sendToSession(session, ack("auth_ok")); + } + + // ============ 心跳超时 ============ + private void heartbeatCheck() { + long now = System.currentTimeMillis(); + + for (Map.Entry e : clients.entrySet()) { + ClientSession s = e.getValue(); + if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS) { + closeSession(s); + } + } + + for (ClientSession s : unAuthSessions.values()) { + if (now - s.lastSeen > HEARTBEAT_TIMEOUT_MS * 2) { + closeSession(s); + } + } + } + + // ============ 发送 ============ + public boolean sendToClient(String clientId, JSONObject json) { + ClientSession s = clients.get(clientId); + return s != null && sendToSession(s, json); + } + + public void broadcast(JSONObject json) { + for (ClientSession s : clients.values()) { + sendToSession(s, json); + } + } + + private boolean sendToSession(ClientSession s, JSONObject json) { + try { + DataOutputStream out = s.out; + + synchronized (out) { + byte[] data = json.toString().getBytes(); + out.writeInt(data.length); + out.write(data); + out.flush(); + } + return true; + + } catch (Exception e) { + closeSession(s); + return false; + } + } + + // ============ ACK ============ + private JSONObject ack(String type) { + JSONObject j = new JSONObject(); + try { + j.put("type", "ack"); + j.put("ack", type); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return j; + } + + // ============ 停止 ============ + public void stop() { + running = false; + + try { + serverSocket.close(); + } catch (Exception ignored) { + } + + for (ClientSession s : clients.values()) closeSession(s); + for (ClientSession s : unAuthSessions.values()) closeSession(s); + + acceptExecutor.shutdownNow(); + clientExecutor.shutdownNow(); + scheduler.shutdownNow(); + + System.out.println("LanCommunicationManager stopped"); + } + + // ============ 会话类 ============ + public static class ClientSession { + public final Socket socket; + public final DataOutputStream out; + public volatile long lastSeen = System.currentTimeMillis(); + public volatile String clientId; + + public ClientSession(Socket socket) throws IOException { + this.socket = socket; + this.out = new DataOutputStream(socket.getOutputStream()); + } + + public void updateLastSeen() { + lastSeen = System.currentTimeMillis(); + } + } + + public void closeSession(ClientSession session) { + if (session == null) return; + + try { + Socket socket = session.socket; + + // 1. 从已认证表移除 + if (session.clientId != null) { + ClientSession removed = clients.remove(session.clientId); + if (removed != null && listener != null) { + listener.onClientDisconnected(session.clientId); + } + } + + // 2. 从未认证表移除 + unAuthSessions.remove(socket); + + // 3. 关闭输出流 + try { + session.out.close(); + } catch (Exception ignored) { + } + + // 4. 关闭 socket + try { + if (!socket.isClosed()) socket.close(); + } catch (Exception ignored) { + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/app/src/main/java/com/sw/platecabinet/socket/LanServer.java b/app/src/main/java/com/sw/platecabinet/socket/LanServer.java new file mode 100644 index 0000000..3e71aeb --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/socket/LanServer.java @@ -0,0 +1,35 @@ +package com.sw.platecabinet.socket; + +public class LanServer { + public static final int TYPE_ADD_FACE_DATA = 1; + public static final int TYPE_CLEAR_FACE_DATA = 2; + private static volatile LanCommunicationManager instance; + + public static LanCommunicationManager getInstance() { + if (instance == null) { + synchronized (LanServer.class) { + if (instance == null) { + instance = new LanCommunicationManager( + 5000, // 监听端口 + 20, // 最大客户端数 + 10_000 // 心跳超时时间 10 秒 + ); + } + } + } + return instance; + } + + void a() { + //HashMap data = new HashMap<>(); + //data.put("type", LanServer.TYPE_ADD_FACE_DATA); + //data.put("faceId", tempUserId);//收到采集信息,每次生成新的ID + //data.put("faceFeature", faceFeatureString); + //lanServer.broadcast(new JSONObject(data)); +// + //HashMap data = new HashMap<>(); + //data.put("type", LanServer.TYPE_CLEAR_FACE_DATA); + //lanServer.broadcast(new JSONObject(data)); + } + +} diff --git a/app/src/main/java/com/sw/platecabinet/socket/LanServerListenerImpl.kt b/app/src/main/java/com/sw/platecabinet/socket/LanServerListenerImpl.kt new file mode 100644 index 0000000..1d28f56 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/socket/LanServerListenerImpl.kt @@ -0,0 +1,21 @@ +package com.sw.platecabinet.socket + +import android.util.Log +import org.json.JSONObject + +open class LanServerListenerImpl: LanCommunicationManager.Listener { + companion object { + private const val TAG = "LanServerListenerImpl" + } + override fun onClientConnected(clientId: String?) { + Log.d(TAG, "addSocketListener,onClientConnected: clientId = $clientId") + } + + override fun onClientDisconnected(clientId: String?) { + Log.d(TAG, "addSocketListener,onClientDisconnected: clientId = $clientId") + } + + override fun onMessageReceived(clientId: String?, message: JSONObject?) { + Log.d(TAG, "addSocketListener,onMessageReceived: clientId = $clientId,message = $message") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/socket/TcpClient.java b/app/src/main/java/com/sw/platecabinet/socket/TcpClient.java new file mode 100644 index 0000000..818b3d3 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/socket/TcpClient.java @@ -0,0 +1,330 @@ +package com.sw.platecabinet.socket; + +import android.util.Log; + +import org.json.JSONObject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * TcpClient - Android ready + *

+ * Features: + * - length-prefix protocol (int length + bytes) + * - separate read thread and write queue & writer thread + * - auto-reconnect with exponential backoff + * - heartbeat scheduler + * - send queue with optional callback for send result + * - auto send "auth" JSON after connection + */ +public class TcpClient { + + private static final String TAG = "TcpClient"; + + // configuration + private final String serverIp; + private final int serverPort; + private final String clientId; // will be sent in auth message + private final int connectTimeoutMs; + private final long heartbeatIntervalMs; + private final long heartbeatTimeoutMs; + + // socket + streams + private Socket socket; + private DataOutputStream out; + private DataInputStream in; + + // threads & executors + private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Writer")); + private final ExecutorService readerExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Reader")); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TcpClient-Scheduler")); + private final ExecutorService connectExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "TcpClient-Connect")); + + // send queue + private final BlockingQueue sendQueue = new LinkedBlockingQueue<>(); + + // state + private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicBoolean connected = new AtomicBoolean(false); + private final AtomicBoolean authSent = new AtomicBoolean(false); + + // reconnection/backoff + private final long baseReconnectDelayMs = 1000; // 1s + private final long maxReconnectDelayMs = 30_000; // 30s + private final AtomicInteger reconnectAttempt = new AtomicInteger(0); + + // heartbeat task future + private ScheduledFuture heartbeatFuture; + + // listener + public interface Listener { + void onConnected(); + + void onDisconnected(Exception e); + + void onMessage(JSONObject json); + + void onSendSuccess(JSONObject json); + + void onSendFailed(JSONObject json, Exception e); + } + + private Listener listener; + + public void setListener(Listener l) { + this.listener = l; + } + + // ctor + public TcpClient(String serverIp, int serverPort, String clientId, + int connectTimeoutMs, long heartbeatIntervalMs, long heartbeatTimeoutMs) { + this.serverIp = serverIp; + this.serverPort = serverPort; + this.clientId = clientId; + this.connectTimeoutMs = connectTimeoutMs; + this.heartbeatIntervalMs = heartbeatIntervalMs; + this.heartbeatTimeoutMs = heartbeatTimeoutMs; + } + + // start client (will attempt connect) + public void start() { + if (running.getAndSet(true)) return; + scheduleConnect(0); + // writer thread drains sendQueue + writerExecutor.execute(this::writerLoop); + } + + // stop client and cleanup + public void stop() { + running.set(false); + cancelHeartbeat(); + closeSocketQuiet(); + writerExecutor.shutdownNow(); + readerExecutor.shutdownNow(); + scheduler.shutdownNow(); + connectExecutor.shutdownNow(); + sendQueue.clear(); + } + + // send JSON (queued). Non-blocking. + public void send(JSONObject json) { + if (!running.get()) return; + sendQueue.offer(json); + } + + // AUTH shortcut (immediately send auth JSON) + private void sendAuth() { + try { + JSONObject auth = new JSONObject(); + auth.put("type", "auth"); + auth.put("clientId", clientId); + sendQueue.offer(auth); + authSent.set(true); + } catch (Exception ignored) { + } + } + + // writer thread loop (serializes sends) + private void writerLoop() { + while (running.get()) { + try { + JSONObject json = sendQueue.take(); // blocks + if (connected.get() && out != null) { + try { + byte[] data = json.toString().getBytes(); + synchronized (out) { + out.writeInt(data.length); + out.write(data); + out.flush(); + } + if (listener != null) listener.onSendSuccess(json); + } catch (Exception e) { + if (listener != null) listener.onSendFailed(json, e); + // on write failure, attempt reconnect + safeCloseAndScheduleReconnect(e); + } + } else { + // not connected: requeue it and wait for connection + sendQueue.offer(json); + Thread.sleep(500); // avoid busy loop + } + } catch (InterruptedException ignored) { + break; + } + } + } + + // reader loop (runs in readerExecutor) + private void startReaderLoop() { + readerExecutor.execute(() -> { + try { + while (running.get() && connected.get() && in != null) { + int length; + try { + length = in.readInt(); // will throw SocketTimeoutException if set + } catch (SocketTimeoutException ste) { + // used to detect socket liveness; continue loop + continue; + } + if (length <= 0 || length > 10 * 1024 * 1024) { + // invalid length, break + throw new RuntimeException("Invalid message length: " + length); + } + byte[] buf = new byte[length]; + in.readFully(buf); + String s = new String(buf); + try { + JSONObject json = new JSONObject(s); + // update last seen time via heartbeat ack if needed + if ("heartbeat".equals(json.optString("type"))) { + // optionally respond or update time + } else if ("auth_ok".equals(json.optString("type")) || "ack".equals(json.optString("type"))) { + // ignore or process ack + } else { + if (listener != null) listener.onMessage(json); + } + } catch (Exception je) { + Log.w(TAG, "Invalid JSON from server: " + s, je); + } + } + } catch (Exception e) { + if (running.get()) { + safeCloseAndScheduleReconnect(e); + } + } + }); + } + + // schedule connect attempt with delay (ms) + private void scheduleConnect(long delayMs) { + connectExecutor.execute(() -> { + try { + if (delayMs > 0) Thread.sleep(delayMs); + } catch (InterruptedException ignored) { + } + if (!running.get()) return; + tryConnect(); + }); + } + + // connect logic + private void tryConnect() { + if (!running.get()) return; + closeSocketQuiet(); // ensure closed + try { + Socket s = new Socket(); + s.connect(new InetSocketAddress(serverIp, serverPort), connectTimeoutMs); + s.setSoTimeout((int) Math.max(heartbeatTimeoutMs, 5_000)); + socket = s; + out = new DataOutputStream(socket.getOutputStream()); + in = new DataInputStream(socket.getInputStream()); + connected.set(true); + reconnectAttempt.set(0); + authSent.set(false); + // start reader + startReaderLoop(); + // send auth immediately + sendAuth(); + // start heartbeat + startHeartbeat(); + if (listener != null) listener.onConnected(); + Log.i(TAG, "Connected to " + serverIp + ":" + serverPort); + } catch (Exception e) { + Log.w(TAG, "Connect failed: " + e.getMessage()); + scheduleReconnectWithBackoff(); + } + } + + // start heartbeat scheduler + private void startHeartbeat() { + cancelHeartbeat(); + heartbeatFuture = scheduler.scheduleAtFixedRate(() -> { + if (!running.get() || !connected.get()) return; + try { + JSONObject hb = new JSONObject(); + hb.put("type", "heartbeat"); + hb.put("time", System.currentTimeMillis()); + sendQueue.offer(hb); + } catch (Exception ignored) { + } + }, 0, heartbeatIntervalMs, TimeUnit.MILLISECONDS); + } + + private void cancelHeartbeat() { + if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) { + heartbeatFuture.cancel(true); + heartbeatFuture = null; + } + } + + // close socket quietly and notify listener + private void safeCloseAndScheduleReconnect(Exception cause) { + closeSocketQuiet(); + if (listener != null) listener.onDisconnected(cause); + scheduleReconnectWithBackoff(); + } + + private void scheduleReconnectWithBackoff() { + int attempt = reconnectAttempt.incrementAndGet(); + long delay = Math.min(maxReconnectDelayMsFromAttempt(attempt), maxReconnectDelayMs); + Log.i(TAG, "Scheduling reconnect attempt " + attempt + " after " + delay + "ms"); + scheduleConnect(delay); + } + + // compute exponential backoff + private long maxReconnectDelayMsFromAttempt(int attempt) { + long d = baseReconnectDelayMs * (1L << Math.min(attempt, 30)); + if (d < 0) d = maxReconnectDelayMs; + return Math.min(d, maxReconnectDelayMs); + } + + // close socket and streams + private void closeSocketQuiet() { + connected.set(false); + cancelHeartbeat(); + try { + if (out != null) { + out.close(); + } + } catch (Exception ignored) { + } + try { + if (in != null) { + in.close(); + } + } catch (Exception ignored) { + } + try { + if (socket != null && !socket.isClosed()) { + socket.close(); + } + } catch (Exception ignored) { + } + out = null; + in = null; + socket = null; + } + + // helper: when manually call reconnect (immediately) + public void reconnectNow() { + scheduleConnect(0); + } + + // helper to set immediate send of a JSON and wait (blocking) until it is queued (not until delivered) + public boolean sendBlocking(JSONObject json, long timeoutMs) throws InterruptedException { + return sendQueue.offer(json, timeoutMs, TimeUnit.MILLISECONDS); + } +} diff --git a/app/src/main/java/com/sw/platecabinet/socket/TcpClientListenerImpl.kt b/app/src/main/java/com/sw/platecabinet/socket/TcpClientListenerImpl.kt new file mode 100644 index 0000000..a256519 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/socket/TcpClientListenerImpl.kt @@ -0,0 +1,33 @@ +package com.sw.platecabinet.socket + +import android.util.Log +import com.sw.platecabinet.socket.TcpClient.* +import org.json.JSONObject +import java.lang.Exception + +open class TcpClientListenerImpl : Listener { + + companion object { + private const val TAG = "TcpClientListenerImpl" + } + + override fun onConnected() { + Log.d(TAG, "addSocketListener,onConnected: ") + } + + override fun onDisconnected(e: Exception?) { + Log.d(TAG, "addSocketListener,onDisconnected: ${e?.toString()}") + } + + override fun onMessage(json: JSONObject?) { + Log.d(TAG, "addSocketListener,onMessage: $json") + } + + override fun onSendSuccess(json: JSONObject?) { + Log.d(TAG, "addSocketListener,onSendSuccess: $json") + } + + override fun onSendFailed(json: JSONObject?, e: Exception?) { + Log.d(TAG, "addSocketListener,onSendFailed: $json,e:${e?.toString()}") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/socket/UdpDiscoveryClient.java b/app/src/main/java/com/sw/platecabinet/socket/UdpDiscoveryClient.java new file mode 100644 index 0000000..24563e3 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/socket/UdpDiscoveryClient.java @@ -0,0 +1,47 @@ +package com.sw.platecabinet.socket; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; + +/** + * Simple UDP discovery client: + * Sends "DISCOVER_SERVER" broadcast and waits for first reply "SERVER_FOUND:serverName:ip" + */ +public class UdpDiscoveryClient { + + public interface Listener { + void onFound(String ip, String serverName); + + void onError(Exception e); + } + + public void discover(int discoveryPort, int timeoutMs, Listener listener) { + new Thread(() -> { + try (DatagramSocket socket = new DatagramSocket()) { + socket.setBroadcast(true); + byte[] data = "DISCOVER_SERVER".getBytes(); + DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("255.255.255.255"), discoveryPort); + socket.send(packet); + + socket.setSoTimeout(timeoutMs); + byte[] buf = new byte[512]; + DatagramPacket resp = new DatagramPacket(buf, buf.length); + socket.receive(resp); + + String msg = new String(resp.getData(), 0, resp.getLength()); + if (msg.startsWith("SERVER_FOUND")) { + // format: SERVER_FOUND:serverName:ip + String[] parts = msg.split(":", 3); + if (parts.length >= 3) { + listener.onFound(parts[2], parts[1]); + return; + } + } + listener.onError(new Exception("Invalid response")); + } catch (Exception e) { + listener.onError(e); + } + }).start(); + } +} diff --git a/app/src/main/java/com/sw/platecabinet/socket/test.java b/app/src/main/java/com/sw/platecabinet/socket/test.java new file mode 100644 index 0000000..5f7d333 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/socket/test.java @@ -0,0 +1,113 @@ +package com.sw.platecabinet.socket; + +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +public class test { + LanCommunicationManager manager = new LanCommunicationManager( + 9999, // 端口 + 20, // 最大客户端线程 + 30_000 // 心跳超时 30 秒 + ); + + public void test() { + manager.setListener(new LanCommunicationManager.Listener() { + @Override + public void onClientConnected(String clientId) { + System.out.println("新的客户端上线:" + clientId); + } + + @Override + public void onClientDisconnected(String clientId) { + System.out.println("客户端离线:" + clientId); + } + + @Override + public void onMessageReceived(String clientId, JSONObject msg) { + System.out.println("收到 " + clientId + " 的消息:" + msg); + } + }); + +// 启动 + try { + manager.start(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // 给某个客户端发送 + JSONObject j = new JSONObject(); + try { + j.put("type", "cmd"); + j.put("content", "hello"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + manager.sendToClient("device123", j); + +// 广播 + manager.broadcast(j); + } + + + public void clientTets() { + // 1) discover server (optional) + UdpDiscoveryClient disc = new UdpDiscoveryClient(); + disc.discover(9876, 3000, new UdpDiscoveryClient.Listener() { + @Override + public void onFound(String ip, String serverName) { + startClient(ip); + } + + @Override + public void onError(Exception e) { /* fallback to manual IP */ } + }); + + + } + + // 2) start client + private TcpClient client; + + private void startClient(String serverIp) { + client = new TcpClient( + serverIp, + 9999, + "device123", // clientId + 5000, // connectTimeoutMs + 10_000, // heartbeatIntervalMs + 30_000 // heartbeatTimeoutMs + ); + + client.setListener(new TcpClient.Listener() { + @Override + public void onConnected() { + Log.i("APP", "connected"); + } + + @Override + public void onDisconnected(Exception e) { + Log.i("APP", "disconnected", e); + } + + @Override + public void onMessage(JSONObject json) { + Log.i("APP", "msg:" + json); + } + + @Override + public void onSendSuccess(JSONObject json) { + } + + @Override + public void onSendFailed(JSONObject json, Exception e) { + } + }); + + client.start(); + } +} diff --git a/app/src/main/java/com/sw/platecabinet/utils/PlateUtils.kt b/app/src/main/java/com/sw/platecabinet/utils/PlateUtils.kt index 2f7833e..23fe85a 100644 --- a/app/src/main/java/com/sw/platecabinet/utils/PlateUtils.kt +++ b/app/src/main/java/com/sw/platecabinet/utils/PlateUtils.kt @@ -26,7 +26,10 @@ object PlateUtils { MainActivity.start( activity, pageType = PageType.PLATE_TIP, - isAdmin = true + isAdmin = true, + block = { + + } ) } diff --git a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt index 85a28e6..e5b1ed3 100644 --- a/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/sw/platecabinet/viewmodel/UserViewModel.kt @@ -58,7 +58,7 @@ class UserViewModel : BaseViewModel() { /** * 获取人脸数据 */ - fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE) { + fun getUserFaceCache(pageNo: Int = 1, pageSize: Int = PAGE_SIZE, refreshFaceBlock: () -> Unit={}) { var currentPageNo = pageNo launchWithLoading { val response = repository.getUserFaceCache(pageNum = pageNo, pageSize = pageSize) @@ -66,7 +66,7 @@ class UserViewModel : BaseViewModel() { // 获取成功一次后缓存状态 SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true) val list: List = response.data ?: emptyList() - if (pageNo==1&&list.isEmpty()) { + if (pageNo == 1 && list.isEmpty()) { return@launchWithLoading } val faceEntityList = list.map { @@ -74,14 +74,18 @@ class UserViewModel : BaseViewModel() { } withContext(Dispatchers.Default) { faceApi.updateFaceData(pageNo, faceEntityList) + refreshFaceBlock() if (list.size >= pageSize) { - faceTimestamp = list.last().faceUpdateTimestamp?:0 + faceTimestamp = list.last().faceUpdateTimestamp ?: 0 currentPageNo++ - getUserFaceCache(currentPageNo) + getUserFaceCache( + pageNo = currentPageNo, + refreshFaceBlock = refreshFaceBlock + ) return@withContext } if (list.isNotEmpty()) { - faceTimestamp = list.last().faceUpdateTimestamp?:0 + faceTimestamp = list.last().faceUpdateTimestamp ?: 0 } SpTool.lastFaceTimestamp = faceTimestamp //activeEngine() @@ -147,7 +151,7 @@ class UserViewModel : BaseViewModel() { /** * 通过用户id获取用户信息 */ - fun getUserInfoById(memberId: String?, action:(EquipmentUserInfo?)->Unit={}) { + fun getUserInfoById(memberId: String?, action: (EquipmentUserInfo?) -> Unit = {}) { _currentUserInfo.value = null launchWithLoading { val loginParam = LoginParam(faceId = memberId) @@ -166,6 +170,7 @@ class UserViewModel : BaseViewModel() { Timber.d("resetUserInfo") _currentUserInfo.value = null } + fun getDeviceConfig(block: (DeviceConfig?) -> Unit) { // Timber.tag(TAG).d("getDeviceConfig") launch { diff --git a/app/src/main/res/layout/activity_login_face.xml b/app/src/main/res/layout/activity_login_face.xml index 529b200..e5aa4f1 100644 --- a/app/src/main/res/layout/activity_login_face.xml +++ b/app/src/main/res/layout/activity_login_face.xml @@ -94,51 +94,51 @@ android:textSize="36sp" tools:text="30" /> - + + + + + + + - + + + + + + + - + + + + + - - + + + + + + + + + - - + + + + + + + + + + + + + diff --git a/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java index b9d7adf..5981a85 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java +++ b/lib_face/src/main/java/com/sw/plate/utils/ToastUtils.java @@ -2,6 +2,8 @@ package com.sw.plate.utils; import android.annotation.SuppressLint; import android.content.Context; +import android.os.Handler; +import android.os.Looper; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -30,18 +32,20 @@ public class ToastUtils { * @param text the text */ public static void showToast(String text) { - Context context = App.getContext(); - if (toast == null) { - View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); - textCenterView = view.findViewById(R.id.toast_tv); - toast = new Toast(context); - toast.setGravity(Gravity.CENTER, 0, 20); - toast.setDuration(Toast.LENGTH_SHORT); - toast.setView(view); - } + new Handler(Looper.getMainLooper()).post(()->{ + Context context = App.getContext(); + if (toast == null) { + View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null); + textCenterView = view.findViewById(R.id.toast_tv); + toast = new Toast(context); + toast.setGravity(Gravity.CENTER, 0, 20); + toast.setDuration(Toast.LENGTH_SHORT); + toast.setView(view); + } - textCenterView.setText(text); - toast.show(); + textCenterView.setText(text); + toast.show(); + }); } /** diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java index 9bf100f..c59f1d5 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/facedb/dao/FaceDao.java @@ -91,4 +91,10 @@ public interface FaceDao { @Query("SELECT * FROM face WHERE user_name = :userName limit 1") FaceEntity queryByUserName(String userName); + + /** + * @return 删除临时用户人脸 + */ + @Query("DELETE from face WHERE user_type = '2'") + int deleteTempUserFaceData(); }