diff --git a/app/src/main/java/com/iflytop/profilometer/MainActivity.java b/app/src/main/java/com/iflytop/profilometer/MainActivity.java index bb4c444..8f7d3ef 100644 --- a/app/src/main/java/com/iflytop/profilometer/MainActivity.java +++ b/app/src/main/java/com/iflytop/profilometer/MainActivity.java @@ -1,6 +1,7 @@ package com.iflytop.profilometer; import android.annotation.SuppressLint; +import android.os.Build; import android.os.Bundle; import android.webkit.WebView; @@ -10,6 +11,7 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import com.iflytop.profilometer.core.bluetooth.BleManager; import com.iflytop.profilometer.server.HttpServer; public class MainActivity extends AppCompatActivity { @@ -33,6 +35,15 @@ public class MainActivity extends AppCompatActivity { webView.getSettings().setJavaScriptEnabled(true); WebView.setWebContentsDebuggingEnabled(true); webView.loadUrl("http://127.0.0.1:8080/"); + + BleManager bleManager = BleManager.getInstance(this); + // 针对 Android 12 及以上版本,请求必要的运行时权限 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + bleManager.checkAndRequestPermissions(this); + } + // 检查蓝牙是否开启(如果未开启,则提示用户开启) + bleManager.promptAndEnableBluetooth(this); + } @Override @@ -40,5 +51,6 @@ public class MainActivity extends AppCompatActivity { super.onDestroy(); // Activity 销毁时停止服务器 HttpServer.stop(); + BleManager.getInstance(this).stopScan(); } } \ No newline at end of file diff --git a/app/src/main/java/com/iflytop/profilometer/ProfilometerApplication.java b/app/src/main/java/com/iflytop/profilometer/ProfilometerApplication.java index e8d4c89..992a8ea 100644 --- a/app/src/main/java/com/iflytop/profilometer/ProfilometerApplication.java +++ b/app/src/main/java/com/iflytop/profilometer/ProfilometerApplication.java @@ -1,15 +1,67 @@ package com.iflytop.profilometer; +import android.app.Activity; import android.app.Application; +import android.os.Bundle; + +import androidx.annotation.NonNull; import com.iflytop.profilometer.dao.UserDao; public class ProfilometerApplication extends Application { + private Activity currentActivity; + @Override public void onCreate() { super.onCreate(); +// +// UserDao userDao = new UserDao(this); +// userDao.insertAdminUserIfNotExists(); + + registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) { + currentActivity = activity; + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + currentActivity = activity; + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + currentActivity = activity; + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + // 如果停止的 Activity 就是当前的 Activity,则清空引用 + if (currentActivity == activity) { + currentActivity = null; + } + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { + // no-op + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + if (currentActivity == activity) { + currentActivity = null; + } + } + }); + } - UserDao userDao = new UserDao(this); - userDao.insertAdminUserIfNotExists(); + public Activity getCurrentActivity() { + return currentActivity; } } diff --git a/app/src/main/java/com/iflytop/profilometer/api/auth/AuthApi.java b/app/src/main/java/com/iflytop/profilometer/api/auth/AuthApi.java index 230f16a..1e7e4ac 100644 --- a/app/src/main/java/com/iflytop/profilometer/api/auth/AuthApi.java +++ b/app/src/main/java/com/iflytop/profilometer/api/auth/AuthApi.java @@ -25,7 +25,6 @@ public class AuthApi { UserDao userDao = new UserDao(context); AppUser user = userDao.login(username, password); if (user != null) { - WebSocketManager.broadcast(GsonUtil.toJson(user)); return Result.success(user); } else { return Result.failed(); diff --git a/app/src/main/java/com/iflytop/profilometer/api/ble/BleApi.java b/app/src/main/java/com/iflytop/profilometer/api/ble/BleApi.java index a97e29d..63db3e5 100644 --- a/app/src/main/java/com/iflytop/profilometer/api/ble/BleApi.java +++ b/app/src/main/java/com/iflytop/profilometer/api/ble/BleApi.java @@ -1,21 +1,50 @@ package com.iflytop.profilometer.api.ble; import android.content.Context; +import android.os.Build; +import android.util.Log; +import com.iflytop.profilometer.ProfilometerApplication; import com.iflytop.profilometer.common.result.Result; -import com.iflytop.profilometer.common.utils.GsonUtil; -import com.iflytop.profilometer.core.websocket.WebSocketManager; -import com.iflytop.profilometer.dao.UserDao; -import com.iflytop.profilometer.model.entiy.AppUser; +import com.iflytop.profilometer.core.bluetooth.BleManager; /** * 蓝牙接口 */ public class BleApi { + private static final String TAG = "BleApi"; private final Context context; public BleApi(Context context) { - this.context = context; + this.context = context.getApplicationContext(); + } + + /** + * 开始获取蓝牙设备列表 + */ + public String start() { + try { + BleManager.getInstance(context).startScan(); + BleWebsocketManager.getInstance(context).startWsPush(); + return Result.success(); + } catch (Exception e) { + Log.e(TAG, "开始获取蓝牙列表失败", e); + return Result.failed("获取蓝牙列表失败"); + } + } + + /** + * 结束获取蓝牙设备列表 + */ + public String stop() { + try { + BleManager.getInstance(context).stopScan(); + BleWebsocketManager.getInstance(context).stopWsPush(); + return Result.success(); + } catch (Exception e) { + Log.e(TAG, "结束获取蓝牙设备列表失败", e); + return Result.failed(); + } } diff --git a/app/src/main/java/com/iflytop/profilometer/api/ble/BleRoutes.kt b/app/src/main/java/com/iflytop/profilometer/api/ble/BleRoutes.kt index db3926f..fb04e47 100644 --- a/app/src/main/java/com/iflytop/profilometer/api/ble/BleRoutes.kt +++ b/app/src/main/java/com/iflytop/profilometer/api/ble/BleRoutes.kt @@ -15,12 +15,16 @@ fun Routing.bleRoutes(context: Context) { * 开始获取蓝牙设备列表 */ post("/api/ble/list/start") { + val jsonResponse = api.start() + call.respondText(jsonResponse, ContentType.Application.Json) } /** * 结束获取蓝牙设备列表 */ post("/api/ble/list/stop") { + val jsonResponse = api.stop() + call.respondText(jsonResponse, ContentType.Application.Json) } /** diff --git a/app/src/main/java/com/iflytop/profilometer/api/ble/BleWebsocketManager.java b/app/src/main/java/com/iflytop/profilometer/api/ble/BleWebsocketManager.java new file mode 100644 index 0000000..9116581 --- /dev/null +++ b/app/src/main/java/com/iflytop/profilometer/api/ble/BleWebsocketManager.java @@ -0,0 +1,60 @@ +package com.iflytop.profilometer.api.ble; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothDevice; +import android.content.Context; + +import com.iflytop.profilometer.common.utils.GsonUtil; +import com.iflytop.profilometer.core.bluetooth.BleManager; +import com.iflytop.profilometer.core.websocket.WebSocketManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class BleWebsocketManager { + private static BleWebsocketManager instance; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private ScheduledFuture scheduledTask; + private final Context context; + + private BleWebsocketManager(Context context) { + this.context = context.getApplicationContext(); + } + + public static synchronized BleWebsocketManager getInstance(Context context) { + if (instance == null) { + instance = new BleWebsocketManager(context); + } + return instance; + } + + /** + * 开始定时任务 + */ + @SuppressLint("MissingPermission") + public void startWsPush() { + stopWsPush(); + scheduledTask = scheduler.scheduleWithFixedDelay(() -> { + List scannedDevices = BleManager.getInstance(context).getScannedDevices(); + List bleList = new ArrayList<>(); + for(BluetoothDevice bluetoothDevice : scannedDevices){ + bleList.add(bluetoothDevice.getName() + "_" + bluetoothDevice.getAddress()); + } + WebSocketManager.send(GsonUtil.toJson(bleList)); + }, 2, 2, TimeUnit.SECONDS); + } + + /** + * 结束定时任务 + */ + public void stopWsPush() { + if (scheduledTask != null && !scheduledTask.isCancelled()) { + scheduledTask.cancel(false); + } + } + +} diff --git a/app/src/main/java/com/iflytop/profilometer/api/record/RecordApi.java b/app/src/main/java/com/iflytop/profilometer/api/record/RecordApi.java index 0c7db4e..75be80e 100644 --- a/app/src/main/java/com/iflytop/profilometer/api/record/RecordApi.java +++ b/app/src/main/java/com/iflytop/profilometer/api/record/RecordApi.java @@ -25,7 +25,7 @@ public class RecordApi { UserDao userDao = new UserDao(context); AppUser user = userDao.login(username, password); if (user != null) { - WebSocketManager.broadcast(GsonUtil.toJson(user)); + WebSocketManager.send(GsonUtil.toJson(user)); return Result.success(user); } else { return Result.failed(); diff --git a/app/src/main/java/com/iflytop/profilometer/api/sync/SyncRoutes.kt b/app/src/main/java/com/iflytop/profilometer/api/sync/SyncRoutes.kt index 2f311f5..bbe5937 100644 --- a/app/src/main/java/com/iflytop/profilometer/api/sync/SyncRoutes.kt +++ b/app/src/main/java/com/iflytop/profilometer/api/sync/SyncRoutes.kt @@ -4,7 +4,7 @@ import android.content.Context import io.ktor.server.routing.Routing import io.ktor.server.routing.post -fun Routing.authRoutes(context: Context) { +fun Routing.syncRoutes(context: Context) { val api = SyncApi(context) /** diff --git a/app/src/main/java/com/iflytop/profilometer/api/system/SystemRoutes.kt b/app/src/main/java/com/iflytop/profilometer/api/system/SystemRoutes.kt index e936ce9..4cac8f3 100644 --- a/app/src/main/java/com/iflytop/profilometer/api/system/SystemRoutes.kt +++ b/app/src/main/java/com/iflytop/profilometer/api/system/SystemRoutes.kt @@ -4,7 +4,7 @@ import android.content.Context import io.ktor.server.routing.Routing import io.ktor.server.routing.post -fun Routing.authRoutes(context: Context) { +fun Routing.systemRoutes(context: Context) { val api = SystemApi(context) /** diff --git a/app/src/main/java/com/iflytop/profilometer/core/bluetooth/BLEManager.java b/app/src/main/java/com/iflytop/profilometer/core/bluetooth/BleManager.java similarity index 92% rename from app/src/main/java/com/iflytop/profilometer/core/bluetooth/BLEManager.java rename to app/src/main/java/com/iflytop/profilometer/core/bluetooth/BleManager.java index 04d3491..7f7e152 100644 --- a/app/src/main/java/com/iflytop/profilometer/core/bluetooth/BLEManager.java +++ b/app/src/main/java/com/iflytop/profilometer/core/bluetooth/BleManager.java @@ -14,19 +14,22 @@ import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanResult; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Build; import android.util.Log; import android.widget.Toast; +import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.List; -public class BLEManager { + +public class BleManager { private static final String TAG = "BLEManager"; // 单例实例 - private static BLEManager instance; + private static BleManager instance; private BluetoothManager bluetoothManager; private BluetoothAdapter bluetoothAdapter; @@ -36,9 +39,7 @@ public class BLEManager { private BluetoothGatt bluetoothGatt; private Context context; - // 私有构造,防止外部直接实例化 - private BLEManager(Context context) { - // 使用 ApplicationContext 防止内存泄露 + private BleManager(Context context) { this.context = context.getApplicationContext(); bluetoothManager = (BluetoothManager) this.context.getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager != null) { @@ -49,9 +50,9 @@ public class BLEManager { /** * 获取单例实例 */ - public static BLEManager getInstance(Context context) { + public static synchronized BleManager getInstance(Context context) { if (instance == null) { - instance = new BLEManager(context); + instance = new BleManager(context); } return instance; } @@ -67,6 +68,7 @@ public class BLEManager { * 检查并请求必要权限(例如:位置、BLUETOOTH_SCAN、BLUETOOTH_CONNECT)。 * 此方法需要在 Activity 中调用,且在 onRequestPermissionsResult 中处理回调。 */ + @RequiresApi(api = Build.VERSION_CODES.S) public void checkAndRequestPermissions(Activity activity) { List permissionsNeeded = new ArrayList<>(); if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) { @@ -79,16 +81,14 @@ public class BLEManager { permissionsNeeded.add(Manifest.permission.BLUETOOTH_CONNECT); } if (!permissionsNeeded.isEmpty()) { - ActivityCompat.requestPermissions(activity, - permissionsNeeded.toArray(new String[0]), - 100); + ActivityCompat.requestPermissions(activity, permissionsNeeded.toArray(new String[0]), 100); } } public void promptAndEnableBluetooth(Activity activity) { if (bluetoothAdapter == null) { - Log.e(TAG, "设备不支持蓝牙"); + Log.e(TAG, "该设备不支持蓝牙"); return; } if (bluetoothAdapter.isEnabled()) { @@ -96,6 +96,7 @@ public class BLEManager { } // 直接提示用户蓝牙未开启,无需点击开启 Toast.makeText(activity, "蓝牙未开启,请先开启蓝牙", Toast.LENGTH_LONG).show(); + startScan(); } /** diff --git a/app/src/main/java/com/iflytop/profilometer/core/websocket/WebSocketManager.kt b/app/src/main/java/com/iflytop/profilometer/core/websocket/WebSocketManager.kt index a8827b6..7e51b12 100644 --- a/app/src/main/java/com/iflytop/profilometer/core/websocket/WebSocketManager.kt +++ b/app/src/main/java/com/iflytop/profilometer/core/websocket/WebSocketManager.kt @@ -24,7 +24,7 @@ object WebSocketManager { */ @OptIn(DelicateCoroutinesApi::class) @JvmStatic - fun broadcast(message: String) { + fun send(message: String) { GlobalScope.launch { sessions.forEach { session -> try { diff --git a/app/src/main/java/com/iflytop/profilometer/server/HttpServer.kt b/app/src/main/java/com/iflytop/profilometer/server/HttpServer.kt index 851d983..b5ccd73 100644 --- a/app/src/main/java/com/iflytop/profilometer/server/HttpServer.kt +++ b/app/src/main/java/com/iflytop/profilometer/server/HttpServer.kt @@ -5,6 +5,8 @@ import com.iflytop.profilometer.api.auth.authRoutes import com.iflytop.profilometer.api.ble.bleRoutes import com.iflytop.profilometer.api.measure.measureRoutes import com.iflytop.profilometer.api.record.recordRoutes +import com.iflytop.profilometer.api.sync.syncRoutes +import com.iflytop.profilometer.api.system.systemRoutes import com.iflytop.profilometer.core.websocket.WebSocketManager import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode @@ -82,6 +84,8 @@ object HttpServer { measureRoutes(context) bleRoutes(context) recordRoutes(context) + syncRoutes(context) + systemRoutes(context) } } server?.start(wait = false) @@ -107,6 +111,7 @@ object HttpServer { else -> ContentType.Application.OctetStream } } + private suspend fun WebSocketSession.awaitClose() { try { incoming.consumeEach {}