From bd38e3c6582f2aeb44bacf7ca12a79c3e172b9cb Mon Sep 17 00:00:00 2001 From: Kurian Vithayathil <1056073+kvithayathil@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:17:58 -0400 Subject: [PATCH 01/11] feat(widget): add calendars column and helpers to Widget model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add nullable calendars field (String? DEFAULT NULL) to Widget entity for per-widget calendar filtering. NULL = fallback to global, empty = show nothing. - Widget.kt: add calendars field, isCalendarsConfigured(), getCalendarIdsAsList() - EventsDatabase.kt: bump version 11→12, MIGRATION_11_12 adds column - Constants.kt: add EVENT_LIST_CALENDARS intent extra key --- .../fossify/calendar/databases/EventsDatabase.kt | 9 ++++++++- .../org/fossify/calendar/helpers/Constants.kt | 1 + .../kotlin/org/fossify/calendar/models/Widget.kt | 16 ++++++++++++++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt b/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt index 04011454a..7cd1750e1 100644 --- a/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt +++ b/app/src/main/kotlin/org/fossify/calendar/databases/EventsDatabase.kt @@ -24,7 +24,7 @@ import java.util.concurrent.Executors @Database( entities = [Event::class, CalendarEntity::class, Widget::class, Task::class], - version = 11 + version = 12 ) @TypeConverters(Converters::class) abstract class EventsDatabase : RoomDatabase() { @@ -65,6 +65,7 @@ abstract class EventsDatabase : RoomDatabase() { .addMigrations(MIGRATION_8_9) .addMigrations(MIGRATION_9_10) .addMigrations(MIGRATION_10_11) + .addMigrations(MIGRATION_11_12) .build() db!!.openHelper.setWriteAheadLoggingEnabled(true) } @@ -182,5 +183,11 @@ abstract class EventsDatabase : RoomDatabase() { } } } + + private val MIGRATION_11_12 = object : Migration(11, 12) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE widgets ADD COLUMN calendars TEXT DEFAULT NULL") + } + } } } diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt index 661c2486f..f056cbbf9 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt @@ -83,6 +83,7 @@ const val EVENT_PERIOD_CUSTOM = -2 const val AUTO_BACKUP_INTERVAL_IN_DAYS = 1 const val EVENT_LIST_PERIOD = "event_list_period" +const val EVENT_LIST_CALENDARS = "event_list_calendars" // Shared Preferences const val WEEK_NUMBERS = "week_numbers" diff --git a/app/src/main/kotlin/org/fossify/calendar/models/Widget.kt b/app/src/main/kotlin/org/fossify/calendar/models/Widget.kt index 2c9c7107c..126b53561 100644 --- a/app/src/main/kotlin/org/fossify/calendar/models/Widget.kt +++ b/app/src/main/kotlin/org/fossify/calendar/models/Widget.kt @@ -10,5 +10,17 @@ data class Widget( @PrimaryKey(autoGenerate = true) var id: Long?, @ColumnInfo(name = "widget_id") var widgetId: Int, @ColumnInfo(name = "period") var period: Int, - @ColumnInfo(name = "header") var header: Boolean -) + @ColumnInfo(name = "header") var header: Boolean, + @ColumnInfo(name = "calendars") var calendars: String? = null +) { + fun isCalendarsConfigured(): Boolean = calendars != null + + fun getCalendarIdsAsList(): List { + val cal = calendars ?: return emptyList() + return if (cal.isNotEmpty()) { + cal.split(",").mapNotNull { it.trim().toLongOrNull() } + } else { + emptyList() + } + } +} From 8ade1c9a364ac3213eab0331a1727b0256775c3a Mon Sep 17 00:00:00 2001 From: Kurian Vithayathil <1056073+kvithayathil@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:18:05 -0400 Subject: [PATCH 02/11] feat(widget): add settings icon drawable and calendar picker strings - ic_settings_vector.xml: Material Settings gear icon (24dp) - strings.xml: add widget_calendars, widget_calendars_summary, widget_calendars_all --- app/src/main/res/drawable/ic_settings_vector.xml | 3 +++ app/src/main/res/values/strings.xml | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 app/src/main/res/drawable/ic_settings_vector.xml diff --git a/app/src/main/res/drawable/ic_settings_vector.xml b/app/src/main/res/drawable/ic_settings_vector.xml new file mode 100644 index 000000000..f22fc2077 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_vector.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 09cedfaee..49617261a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -294,6 +294,9 @@ Within the next %d months Show header + Calendars + Select which calendars to show in this widget + All calendars Go to previous month From 3a739dfb7963a7e5b178d4a4f2f47275740420dc Mon Sep 17 00:00:00 2001 From: Kurian Vithayathil <1056073+kvithayathil@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:03:29 -0400 Subject: [PATCH 03/11] feat(widget): implement per-widget calendar filtering for event list widget Add per-widget calendar selection to the event list widget. Each widget instance can now filter events to specific calendars, overriding the global display settings. - Add overrideCalendarIds parameter to EventsHelper.getEventsSync() with widget-wins-completely logic (skips all global filter including birthday/anniversary) - Add calendars Intent extra to WidgetService for per-instance filtering - Add settings gear icon to widget header via direct PendingIntent - Add calendar picker row to widget config screen with SelectCalendarsDialog - Guard back press during re-configuration to avoid clearing widget - Disable save button until existing config loads from Room --- CHANGELOG.md | 4 ++ .../activities/WidgetListConfigureActivity.kt | 64 ++++++++++++++++++- .../adapters/EventListWidgetAdapter.kt | 10 ++- .../fossify/calendar/helpers/EventsHelper.kt | 45 +++++++++++-- .../calendar/helpers/MyWidgetListProvider.kt | 17 +++++ .../main/res/layout/widget_config_list.xml | 30 ++++++++- .../res/layout/widget_event_list_header.xml | 18 +++++- 7 files changed, 176 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363e8bd32..d1bb0ef7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Per-widget calendar filtering for event list widget +- Settings icon on event list widget header for re-configuration - Holidays for New Zealand ([#1157]) ### Changed +- Database schema migration 11→12: added nullable calendars column to widget table +- Refactored widget config and header layouts for calendar picker integration - Updated holiday data ### Fixed diff --git a/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt b/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt index d3c4e1d1a..2d4144a48 100644 --- a/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt +++ b/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt @@ -11,6 +11,7 @@ import org.fossify.calendar.R import org.fossify.calendar.adapters.EventListAdapter import org.fossify.calendar.databinding.WidgetConfigListBinding import org.fossify.calendar.dialogs.CustomPeriodPickerDialog +import org.fossify.calendar.dialogs.SelectCalendarsDialog import org.fossify.calendar.extensions.config import org.fossify.calendar.extensions.seconds import org.fossify.calendar.extensions.widgetsDB @@ -37,6 +38,9 @@ class WidgetListConfigureActivity : SimpleActivity() { private var mBgColor = 0 private var mTextColor = 0 private var mSelectedPeriodOption = 0 + private var mSelectedCalendars = HashSet() + private var mIsReconfiguring = false + private var mWidgetLoaded = false private val binding by viewBinding(WidgetConfigListBinding::inflate) @@ -72,6 +76,9 @@ class WidgetListConfigureActivity : SimpleActivity() { val primaryColor = getProperPrimaryColor() configBgSeekbar.setColors(mTextColor, primaryColor, primaryColor) + + configSave.isEnabled = false + calendarPickerValue.setOnClickListener { showCalendarSelector() } } updateSelectedPeriod(config.lastUsedEventSpan) @@ -85,6 +92,29 @@ class WidgetListConfigureActivity : SimpleActivity() { } updateTextColors(binding.periodPickerHolder) + + ensureBackgroundThread { + val existingWidget = widgetsDB.getWidgetWithWidgetId(mWidgetId) + if (existingWidget != null) { + mIsReconfiguring = true + mSelectedPeriodOption = existingWidget.period + binding.showWidgetHeader.isChecked = existingWidget.header + if (existingWidget.isCalendarsConfigured()) { + mSelectedCalendars = existingWidget.getCalendarIdsAsList() + .map { it.toString() }.toHashSet() + } + runOnUiThread { + updateSelectedPeriod(existingWidget.period) + binding.showWidgetHeader.isChecked = existingWidget.header + binding.configWidgetPreview.widgetHeaderInclude.widgetHeader.beVisibleIf(existingWidget.header) + updateCalendarPickerLabel() + } + } + mWidgetLoaded = true + runOnUiThread { + binding.configSave.isEnabled = true + } + } } private fun initVariables() { @@ -111,7 +141,14 @@ class WidgetListConfigureActivity : SimpleActivity() { } private fun saveConfig() { - val widget = Widget(null, mWidgetId, mSelectedPeriodOption, binding.showWidgetHeader.isChecked) + if (!mWidgetLoaded) return + + val calendarsStr = if (mSelectedCalendars.isEmpty()) { + null + } else { + mSelectedCalendars.joinToString(",") + } + val widget = Widget(null, mWidgetId, mSelectedPeriodOption, binding.showWidgetHeader.isChecked, calendarsStr) ensureBackgroundThread { widgetsDB.insertOrUpdate(widget) } @@ -238,6 +275,31 @@ class WidgetListConfigureActivity : SimpleActivity() { binding.configSave.backgroundTintList = ColorStateList.valueOf(getProperPrimaryColor()) } + @Deprecated("Deprecated in Java") + override fun onBackPressed() { + if (mIsReconfiguring) { + finish() + } else { + @Suppress("DEPRECATION") + super.onBackPressed() + } + } + + private fun showCalendarSelector() { + SelectCalendarsDialog(this, mSelectedCalendars) { selectedCalendars -> + mSelectedCalendars = selectedCalendars + updateCalendarPickerLabel() + } + } + + private fun updateCalendarPickerLabel() { + if (mSelectedCalendars.isEmpty()) { + binding.calendarPickerValue.setText(R.string.widget_calendars_all) + } else { + binding.calendarPickerValue.text = getString(R.string.widget_calendars_summary) + } + } + private fun getListItems(): ArrayList { val listItems = ArrayList(10) var dateTime = DateTime.now().withTime(0, 0, 0, 0).plusDays(1) diff --git a/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt b/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt index 2644c4b9c..3301294cd 100644 --- a/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt +++ b/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt @@ -178,6 +178,12 @@ class EventListWidgetAdapter(val context: Context, val intent: Intent) : RemoteV override fun onDataSetChanged() { initConfigValues() val period = intent.getIntExtra(EVENT_LIST_PERIOD, 0) + val calendarsExtra = intent.getStringExtra(EVENT_LIST_CALENDARS) ?: "" + val overrideCalendarIds: List? = if (calendarsExtra.isNotEmpty()) { + calendarsExtra.split(",").mapNotNull { it.trim().toLongOrNull() } + } else { + null + } val currentDate = DateTime() val fromTS = currentDate.seconds() - context.config.displayPastEvents * 60 val toTS = when (period) { @@ -185,7 +191,7 @@ class EventListWidgetAdapter(val context: Context, val intent: Intent) : RemoteV EVENT_PERIOD_TODAY -> currentDate.withTime(23, 59, 59, 999).seconds() else -> currentDate.plusSeconds(period).seconds() } - context.eventsHelper.getEventsSync(fromTS, toTS, applyTypeFilter = true) { + context.eventsHelper.getEventsSync(fromTS, toTS, applyTypeFilter = true, callback = { val listItems = ArrayList(it.size) val replaceDescription = context.config.replaceDescription val sorted = it.sortedWith(compareBy { event -> @@ -244,7 +250,7 @@ class EventListWidgetAdapter(val context: Context, val intent: Intent) : RemoteV } this@EventListWidgetAdapter.events = listItems - } + }, overrideCalendarIds = overrideCalendarIds) } override fun hasStableIds() = true diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt index 0609177f7..6ebc3cbdc 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt @@ -446,10 +446,11 @@ class EventsHelper(val context: Context) { eventId: Long = -1L, applyTypeFilter: Boolean = true, searchQuery: String = "", + overrideCalendarIds: List? = null, callback: (events: ArrayList) -> Unit ) { ensureBackgroundThread { - getEventsSync(fromTS, toTS, eventId, applyTypeFilter, searchQuery, callback) + getEventsSync(fromTS, toTS, eventId, applyTypeFilter, searchQuery, overrideCalendarIds, callback) } } @@ -459,13 +460,39 @@ class EventsHelper(val context: Context) { eventId: Long = -1L, applyTypeFilter: Boolean, searchQuery: String = "", + overrideCalendarIds: List? = null, callback: (events: ArrayList) -> Unit ) { val birthDayEventId = getLocalBirthdaysCalendarId(createIfNotExists = false) val anniversaryEventId = getAnniversariesCalendarId(createIfNotExists = false) var events = ArrayList() - if (applyTypeFilter) { + if (overrideCalendarIds != null) { + if (overrideCalendarIds.isEmpty()) { + callback(ArrayList()) + return + } + try { + events.addAll( + eventsDB.getOneTimeEventsFromToWithCalendarIds( + toTS, + fromTS, + overrideCalendarIds + ).toMutableList() as ArrayList + ) + } catch (e: Exception) { + } + events.addAll( + getRepeatableEventsFor( + fromTS, + toTS, + eventId, + applyTypeFilter = false, + searchQuery, + overrideCalendarIds + ) + ) + } else if (applyTypeFilter) { val displayCalendars = context.config.displayCalendars if (displayCalendars.isEmpty()) { callback(ArrayList()) @@ -509,7 +536,9 @@ class EventsHelper(val context: Context) { ) } - events.addAll(getRepeatableEventsFor(fromTS, toTS, eventId, applyTypeFilter, searchQuery)) + if (overrideCalendarIds == null) { + events.addAll(getRepeatableEventsFor(fromTS, toTS, eventId, applyTypeFilter, searchQuery)) + } events = events .asSequence() @@ -599,9 +628,15 @@ class EventsHelper(val context: Context) { toTS: Long, eventId: Long = -1L, applyTypeFilter: Boolean = false, - searchQuery: String = "" + searchQuery: String = "", + overrideCalendarIds: List? = null ): List { - val events = if (applyTypeFilter) { + val events = if (overrideCalendarIds != null) { + eventsDB.getRepeatableEventsOrTasksWithCalendarIds( + toTS, + overrideCalendarIds + ).toMutableList() as ArrayList + } else if (applyTypeFilter) { val displayCalendars = context.config.displayCalendars if (displayCalendars.isEmpty()) { return ArrayList() diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt index e14e4f2bf..8abc74b19 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt @@ -11,6 +11,7 @@ import android.view.View import android.widget.RemoteViews import org.fossify.calendar.R import org.fossify.calendar.activities.SplashActivity +import org.fossify.calendar.activities.WidgetListConfigureActivity import org.fossify.calendar.extensions.config import org.fossify.calendar.extensions.getWidgetFontSize import org.fossify.calendar.extensions.launchNewEventOrTaskActivity @@ -18,6 +19,7 @@ import org.fossify.calendar.extensions.widgetsDB import org.fossify.calendar.services.WidgetService import org.fossify.calendar.services.WidgetServiceEmpty import org.fossify.commons.extensions.* +import org.fossify.commons.helpers.IS_CUSTOMIZING_COLORS import org.fossify.commons.helpers.ensureBackgroundThread import org.joda.time.DateTime @@ -68,8 +70,22 @@ class MyWidgetListProvider : AppWidgetProvider() { views.setImageViewBitmap(R.id.widget_event_go_to_today, context.resources.getColoredBitmap(R.drawable.ic_today_vector, textColor)) setupIntent(context, views, GO_TO_TODAY, R.id.widget_event_go_to_today) + views.setImageViewBitmap(R.id.widget_event_configure, context.resources.getColoredBitmap(R.drawable.ic_settings_vector, textColor)) + val configIntent = Intent(context, WidgetListConfigureActivity::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, it) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + val configPendingIntent = PendingIntent.getActivity( + context, + it, + configIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + views.setOnClickPendingIntent(R.id.widget_event_configure, configPendingIntent) + Intent(context, WidgetService::class.java).apply { putExtra(EVENT_LIST_PERIOD, widget?.period) + putExtra(EVENT_LIST_CALENDARS, widget?.calendars ?: "") data = Uri.parse(this.toUri(Intent.URI_INTENT_SCHEME)) views.setRemoteAdapter(R.id.widget_event_list, this) } @@ -139,6 +155,7 @@ class MyWidgetListProvider : AppWidgetProvider() { setViewVisibility(R.id.widget_event_list_today, headerVisibility) setViewVisibility(R.id.widget_event_go_to_today, headerVisibility) setViewVisibility(R.id.widget_event_new_event, headerVisibility) + setViewVisibility(R.id.widget_event_configure, headerVisibility) } Intent(context, WidgetServiceEmpty::class.java).apply { data = Uri.parse(this.toUri(Intent.URI_INTENT_SCHEME)) diff --git a/app/src/main/res/layout/widget_config_list.xml b/app/src/main/res/layout/widget_config_list.xml index 9b452ac56..dc05684ed 100644 --- a/app/src/main/res/layout/widget_config_list.xml +++ b/app/src/main/res/layout/widget_config_list.xml @@ -59,13 +59,41 @@ + + + + + + + + + + Date: Thu, 23 Apr 2026 19:03:35 -0400 Subject: [PATCH 04/11] chore: add Room schema 12.json Generated by KSP during build. Captures the new nullable calendars TEXT column on the widgets table introduced by migration 11->12. --- CHANGELOG.md | 4 + .../12.json | 384 ++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 app/schemas/org.fossify.calendar.databases.EventsDatabase/12.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d1bb0ef7d..d7c42ee72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Database schema migration 11→12: added nullable calendars column to widget table - Refactored widget config and header layouts for calendar picker integration - Updated holiday data +- Updated holiday data ### Fixed - Fixed event text readability on colored backgrounds ([#1065]) @@ -250,7 +251,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#1019]: https://github.com/FossifyOrg/Calendar/issues/1019 [#1024]: https://github.com/FossifyOrg/Calendar/issues/1024 [#1065]: https://github.com/FossifyOrg/Calendar/issues/1065 +<<<<<<< HEAD [#1157]: https://github.com/FossifyOrg/Calendar/issues/1157 +======= +>>>>>>> 1c136aede (chore: add Room schema 12.json) [Unreleased]: https://github.com/FossifyOrg/Calendar/compare/1.10.3...HEAD [1.10.3]: https://github.com/FossifyOrg/Calendar/compare/1.10.2...1.10.3 diff --git a/app/schemas/org.fossify.calendar.databases.EventsDatabase/12.json b/app/schemas/org.fossify.calendar.databases.EventsDatabase/12.json new file mode 100644 index 000000000..b0c04da12 --- /dev/null +++ b/app/schemas/org.fossify.calendar.databases.EventsDatabase/12.json @@ -0,0 +1,384 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "09c1bece75c70dcf75c0d4b3d3ccfcac", + "entities": [ + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `start_ts` INTEGER NOT NULL, `end_ts` INTEGER NOT NULL, `title` TEXT NOT NULL, `location` TEXT NOT NULL, `description` TEXT NOT NULL, `reminder_1_minutes` INTEGER NOT NULL, `reminder_2_minutes` INTEGER NOT NULL, `reminder_3_minutes` INTEGER NOT NULL, `reminder_1_type` INTEGER NOT NULL, `reminder_2_type` INTEGER NOT NULL, `reminder_3_type` INTEGER NOT NULL, `repeat_interval` INTEGER NOT NULL, `repeat_rule` INTEGER NOT NULL, `repeat_limit` INTEGER NOT NULL, `repetition_exceptions` TEXT NOT NULL, `attendees` TEXT NOT NULL, `import_id` TEXT NOT NULL, `time_zone` TEXT NOT NULL, `flags` INTEGER NOT NULL, `event_type` INTEGER NOT NULL, `parent_id` INTEGER NOT NULL, `last_updated` INTEGER NOT NULL, `source` TEXT NOT NULL, `availability` INTEGER NOT NULL, `access_level` INTEGER NOT NULL, `color` INTEGER NOT NULL, `type` INTEGER NOT NULL, `status` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "startTS", + "columnName": "start_ts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endTS", + "columnName": "end_ts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reminder1Minutes", + "columnName": "reminder_1_minutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder2Minutes", + "columnName": "reminder_2_minutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder3Minutes", + "columnName": "reminder_3_minutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder1Type", + "columnName": "reminder_1_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder2Type", + "columnName": "reminder_2_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reminder3Type", + "columnName": "reminder_3_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatInterval", + "columnName": "repeat_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatRule", + "columnName": "repeat_rule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatLimit", + "columnName": "repeat_limit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repetitionExceptions", + "columnName": "repetition_exceptions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attendees", + "columnName": "attendees", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "importId", + "columnName": "import_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "calendarId", + "columnName": "event_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "availability", + "columnName": "availability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessLevel", + "columnName": "access_level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_events_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_events_id` ON `${TABLE_NAME}` (`id`)" + } + ] + }, + { + "tableName": "event_types", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `color` INTEGER NOT NULL, `caldav_calendar_id` INTEGER NOT NULL, `caldav_display_name` TEXT NOT NULL, `caldav_email` TEXT NOT NULL, `type` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "caldavCalendarId", + "columnName": "caldav_calendar_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "caldavDisplayName", + "columnName": "caldav_display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caldavEmail", + "columnName": "caldav_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_event_types_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_event_types_id` ON `${TABLE_NAME}` (`id`)" + } + ] + }, + { + "tableName": "widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `widget_id` INTEGER NOT NULL, `period` INTEGER NOT NULL, `header` INTEGER NOT NULL, `calendars` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "period", + "columnName": "period", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "calendars", + "columnName": "calendars", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_widgets_widget_id", + "unique": true, + "columnNames": [ + "widget_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_widgets_widget_id` ON `${TABLE_NAME}` (`widget_id`)" + } + ] + }, + { + "tableName": "tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `task_id` INTEGER NOT NULL, `start_ts` INTEGER NOT NULL, `flags` INTEGER NOT NULL, FOREIGN KEY(`task_id`) REFERENCES `events`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "task_id", + "columnName": "task_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTS", + "columnName": "start_ts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tasks_id_task_id", + "unique": true, + "columnNames": [ + "id", + "task_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tasks_id_task_id` ON `${TABLE_NAME}` (`id`, `task_id`)" + } + ], + "foreignKeys": [ + { + "table": "events", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "task_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09c1bece75c70dcf75c0d4b3d3ccfcac')" + ] + } +} \ No newline at end of file From 6038b47b836aa9497aee1f7e71e3b392b02250ae Mon Sep 17 00:00:00 2001 From: Kurian Vithayathil <1056073+kvithayathil@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:08:29 -0400 Subject: [PATCH 05/11] fix(widget): pre-check all calendars when default selection is used --- .../activities/WidgetListConfigureActivity.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt b/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt index 2d4144a48..51c7f13be 100644 --- a/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt +++ b/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt @@ -13,6 +13,7 @@ import org.fossify.calendar.databinding.WidgetConfigListBinding import org.fossify.calendar.dialogs.CustomPeriodPickerDialog import org.fossify.calendar.dialogs.SelectCalendarsDialog import org.fossify.calendar.extensions.config +import org.fossify.calendar.extensions.eventsHelper import org.fossify.calendar.extensions.seconds import org.fossify.calendar.extensions.widgetsDB import org.fossify.calendar.helpers.EVENT_PERIOD_CUSTOM @@ -47,7 +48,7 @@ class WidgetListConfigureActivity : SimpleActivity() { public override fun onCreate(savedInstanceState: Bundle?) { useDynamicTheme = false super.onCreate(savedInstanceState) - setResult(Activity.RESULT_CANCELED) + setResult(RESULT_CANCELED) setContentView(binding.root) setupEdgeToEdge(padTopSystem = listOf(binding.configListHolder), padBottomSystem = listOf(binding.root)) initVariables() @@ -286,9 +287,19 @@ class WidgetListConfigureActivity : SimpleActivity() { } private fun showCalendarSelector() { - SelectCalendarsDialog(this, mSelectedCalendars) { selectedCalendars -> - mSelectedCalendars = selectedCalendars - updateCalendarPickerLabel() + if (mSelectedCalendars.isEmpty()) { + eventsHelper.getCalendars(this, false) { calendars -> + val allIds = calendars.map { it.id.toString() }.toHashSet() + SelectCalendarsDialog(this, allIds) { selectedCalendars -> + mSelectedCalendars = selectedCalendars + updateCalendarPickerLabel() + } + } + } else { + SelectCalendarsDialog(this, mSelectedCalendars) { selectedCalendars -> + mSelectedCalendars = selectedCalendars + updateCalendarPickerLabel() + } } } From 990901b397d6a6d45e7954f3e51ed664a1d809d1 Mon Sep 17 00:00:00 2001 From: Kurian Vithayathil <1056073+kvithayathil@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:27:17 -0400 Subject: [PATCH 06/11] feat(widget): add 3-state calendar picker to widget config Add calendar selection UI to the widget configuration screen with explicit tracking of user intent to distinguish three states: - null (never configured): falls back to global filter, shows 'All calendars' - empty string (explicitly chose none): shows 'No calendars selected' + warning - comma-separated IDs: shows 'N calendars selected' Changes: - Flatten widget_config_list.xml to ConstraintLayout - Add calendar picker button with SelectCalendarsDialog - Track mCalendarsExplicitlyChosen flag through config/reconfig cycle - Fix saveConfig to preserve explicit-zero as empty string sentinel - Fix updateCalendarPickerLabel for 3-state display - Show warning when zero calendars explicitly selected - Fix EventListWidgetAdapter intent parsing for null vs empty-string - Fix MyWidgetListProvider to only set calendars extra when non-null - Pre-check all calendars when default selection is used - Add ic_calendar_add_vector drawable and dimension resources --- .../activities/WidgetListConfigureActivity.kt | 44 ++++--- .../adapters/EventListWidgetAdapter.kt | 11 +- .../calendar/helpers/MyWidgetListProvider.kt | 2 +- .../res/drawable/ic_calendar_add_vector.xml | 20 +++ .../main/res/layout/widget_config_list.xml | 122 +++++++++--------- app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 10 +- 7 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 app/src/main/res/drawable/ic_calendar_add_vector.xml diff --git a/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt b/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt index 51c7f13be..47270d6e2 100644 --- a/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt +++ b/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt @@ -1,12 +1,11 @@ package org.fossify.calendar.activities -import android.app.Activity import android.appwidget.AppWidgetManager import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Bundle +import androidx.core.graphics.drawable.toDrawable import org.fossify.calendar.R import org.fossify.calendar.adapters.EventListAdapter import org.fossify.calendar.databinding.WidgetConfigListBinding @@ -42,6 +41,7 @@ class WidgetListConfigureActivity : SimpleActivity() { private var mSelectedCalendars = HashSet() private var mIsReconfiguring = false private var mWidgetLoaded = false + private var mCalendarsExplicitlyChosen = false private val binding by viewBinding(WidgetConfigListBinding::inflate) @@ -66,7 +66,9 @@ class WidgetListConfigureActivity : SimpleActivity() { configWidgetPreview.configEventsList.adapter = this } - periodPickerHolder.background = ColorDrawable(getProperBackgroundColor()) + val currentBgColor = getProperBackgroundColor().toDrawable() + + periodPickerHolder.background = currentBgColor periodPickerValue.setOnClickListener { showPeriodSelector() } configSave.setOnClickListener { saveConfig() } @@ -79,7 +81,7 @@ class WidgetListConfigureActivity : SimpleActivity() { configBgSeekbar.setColors(mTextColor, primaryColor, primaryColor) configSave.isEnabled = false - calendarPickerValue.setOnClickListener { showCalendarSelector() } + calendarPickerHolder.setOnClickListener { showCalendarSelector() } } updateSelectedPeriod(config.lastUsedEventSpan) @@ -103,12 +105,13 @@ class WidgetListConfigureActivity : SimpleActivity() { if (existingWidget.isCalendarsConfigured()) { mSelectedCalendars = existingWidget.getCalendarIdsAsList() .map { it.toString() }.toHashSet() + mCalendarsExplicitlyChosen = true } runOnUiThread { updateSelectedPeriod(existingWidget.period) binding.showWidgetHeader.isChecked = existingWidget.header binding.configWidgetPreview.widgetHeaderInclude.widgetHeader.beVisibleIf(existingWidget.header) - updateCalendarPickerLabel() + updateCalendarSelectionDisplay(mSelectedCalendars) } } mWidgetLoaded = true @@ -144,10 +147,10 @@ class WidgetListConfigureActivity : SimpleActivity() { private fun saveConfig() { if (!mWidgetLoaded) return - val calendarsStr = if (mSelectedCalendars.isEmpty()) { - null - } else { + val calendarsStr = if (mCalendarsExplicitlyChosen) { mSelectedCalendars.joinToString(",") + } else { + null } val widget = Widget(null, mWidgetId, mSelectedPeriodOption, binding.showWidgetHeader.isChecked, calendarsStr) ensureBackgroundThread { @@ -162,7 +165,7 @@ class WidgetListConfigureActivity : SimpleActivity() { Intent().apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) - setResult(Activity.RESULT_OK, this) + setResult(RESULT_OK, this) } finish() } @@ -291,24 +294,32 @@ class WidgetListConfigureActivity : SimpleActivity() { eventsHelper.getCalendars(this, false) { calendars -> val allIds = calendars.map { it.id.toString() }.toHashSet() SelectCalendarsDialog(this, allIds) { selectedCalendars -> + mCalendarsExplicitlyChosen = true mSelectedCalendars = selectedCalendars - updateCalendarPickerLabel() + updateCalendarSelectionDisplay(selectedCalendars) } } } else { SelectCalendarsDialog(this, mSelectedCalendars) { selectedCalendars -> + mCalendarsExplicitlyChosen = true mSelectedCalendars = selectedCalendars - updateCalendarPickerLabel() + updateCalendarSelectionDisplay(selectedCalendars) } } } - private fun updateCalendarPickerLabel() { - if (mSelectedCalendars.isEmpty()) { - binding.calendarPickerValue.setText(R.string.widget_calendars_all) - } else { - binding.calendarPickerValue.text = getString(R.string.widget_calendars_summary) + private fun updateCalendarSelectionDisplay(selectedCalendars: Collection) { + val label = when { + selectedCalendars.isEmpty() && !mCalendarsExplicitlyChosen -> + getString(R.string.widget_calendars_selected_all) + else -> + resources.getQuantityString( + R.plurals.widget_calendars_selected_count, + selectedCalendars.size, + selectedCalendars.size + ) } + binding.calendarPickerHolder.text = label } private fun getListItems(): ArrayList { @@ -379,7 +390,6 @@ class WidgetListConfigureActivity : SimpleActivity() { color = getProperPrimaryColor(), ) ) - return listItems } } diff --git a/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt b/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt index 3301294cd..892934545 100644 --- a/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt +++ b/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt @@ -178,9 +178,14 @@ class EventListWidgetAdapter(val context: Context, val intent: Intent) : RemoteV override fun onDataSetChanged() { initConfigValues() val period = intent.getIntExtra(EVENT_LIST_PERIOD, 0) - val calendarsExtra = intent.getStringExtra(EVENT_LIST_CALENDARS) ?: "" - val overrideCalendarIds: List? = if (calendarsExtra.isNotEmpty()) { - calendarsExtra.split(",").mapNotNull { it.trim().toLongOrNull() } + // null extra = never configured (global filter); empty string = explicit-zero (show nothing) + val calendarsExtra = intent.getStringExtra(EVENT_LIST_CALENDARS) + val overrideCalendarIds: List? = if (calendarsExtra != null) { + if (calendarsExtra.isNotEmpty()) { + calendarsExtra.split(",").mapNotNull { it.trim().toLongOrNull() } + } else { + emptyList() + } } else { null } diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt index 8abc74b19..ead3f29f2 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetListProvider.kt @@ -85,7 +85,7 @@ class MyWidgetListProvider : AppWidgetProvider() { Intent(context, WidgetService::class.java).apply { putExtra(EVENT_LIST_PERIOD, widget?.period) - putExtra(EVENT_LIST_CALENDARS, widget?.calendars ?: "") + widget?.calendars?.let { putExtra(EVENT_LIST_CALENDARS, it) } data = Uri.parse(this.toUri(Intent.URI_INTENT_SCHEME)) views.setRemoteAdapter(R.id.widget_event_list, this) } diff --git a/app/src/main/res/drawable/ic_calendar_add_vector.xml b/app/src/main/res/drawable/ic_calendar_add_vector.xml new file mode 100644 index 000000000..740fc2a2b --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_add_vector.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/widget_config_list.xml b/app/src/main/res/layout/widget_config_list.xml index dc05684ed..05ac5d6b5 100644 --- a/app/src/main/res/layout/widget_config_list.xml +++ b/app/src/main/res/layout/widget_config_list.xml @@ -1,29 +1,36 @@ - - + - - + android:orientation="vertical" + android:paddingStart="@dimen/activity_margin" + android:paddingEnd="@dimen/activity_margin" + android:paddingBottom="@dimen/activity_margin" + app:layout_constraintTop_toTopOf="parent"> - + android:paddingTop="@dimen/activity_margin" + android:paddingBottom="@dimen/activity_margin"> - - + app:switchPadding="@dimen/medium_margin"/> + + android:text="@string/show_events_happening"/> - - - - - + android:text="@string/within_the_next_one_year" + android:textAllCaps="false"/> + android:text="@string/widget_calendars_selection_header"/> - - - + android:text="@string/widget_calendars_selected_all" + android:textAllCaps="false" + android:drawableStart="@drawable/ic_calendar_add_vector" + android:drawablePadding="@dimen/small_margin" + tools:text="@string/widget_calendars_selected_all"/> + + app:layout_constraintTop_toBottomOf="@id/period_picker_holder" + app:layout_constraintBottom_toTopOf="@id/config_bg_color" + /> + android:layout_marginBottom="@dimen/tiny_margin" + app:layout_constraintStart_toStartOf="@id/config_text_color" + app:layout_constraintBottom_toTopOf="@id/config_text_color" + /> - + android:background="@drawable/widget_config_seekbar_background" + app:layout_constraintStart_toEndOf="@id/config_bg_color" + app:layout_constraintTop_toTopOf="@id/config_bg_color" + app:layout_constraintEnd_toEndOf="parent"> + android:paddingEnd="@dimen/activity_margin"/> - + + android:layout_margin="@dimen/tiny_margin" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent"/>