Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@
android:resource="@xml/widget_agenda_info" />
</receiver>

<receiver
android:name=".widget.LockAgendaCalendarWidgetReceiver"
android:exported="true"
android:label="@string/widget_lock_agenda_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_lock_agenda_info" />
</receiver>

<receiver
android:name=".widget.TasksWidgetReceiver"
android:exported="true"
Expand Down
212 changes: 212 additions & 0 deletions app/src/main/java/app/taskdav/widget/LockAgendaCalendarWidget.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package app.taskdav.widget

import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.action.ActionParameters
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.Switch
import androidx.glance.appwidget.SwitchDefaults
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.ToggleableStateKey
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.items
import androidx.glance.appwidget.provideContent
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.background
import androidx.glance.currentState
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import app.taskdav.data.LocaleHelper

/**
* Compact week/month agenda for lock-screen hosts (Samsung LockStar / OEMs that
* still honor [android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD]).
* Stock Pixel lock screens generally do not host third-party widgets.
*/
class LockAgendaCalendarWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition

override suspend fun provideGlance(context: Context, id: GlanceId) {
val localized = LocaleHelper.wrap(context)
val colors = WidgetTheme.colors(context)
val weekModel = WidgetDataLoader.loadAgenda(context, AgendaRange.WEEK)
val monthModel = WidgetDataLoader.loadAgenda(context, AgendaRange.MONTH)
val openCalendar = actionStartActivity(WidgetIntents.openTab(context, "calendar"))
provideContent {
val range = rangeFromPrefs(currentState<Preferences>())
val model = if (range == AgendaRange.MONTH) monthModel else weekModel
GlanceTheme {
LockAgendaContent(
context = localized,
colors = colors,
model = model,
openCalendar = openCalendar,
)
}
}
}

companion object {
val RangePrefKey = stringPreferencesKey("lock_agenda_range")
const val MaxEvents = 4

fun rangeFromPrefs(prefs: Preferences): AgendaRange =
if (prefs[RangePrefKey] == "month") AgendaRange.MONTH else AgendaRange.WEEK
}
}

@Composable
private fun LockAgendaContent(
context: Context,
colors: WidgetColors,
model: AgendaWidgetModel,
openCalendar: androidx.glance.action.Action,
) {
val events = model.events.take(LockAgendaCalendarWidget.MaxEvents)
Column(
modifier = GlanceModifier
.fillMaxSize()
.cornerRadius(16.dp)
.background(colors.surface)
.padding(horizontal = 10.dp, vertical = 8.dp)
.clickable(openCalendar),
) {
Row(
modifier = GlanceModifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = GlanceModifier.defaultWeight()) {
Text(
text = model.headerTitle,
style = TextStyle(
color = colors.primary,
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
),
maxLines = 1,
)
Text(
text = model.subtitle,
style = TextStyle(
color = colors.onSurfaceVariant,
fontSize = 10.sp,
),
maxLines = 1,
)
}
Text(
text = "W",
style = TextStyle(
color = if (model.range == AgendaRange.WEEK) colors.primary else colors.onSurfaceVariant,
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
),
)
Spacer(modifier = GlanceModifier.width(2.dp))
Switch(
checked = model.range == AgendaRange.MONTH,
onCheckedChange = actionRunCallback<ToggleLockAgendaRangeAction>(),
colors = SwitchDefaults.switchColors(
checkedThumbColor = colors.onPrimary,
checkedTrackColor = colors.primary,
uncheckedThumbColor = colors.onSurfaceVariant,
uncheckedTrackColor = colors.primaryContainer,
),
)
Spacer(modifier = GlanceModifier.width(2.dp))
Text(
text = "M",
style = TextStyle(
color = if (model.range == AgendaRange.MONTH) colors.primary else colors.onSurfaceVariant,
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
),
)
}
Spacer(modifier = GlanceModifier.height(6.dp))
if (events.isEmpty()) {
Text(
text = if (model.range == AgendaRange.WEEK) {
context.getString(app.taskdav.R.string.widget_empty_events_week)
} else {
context.getString(app.taskdav.R.string.widget_empty_events_month)
},
style = TextStyle(color = colors.onSurfaceVariant, fontSize = 11.sp),
maxLines = 2,
)
} else {
LazyColumn(modifier = GlanceModifier.fillMaxSize()) {
items(events, itemId = { it.id }) { event ->
Row(
modifier = GlanceModifier
.fillMaxWidth()
.padding(vertical = 2.dp),
) {
Text(
text = event.timeLabel,
style = TextStyle(
color = colors.onSurfaceVariant,
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
),
maxLines = 1,
modifier = GlanceModifier.width(72.dp),
)
Spacer(modifier = GlanceModifier.width(4.dp))
Text(
text = event.title,
style = TextStyle(color = colors.onSurface, fontSize = 12.sp),
maxLines = 1,
modifier = GlanceModifier.defaultWeight(),
)
}
}
}
}
}
}

class ToggleLockAgendaRangeAction : ActionCallback {
override suspend fun onAction(
context: Context,
glanceId: GlanceId,
parameters: ActionParameters,
) {
val toMonth = parameters[ToggleableStateKey] == true
val value = if (toMonth) "month" else "week"
Log.i("LockAgendaWidget", "Toggle range → $value")
updateAppWidgetState(context, glanceId) { prefs ->
prefs[LockAgendaCalendarWidget.RangePrefKey] = value
}
LockAgendaCalendarWidget().update(context, glanceId)
}
}

class LockAgendaCalendarWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = LockAgendaCalendarWidget()
}
34 changes: 23 additions & 11 deletions app/src/main/java/app/taskdav/widget/WidgetDataLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.taskdav.widget
import android.content.Context
import app.taskdav.R
import app.taskdav.TaskDavApp
import app.taskdav.data.EventEntity
import app.taskdav.data.LocaleHelper
import app.taskdav.domain.EventRecurrence
import app.taskdav.domain.TaskTreeBuilder
Expand Down Expand Up @@ -97,38 +98,38 @@ object WidgetDataLoader {
val loc = locale(context)
val app = context.applicationContext as TaskDavApp
val now = System.currentTimeMillis()
val (rangeStart, rangeEnd, header, subtitle) = when (range) {
val (rangeEnd, header, subtitle) = when (range) {
AgendaRange.WEEK -> {
val start = CalendarViewModel.startOfWeek(now)
val end = start + TimeUnit.DAYS.toMillis(7)
val weekStart = CalendarViewModel.startOfWeek(now)
val end = weekStart + TimeUnit.DAYS.toMillis(7)
val endLabel = SimpleDateFormat("MMM d", loc)
.format(Date(end - TimeUnit.DAYS.toMillis(1)))
val startLabel = SimpleDateFormat("MMM d", loc).format(Date(start))
val startLabel = SimpleDateFormat("MMM d", loc).format(Date(now))
AgendaBounds(
start,
end,
strings.getString(R.string.widget_subtitle_this_week),
"$startLabel – $endLabel",
)
}
AgendaRange.MONTH -> {
val start = CalendarViewModel.startOfMonth(now)
val monthStart = CalendarViewModel.startOfMonth(now)
val cal = Calendar.getInstance().apply {
timeInMillis = start
timeInMillis = monthStart
add(Calendar.MONTH, 1)
}
val end = cal.timeInMillis
val monthLabel = SimpleDateFormat("MMMM yyyy", loc).format(Date(start))
val monthLabel = SimpleDateFormat("MMMM yyyy", loc).format(Date(monthStart))
AgendaBounds(
start,
end,
monthLabel,
strings.getString(R.string.widget_subtitle_this_month),
)
}
}
val events = app.database.events().getActive()
val expanded = EventRecurrence.expandAll(events, rangeStart, rangeEnd)
// From now forward only — skip earlier days this week/month and events that already ended.
val expanded = EventRecurrence.expandAll(events, now, rangeEnd)
.filter { event -> eventStillRelevant(event, now) }
.sortedBy { it.dtStartMillis ?: Long.MAX_VALUE }
val dayFmt = SimpleDateFormat("EEE d", loc)
return AgendaWidgetModel(
Expand All @@ -154,6 +155,18 @@ object WidgetDataLoader {
)
}

/** True while the occurrence has not finished (ongoing or future). */
private fun eventStillRelevant(event: EventEntity, now: Long): Boolean {
val start = event.dtStartMillis ?: return false
val end = event.dtEndMillis?.takeIf { it > start }
?: if (event.allDay) {
start + TimeUnit.DAYS.toMillis(1)
} else {
start + TimeUnit.HOURS.toMillis(1)
}
return end > now
}

suspend fun loadTasks(context: Context): TasksWidgetModel {
val strings = localized(context)
val app = context.applicationContext as TaskDavApp
Expand Down Expand Up @@ -212,7 +225,6 @@ object WidgetDataLoader {
}

private data class AgendaBounds(
val start: Long,
val end: Long,
val header: String,
val subtitle: String,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/app/taskdav/widget/WidgetUpdater.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object WidgetUpdater {
withContext(Dispatchers.Default) {
CalendarWidget().updateAll(appContext)
AgendaCalendarWidget().updateAll(appContext)
LockAgendaCalendarWidget().updateAll(appContext)
TasksWidget().updateAll(appContext)
NotesWidget().updateAll(appContext)
}
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/res/layout/widget_lock_agenda_preview.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F4F7F5"
android:padding="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This week"
android:textColor="#1F6B4A"
android:textSize="13sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Mon 10:00 Team sync"
android:textColor="#161D19"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Wed All day Travel"
android:textColor="#161D19"
android:textSize="12sp" />
</LinearLayout>
2 changes: 2 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@
<string name="widget_loading">TaskDav</string>
<string name="widget_calendar_description">Eventos del calendario de hoy</string>
<string name="widget_agenda_description">Eventos de esta semana o mes</string>
<string name="widget_lock_agenda_description">Agenda compacta de semana o mes para la pantalla de bloqueo</string>
<string name="widget_tasks_description">Tareas abiertas con fecha de vencimiento</string>
<string name="widget_notes_description">Notas recientes</string>
<string name="widget_calendar_label">TaskDav Hoy</string>
<string name="widget_agenda_label">TaskDav Semana / Mes</string>
<string name="widget_lock_agenda_label">TaskDav Agenda bloqueo</string>
<string name="widget_tasks_label">TaskDav Tareas</string>
<string name="widget_notes_label">TaskDav Notas</string>

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@
<string name="widget_loading">TaskDav</string>
<string name="widget_calendar_description">Eventi del calendario di oggi</string>
<string name="widget_agenda_description">Eventi di questa settimana o mese</string>
<string name="widget_lock_agenda_description">Agenda compatta settimana o mese per la schermata di blocco</string>
<string name="widget_tasks_description">Attività aperte con scadenza</string>
<string name="widget_notes_description">Note recenti</string>
<string name="widget_calendar_label">TaskDav Oggi</string>
<string name="widget_agenda_label">TaskDav Settimana / Mese</string>
<string name="widget_lock_agenda_label">TaskDav Agenda blocco</string>
<string name="widget_tasks_label">TaskDav Attività</string>
<string name="widget_notes_label">TaskDav Note</string>

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@
<string name="widget_loading">TaskDav</string>
<string name="widget_calendar_description">Today\'s calendar events</string>
<string name="widget_agenda_description">This week or month\'s events</string>
<string name="widget_lock_agenda_description">Compact week or month agenda for the lock screen</string>
<string name="widget_tasks_description">Open tasks with due dates</string>
<string name="widget_notes_description">Recent notes</string>
<string name="widget_calendar_label">TaskDav Today</string>
<string name="widget_agenda_label">TaskDav Week / Month</string>
<string name="widget_lock_agenda_label">TaskDav Lock agenda</string>
<string name="widget_tasks_label">TaskDav Tasks</string>
<string name="widget_notes_label">TaskDav Notes</string>

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/xml/widget_agenda_info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
android:initialLayout="@layout/widget_loading"
android:previewLayout="@layout/widget_agenda_preview"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetCategory="home_screen|keyguard"
android:description="@string/widget_agenda_description" />
Loading
Loading