nordiccalendar

A simple Material 3 native calendar app fitting my personal requirements
git clone git://archive.git.mtrnord.blog/MTRNord/nordiccalendar.git
Log | Files | Refs | README | LICENSE

commit 39ba01bb7820a88e8810f06781be56b9b0d0145e
parent dd3898de414c04f17346fcf27f7a67a26d9efe3f
Author: MTRNord <MTRNord@users.noreply.github.com>
Date:   Sun,  3 Aug 2025 10:33:16 +0200

Fix permission issues on first usage

Diffstat:
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt | 13+++++++++++--
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt | 130++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/background/CalendarSyncWorker.kt | 70+++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mapp/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt | 37+++++++++++++++++++++++++++++--------
4 files changed, 197 insertions(+), 53 deletions(-)

diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt @@ -102,6 +102,7 @@ class MainActivity : ComponentActivity() { } catch (_: PackageManager.NameNotFoundException) { "0.1.0-exp" } + //OnboardingPrefs.resetOnboarding(this) // Reset onboarding for testing purposes val showOnboarding = OnboardingPrefs.isOnboardingNeeded(this, currentVersion) val navigateToEventId = intent.getLongExtra("navigateToEventId", -1) @@ -110,7 +111,6 @@ class MainActivity : ComponentActivity() { NordicCalendarTheme { val navController = rememberNavController() - // TODO: Recheck calendar permissions and show dialog if not granted val permissionsState = rememberMultiplePermissionsState( listOf( android.Manifest.permission.READ_CALENDAR, @@ -118,11 +118,18 @@ class MainActivity : ComponentActivity() { ), ) - if (!permissionsState.allPermissionsGranted) { + if (!permissionsState.allPermissionsGranted && !showOnboarding) { LaunchedEffect(Unit) { permissionsState.launchMultiplePermissionRequest() } } else { + // Initialize calendar services when permissions are granted + LaunchedEffect(permissionsState.allPermissionsGranted) { + if (!showOnboarding) { + (application as NordicCalendarApp).initializeCalendarServices() + } + } + LaunchedEffect(navigateToEventId) { if (navigateToEventId > 0) { navController.navigate("eventDetails/$navigateToEventId") @@ -148,6 +155,8 @@ class MainActivity : ComponentActivity() { composable(Destinations.Intro.route) { IntroScreen(navController) { // Callback nach Abschluss OnboardingPrefs.setOnboardingDone(this@MainActivity, currentVersion) + // Initialize calendar services after onboarding completion + (application as NordicCalendarApp).initializeCalendarServices() navController.navigate(Destinations.Calendar.route) { popUpTo(Destinations.Intro.route) { inclusive = true diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/NordicCalendarApp.kt @@ -3,9 +3,11 @@ package space.midnightthoughts.nordiccalendar import android.app.Application import android.app.LocaleManager import android.content.Context +import android.content.pm.PackageManager import android.os.Build import android.os.LocaleList import android.util.Log +import androidx.core.content.ContextCompat import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import androidx.work.ExistingPeriodicWorkPolicy @@ -14,6 +16,7 @@ import androidx.work.WorkManager import dagger.hilt.android.HiltAndroidApp import space.midnightthoughts.nordiccalendar.background.CalendarSyncWorker import space.midnightthoughts.nordiccalendar.data.CalendarRepository +import space.midnightthoughts.nordiccalendar.util.OnboardingPrefs import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -43,6 +46,11 @@ class NordicCalendarApp : Application(), Configuration.Provider { lateinit var calendarRepository: CalendarRepository /** + * Tracks whether calendar services have been initialized. + */ + private var calendarServicesInitialized = false + + /** * Provides the WorkManager configuration with the HiltWorkerFactory. * This is used to create workers that can inject dependencies via Hilt. */ @@ -53,31 +61,103 @@ class NordicCalendarApp : Application(), Configuration.Provider { /** * Called when the application is created. - * This method sets up the periodic calendar synchronization work and registers a content observer - * for calendar changes. + * This method only initializes calendar services if onboarding is complete and permissions are granted. */ override fun onCreate() { super.onCreate() - // WorkManager für periodische Kalender-Synchronisation einrichten - Log.d("NordicCalendarApp", "Setting up periodic calendar sync work") - val workRequest = - PeriodicWorkRequestBuilder<CalendarSyncWorker>( - 15, - TimeUnit.MINUTES + Log.d("NordicCalendarApp", "App created") + + // Only initialize calendar services if onboarding is complete and permissions are granted + val currentVersion = try { + packageManager.getPackageInfo(packageName, 0).versionName ?: "0.1.0-exp" + } catch (_: PackageManager.NameNotFoundException) { + "0.1.0-exp" + } + + val onboardingNeeded = OnboardingPrefs.isOnboardingNeeded(this, currentVersion) + val hasCalendarPermissions = hasCalendarPermissions() + + if (!onboardingNeeded && hasCalendarPermissions) { + Log.d( + "NordicCalendarApp", + "Onboarding complete and permissions granted, initializing calendar services" + ) + initializeCalendarServices() + } else { + Log.d( + "NordicCalendarApp", + "Onboarding needed: $onboardingNeeded, Has permissions: $hasCalendarPermissions - skipping calendar services initialization" + ) + } + } + + /** + * Checks if the app has calendar permissions. + * @return True if both READ_CALENDAR and WRITE_CALENDAR permissions are granted. + */ + private fun hasCalendarPermissions(): Boolean { + val readPermission = ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.READ_CALENDAR + ) == PackageManager.PERMISSION_GRANTED + + val writePermission = ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.WRITE_CALENDAR + ) == PackageManager.PERMISSION_GRANTED + + return readPermission && writePermission + } + + /** + * Initializes calendar services including WorkManager and content observer. + * This should only be called after onboarding is complete and permissions are granted. + */ + fun initializeCalendarServices() { + if (calendarServicesInitialized) { + Log.d("NordicCalendarApp", "Calendar services already initialized") + return + } + + if (!hasCalendarPermissions()) { + Log.w("NordicCalendarApp", "Cannot initialize calendar services without permissions") + return + } + + try { + // Initialize the CalendarRepository first + Log.d("NordicCalendarApp", "Initializing CalendarRepository") + calendarRepository.initialize() + + // WorkManager für periodische Kalender-Synchronisation einrichten + Log.d("NordicCalendarApp", "Setting up periodic calendar sync work") + val workRequest = + PeriodicWorkRequestBuilder<CalendarSyncWorker>( + 15, + TimeUnit.MINUTES + ) + .build() + Log.d( + "NordicCalendarApp", + "Enqueuing periodic work request for calendar sync" ) - .build() - Log.d( - "NordicCalendarApp", - "Enqueuing periodic work request for calendar sync" - ) - WorkManager.getInstance(this).enqueueUniquePeriodicWork( - "calendar_sync", - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - // CalendarContentObserver registrieren - Log.d("NordicCalendarApp", "Registering calendar content observer") - calendarRepository.registerCalendarContentObserver(this) + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + "calendar_sync", + ExistingPeriodicWorkPolicy.KEEP, + workRequest + ) + + // CalendarContentObserver registrieren + Log.d("NordicCalendarApp", "Registering calendar content observer") + calendarRepository.registerCalendarContentObserver(this) + + calendarServicesInitialized = true + Log.d("NordicCalendarApp", "Calendar services initialized successfully") + } catch (e: SecurityException) { + Log.e("NordicCalendarApp", "SecurityException while initializing calendar services", e) + } catch (e: Exception) { + Log.e("NordicCalendarApp", "Error initializing calendar services", e) + } } /** @@ -86,9 +166,11 @@ class NordicCalendarApp : Application(), Configuration.Provider { */ override fun onTerminate() { super.onTerminate() - // CalendarContentObserver abmelden, um Speicherlecks zu vermeiden - Log.d("NordicCalendarApp", "Unregistering calendar content observer") - calendarRepository.unregisterCalendarContentObserver(this) + if (calendarServicesInitialized) { + // CalendarContentObserver abmelden, um Speicherlecks zu vermeiden + Log.d("NordicCalendarApp", "Unregistering calendar content observer") + calendarRepository.unregisterCalendarContentObserver(this) + } } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/background/CalendarSyncWorker.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/background/CalendarSyncWorker.kt @@ -6,7 +6,7 @@ // CalendarSyncWorker: A Worker subclass that performs calendar synchronization tasks in the background. It fetches the current and next day's events from selected calendars and updates reminders accordingly. // // Interfaces: -// RepositoryEntryPoint: Hilt entry point interface to provide CalendarRepository dependency. +// RepositoryEntryPoint: Hilt entry point interface to provide CalendarRepository dependency // package space.midnightthoughts.nordiccalendar.background @@ -43,28 +43,60 @@ class CalendarSyncWorker @AssistedInject constructor( * * Steps: * 1. Retrieves the CalendarRepository via Hilt entry point. - * 2. Sets the time range for event retrieval (now to 24h later). - * 3. Fetches all calendars and filters for selected ones. - * 4. Retrieves events for selected calendars within the time range. - * 5. Cancels existing reminders and schedules new ones for these events. + * 2. Checks if calendar permissions are granted. + * 3. Initializes the repository if needed. + * 4. Sets the time range for event retrieval (now to 24h later). + * 5. Fetches all calendars and filters for selected ones. + * 6. Retrieves events for selected calendars within the time range. + * 7. Cancels existing reminders and schedules new ones for these events. * * @return Result of the work (success or failure) */ override fun doWork(): Result { Log.d("CalendarSyncWorker", "Starting calendar sync work") - val repo = EntryPointAccessors.fromApplication( - applicationContext, - RepositoryEntryPoint::class.java - ).calendarRepository() - val now = System.currentTimeMillis() - val tomorrow = now + 24 * 60 * 60 * 1000 - repo.setTimeRange(now, tomorrow) - val calendars = repo.getCalendars(applicationContext) - val selectedIds = calendars.filter { it.selected }.map { it.id } - val events = repo.getEventsForCalendars(applicationContext, selectedIds, now, tomorrow) - repo.cancelRemindersForEvents(applicationContext, events) - repo.scheduleRemindersForEvents(applicationContext, events) - Log.d("CalendarSyncWorker", "Calendar sync work completed, ${events.size} events processed") - return Result.success() + + // Check permissions first + val hasReadPermission = applicationContext.checkSelfPermission( + android.Manifest.permission.READ_CALENDAR + ) == android.content.pm.PackageManager.PERMISSION_GRANTED + + val hasWritePermission = applicationContext.checkSelfPermission( + android.Manifest.permission.WRITE_CALENDAR + ) == android.content.pm.PackageManager.PERMISSION_GRANTED + + if (!hasReadPermission || !hasWritePermission) { + Log.w("CalendarSyncWorker", "Calendar permissions not granted, skipping sync") + return Result.success() // Don't fail, just skip + } + + return try { + val repo = EntryPointAccessors.fromApplication( + applicationContext, + RepositoryEntryPoint::class.java + ).calendarRepository() + + // Initialize repository if needed + repo.initialize() + + val now = System.currentTimeMillis() + val tomorrow = now + 24 * 60 * 60 * 1000 + repo.setTimeRange(now, tomorrow) + val calendars = repo.getCalendars(applicationContext) + val selectedIds = calendars.filter { it.selected }.map { it.id } + val events = repo.getEventsForCalendars(applicationContext, selectedIds, now, tomorrow) + repo.cancelRemindersForEvents(applicationContext, events) + repo.scheduleRemindersForEvents(applicationContext, events) + Log.d( + "CalendarSyncWorker", + "Calendar sync work completed, ${events.size} events processed" + ) + Result.success() + } catch (e: SecurityException) { + Log.e("CalendarSyncWorker", "SecurityException during calendar sync", e) + Result.success() // Don't retry on permission issues + } catch (e: Exception) { + Log.e("CalendarSyncWorker", "Error during calendar sync", e) + Result.retry() + } } } diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/data/CalendarRepository.kt @@ -94,17 +94,38 @@ class CalendarRepository @Inject constructor(@param:ApplicationContext private v private var calendarContentObserver: ContentObserver? = null /** + * Tracks whether the repository has been initialized with calendar data. + */ + private var isInitialized = false + + /** * Initializes the repository by loading calendars and setting up event synchronization. + * This should only be called after calendar permissions are granted. */ - init { - loadCalendarsToFlow(context) - repoScope.launch { - combine(_calendarsFlow, _startMillis, _endMillis) { calendars, start, end -> - val selectedIds = calendars.filter { it.selected }.map { it.id } - getEventsForCalendars(context, selectedIds, start, end) - }.collect { events -> - _eventsFlow.value = events + fun initialize() { + if (isInitialized) { + Log.d("CalendarRepository", "Repository already initialized") + return + } + + try { + loadCalendarsToFlow(context) + repoScope.launch { + combine(_calendarsFlow, _startMillis, _endMillis) { calendars, start, end -> + val selectedIds = calendars.filter { it.selected }.map { it.id } + getEventsForCalendars(context, selectedIds, start, end) + }.collect { events -> + _eventsFlow.value = events + } } + isInitialized = true + Log.d("CalendarRepository", "Repository initialized successfully") + } catch (e: SecurityException) { + Log.e("CalendarRepository", "SecurityException during initialization", e) + throw e + } catch (e: Exception) { + Log.e("CalendarRepository", "Error during repository initialization", e) + throw e } }