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:
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
}
}