diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index 47444e7..54fd144 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -246,14 +246,58 @@ class BleManager private constructor(private val context: Context) { handler.postDelayed(timeout, 10000) } + /** + * 특정 주소의 known device 페어링 해제 — 현재 연결된 기기가 아니어도 사용 가능. + * 연결된 상태라면 일반 disconnectAndUnbond() 를 사용. + */ + fun unbondAndRemoveAddress(address: String) { + // 현재 연결된 기기를 해제하려는 경우 + if (bluetoothGatt?.device?.address == address) { + disconnectAndUnbond() + return + } + // 연결 안 된 known device — OS bond 만 끊고 list 에서 제거 + val adapter = bluetoothAdapter ?: return + val device = runCatching { adapter.getRemoteDevice(address) }.getOrNull() + device?.let { dev -> + try { + val method = dev.javaClass.getMethod("removeBond") + method.invoke(dev) + logd { "removeBond called for ${dev.address}" } + } catch (e: Exception) { + logw { "removeBond failed: ${e.message}" } + } + } + com.example.medilightv2android.services.KnownDeviceStore.remove(context, address) + debugLogger.info("DEVICE_UNREGISTERED $address") + } + + /** + * 저장된 기기 주소로 직접 연결. 스캔 결과에 안 잡혀도 OS bond 가 있으면 바로 됨. + * KnownDeviceStore 에서 골라 호출. + */ + fun connectByAddress(address: String, name: String) { + val adapter = bluetoothAdapter ?: run { + connectionError.value = "Bluetooth not available" + return + } + val device = try { + adapter.getRemoteDevice(address) + } catch (e: IllegalArgumentException) { + connectionError.value = "Invalid device address" + return + } + connect(BleDevice(device = device, rssi = 0)) + // discoveredDevices 에 없을 수 있으므로 name 을 별도로 저장 (기존 connect 가 onConnected 시점에 name 설정) + connectedDeviceName.value = name + } + /** 저장된 마지막 기기에 자동 연결 시도 */ fun autoConnectLastDevice() { if (isConnected.value) return - val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE) - val address = prefs.getString("last_device_address", null) ?: return - val name = prefs.getString("last_device_name", null) ?: return - debugLogger.info("AUTO_CONNECT attempting $name ($address)") - lastConnectedAddress = address + val recent = com.example.medilightv2android.services.KnownDeviceStore.mostRecent(context) ?: return + debugLogger.info("AUTO_CONNECT attempting ${recent.name} (${recent.address})") + lastConnectedAddress = recent.address scheduleAutoReconnect() } @@ -308,10 +352,11 @@ class BleManager private constructor(private val context: Context) { } } - // 저장된 기기 정보 삭제 + // 저장된 기기 정보 삭제 — known device list 에서도 제거 try { - val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE) - prefs.edit().remove("last_device_address").remove("last_device_name").apply() + device?.let { dev -> + com.example.medilightv2android.services.KnownDeviceStore.remove(context, dev.address) + } } catch (_: Exception) {} debugLogger.info("DEVICE_UNREGISTERED") @@ -809,11 +854,9 @@ class BleManager private constructor(private val context: Context) { debugLogger.connected(connectedDeviceName.value, gatt.device.address) try { - val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE) - prefs.edit() - .putString("last_device_address", gatt.device.address) - .putString("last_device_name", connectedDeviceName.value) - .apply() + com.example.medilightv2android.services.KnownDeviceStore.add( + context, gatt.device.address, connectedDeviceName.value + ) } catch (_: Exception) {} // MTU 247 요청 (reb: 208B 패킷 드롭 방지) diff --git a/app/src/main/java/com/example/medilightv2android/services/KnownDeviceStore.kt b/app/src/main/java/com/example/medilightv2android/services/KnownDeviceStore.kt new file mode 100644 index 0000000..a9eeb64 --- /dev/null +++ b/app/src/main/java/com/example/medilightv2android/services/KnownDeviceStore.kt @@ -0,0 +1,91 @@ +package com.example.medilightv2android.services + +import android.content.Context +import org.json.JSONArray +import org.json.JSONObject + +/** + * 페어링했던 BLE 기기 다수 영속화. + * SharedPreferences "medilight_prefs" 의 "known_devices" 키에 JSON array. + * + * 기존 단일 키(last_device_address/last_device_name) 는 마이그레이션 위해 1회 읽어 합치고 + * BleManager 가 새로 add() 호출 시 같이 정리. + */ +data class KnownDevice( + val address: String, + val name: String, + val lastConnectedAt: Long, +) + +object KnownDeviceStore { + private const val PREFS = "medilight_prefs" + private const val KEY = "known_devices" + + fun getAll(context: Context): List { + val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + val raw = prefs.getString(KEY, null) + val list = mutableListOf() + if (!raw.isNullOrBlank()) { + runCatching { + val arr = JSONArray(raw) + for (i in 0 until arr.length()) { + val o = arr.optJSONObject(i) ?: continue + val addr = o.optString("address").takeIf { it.isNotBlank() } ?: continue + val name = o.optString("name", "Unknown") + val ts = o.optLong("lastConnectedAt", 0L) + list.add(KnownDevice(addr, name, ts)) + } + } + } + // 구 키 마이그레이션 — 한 번이라도 들어와 보면 list 에 흡수 + val legacyAddr = prefs.getString("last_device_address", null) + val legacyName = prefs.getString("last_device_name", null) + if (!legacyAddr.isNullOrBlank() && list.none { it.address == legacyAddr }) { + list.add(KnownDevice(legacyAddr, legacyName ?: "Unknown", System.currentTimeMillis())) + saveAll(context, list) + } + return list.sortedByDescending { it.lastConnectedAt } + } + + /** 새 연결 성공 시 호출 — 있으면 lastConnectedAt 갱신, 없으면 추가. 구 단일 키도 같이 채움. */ + fun add(context: Context, address: String, name: String) { + val current = getAll(context).toMutableList() + val idx = current.indexOfFirst { it.address == address } + val now = System.currentTimeMillis() + if (idx >= 0) current[idx] = current[idx].copy(name = name, lastConnectedAt = now) + else current.add(KnownDevice(address, name, now)) + saveAll(context, current) + // 구 키도 호환성 유지 + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() + .putString("last_device_address", address) + .putString("last_device_name", name) + .apply() + } + + /** 페어링 해제 시 호출 — known list 에서 제거. */ + fun remove(context: Context, address: String) { + val current = getAll(context).filter { it.address != address } + saveAll(context, current) + // 구 키 정리 — 제거한 게 마지막 기기였다면 + val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + if (prefs.getString("last_device_address", null) == address) { + prefs.edit().remove("last_device_address").remove("last_device_name").apply() + } + } + + fun mostRecent(context: Context): KnownDevice? = getAll(context).firstOrNull() + + private fun saveAll(context: Context, list: List) { + val arr = JSONArray() + list.forEach { d -> + arr.put(JSONObject().apply { + put("address", d.address) + put("name", d.name) + put("lastConnectedAt", d.lastConnectedAt) + }) + } + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() + .putString(KEY, arr.toString()) + .apply() + } +}