ble: 다중 기기 페어링 영속화 (KnownDeviceStore)

기존 단일 last_device_address/name 키를 JSON 리스트로 확장.
여러 각도 변형 기기를 등록해두고 PIN 재입력 없이 골라 연결 가능.

KnownDeviceStore (services/):
- getAll / add / remove / mostRecent
- 구 단일 키 자동 마이그레이션 (read 시 흡수, 새 add 시 호환 유지)

BleManager:
- 연결 성공 시 KnownDeviceStore.add 호출 (단일 키 직접 쓰기 제거)
- autoConnectLastDevice: mostRecent() 사용
- disconnectAndUnbond: known list 에서도 제거
- connectByAddress(addr,name) 신규 — 스캔 결과 없어도 OS bond 가
  있으면 바로 연결. KnownDeviceStore 에서 골라 호출.
- unbondAndRemoveAddress(addr) 신규 — 연결 안 된 기기도 페어링
  해제 가능 (removeBond + KnownDeviceStore.remove)
This commit is contained in:
2026-06-11 15:01:20 +09:00
parent 7fa18ec05a
commit b782db16b3
2 changed files with 147 additions and 13 deletions
@@ -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<KnownDevice> {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val raw = prefs.getString(KEY, null)
val list = mutableListOf<KnownDevice>()
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<KnownDevice>) {
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()
}
}