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
18 changes: 17 additions & 1 deletion .github/workflows/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,25 @@ jobs:
outputs:
build_datetime: ${{ steps.set_date.outputs.build_datetime }}

safety_checks:
name: Safety Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
sparse-checkout: |
android/app/src/main/res/menu/main.xml
scripts/check_dev_page_hidden.sh

- name: Ensure dev page is not exposed
run: |
chmod +x ./scripts/check_dev_page_hidden.sh
./scripts/check_dev_page_hidden.sh

build:
name: Build Android App
needs: set_build_datetime
needs: [set_build_datetime, safety_checks]
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ jobs:
ssh-key: ${{ secrets.VERSION_BUMP_DEPLOY_PRIVATE_KEY }}
fetch-depth: 0

- name: Ensure dev page is not exposed
run: |
chmod +x ./scripts/check_dev_page_hidden.sh
./scripts/check_dev_page_hidden.sh

- name: Setup Node.js
uses: actions/setup-node@v4
with:
Expand Down
9 changes: 9 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def isGitHubActions = System.getenv("GITHUB_ACTIONS") != null && System.getenv("
// so now we just do it ci
def isCIBuild = isCiBuild || isGitHubActions

// Read dev page flag from local.properties (gitignored = can't leak to releases)
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localProperties.load(new FileInputStream(localPropertiesFile))
}
def devPageEnabled = localProperties.getProperty('devPage.enabled', 'false').toBoolean()

android {
compileSdkVersion rootProject.ext.compileSdkVersion

Expand Down Expand Up @@ -94,6 +102,7 @@ android {
// React Native New Architecture flags
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", project.hasProperty("newArchEnabled") ? project.property("newArchEnabled") : "false"
buildConfigField "boolean", "IS_HERMES_ENABLED", project.hasProperty("hermesEnabled") ? project.property("hermesEnabled") : "true"
buildConfigField "boolean", "DEV_PAGE_ENABLED", devPageEnabled.toString()

// Enhanced test instrumentation with JaCoCo and Ultron/Allure
testInstrumentationRunner "com.atiurin.ultron.allure.UltronAllureTestRunner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,5 +170,21 @@ class SettingsTest {
DevLog.info(LOG_TAG, "Running testFormatSnoozePresetSeconds")
assertEquals("45s", PreferenceUtils.formatSnoozePreset(45 * 1000L))
}

// === Display Next Alert Settings Tests ===

@Test
fun testDisplayNextGCalReminderDefaultValue() {
DevLog.info(LOG_TAG, "Running testDisplayNextGCalReminderDefaultValue")
// The default value should be true
assertTrue("displayNextGCalReminder should default to true", settings.displayNextGCalReminder)
}

@Test
fun testDisplayNextAppAlertDefaultValue() {
DevLog.info(LOG_TAG, "Running testDisplayNextAppAlertDefaultValue")
// The default value should be false
assertFalse("displayNextAppAlert should default to false", settings.displayNextAppAlert)
}
}

Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package com.github.quarck.calnotify.textutils

import android.content.Context
import android.preference.PreferenceManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.github.quarck.calnotify.Consts
import com.github.quarck.calnotify.calendar.AttendanceStatus
import com.github.quarck.calnotify.calendar.CalendarEventDetails
import com.github.quarck.calnotify.calendar.CalendarProvider
import com.github.quarck.calnotify.calendar.EventAlertRecord
import com.github.quarck.calnotify.calendar.EventDisplayStatus
import com.github.quarck.calnotify.calendar.EventOrigin
import com.github.quarck.calnotify.calendar.EventRecord
import com.github.quarck.calnotify.calendar.EventReminderRecord
import com.github.quarck.calnotify.calendar.EventStatus
import com.github.quarck.calnotify.logs.DevLog
import com.github.quarck.calnotify.utils.CNPlusTestClock
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -39,6 +48,23 @@ class EventFormatterTest {
DevLog.info(LOG_TAG, "Setup complete with baseTime=$baseTime")
}

@After
fun cleanup() {
DevLog.info(LOG_TAG, "Cleaning up after test")
// Reset the next alert settings to defaults
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.remove("pref_display_next_gcal_reminder")
.remove("pref_display_next_app_alert")
.commit()
// Unmock CalendarProvider if it was mocked
try {
unmockkObject(CalendarProvider)
} catch (e: Exception) {
// Ignore if not mocked
}
}

private fun createTestEvent(
eventId: Long = 1L,
title: String = "Test Event",
Expand Down Expand Up @@ -265,5 +291,128 @@ class EventFormatterTest {

assertNotEquals("Different times should produce different timestamps", result1, result2)
}

// === Next Alert Indicator feature tests ===

private fun setNextGCalReminderSetting(enabled: Boolean) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean("pref_display_next_gcal_reminder", enabled)
.commit()
}

private fun setNextAppAlertSetting(enabled: Boolean) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean("pref_display_next_app_alert", enabled)
.commit()
}

private fun createMockEventRecord(
eventId: Long,
startTime: Long,
reminders: List<EventReminderRecord>
): EventRecord {
return EventRecord(
calendarId = 1L,
eventId = eventId,
details = CalendarEventDetails(
title = "Test Event",
desc = "Test Description",
location = "",
timezone = "UTC",
startTime = startTime,
endTime = startTime + Consts.HOUR_IN_MILLISECONDS,
isAllDay = false,
reminders = reminders,
repeatingRule = "",
repeatingRDate = "",
repeatingExRule = "",
repeatingExRDate = "",
color = 0
),
eventStatus = EventStatus.Confirmed,
attendanceStatus = AttendanceStatus.None
)
}

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

// Ensure the setting is disabled (default)
setNextGCalReminderSetting(false)

// Create a new formatter to pick up the setting
val testFormatter = EventFormatter(context, testClock)

// Event starts 2 hours from now with a reminder 30 minutes before
val eventStartTime = baseTime + 2 * Consts.HOUR_IN_MILLISECONDS
val event = createTestEvent(
eventId = 100L,
startTime = eventStartTime,
endTime = eventStartTime + Consts.HOUR_IN_MILLISECONDS
)

val result = testFormatter.formatNotificationSecondaryText(event)

DevLog.info(LOG_TAG, "Result with setting disabled: $result")

// Should NOT contain "reminder in" when setting is disabled
assertFalse(
"Should NOT contain 'reminder in' when setting is disabled",
result.contains("reminder in", ignoreCase = true)
)
}

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

// Enable GCal setting, disable app alert
setNextGCalReminderSetting(true)
setNextAppAlertSetting(false)

// Event starts 2 hours from now
val eventStartTime = baseTime + 2 * Consts.HOUR_IN_MILLISECONDS
val eventId = 101L

// Create reminders: one that fires in the future (60min before = baseTime + 1h)
val reminders = listOf(
EventReminderRecord.minutes(60), // fires at baseTime + 1h (future from baseTime)
EventReminderRecord.minutes(30) // fires at baseTime + 1.5h (future from baseTime)
)

// Mock CalendarProvider to return our event with reminders
mockkObject(CalendarProvider)
every { CalendarProvider.getEvent(any(), eq(eventId)) } returns createMockEventRecord(
eventId = eventId,
startTime = eventStartTime,
reminders = reminders
)

// Create the test event (EventAlertRecord)
val event = createTestEvent(
eventId = eventId,
startTime = eventStartTime,
endTime = eventStartTime + Consts.HOUR_IN_MILLISECONDS
)

// Create a new formatter to pick up the setting
val testFormatter = EventFormatter(context, testClock)

val result = testFormatter.formatNotificationSecondaryText(event)

DevLog.info(LOG_TAG, "Result with setting enabled: $result")
DevLog.info(LOG_TAG, "Event start time: $eventStartTime, Base time: $baseTime")
DevLog.info(LOG_TAG, "Reminder 1 fires at: ${eventStartTime - 60 * Consts.MINUTE_IN_MILLISECONDS}")
DevLog.info(LOG_TAG, "Reminder 2 fires at: ${eventStartTime - 30 * Consts.MINUTE_IN_MILLISECONDS}")

// Should contain 📅 when GCal setting is enabled and there are future reminders
assertTrue(
"Should contain 📅 when setting is enabled and future GCal reminders exist. Result: $result",
result.contains("📅")
)
}
}

10 changes: 10 additions & 0 deletions android/app/src/main/java/com/github/quarck/calnotify/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ class Settings(context: Context) : PersistentStorageBase(context), SettingsInter
val snoozePresetsRaw: String
get() = getString(SNOOZE_PRESET_KEY, DEFAULT_SNOOZE_PRESET)

var displayNextGCalReminder: Boolean
get() = getBoolean(DISPLAY_NEXT_GCAL_REMINDER, true)
set(value) = setBoolean(DISPLAY_NEXT_GCAL_REMINDER, value)

var displayNextAppAlert: Boolean
get() = getBoolean(DISPLAY_NEXT_APP_ALERT, false)
set(value) = setBoolean(DISPLAY_NEXT_APP_ALERT, value)

val snoozePresets: LongArray
get() {
var ret = PreferenceUtils.parseSnoozePresets(snoozePresetsRaw)
Expand Down Expand Up @@ -443,6 +451,8 @@ class Settings(context: Context) : PersistentStorageBase(context), SettingsInter
private const val CALENDAR_IS_HANDLED_KEY_PREFIX = "calendar_handled_"

private const val SNOOZE_PRESET_KEY = "pref_snooze_presets" //"15m, 1h, 4h, 1d"
private const val DISPLAY_NEXT_GCAL_REMINDER = "pref_display_next_gcal_reminder" // true
private const val DISPLAY_NEXT_APP_ALERT = "pref_display_next_app_alert" // false
private const val VIEW_AFTER_EDIT_KEY = "show_event_after_reschedule" // true
private const val ENABLE_REMINDERS_KEY = "enable_reminding_key" // false
private const val REMINDER_INTERVAL_PATTERN_KEY = "remind_interval_key_pattern" // "10m"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,12 @@ fun EventRecord.nextAlarmTime(currentTime: Long): Long {
}

return ret
}
}

fun EventRecord.getNextAlertTimeAfter(anchor: Long): Long? {
val futureReminders = this
.reminders
.map { this.startTime - it.millisecondsBefore }
.filter { it > anchor }
return futureReminders.maxOrNull()
}
Original file line number Diff line number Diff line change
Expand Up @@ -507,9 +507,19 @@ open class EventNotificationManager : EventNotificationManagerInterface {

val numEvents = events.size

val title = java.lang.String.format(
val baseTitle = java.lang.String.format(
context.getString(R.string.multiple_events_single_notification),
numEvents)

// Add next notification indicator to title if enabled
val formatter = EventFormatter(context, clock)
val nextIndicator = formatter.formatNextNotificationIndicatorForCollapsed(
events = events,
displayNextGCalReminder = settings.displayNextGCalReminder,
displayNextAppAlert = settings.displayNextAppAlert,
remindersEnabled = settings.remindersEnabled
)
val title = if (nextIndicator != null) "$baseTitle $nextIndicator" else baseTitle

val text = context.getString(R.string.multiple_events_details_2)

Expand Down Expand Up @@ -1471,7 +1481,17 @@ open class EventNotificationManager : EventNotificationManagerInterface {

val numEvents = events.size

val title = java.lang.String.format(context.getString(R.string.multiple_events), numEvents)
val baseTitle = java.lang.String.format(context.getString(R.string.multiple_events), numEvents)

// Add next notification indicator to title if enabled
val formatter = EventFormatter(context, clock)
val nextIndicator = formatter.formatNextNotificationIndicatorForCollapsed(
events = events,
displayNextGCalReminder = settings.displayNextGCalReminder,
displayNextAppAlert = settings.displayNextAppAlert,
remindersEnabled = settings.remindersEnabled
)
val title = if (nextIndicator != null) "$baseTitle $nextIndicator" else baseTitle

val text = context.getString(com.github.quarck.calnotify.R.string.multiple_events_details_2)

Expand Down
Loading
Loading