more entropy
Algunas comprobaciones han fallado
Build & Publish APK Release / build (push) Failing after 7m16s
Algunas comprobaciones han fallado
Build & Publish APK Release / build (push) Failing after 7m16s
Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
@@ -7,12 +7,19 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.work.*
|
||||
import com.manalejandro.motivame.data.Task
|
||||
import com.manalejandro.motivame.data.TaskRepository
|
||||
import com.manalejandro.motivame.ui.screens.AddTaskScreen
|
||||
import com.manalejandro.motivame.ui.screens.MainScreen
|
||||
import com.manalejandro.motivame.ui.screens.SettingsScreen
|
||||
import com.manalejandro.motivame.ui.theme.MotivameTheme
|
||||
import com.manalejandro.motivame.ui.viewmodel.TaskViewModel
|
||||
import com.manalejandro.motivame.worker.DailyReminderWorker
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@@ -20,57 +27,129 @@ class MainActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
// Configurar recordatorios diarios
|
||||
setupDailyReminders()
|
||||
// Programar recordatorios para todas las tareas activas
|
||||
scheduleAllReminders()
|
||||
|
||||
setContent {
|
||||
MotivameTheme {
|
||||
MotivameApp()
|
||||
MotivameApp(onRescheduleReminders = { scheduleAllReminders() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDailyReminders() {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
|
||||
/**
|
||||
* Cancela todos los workers anteriores y programa nuevos recordatorios
|
||||
* para cada tarea activa, distribuyendo los avisos entre las 9:00 y las 21:00.
|
||||
*/
|
||||
fun scheduleAllReminders() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val repository = TaskRepository(applicationContext)
|
||||
val tasks = repository.tasks.first()
|
||||
val notificationEnabled = repository.notificationEnabled.first()
|
||||
|
||||
// Cancelar todos los workers existentes de recordatorios de tareas
|
||||
WorkManager.getInstance(applicationContext)
|
||||
.cancelAllWorkByTag("task_reminder")
|
||||
|
||||
if (!notificationEnabled) return@launch
|
||||
|
||||
tasks.filter { it.isActive }.forEach { task ->
|
||||
scheduleRemindersForTask(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleRemindersForTask(task: Task) {
|
||||
val reminders = task.dailyReminders.coerceIn(1, 10)
|
||||
val cycleDays = task.repeatEveryDays.coerceIn(1, 30)
|
||||
val workManager = WorkManager.getInstance(applicationContext)
|
||||
|
||||
// Ventana de notificaciones: 9:00 a 21:00 (12 horas = 720 minutos)
|
||||
val windowStartHour = 9
|
||||
val windowEndHour = 21
|
||||
val windowMinutes = (windowEndHour - windowStartHour) * 60 // 720 min
|
||||
|
||||
// Distribuir los N avisos a lo largo del ciclo de 'cycleDays' días,
|
||||
// repartidos uniformemente para que no coincidan todos el mismo día.
|
||||
// Cada aviso cae en un día y hora distintos dentro del ciclo.
|
||||
val totalSlots = cycleDays // un aviso por día máximo
|
||||
val step = totalSlots.toDouble() / reminders // paso fraccionario entre avisos
|
||||
|
||||
for (i in 0 until reminders) {
|
||||
// Día dentro del ciclo (0-based), distribuido uniformemente
|
||||
val slotIndex = (i * step).toInt()
|
||||
val dayOffset = slotIndex % cycleDays
|
||||
|
||||
// Hora dentro de la ventana: distribuida para que los avisos del mismo día
|
||||
// no se solapen, o usando posición i para variar la hora entre días
|
||||
val offsetMinutes = if (reminders == 1) {
|
||||
windowMinutes / 2 // Al mediodía si solo hay 1 aviso
|
||||
} else {
|
||||
((windowMinutes * i) / reminders).coerceIn(0, windowMinutes - 30)
|
||||
}
|
||||
|
||||
val targetHour = windowStartHour + offsetMinutes / 60
|
||||
val targetMinute = offsetMinutes % 60
|
||||
|
||||
val delayMs = calculateDelayToTimeWithDayOffset(targetHour, targetMinute, dayOffset)
|
||||
|
||||
val inputData = workDataOf(DailyReminderWorker.KEY_TASK_ID to task.id)
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<DailyReminderWorker>()
|
||||
.setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
|
||||
.setInputData(inputData)
|
||||
.addTag("task_reminder")
|
||||
.addTag("task_${task.id}")
|
||||
.build()
|
||||
|
||||
val dailyWorkRequest = PeriodicWorkRequestBuilder<DailyReminderWorker>(
|
||||
1, TimeUnit.DAYS
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(calculateInitialDelay(), TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
|
||||
"daily_reminder",
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
dailyWorkRequest
|
||||
)
|
||||
workManager.enqueue(workRequest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateInitialDelay(): Long {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val calendar = java.util.Calendar.getInstance().apply {
|
||||
timeInMillis = currentTime
|
||||
set(java.util.Calendar.HOUR_OF_DAY, 9) // 9 AM
|
||||
set(java.util.Calendar.MINUTE, 0)
|
||||
set(java.util.Calendar.SECOND, 0)
|
||||
/**
|
||||
* Calcula el retardo en milisegundos hasta la próxima ocurrencia de la hora indicada.
|
||||
*/
|
||||
private fun calculateDelayToTime(hour: Int, minute: Int): Long =
|
||||
calculateDelayToTimeWithDayOffset(hour, minute, 0)
|
||||
|
||||
/**
|
||||
* Calcula el retardo hasta la hora indicada más un desplazamiento de días.
|
||||
* Si la hora ya pasó hoy, se mueve al día siguiente antes de aplicar el offset.
|
||||
*/
|
||||
private fun calculateDelayToTimeWithDayOffset(hour: Int, minute: Int, dayOffset: Int): Long {
|
||||
val now = System.currentTimeMillis()
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
timeInMillis = now
|
||||
set(Calendar.HOUR_OF_DAY, hour)
|
||||
set(Calendar.MINUTE, minute)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
||||
if (calendar.timeInMillis <= currentTime) {
|
||||
calendar.add(java.util.Calendar.DAY_OF_YEAR, 1)
|
||||
// Si ya pasó esa hora hoy, mover a mañana antes de aplicar el offset
|
||||
if (calendar.timeInMillis <= now) {
|
||||
calendar.add(Calendar.DAY_OF_YEAR, 1)
|
||||
}
|
||||
|
||||
return calendar.timeInMillis - currentTime
|
||||
// Aplicar el offset de días adicionales
|
||||
if (dayOffset > 0) {
|
||||
calendar.add(Calendar.DAY_OF_YEAR, dayOffset)
|
||||
}
|
||||
|
||||
return calendar.timeInMillis - now
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MotivameApp() {
|
||||
fun MotivameApp(onRescheduleReminders: () -> Unit = {}) {
|
||||
val viewModel: TaskViewModel = viewModel()
|
||||
var currentScreen by remember { mutableStateOf("main") }
|
||||
|
||||
// Registrar callback para reprogramar avisos cuando cambian las tareas
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.onRescheduleReminders = onRescheduleReminders
|
||||
}
|
||||
|
||||
when (currentScreen) {
|
||||
"main" -> MainScreen(
|
||||
viewModel = viewModel,
|
||||
|
||||
@@ -7,6 +7,8 @@ data class Task(
|
||||
val title: String,
|
||||
val goals: List<String>,
|
||||
val isActive: Boolean = true,
|
||||
val createdAt: Long = System.currentTimeMillis()
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
val dailyReminders: Int = 3, // Número de avisos por día (entre 9:00 y 21:00)
|
||||
val repeatEveryDays: Int = 3 // Cada cuántos días se repite el ciclo de avisos
|
||||
)
|
||||
|
||||
|
||||
@@ -119,6 +119,8 @@ class TaskRepository(private val context: Context) {
|
||||
put("goals", JSONArray(task.goals))
|
||||
put("isActive", task.isActive)
|
||||
put("createdAt", task.createdAt)
|
||||
put("dailyReminders", task.dailyReminders)
|
||||
put("repeatEveryDays", task.repeatEveryDays)
|
||||
}
|
||||
jsonArray.put(jsonObject)
|
||||
}
|
||||
@@ -140,7 +142,9 @@ class TaskRepository(private val context: Context) {
|
||||
title = jsonObject.getString("title"),
|
||||
goals = goals,
|
||||
isActive = jsonObject.getBoolean("isActive"),
|
||||
createdAt = jsonObject.getLong("createdAt")
|
||||
createdAt = jsonObject.getLong("createdAt"),
|
||||
dailyReminders = if (jsonObject.has("dailyReminders")) jsonObject.getInt("dailyReminders") else 3,
|
||||
repeatEveryDays = if (jsonObject.has("repeatEveryDays")) jsonObject.getInt("repeatEveryDays") else 3
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.manalejandro.motivame.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -26,6 +25,8 @@ fun AddTaskScreen(
|
||||
var taskTitle by remember { mutableStateOf("") }
|
||||
var currentGoal by remember { mutableStateOf("") }
|
||||
var goals by remember { mutableStateOf(listOf<String>()) }
|
||||
var dailyReminders by remember { mutableStateOf(3) }
|
||||
var repeatEveryDays by remember { mutableStateOf(3) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -181,6 +182,166 @@ fun AddTaskScreen(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "🔔 Avisos diarios",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Número de recordatorios entre las 9:00 y las 21:00",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (dailyReminders > 1) dailyReminders-- },
|
||||
enabled = dailyReminders > 1
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = "Reducir",
|
||||
tint = if (dailyReminders > 1) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "$dailyReminders",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = if (dailyReminders == 1) "aviso" else "avisos",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { if (dailyReminders < 10) dailyReminders++ },
|
||||
enabled = dailyReminders < 10
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowUp,
|
||||
contentDescription = "Aumentar",
|
||||
tint = if (dailyReminders < 10) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (dailyReminders > 1) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val intervalMinutes = 720 / (dailyReminders - 1)
|
||||
Text(
|
||||
text = "⏱️ Un aviso cada ${formatInterval(intervalMinutes)} aprox.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "📅 Cada cuántos días",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Intervalo de días entre cada ciclo de avisos",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { if (repeatEveryDays > 1) repeatEveryDays-- },
|
||||
enabled = repeatEveryDays > 1
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = "Reducir días",
|
||||
tint = if (repeatEveryDays > 1) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "$repeatEveryDays",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = if (repeatEveryDays == 1) "día" else "días",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { if (repeatEveryDays < 30) repeatEveryDays++ },
|
||||
enabled = repeatEveryDays < 30
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowUp,
|
||||
contentDescription = "Aumentar días",
|
||||
tint = if (repeatEveryDays < 30) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = if (repeatEveryDays == 1) "🔁 Avisos todos los días"
|
||||
else "🔁 Avisos cada $repeatEveryDays días, repartidos para no coincidir",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
@@ -189,7 +350,9 @@ fun AddTaskScreen(
|
||||
val newTask = Task(
|
||||
title = taskTitle.trim(),
|
||||
goals = goals,
|
||||
isActive = true
|
||||
isActive = true,
|
||||
dailyReminders = dailyReminders,
|
||||
repeatEveryDays = repeatEveryDays
|
||||
)
|
||||
viewModel.addTask(newTask)
|
||||
onNavigateBack()
|
||||
@@ -214,6 +377,12 @@ fun AddTaskScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private fun formatInterval(minutes: Int): String {
|
||||
return if (minutes >= 60) {
|
||||
val h = minutes / 60
|
||||
val m = minutes % 60
|
||||
if (m == 0) "${h}h" else "${h}h ${m}min"
|
||||
} else {
|
||||
"${minutes}min"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val _soundEnabled = MutableStateFlow(true)
|
||||
val soundEnabled: StateFlow<Boolean> = _soundEnabled.asStateFlow()
|
||||
|
||||
/** Callback que se invoca tras cualquier cambio en las tareas para reprogramar avisos */
|
||||
var onRescheduleReminders: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
loadTasks()
|
||||
loadSettings()
|
||||
@@ -52,18 +55,21 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) {
|
||||
fun addTask(task: Task) {
|
||||
viewModelScope.launch {
|
||||
repository.addTask(task)
|
||||
onRescheduleReminders?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTask(task: Task) {
|
||||
viewModelScope.launch {
|
||||
repository.updateTask(task)
|
||||
onRescheduleReminders?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTask(taskId: String) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteTask(taskId)
|
||||
onRescheduleReminders?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +77,7 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) {
|
||||
viewModelScope.launch {
|
||||
repository.setNotificationEnabled(enabled)
|
||||
_notificationEnabled.value = enabled
|
||||
onRescheduleReminders?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,30 @@ class DailyReminderWorker(
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
const val KEY_TASK_ID = "task_id"
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val repository = TaskRepository(applicationContext)
|
||||
val notificationHelper = NotificationHelper(applicationContext)
|
||||
|
||||
val tasks = repository.tasks.first()
|
||||
val notificationEnabled = repository.notificationEnabled.first()
|
||||
val soundEnabled = repository.soundEnabled.first()
|
||||
if (!notificationEnabled) return Result.success()
|
||||
|
||||
if (notificationEnabled && tasks.isNotEmpty()) {
|
||||
notificationHelper.sendMotivationalReminder(tasks, soundEnabled)
|
||||
val soundEnabled = repository.soundEnabled.first()
|
||||
val taskId = inputData.getString(KEY_TASK_ID)
|
||||
|
||||
val tasks = repository.tasks.first()
|
||||
|
||||
val taskToNotify = if (taskId != null) {
|
||||
tasks.firstOrNull { it.id == taskId && it.isActive }
|
||||
} else {
|
||||
tasks.firstOrNull { it.isActive }
|
||||
}
|
||||
|
||||
taskToNotify?.let {
|
||||
notificationHelper.sendTaskReminder(it, soundEnabled)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
|
||||
Referencia en una nueva incidencia
Block a user