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:
@@ -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.")
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -72,7 +72,6 @@ fun getIconResIdByName(name: String): Int {
|
||||
|
||||
@Composable
|
||||
fun IconPickerDialog(
|
||||
currentIcon: String,
|
||||
onIconSelected: (String) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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()
|
||||
)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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 = {
|
||||
|
@@ -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(
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user