1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-16 05:34:05 +02:00

Fixed some warnings e.g. clean up unused variables and simplify logic

This commit is contained in:
oliexdev
2025-08-02 17:49:52 +02:00
parent 8cff0b34bf
commit 48d2fb85ce
24 changed files with 65 additions and 161 deletions

View File

@@ -17,7 +17,6 @@
*/
package com.health.openscale
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -57,7 +56,7 @@ import kotlinx.coroutines.launch
* @param context The context used to access string resources.
* @return A list of [MeasurementType] objects.
*/
fun getDefaultMeasurementTypes(context: Context): List<MeasurementType> {
fun getDefaultMeasurementTypes(): List<MeasurementType> {
return listOf(
MeasurementType(key = MeasurementTypeKey.WEIGHT, unit = UnitType.KG, color = 0xFFEF2929.toInt(), icon = "ic_weight", isPinned = true, isEnabled = true),
MeasurementType(key = MeasurementTypeKey.BMI, color = 0xFFF57900.toInt(), icon = "ic_bmi", isDerived = true, isPinned = true, isEnabled = true),
@@ -152,7 +151,7 @@ class MainActivity : ComponentActivity() {
LogManager.d(TAG, "Checking for first app start. isFirstAppStart: $isActuallyFirstStart")
if (isActuallyFirstStart) {
LogManager.i(TAG, "First app start detected. Inserting default measurement types...")
val defaultTypesToInsert = getDefaultMeasurementTypes(this@MainActivity)
val defaultTypesToInsert = getDefaultMeasurementTypes()
db.measurementTypeDao().insertAll(defaultTypesToInsert)
userSettingsRepository.setFirstAppStartCompleted(false)
LogManager.i(TAG, "Default measurement types inserted and first start marked as completed.")

View File

@@ -19,10 +19,9 @@ package com.health.openscale.core.bluetooth.data;
import java.util.Date;
public class ScaleMeasurement implements Cloneable {
public class ScaleMeasurement {
private int id;
private int userId;
private boolean enabled;
private Date dateTime;
private float weight;
private float fat;
@@ -34,7 +33,6 @@ public class ScaleMeasurement implements Cloneable {
public ScaleMeasurement() {
userId = -1;
enabled = true;
dateTime = new Date();
weight = 0.0f;
fat = 0.0f;

View File

@@ -99,7 +99,7 @@ class LegacyScaleAdapter(
override fun handleMessage(msg: Message) {
val adapter = adapterRef.get() ?: return // Adapter instance might have been garbage collected
val status = BluetoothCommunication.BT_STATUS.values().getOrNull(msg.what)
val status = BluetoothCommunication.BT_STATUS.entries.getOrNull(msg.what)
val eventData = msg.obj
val arg1 = msg.arg1
val arg2 = msg.arg2
@@ -219,43 +219,43 @@ class LegacyScaleAdapter(
}
}
override fun connect(deviceAddress: String, uiScaleUser: ScaleUser?, appUserId: Int?) {
override fun connect(address: String, scaleUser: ScaleUser?, appUserId: Int?) {
adapterScope.launch {
val currentDeviceName = currentTargetAddress ?: bluetoothDriverInstance.driverName()
if (_isConnected.value || _isConnecting.value) {
LogManager.w(TAG, "connect: Already connected/connecting to $currentDeviceName. Ignoring request for $deviceAddress.")
if (currentTargetAddress != deviceAddress && currentTargetAddress != null) {
LogManager.w(TAG, "connect: Already connected/connecting to $currentDeviceName. Ignoring request for $address.")
if (currentTargetAddress != address && currentTargetAddress != null) {
val message = applicationContext.getString(R.string.legacy_adapter_connect_busy, currentTargetAddress)
_eventsFlow.tryEmit(BluetoothEvent.ConnectionFailed(deviceAddress, message))
_eventsFlow.tryEmit(BluetoothEvent.ConnectionFailed(address, message))
} else if (currentTargetAddress == null) {
// This case implies isConnecting is true but currentTargetAddress is null,
// which might indicate a race condition or an incomplete previous cleanup.
// Allow proceeding with the new connection attempt.
LogManager.d(TAG, "connect: Retrying connection for $deviceAddress to ${bluetoothDriverInstance.driverName()} while isConnecting=true but currentTargetAddress=null")
LogManager.d(TAG, "connect: Retrying connection for $address to ${bluetoothDriverInstance.driverName()} while isConnecting=true but currentTargetAddress=null")
} else {
// Already connecting to or connected to the same deviceAddress
return@launch
}
}
LogManager.i(TAG, "connect: REQUEST for address $deviceAddress to driver ${bluetoothDriverInstance.driverName()}, UI ScaleUser ID: ${uiScaleUser?.id}, AppUserID: $appUserId")
LogManager.i(TAG, "connect: REQUEST for address $address to driver ${bluetoothDriverInstance.driverName()}, UI ScaleUser ID: ${scaleUser?.id}, AppUserID: $appUserId")
_isConnecting.value = true
_isConnected.value = false
currentTargetAddress = deviceAddress // Store the address being connected to
currentInternalUser = uiScaleUser
currentTargetAddress = address // Store the address being connected to
currentInternalUser = scaleUser
LogManager.d(TAG, "connect: Internal user for connection: ${currentInternalUser?.id}, AppUserID: $appUserId")
currentInternalUser?.let { bluetoothDriverInstance.setSelectedScaleUser(it) }
appUserId?.let { bluetoothDriverInstance.setSelectedScaleUserId(it) }
LogManager.d(TAG, "connect: Calling connect() on Java driver instance (${bluetoothDriverInstance.driverName()}) for $deviceAddress.")
LogManager.d(TAG, "connect: Calling connect() on Java driver instance (${bluetoothDriverInstance.driverName()}) for $address.")
try {
bluetoothDriverInstance.connect(deviceAddress)
bluetoothDriverInstance.connect(address)
} catch (e: Exception) {
LogManager.e(TAG, "connect: Exception while calling bluetoothDriverInstance.connect() for $deviceAddress to ${bluetoothDriverInstance.driverName()}", e)
LogManager.e(TAG, "connect: Exception while calling bluetoothDriverInstance.connect() for $address to ${bluetoothDriverInstance.driverName()}", e)
val message = applicationContext.getString(R.string.legacy_adapter_connect_exception, bluetoothDriverInstance.driverName(), e.message)
_eventsFlow.tryEmit(BluetoothEvent.ConnectionFailed(deviceAddress, message))
_eventsFlow.tryEmit(BluetoothEvent.ConnectionFailed(address, message))
cleanupAfterDisconnect() // Ensure state is reset
}
}

View File

@@ -83,28 +83,28 @@ enum class WeightUnit {
override fun toString(): String {
when (this) {
WeightUnit.LB -> return "lb"
WeightUnit.ST -> return "st"
WeightUnit.KG -> return "kg"
LB -> return "lb"
ST -> return "st"
KG -> return "kg"
}
}
fun toInt(): Int {
when (this) {
WeightUnit.LB -> return 1
WeightUnit.ST -> return 2
WeightUnit.KG -> return 0
LB -> return 1
ST -> return 2
KG -> return 0
}
}
companion object {
fun fromInt(unit: Int): WeightUnit {
when (unit) {
1 -> return WeightUnit.LB
2 -> return WeightUnit.ST
1 -> return LB
2 -> return ST
}
return WeightUnit.KG
return KG
}
}
}

View File

@@ -71,7 +71,7 @@ abstract class AppDatabase : RoomDatabase() {
companion object {
private const val TAG = "AppDatabase"
const val DATABASE_NAME = "openScaleDB.db"
const val DATABASE_NAME = "openScale.db"
@Volatile
private var INSTANCE: AppDatabase? = null

View File

@@ -354,7 +354,7 @@ class DatabaseRepository(
if (weightKg == null || weightKg <= 0f ||
heightCm == null || heightCm <= 0f ||
birthDateTimestamp <= 0L || gender == null
birthDateTimestamp <= 0L
) {
LogManager.d(CALC_PROCESS_TAG, "BMR calculation skipped: Missing or invalid weight, height, birthdate, or gender.")
return null
@@ -367,10 +367,6 @@ class DatabaseRepository(
when (gender) {
GenderType.MALE -> (10.0f * weightKg) + (6.25f * heightCm) - (5.0f * ageYears) + 5.0f
GenderType.FEMALE -> (10.0f * weightKg) + (6.25f * heightCm) - (5.0f * ageYears) - 161.0f
else -> {
LogManager.w(CALC_PROCESS_TAG, "BMR calculation not supported for gender: '$gender'. User ID: ${user.id}")
null
}
}
} else {
LogManager.w(CALC_PROCESS_TAG, "Invalid age for BMR calculation: $ageYears years. User ID: ${user.id}")
@@ -414,7 +410,7 @@ class DatabaseRepository(
val gender = user.gender
val ageYears = CalculationUtil.dateToAge(user.birthDate)
if (gender == null || ageYears <= 0) {
if (ageYears <= 0) {
LogManager.w(CALC_PROCESS_TAG, "Fat Caliper calculation skipped: Invalid gender ($gender) or age ($ageYears years). User ID: ${user.id}")
return null
}
@@ -441,10 +437,6 @@ class DatabaseRepository(
k2 = 0.0000023f
ka = 0.0001392f
}
else -> {
LogManager.w(CALC_PROCESS_TAG, "Fat Caliper calculation not supported for gender: '$gender'. User ID: ${user.id}")
return null
}
}
val bodyDensity = k0 - (k1 * sumSkinfoldsMm) + (k2 * sumSkinfoldsMm * sumSkinfoldsMm) - (ka * ageYears)

View File

@@ -107,7 +107,7 @@ interface UserSettingsRepository {
/**
* Implementation of [UserSettingsRepository] using Jetpack DataStore.
*/
class UserSettingsRepositoryImpl(private val context: Context) : UserSettingsRepository {
class UserSettingsRepositoryImpl(context: Context) : UserSettingsRepository {
private val dataStore: DataStore<Preferences> = context.userSettingsDataStore
private val TAG = "UserSettingsRepository" // Tag for logging
@@ -315,7 +315,7 @@ class UserSettingsRepositoryImpl(private val context: Context) : UserSettingsRep
}
}
else -> {
val errorMsg = "Unsupported type for preference: $keyName (Type: ${value!!::class.java.name})"
val errorMsg = "Unsupported type for preference: $keyName (Type: ${value::class.java.name})"
LogManager.e(TAG, errorMsg)
throw IllegalArgumentException(errorMsg) // This will be caught by the outer try-catch
}

View File

@@ -395,7 +395,7 @@ class SharedViewModel(
return@combine emptyList<EnrichedMeasurement>()
}
if (globalTypes.isEmpty() && measurements.isNotEmpty()) {
if (globalTypes.isEmpty()) {
LogManager.w(TAG, "Global measurement types are empty during enrichment. Trend calculation will be limited or inaccurate. (Data Enrichment Warning)")
return@combine measurements.map { currentMeasurement ->
val trendValuesUnsorted = currentMeasurement.values.map { currentValueWithType ->
@@ -472,7 +472,7 @@ class SharedViewModel(
TimeRangeFilter.LAST_7_DAYS -> calendar.add(Calendar.DAY_OF_YEAR, -7)
TimeRangeFilter.LAST_30_DAYS -> calendar.add(Calendar.DAY_OF_YEAR, -30)
TimeRangeFilter.LAST_365_DAYS -> calendar.add(Calendar.DAY_OF_YEAR, -365)
TimeRangeFilter.ALL_DAYS -> { /* Handled */ }
else -> { /* Handled */ }
}
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.set(Calendar.MINUTE, 0)

View File

@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.util.UUID
import androidx.core.util.isNotEmpty
/**
* Data class to hold information about a scanned Bluetooth LE device.
@@ -294,7 +295,7 @@ class BluetoothScannerManager(
if (newDevice.isSupported ||
!newDevice.name.isNullOrEmpty() ||
newDevice.serviceUuids.isNotEmpty() ||
(newDevice.manufacturerData != null && newDevice.manufacturerData.size() > 0)
(newDevice.manufacturerData != null && newDevice.manufacturerData.isNotEmpty())
) {
deviceMap[newDevice.address] = newDevice
listShouldBeUpdated = true
@@ -323,23 +324,4 @@ class BluetoothScannerManager(
}
}
/**
* Extension function for content-based comparison of two `SparseArray<ByteArray>?` instances.
* The standard `equals` on `SparseArray` only checks for reference equality.
*/
private fun SparseArray<ByteArray>?.contentEquals(other: SparseArray<ByteArray>?): Boolean {
if (this === other) return true
if (this == null || other == null) return false
if (this.size() != other.size()) return false
for (i in 0 until this.size()) {
val key = this.keyAt(i)
val valueThis = this.valueAt(i)
val valueOther = other.get(key)
if (valueOther == null || !valueThis.contentEquals(valueOther)) {
return false
}
}
return true
}
}

View File

@@ -59,8 +59,6 @@ enum class ConnectionStatus {
NONE,
/** Bluetooth adapter is present and enabled, but not actively scanning or connected. */
IDLE,
/** Actively scanning for Bluetooth devices. */
SCANNING,
/** No active connection to a device. */
DISCONNECTED,
/** Attempting to establish a connection to a device. */
@@ -142,8 +140,6 @@ class BluetoothViewModel(
val scanError: StateFlow<String?> = bluetoothScannerManager.scanError
// --- Connection State Flows (from BluetoothConnectionManager) ---
/** Emits the name of the currently connected device, or null if not connected. */
val connectedDeviceName: StateFlow<String?> = bluetoothConnectionManager.connectedDeviceName
/** Emits the MAC address of the currently connected device, or null if not connected. */
val connectedDeviceAddress: StateFlow<String?> = bluetoothConnectionManager.connectedDeviceAddress
/** Emits the current [ConnectionStatus] of the Bluetooth device. */
@@ -253,7 +249,7 @@ class BluetoothViewModel(
id = appUser.id
userName = appUser.name
birthday = Date(appUser.birthDate) // Ensure birthDate is in millis
bodyHeight = appUser.heightCm?.toFloat() ?: 0f // Default to 0f if height is null
bodyHeight = appUser.heightCm ?: 0f // Default to 0f if height is null
gender = appUser.gender
}
}

View File

@@ -111,7 +111,7 @@ fun MeasurementTypeFilterRow(
if (selectableTypes.isNotEmpty()) {
if (savedTypeIdsSet.isNotEmpty()) {
// Filter saved IDs to include only those present in the current selectableTypes
var validPersistedIds = savedTypeIdsSet
val validPersistedIds = savedTypeIdsSet
.mapNotNull { it.toIntOrNull() }
.filter { id -> selectableTypes.any { type -> type.id == id } }
@@ -145,7 +145,6 @@ fun MeasurementTypeFilterRow(
}
} else {
// No selectable types are available
initialIdsToDisplay = emptyList()
if (displayedSelectedIds.isNotEmpty() || savedTypeIdsSet.isNotEmpty()) {
// Clear any previous selection if types become unavailable
displayedSelectedIds = emptyList()

View File

@@ -37,7 +37,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@@ -51,7 +50,6 @@ fun DateInputDialog(
onDismiss: () -> Unit,
onConfirm: (Long) -> Unit
) {
val context = LocalContext.current
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = initialTimestamp
)

View File

@@ -72,7 +72,6 @@ fun getIconResIdByName(name: String): Int {
@Composable
fun IconPickerDialog(
currentIcon: String,
onIconSelected: (String) -> Unit,
onDismiss: () -> Unit
) {

View File

@@ -38,7 +38,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

View File

@@ -439,7 +439,6 @@ fun DeviceCardItem(
isCurrentlySaved: Boolean,
onClick: () -> Unit
) {
val context = LocalContext.current
val supportColor = if (deviceInfo.isSupported) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
val unknownDeviceName = stringResource(R.string.unknown_device_placeholder)

View File

@@ -91,12 +91,6 @@ sealed class DataManagementSettingListItem {
) : DataManagementSettingListItem()
}
/**
* Represents a header item in a list, used for section titles.
* @param title The text of the header.
*/
data class HeaderItem(val title: String) : DataManagementSettingListItem() // While not used in the provided snippet, it's good practice to document all parts of a sealed class if they exist.
/**
* Composable screen for managing application data, including import/export of measurements,
* database backup/restore, and deletion of user data or the entire database.
@@ -133,7 +127,6 @@ fun DataManagementSettingsScreen(
val context = LocalContext.current
var activeSafActionUserId by remember { mutableStateOf<Int?>(null) } // Stores user ID for SAF actions like CSV export/import
var activeSafActionId by remember { mutableStateOf<String?>(null) } // Stores action ID for distinguishing SAF operations
// --- ActivityResultLauncher for CSV Export ---
val exportCsvLauncher = rememberLauncherForActivityResult(
@@ -143,7 +136,6 @@ fun DataManagementSettingsScreen(
activeSafActionUserId?.let { userId ->
settingsViewModel.performCsvExport(userId, fileUri, context.contentResolver)
activeSafActionUserId = null // Reset after use
activeSafActionId = null
}
}
}
@@ -157,7 +149,6 @@ fun DataManagementSettingsScreen(
activeSafActionUserId?.let { userId ->
settingsViewModel.performCsvImport(userId, fileUri, context.contentResolver)
activeSafActionUserId = null // Reset after use
activeSafActionId = null
}
}
}
@@ -170,7 +161,6 @@ fun DataManagementSettingsScreen(
uri?.let { fileUri ->
// activeSafActionUserId is not relevant here as it's a global backup.
settingsViewModel.performDatabaseBackup(fileUri, context.applicationContext, context.contentResolver)
activeSafActionId = null // Reset
}
}
)
@@ -182,7 +172,6 @@ fun DataManagementSettingsScreen(
uri?.let { fileUri ->
// activeSafActionUserId is not relevant here.
settingsViewModel.performDatabaseRestore(fileUri, context.applicationContext, context.contentResolver)
activeSafActionId = null // Reset
}
}
)
@@ -193,7 +182,6 @@ fun DataManagementSettingsScreen(
when (event) {
is SafEvent.RequestCreateFile -> {
activeSafActionUserId = event.userId // Retain for CSV export if applicable
activeSafActionId = event.actionId
if (event.actionId == SettingsViewModel.ACTION_ID_BACKUP_DB) {
backupDbLauncher.launch(event.suggestedName)
} else { // Assumption: other CreateFile is CSV export
@@ -202,7 +190,6 @@ fun DataManagementSettingsScreen(
}
is SafEvent.RequestOpenFile -> {
activeSafActionUserId = event.userId // Retain for CSV import if applicable
activeSafActionId = event.actionId
if (event.actionId == SettingsViewModel.ACTION_ID_RESTORE_DB) {
// For DB Restore, we might expect specific MIME types,
// e.g., "application/octet-stream" or "application/x-sqlite3" for .db,
@@ -305,7 +292,7 @@ fun DataManagementSettingsScreen(
) {
// Regular Actions
items(regularDataManagementItems.size) { index ->
val item = regularDataManagementItems[index] as DataManagementSettingListItem.ActionItem // Safe cast
val item = regularDataManagementItems[index]
SettingsCardItem(
label = item.label,
icon = item.icon,
@@ -334,7 +321,7 @@ fun DataManagementSettingsScreen(
}
items(destructiveDataManagementItems.size) { index ->
val item = destructiveDataManagementItems[index] as DataManagementSettingListItem.ActionItem // Safe cast
val item = destructiveDataManagementItems[index]
SettingsCardItem(
label = item.label,
icon = item.icon,

View File

@@ -38,6 +38,7 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
@@ -64,7 +65,6 @@ import com.health.openscale.core.utils.LogManager
import com.health.openscale.ui.screen.SharedViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -75,7 +75,6 @@ fun GeneralSettingsScreen(
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val currentAppDisplayLocale = Locale.getDefault()
// Get supported languages (enum instances)
val supportedLanguagesEnumEntries = remember {
@@ -190,7 +189,7 @@ fun GeneralSettingsScreen(
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedLanguageMenu)
},
modifier = Modifier
.menuAnchor()
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable)
.fillMaxWidth()
)

View File

@@ -257,7 +257,7 @@ fun MeasurementTypeDetailScreen(
expanded = expandedUnit,
onDismissRequest = { expandedUnit = false }
) {
UnitType.values().forEach { unit ->
UnitType.entries.forEach { unit ->
DropdownMenuItem(
text = { Text(unit.name.lowercase().replaceFirstChar { it.uppercase() }) },
onClick = {
@@ -293,7 +293,7 @@ fun MeasurementTypeDetailScreen(
expanded = expandedInputType,
onDismissRequest = { expandedInputType = false }
) {
InputFieldType.values().forEach { type ->
InputFieldType.entries.forEach { type ->
DropdownMenuItem(
text = { Text(type.name.lowercase().replaceFirstChar { it.uppercase() }) },
onClick = {
@@ -328,7 +328,6 @@ fun MeasurementTypeDetailScreen(
// Icon Picker Dialog
if (showIconPicker) {
IconPickerDialog(
currentIcon = selectedIcon,
onIconSelected = {
selectedIcon = it
showIconPicker = false // Close picker after selection

View File

@@ -224,10 +224,10 @@ class SettingsViewModel(
try {
val allAppTypes: List<MeasurementType> = repository.getAllMeasurementTypes().first()
val exportableValueTypes = allAppTypes.filter {
it.key != null && it.key != MeasurementTypeKey.DATE && it.key != MeasurementTypeKey.TIME
it.key != MeasurementTypeKey.DATE && it.key != MeasurementTypeKey.TIME
}
val valueColumnKeys = exportableValueTypes
.mapNotNull { it.key?.name }
.map { it.key.name }
.distinct()
val dateColumnKey = MeasurementTypeKey.DATE.name
@@ -265,10 +265,7 @@ class SettingsViewModel(
measurementData.values.forEach { mvWithType ->
val typeEntity = mvWithType.type
val valueEntity = mvWithType.value
if (typeEntity.key != null &&
typeEntity.key != MeasurementTypeKey.DATE &&
typeEntity.key != MeasurementTypeKey.TIME &&
valueColumnKeys.contains(typeEntity.key.name)
if (typeEntity.key != MeasurementTypeKey.DATE && typeEntity.key != MeasurementTypeKey.TIME && valueColumnKeys.contains(typeEntity.key.name)
) {
val valueAsString: String? = when (typeEntity.inputType) {
InputFieldType.TEXT -> valueEntity.textValue
@@ -339,7 +336,7 @@ class SettingsViewModel(
try {
// ... (Rest of the import logic including CSV parsing as before) ...
val allAppTypes: List<MeasurementType> = repository.getAllMeasurementTypes().first()
val typeMapByKeyName = allAppTypes.filter { it.key != null }.associateBy { it.key!!.name }
val typeMapByKeyName = allAppTypes.associateBy { it.key.name }
val dateColumnKey = MeasurementTypeKey.DATE.name
val timeColumnKey = MeasurementTypeKey.TIME.name
@@ -360,18 +357,17 @@ class SettingsViewModel(
readAllAsSequence().forEachIndexed { rowIndex, row ->
if (rowIndex == 0) { // Header row
header = row
dateColumnIndex = header?.indexOf(dateColumnKey)
?: throw IOException("CSV header is missing the mandatory column '$dateColumnKey'.")
dateColumnIndex = header.indexOf(dateColumnKey)
// ... (rest of header processing)
timeColumnIndex = header?.indexOf(timeColumnKey) ?: -1
header?.forEachIndexed { colIdx, columnName ->
timeColumnIndex = header.indexOf(timeColumnKey)
header.forEachIndexed { colIdx, columnName ->
if (columnName != dateColumnKey && columnName != timeColumnKey) {
typeMapByKeyName[columnName]?.let { type ->
valueColumnMap[colIdx] = type
} ?: LogManager.w(TAG, "CSV import for user $userId: Column '$columnName' in CSV not found in known measurement types. It will be ignored.")
}
}
if (valueColumnMap.isEmpty() && header?.any { it != dateColumnKey && it != timeColumnKey } == true) {
if (valueColumnMap.isEmpty() && header.any { it != dateColumnKey && it != timeColumnKey }) {
LogManager.w(TAG, "CSV import for user $userId: No measurement value columns in CSV could be mapped to known types.")
}
return@forEachIndexed // Continue to next row
@@ -437,11 +433,11 @@ class SettingsViewModel(
if (isValidValue) {
measurementValues.add(mv)
} else {
LogManager.w(TAG, "CSV import for user $userId: Could not parse value '$valueString' for type '${type.key?.name}' in row ${rowIndex + 1}.")
LogManager.w(TAG, "CSV import for user $userId: Could not parse value '$valueString' for type '${type.key.name}' in row ${rowIndex + 1}.")
valuesSkippedParseError++
}
} catch (e: Exception) {
LogManager.w(TAG, "CSV import for user $userId: Error processing value '$valueString' for type '${type.key?.name}' in row ${rowIndex + 1}.", e)
LogManager.w(TAG, "CSV import for user $userId: Error processing value '$valueString' for type '${type.key.name}' in row ${rowIndex + 1}.", e)
valuesSkippedParseError++
}
}
@@ -460,19 +456,8 @@ class SettingsViewModel(
importedMeasurementsCount = newMeasurementsToSave.size
LogManager.i(TAG, "CSV Import for User ID $userId successful. $importedMeasurementsCount measurements imported.")
// Constructing the detailed message for UI
// This part is tricky if you want one single formatted string from resources.
// Often, it's better to send a base success message and log details,
// or have multiple UiMessageEvents if details are crucial for UI.
// Here's an attempt to build arguments for a potentially complex string resource:
val messageArgs = mutableListOf<Any>(importedMeasurementsCount)
var detailsForMessage = ""
if (linesSkippedMissingDate > 0) {
// This assumes you have a string like: "%1$d records. %2$d skipped (date), %3$d skipped (parse), %4$d values skipped."
// Or you emit separate messages.
// For simplicity, let's assume a main message and details are appended if they exist.
// This would require a more complex string resource or multiple resources.
// R.string.import_successful_details might take multiple args
detailsForMessage += " ($linesSkippedMissingDate rows skipped due to missing dates"
}
if (linesSkippedDateParseError > 0) {
@@ -686,7 +671,7 @@ class SettingsViewModel(
fun startDatabaseBackup() {
viewModelScope.launch {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val dbName = repository.getDatabaseName() ?: "openscale_db"
val dbName = repository.getDatabaseName()
val suggestedName = "${dbName}_backup_${timeStamp}.zip"
_safEvent.emit(SafEvent.RequestCreateFile(suggestedName, ACTION_ID_BACKUP_DB, userId = 0))
LogManager.i(TAG, "Database backup process started. Suggested name: $suggestedName. SAF event emitted.")
@@ -698,12 +683,7 @@ class SettingsViewModel(
_isLoadingBackup.value = true
LogManager.i(TAG, "Performing database backup to URI: $backupUri")
try {
val dbName = repository.getDatabaseName() ?: run {
LogManager.e(TAG, "Database backup error: Database name could not be retrieved.")
_uiMessageEvents.emit(UiMessageEvent.Resource(R.string.backup_error_db_name_not_retrieved))
_isLoadingBackup.value = false
return@launch
}
val dbName = repository.getDatabaseName()
val dbFile = applicationContext.getDatabasePath(dbName)
val dbDir = dbFile.parentFile ?: run {
LogManager.e(TAG, "Database backup error: Database directory could not be determined for $dbName.")
@@ -788,12 +768,7 @@ class SettingsViewModel(
_isLoadingRestore.value = true
LogManager.i(TAG, "Performing database restore from URI: $restoreUri")
try {
val dbName = repository.getDatabaseName() ?: run {
LogManager.e(TAG, "Database restore error: Database name could not be retrieved.")
_uiMessageEvents.emit(UiMessageEvent.Resource(R.string.backup_error_db_name_not_retrieved)) // Re-use backup error string
_isLoadingRestore.value = false
return@launch
}
val dbName = repository.getDatabaseName()
val dbFile = applicationContext.getDatabasePath(dbName)
val dbDir = dbFile.parentFile ?: run {
LogManager.e(TAG, "Database restore error: Database directory could not be determined for $dbName.")
@@ -923,12 +898,7 @@ class SettingsViewModel(
LogManager.i(TAG, "Database closed for deletion.")
withContext(Dispatchers.IO) {
val dbName = repository.getDatabaseName() // Get it before it's potentially gone
if (dbName == null) {
LogManager.e(TAG, "Failed to get database name. Cannot ensure complete deletion.")
_uiMessageEvents.emit(UiMessageEvent.Resource(R.string.delete_db_error)) // Generic error
return@withContext
}
val dbName = repository.getDatabaseName()
val databaseDeleted = applicationContext.deleteDatabase(dbName)
// Also try to delete -shm and -wal files explicitly, as deleteDatabase might not always get them.

View File

@@ -36,6 +36,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
@@ -198,7 +199,7 @@ fun UserDetailScreen(
Text(stringResource(id = R.string.user_detail_label_gender)) // "Gender"
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
GenderType.values().forEach { option ->
GenderType.entries.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -230,7 +231,7 @@ fun UserDetailScreen(
ExposedDropdownMenuDefaults.TrailingIcon(expanded = activityLevelExpanded)
},
modifier = Modifier
.menuAnchor() // Anchors the dropdown menu to this text field
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable) // Anchors the dropdown menu to this text field
.fillMaxWidth()
)
@@ -238,7 +239,7 @@ fun UserDetailScreen(
expanded = activityLevelExpanded,
onDismissRequest = { activityLevelExpanded = false }
) {
ActivityLevel.values().forEach { selectionOption ->
ActivityLevel.entries.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption.name.lowercase().replaceFirstChar { it.uppercaseChar().toString() }) },
onClick = {

View File

@@ -154,7 +154,7 @@ fun StatisticsScreen(sharedViewModel: SharedViewModel) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else if (measurementsForStatistics.isEmpty() && !isLoadingData && relevantTypesForStatsDisplay.isEmpty()) {
} else if (measurementsForStatistics.isEmpty() && relevantTypesForStatsDisplay.isEmpty()) {
// Show a message if no relevant measurement types are configured or no data is present.
// This condition is refined to also check relevantTypesForStatsDisplay.
Box(

View File

@@ -269,7 +269,7 @@ fun TableScreen(
.fillMaxSize()
.padding(16.dp), Alignment.Center
) { Text(noColumnsSelectedMessage) }
} else if (tableData.isEmpty() && enrichedMeasurements.isNotEmpty() && displayedTypes.isNotEmpty()) {
} else if (tableData.isEmpty()) {
// This case implies data exists, but not for the currently selected combination of columns.
Box(
Modifier
@@ -458,7 +458,7 @@ fun TableDataCellInternal(
else -> null
}
if (trendIconVector != null && trendContentDescription != null) {
if (trendIconVector != null) {
Icon(
imageVector = trendIconVector,
contentDescription = trendContentDescription,

View File

@@ -17,7 +17,6 @@
*/
package com.health.openscale.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
@@ -48,7 +47,7 @@ fun OpenScaleTheme(
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<!--
This path allows sharing files from the "logs" subdirectory
within your app's specific directory on external storage.
@@ -8,16 +8,4 @@
<external-files-path
name="log_files"
path="logs/" />
<!--
Consider if LogManager might also use internal storage in some cases or for old logs.
If your LogManager.getOldLogFile() could potentially point to
context.getFilesDir() + "/logs/", you might need this too.
If you are *only* using getExternalFilesDir("logs"), then the entry above is sufficient.
-->
<!--
<files-path
name="internal_log_files"
path="logs/" />
-->
</paths>