commit 78745436ad492341c09e276971ce6f127b0aa5d2
parent d0cab7a6ae631441e0b887aece983b4069e4127e
Author: MTRNord <MTRNord@users.noreply.github.com>
Date: Sat, 2 Aug 2025 18:03:16 +0200
Update gradle and performance optimizing
Diffstat:
3 files changed, 131 insertions(+), 47 deletions(-)
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt
@@ -14,11 +14,13 @@ import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import space.midnightthoughts.nordiccalendar.notifications.NotificationReceiver
import space.midnightthoughts.nordiccalendar.util.Calendar
import space.midnightthoughts.nordiccalendar.util.CalendarData
@@ -35,7 +37,7 @@ import javax.inject.Singleton
* @constructor Injects the application context for accessing system services and content providers.
*/
@Singleton
-class CalendarRepository @Inject constructor(@ApplicationContext context: Context) {
+class CalendarRepository @Inject constructor(@param:ApplicationContext private val context: Context) {
/**
* Holds the calendar data utility instance.
*/
@@ -185,6 +187,26 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex
)
/**
+ * Asynchronous version of getEventsForCalendars that returns a Flow.
+ * This prevents blocking the main thread during database operations.
+ */
+ fun getEventsForCalendarsAsync(
+ calendarIds: List<Long>,
+ startMillis: Long,
+ endMillis: Long
+ ): Flow<List<Event>> = kotlinx.coroutines.flow.flow {
+ val events = withContext(Dispatchers.IO) {
+ calendarData.getEventsForCalendars(
+ context.contentResolver, // Fixed: Use injected context
+ calendarIds,
+ startMillis,
+ endMillis
+ )
+ }
+ emit(events)
+ }
+
+ /**
* Retrieves an event by its ID.
* @param context The application context.
* @param eventId The ID of the event to retrieve.
@@ -334,41 +356,93 @@ class CalendarRepository @Inject constructor(@ApplicationContext context: Contex
)
}
} catch (e: SecurityException) {
- e.printStackTrace()
+ Log.e(
+ "CalendarRepository",
+ "SecurityException while scheduling alarm for event ${event.eventId}: ${e.message}",
+ e
+ )
+ } catch (e: Exception) {
+ Log.e(
+ "CalendarRepository",
+ "Unexpected error while scheduling alarm for event ${event.eventId}: ${e.message}",
+ e
+ )
}
}
}
}
/**
+ * Asynchronous version of scheduleRemindersForEvents.
+ */
+ suspend fun scheduleRemindersForEventsAsync(events: List<Event>) {
+ withContext(Dispatchers.IO) {
+ scheduleRemindersForEvents(context, events)
+ }
+ }
+
+ /**
* Registers a ContentObserver to listen for calendar provider changes.
+ * Uses a background thread for better performance.
* @param context The application context.
*/
fun registerCalendarContentObserver(context: Context) {
if (calendarContentObserver != null) return // Only register once
- val handler = Handler(Looper.getMainLooper())
- calendarContentObserver = object : ContentObserver(handler) {
+
+ // Use IO thread instead of main thread for ContentObserver
+ val backgroundHandler = Handler(Looper.getMainLooper().let {
+ val backgroundThread = android.os.HandlerThread("CalendarObserver").apply { start() }
+ backgroundThread.looper
+ })
+
+ calendarContentObserver = object : ContentObserver(backgroundHandler) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
- refreshEvents(context)
+ Log.d("CalendarRepository", "Calendar data changed, refreshing events")
+ // Use coroutine scope to avoid blocking the observer thread
+ repoScope.launch {
+ try {
+ refreshEvents(context)
+ } catch (e: Exception) {
+ Log.e(
+ "CalendarRepository",
+ "Error refreshing events after calendar change",
+ e
+ )
+ }
+ }
}
}
+
val cr = context.contentResolver
- cr.registerContentObserver(
- CalendarContract.Events.CONTENT_URI,
- true,
- calendarContentObserver!!
- )
- cr.registerContentObserver(
- CalendarContract.Instances.CONTENT_URI,
- true,
- calendarContentObserver!!
- )
- cr.registerContentObserver(
- CalendarContract.Reminders.CONTENT_URI,
- true,
- calendarContentObserver!!
- )
+ try {
+ cr.registerContentObserver(
+ CalendarContract.Events.CONTENT_URI,
+ true,
+ calendarContentObserver!!
+ )
+ cr.registerContentObserver(
+ CalendarContract.Instances.CONTENT_URI,
+ true,
+ calendarContentObserver!!
+ )
+ cr.registerContentObserver(
+ CalendarContract.Reminders.CONTENT_URI,
+ true,
+ calendarContentObserver!!
+ )
+ Log.d("CalendarRepository", "Calendar ContentObserver registered successfully")
+ } catch (e: SecurityException) {
+ Log.e(
+ "CalendarRepository",
+ "Failed to register ContentObserver due to security exception",
+ e
+ )
+ calendarContentObserver = null
+ } catch (e: Exception) {
+ Log.e("CalendarRepository", "Failed to register ContentObserver", e)
+ calendarContentObserver = null
+ }
}
/**
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt
@@ -1,18 +1,18 @@
package space.midnightthoughts.nordiccalendar.viewmodels
-import android.content.Context
import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import space.midnightthoughts.nordiccalendar.data.CalendarRepository
@@ -25,7 +25,6 @@ import javax.inject.Inject
* and manages tab selection. This ensures each view maintains its own independent time range
* while providing a unified interface for the CalendarScreen.
*
- * @property context The application context injected by Hilt.
* @property repository The CalendarRepository for accessing calendar and event data.
* @property monthViewModel Specialized ViewModel for month view operations.
* @property weekViewModel Specialized ViewModel for week view operations.
@@ -34,7 +33,6 @@ import javax.inject.Inject
*/
@HiltViewModel
class CalendarViewModel @Inject constructor(
- @param:ApplicationContext private val context: Context,
private val repository: CalendarRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
@@ -82,29 +80,31 @@ class CalendarViewModel @Inject constructor(
}
/**
- * Centralized events flow that handles all event loading and reminder scheduling.
- * Now properly reacts to tab changes and time range updates from all ViewModels.
+ * Optimized events flow that only loads events for the currently active view.
+ * This prevents unnecessary database queries when switching tabs.
*/
+ @OptIn(ExperimentalCoroutinesApi::class)
val events: StateFlow<List<Event>> = combine(
repository.calendarsFlow,
- _selectedTab,
- combine(
- monthViewModel.startMillis,
- monthViewModel.endMillis
- ) { start, end -> start to end },
- combine(weekViewModel.startMillis, weekViewModel.endMillis) { start, end -> start to end },
- combine(dayViewModel.startMillis, dayViewModel.endMillis) { start, end -> start to end }
- ) { calendars, selectedTab, monthRange, weekRange, dayRange ->
+ _selectedTab
+ ) { calendars, selectedTab ->
val selectedIds = calendars.filter { it.selected }.map { it.id }
-
- val (start, end) = when (selectedTab) {
- 0 -> monthRange
- 1 -> weekRange
- 2 -> dayRange
- else -> monthRange
+ selectedTab to selectedIds
+ }.flatMapLatest { (selectedTab, selectedIds) ->
+ // Only observe the time range for the currently active view
+ val activeViewModel = when (selectedTab) {
+ 0 -> monthViewModel
+ 1 -> weekViewModel
+ 2 -> dayViewModel
+ else -> monthViewModel
}
- repository.getEventsForCalendars(context, selectedIds, start, end)
+ combine(
+ activeViewModel.startMillis,
+ activeViewModel.endMillis
+ ) { start, end ->
+ repository.getEventsForCalendarsAsync(selectedIds, start, end)
+ }.flatMapLatest { it }
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
@@ -128,9 +128,14 @@ class CalendarViewModel @Inject constructor(
events.collect { eventList ->
if (eventList.isNotEmpty()) {
try {
- repository.scheduleRemindersForEvents(context, eventList)
+ // Repository handles context internally now
+ repository.scheduleRemindersForEventsAsync(eventList)
} catch (e: Exception) {
- // Handle exception
+ Log.e(
+ "CalendarViewModel",
+ "Failed to schedule reminders for ${eventList.size} events",
+ e
+ )
}
}
}
@@ -173,9 +178,14 @@ class CalendarViewModel @Inject constructor(
* Delegates refresh to the current view model.
*/
fun refreshEvents() {
- _isRefreshing.value = true
- currentViewModel.refreshEvents()
- _isRefreshing.value = false
+ viewModelScope.launch {
+ _isRefreshing.value = true
+ try {
+ currentViewModel.refreshEvents()
+ } finally {
+ _isRefreshing.value = false
+ }
+ }
}
fun setTabAndDate(tabFromNav: Int, dateFromNav: String?) {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.11.1"
+agp = "8.12.0"
kotlin = "2.2.0"
coreKtx = "1.16.0"
junit = "4.13.2"