feat: consent screen, reminder system, medical metrics, sex field, design tokens, code quality

A. Reminder: ReminderService (AlarmManager chain), ReminderSettingsView,
   bell icon in Piezo top bar, reschedule on catheterization
B. Consent: ConsentView (3 mandatory items), onboarding→consent→registration flow,
   persistent consent storage, sex field in User + Registration Step 2
C. Diary: 24hUV, NUV, NPI, max capacity, day/night frequency metrics card
D. Design: DesignTokens.kt, 48dp touch targets, DonutChart semantics, pressScale modifier
E. Quality: BuildConfig.DEBUG log wrapping, DateFormatter caching, background I/O for diary store

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 14:26:02 +09:00
parent 1d5c6f15f8
commit 0e13f2bbb6
19 changed files with 965 additions and 68 deletions
@@ -8,6 +8,7 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.example.medilightv2android.BuildConfig
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import java.util.UUID
@@ -27,6 +28,9 @@ class BleManager private constructor(private val context: Context) {
companion object {
private const val TAG = "BleManager"
private inline fun logd(msg: () -> String) { if (BuildConfig.DEBUG) Log.d(TAG, msg()) }
private inline fun logw(msg: () -> String) { if (BuildConfig.DEBUG) Log.w(TAG, msg()) }
private inline fun loge(msg: () -> String) { if (BuildConfig.DEBUG) Log.e(TAG, msg()) }
@Volatile
private var instance: BleManager? = null
@@ -87,7 +91,7 @@ class BleManager private constructor(private val context: Context) {
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
if (bluetoothLeScanner == null) {
Log.e(TAG, "BluetoothLeScanner is null")
loge { "BluetoothLeScanner is null" }
isScanning.value = false
return
}
@@ -98,9 +102,9 @@ class BleManager private constructor(private val context: Context) {
try {
bluetoothLeScanner?.startScan(null, settings, scanCallback)
Log.d(TAG, "BLE scan started")
logd { "BLE scan started" }
} catch (e: Exception) {
Log.e(TAG, "Failed to start scan: ${e.message}")
loge { "Failed to start scan: ${e.message}" }
isScanning.value = false
return
}
@@ -115,7 +119,7 @@ class BleManager private constructor(private val context: Context) {
try {
bluetoothLeScanner?.stopScan(scanCallback)
} catch (e: Exception) {
Log.w(TAG, "Failed to stop scan: ${e.message}")
logw { "Failed to stop scan: ${e.message}" }
}
isScanning.value = false
scanTimer?.let { handler.removeCallbacks(it) }
@@ -134,13 +138,13 @@ class BleManager private constructor(private val context: Context) {
discoveredDevices[existing] = discoveredDevices[existing].copy(rssi = result.rssi)
} else {
discoveredDevices.add(BleDevice(device, result.rssi))
Log.d(TAG, "Discovered: $name (${device.address}) rssi=${result.rssi}")
logd { "Discovered: $name (${device.address}) rssi=${result.rssi}" }
}
}
}
override fun onScanFailed(errorCode: Int) {
Log.e(TAG, "Scan failed with error code: $errorCode")
loge { "Scan failed with error code: $errorCode" }
handler.post {
isScanning.value = false
connectionError.value = "Scan failed (error $errorCode)"
@@ -151,16 +155,16 @@ class BleManager private constructor(private val context: Context) {
// Connection
fun connect(bleDevice: BleDevice) {
stopScan()
Log.d(TAG, "Connecting to ${bleDevice.name} (${bleDevice.address})")
logd { "Connecting to ${bleDevice.name} (${bleDevice.address})" }
// Clean up any existing GATT connection
bluetoothGatt?.let { gatt ->
Log.d(TAG, "Closing existing GATT connection")
logd { "Closing existing GATT connection" }
try {
gatt.disconnect()
gatt.close()
} catch (e: Exception) {
Log.w(TAG, "Error closing old GATT: ${e.message}")
logw { "Error closing old GATT: ${e.message}" }
}
bluetoothGatt = null
txCharacteristic = null
@@ -174,9 +178,9 @@ class BleManager private constructor(private val context: Context) {
try {
bluetoothGatt = bleDevice.device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
Log.d(TAG, "connectGatt called, gatt=${bluetoothGatt != null}")
logd { "connectGatt called, gatt=${bluetoothGatt != null}" }
} catch (e: Exception) {
Log.e(TAG, "connectGatt failed: ${e.message}")
loge { "connectGatt failed: ${e.message}" }
handler.post {
connectionError.value = "Connection failed: ${e.message}"
if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnecting = false)
@@ -188,7 +192,7 @@ class BleManager private constructor(private val context: Context) {
connectionTimer?.let { handler.removeCallbacks(it) }
val timeout = Runnable {
if (!isConnected.value) {
Log.w(TAG, "Connection timeout")
logw { "Connection timeout" }
bluetoothGatt?.let { gatt ->
try {
gatt.disconnect()
@@ -247,25 +251,25 @@ class BleManager private constructor(private val context: Context) {
val characteristic = txCharacteristic
val gatt = bluetoothGatt
if (characteristic == null) {
Log.e(TAG, "sendRaw: txCharacteristic is null!")
loge { "sendRaw: txCharacteristic is null!" }
return
}
if (gatt == null) {
Log.e(TAG, "sendRaw: bluetoothGatt is null!")
loge { "sendRaw: bluetoothGatt is null!" }
return
}
val cmdPreview = if (data.size >= 4) String(data, 0, 4, Charsets.US_ASCII) else "?"
Log.d(TAG, "sendRaw: $cmdPreview (${data.size} bytes)")
logd { "sendRaw: $cmdPreview (${data.size} bytes)" }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val result = gatt.writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE)
Log.d(TAG, "sendRaw result: $result")
logd { "sendRaw result: $result" }
} else {
@Suppress("DEPRECATION")
characteristic.value = data
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
@Suppress("DEPRECATION")
val ok = gatt.writeCharacteristic(characteristic)
Log.d(TAG, "sendRaw success: $ok")
logd { "sendRaw success: $ok" }
}
}
@@ -328,7 +332,7 @@ class BleManager private constructor(private val context: Context) {
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
Log.d(TAG, "onConnectionStateChange: status=$status newState=$newState")
logd { "onConnectionStateChange: status=$status newState=$newState" }
handler.post {
// Cancel connection timeout
connectionTimer?.let { handler.removeCallbacks(it) }
@@ -336,7 +340,7 @@ class BleManager private constructor(private val context: Context) {
if (status != BluetoothGatt.GATT_SUCCESS && newState != BluetoothProfile.STATE_CONNECTED) {
// Connection failed with error
Log.e(TAG, "GATT connection failed with status: $status")
loge { "GATT connection failed with status: $status" }
bluetoothGatt = null
txCharacteristic = null
rxCharacteristic = null
@@ -354,7 +358,7 @@ class BleManager private constructor(private val context: Context) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
Log.d(TAG, "Connected to ${gatt.device.address}")
logd { "Connected to ${gatt.device.address}" }
isReconnecting.value = false
isUserDisconnect = false
connectionError.value = null
@@ -373,7 +377,7 @@ class BleManager private constructor(private val context: Context) {
onConnectionStateChanged?.invoke(true)
}
BluetoothProfile.STATE_DISCONNECTED -> {
Log.d(TAG, "Disconnected from ${gatt.device.address}")
logd { "Disconnected from ${gatt.device.address}" }
stopBatteryPolling()
bluetoothGatt = null
txCharacteristic = null
@@ -399,21 +403,21 @@ class BleManager private constructor(private val context: Context) {
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
Log.d(TAG, "onServicesDiscovered: status=$status")
logd { "onServicesDiscovered: status=$status" }
if (status != BluetoothGatt.GATT_SUCCESS) {
handler.post { connectionError.value = "Service discovery failed (status $status)" }
return
}
val service = gatt.getService(SERVICE_UUID)
if (service == null) {
Log.e(TAG, "NUS service not found!")
loge { "NUS service not found!" }
handler.post { connectionError.value = "Device service not found" }
return
}
txCharacteristic = service.getCharacteristic(TX_CHAR_UUID)
rxCharacteristic = service.getCharacteristic(RX_CHAR_UUID)
Log.d(TAG, "TX=${txCharacteristic != null}, RX=${rxCharacteristic != null}")
logd { "TX=${txCharacteristic != null}, RX=${rxCharacteristic != null}" }
rxCharacteristic?.let { rx ->
gatt.setCharacteristicNotification(rx, true)
@@ -428,12 +432,12 @@ class BleManager private constructor(private val context: Context) {
gatt.writeDescriptor(descriptor)
}
} else {
Log.w(TAG, "CCCD descriptor not found for RX characteristic")
logw { "CCCD descriptor not found for RX characteristic" }
// No CCCD but still mark ready if characteristics exist
handler.post {
if (txCharacteristic != null && rxCharacteristic != null) {
isServiceReady.value = true
Log.d(TAG, "Service ready (no CCCD)")
logd { "Service ready (no CCCD)" }
}
}
}
@@ -442,24 +446,24 @@ class BleManager private constructor(private val context: Context) {
handler.post {
if (txCharacteristic != null) {
isServiceReady.value = true
Log.d(TAG, "Service ready (no RX)")
logd { "Service ready (no RX)" }
}
}
}
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
Log.d(TAG, "onDescriptorWrite: status=$status uuid=${descriptor.uuid}")
logd { "onDescriptorWrite: status=$status uuid=${descriptor.uuid}" }
if (descriptor.uuid == CCCD_UUID) {
handler.post {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (txCharacteristic != null && rxCharacteristic != null) {
isServiceReady.value = true
startBatteryPolling()
Log.d(TAG, "Service ready! (CCCD written)")
logd { "Service ready! (CCCD written)" }
}
} else {
Log.e(TAG, "CCCD write failed with status: $status")
loge { "CCCD write failed with status: $status" }
connectionError.value = "Notification setup failed (status $status)"
}
}
@@ -486,7 +490,7 @@ class BleManager private constructor(private val context: Context) {
private fun processReceivedData(data: ByteArray) {
val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII)
Log.d(TAG, "Received: $prefix (${data.size} bytes), callback=${
logd { "Received: $prefix (${data.size} bytes), callback=${
when {
prefix.startsWith("rcj") -> "onNirsMcjReceived=${onNirsMcjReceived != null}"
prefix.startsWith("rta") -> "onNirsPowerOnReceived=${onNirsPowerOnReceived != null}"
@@ -494,7 +498,7 @@ class BleManager private constructor(private val context: Context) {
prefix.startsWith("rag") -> "onNirsMagReceived=${onNirsMagReceived != null}"
else -> "n/a"
}
}")
}" }
when (prefix) {
"rta:", "rta!" -> onNirsPowerOnReceived?.invoke()
"rsh:", "rsh!", "rqq:", "rqq!" -> onNirsSensorActivated?.invoke()
@@ -507,7 +511,7 @@ class BleManager private constructor(private val context: Context) {
batteryLevel.value = ((millivolts - 3700) * 100 / 500).coerceIn(0, 100)
}
}
else -> Log.w(TAG, "Unknown prefix: '$prefix' hex=${data.take(8).joinToString(" ") { "%02X".format(it) }}")
else -> logw { "Unknown prefix: '$prefix' hex=${data.take(8).joinToString(" ") { "%02X".format(it) }}" }
}
}
}