diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a6fc1f1..a092d8f22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,14 @@ 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]) - Grid support for monthly calendar widget ([#406]) ### 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 @@ -252,7 +256,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 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..1dc1c408c 100644 --- a/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt +++ b/app/src/main/kotlin/org/fossify/calendar/activities/WidgetListConfigureActivity.kt @@ -1,17 +1,18 @@ 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 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 @@ -37,13 +38,17 @@ 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 var mCalendarsExplicitlyChosen = false private val binding by viewBinding(WidgetConfigListBinding::inflate) 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() @@ -61,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() } @@ -72,6 +79,9 @@ class WidgetListConfigureActivity : SimpleActivity() { val primaryColor = getProperPrimaryColor() configBgSeekbar.setColors(mTextColor, primaryColor, primaryColor) + + configSave.isEnabled = false + calendarPickerHolder.setOnClickListener { showCalendarSelector() } } updateSelectedPeriod(config.lastUsedEventSpan) @@ -85,6 +95,30 @@ 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() + mCalendarsExplicitlyChosen = true + } + runOnUiThread { + updateSelectedPeriod(existingWidget.period) + binding.showWidgetHeader.isChecked = existingWidget.header + binding.configWidgetPreview.widgetHeaderInclude.widgetHeader.beVisibleIf(existingWidget.header) + updateCalendarSelectionDisplay(mSelectedCalendars) + } + } + mWidgetLoaded = true + runOnUiThread { + binding.configSave.isEnabled = true + } + } } private fun initVariables() { @@ -111,7 +145,14 @@ class WidgetListConfigureActivity : SimpleActivity() { } private fun saveConfig() { - val widget = Widget(null, mWidgetId, mSelectedPeriodOption, binding.showWidgetHeader.isChecked) + if (!mWidgetLoaded) return + + val calendarsStr = if (mCalendarsExplicitlyChosen) { + mSelectedCalendars.joinToString(",") + } else { + null + } + val widget = Widget(null, mWidgetId, mSelectedPeriodOption, binding.showWidgetHeader.isChecked, calendarsStr) ensureBackgroundThread { widgetsDB.insertOrUpdate(widget) } @@ -124,7 +165,7 @@ class WidgetListConfigureActivity : SimpleActivity() { Intent().apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) - setResult(Activity.RESULT_OK, this) + setResult(RESULT_OK, this) } finish() } @@ -238,6 +279,48 @@ class WidgetListConfigureActivity : SimpleActivity() { binding.configSave.backgroundTintList = ColorStateList.valueOf(getProperPrimaryColor()) } + override fun onBackPressedCompat(): Boolean { + if (mIsReconfiguring) { + finish() + } else { + performDefaultBack() + } + return true + } + + private fun showCalendarSelector() { + if (mSelectedCalendars.isEmpty()) { + eventsHelper.getCalendars(this, false) { calendars -> + val allIds = calendars.map { it.id.toString() }.toHashSet() + SelectCalendarsDialog(this, allIds) { selectedCalendars -> + mCalendarsExplicitlyChosen = true + mSelectedCalendars = selectedCalendars + updateCalendarSelectionDisplay(selectedCalendars) + } + } + } else { + SelectCalendarsDialog(this, mSelectedCalendars) { selectedCalendars -> + mCalendarsExplicitlyChosen = true + mSelectedCalendars = selectedCalendars + updateCalendarSelectionDisplay(selectedCalendars) + } + } + } + + 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 { val listItems = ArrayList(10) var dateTime = DateTime.now().withTime(0, 0, 0, 0).plusDays(1) @@ -306,7 +389,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 2644c4b9c..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,6 +178,17 @@ class EventListWidgetAdapter(val context: Context, val intent: Intent) : RemoteV override fun onDataSetChanged() { initConfigValues() val period = intent.getIntExtra(EVENT_LIST_PERIOD, 0) + // 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 + } val currentDate = DateTime() val fromTS = currentDate.seconds() - context.config.displayPastEvents * 60 val toTS = when (period) { @@ -185,7 +196,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 +255,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/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 45ed8acab..124ef942f 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/helpers/EventsHelper.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt index 0609177f7..5c5954758 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt @@ -446,70 +446,114 @@ 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) } } + private fun getEventsForCalendars( + fromTS: Long, + toTS: Long, + eventId: Long, + searchQuery: String, + calendarIds: List + ): ArrayList { + if (calendarIds.isEmpty()) return ArrayList() + val events = ArrayList() + try { + events.addAll( + eventsDB.getOneTimeEventsFromToWithCalendarIds( + toTS, fromTS, calendarIds + ).toMutableList() as ArrayList + ) + } catch (e: Exception) { + } + events.addAll( + getRepeatableEventsFor( + fromTS, toTS, eventId, applyTypeFilter = false, searchQuery, calendarIds + ) + ) + return events + } + + private fun getDisplayFilteredEvents( + fromTS: Long, + toTS: Long, + searchQuery: String + ): ArrayList { + val displayCalendars = context.config.displayCalendars + if (displayCalendars.isEmpty()) return ArrayList() + val events = ArrayList() + try { + val typesList = context.config.getDisplayCalendarsAsList() + events.addAll( + if (searchQuery.isEmpty()) { + eventsDB.getOneTimeEventsFromToWithCalendarIds( + toTS, fromTS, typesList + ).toMutableList() as ArrayList + } else { + eventsDB.getOneTimeEventsFromToWithTypesForSearch( + toTS, fromTS, typesList, "%$searchQuery%" + ).toMutableList() as ArrayList + } + ) + } catch (e: Exception) { + } + return events + } + + private fun getUnfilteredEvents( + fromTS: Long, toTS: Long, eventId: Long + ): ArrayList { + val events = ArrayList() + events.addAll(eventsDB.getTasksFromTo(fromTS, toTS, ArrayList())) + events.addAll( + if (eventId == -1L) { + eventsDB.getOneTimeEventsOrTasksFromTo(toTS, fromTS) + .toMutableList() as ArrayList + } else { + eventsDB.getOneTimeEventFromToWithId(eventId, toTS, fromTS) + .toMutableList() as ArrayList + } + ) + return events + } + fun getEventsSync( fromTS: Long, toTS: Long, 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) { - val displayCalendars = context.config.displayCalendars - if (displayCalendars.isEmpty()) { - callback(ArrayList()) + if (overrideCalendarIds != null) { + events = getEventsForCalendars(fromTS, toTS, eventId, searchQuery, overrideCalendarIds) + if (events.isEmpty()) { + callback(events) + return + } + } else if (applyTypeFilter) { + events = getDisplayFilteredEvents(fromTS, toTS, searchQuery) + if (events.isEmpty()) { + callback(events) return - } else { - try { - val typesList = context.config.getDisplayCalendarsAsList() - - if (searchQuery.isEmpty()) { - events.addAll( - eventsDB.getOneTimeEventsFromToWithCalendarIds( - toTS, - fromTS, - typesList - ).toMutableList() as ArrayList - ) - } else { - events.addAll( - eventsDB.getOneTimeEventsFromToWithTypesForSearch( - toTS, - fromTS, - typesList, - "%$searchQuery%" - ).toMutableList() as ArrayList - ) - } - } catch (e: Exception) { - } } } else { - events.addAll(eventsDB.getTasksFromTo(fromTS, toTS, ArrayList())) - - events.addAll( - if (eventId == -1L) { - eventsDB.getOneTimeEventsOrTasksFromTo(toTS, fromTS) - .toMutableList() as ArrayList - } else { - eventsDB.getOneTimeEventFromToWithId(eventId, toTS, fromTS) - .toMutableList() as ArrayList - } - ) + events = getUnfilteredEvents(fromTS, toTS, eventId) } - events.addAll(getRepeatableEventsFor(fromTS, toTS, eventId, applyTypeFilter, searchQuery)) + if (overrideCalendarIds == null) { + events.addAll(getRepeatableEventsFor(fromTS, toTS, eventId, applyTypeFilter, searchQuery)) + } events = events .asSequence() @@ -518,34 +562,41 @@ class EventsHelper(val context: Context) { .toMutableList() as ArrayList val calendarColors = getCalendarColors() - events.forEach { - if (it.isTask()) { - updateIsTaskCompleted(it) - } + decorateEvent(it, birthDayEventId, anniversaryEventId, calendarColors) + } - it.updateIsPastEvent() - val originalEvent = eventsDB.getEventWithId(it.id!!) - if (originalEvent != null && - (birthDayEventId != -1L && it.calendarId == birthDayEventId) or - (anniversaryEventId != -1L && it.calendarId == anniversaryEventId) - ) { - val eventStartDate = Formatter.getDateFromTS(it.startTS) - val originalEventStartDate = Formatter.getDateFromTS(originalEvent.startTS) - if (it.hasMissingYear().not()) { - val years = (eventStartDate.year - originalEventStartDate.year).coerceAtLeast(0) - if (years > 0) { - it.title = "${it.title} ($years)" - } - } - } + callback(events) + } - if (it.color == 0) { - it.color = calendarColors.get(it.calendarId) ?: context.getProperPrimaryColor() + private fun decorateEvent( + event: Event, + birthDayEventId: Long, + anniversaryEventId: Long, + calendarColors: LongSparseArray + ) { + if (event.isTask()) { + updateIsTaskCompleted(event) + } + + event.updateIsPastEvent() + val originalEvent = eventsDB.getEventWithId(event.id!!) + val isBirthday = birthDayEventId != -1L && event.calendarId == birthDayEventId + val isAnniversary = anniversaryEventId != -1L && event.calendarId == anniversaryEventId + if (originalEvent != null && (isBirthday || isAnniversary)) { + val eventStartDate = Formatter.getDateFromTS(event.startTS) + val originalEventStartDate = Formatter.getDateFromTS(originalEvent.startTS) + if (event.hasMissingYear().not()) { + val years = (eventStartDate.year - originalEventStartDate.year).coerceAtLeast(0) + if (years > 0) { + event.title = "${event.title} ($years)" + } } } - callback(events) + if (event.color == 0) { + event.color = calendarColors.get(event.calendarId) ?: context.getProperPrimaryColor() + } } fun createPredefinedCalendar( @@ -599,9 +650,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..04b7ce66d 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,25 @@ 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) + val configBitmap = context.resources.getColoredBitmap( + R.drawable.ic_settings_vector, textColor + ) + views.setImageViewBitmap(R.id.widget_event_configure, configBitmap) + 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) + 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) } @@ -139,6 +158,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/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() + } + } +} 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/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 23fadeb21..f6b578ed5 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,3 +1,3 @@ - + 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/layout/widget_config_list.xml b/app/src/main/res/layout/widget_config_list.xml index 9b452ac56..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"/> + + - + + + 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"/>