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:
@@ -246,14 +246,58 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
handler.postDelayed(timeout, 10000)
|
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() {
|
fun autoConnectLastDevice() {
|
||||||
if (isConnected.value) return
|
if (isConnected.value) return
|
||||||
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
val recent = com.example.medilightv2android.services.KnownDeviceStore.mostRecent(context) ?: return
|
||||||
val address = prefs.getString("last_device_address", null) ?: return
|
debugLogger.info("AUTO_CONNECT attempting ${recent.name} (${recent.address})")
|
||||||
val name = prefs.getString("last_device_name", null) ?: return
|
lastConnectedAddress = recent.address
|
||||||
debugLogger.info("AUTO_CONNECT attempting $name ($address)")
|
|
||||||
lastConnectedAddress = address
|
|
||||||
scheduleAutoReconnect()
|
scheduleAutoReconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,10 +352,11 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 저장된 기기 정보 삭제
|
// 저장된 기기 정보 삭제 — known device list 에서도 제거
|
||||||
try {
|
try {
|
||||||
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
device?.let { dev ->
|
||||||
prefs.edit().remove("last_device_address").remove("last_device_name").apply()
|
com.example.medilightv2android.services.KnownDeviceStore.remove(context, dev.address)
|
||||||
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
debugLogger.info("DEVICE_UNREGISTERED")
|
debugLogger.info("DEVICE_UNREGISTERED")
|
||||||
@@ -809,11 +854,9 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
debugLogger.connected(connectedDeviceName.value, gatt.device.address)
|
debugLogger.connected(connectedDeviceName.value, gatt.device.address)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
com.example.medilightv2android.services.KnownDeviceStore.add(
|
||||||
prefs.edit()
|
context, gatt.device.address, connectedDeviceName.value
|
||||||
.putString("last_device_address", gatt.device.address)
|
)
|
||||||
.putString("last_device_name", connectedDeviceName.value)
|
|
||||||
.apply()
|
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
// MTU 247 요청 (reb: 208B 패킷 드롭 방지)
|
// MTU 247 요청 (reb: 208B 패킷 드롭 방지)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user