diff --git a/android/app/build.gradle b/android/app/build.gradle index 04da2b78e..92ff67e60 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -299,6 +299,8 @@ dependencies { testImplementation 'junit:junit:4.13.2' // Add Robolectric dependency testImplementation 'org.robolectric:robolectric:4.16' + // Truth assertions - better failure messages and fluent API + testImplementation 'com.google.truth:truth:1.4.2' // Mockito for unit tests testImplementation 'org.mockito:mockito-core:5.16.1' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' @@ -337,6 +339,8 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' // Downgraded for Ultron 2.3.1 compatibility androidTestImplementation 'junit:junit:4.13.2' + // Truth assertions - better failure messages and fluent API + androidTestImplementation 'com.google.truth:truth:1.4.2' // Ultron UI Testing Framework (for traditional View-based UI tests) // Using 2.3.1 for Kotlin 1.9.x compatibility (2.6.x requires Kotlin 2.x) diff --git a/android/app/src/test/java/com/github/quarck/calnotify/testutils/AlarmManagerTestHelper.kt b/android/app/src/test/java/com/github/quarck/calnotify/testutils/AlarmManagerTestHelper.kt new file mode 100644 index 000000000..18f09bb25 --- /dev/null +++ b/android/app/src/test/java/com/github/quarck/calnotify/testutils/AlarmManagerTestHelper.kt @@ -0,0 +1,146 @@ +// +// Calendar Notifications Plus +// Copyright (C) 2025 William Harris (wharris+cnplus@upscalews.com) +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +package com.github.quarck.calnotify.testutils + +import android.app.AlarmManager +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowAlarmManager + +/** + * Helper class for testing AlarmManager interactions using Robolectric's ShadowAlarmManager. + * + * ShadowAlarmManager provides better introspection capabilities than MockK mocks: + * - Access to scheduled alarms + * - Ability to verify alarm parameters + * - Fire alarms programmatically + * + * Usage: + * ``` + * @RunWith(RobolectricTestRunner::class) + * class AlarmTest { + * private lateinit var alarmHelper: AlarmManagerTestHelper + * + * @Before + * fun setup() { + * alarmHelper = AlarmManagerTestHelper() + * } + * + * @Test + * fun testAlarmScheduling() { + * // Code that schedules an alarm... + * + * // Verify alarm was scheduled + * val nextAlarm = alarmHelper.getNextScheduledAlarm() + * assertNotNull(nextAlarm) + * assertEquals(expectedTime, nextAlarm?.triggerAtTime) + * } + * } + * ``` + * + * Note: For tests using MockContextProvider, the AlarmManager is already mocked. + * This helper is for tests that want to use native Robolectric shadow behavior + * without the additional MockK layer. + */ +class AlarmManagerTestHelper { + + private val context: Context = ApplicationProvider.getApplicationContext() + private val alarmManager: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + private val shadowAlarmManager: ShadowAlarmManager = shadowOf(alarmManager) + + /** + * Gets the next scheduled alarm, if any. + */ + fun getNextScheduledAlarm(): ShadowAlarmManager.ScheduledAlarm? { + return shadowAlarmManager.nextScheduledAlarm + } + + /** + * Gets all scheduled alarms. + */ + fun getAllScheduledAlarms(): List { + return shadowAlarmManager.scheduledAlarms + } + + /** + * Checks if any alarm is scheduled. + */ + fun hasScheduledAlarms(): Boolean { + return shadowAlarmManager.scheduledAlarms.isNotEmpty() + } + + /** + * Gets the count of scheduled alarms. + */ + fun getScheduledAlarmCount(): Int { + return shadowAlarmManager.scheduledAlarms.size + } + + /** + * Gets an alarm scheduled for a specific time. + */ + fun getAlarmAtTime(triggerTime: Long): ShadowAlarmManager.ScheduledAlarm? { + return shadowAlarmManager.scheduledAlarms.find { it.triggerAtTime == triggerTime } + } + + /** + * Clears all scheduled alarms (useful in test setup/teardown). + * Note: This uses reflection to clear internal state. + */ + fun clearAllAlarms() { + // Cancel all pending intents + shadowAlarmManager.scheduledAlarms.forEach { alarm -> + alarm.operation?.let { alarmManager.cancel(it) } + } + } + + /** + * Verifies that an exact alarm was scheduled at the specified time. + */ + fun assertExactAlarmScheduledAt(triggerTime: Long, message: String = "Expected exact alarm at $triggerTime") { + val alarm = getAlarmAtTime(triggerTime) + if (alarm == null) { + val scheduled = getAllScheduledAlarms().map { it.triggerAtTime } + throw AssertionError("$message\nScheduled alarms: $scheduled") + } + } + + /** + * Verifies that no alarms are currently scheduled. + */ + fun assertNoAlarmsScheduled(message: String = "Expected no alarms scheduled") { + val alarms = getAllScheduledAlarms() + if (alarms.isNotEmpty()) { + val scheduled = alarms.map { it.triggerAtTime } + throw AssertionError("$message\nFound scheduled alarms at: $scheduled") + } + } + + /** + * Gets the underlying ShadowAlarmManager for advanced usage. + */ + fun getShadow(): ShadowAlarmManager = shadowAlarmManager + + /** + * Gets the AlarmManager instance. + */ + fun getAlarmManager(): AlarmManager = alarmManager +} diff --git a/android/app/src/test/java/com/github/quarck/calnotify/testutils/EventMother.kt b/android/app/src/test/java/com/github/quarck/calnotify/testutils/EventMother.kt new file mode 100644 index 000000000..722ce067b --- /dev/null +++ b/android/app/src/test/java/com/github/quarck/calnotify/testutils/EventMother.kt @@ -0,0 +1,253 @@ +// +// Calendar Notifications Plus +// Copyright (C) 2025 William Harris (wharris+cnplus@upscalews.com) +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +package com.github.quarck.calnotify.testutils + +import com.github.quarck.calnotify.Consts +import com.github.quarck.calnotify.calendar.AttendanceStatus +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.EventStatus + +/** + * Object Mother pattern for creating EventAlertRecord instances in tests. + * + * Provides factory methods for common test scenarios, reducing boilerplate + * and ensuring consistent test data creation across the test suite. + * + * Usage: + * ``` + * // Simple default event + * val event = EventMother.createDefault() + * + * // Muted event + * val mutedEvent = EventMother.createMuted() + * + * // Custom event using builder-style + * val customEvent = EventMother.createDefault( + * eventId = 42L, + * title = "Team Meeting" + * ) + * + * // Snoozed event + * val snoozedEvent = EventMother.createSnoozed(until = System.currentTimeMillis() + 3600000) + * ``` + */ +object EventMother { + + // Default base time for tests (can be overridden) + private const val DEFAULT_BASE_TIME = 1635724800000L // 2021-11-01 00:00:00 UTC + + /** + * Creates a default EventAlertRecord with sensible test values. + * All parameters can be overridden for specific test needs. + */ + fun createDefault( + calendarId: Long = 1L, + eventId: Long = 1L, + isAllDay: Boolean = false, + isRepeating: Boolean = false, + alertTime: Long = DEFAULT_BASE_TIME, + notificationId: Int = 1, + title: String = "Test Event", + desc: String = "Test Description", + startTime: Long = DEFAULT_BASE_TIME + Consts.HOUR_IN_MILLISECONDS, + endTime: Long = DEFAULT_BASE_TIME + 2 * Consts.HOUR_IN_MILLISECONDS, + instanceStartTime: Long = DEFAULT_BASE_TIME + Consts.HOUR_IN_MILLISECONDS, + instanceEndTime: Long = DEFAULT_BASE_TIME + 2 * Consts.HOUR_IN_MILLISECONDS, + location: String = "", + lastStatusChangeTime: Long = DEFAULT_BASE_TIME, + snoozedUntil: Long = 0L, + displayStatus: EventDisplayStatus = EventDisplayStatus.Hidden, + color: Int = Consts.DEFAULT_CALENDAR_EVENT_COLOR, + origin: EventOrigin = EventOrigin.ProviderBroadcast, + timeFirstSeen: Long = DEFAULT_BASE_TIME, + eventStatus: EventStatus = EventStatus.Confirmed, + attendanceStatus: AttendanceStatus = AttendanceStatus.None, + flags: Long = 0L + ) = EventAlertRecord( + calendarId = calendarId, + eventId = eventId, + isAllDay = isAllDay, + isRepeating = isRepeating, + alertTime = alertTime, + notificationId = notificationId, + title = title, + desc = desc, + startTime = startTime, + endTime = endTime, + instanceStartTime = instanceStartTime, + instanceEndTime = instanceEndTime, + location = location, + lastStatusChangeTime = lastStatusChangeTime, + snoozedUntil = snoozedUntil, + displayStatus = displayStatus, + color = color, + origin = origin, + timeFirstSeen = timeFirstSeen, + eventStatus = eventStatus, + attendanceStatus = attendanceStatus, + flags = flags + ) + + /** + * Creates a muted event (won't trigger sound/vibration). + */ + fun createMuted( + eventId: Long = 1L, + title: String = "Muted Event" + ) = createDefault(eventId = eventId, title = title).also { + it.isMuted = true + } + + /** + * Creates a task event (different handling than regular calendar events). + */ + fun createTask( + eventId: Long = 1L, + title: String = "Task Event" + ) = createDefault(eventId = eventId, title = title).also { + it.isTask = true + } + + /** + * Creates an alarm event (overrides quiet hours). + */ + fun createAlarm( + eventId: Long = 1L, + title: String = "Alarm Event" + ) = createDefault(eventId = eventId, title = title).also { + it.isAlarm = true + } + + /** + * Creates a snoozed event. + */ + fun createSnoozed( + eventId: Long = 1L, + until: Long, + title: String = "Snoozed Event" + ) = createDefault(eventId = eventId, title = title, snoozedUntil = until) + + /** + * Creates an all-day event. + */ + fun createAllDay( + eventId: Long = 1L, + title: String = "All Day Event", + startTime: Long = DEFAULT_BASE_TIME + ) = createDefault( + eventId = eventId, + title = title, + isAllDay = true, + startTime = startTime, + endTime = startTime + Consts.DAY_IN_MILLISECONDS, + instanceStartTime = startTime, + instanceEndTime = startTime + Consts.DAY_IN_MILLISECONDS + ) + + /** + * Creates a repeating event. + */ + fun createRepeating( + eventId: Long = 1L, + title: String = "Repeating Event" + ) = createDefault(eventId = eventId, title = title, isRepeating = true) + + /** + * Creates an event with specific timing relative to a base time. + * Useful for tests that need precise time control. + */ + fun createWithTiming( + eventId: Long = 1L, + baseTime: Long, + alertOffsetMinutes: Int = 15, + durationMinutes: Int = 60, + title: String = "Timed Event" + ): EventAlertRecord { + val startTime = baseTime + alertOffsetMinutes * Consts.MINUTE_IN_MILLISECONDS + val endTime = startTime + durationMinutes * Consts.MINUTE_IN_MILLISECONDS + val alertTime = startTime - alertOffsetMinutes * Consts.MINUTE_IN_MILLISECONDS + + return createDefault( + eventId = eventId, + title = title, + alertTime = alertTime, + startTime = startTime, + endTime = endTime, + instanceStartTime = startTime, + instanceEndTime = endTime, + lastStatusChangeTime = baseTime, + timeFirstSeen = baseTime + ) + } + + /** + * Creates a cancelled event. + */ + fun createCancelled( + eventId: Long = 1L, + title: String = "Cancelled Event" + ) = createDefault( + eventId = eventId, + title = title, + eventStatus = EventStatus.Cancelled + ) + + /** + * Creates a tentative event. + */ + fun createTentative( + eventId: Long = 1L, + title: String = "Tentative Event" + ) = createDefault( + eventId = eventId, + title = title, + eventStatus = EventStatus.Tentative + ) + + /** + * Creates an event with declined attendance. + */ + fun createDeclined( + eventId: Long = 1L, + title: String = "Declined Event" + ) = createDefault( + eventId = eventId, + title = title, + attendanceStatus = AttendanceStatus.Declined + ) + + /** + * Creates multiple events with sequential IDs. + * Useful for tests that need a batch of events. + */ + fun createBatch( + count: Int, + startingEventId: Long = 1L, + titlePrefix: String = "Event" + ): List = (0 until count).map { index -> + createDefault( + eventId = startingEventId + index, + notificationId = (startingEventId + index).toInt(), + title = "$titlePrefix ${index + 1}" + ) + } +} diff --git a/android/app/src/test/java/com/github/quarck/calnotify/testutils/MonitorAlertMother.kt b/android/app/src/test/java/com/github/quarck/calnotify/testutils/MonitorAlertMother.kt new file mode 100644 index 000000000..5db0192b0 --- /dev/null +++ b/android/app/src/test/java/com/github/quarck/calnotify/testutils/MonitorAlertMother.kt @@ -0,0 +1,156 @@ +// +// Calendar Notifications Plus +// Copyright (C) 2025 William Harris (wharris+cnplus@upscalews.com) +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +package com.github.quarck.calnotify.testutils + +import com.github.quarck.calnotify.Consts +import com.github.quarck.calnotify.calendar.MonitorEventAlertEntry + +/** + * Object Mother pattern for creating MonitorEventAlertEntry instances in tests. + * + * Usage: + * ``` + * // Simple unhandled alert + * val alert = MonitorAlertMother.createDefault() + * + * // Already handled alert + * val handledAlert = MonitorAlertMother.createHandled() + * + * // Pre-muted alert + * val mutedAlert = MonitorAlertMother.createPreMuted() + * ``` + */ +object MonitorAlertMother { + + private const val DEFAULT_BASE_TIME = 1635724800000L // 2021-11-01 00:00:00 UTC + + /** + * Creates a default MonitorEventAlertEntry with sensible test values. + */ + fun createDefault( + eventId: Long = 1L, + isAllDay: Boolean = false, + alertTime: Long = DEFAULT_BASE_TIME, + instanceStartTime: Long = DEFAULT_BASE_TIME + Consts.HOUR_IN_MILLISECONDS, + instanceEndTime: Long = DEFAULT_BASE_TIME + 2 * Consts.HOUR_IN_MILLISECONDS, + alertCreatedByUs: Boolean = false, + wasHandled: Boolean = false, + flags: Long = 0L + ) = MonitorEventAlertEntry( + eventId = eventId, + isAllDay = isAllDay, + alertTime = alertTime, + instanceStartTime = instanceStartTime, + instanceEndTime = instanceEndTime, + alertCreatedByUs = alertCreatedByUs, + wasHandled = wasHandled, + flags = flags + ) + + /** + * Creates an alert that has already been handled. + */ + fun createHandled( + eventId: Long = 1L, + alertTime: Long = DEFAULT_BASE_TIME + ) = createDefault( + eventId = eventId, + alertTime = alertTime, + wasHandled = true + ) + + /** + * Creates an alert that is pre-muted. + */ + fun createPreMuted( + eventId: Long = 1L, + alertTime: Long = DEFAULT_BASE_TIME + ) = createDefault( + eventId = eventId, + alertTime = alertTime, + flags = MonitorEventAlertEntry.PRE_MUTED_FLAG + ) + + /** + * Creates an alert that was created by the app (not from provider). + */ + fun createByUs( + eventId: Long = 1L, + alertTime: Long = DEFAULT_BASE_TIME + ) = createDefault( + eventId = eventId, + alertTime = alertTime, + alertCreatedByUs = true + ) + + /** + * Creates an all-day event alert. + */ + fun createAllDay( + eventId: Long = 1L, + alertTime: Long = DEFAULT_BASE_TIME, + instanceStartTime: Long = DEFAULT_BASE_TIME + ) = createDefault( + eventId = eventId, + isAllDay = true, + alertTime = alertTime, + instanceStartTime = instanceStartTime, + instanceEndTime = instanceStartTime + Consts.DAY_IN_MILLISECONDS + ) + + /** + * Creates an alert with specific timing relative to a base time. + */ + fun createWithTiming( + eventId: Long = 1L, + baseTime: Long, + alertOffsetMinutes: Int = 15, + durationMinutes: Int = 60 + ): MonitorEventAlertEntry { + val instanceStart = baseTime + alertOffsetMinutes * Consts.MINUTE_IN_MILLISECONDS + val instanceEnd = instanceStart + durationMinutes * Consts.MINUTE_IN_MILLISECONDS + val alertTime = instanceStart - alertOffsetMinutes * Consts.MINUTE_IN_MILLISECONDS + + return createDefault( + eventId = eventId, + alertTime = alertTime, + instanceStartTime = instanceStart, + instanceEndTime = instanceEnd + ) + } + + /** + * Creates multiple alerts with sequential event IDs. + */ + fun createBatch( + count: Int, + startingEventId: Long = 1L, + baseAlertTime: Long = DEFAULT_BASE_TIME, + intervalMinutes: Int = 60 + ): List = (0 until count).map { index -> + val offset = index * intervalMinutes * Consts.MINUTE_IN_MILLISECONDS + createDefault( + eventId = startingEventId + index, + alertTime = baseAlertTime + offset, + instanceStartTime = baseAlertTime + offset + Consts.HOUR_IN_MILLISECONDS, + instanceEndTime = baseAlertTime + offset + 2 * Consts.HOUR_IN_MILLISECONDS + ) + } +} diff --git a/android/app/src/test/resources/robolectric.properties b/android/app/src/test/resources/robolectric.properties new file mode 100644 index 000000000..c1dbc6284 --- /dev/null +++ b/android/app/src/test/resources/robolectric.properties @@ -0,0 +1,16 @@ +# Robolectric Configuration +# See: http://robolectric.org/configuring/ + +# Use PAUSED looper mode for more realistic async testing +# This is the default in Robolectric 4.x, but we make it explicit +# PAUSED mode requires explicit control of the main looper via: +# shadowOf(getMainLooper()).idle() +# shadowOf(getMainLooper()).runToEndOfTasks() +looperMode=PAUSED + +# Target SDK for Robolectric tests +# Using 34 for latest Android 14 behavior +sdk=34 + +# Application class (uses test application if available) +# application=com.github.quarck.calnotify.TestApplication diff --git a/docs/android-testing-best-practices.md b/docs/android-testing-best-practices.md new file mode 100644 index 000000000..613755b9c --- /dev/null +++ b/docs/android-testing-best-practices.md @@ -0,0 +1,546 @@ +# Android Testing Best Practices Research + +This document captures research on Android testing best practices, comparing against the current CalendarNotification Plus test setup, and identifies opportunities for improvement. + +## Current Setup Summary + +### Dependencies Used +| Category | Library | Version | Notes | +|----------|---------|---------|-------| +| Unit Testing | JUnit | 4.13.2 | Standard | +| Mocking | MockK | 1.13.9 | Kotlin-first mocking | +| Mocking | Mockito | 5.16.1 | Also available | +| Robolectric | Robolectric | 4.16 | Latest stable | +| UI Testing | Ultron | 2.3.1 | With Allure integration | +| UI Testing | Espresso | 3.4.0 | Older version for Ultron compat | +| AndroidX Test | Various | 1.5.x | Standard | +| Coverage | JaCoCo | 0.8.14 | Latest | +| Database | Room Testing | 2.8.4 | Latest | + +### Current Fixture Architecture +- `BaseCalendarTestFixture` - Builder pattern for test setup +- `MockCalendarProvider` - In-memory calendar simulation +- `MockTimeProvider` / `CNPlusTestClock` - Time control +- `MockContextProvider` - Context/SharedPreferences mocking +- `MockApplicationComponents` - App controller mocking +- `UITestFixture` - UI test helpers with IdlingResource support +- `TestStorageFactory` - Singleton storage for Robolectric tests + +--- + +## Best Practices Research + +### 1. Test Organization & Architecture + +#### Google's Testing Pyramid Recommendation +``` + /\ + / \ E2E Tests (10%) + /----\ + / \ Integration Tests (20%) + /--------\ + / \ Unit Tests (70%) + /------------\ +``` + +**Your Current Split:** +- Unit (Robolectric): ~18,582 lines +- Integration (Instrumented): ~19,743 lines +- Ratio: ~48% / 52% (should be more unit-heavy) + +**Recommendation:** The near 50/50 split suggests potential for moving more tests to Robolectric where appropriate. However, your note about SQLite limitations is valid - storage tests must stay instrumented. + +--- + +### 2. Fixture Libraries to Consider + +#### a) **Hilt Testing** (Recommended for DI-heavy apps) +```kotlin +@HiltAndroidTest +class MyTest { + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var repository: Repository +} +``` +**Status:** Not currently using Hilt. Would require architectural changes. +**Verdict:** Low priority unless moving to Hilt for production code. + +#### b) **Truth Assertions** (Google's assertion library) +```kotlin +// Instead of: +assertEquals(expected, actual) +assertTrue(list.contains(item)) + +// Use: +assertThat(actual).isEqualTo(expected) +assertThat(list).contains(item) +``` +**Benefits:** +- More readable failure messages +- Fluent API +- Better collection assertions + +**Recommendation:** ✅ Easy win - add `com.google.truth:truth:1.1.5` for better assertions. + +#### c) **Turbine** (For Flow/Coroutine testing) +```kotlin +@Test +fun testFlow() = runTest { + viewModel.uiState.test { + assertEquals(Initial, awaitItem()) + viewModel.loadData() + assertEquals(Loading, awaitItem()) + assertEquals(Success(data), awaitItem()) + } +} +``` +**Status:** Not using Flows heavily in current codebase. +**Verdict:** Consider if adopting more Kotlin Flows. + +#### d) **Kaspresso** (UI testing framework) +```kotlin +@Test +fun test() = run { + step("Open main screen") { + MainScreen { + title.isVisible() + button.click() + } + } + step("Check result") { + ResultScreen { + message.hasText("Success") + } + } +} +``` +**Status:** Currently using Ultron (similar purpose). +**Verdict:** Ultron is fine, but Kaspresso has better documentation. Consider if Ultron becomes problematic. + +--- + +### 3. Robolectric Features You May Not Be Using + +#### a) **Qualifiers for Configuration Testing** +```kotlin +@Config(qualifiers = "w820dp-h1180dp-land") +@Test +fun testLandscapeTablet() { + // Test tablet landscape layout +} + +@Config(qualifiers = "night") +@Test +fun testDarkMode() { + // Test dark theme +} +``` +**Use Case:** Testing different screen sizes, orientations, locales. + +#### b) **Shadow Classes for System Services** +```kotlin +val shadowAlarmManager = Shadows.shadowOf(alarmManager) +val nextAlarm = shadowAlarmManager.nextScheduledAlarm +assertThat(nextAlarm.triggerAtTime).isEqualTo(expectedTime) +``` +**Current:** You're mocking AlarmManager manually. +**Recommendation:** ✅ Use `ShadowAlarmManager` instead of MockK for cleaner alarm testing. + +#### c) **Robolectric's ContentProvider Support** +```kotlin +@Config(shadows = [ShadowContentResolver::class]) +class CalendarProviderTest { + @Test + fun testQueryCalendars() { + val shadowResolver = Shadows.shadowOf(contentResolver) + shadowResolver.registerProvider(CalendarContract.AUTHORITY, + InMemoryCalendarProvider()) + // Test with in-memory provider + } +} +``` +**Current:** You use real ContentProvider in instrumented tests. +**Note:** This may not work well with your custom SQLite extensions - keep instrumented. + +#### d) **Paused Looper Mode** (Modern Robolectric) +```kotlin +@LooperMode(LooperMode.Mode.PAUSED) +class MyTest { + @Test + fun testAsyncBehavior() { + // Async code runs on paused looper + viewModel.loadData() + + // Advance time explicitly + shadowOf(getMainLooper()).idle() + + // Now verify results + } +} +``` +**Status:** Check if using `LEGACY` mode (older behavior). +**Recommendation:** ✅ Ensure using `PAUSED` mode for more realistic async testing. + +#### e) **AndroidX Test with Robolectric** +```kotlin +@RunWith(AndroidJUnit4::class) // Works with both Robolectric and real device +class UnifiedTest { + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) +} +``` +**Status:** You're already using some AndroidX Test APIs. +**Recommendation:** Ensure consistent use of `ActivityScenario` vs manual launching. + +--- + +### 4. Instrumented Test Features You May Not Be Using + +#### a) **Test Orchestrator** (Isolation) +```groovy +android { + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } +} +dependencies { + androidTestUtil 'androidx.test:orchestrator:1.4.2' +} +``` +**Benefits:** +- Each test runs in its own Instrumentation instance +- Tests can't affect each other through static state +- Better crash isolation + +**Cost:** Slower test execution. +**Recommendation:** Consider for flaky test debugging. + +#### b) **Managed Virtual Devices** (Gradle Managed Devices) +```groovy +android { + testOptions { + managedDevices { + devices { + pixel2Api30(com.android.build.api.dsl.ManagedVirtualDevice) { + device = "Pixel 2" + apiLevel = 30 + systemImageSource = "aosp" + } + } + } + } +} +``` +**Benefits:** +- Reproducible emulator configurations in CI +- No need to manually manage emulators +- Better caching + +**Recommendation:** ✅ Good for CI stability - investigate for GitHub Actions. + +#### c) **Macrobenchmark for Performance Tests** +```kotlin +@RunWith(AndroidJUnit4::class) +class StartupBenchmark { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startup() = benchmarkRule.measureRepeated( + packageName = "com.github.quarck.calnotify", + metrics = listOf(StartupTimingMetric()), + iterations = 5, + startupMode = StartupMode.COLD + ) { + pressHome() + startActivityAndWait() + } +} +``` +**Use Case:** Measuring startup time, scroll performance. +**Verdict:** Nice-to-have for performance regression testing. + +--- + +### 5. Fixture Patterns to Consider + +#### a) **Object Mother Pattern** +```kotlin +object EventMother { + fun createDefault() = EventAlertRecord( + calendarId = 1L, + eventId = 1L, + title = "Test Event", + // ... defaults + ) + + fun createMuted() = createDefault().copy(flags = MUTED_FLAG) + fun createTask() = createDefault().copy(flags = TASK_FLAG) + fun createWithReminder(minutes: Int) = createDefault().copy( + alertTime = System.currentTimeMillis() - minutes * 60000 + ) +} + +// Usage: +val event = EventMother.createMuted() +``` +**Benefits:** Centralized test data creation, self-documenting. +**Status:** You have `createTestEvent()` methods scattered across tests. +**Recommendation:** ✅ Consolidate into Object Mother classes. + +#### b) **Test Data Builders** (More flexible than Object Mother) +```kotlin +class EventBuilder { + private var title = "Test Event" + private var isMuted = false + private var startTime = System.currentTimeMillis() + + fun withTitle(title: String) = apply { this.title = title } + fun muted() = apply { this.isMuted = true } + fun startingAt(time: Long) = apply { this.startTime = time } + + fun build() = EventAlertRecord( + title = title, + // ... map all fields + ) +} + +// Usage: +val event = EventBuilder() + .withTitle("Meeting") + .muted() + .startingAt(tomorrow) + .build() +``` +**Current:** Your `BaseCalendarTestFixture.Builder` is similar. +**Recommendation:** Extract event/calendar builders from fixtures. + +#### c) **Robot Pattern for UI Tests** +```kotlin +class MainActivityRobot { + fun clickDismissAll(): MainActivityRobot { + onView(withId(R.id.dismiss_all)).perform(click()) + return this + } + + fun verifyEventCount(count: Int): MainActivityRobot { + onView(withId(R.id.event_list)) + .check(matches(hasChildCount(count))) + return this + } +} + +// Usage: +MainActivityRobot() + .clickDismissAll() + .verifyEventCount(0) +``` +**Benefits:** Encapsulates UI interactions, readable tests. +**Status:** UITestFixture has some of this, but not full robot pattern. +**Recommendation:** ✅ Consider formalizing robot pattern for complex UI tests. + +--- + +### 6. Testing Database (Room) Best Practices + +#### a) **In-Memory Database for Tests** +```kotlin +@Before +fun setup() { + db = Room.inMemoryDatabaseBuilder( + context, + AppDatabase::class.java + ).allowMainThreadQueries().build() +} +``` +**Status:** Your instrumented tests use real database. +**Note:** For your custom SQLite extensions, real DB is correct. But for pure Room tests, in-memory is faster. + +#### b) **Migration Testing** +```kotlin +@RunWith(AndroidJUnit4::class) +class MigrationTest { + @get:Rule + val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java + ) + + @Test + fun migrate1To2() { + helper.createDatabase(TEST_DB, 1).apply { + execSQL("INSERT INTO events ...") + close() + } + + helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2) + } +} +``` +**Status:** You have migration tests. +**Recommendation:** Ensure using `MigrationTestHelper` for automatic validation. + +--- + +### 7. Parameterized Testing + +#### a) **JUnit 4 Parameterized** (Current) +```kotlin +@RunWith(Parameterized::class) +class EventFormatterTest( + private val input: Long, + private val expected: String +) { + companion object { + @JvmStatic + @Parameterized.Parameters + fun data() = listOf( + arrayOf(3600000L, "1 hour"), + arrayOf(7200000L, "2 hours"), + ) + } +} +``` + +#### b) **JUnit 5 Parameterized** (Better syntax) +```kotlin +@ParameterizedTest +@CsvSource( + "3600000, '1 hour'", + "7200000, '2 hours'" +) +fun formatDuration(input: Long, expected: String) { + assertEquals(expected, formatter.format(input)) +} +``` +**Blocker:** JUnit 5 has limited Android support. +**Recommendation:** Stay with JUnit 4 parameterized for now. + +--- + +### 8. Flaky Test Prevention + +#### a) **IdlingResources** (You're already using this!) +```kotlin +class AsyncTaskIdlingResource : IdlingResource { + // Track async operations +} +``` +**Status:** ✅ Already implemented in `AsyncTaskIdlingResource`. + +#### b) **Retry Rules** +```kotlin +class RetryRule(private val retryCount: Int) : TestRule { + override fun apply(base: Statement, description: Description) = + object : Statement() { + override fun evaluate() { + var lastThrowable: Throwable? = null + repeat(retryCount) { + try { + base.evaluate() + return + } catch (t: Throwable) { + lastThrowable = t + } + } + throw lastThrowable!! + } + } +} +``` +**Use Case:** For inherently flaky tests (timing-dependent). +**Recommendation:** Use sparingly - fix root cause when possible. + +#### c) **Test Sharding** +**Status:** ✅ Already using sharding in CI (4 shards visible in artifacts). + +--- + +## Priority Recommendations + +### Quick Wins (Low Effort, High Value) +1. ✅ **Add Truth assertions** - Better failure messages, minimal change +2. ✅ **Use ShadowAlarmManager** - Cleaner than manual mocking +3. ✅ **Create Object Mother classes** - Consolidate `createTestEvent()` variants +4. ✅ **Verify PAUSED looper mode** - More realistic async testing + +### Medium Effort +5. 📝 **Formalize Robot Pattern** - For complex UI test flows +6. 📝 **Extract Test Data Builders** - From fixtures to standalone classes +7. 📝 **Document fixture usage** - Add examples and explanations + +### Investigate Further +8. 🔍 **Gradle Managed Devices** - For CI stability +9. 🔍 **Test Orchestrator** - If seeing cross-test pollution +10. 🔍 **Kaspresso** - If Ultron becomes limiting + +### Not Recommended Now +- ❌ Hilt Testing - Requires architectural changes +- ❌ JUnit 5 - Limited Android support +- ❌ Moving SQLite tests to Robolectric - Your concerns are valid + +--- + +## Sample Implementation: Object Mother + +Here's how you could consolidate event creation: + +```kotlin +// testutils/EventMother.kt +object EventMother { + private val clock = { System.currentTimeMillis() } + + fun default( + eventId: Long = 1L, + calendarId: Long = 1L, + title: String = "Test Event" + ) = EventAlertRecord( + calendarId = calendarId, + eventId = eventId, + isAllDay = false, + isRepeating = false, + alertTime = clock(), + notificationId = eventId.toInt(), + title = title, + desc = "", + startTime = clock() + Consts.HOUR_IN_MILLISECONDS, + endTime = clock() + 2 * Consts.HOUR_IN_MILLISECONDS, + instanceStartTime = clock() + Consts.HOUR_IN_MILLISECONDS, + instanceEndTime = clock() + 2 * Consts.HOUR_IN_MILLISECONDS, + location = "", + lastStatusChangeTime = clock(), + snoozedUntil = 0L, + displayStatus = EventDisplayStatus.Hidden, + color = 0, + origin = EventOrigin.ProviderBroadcast, + timeFirstSeen = clock(), + eventStatus = EventStatus.Confirmed, + attendanceStatus = AttendanceStatus.None, + flags = 0 + ) + + fun muted(eventId: Long = 1L) = default(eventId).also { it.isMuted = true } + fun task(eventId: Long = 1L) = default(eventId).also { it.isTask = true } + fun alarm(eventId: Long = 1L) = default(eventId).also { it.isAlarm = true } + fun snoozed(eventId: Long = 1L, until: Long) = default(eventId).copy(snoozedUntil = until) + fun allDay(eventId: Long = 1L) = default(eventId).copy(isAllDay = true) +} + +// Usage in tests: +val event = EventMother.muted() +val snoozedEvent = EventMother.snoozed(eventId = 2, until = tomorrow) +``` + +--- + +## References + +- [Android Testing Codelab](https://developer.android.com/codelabs/advanced-android-kotlin-testing) +- [Robolectric Documentation](http://robolectric.org/) +- [Testing Kotlin Flows](https://developer.android.com/kotlin/flow/test) +- [Espresso Idling Resources](https://developer.android.com/training/testing/espresso/idling-resource) +- [Truth Assertion Library](https://truth.dev/) +- [Kaspresso Framework](https://github.com/KasperskyLab/Kaspresso) diff --git a/docs/coverage-baseline/BASELINE-SUMMARY.md b/docs/coverage-baseline/BASELINE-SUMMARY.md new file mode 100644 index 000000000..5e9712bcb --- /dev/null +++ b/docs/coverage-baseline/BASELINE-SUMMARY.md @@ -0,0 +1,42 @@ +# JaCoCo Coverage Baseline + +This coverage baseline was captured from the `cursor/unit-test-jacoco-csv-66a5` branch. + +**Run Date:** 2026-01-17 +**Workflow Run ID:** 21102796115 +**Commit:** f68911f60b359d2c5667855336837aa7440d12c8 + +## Summary + +### Unit Test Coverage + +| Metric | Covered | Total | Percentage | +|--------|---------|-------|------------| +| Instructions | 20,646 | 79,496 | **25.97%** | +| Branches | 1,235 | 5,664 | **21.80%** | +| Lines | 3,688 | 14,849 | **24.84%** | +| Methods | 959 | 2,796 | **34.30%** | + +### Integration Test Coverage + +| Metric | Covered | Total | Percentage | +|--------|---------|-------|------------| +| Instructions | 34,814 | 79,496 | **43.79%** | +| Branches | 1,760 | 5,664 | **31.07%** | +| Lines | 6,732 | 14,849 | **45.34%** | +| Methods | 1,288 | 2,796 | **46.07%** | + +## Notes + +- **Classes analyzed:** 334 classes total +- Integration tests provide significantly higher coverage due to testing actual runtime behavior +- Unit tests focus more on isolated logic testing + +## Files + +- `unit-test-coverage.csv` - Raw JaCoCo CSV for unit tests +- `integration-test-coverage.csv` - Raw JaCoCo CSV for integration tests + +## Purpose + +This baseline establishes the current coverage state before making changes per [Issue #174](https://github.com/williscool/CalendarNotification/issues/174) to consolidate and streamline the test suite while maintaining coverage. diff --git a/docs/coverage-baseline/integration-test-coverage.csv b/docs/coverage-baseline/integration-test-coverage.csv new file mode 100644 index 000000000..15e6b19b8 --- /dev/null +++ b/docs/coverage-baseline/integration-test-coverage.csv @@ -0,0 +1,335 @@ +GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED +app,com.github.quarck.calnotify.maps,MapsIntents,23,0,0,0,2,0,2,0,2,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl,451,1206,57,45,126,295,54,28,8,23 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.Companion,0,2,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase_Impl,12,84,0,0,2,17,1,8,1,8 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertEntity,24,172,6,10,0,22,6,15,0,13 +app,com.github.quarck.calnotify.monitorstorage,RoomMonitorStorage,234,0,6,0,32,0,21,0,18,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase_Impl.createOpenDelegate._openDelegate.new RoomOpenDelegate() {...},25,250,1,1,8,27,4,5,3,5 +app,com.github.quarck.calnotify.monitorstorage,MigrationException,18,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorStorageImplV1,754,181,63,11,181,44,48,9,15,5 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase.Companion.LoggingCallback,0,29,0,0,0,7,0,3,0,3 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertEntity.Companion,0,44,0,6,0,10,0,4,0,1 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase,0,3,0,0,0,2,0,1,0,1 +app,com.github.quarck.calnotify.monitorstorage,LegacyMonitorStorage,444,0,2,0,25,0,20,0,19,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},28,115,7,7,7,30,7,3,0,3 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},0,27,0,0,0,6,0,3,0,3 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.new EntityInsertAdapter() {...},28,100,7,7,7,27,7,3,0,3 +app,com.github.quarck.calnotify.monitorstorage,MonitorStorage,5,42,2,2,0,7,3,3,1,3 +app,com.github.quarck.calnotify.monitorstorage,MonitorStorage.Companion,18,26,0,0,2,6,0,1,0,1 +app,com.github.quarck.calnotify.monitorstorage,MD5,396,0,25,0,66,0,17,0,2,0 +app,com.github.quarck.calnotify.monitorstorage,MD5Kt,104,0,2,0,7,0,5,0,4,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase.Companion,257,81,16,4,59,23,11,4,2,3 +app,com.github.quarck.calnotify.reminders,ReminderState,249,0,2,0,21,0,16,0,15,0 +app,com.github.quarck.calnotify.permissions,PermissionsManager,160,86,21,11,33,13,25,11,12,8 +app,com.github.quarck.calnotify.react,ThemePackage,17,0,0,0,3,0,3,0,3,0 +app,com.github.quarck.calnotify.react,ThemeModule,86,0,5,0,21,0,7,0,4,0 +app,com.github.quarck.calnotify.prefs,CalendarListEntryType,27,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.prefs,MaxRemindersPreferenceX.Dialog,58,0,4,0,13,0,5,0,3,0 +app,com.github.quarck.calnotify.prefs,SnoozePresetPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,TimeOfDayPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,MaxNotificationsPreferenceX,42,86,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,CalendarsActivity,484,0,25,0,97,0,30,0,17,0 +app,com.github.quarck.calnotify.prefs,SnoozePresetPreferenceX,45,85,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,ReminderSettingsFragmentX,137,0,16,0,30,0,14,0,6,0 +app,com.github.quarck.calnotify.prefs,LEDColorPickerPreferenceX.Dialog,225,0,20,0,44,0,16,0,6,0 +app,com.github.quarck.calnotify.prefs,ListPreference,118,0,6,0,18,0,12,0,9,0 +app,com.github.quarck.calnotify.prefs,TimeOfDayPreferenceX.Dialog,86,0,4,0,16,0,5,0,3,0 +app,com.github.quarck.calnotify.prefs,CalendarsActivity.loadCalendars..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,ReminderPatternPreferenceX,134,0,6,0,18,0,12,0,9,0 +app,com.github.quarck.calnotify.prefs,PreferenceHeadersFragment,77,71,6,4,11,13,8,3,3,3 +app,com.github.quarck.calnotify.prefs,QuietHoursSettingsFragmentX,38,0,2,0,9,0,4,0,3,0 +app,com.github.quarck.calnotify.prefs,DefaultManualAllDayNotificationPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,DefaultManualAllDayNotificationPreferenceX.Dialog,151,0,8,0,28,0,7,0,3,0 +app,com.github.quarck.calnotify.prefs,CarModeActivity,362,0,27,0,61,0,25,0,11,0 +app,com.github.quarck.calnotify.prefs,TimeOfDayPreferenceX,182,0,4,0,24,0,12,0,10,0 +app,com.github.quarck.calnotify.prefs,ReminderPatternPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,BlueboothDeviceListEntry,22,0,0,0,3,0,4,0,4,0 +app,com.github.quarck.calnotify.prefs,ReminderPatternPreferenceX.Dialog,446,0,42,0,97,0,31,0,10,0 +app,com.github.quarck.calnotify.prefs,MaxRemindersPreferenceX,45,87,3,3,6,12,7,5,4,5 +app,com.github.quarck.calnotify.prefs,CalendarListAdapter.ViewHolder,162,0,6,0,25,0,19,0,16,0 +app,com.github.quarck.calnotify.prefs,BehaviorSettingsFragmentX,47,8,4,0,10,3,3,2,1,2 +app,com.github.quarck.calnotify.prefs,MaxRemindersPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,PreferenceUtils,122,137,6,13,16,30,8,9,4,2 +app,com.github.quarck.calnotify.prefs,LEDPatternPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,SnoozePresetPreferenceX.Dialog,163,0,22,0,32,0,16,0,5,0 +app,com.github.quarck.calnotify.prefs,CalendarListAdapter,195,0,17,0,28,0,18,0,9,0 +app,com.github.quarck.calnotify.prefs,DefaultManualNotificationPreferenceX.Dialog,59,0,4,0,13,0,5,0,3,0 +app,com.github.quarck.calnotify.prefs,DefaultManualNotificationPreferenceX,42,86,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,MaxNotificationsPreferenceX.Dialog,69,0,6,0,14,0,6,0,3,0 +app,com.github.quarck.calnotify.prefs,LEDColorPickerPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,MiscSettingsFragmentX,532,64,36,2,104,10,31,2,12,2 +app,com.github.quarck.calnotify.prefs,LEDColorPickerPreferenceX,89,62,6,0,13,10,10,3,7,3 +app,com.github.quarck.calnotify.prefs,DefaultManualNotificationPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,NumberPickerController,68,0,0,0,12,0,9,0,9,0 +app,com.github.quarck.calnotify.prefs,NotificationSettingsFragmentX,152,33,21,1,32,8,14,3,3,3 +app,com.github.quarck.calnotify.prefs,MaxNotificationsPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,NavigationSettingsFragmentX,204,0,12,0,49,0,18,0,12,0 +app,com.github.quarck.calnotify.prefs,DefaultManualAllDayNotificationPreferenceX,42,86,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,CalendarListEntry,68,0,0,0,7,0,8,0,8,0 +app,com.github.quarck.calnotify.prefs,BlueboothDeviceListAdapter,96,0,4,0,13,0,11,0,9,0 +app,com.github.quarck.calnotify.prefs,NotificationSoundPreference,216,112,30,2,32,20,26,5,10,5 +app,com.github.quarck.calnotify.prefs,LEDPatternPreferenceX,45,85,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,SnoozeSettingsFragmentX,30,8,2,0,6,3,2,2,1,2 +app,com.github.quarck.calnotify.prefs,LEDPatternPreferenceX.Dialog,259,0,12,0,50,0,15,0,9,0 +app,com.github.quarck.calnotify.prefs,BlueboothDeviceListAdapter.ViewHolder,121,0,4,0,16,0,12,0,10,0 +app,com.github.quarck.calnotify.ui,UpcomingEventsFragment.dataUpdatedReceiver.new BroadcastReceiver() {...},4,6,0,0,2,1,1,1,1,1 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapter.ViewHolder,47,121,2,2,1,27,11,11,9,11 +app,com.github.quarck.calnotify.ui,EditEventActivityTextReceiver,3,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.ui,EventListAdapter.onRecycleViewRegistered.new RecyclerView.OnScrollListener() {...},0,25,0,0,0,4,0,2,0,2 +app,com.github.quarck.calnotify.ui,MainActivityLegacy,729,706,63,43,148,129,81,21,31,16 +app,com.github.quarck.calnotify.ui,PrivacyPolicyActivity,37,0,4,0,8,0,4,0,2,0 +app,com.github.quarck.calnotify.ui,EditEventActivity,3717,0,348,0,599,0,230,0,56,0 +app,com.github.quarck.calnotify.ui,ViewEventActivityStateCode.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.ui,UINotifier,1,16,1,1,1,5,1,1,0,1 +app,com.github.quarck.calnotify.ui,ReportABugActivity,159,0,10,0,26,0,9,0,4,0 +app,com.github.quarck.calnotify.ui,DismissedEventsActivity.Companion,7,19,3,1,0,2,3,2,1,2 +app,com.github.quarck.calnotify.ui,AboutActivity.Companion,6,0,0,0,3,0,3,0,3,0 +app,com.github.quarck.calnotify.ui,ViewEventActivityState,14,66,0,0,0,9,4,5,4,5 +app,com.github.quarck.calnotify.ui,MainActivity,60,0,4,0,13,0,4,0,2,0 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapter,104,415,23,25,19,69,25,19,7,13 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment.Companion,5,26,3,1,0,4,2,4,0,4 +app,com.github.quarck.calnotify.ui,TimeIntervalPickerController,301,157,32,8,65,42,41,4,21,4 +app,com.github.quarck.calnotify.ui,MyReactActivity,15,0,0,0,3,0,3,0,3,0 +app,com.github.quarck.calnotify.ui,SnoozeAllActivity,590,886,57,47,113,175,81,27,35,19 +app,com.github.quarck.calnotify.ui,EditEventActivityState,248,0,0,0,28,0,28,0,28,0 +app,com.github.quarck.calnotify.ui,MainActivityBase.Companion,32,131,6,8,4,19,6,10,2,7 +app,com.github.quarck.calnotify.ui,UpcomingEventsFragment.Companion,5,43,4,4,0,7,3,8,0,7 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapterKt,130,46,4,1,4,3,4,1,0,1 +app,com.github.quarck.calnotify.ui,DataUpdatedReceiverLegacy,18,9,2,0,3,1,3,1,2,1 +app,com.github.quarck.calnotify.ui,DismissedEventsActivity,58,229,4,4,7,55,11,11,7,11 +app,com.github.quarck.calnotify.ui,EditEventActivityKt,226,0,9,0,25,0,7,0,1,0 +app,com.github.quarck.calnotify.ui,AboutActivity,158,0,8,0,20,0,12,0,8,0 +app,com.github.quarck.calnotify.ui,SettingsActivityX,62,155,9,13,12,37,9,9,1,6 +app,com.github.quarck.calnotify.ui,SearchableFragment.DefaultImpls,4,13,0,0,3,6,3,6,3,6 +app,com.github.quarck.calnotify.ui,EventListAdapter.setUpItemTouchHelper.itemTouchCallback.new ItemTouchHelper.Callback() {...},308,41,19,1,50,8,24,1,14,1 +app,com.github.quarck.calnotify.ui,EventListAdapter,566,537,74,46,82,87,62,29,13,18 +app,com.github.quarck.calnotify.ui,ViewEventActivityNoRecents.Companion,8,16,3,1,0,2,3,2,1,2 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment.setupMenu.new MenuProvider() {...},0,31,0,2,0,8,0,4,0,3 +app,com.github.quarck.calnotify.ui,MainActivityModern,267,584,64,76,54,87,71,24,14,9 +app,com.github.quarck.calnotify.ui,ViewEventActivityNoRecents,1180,1216,119,66,226,221,133,42,47,30 +app,com.github.quarck.calnotify.ui,MainActivity.Companion,28,7,0,0,6,1,6,1,6,1 +app,com.github.quarck.calnotify.ui,ViewEventActivityState.Companion,27,0,0,0,4,0,1,0,1,0 +app,com.github.quarck.calnotify.ui,ActiveEventsFragment,274,365,30,16,63,75,36,23,14,22 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment,81,311,12,10,19,70,18,17,8,16 +app,com.github.quarck.calnotify.ui,EditEventActivity.ReminderWrapper,34,0,0,0,1,0,5,0,5,0 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapter.setUpItemTouchHelper.itemTouchCallback.new ItemTouchHelper.Callback() {...},313,41,19,1,44,8,24,1,14,1 +app,com.github.quarck.calnotify.ui,ActiveEventsFragment.dataUpdatedReceiver.new BroadcastReceiver() {...},4,6,0,0,2,1,1,1,1,1 +app,com.github.quarck.calnotify.ui,ActiveEventsFragment.Companion,5,23,3,1,0,4,2,4,0,4 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment.dataUpdatedReceiver.new BroadcastReceiver() {...},4,6,0,0,2,1,1,1,1,1 +app,com.github.quarck.calnotify.ui,UpcomingEventsFragment,81,254,9,7,15,53,14,13,7,12 +app,com.github.quarck.calnotify.ui,HorizontalSwipeAwareRefreshLayout,56,35,7,2,6,9,6,3,2,2 +app,com.github.quarck.calnotify.ui,EventListAdapter.ViewHolder,62,156,1,1,2,35,14,14,13,14 +app,com.github.quarck.calnotify.ui,ViewEventActivity,0,3,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.ui,ViewEventActivityStateCode,2,51,0,0,1,5,1,3,1,3 +app,com.github.quarck.calnotify.ui,MainActivityBase,267,199,22,6,73,48,27,19,11,19 +app,com.github.quarck.calnotify.ui,EditEventActivityState.Companion,97,0,0,0,27,0,1,0,1,0 +app,com.github.quarck.calnotify.ui,MainActivityLegacy.onCreateOptionsMenu.new SearchView.OnQueryTextListener() {...},35,22,4,0,5,4,3,2,1,2 +app,com.github.quarck.calnotify.ui,MainActivityModern.onCreateOptionsMenu.new SearchView.OnQueryTextListener() {...},31,16,7,1,4,3,5,2,1,2 +app,com.github.quarck.calnotify.ui,MainActivityModern.onCreateOptionsMenu.new MenuItem.OnActionExpandListener() {...},15,35,6,4,2,5,5,3,0,3 +app,com.github.quarck.calnotify.ui,MainActivityLegacy.onCreateOptionsMenu.new MenuItem.OnActionExpandListener() {...},17,65,9,7,2,8,8,3,0,3 +app,com.github.quarck.calnotify.textutils,NextNotificationType,2,19,0,0,1,2,1,1,1,1 +app,com.github.quarck.calnotify.textutils,EventFormatterInterface.DefaultImpls,0,30,0,0,0,3,0,3,0,3 +app,com.github.quarck.calnotify.textutils,EventFormatter,400,805,59,61,67,175,50,36,7,19 +app,com.github.quarck.calnotify.textutils,EventFormatter.Companion,82,101,16,14,7,18,12,6,0,3 +app,com.github.quarck.calnotify.textutils,EventFormatterKt,5,76,0,2,0,13,0,4,0,3 +app,com.github.quarck.calnotify.textutils,NextNotificationInfo,0,24,0,0,0,4,0,4,0,4 +app,com.github.quarck.calnotify.upcoming,UpcomingEventsProvider.getUpcomingEvents..inlined.sortedBy.new Comparator() {...},0,15,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.upcoming,UpcomingEventsLookahead,47,40,3,1,10,9,3,3,1,3 +app,com.github.quarck.calnotify.upcoming,UpcomingEventsProvider,14,152,4,4,2,41,4,3,0,3 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase.Companion.LoggingCallback,0,29,0,0,0,7,0,3,0,3 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageState,8,86,0,0,0,3,1,5,1,5 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl,382,1794,87,93,111,448,94,23,8,19 +app,com.github.quarck.calnotify.eventsstorage,EventsMigrationException,18,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase_Impl,12,84,0,0,2,17,1,8,1,8 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV7,950,0,42,0,200,0,41,0,20,0 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV8,1081,0,66,0,239,0,54,0,21,0 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV6,771,0,34,0,163,0,37,0,20,0 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV9,823,314,59,7,187,70,46,8,16,5 +app,com.github.quarck.calnotify.eventsstorage,EventAlertEntity,99,505,21,23,0,56,21,34,0,33 +app,com.github.quarck.calnotify.eventsstorage,EventsStorage,3,73,3,3,1,14,3,4,0,4 +app,com.github.quarck.calnotify.eventsstorage,EventWithNewInstanceTime,9,15,0,0,0,4,3,1,3,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.Companion,0,2,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase,0,3,0,0,0,2,0,1,0,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertEntity.Companion,0,90,0,4,0,24,0,3,0,1 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase_Impl.createOpenDelegate._openDelegate.new RoomOpenDelegate() {...},25,502,1,1,8,47,4,5,3,5 +app,com.github.quarck.calnotify.eventsstorage,EventsStorage.Companion,18,26,0,0,2,6,0,1,0,1 +app,com.github.quarck.calnotify.eventsstorage,LegacyEventsStorage,821,45,64,0,83,9,52,4,19,4 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase.Companion,258,81,16,4,59,23,11,4,2,3 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageInterface.DefaultImpls,64,70,0,0,11,12,1,1,1,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},19,3,0,0,4,1,2,1,2,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},112,316,28,28,28,91,28,3,0,3 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.new EntityInsertAdapter() {...},112,306,28,28,28,89,28,3,0,3 +app,com.github.quarck.calnotify.eventsstorage,RoomEventsStorage,1079,0,100,0,156,0,78,0,28,0 +app,com.github.quarck.calnotify.utils,RGB,56,0,0,0,3,0,6,0,6,0 +app,com.github.quarck.calnotify.utils,AsyncUtilsKt,15,5,0,0,2,2,1,2,1,2 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.LongProperty,22,48,2,2,0,8,5,3,3,3 +app,com.github.quarck.calnotify.utils,CNPlusSystemClock,3,16,0,0,1,5,1,3,1,3 +app,com.github.quarck.calnotify.utils,PersistentStorageBase,79,141,3,3,12,27,9,11,7,10 +app,com.github.quarck.calnotify.utils,DateTimeUtils,47,177,4,6,14,26,6,15,2,14 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.StringProperty,22,54,2,2,0,8,5,3,3,3 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.FloatProperty,70,0,4,0,8,0,8,0,6,0 +app,com.github.quarck.calnotify.utils,ViewUtilsKt,49,32,8,2,2,4,7,4,2,4 +app,com.github.quarck.calnotify.utils,ColorUtilsKt,170,180,12,12,23,23,9,7,1,3 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.BooleanProperty,22,48,2,2,0,8,5,3,3,3 +app,com.github.quarck.calnotify.utils,DateTimeUtilsKt,114,0,0,0,18,0,15,0,15,0 +app,com.github.quarck.calnotify.utils,AsyncOperation,29,40,4,2,3,10,7,4,4,4 +app,com.github.quarck.calnotify.utils,SystemUtilsKt,50,259,2,4,8,41,8,11,6,10 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.StringSetProperty,76,0,4,0,8,0,8,0,6,0 +app,com.github.quarck.calnotify.utils,StringUtilsKt,19,13,0,0,8,4,1,1,1,1 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.IntProperty,22,48,2,2,0,8,5,3,3,3 +app,com.github.quarck.calnotify,PersistentStateKt,0,8,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify,GlobalStateKt,2,12,1,1,1,3,1,1,0,1 +app,com.github.quarck.calnotify,Consts,2,179,0,0,0,8,1,3,1,3 +app,com.github.quarck.calnotify,GlobalState.reactNativeHost.new DefaultReactNativeHost() {...},28,14,0,0,3,3,5,1,5,1 +app,com.github.quarck.calnotify,NotificationSettings,9,118,0,0,0,22,3,13,3,13 +app,com.github.quarck.calnotify,PersistentState,17,113,0,0,0,4,2,6,2,6 +app,com.github.quarck.calnotify,VibrationSettings,52,25,10,0,9,4,7,4,2,4 +app,com.github.quarck.calnotify,PebbleSettings,9,27,0,0,0,6,3,3,3,3 +app,com.github.quarck.calnotify,Settings,227,595,29,23,37,118,49,58,23,58 +app,com.github.quarck.calnotify,GlobalState,24,53,1,1,4,16,4,6,3,6 +app,com.github.quarck.calnotify,LedSettings,65,24,12,0,10,4,9,4,3,4 +app,com.github.quarck.calnotify,NotificationBehaviorSettings,0,27,0,0,0,5,0,5,0,5 +app,com.github.quarck.calnotify.calendareditor,EventChangeStatus,50,0,0,0,6,0,5,0,5,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeManager,715,0,36,0,141,0,28,0,10,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangePersistentState,61,0,0,0,2,0,5,0,5,0 +app,com.github.quarck.calnotify.calendareditor,EventChangeRequestType.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendareditor,EventChangeStatus.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeRequestMonitor.ValidationResultCommand,33,0,0,0,5,0,2,0,2,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeRequestMonitor,1059,107,94,9,242,19,62,4,8,4 +app,com.github.quarck.calnotify.calendareditor,EventChangeRequestType,50,0,0,0,6,0,5,0,5,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeRequest,180,0,2,0,17,0,23,0,22,0 +app,com.github.quarck.calnotify.logs,DevLog,0,40,0,0,0,4,0,4,0,4 +app,com.github.quarck.calnotify.logs,LogcatProvider,110,0,6,0,16,0,4,0,1,0 +app,com.github.quarck.calnotify.notification,EventNotificationManager.Companion,9,24,6,4,3,8,5,1,0,1 +app,com.github.quarck.calnotify.notification,EventNotificationManager.postEverythingCollapsed..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.notification,DisplayToast,34,0,0,0,3,0,4,0,4,0 +app,com.github.quarck.calnotify.notification,NotificationContext,141,0,26,0,23,0,24,0,11,0 +app,com.github.quarck.calnotify.notification,EventNotificationManager.postNumNotificationsCollapsed..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.notification,NotificationContext.Companion,246,126,70,24,46,22,51,13,8,9 +app,com.github.quarck.calnotify.notification,NotificationActionMuteToggleService,78,0,10,0,14,0,7,0,2,0 +app,com.github.quarck.calnotify.notification,NotificationActionSnoozeService,258,0,18,0,38,0,14,0,5,0 +app,com.github.quarck.calnotify.notification,ChannelCategory,56,0,5,0,13,0,7,0,3,0 +app,com.github.quarck.calnotify.notification,NotificationActionDismissService,108,0,10,0,27,0,7,0,2,0 +app,com.github.quarck.calnotify.notification,EventNotificationManagerInterface.DefaultImpls,0,17,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.notification,EventNotificationManager,1771,1507,219,112,410,355,155,58,17,30 +app,com.github.quarck.calnotify.notification,EventNotificationManager.NotificationMode,2,25,0,0,1,3,1,1,1,1 +app,com.github.quarck.calnotify.notification,NotificationChannels,37,308,13,11,4,86,12,4,0,4 +app,com.github.quarck.calnotify.notification,EventNotificationManager.arrangeEvents..inlined.sortedBy.new Comparator() {...},0,15,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.broadcastreceivers,RemoteCommandBroadcastReceiver,9,0,4,0,4,0,4,0,2,0 +app,com.github.quarck.calnotify.broadcastreceivers,SnoozeExactAlarmBroadcastReceiver,0,9,0,2,0,4,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderAlarmGenericBroadcastReceiver.Companion,49,0,0,0,13,0,13,0,13,0 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventAlarmBroadcastReceiver,0,3,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventExactAlarmBroadcastReceiver,0,3,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.broadcastreceivers,BootCompleteBroadcastReceiver,0,14,0,2,0,5,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,RescheduleConfirmationsBroadcastReceiver,30,0,6,0,9,0,5,0,2,0 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventAlarmPeriodicRescanBroadcastReceiver,0,14,1,3,0,5,1,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderAlarmBroadcastReceiver,8,0,0,0,2,0,2,0,2,0 +app,com.github.quarck.calnotify.broadcastreceivers,CalendarChangedBroadcastReceiver,1,14,1,1,1,5,1,2,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderAlarmGenericBroadcastReceiver,435,0,62,0,87,0,42,0,11,0 +app,com.github.quarck.calnotify.broadcastreceivers,EventReminderBroadcastReceiver,5,22,2,2,2,5,2,2,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderExactAlarmBroadcastReceiver,8,0,0,0,2,0,2,0,2,0 +app,com.github.quarck.calnotify.broadcastreceivers,SnoozeAlarmBroadcastReceiver,0,9,0,2,0,4,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,TimeSetBroadcastReceiver,0,14,0,2,0,5,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventAlarmGenericBroadcastReceiver,0,14,1,3,0,5,1,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,AppUpdatedBroadcastReceiver,0,14,0,2,0,5,0,3,0,2 +app,com.github.quarck.calnotify.pebble,PebbleUtils,124,0,2,0,23,0,11,0,10,0 +app,com.github.quarck.calnotify.bluetooth,BTCarModeStorageKt,0,8,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.bluetooth,BTDeviceSummary,27,0,0,0,1,0,4,0,4,0 +app,com.github.quarck.calnotify.bluetooth,BTDeviceManager,153,90,21,1,19,14,17,9,6,9 +app,com.github.quarck.calnotify.bluetooth,BTCarModeStorage,36,95,0,0,2,4,3,5,3,5 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorService.Companion,37,46,0,0,7,13,1,1,1,1 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorInterface.DefaultImpls,0,28,0,0,0,5,0,1,0,1 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual.manualFireEventsAt_NoHousekeeping..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual.scanNextEvent..inlined.sortedBy.new Comparator() {...},0,15,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual.scanNextEvent..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorState,0,163,0,0,0,5,0,10,0,10 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitor,344,767,29,31,72,158,30,25,7,18 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorService,37,145,1,11,9,43,2,11,1,6 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual,313,796,37,45,52,141,32,27,5,13 +app,com.github.quarck.calnotify.app,ApplicationController,1232,3026,134,182,207,459,133,126,30,71 +app,com.github.quarck.calnotify.app,TagsManager,1,83,1,9,0,14,1,8,0,4 +app,com.github.quarck.calnotify.app,SnoozeResult,6,18,0,0,0,1,2,2,2,2 +app,com.github.quarck.calnotify.app,AlarmScheduler,0,268,3,27,0,62,3,16,0,4 +app,com.github.quarck.calnotify.app,CalendarReloadManager.ReloadCalendarResult,0,62,0,0,0,7,0,7,0,7 +app,com.github.quarck.calnotify.app,AlarmScheduler.Companion,10,39,6,2,0,7,4,7,0,7 +app,com.github.quarck.calnotify.app,UndoState,30,0,0,0,1,0,4,0,4,0 +app,com.github.quarck.calnotify.app,SnoozeResultKt,81,23,5,3,11,6,4,1,0,1 +app,com.github.quarck.calnotify.app,CalendarReloadManager,236,504,27,41,44,92,22,22,0,9 +app,com.github.quarck.calnotify.app,ApplicationControllerInterface.DefaultImpls,0,91,0,0,0,12,0,6,0,6 +app,com.github.quarck.calnotify.app,CalendarReloadManager.ReloadCalendarResultCode,2,31,0,0,1,4,1,1,1,1 +app,com.github.quarck.calnotify.app,SnoozeType,2,19,0,0,1,2,1,1,1,1 +app,com.github.quarck.calnotify.app,UndoManager,56,37,8,2,10,8,9,2,4,2 +app,com.github.quarck.calnotify.backup,ImportResult.VersionTooNew,16,0,0,0,1,0,3,0,3,0 +app,com.github.quarck.calnotify.backup,SettingsBackupManager.Companion,15,0,0,0,3,0,1,0,1,0 +app,com.github.quarck.calnotify.backup,BackupData.Companion,3,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.backup,ImportResult.Success,13,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.backup,ImportResult.IoError,13,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.backup,ImportResult.ParseError,13,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.backup,BackupData,122,0,0,0,10,0,10,0,10,0 +app,com.github.quarck.calnotify.backup,CalendarSettingBackup.Companion,3,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.backup,ImportStats,69,0,0,0,7,0,7,0,7,0 +app,com.github.quarck.calnotify.backup,SettingsBackupManager,903,68,84,0,177,12,52,3,10,3 +app,com.github.quarck.calnotify.backup,CalendarSettingBackup,54,0,0,0,7,0,7,0,7,0 +app,com.github.quarck.calnotify.database,SQLiteOpenHelper,83,56,6,0,9,13,6,4,3,4 +app,com.github.quarck.calnotify.database,CrSqliteRoomFactory.crSqliteOptions.new RequerySQLiteOpenHelperFactory.ConfigurationOptions() {...},0,22,0,0,0,5,0,2,0,2 +app,com.github.quarck.calnotify.database,CrSqliteRoomFactory,0,34,0,0,0,5,0,2,0,2 +app,com.github.quarck.calnotify.database,SQLiteDatabaseExtensions,152,82,5,9,21,20,7,5,3,2 +app,com.github.quarck.calnotify.database,CrSqliteFinalizeWrapper,21,32,0,0,4,6,0,2,0,2 +app,com.github.quarck.calnotify.quiethours,QuietHoursManagerInterface.DefaultImpls,15,10,0,0,2,1,2,1,2,1 +app,com.github.quarck.calnotify.quiethours,QuietHoursManager,299,232,45,17,61,31,31,16,3,13 +app,com.github.quarck.calnotify.calendareditor.storage,CalendarChangeRequestsStorage,204,64,4,0,15,8,11,4,9,4 +app,com.github.quarck.calnotify.calendareditor.storage,CalendarChangeRequestsStorageImplV3,728,213,13,1,139,58,16,4,9,4 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventAlertRecord,0,27,0,0,0,4,0,4,0,4 +app,com.github.quarck.calnotify.dismissedeventsstorage,RoomDismissedEventsStorage.special..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase_Impl.createOpenDelegate._openDelegate.new RoomOpenDelegate() {...},25,489,1,1,8,46,4,5,3,5 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl.Companion,0,2,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorage.Companion,19,27,0,0,2,6,0,1,0,1 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase.Companion,255,81,16,4,59,23,11,4,2,3 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl,144,667,29,29,41,165,32,14,4,13 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl.new EntityInsertAdapter() {...},108,292,27,27,27,86,27,3,0,3 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl.new EntityDeleteOrUpdateAdapter() {...},19,3,0,0,4,1,2,1,2,1 +app,com.github.quarck.calnotify.dismissedeventsstorage,RoomDismissedEventsStorage,157,0,0,0,26,0,12,0,12,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,LegacyDismissedEventsStorage,430,0,12,0,47,0,22,0,16,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventEntity.Companion,4,176,2,6,0,42,2,4,0,2 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase.Companion.LoggingCallback,0,29,0,0,0,7,0,3,0,3 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase,0,3,0,0,0,2,0,1,0,1 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissType.Companion,0,4,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventEntity,86,474,19,19,0,54,19,32,0,32 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorage,16,48,4,0,2,5,3,4,1,4 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissResult.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorageImplV1,558,0,12,0,121,0,18,0,12,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorageImplV2,335,288,5,7,66,67,10,8,7,5 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissResult,9,76,0,0,2,9,3,2,3,2 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase_Impl,12,84,0,0,2,17,1,8,1,8 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsMigrationException,18,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissType,19,60,6,0,3,7,6,4,3,4 +app,com.github.quarck.calnotify.dismissedeventsstorage,LegacyDismissedEventsStorage._get_eventsForDisplay_.lambda.14..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,CalendarRecord,15,78,0,0,0,13,5,8,5,8 +app,com.github.quarck.calnotify.calendar,CalendarEventDetails,2,154,0,0,0,15,0,16,0,16 +app,com.github.quarck.calnotify.calendar,EventStatus,6,51,0,0,2,5,2,3,2,3 +app,com.github.quarck.calnotify.calendar,CalendarEventDetails.Companion,21,0,0,0,9,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,EventRecord,48,86,0,0,5,17,7,16,7,16 +app,com.github.quarck.calnotify.calendar,RecurrenceExpander,368,0,49,0,74,0,32,0,6,0 +app,com.github.quarck.calnotify.calendar,CalendarIntents,129,55,8,4,21,10,11,2,5,2 +app,com.github.quarck.calnotify.calendar,CalendarBackupInfo,3,51,0,0,0,7,1,6,1,6 +app,com.github.quarck.calnotify.calendar,CalendarProviderInterface.DefaultImpls,10,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,EventAlertRecordKey,6,9,0,0,0,1,2,1,2,1 +app,com.github.quarck.calnotify.calendar,CalendarProvider.EventEntry,0,27,0,0,0,5,0,5,0,5 +app,com.github.quarck.calnotify.calendar,EventReminderRecord.Companion,37,11,0,0,4,1,1,1,1,1 +app,com.github.quarck.calnotify.calendar,AttendanceStatus,6,65,0,0,2,7,2,3,2,3 +app,com.github.quarck.calnotify.calendar,EventStatus.Companion,11,18,4,4,3,4,3,2,0,1 +app,com.github.quarck.calnotify.calendar,EventDisplayStatus.Companion,0,4,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.calendar,EventRecordKt,177,70,26,6,21,12,19,4,5,2 +app,com.github.quarck.calnotify.calendar,EventReminderRecord,6,25,0,0,1,4,1,4,1,4 +app,com.github.quarck.calnotify.calendar,EventAlertRecordSpecialType,43,0,0,0,5,0,5,0,5,0 +app,com.github.quarck.calnotify.calendar,EventAlertRecord,91,349,4,0,2,32,13,44,11,44 +app,com.github.quarck.calnotify.calendar,EventDisplayStatus,6,44,0,0,2,4,2,3,2,3 +app,com.github.quarck.calnotify.calendar,EventOrigin,12,60,3,1,5,8,5,4,2,4 +app,com.github.quarck.calnotify.calendar,EventAlertRecordSpecialType.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,AttendanceStatus.Companion,27,18,8,4,5,4,5,2,0,1 +app,com.github.quarck.calnotify.calendar,CalendarProvider,1947,2548,262,164,419,605,197,49,8,23 +app,com.github.quarck.calnotify.calendar,EventOrigin.Companion,0,4,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.calendar,MonitorEventAlertEntryKey,9,12,0,0,0,4,3,1,3,1 +app,com.github.quarck.calnotify.calendar,EventAlertRecordKt,302,280,37,43,83,47,35,21,5,11 +app,com.github.quarck.calnotify.calendar,MonitorEventAlertEntry,67,90,13,1,9,12,10,13,3,13 diff --git a/docs/coverage-baseline/unit-test-coverage.csv b/docs/coverage-baseline/unit-test-coverage.csv new file mode 100644 index 000000000..f44aa56e1 --- /dev/null +++ b/docs/coverage-baseline/unit-test-coverage.csv @@ -0,0 +1,335 @@ +GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED +app,com.github.quarck.calnotify.maps,MapsIntents,23,0,0,0,2,0,2,0,2,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl,1657,0,102,0,421,0,82,0,31,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.Companion,2,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase_Impl,96,0,0,0,19,0,9,0,9,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertEntity,196,0,16,0,22,0,21,0,13,0 +app,com.github.quarck.calnotify.monitorstorage,RoomMonitorStorage,234,0,6,0,32,0,21,0,18,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase_Impl.createOpenDelegate._openDelegate.new RoomOpenDelegate() {...},275,0,2,0,35,0,9,0,8,0 +app,com.github.quarck.calnotify.monitorstorage,MigrationException,18,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorStorageImplV1,935,0,74,0,225,0,57,0,20,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase.Companion.LoggingCallback,29,0,0,0,7,0,3,0,3,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertEntity.Companion,44,0,6,0,10,0,4,0,1,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase,3,0,0,0,2,0,1,0,1,0 +app,com.github.quarck.calnotify.monitorstorage,LegacyMonitorStorage,444,0,2,0,25,0,20,0,19,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},143,0,14,0,37,0,10,0,3,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},27,0,0,0,6,0,3,0,3,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorAlertDao_Impl.new EntityInsertAdapter() {...},128,0,14,0,34,0,10,0,3,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorStorage,47,0,4,0,7,0,6,0,4,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorStorage.Companion,44,0,0,0,8,0,1,0,1,0 +app,com.github.quarck.calnotify.monitorstorage,MD5,396,0,25,0,66,0,17,0,2,0 +app,com.github.quarck.calnotify.monitorstorage,MD5Kt,104,0,2,0,7,0,5,0,4,0 +app,com.github.quarck.calnotify.monitorstorage,MonitorDatabase.Companion,338,0,20,0,82,0,15,0,5,0 +app,com.github.quarck.calnotify.reminders,ReminderState,109,140,2,0,11,10,10,6,9,6 +app,com.github.quarck.calnotify.permissions,PermissionsManager,170,76,22,10,33,13,25,11,13,7 +app,com.github.quarck.calnotify.react,ThemePackage,17,0,0,0,3,0,3,0,3,0 +app,com.github.quarck.calnotify.react,ThemeModule,86,0,5,0,21,0,7,0,4,0 +app,com.github.quarck.calnotify.prefs,CalendarListEntryType,2,25,0,0,0,1,1,1,1,1 +app,com.github.quarck.calnotify.prefs,MaxRemindersPreferenceX.Dialog,58,0,4,0,13,0,5,0,3,0 +app,com.github.quarck.calnotify.prefs,SnoozePresetPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,TimeOfDayPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,MaxNotificationsPreferenceX,42,86,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,CalendarsActivity,163,321,12,13,47,50,21,9,10,7 +app,com.github.quarck.calnotify.prefs,SnoozePresetPreferenceX,45,85,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,ReminderSettingsFragmentX,104,33,15,1,22,8,11,3,3,3 +app,com.github.quarck.calnotify.prefs,LEDColorPickerPreferenceX.Dialog,225,0,20,0,44,0,16,0,6,0 +app,com.github.quarck.calnotify.prefs,ListPreference,118,0,6,0,18,0,12,0,9,0 +app,com.github.quarck.calnotify.prefs,TimeOfDayPreferenceX.Dialog,86,0,4,0,16,0,5,0,3,0 +app,com.github.quarck.calnotify.prefs,CalendarsActivity.loadCalendars..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,ReminderPatternPreferenceX,47,87,3,3,5,13,7,5,4,5 +app,com.github.quarck.calnotify.prefs,PreferenceHeadersFragment,77,71,6,4,11,13,8,3,3,3 +app,com.github.quarck.calnotify.prefs,QuietHoursSettingsFragmentX,30,8,2,0,6,3,2,2,1,2 +app,com.github.quarck.calnotify.prefs,DefaultManualAllDayNotificationPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,DefaultManualAllDayNotificationPreferenceX.Dialog,151,0,8,0,28,0,7,0,3,0 +app,com.github.quarck.calnotify.prefs,CarModeActivity,362,0,27,0,61,0,25,0,11,0 +app,com.github.quarck.calnotify.prefs,TimeOfDayPreferenceX,53,129,2,2,6,18,6,6,4,6 +app,com.github.quarck.calnotify.prefs,ReminderPatternPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,BlueboothDeviceListEntry,22,0,0,0,3,0,4,0,4,0 +app,com.github.quarck.calnotify.prefs,ReminderPatternPreferenceX.Dialog,446,0,42,0,97,0,31,0,10,0 +app,com.github.quarck.calnotify.prefs,MaxRemindersPreferenceX,45,87,3,3,6,12,7,5,4,5 +app,com.github.quarck.calnotify.prefs,CalendarListAdapter.ViewHolder,80,82,6,0,8,17,12,7,9,7 +app,com.github.quarck.calnotify.prefs,BehaviorSettingsFragmentX,47,8,4,0,10,3,3,2,1,2 +app,com.github.quarck.calnotify.prefs,MaxRemindersPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,PreferenceUtils,110,149,6,13,15,31,7,10,3,3 +app,com.github.quarck.calnotify.prefs,LEDPatternPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,SnoozePresetPreferenceX.Dialog,163,0,22,0,32,0,16,0,5,0 +app,com.github.quarck.calnotify.prefs,CalendarListAdapter,35,160,8,9,1,27,10,8,3,6 +app,com.github.quarck.calnotify.prefs,DefaultManualNotificationPreferenceX.Dialog,59,0,4,0,13,0,5,0,3,0 +app,com.github.quarck.calnotify.prefs,DefaultManualNotificationPreferenceX,42,86,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,MaxNotificationsPreferenceX.Dialog,69,0,6,0,14,0,6,0,3,0 +app,com.github.quarck.calnotify.prefs,LEDColorPickerPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,MiscSettingsFragmentX,532,64,36,2,104,10,31,2,12,2 +app,com.github.quarck.calnotify.prefs,LEDColorPickerPreferenceX,89,62,6,0,13,10,10,3,7,3 +app,com.github.quarck.calnotify.prefs,DefaultManualNotificationPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,NumberPickerController,68,0,0,0,12,0,9,0,9,0 +app,com.github.quarck.calnotify.prefs,NotificationSettingsFragmentX,152,33,21,1,32,8,14,3,3,3 +app,com.github.quarck.calnotify.prefs,MaxNotificationsPreferenceX.Dialog.Companion,21,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.prefs,NavigationSettingsFragmentX,204,0,12,0,49,0,18,0,12,0 +app,com.github.quarck.calnotify.prefs,DefaultManualAllDayNotificationPreferenceX,42,86,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,CalendarListEntry,7,61,0,0,0,7,2,6,2,6 +app,com.github.quarck.calnotify.prefs,BlueboothDeviceListAdapter,96,0,4,0,13,0,11,0,9,0 +app,com.github.quarck.calnotify.prefs,NotificationSoundPreference,214,114,29,3,29,23,26,5,10,5 +app,com.github.quarck.calnotify.prefs,LEDPatternPreferenceX,45,85,2,2,5,13,6,5,4,5 +app,com.github.quarck.calnotify.prefs,SnoozeSettingsFragmentX,30,8,2,0,6,3,2,2,1,2 +app,com.github.quarck.calnotify.prefs,LEDPatternPreferenceX.Dialog,259,0,12,0,50,0,15,0,9,0 +app,com.github.quarck.calnotify.prefs,BlueboothDeviceListAdapter.ViewHolder,121,0,4,0,16,0,12,0,10,0 +app,com.github.quarck.calnotify.ui,UpcomingEventsFragment.dataUpdatedReceiver.new BroadcastReceiver() {...},4,6,0,0,2,1,1,1,1,1 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapter.ViewHolder,63,105,3,1,4,24,12,10,10,10 +app,com.github.quarck.calnotify.ui,EditEventActivityTextReceiver,3,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.ui,EventListAdapter.onRecycleViewRegistered.new RecyclerView.OnScrollListener() {...},0,25,0,0,0,4,0,2,0,2 +app,com.github.quarck.calnotify.ui,MainActivityLegacy,793,642,68,38,164,113,83,19,32,15 +app,com.github.quarck.calnotify.ui,PrivacyPolicyActivity,37,0,4,0,8,0,4,0,2,0 +app,com.github.quarck.calnotify.ui,EditEventActivity,3717,0,348,0,599,0,230,0,56,0 +app,com.github.quarck.calnotify.ui,ViewEventActivityStateCode.Companion,0,4,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.ui,UINotifier,1,16,1,1,1,5,1,1,0,1 +app,com.github.quarck.calnotify.ui,ReportABugActivity,159,0,10,0,26,0,9,0,4,0 +app,com.github.quarck.calnotify.ui,DismissedEventsActivity.Companion,9,17,2,2,0,2,2,3,0,3 +app,com.github.quarck.calnotify.ui,AboutActivity.Companion,6,0,0,0,3,0,3,0,3,0 +app,com.github.quarck.calnotify.ui,ViewEventActivityState,15,65,0,0,0,9,3,6,3,6 +app,com.github.quarck.calnotify.ui,MainActivity,60,0,4,0,13,0,4,0,2,0 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapter,158,361,25,23,24,64,26,18,8,12 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment.Companion,9,22,2,2,0,4,2,4,0,4 +app,com.github.quarck.calnotify.ui,TimeIntervalPickerController,97,361,11,29,13,94,21,24,12,13 +app,com.github.quarck.calnotify.ui,MyReactActivity,15,0,0,0,3,0,3,0,3,0 +app,com.github.quarck.calnotify.ui,SnoozeAllActivity,904,572,73,31,182,106,91,17,45,9 +app,com.github.quarck.calnotify.ui,EditEventActivityState,248,0,0,0,28,0,28,0,28,0 +app,com.github.quarck.calnotify.ui,MainActivityBase.Companion,85,78,9,5,11,12,7,9,0,9 +app,com.github.quarck.calnotify.ui,UpcomingEventsFragment.Companion,10,38,5,3,0,7,4,7,0,7 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapterKt,130,46,4,1,4,3,4,1,0,1 +app,com.github.quarck.calnotify.ui,DataUpdatedReceiverLegacy,18,9,2,0,3,1,3,1,2,1 +app,com.github.quarck.calnotify.ui,DismissedEventsActivity,146,141,6,2,29,33,14,8,10,8 +app,com.github.quarck.calnotify.ui,EditEventActivityKt,226,0,9,0,25,0,7,0,1,0 +app,com.github.quarck.calnotify.ui,AboutActivity,158,0,8,0,20,0,12,0,8,0 +app,com.github.quarck.calnotify.ui,SettingsActivityX,116,101,10,12,24,25,10,8,2,5 +app,com.github.quarck.calnotify.ui,SearchableFragment.DefaultImpls,4,13,0,0,3,6,3,6,3,6 +app,com.github.quarck.calnotify.ui,EventListAdapter.setUpItemTouchHelper.itemTouchCallback.new ItemTouchHelper.Callback() {...},308,41,19,1,50,8,24,1,14,1 +app,com.github.quarck.calnotify.ui,EventListAdapter,595,508,77,43,87,82,63,28,14,17 +app,com.github.quarck.calnotify.ui,ViewEventActivityNoRecents.Companion,6,18,2,2,0,2,2,3,0,3 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment.setupMenu.new MenuProvider() {...},14,17,2,0,5,3,2,2,1,2 +app,com.github.quarck.calnotify.ui,MainActivityModern,283,568,68,72,58,83,73,22,14,9 +app,com.github.quarck.calnotify.ui,ViewEventActivityNoRecents,1400,996,129,56,274,173,140,35,55,22 +app,com.github.quarck.calnotify.ui,MainActivity.Companion,27,8,0,0,5,2,5,2,5,2 +app,com.github.quarck.calnotify.ui,ViewEventActivityState.Companion,0,27,0,0,0,4,0,1,0,1 +app,com.github.quarck.calnotify.ui,ActiveEventsFragment,326,313,33,13,75,63,37,22,15,21 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment,163,229,15,7,41,48,21,14,11,13 +app,com.github.quarck.calnotify.ui,EditEventActivity.ReminderWrapper,34,0,0,0,1,0,5,0,5,0 +app,com.github.quarck.calnotify.ui,DismissedEventListAdapter.setUpItemTouchHelper.itemTouchCallback.new ItemTouchHelper.Callback() {...},313,41,19,1,44,8,24,1,14,1 +app,com.github.quarck.calnotify.ui,ActiveEventsFragment.dataUpdatedReceiver.new BroadcastReceiver() {...},4,6,0,0,2,1,1,1,1,1 +app,com.github.quarck.calnotify.ui,ActiveEventsFragment.Companion,6,22,2,2,0,4,2,4,0,4 +app,com.github.quarck.calnotify.ui,DismissedEventsFragment.dataUpdatedReceiver.new BroadcastReceiver() {...},4,6,0,0,2,1,1,1,1,1 +app,com.github.quarck.calnotify.ui,UpcomingEventsFragment,81,254,9,7,15,53,14,13,7,12 +app,com.github.quarck.calnotify.ui,HorizontalSwipeAwareRefreshLayout,78,13,9,0,12,3,8,1,3,1 +app,com.github.quarck.calnotify.ui,EventListAdapter.ViewHolder,71,147,1,1,4,33,15,13,14,13 +app,com.github.quarck.calnotify.ui,ViewEventActivity,3,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.ui,ViewEventActivityStateCode,2,51,0,0,1,5,1,3,1,3 +app,com.github.quarck.calnotify.ui,MainActivityBase,247,219,22,6,70,51,29,17,13,17 +app,com.github.quarck.calnotify.ui,EditEventActivityState.Companion,97,0,0,0,27,0,1,0,1,0 +app,com.github.quarck.calnotify.ui,MainActivityLegacy.onCreateOptionsMenu.new SearchView.OnQueryTextListener() {...},35,22,4,0,5,4,3,2,1,2 +app,com.github.quarck.calnotify.ui,MainActivityModern.onCreateOptionsMenu.new SearchView.OnQueryTextListener() {...},4,43,4,4,0,7,4,3,0,3 +app,com.github.quarck.calnotify.ui,MainActivityModern.onCreateOptionsMenu.new MenuItem.OnActionExpandListener() {...},4,46,3,7,0,7,3,5,0,3 +app,com.github.quarck.calnotify.ui,MainActivityLegacy.onCreateOptionsMenu.new MenuItem.OnActionExpandListener() {...},6,76,6,10,0,10,6,5,0,3 +app,com.github.quarck.calnotify.textutils,NextNotificationType,2,19,0,0,1,2,1,1,1,1 +app,com.github.quarck.calnotify.textutils,EventFormatterInterface.DefaultImpls,0,30,0,0,0,3,0,3,0,3 +app,com.github.quarck.calnotify.textutils,EventFormatter,362,843,55,65,54,188,47,39,6,20 +app,com.github.quarck.calnotify.textutils,EventFormatter.Companion,3,180,2,28,0,25,2,16,0,3 +app,com.github.quarck.calnotify.textutils,EventFormatterKt,5,76,0,2,0,13,0,4,0,3 +app,com.github.quarck.calnotify.textutils,NextNotificationInfo,0,24,0,0,0,4,0,4,0,4 +app,com.github.quarck.calnotify.upcoming,UpcomingEventsProvider.getUpcomingEvents..inlined.sortedBy.new Comparator() {...},0,15,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.upcoming,UpcomingEventsLookahead,0,87,0,4,0,19,0,6,0,4 +app,com.github.quarck.calnotify.upcoming,UpcomingEventsProvider,2,164,1,7,0,43,1,6,0,3 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase.Companion.LoggingCallback,26,3,0,0,6,1,2,1,2,1 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageState,8,86,0,0,0,3,1,5,1,5 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl,2145,31,180,0,551,8,115,2,25,2 +app,com.github.quarck.calnotify.eventsstorage,EventsMigrationException,18,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase_Impl,12,84,0,0,2,17,1,8,1,8 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV7,950,0,42,0,200,0,41,0,20,0 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV8,1081,0,66,0,239,0,54,0,21,0 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV6,771,0,34,0,163,0,37,0,20,0 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageImplV9,1137,0,66,0,257,0,54,0,21,0 +app,com.github.quarck.calnotify.eventsstorage,EventAlertEntity,604,0,44,0,56,0,55,0,33,0 +app,com.github.quarck.calnotify.eventsstorage,EventsStorage,20,56,5,1,3,12,5,2,2,2 +app,com.github.quarck.calnotify.eventsstorage,EventWithNewInstanceTime,9,15,0,0,0,4,3,1,3,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.Companion,0,2,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase,0,3,0,0,0,2,0,1,0,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertEntity.Companion,90,0,4,0,24,0,3,0,1,0 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase_Impl.createOpenDelegate._openDelegate.new RoomOpenDelegate() {...},518,9,2,0,54,1,8,1,7,1 +app,com.github.quarck.calnotify.eventsstorage,EventsStorage.Companion,18,26,0,0,2,6,0,1,0,1 +app,com.github.quarck.calnotify.eventsstorage,LegacyEventsStorage,866,0,64,0,92,0,56,0,23,0 +app,com.github.quarck.calnotify.eventsstorage,EventsDatabase.Companion,258,81,16,4,59,23,11,4,2,3 +app,com.github.quarck.calnotify.eventsstorage,EventsStorageInterface.DefaultImpls,64,70,0,0,11,12,1,1,1,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},19,3,0,0,4,1,2,1,2,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.new EntityDeleteOrUpdateAdapter() {...},425,3,56,0,118,1,30,1,2,1 +app,com.github.quarck.calnotify.eventsstorage,EventAlertDao_Impl.new EntityInsertAdapter() {...},415,3,56,0,116,1,30,1,2,1 +app,com.github.quarck.calnotify.eventsstorage,RoomEventsStorage,1048,31,100,0,151,5,75,3,25,3 +app,com.github.quarck.calnotify.utils,RGB,9,47,0,0,0,3,3,3,3,3 +app,com.github.quarck.calnotify.utils,AsyncUtilsKt,15,5,0,0,2,2,1,2,1,2 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.LongProperty,22,48,2,2,0,8,5,3,3,3 +app,com.github.quarck.calnotify.utils,CNPlusSystemClock,6,13,0,0,3,3,2,2,2,2 +app,com.github.quarck.calnotify.utils,PersistentStorageBase,79,141,3,3,12,27,9,11,7,10 +app,com.github.quarck.calnotify.utils,DateTimeUtils,26,198,4,6,8,32,5,16,1,15 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.StringProperty,22,54,2,2,0,8,5,3,3,3 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.FloatProperty,70,0,4,0,8,0,8,0,6,0 +app,com.github.quarck.calnotify.utils,ViewUtilsKt,49,32,8,2,2,4,7,4,2,4 +app,com.github.quarck.calnotify.utils,ColorUtilsKt,195,155,13,11,26,20,10,6,1,3 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.BooleanProperty,42,28,3,1,3,5,6,2,4,2 +app,com.github.quarck.calnotify.utils,DateTimeUtilsKt,106,8,0,0,16,2,14,1,14,1 +app,com.github.quarck.calnotify.utils,AsyncOperation,27,42,2,4,3,10,5,6,4,4 +app,com.github.quarck.calnotify.utils,SystemUtilsKt,106,203,3,3,16,33,10,9,8,8 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.StringSetProperty,76,0,4,0,8,0,8,0,6,0 +app,com.github.quarck.calnotify.utils,StringUtilsKt,19,13,0,0,8,4,1,1,1,1 +app,com.github.quarck.calnotify.utils,PersistentStorageBase.IntProperty,42,28,3,1,3,5,6,2,4,2 +app,com.github.quarck.calnotify,PersistentStateKt,0,8,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify,GlobalStateKt,2,12,1,1,1,3,1,1,0,1 +app,com.github.quarck.calnotify,Consts,181,0,0,0,8,0,4,0,4,0 +app,com.github.quarck.calnotify,GlobalState.reactNativeHost.new DefaultReactNativeHost() {...},28,14,0,0,3,3,5,1,5,1 +app,com.github.quarck.calnotify,NotificationSettings,127,0,0,0,22,0,16,0,16,0 +app,com.github.quarck.calnotify,PersistentState,26,104,0,0,0,4,3,5,3,5 +app,com.github.quarck.calnotify,VibrationSettings,77,0,10,0,13,0,11,0,6,0 +app,com.github.quarck.calnotify,PebbleSettings,36,0,0,0,6,0,6,0,6,0 +app,com.github.quarck.calnotify,Settings,510,312,40,12,98,57,71,36,45,36 +app,com.github.quarck.calnotify,GlobalState,32,45,1,1,6,14,5,5,4,5 +app,com.github.quarck.calnotify,LedSettings,89,0,12,0,14,0,13,0,7,0 +app,com.github.quarck.calnotify,NotificationBehaviorSettings,12,15,0,0,0,5,4,1,4,1 +app,com.github.quarck.calnotify.calendareditor,EventChangeStatus,50,0,0,0,6,0,5,0,5,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeManager,715,0,36,0,141,0,28,0,10,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangePersistentState,61,0,0,0,2,0,5,0,5,0 +app,com.github.quarck.calnotify.calendareditor,EventChangeRequestType.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendareditor,EventChangeStatus.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeRequestMonitor.ValidationResultCommand,33,0,0,0,5,0,2,0,2,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeRequestMonitor,1166,0,103,0,261,0,66,0,12,0 +app,com.github.quarck.calnotify.calendareditor,EventChangeRequestType,50,0,0,0,6,0,5,0,5,0 +app,com.github.quarck.calnotify.calendareditor,CalendarChangeRequest,180,0,2,0,17,0,23,0,22,0 +app,com.github.quarck.calnotify.logs,DevLog,0,40,0,0,0,4,0,4,0,4 +app,com.github.quarck.calnotify.logs,LogcatProvider,110,0,6,0,16,0,4,0,1,0 +app,com.github.quarck.calnotify.notification,EventNotificationManager.Companion,0,33,0,10,0,11,0,6,0,1 +app,com.github.quarck.calnotify.notification,EventNotificationManager.postEverythingCollapsed..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.notification,DisplayToast,34,0,0,0,3,0,4,0,4,0 +app,com.github.quarck.calnotify.notification,NotificationContext,9,132,0,26,0,23,3,21,3,8 +app,com.github.quarck.calnotify.notification,EventNotificationManager.postNumNotificationsCollapsed..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.notification,NotificationContext.Companion,31,341,10,84,7,61,10,54,2,15 +app,com.github.quarck.calnotify.notification,NotificationActionMuteToggleService,78,0,10,0,14,0,7,0,2,0 +app,com.github.quarck.calnotify.notification,NotificationActionSnoozeService,258,0,18,0,38,0,14,0,5,0 +app,com.github.quarck.calnotify.notification,ChannelCategory,2,54,0,5,1,12,1,6,1,2 +app,com.github.quarck.calnotify.notification,NotificationActionDismissService,108,0,10,0,27,0,7,0,2,0 +app,com.github.quarck.calnotify.notification,EventNotificationManagerInterface.DefaultImpls,9,8,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.notification,EventNotificationManager,3214,64,330,1,755,10,208,5,42,5 +app,com.github.quarck.calnotify.notification,EventNotificationManager.NotificationMode,2,25,0,0,1,3,1,1,1,1 +app,com.github.quarck.calnotify.notification,NotificationChannels,27,318,7,17,4,86,5,11,0,4 +app,com.github.quarck.calnotify.notification,EventNotificationManager.arrangeEvents..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.broadcastreceivers,RemoteCommandBroadcastReceiver,6,3,4,0,3,1,3,1,1,1 +app,com.github.quarck.calnotify.broadcastreceivers,SnoozeExactAlarmBroadcastReceiver,0,9,0,2,0,4,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderAlarmGenericBroadcastReceiver.Companion,12,37,0,0,0,13,6,7,6,7 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventAlarmBroadcastReceiver,0,3,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventExactAlarmBroadcastReceiver,0,3,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.broadcastreceivers,BootCompleteBroadcastReceiver,0,14,0,2,0,5,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,RescheduleConfirmationsBroadcastReceiver,0,30,0,6,0,9,0,5,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventAlarmPeriodicRescanBroadcastReceiver,0,14,1,3,0,5,1,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderAlarmBroadcastReceiver,0,8,0,0,0,2,0,2,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,CalendarChangedBroadcastReceiver,12,3,2,0,5,1,2,1,1,1 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderAlarmGenericBroadcastReceiver,53,382,18,44,1,86,19,23,1,10 +app,com.github.quarck.calnotify.broadcastreceivers,EventReminderBroadcastReceiver,24,3,4,0,6,1,3,1,1,1 +app,com.github.quarck.calnotify.broadcastreceivers,ReminderExactAlarmBroadcastReceiver,5,3,0,0,1,1,1,1,1,1 +app,com.github.quarck.calnotify.broadcastreceivers,SnoozeAlarmBroadcastReceiver,0,9,0,2,0,4,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,TimeSetBroadcastReceiver,0,14,0,2,0,5,0,3,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,ManualEventAlarmGenericBroadcastReceiver,0,14,0,4,0,5,0,4,0,2 +app,com.github.quarck.calnotify.broadcastreceivers,AppUpdatedBroadcastReceiver,0,14,0,2,0,5,0,3,0,2 +app,com.github.quarck.calnotify.pebble,PebbleUtils,124,0,2,0,23,0,11,0,10,0 +app,com.github.quarck.calnotify.bluetooth,BTCarModeStorageKt,0,8,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.bluetooth,BTDeviceSummary,27,0,0,0,1,0,4,0,4,0 +app,com.github.quarck.calnotify.bluetooth,BTDeviceManager,153,90,21,1,19,14,17,9,6,9 +app,com.github.quarck.calnotify.bluetooth,BTCarModeStorage,36,95,0,0,2,4,3,5,3,5 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorService.Companion,83,0,0,0,20,0,2,0,2,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorInterface.DefaultImpls,28,0,0,0,5,0,1,0,1,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual.manualFireEventsAt_NoHousekeeping..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual.scanNextEvent..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual.scanNextEvent..inlined.sortedBy.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorState,163,0,0,0,5,0,10,0,10,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitor,1111,0,60,0,230,0,55,0,25,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorService,182,0,12,0,52,0,13,0,7,0 +app,com.github.quarck.calnotify.calendarmonitor,CalendarMonitorManual,1109,0,82,0,193,0,59,0,18,0 +app,com.github.quarck.calnotify.app,ApplicationController,1795,2463,152,164,281,385,151,108,46,55 +app,com.github.quarck.calnotify.app,TagsManager,1,83,1,9,0,14,1,8,0,4 +app,com.github.quarck.calnotify.app,SnoozeResult,6,18,0,0,0,1,2,2,2,2 +app,com.github.quarck.calnotify.app,AlarmScheduler,0,268,3,27,0,62,3,16,0,4 +app,com.github.quarck.calnotify.app,CalendarReloadManager.ReloadCalendarResult,0,62,0,0,0,7,0,7,0,7 +app,com.github.quarck.calnotify.app,AlarmScheduler.Companion,12,37,4,4,0,7,4,7,0,7 +app,com.github.quarck.calnotify.app,UndoState,30,0,0,0,1,0,4,0,4,0 +app,com.github.quarck.calnotify.app,SnoozeResultKt,104,0,8,0,17,0,5,0,1,0 +app,com.github.quarck.calnotify.app,CalendarReloadManager,356,384,41,27,64,72,28,16,1,8 +app,com.github.quarck.calnotify.app,ApplicationControllerInterface.DefaultImpls,56,35,0,0,8,4,4,2,4,2 +app,com.github.quarck.calnotify.app,CalendarReloadManager.ReloadCalendarResultCode,2,31,0,0,1,4,1,1,1,1 +app,com.github.quarck.calnotify.app,SnoozeType,2,19,0,0,1,2,1,1,1,1 +app,com.github.quarck.calnotify.app,UndoManager,56,37,8,2,10,8,9,2,4,2 +app,com.github.quarck.calnotify.backup,ImportResult.VersionTooNew,0,16,0,0,0,1,0,3,0,3 +app,com.github.quarck.calnotify.backup,SettingsBackupManager.Companion,0,15,0,0,0,3,0,1,0,1 +app,com.github.quarck.calnotify.backup,BackupData.Companion,0,3,0,0,0,1,0,1,0,1 +app,com.github.quarck.calnotify.backup,ImportResult.Success,0,13,0,0,0,1,0,2,0,2 +app,com.github.quarck.calnotify.backup,ImportResult.IoError,13,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.backup,ImportResult.ParseError,3,10,0,0,0,1,1,1,1,1 +app,com.github.quarck.calnotify.backup,BackupData,8,114,0,0,0,10,2,8,2,8 +app,com.github.quarck.calnotify.backup,CalendarSettingBackup.Companion,3,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.backup,ImportStats,33,36,0,0,1,6,1,6,1,6 +app,com.github.quarck.calnotify.backup,SettingsBackupManager,244,727,28,56,46,143,21,34,0,13 +app,com.github.quarck.calnotify.backup,CalendarSettingBackup,39,15,0,0,2,5,2,5,2,5 +app,com.github.quarck.calnotify.database,SQLiteOpenHelper,139,0,6,0,22,0,10,0,7,0 +app,com.github.quarck.calnotify.database,CrSqliteRoomFactory.crSqliteOptions.new RequerySQLiteOpenHelperFactory.ConfigurationOptions() {...},0,22,0,0,0,5,0,2,0,2 +app,com.github.quarck.calnotify.database,CrSqliteRoomFactory,0,34,0,0,0,5,0,2,0,2 +app,com.github.quarck.calnotify.database,SQLiteDatabaseExtensions,156,78,7,7,23,18,8,4,3,2 +app,com.github.quarck.calnotify.database,CrSqliteFinalizeWrapper,44,9,0,0,8,2,1,1,1,1 +app,com.github.quarck.calnotify.quiethours,QuietHoursManagerInterface.DefaultImpls,15,10,0,0,2,1,2,1,2,1 +app,com.github.quarck.calnotify.quiethours,QuietHoursManager,198,333,36,26,41,51,28,19,3,13 +app,com.github.quarck.calnotify.calendareditor.storage,CalendarChangeRequestsStorage,268,0,4,0,23,0,15,0,13,0 +app,com.github.quarck.calnotify.calendareditor.storage,CalendarChangeRequestsStorageImplV3,941,0,14,0,197,0,20,0,13,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventAlertRecord,0,27,0,0,0,4,0,4,0,4 +app,com.github.quarck.calnotify.dismissedeventsstorage,RoomDismissedEventsStorage.special..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase_Impl.createOpenDelegate._openDelegate.new RoomOpenDelegate() {...},514,0,2,0,54,0,9,0,8,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl.Companion,2,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorage.Companion,46,0,0,0,8,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase.Companion,336,0,20,0,82,0,15,0,5,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl,811,0,58,0,206,0,46,0,17,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl.new EntityInsertAdapter() {...},400,0,54,0,113,0,30,0,3,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventDao_Impl.new EntityDeleteOrUpdateAdapter() {...},22,0,0,0,5,0,3,0,3,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,RoomDismissedEventsStorage,157,0,0,0,26,0,12,0,12,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,LegacyDismissedEventsStorage,430,0,12,0,47,0,22,0,16,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventEntity.Companion,180,0,8,0,42,0,6,0,2,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase.Companion.LoggingCallback,29,0,0,0,7,0,3,0,3,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase,3,0,0,0,2,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissType.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventEntity,560,0,38,0,54,0,51,0,32,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorage,64,0,4,0,7,0,7,0,5,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissResult.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorageImplV1,558,0,12,0,121,0,18,0,12,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsStorageImplV2,623,0,12,0,133,0,18,0,12,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissResult,9,76,0,0,2,9,3,2,3,2 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsDatabase_Impl,96,0,0,0,19,0,9,0,9,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,DismissedEventsMigrationException,18,0,0,0,1,0,2,0,2,0 +app,com.github.quarck.calnotify.dismissedeventsstorage,EventDismissType,22,57,6,0,3,7,7,3,4,3 +app,com.github.quarck.calnotify.dismissedeventsstorage,LegacyDismissedEventsStorage._get_eventsForDisplay_.lambda.14..inlined.sortedByDescending.new Comparator() {...},15,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,CalendarRecord,6,87,0,0,0,13,2,11,2,11 +app,com.github.quarck.calnotify.calendar,CalendarEventDetails,18,138,0,0,0,15,4,12,4,12 +app,com.github.quarck.calnotify.calendar,EventStatus,9,48,0,0,2,5,3,2,3,2 +app,com.github.quarck.calnotify.calendar,CalendarEventDetails.Companion,21,0,0,0,9,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,EventRecord,38,96,0,0,6,16,7,16,7,16 +app,com.github.quarck.calnotify.calendar,RecurrenceExpander,74,294,24,25,12,62,22,10,2,4 +app,com.github.quarck.calnotify.calendar,CalendarIntents,66,118,6,6,8,23,8,5,3,4 +app,com.github.quarck.calnotify.calendar,CalendarBackupInfo,3,51,0,0,0,7,1,6,1,6 +app,com.github.quarck.calnotify.calendar,CalendarProviderInterface.DefaultImpls,10,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,EventAlertRecordKey,6,9,0,0,0,1,2,1,2,1 +app,com.github.quarck.calnotify.calendar,CalendarProvider.EventEntry,27,0,0,0,5,0,5,0,5,0 +app,com.github.quarck.calnotify.calendar,EventReminderRecord.Companion,37,11,0,0,4,1,1,1,1,1 +app,com.github.quarck.calnotify.calendar,AttendanceStatus,9,62,0,0,2,7,3,2,3,2 +app,com.github.quarck.calnotify.calendar,EventStatus.Companion,29,0,8,0,7,0,5,0,1,0 +app,com.github.quarck.calnotify.calendar,EventDisplayStatus.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,EventRecordKt,164,83,25,7,20,13,17,6,4,3 +app,com.github.quarck.calnotify.calendar,EventReminderRecord,6,25,0,0,1,4,1,4,1,4 +app,com.github.quarck.calnotify.calendar,EventAlertRecordSpecialType,43,0,0,0,5,0,5,0,5,0 +app,com.github.quarck.calnotify.calendar,EventAlertRecord,117,323,4,0,2,32,19,38,17,38 +app,com.github.quarck.calnotify.calendar,EventDisplayStatus,9,41,0,0,2,4,3,2,3,2 +app,com.github.quarck.calnotify.calendar,EventOrigin,15,57,3,1,5,8,6,3,3,3 +app,com.github.quarck.calnotify.calendar,EventAlertRecordSpecialType.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,AttendanceStatus.Companion,45,0,12,0,9,0,7,0,1,0 +app,com.github.quarck.calnotify.calendar,CalendarProvider,4357,138,423,3,986,38,242,4,27,4 +app,com.github.quarck.calnotify.calendar,EventOrigin.Companion,4,0,0,0,1,0,1,0,1,0 +app,com.github.quarck.calnotify.calendar,MonitorEventAlertEntryKey,21,0,0,0,4,0,4,0,4,0 +app,com.github.quarck.calnotify.calendar,EventAlertRecordKt,300,282,38,42,83,47,36,20,5,11 +app,com.github.quarck.calnotify.calendar,MonitorEventAlertEntry,85,72,12,2,10,11,13,10,7,9 diff --git a/docs/test-consolidation-plan.md b/docs/test-consolidation-plan.md new file mode 100644 index 000000000..213912380 --- /dev/null +++ b/docs/test-consolidation-plan.md @@ -0,0 +1,259 @@ +# Test Suite Consolidation Plan + +**Issue:** [#174 - Test suite getting bloated](https://github.com/williscool/CalendarNotification/issues/174) + +**Coverage Baseline:** See `docs/coverage-baseline/BASELINE-SUMMARY.md` + +## Current State + +| Category | Files | Lines of Code | +|----------|-------|---------------| +| Unit Tests (Robolectric) | ~50 | ~18,582 | +| Integration Tests (Instrumented) | ~45 | ~19,743 | +| **Total** | ~95 | **~38,325** | + +## Analysis Summary + +### Coverage Baseline (Pre-Consolidation) + +| Metric | Unit Tests | Integration Tests | +|--------|------------|-------------------| +| Instruction Coverage | 25.97% | 43.79% | +| Branch Coverage | 21.80% | 31.07% | +| Line Coverage | 24.84% | 45.34% | +| Method Coverage | 34.30% | 46.07% | + +### Key Findings + +1. **Intentional Duplication**: Robolectric + Instrumented pairs are **intentional** - Robolectric provides quick PR-blocking sanity checks, while Instrumentation provides real Android verification for releases +2. **Deprecated Tests**: ~5,000 lines in `deprecated_raw_calendarmonitor/` - these are actually **valuable** for their clarity and test core functionality; keep until fixtures are refactored to be equally clear +3. **Testing Overly Simple Logic**: Some tests verify trivial functionality that could be simplified ✅ +4. **Large Monolithic Tests**: Several test files exceed 700+ lines and could be split or simplified ✅ + +### Important Architecture Notes + +- **Robolectric tests block PRs** - fast sanity checks +- **Instrumentation tests block releases** - real Android verification +- **SQLite/Room testing MUST use instrumentation** - Robolectric's SQLite support is rudimentary and we use custom extensions +- **Storage is critical** - if storage breaks, nothing works + +--- + +## Revised Consolidation Plan + +### ~~Phase 1: Remove Deprecated Tests~~ → DEFERRED + +**Status:** ON HOLD + +The `deprecated_raw_calendarmonitor/` tests (~5,000 lines) are actually valuable because: +- They're straightforward to follow (easier than fixtures) +- They test core calendar monitoring functionality +- If this breaks, nothing else works + +**Prerequisite:** Refactor fixtures to be equally clear and easy to follow before considering removal. Possibly investigate fixture frameworks that could help. + +--- + +### Phase 1 (NEW): Simplify Overly Simple Tests + +Focus on tests that verify trivial logic or have excessive boilerplate: + +| Area | Opportunity | +|------|-------------| +| Simple getter/setter tests | Remove or consolidate | +| Excessive setup for simple assertions | Reduce boilerplate | +| Duplicate assertions across test methods | Use parameterized tests | + +**Approach:** Review tests for low-value assertions that don't catch real bugs. + +--- + +### Phase 2: Refactor Large Monolithic Tests + +#### Tests to Refactor + +| File | Lines | Opportunity | +|------|-------|-------------| +| `EventNotificationManagerRobolectricTest.kt` | 1,480 | Extract helper methods, reduce setup boilerplate | +| `NotificationContextInvariantTest.kt` | 999 | Review if all cases are necessary | +| `EventFormatterRobolectricTest.kt` | 956 | Consider parameterized tests | +| `MainActivityModernTest.kt` | 873 | Split into focused feature tests | +| `SettingsBackupManagerRobolectricTest.kt` | 760 | Use shared fixtures | +| `EventDismissTest.kt` | 735 | Consolidate similar test cases | + +**Goal:** Make tests more readable and maintainable without removing coverage. + +--- + +### Phase 3: Improve Test Fixtures (Prerequisite for Deprecated Test Removal) + +Current fixtures are powerful but hard to follow. Options to explore: + +1. **Add documentation** to existing fixtures explaining the flow +2. **Investigate fixture frameworks** (e.g., test containers, better base classes) +3. **Create "example" tests** that show how to use fixtures clearly +4. **Simplify fixture APIs** where possible + +Once fixtures are as clear as the deprecated tests, we can revisit Phase 1. + +--- + +### ~~Phase 2: Consolidate Duplicate Test Pairs~~ → NOT RECOMMENDED + +**Status:** KEEP AS-IS + +The Robolectric + Instrumentation duplication is **intentional**: +- Different purposes (PR blocking vs release blocking) +- Different capabilities (Robolectric can't test real SQLite) +- Defense in depth for critical functionality + +**Exception:** If specific test pairs are truly identical with no value difference, those can be consolidated case-by-case. + +--- + +## Implementation Order + +### Batch 1: Low-Hanging Fruit (Safe Simplifications) +1. Review and remove overly simple tests that don't catch real bugs +2. Consolidate duplicate assertions using parameterized tests +3. Extract common setup code into helper methods + +### Batch 2: Large Test Refactoring +4. Refactor `EventNotificationManagerRobolectricTest.kt` (1,480 lines) +5. Review `NotificationContextInvariantTest.kt` (999 lines) for necessary cases +6. Apply parameterized tests to `EventFormatterRobolectricTest.kt` (956 lines) + +### Batch 3: Fixture Improvements (Prerequisite for Future Work) +7. Document existing fixtures with clear explanations +8. Research fixture frameworks that could help +9. Create example tests showing proper fixture usage + +### Batch 4: Future Consideration (After Fixtures Improved) +10. Revisit deprecated tests once fixtures are equally clear +11. Case-by-case review of any truly redundant test pairs + +--- + +## Success Criteria + +1. **No Coverage Regression**: Coverage percentages should remain at or above baseline +2. **Improved Readability**: Tests should be easier to understand and maintain +3. **Faster Feedback**: Reduce test execution time where possible without losing coverage +4. **Preserve Defense in Depth**: Keep intentional Robolectric + Instrumentation separation + +--- + +## Revised Estimated Reduction + +| Phase | Lines Removed | +|-------|---------------| +| Phase 1: Simplify overly simple tests | ~500-1,000 | +| Phase 2: Refactor large tests | ~500-1,500 | +| Phase 3: Fixture improvements | ~0 (readability focus) | +| **Conservative Total** | **~1,000-2,500 lines (3-7% reduction)** | + +**Note:** This is a more conservative estimate that respects the intentional architecture choices. The focus shifts from "delete code" to "improve code quality." + +--- + +## Key Decisions + +### Keep (Intentional Design) +- ✅ Robolectric + Instrumentation pairs (different purposes) +- ✅ Deprecated calendar monitor tests (clear, test core functionality) +- ✅ All storage/SQLite instrumentation tests (Robolectric can't handle custom SQLite) + +### Remove/Simplify +- ❌ Overly simple tests that don't catch bugs +- ❌ Excessive boilerplate in test setup +- ❌ Duplicate assertions that could be parameterized + +### Improve +- 📝 Fixture documentation and clarity +- 📝 Large test file organization +- 📝 Helper method extraction + +--- + +## Quick Wins Implemented + +The following improvements have been implemented: + +### 1. Truth Assertions Library +Added `com.google.truth:truth:1.4.2` for better assertion failure messages. + +```kotlin +// Instead of: +assertEquals(expected, actual) +assertTrue(list.contains(item)) + +// Use: +assertThat(actual).isEqualTo(expected) +assertThat(list).contains(item) +``` + +### 2. Object Mother Pattern +Created centralized test data factories in `testutils/`: +- `EventMother.kt` - Factory for `EventAlertRecord` test data +- `MonitorAlertMother.kt` - Factory for `MonitorEventAlertEntry` test data + +```kotlin +// Usage: +val event = EventMother.createDefault() +val mutedEvent = EventMother.createMuted() +val snoozedEvent = EventMother.createSnoozed(until = tomorrow) +``` + +### 3. Robolectric PAUSED Looper Mode +Created `robolectric.properties` with explicit configuration: +- `looperMode=PAUSED` for realistic async testing +- `sdk=34` for Android 14 behavior + +### 4. ShadowAlarmManager Helper +Created `AlarmManagerTestHelper.kt` for cleaner alarm testing: + +```kotlin +val helper = AlarmManagerTestHelper() +val nextAlarm = helper.getNextScheduledAlarm() +helper.assertExactAlarmScheduledAt(expectedTime) +``` + +--- + +## Future Improvements (Potential) + +Based on Android testing best practices research (see `docs/android-testing-best-practices.md`): + +### Medium Effort + +| Improvement | Description | Benefit | +|-------------|-------------|---------| +| **Robot Pattern** | Encapsulate UI interactions in screen-specific classes | More readable UI tests | +| **Test Data Builders** | Fluent builders for complex test objects | Flexible test data creation | +| **Parameterized Tests** | JUnit 4 `@Parameterized` for repetitive tests | Reduce test duplication | + +### Investigate + +| Improvement | Description | When to Consider | +|-------------|-------------|------------------| +| **Gradle Managed Devices** | Reproducible emulator configs in CI | If CI flakiness increases | +| **Test Orchestrator** | Run each test in isolated process | If cross-test pollution occurs | +| **Kaspresso** | Alternative UI testing framework | If Ultron becomes limiting | +| **Turbine** | Flow/Coroutine testing | If adopting more Kotlin Flows | + +### Not Recommended + +| Improvement | Reason | +|-------------|--------| +| **Hilt Testing** | Requires architectural changes to adopt Hilt | +| **JUnit 5** | Limited Android support | +| **SQLite in Robolectric** | Custom extensions require real database | + +--- + +## Notes + +- Always verify coverage after each change +- Run full test suite before and after changes +- Instrumentation test coverage is particularly important (blocks releases) +- When in doubt, keep the test - false confidence is worse than bloat