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
Original file line number Diff line number Diff line change
Expand Up @@ -341,5 +341,86 @@ class ApplicationControllerCoreTest {
}
assertFalse("Task event should NOT be muted", updatedEvent?.isMuted == true)
}

// === muteAllVisibleEvents does NOT skip pinned (pinning only affects batch snooze) ===

@Test
fun testMuteAllVisibleEvents_mutesPinnedEvents() {
DevLog.info(LOG_TAG, "Running testMuteAllVisibleEvents_mutesPinnedEvents")

val pinnedEventId = 100040L
val normalEventId = 100041L
val pinnedEvent = createTestEvent(eventId = pinnedEventId, isMuted = false).apply { isPinned = true }
val normalEvent = createTestEvent(eventId = normalEventId, isMuted = false)
EventsStorage(context).use { db ->
db.addEvent(pinnedEvent)
db.addEvent(normalEvent)
}

ApplicationController.muteAllVisibleEvents(context)

EventsStorage(context).use { db ->
val updatedPinned = db.getEvent(pinnedEventId, baseTime)
val updatedNormal = db.getEvent(normalEventId, baseTime)
assertTrue("Pinned event should be muted (pinning only affects batch snooze)", updatedPinned?.isMuted == true)
assertTrue("Normal event should be muted", updatedNormal?.isMuted == true)
}
}

// === pinAllVisibleEvents / unpinAllVisibleEvents ===

@Test
fun testPinAllVisibleEvents_pinsVisibleEvents() {
DevLog.info(LOG_TAG, "Running testPinAllVisibleEvents_pinsVisibleEvents")

val eventId = 100042L
val event = createTestEvent(eventId = eventId)
EventsStorage(context).use { db ->
db.addEvent(event)
}

ApplicationController.pinAllVisibleEvents(context)

val updatedEvent = EventsStorage(context).use { db ->
db.getEvent(eventId, baseTime)
}
assertTrue("Visible event should be pinned", updatedEvent?.isPinned == true)
}

@Test
fun testPinAllVisibleEvents_skipsSnoozedEvents() {
DevLog.info(LOG_TAG, "Running testPinAllVisibleEvents_skipsSnoozedEvents")

val eventId = 100043L
val event = createTestEvent(eventId = eventId, snoozedUntil = baseTime + 3600000L)
EventsStorage(context).use { db ->
db.addEvent(event)
}

ApplicationController.pinAllVisibleEvents(context)

val updatedEvent = EventsStorage(context).use { db ->
db.getEvent(eventId, baseTime)
}
assertFalse("Snoozed event should NOT be pinned", updatedEvent?.isPinned == true)
}

@Test
fun testUnpinAllVisibleEvents_unpinsAll() {
DevLog.info(LOG_TAG, "Running testUnpinAllVisibleEvents_unpinsAll")

val eventId = 100044L
val event = createTestEvent(eventId = eventId).apply { isPinned = true }
EventsStorage(context).use { db ->
db.addEvent(event)
}

ApplicationController.unpinAllVisibleEvents(context)

val updatedEvent = EventsStorage(context).use { db ->
db.getEvent(eventId, baseTime)
}
assertFalse("Event should be unpinned", updatedEvent?.isPinned == true)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -376,5 +376,65 @@ class SnoozeTest {
assertTrue("Not snoozed event should be snoozed", updatedNot!!.snoozedUntil > 0)
}
}

// === Pinned exclusion from snoozeAll ===

@Test
fun testSnoozeAllEvents_skipsPinnedEvents() {
DevLog.info(LOG_TAG, "Running testSnoozeAllEvents_skipsPinnedEvents")

val pinnedEvent = createTestEvent(eventId = 600L, title = "Pinned").apply { isPinned = true }
val normalEvent = createTestEvent(eventId = 601L, title = "Normal")
addEventToStorage(pinnedEvent)
addEventToStorage(normalEvent)

val snoozeDelay = 30 * 60 * 1000L

val result = ApplicationController.snoozeAllEvents(
context, snoozeDelay, isChange = false, onlySnoozeVisible = false
)

assertNotNull("Snooze result should not be null", result)

EventsStorage(context).use { db ->
val updatedPinned = db.getEvent(pinnedEvent.eventId, pinnedEvent.instanceStartTime)
val updatedNormal = db.getEvent(normalEvent.eventId, normalEvent.instanceStartTime)

assertEquals("Pinned event should NOT be snoozed", 0L, updatedPinned!!.snoozedUntil)
assertTrue("Normal event should be snoozed", updatedNormal!!.snoozedUntil > 0)
}
}

@Test
fun testSnoozeAllCollapsedEvents_skipsPinnedEvents() {
DevLog.info(LOG_TAG, "Running testSnoozeAllCollapsedEvents_skipsPinnedEvents")

val pinnedCollapsed = createTestEvent(
eventId = 700L,
displayStatus = EventDisplayStatus.DisplayedCollapsed
).apply { isPinned = true }
val normalCollapsed = createTestEvent(
eventId = 701L,
displayStatus = EventDisplayStatus.DisplayedCollapsed
)
addEventToStorage(pinnedCollapsed)
addEventToStorage(normalCollapsed)

val snoozeDelay = 30 * 60 * 1000L

val result = ApplicationController.snoozeAllCollapsedEvents(
context, snoozeDelay, isChange = false, onlySnoozeVisible = false
)

assertNotNull("Snooze result should not be null", result)

EventsStorage(context).use { db ->
val updatedPinned = db.getEvent(pinnedCollapsed.eventId, pinnedCollapsed.instanceStartTime)
val updatedNormal = db.getEvent(normalCollapsed.eventId, normalCollapsed.instanceStartTime)

assertEquals("Pinned collapsed event should NOT be snoozed", 0L, updatedPinned!!.snoozedUntil)
assertTrue("Normal collapsed event should be snoozed", updatedNormal!!.snoozedUntil > 0)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ object Consts {
const val INTENT_SEARCH_QUERY = "search_query"
const val INTENT_SEARCH_QUERY_EVENT_COUNT = "search_query_event_count"
const val INTENT_FILTER_STATE = "filter_state"
const val INTENT_PINNED_EVENT_COUNT = "pinned_event_count"
const val INTENT_SNOOZE_ALL_COLLAPSED_KEY = "snooze_all_collapsed"
const val INTENT_DISMISS_ALL_KEY = "dismiss_all"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}

fun snoozeAllCollapsedEvents(context: Context, snoozeDelay: Long, isChange: Boolean, onlySnoozeVisible: Boolean): SnoozeResult? {
return snoozeEvents(context, { it.displayStatus == EventDisplayStatus.DisplayedCollapsed }, snoozeDelay, isChange, onlySnoozeVisible)
return snoozeEvents(context, { it.displayStatus == EventDisplayStatus.DisplayedCollapsed && !it.isPinned }, snoozeDelay, isChange, onlySnoozeVisible)
}

fun snoozeAllEvents(
Expand All @@ -943,20 +943,19 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
): SnoozeResult? {
val now = clock.currentTimeMillis()
return snoozeEvents(context, { event ->
// Pinned events are excluded from batch snooze
!event.isPinned &&
// Search query filter (existing behavior)
val matchesSearch = searchQuery?.let { query ->
(searchQuery?.let { query ->
event.title.contains(query, ignoreCase = true) ||
event.desc.contains(query, ignoreCase = true)
} ?: true

// FilterState filters (new)
val matchesFilter = filterState?.let { filter ->
} ?: true) &&
// FilterState filters
(filterState?.let { filter ->
filter.matchesCalendar(event) &&
filter.matchesStatus(event) &&
filter.matchesTime(event, now)
} ?: true

matchesSearch && matchesFilter
} ?: true)
}, snoozeDelay, isChange, onlySnoozeVisible)
}

Expand Down Expand Up @@ -1147,6 +1146,28 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}
}

fun pinAllVisibleEvents(context: Context) {
getEventsStorage(context).use { db ->
val eventsToPin = db.events.filter {
event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isPinned
}
if (eventsToPin.isNotEmpty()) {
val pinnedEvents = eventsToPin.map { it.isPinned = true; it }
db.updateEvents(pinnedEvents)
}
}
}

fun unpinAllVisibleEvents(context: Context) {
getEventsStorage(context).use { db ->
val eventsToUnpin = db.events.filter { event -> event.isPinned }
if (eventsToUnpin.isNotEmpty()) {
val unpinnedEvents = eventsToUnpin.map { it.isPinned = false; it }
db.updateEvents(unpinnedEvents)
}
}
}
Comment thread
williscool marked this conversation as resolved.

fun dismissEvent(
context: Context,
db: EventsStorageInterface,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ object EventAlertFlags {
const val IS_MUTED = 1L
const val IS_TASK = 2L
const val IS_ALARM = 4L
const val IS_PINNED = 8L
}

fun Long.isFlagSet(flag: Long)
Expand Down Expand Up @@ -166,6 +167,10 @@ data class EventAlertRecord(
get() = flags.isFlagSet(EventAlertFlags.IS_ALARM)
set(value) { flags = flags.setFlag(EventAlertFlags.IS_ALARM, value) }

var isPinned: Boolean
get() = flags.isFlagSet(EventAlertFlags.IS_PINNED)
set(value) { flags = flags.setFlag(EventAlertFlags.IS_PINNED, value) }

val isUnmutedAlarm: Boolean
get() = isAlarm && !isMuted

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,23 @@ class ActiveEventsFragment : Fragment(), EventListCallback, SearchableFragment,
updateEmptyState()
}

override fun onPinToggle(event: EventAlertRecord) {
val ctx = context ?: return
background {
getEventsStorage(ctx).use { db ->
val current = db.getEvent(event.eventId, event.instanceStartTime)
if (current != null) {
current.isPinned = !current.isPinned
db.updateEvent(current)
}
}
activity?.runOnUiThread {
loadEvents()
activity?.invalidateOptionsMenu()
}
}
}

override fun onScrollPositionChange(newPos: Int) {
// Not needed for fragments - handled within adapter if needed
}
Expand All @@ -371,6 +388,8 @@ class ActiveEventsFragment : Fragment(), EventListCallback, SearchableFragment,

override fun hasActiveEvents(): Boolean = adapter.hasActiveEvents

override fun hasUnpinnedActiveEvents(): Boolean = adapter.hasUnpinnedActiveEvents

override fun supportsSnoozeAll(): Boolean = true

override fun supportsMuteAll(): Boolean = true
Expand All @@ -381,6 +400,14 @@ class ActiveEventsFragment : Fragment(), EventListCallback, SearchableFragment,

override fun anyForDismissAll(): Boolean = adapter.anyForDismissAllButRecentAndSnoozed

override fun supportsPinAll(): Boolean = true

override fun anyForPinAll(): Boolean = adapter.anyForPinAll

override fun anyForUnpinAll(): Boolean = adapter.anyForUnpinAll

override fun getPinnedEventCount(): Int = adapter.pinnedCount

override fun onMuteAllComplete() {
loadEvents()
}
Expand All @@ -389,6 +416,10 @@ class ActiveEventsFragment : Fragment(), EventListCallback, SearchableFragment,
loadEvents()
}

override fun onPinAllComplete() {
loadEvents()
}

override fun onFilterChanged() {
loadEvents()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface EventListCallback {
fun onItemRemoved(event: EventAlertRecord)
fun onItemRestored(event: EventAlertRecord) // e.g. undo
fun onScrollPositionChange(newPos: Int)
fun onPinToggle(event: EventAlertRecord) {}
}

interface SelectionModeCallback {
Expand Down Expand Up @@ -91,6 +92,7 @@ class EventListAdapter(

var undoButton: Button?

var pinImage: ImageView?
var muteImage: ImageView?
var taskImage: ImageView?
val alarmImage: ImageView?
Expand All @@ -114,6 +116,7 @@ class EventListAdapter(

undoButton = itemView.find<Button?>(R.id.card_view_button_undo)

pinImage = itemView.find<ImageView?>(R.id.imageview_is_pinned_indicator)
muteImage = itemView.find<ImageView?>(R.id.imageview_is_muted_indicator)
taskImage = itemView.find<ImageView?>(R.id.imageview_is_task_indicator)
alarmImage = itemView.find<ImageView?>(R.id.imageview_is_alarm_indicator)
Expand Down Expand Up @@ -370,6 +373,12 @@ class EventListAdapter(

holder.eventTitleText.text = event.titleAsOneLine

holder.pinImage?.visibility = if (event.isPinned) View.VISIBLE else View.GONE
holder.pinImage?.setOnClickListener {
val ev = getEventAtPosition(holder.adapterPosition)
if (ev != null) callback.onPinToggle(ev)
}

holder.muteImage?.visibility = if (event.isMuted) View.VISIBLE else View.GONE

holder.taskImage?.visibility = if (event.isTask) View.VISIBLE else View.GONE
Expand Down Expand Up @@ -430,6 +439,9 @@ class EventListAdapter(
val hasActiveEvents: Boolean
get() = events.any { it.snoozedUntil == 0L }

val hasUnpinnedActiveEvents: Boolean
get() = events.any { it.snoozedUntil == 0L && !it.isPinned }

fun setSearchText(query: String?) {
currentSearchString = query
setEventsToDisplay()
Expand Down Expand Up @@ -589,6 +601,15 @@ class EventListAdapter(
val anyForMute: Boolean
get() = events.any { it.snoozedUntil == 0L && it.isNotSpecial}

val anyForPinAll: Boolean
Comment thread
cursor[bot] marked this conversation as resolved.
get() = events.any { it.snoozedUntil == 0L && it.isNotSpecial && !it.isPinned }

val anyForUnpinAll: Boolean
get() = events.any { it.isPinned }

val pinnedCount: Int
get() = events.count { it.isPinned }

// === Selection Mode Methods ===

fun enterSelectionMode(firstEvent: EventAlertRecord) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ data class FilterState(
StatusOption.ACTIVE -> context.getString(R.string.filter_status_active)
StatusOption.MUTED -> context.getString(R.string.filter_status_muted)
StatusOption.RECURRING -> context.getString(R.string.filter_status_recurring)
StatusOption.PINNED -> context.getString(R.string.filter_status_pinned)
StatusOption.UNPINNED -> context.getString(R.string.filter_status_unpinned)
}
}
parts.add(names.joinToString(", "))
Expand Down Expand Up @@ -275,14 +277,16 @@ data class FilterState(
* Individual status filter options. Multiple can be selected (OR logic).
*/
enum class StatusOption {
SNOOZED, ACTIVE, MUTED, RECURRING;
SNOOZED, ACTIVE, MUTED, RECURRING, PINNED, UNPINNED;

/** Check if an event matches this specific option */
fun matches(event: EventAlertRecord): Boolean = when (this) {
SNOOZED -> event.snoozedUntil > 0
ACTIVE -> event.snoozedUntil == 0L
MUTED -> event.isMuted
RECURRING -> event.isRepeating
PINNED -> event.isPinned
UNPINNED -> !event.isPinned
}
}

Expand Down
Loading
Loading