diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 25b93d5..fc20dc1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -177,6 +177,7 @@ dependencies { implementation("de.jeanlucmakiola.floret:core-crash") implementation("de.jeanlucmakiola.floret:core-di") implementation("de.jeanlucmakiola.floret:core-reminders") + implementation("de.jeanlucmakiola.floret:core-android") implementation("de.jeanlucmakiola.floret:identity") implementation("de.jeanlucmakiola.floret:components") implementation(libs.kotlinx.coroutines.core) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt index 64f5091..8c504d9 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt @@ -5,12 +5,10 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context -import android.content.pm.PackageManager import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy @@ -28,6 +26,7 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.ics.IcsExporter import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.domain.ics.IcsWriter +import de.jeanlucmakiola.floret.android.hasPermission import kotlinx.coroutines.flow.first import java.util.concurrent.TimeUnit import kotlin.time.Clock @@ -133,8 +132,7 @@ class BackupWorker( private fun notifyFailure(context: Context) { val canPost = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || - ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED + context.hasPermission(Manifest.permission.POST_NOTIFICATIONS) if (!canPost) return val manager = NotificationManagerCompat.from(context) manager.createNotificationChannel( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt index 78c2d78..d938951 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt @@ -1,12 +1,12 @@ package de.jeanlucmakiola.calendula.data.calendar +import de.jeanlucmakiola.floret.android.hasPermission import de.jeanlucmakiola.floret.time.toKotlinInstantFromEpochMillis import android.Manifest import android.content.ContentResolver import android.content.ContentUris import android.content.ContentValues import android.content.Context -import android.content.pm.PackageManager import android.database.ContentObserver import android.database.Cursor import android.net.Uri @@ -14,7 +14,6 @@ import android.os.Handler import android.os.Looper import android.provider.CalendarContract import android.util.Log -import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.domain.Attendee import de.jeanlucmakiola.calendula.domain.AttendeeRelationship @@ -326,8 +325,7 @@ class AndroidCalendarDataSource @Inject constructor( private val registeredObservers = mutableSetOf() private fun hasCalendarPermission(): Boolean = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == - PackageManager.PERMISSION_GRANTED + context.hasPermission(Manifest.permission.READ_CALENDAR) /** * Attach any not-yet-registered observers to the provider, but only once the @@ -463,8 +461,7 @@ class AndroidCalendarDataSource @Inject constructor( } override fun canWriteCalendars(): Boolean = - ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) == - PackageManager.PERMISSION_GRANTED + context.hasPermission(Manifest.permission.WRITE_CALENDAR) override fun createManagedCalendar(displayName: String, color: Int, type: SpecialDateType): Long { val name = displayName.trim() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarVisibilityReconciler.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarVisibilityReconciler.kt index 43d62b8..4d3518d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarVisibilityReconciler.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarVisibilityReconciler.kt @@ -4,7 +4,6 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.util.Log -import androidx.core.content.ContextCompat import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext @@ -12,6 +11,7 @@ import dagger.hilt.components.SingletonComponent import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs import de.jeanlucmakiola.calendula.domain.calendarVisibilityPlan import de.jeanlucmakiola.calendula.domain.hasSystemHiddenCalendars +import de.jeanlucmakiola.floret.android.hasPermission import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.first @@ -47,7 +47,7 @@ class CalendarVisibilityReconciler @Inject constructor( // Settled ahead of the permission gate, so an update installed // before the first grant can't later look like an upgrade. if (!isUpgradeInstall()) settleNoticeOnce(pending = false) - if (!hasPermission(Manifest.permission.READ_CALENDAR)) return@withContext + if (!context.hasPermission(Manifest.permission.READ_CALENDAR)) return@withContext val pending = prefs.pendingDisabledCalendarIds.first() val noticeSettled = prefs.visibilityNoticePending.first() != null if (pending.isEmpty() && noticeSettled) return@withContext @@ -56,7 +56,7 @@ class CalendarVisibilityReconciler @Inject constructor( // Both decisions below are one-way, so leave them to the next run. if (calendars.isEmpty()) return@withContext settleNoticeOnce(hasSystemHiddenCalendars(calendars, pending)) - if (pending.isEmpty() || !hasPermission(Manifest.permission.WRITE_CALENDAR)) { + if (pending.isEmpty() || !context.hasPermission(Manifest.permission.WRITE_CALENDAR)) { return@withContext } val plan = calendarVisibilityPlan(calendars, pending) @@ -97,9 +97,6 @@ class CalendarVisibilityReconciler @Inject constructor( false } - private fun hasPermission(permission: String): Boolean = - ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED - /** Lets non-injectable entry points (the Application) reach the reconciler. */ @EntryPoint @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt index f4edca0..d8c6997 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt @@ -2,16 +2,15 @@ package de.jeanlucmakiola.calendula.data.contacts import android.Manifest import android.content.Context -import android.content.pm.PackageManager import android.provider.ContactsContract import android.provider.ContactsContract.CommonDataKinds.Event import android.util.Log -import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import de.jeanlucmakiola.calendula.domain.contacts.managedUid import de.jeanlucmakiola.calendula.domain.contacts.parseContactEventDate +import de.jeanlucmakiola.floret.android.hasPermission import javax.inject.Inject import javax.inject.Singleton @@ -31,8 +30,7 @@ interface ContactSpecialDatesDataSource { /** Whether `READ_CONTACTS` is granted — the gate for every contacts read. */ fun Context.hasContactsPermission(): Boolean = - ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == - PackageManager.PERMISSION_GRANTED + hasPermission(Manifest.permission.READ_CONTACTS) /** Map a `ContactsContract` event `TYPE` to our calendar bucket. */ internal fun specialDateTypeForRawEventType(type: Int): SpecialDateType = when (type) { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index d051ab9..5b66b08 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -7,6 +7,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.reminders.ReminderOverride import de.jeanlucmakiola.floret.reminders.ReminderOverrideCodec import de.jeanlucmakiola.floret.reminders.applyReminderOverride @@ -30,10 +31,8 @@ import de.jeanlucmakiola.calendula.widget.WidgetSize import java.time.ZoneId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.DayOfWeek import java.time.temporal.WeekFields import java.util.Locale @@ -104,9 +103,8 @@ fun WeekStartPref.resolveFirstDay(locale: Locale): DayOfWeek = when (this) { fun SettingsPrefs.firstDayOfWeek(scope: CoroutineScope): StateFlow = weekStart .map { it.resolveFirstDay(Locale.getDefault()) } - .stateIn( + .stateWhileSubscribed( scope = scope, - started = SharingStarted.WhileSubscribed(5_000L), // Seed with the locale convention rather than a hardcoded weekday, so // the first frame is already right for everyone still on Auto. initialValue = WeekStartPref.Auto.resolveFirstDay(Locale.getDefault()), diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt index d62ee0d..b4d5e94 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt @@ -6,11 +6,8 @@ import android.content.Intent import androidx.core.net.toUri import dagger.hilt.android.AndroidEntryPoint import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob +import de.jeanlucmakiola.floret.android.goAsync import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -44,27 +41,17 @@ class ReminderActionReceiver : BroadcastReceiver() { // Cancel now so the notification doesn't linger until the alarm; // the snooze delay read is the only async work. notifier.cancel(alert) - val pendingResult = goAsync() - CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { - try { - val minutes = settingsPrefs.snoozeMinutes.first() - val triggerAt = System.currentTimeMillis() + minutes * 60_000L - scheduler.schedule(alert, triggerAt) - } finally { - pendingResult.finish() - } + goAsync { + val minutes = settingsPrefs.snoozeMinutes.first() + val triggerAt = System.currentTimeMillis() + minutes * 60_000L + scheduler.schedule(alert, triggerAt) } } ACTION_SHOW -> { - val pendingResult = goAsync() - CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { - try { - if (settingsPrefs.remindersEnabled.first() && notifier.canPost()) { - notifier.post(alert) - } - } finally { - pendingResult.finish() + goAsync { + if (settingsPrefs.remindersEnabled.first() && notifier.canPost()) { + notifier.post(alert) } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlarms.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlarms.kt index 1fd5a75..735d091 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlarms.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlarms.kt @@ -4,20 +4,12 @@ import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext +import de.jeanlucmakiola.floret.android.setWakeup import javax.inject.Inject import javax.inject.Singleton -/** - * True on API < 31 (no restriction), and on 31+ when the exact-alarm capability - * is held — auto-granted via `USE_EXACT_ALARM` on API 33+ (Calendula is a - * calendar app), user-revocable on 31–32. - */ -internal fun AlarmManager.canScheduleExactCompat(): Boolean = - Build.VERSION.SDK_INT < Build.VERSION_CODES.S || canScheduleExactAlarms() - /** * Holds the app's own wake-up for the next reminder (#75). Exactly one alarm * exists at a time, for the earliest reminder ahead; every firing re-scans and @@ -31,16 +23,7 @@ class ReminderAlarmScheduler @Inject constructor( fun scheduleScan(triggerAtMillis: Long) { val alarmManager = context.getSystemService() ?: return - val pendingIntent = scanPendingIntent(PendingIntent.FLAG_UPDATE_CURRENT) - if (alarmManager.canScheduleExactCompat()) { - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent, - ) - } else { - alarmManager.setAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent, - ) - } + alarmManager.setWakeup(triggerAtMillis, scanPendingIntent(PendingIntent.FLAG_UPDATE_CURRENT)) } /** Drop the pending wake-up — reminders are off, or there is nothing to wait for. */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt index 8a1009b..07a0004 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt @@ -5,12 +5,10 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context -import android.content.pm.PackageManager import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.MainActivity import de.jeanlucmakiola.calendula.R @@ -19,6 +17,7 @@ import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.is24Hour import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay +import de.jeanlucmakiola.floret.android.hasPermission import kotlinx.coroutines.flow.first import kotlinx.datetime.isoDayNumber import java.time.DayOfWeek @@ -48,8 +47,7 @@ class ReminderNotifier @Inject constructor( /** False when the user declined `POST_NOTIFICATIONS` or muted the app. */ fun canPost(): Boolean { val granted = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || - ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED + context.hasPermission(Manifest.permission.POST_NOTIFICATIONS) return granted && NotificationManagerCompat.from(context).areNotificationsEnabled() } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScanner.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScanner.kt index 518b71c..38ea963 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScanner.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScanner.kt @@ -2,9 +2,7 @@ package de.jeanlucmakiola.calendula.data.reminders import android.Manifest import android.content.Context -import android.content.pm.PackageManager import android.util.Log -import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.data.calendar.CalendarDataSource import de.jeanlucmakiola.calendula.data.calendar.withoutProviderDefaults @@ -14,6 +12,7 @@ import de.jeanlucmakiola.calendula.domain.reminders.planReminders import de.jeanlucmakiola.calendula.domain.reminders.reminderQueryHorizon import de.jeanlucmakiola.calendula.domain.reminders.reminderWatermark import de.jeanlucmakiola.calendula.domain.reminders.scheduleReminders +import de.jeanlucmakiola.floret.android.hasPermission import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -113,9 +112,7 @@ class ReminderScanner @Inject constructor( alarms.scheduleScan(schedule.nextAlarmMillis) } - private fun hasReadCalendar(): Boolean = ContextCompat.checkSelfPermission( - context, Manifest.permission.READ_CALENDAR, - ) == PackageManager.PERMISSION_GRANTED + private fun hasReadCalendar(): Boolean = context.hasPermission(Manifest.permission.READ_CALENDAR) /** * Re-scan when the provider changes, so a saved or deleted event re-arms the diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScheduleReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScheduleReceiver.kt index 0df3eab..4789ee9 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScheduleReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderScheduleReceiver.kt @@ -4,10 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import de.jeanlucmakiola.floret.android.goAsync import javax.inject.Inject /** @@ -29,14 +26,7 @@ class ReminderScheduleReceiver : BroadcastReceiver() { // exported and the broadcasts it takes are protected, so any other // action did not come from where it claims to. if (intent.action !in HANDLED_ACTIONS) return - val pendingResult = goAsync() - CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { - try { - scanner.scan() - } finally { - pendingResult.finish() - } - } + goAsync { scanner.scan() } } companion object { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderSnoozeScheduler.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderSnoozeScheduler.kt index 694f052..0c9e05c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderSnoozeScheduler.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderSnoozeScheduler.kt @@ -3,9 +3,9 @@ package de.jeanlucmakiola.calendula.data.reminders import android.app.AlarmManager import android.app.PendingIntent import android.content.Context -import android.os.Build import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext +import de.jeanlucmakiola.floret.android.setWakeup import javax.inject.Inject import javax.inject.Singleton @@ -31,24 +31,6 @@ class ReminderSnoozeScheduler @Inject constructor( ReminderActionReceiver.intent(context, ReminderActionReceiver.ACTION_SHOW, alert), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) - if (canScheduleExact(alarmManager)) { - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent, - ) - } else { - // Exact alarms revoked (API 31–32); an inexact wake is the best - // available without nagging for SCHEDULE_EXACT_ALARM. - alarmManager.setAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent, - ) - } + alarmManager.setWakeup(triggerAtMillis, pendingIntent) } - - /** - * True on API < 31 (no restriction), and on 31+ when the exact-alarm - * capability is held — auto-granted via `USE_EXACT_ALARM` on API 33+ - * (Calendula is a calendar app), user-revocable on 31–32. - */ - private fun canScheduleExact(alarmManager: AlarmManager): Boolean = - Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarmManager.canScheduleExactAlarms() } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt index 9ff1394..0653c41 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt @@ -1,18 +1,13 @@ package de.jeanlucmakiola.calendula.data.time -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext +import de.jeanlucmakiola.floret.android.broadcastTicks import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformLatest import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone @@ -34,21 +29,8 @@ class AndroidTodaySource @Inject constructor( @ApplicationContext private val context: Context, ) : TodaySource { - override val today: Flow = callbackFlow { - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - trySend(Unit) - } - } - val filter = IntentFilter().apply { - addAction(Intent.ACTION_DATE_CHANGED) - addAction(Intent.ACTION_TIME_CHANGED) - addAction(Intent.ACTION_TIMEZONE_CHANGED) - } - ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) - awaitClose { context.unregisterReceiver(receiver) } - } - .onStart { emit(Unit) } + override val today: Flow = context + .broadcastTicks(Intent.ACTION_DATE_CHANGED, Intent.ACTION_TIME_CHANGED, Intent.ACTION_TIMEZONE_CHANGED) .transformLatest { while (true) { emit(currentDate()) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/time/ZoneSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/time/ZoneSource.kt index 5505ddd..e4ca03f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/time/ZoneSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/time/ZoneSource.kt @@ -1,17 +1,12 @@ package de.jeanlucmakiola.calendula.data.time -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.channels.awaitClose +import de.jeanlucmakiola.floret.android.broadcastTicks import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import kotlinx.datetime.TimeZone import javax.inject.Inject @@ -33,17 +28,7 @@ class AndroidZoneSource @Inject constructor( @ApplicationContext private val context: Context, ) : ZoneSource { - override val zone: Flow = callbackFlow { - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - trySend(Unit) - } - } - val filter = IntentFilter(Intent.ACTION_TIMEZONE_CHANGED) - ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) - awaitClose { context.unregisterReceiver(receiver) } - } - .onStart { emit(Unit) } + override val zone: Flow = context.broadcastTicks(Intent.ACTION_TIMEZONE_CHANGED) .map { current() } .distinctUntilChanged() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt index ba9d915..cba2890 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt @@ -8,11 +8,10 @@ import de.jeanlucmakiola.calendula.data.time.TodaySource import de.jeanlucmakiola.calendula.data.time.currentDate import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS -import kotlinx.coroutines.flow.SharingStarted +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.LocalDate import javax.inject.Inject @@ -29,40 +28,35 @@ class CalendarHostViewModel @Inject constructor( ) : ViewModel() { /** Today, kept current across midnight so the shared focus can follow it (#373). */ - val today: StateFlow = todaySource.today.stateIn( + val today: StateFlow = todaySource.today.stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = currentDate(), ) - val defaultView: StateFlow = prefs.defaultView.stateIn( + val defaultView: StateFlow = prefs.defaultView.stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = null, ) /** Views the top-bar quick-switch pill cycles through, in the user's order (#24). */ val quickSwitchViews: StateFlow> = prefs.quickSwitchConfig .map { it.cycle } - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = IMPLEMENTED_VIEWS, ) /** Order of the views in the navigation drawer (#24); every view always shown. */ val drawerViewOrder: StateFlow> = prefs.drawerViewOrder - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = IMPLEMENTED_VIEWS, ) /** Whether each view's jump-to-today control lives in the top bar, not the FAB (#60). */ val todayButtonInToolbar: StateFlow = prefs.todayButtonInToolbar - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = false, ) @@ -74,9 +68,8 @@ class CalendarHostViewModel @Inject constructor( * switched off to be rid of. */ val dragToReschedule: StateFlow = prefs.dragToReschedule - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = false, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt index 62f034f..c29f0ab 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt @@ -1,7 +1,6 @@ package de.jeanlucmakiola.calendula.ui import android.Manifest -import android.content.pm.PackageManager import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -20,7 +19,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler @@ -31,6 +29,7 @@ import de.jeanlucmakiola.calendula.ui.onboarding.OnboardingSteps import de.jeanlucmakiola.calendula.ui.onboarding.OnboardingViewModel import de.jeanlucmakiola.calendula.ui.onboarding.onboardingSlideTransition import de.jeanlucmakiola.calendula.ui.onboarding.rememberOnboardingSlideSpec +import de.jeanlucmakiola.floret.android.hasPermission import de.jeanlucmakiola.floret.identity.rememberReduceMotion /** What the root is showing: a wizard step, the app, or neither yet. */ @@ -61,8 +60,7 @@ fun RootScreen( val context = LocalContext.current var hasPermission by remember { mutableStateOf( - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) - == PackageManager.PERMISSION_GRANTED + context.hasPermission(Manifest.permission.READ_CALENDAR) ) } @@ -74,9 +72,7 @@ fun RootScreen( DisposableEffect(lifecycle) { val obs = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { - hasPermission = ContextCompat.checkSelfPermission( - context, Manifest.permission.READ_CALENDAR - ) == PackageManager.PERMISSION_GRANTED + hasPermission = context.hasPermission(Manifest.permission.READ_CALENDAR) // Refresh the contact special-dates mirror on foreground (the // worker is debounced and no-ops when the feature is off). Gated // on the permission so users without the opt-in never enqueue it. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt index f90ad7f..4915f16 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt @@ -13,6 +13,7 @@ import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.FailureReason import de.jeanlucmakiola.calendula.domain.calendarListFailure +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -66,9 +67,8 @@ class AgendaViewModel @Inject constructor( * the screen combines it with a per-minute "now" to fade or drop past rows. */ val pastEventDisplay: StateFlow = settingsPrefs.pastEventDisplay - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = PastEventDisplay.SHOW, ) @@ -79,9 +79,8 @@ class AgendaViewModel @Inject constructor( * alongside the data state like [pastEventDisplay] rather than re-querying. */ val showToday: StateFlow = settingsPrefs.agendaShowToday - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = true, ) @@ -115,7 +114,7 @@ class AgendaViewModel @Inject constructor( _window.value = _window.value.copy(anchor = date) } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), todayDate) + .stateWhileSubscribed(viewModelScope, todayDate) // A transient, in-view override of the saved agenda range. Held in memory // (not persisted), so it survives view switches and rotation within the @@ -156,9 +155,8 @@ class AgendaViewModel @Inject constructor( } .catch { emit(AgendaUiState.Failure(FailureReason.ProviderUnavailable)) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = AgendaUiState.Loading, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt index ff75b37..861f452 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt @@ -15,10 +15,10 @@ import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.FailureReason import de.jeanlucmakiola.calendula.domain.ics.IcsWriter +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.cancellation.CancellationException @@ -52,9 +51,8 @@ class CalendarsViewModel @Inject constructor( repository.calendars() .catch { emit(emptyList()) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = emptyList(), ) @@ -68,9 +66,8 @@ class CalendarsViewModel @Inject constructor( .map { backupUiState(it) } .catch { emit(BackupUiState.Failure(FailureReason.ProviderUnavailable)) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = BackupUiState.Loading, ) @@ -84,9 +81,8 @@ class CalendarsViewModel @Inject constructor( AutoBackupUiState(enabled, interval, folder, status) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = AutoBackupUiState(), ) @@ -103,9 +99,8 @@ class CalendarsViewModel @Inject constructor( if (!enabled) emptySet() else sources.filter { it.isManaged }.map { it.id }.toSet() } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = emptySet(), ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActions.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActions.kt index 4527911..b9bef70 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActions.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActions.kt @@ -3,7 +3,6 @@ package de.jeanlucmakiola.calendula.ui.common import android.Manifest import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult @@ -23,7 +22,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.calendar.NoSuchEventException @@ -32,6 +30,7 @@ import de.jeanlucmakiola.calendula.domain.EventDetail import de.jeanlucmakiola.calendula.domain.RecurringWriteScope import de.jeanlucmakiola.calendula.domain.ics.IcsWriter import de.jeanlucmakiola.calendula.domain.ics.toShareIcsEvent +import de.jeanlucmakiola.floret.android.hasPermission import de.jeanlucmakiola.floret.components.OptionCard import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Clock @@ -75,8 +74,7 @@ enum class DeleteOutcome { Deleted, Gone, NeedsPermission, Failed } /** Whether this install may write to the calendar; v1.0 installs only hold read access. */ fun Context.canWriteCalendar(): Boolean = - ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) == - PackageManager.PERMISSION_GRANTED + hasPermission(Manifest.permission.WRITE_CALENDAR) /** [deleteEvent], with the provider's failures mapped to a [DeleteOutcome]. */ suspend fun CalendarRepository.tryDeleteEvent( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActionsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActionsViewModel.kt index 1aa908b..e2da8e5 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActionsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventActionsViewModel.kt @@ -14,17 +14,16 @@ import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.RecurringWriteScope import de.jeanlucmakiola.calendula.domain.atOccurrence import de.jeanlucmakiola.calendula.domain.toDuplicateForm +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.TimeZone @@ -50,9 +49,8 @@ class EventActionsViewModel @Inject constructor( .map { list -> list.associateBy { it.id } } .catch { emit(emptyMap()) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = emptyMap(), ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/PageStateCache.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/PageStateCache.kt index 8358820..5e92a83 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/PageStateCache.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/PageStateCache.kt @@ -1,10 +1,10 @@ package de.jeanlucmakiola.calendula.ui.common +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus @@ -45,7 +45,7 @@ class PageStateCache( return entries.getOrPut(key) { val job = SupervisorJob(scope.coroutineContext[Job]) Entry( - state = load(key).stateIn(scope + job, SharingStarted.WhileSubscribed(5_000L), initial), + state = load(key).stateWhileSubscribed(scope + job, initial), job = job, ) }.state diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/RescheduleViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/RescheduleViewModel.kt index 33210b5..ba1f245 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/RescheduleViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/RescheduleViewModel.kt @@ -22,16 +22,15 @@ import de.jeanlucmakiola.calendula.domain.resolvedZone import de.jeanlucmakiola.calendula.domain.shiftedByDays import de.jeanlucmakiola.calendula.domain.shiftedTo import de.jeanlucmakiola.calendula.domain.toEditForm +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate @@ -188,9 +187,8 @@ class RescheduleViewModel @Inject constructor( .map { calendars -> calendars.filter { it.allowsEventMove }.map { it.id }.toSet() } .catch { emit(emptySet()) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = emptySet(), ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt index ae38d00..fdf3247 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt @@ -16,12 +16,12 @@ import de.jeanlucmakiola.calendula.domain.toDuplicateForm import de.jeanlucmakiola.calendula.ui.common.DeleteOutcome import de.jeanlucmakiola.calendula.ui.common.tryDeleteEvent import de.jeanlucmakiola.calendula.ui.common.stageEventShare +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext 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 @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.datetime.TimeZone import kotlin.coroutines.cancellation.CancellationException @@ -76,9 +75,8 @@ class EventDetailViewModel @Inject constructor( } } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = EventDetailUiState.Loading, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt index 6cd6e3b..aee0e8d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt @@ -41,6 +41,7 @@ import de.jeanlucmakiola.calendula.ui.detail.EventDetailViewModel.Companion.NO_O import de.jeanlucmakiola.calendula.ui.common.DeleteOutcome import de.jeanlucmakiola.calendula.ui.common.tryDeleteEvent import de.jeanlucmakiola.calendula.ui.common.stageEventShare +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -338,9 +339,8 @@ class EventEditViewModel @Inject constructor( allowColorOnUnsupportedCalendars = external.allowColorOnUnsupported, ) } - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = null, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/filter/FilterViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/filter/FilterViewModel.kt index 1f8f9b7..5d615c4 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/filter/FilterViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/filter/FilterViewModel.kt @@ -9,15 +9,14 @@ import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.FailureReason import de.jeanlucmakiola.calendula.domain.calendarListFailure import de.jeanlucmakiola.calendula.ui.common.groupByAccount +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -45,9 +44,8 @@ class FilterViewModel @Inject constructor( } .catch { emit(FilterUiState.Failure(FailureReason.ProviderUnavailable)) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = FilterUiState.Loading, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt index 2e9ba15..f59c4e3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt @@ -18,11 +18,11 @@ import de.jeanlucmakiola.calendula.domain.eventDisplayComparator import de.jeanlucmakiola.calendula.ui.week.coversDay import de.jeanlucmakiola.calendula.ui.week.layoutAllDay import de.jeanlucmakiola.calendula.ui.week.spansMultipleDays +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine @@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.map import kotlin.math.abs import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate @@ -69,25 +68,22 @@ class MonthViewModel @Inject constructor( /** Whether to fade events that have already finished (display concern only). */ val dimCompletedEvents: StateFlow = settingsPrefs.dimCompletedEvents - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = false, ) /** Whether to show the calendar-week number gutter (#25; display only). */ val showWeekNumbers: StateFlow = settingsPrefs.showWeekNumbers - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = false, ) /** How the grid is laid out and navigated (#38, #53). */ val viewStyle: StateFlow = settingsPrefs.monthViewStyle - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = MonthViewStyle.Paged, ) @@ -103,7 +99,7 @@ class MonthViewModel @Inject constructor( lastToday = date if (old != date) followToday(old, date) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), todayDate) + .stateWhileSubscribed(viewModelScope, todayDate) private val _month = MutableStateFlow(YearMonth(todayDate.year, todayDate.month)) val month: StateFlow = _month @@ -124,9 +120,8 @@ class MonthViewModel @Inject constructor( month(ym, ws).filterNot { it is MonthUiState.Loading } } .catch { emit(MonthUiState.Failure(FailureReason.ProviderUnavailable)) } - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = MonthUiState.Loading, ) @@ -228,9 +223,8 @@ class MonthViewModel @Inject constructor( .onEach { if (it is ContinuousMonthUiState.Success) widenLoadedWindow() } .catch { emit(ContinuousMonthUiState.Failure(FailureReason.ProviderUnavailable)) } .flowOn(io) - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = ContinuousMonthUiState.Loading, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/onboarding/OnboardingViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/onboarding/OnboardingViewModel.kt index 7da77d7..5828660 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/onboarding/OnboardingViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/onboarding/OnboardingViewModel.kt @@ -19,6 +19,7 @@ import de.jeanlucmakiola.calendula.domain.hasVisibilitySwitch import de.jeanlucmakiola.calendula.domain.orderedForManager import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -92,9 +93,8 @@ class OnboardingViewModel @Inject constructor( list.catch { emit(null) }.flowOn(io) } } - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = null, ) @@ -183,9 +183,8 @@ class OnboardingViewModel @Inject constructor( */ val hiddenCalendars: StateFlow> = calendars .map { it.orEmpty().orderedForManager() } - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = emptyList(), ) @@ -232,9 +231,8 @@ class OnboardingViewModel @Inject constructor( doneShown = closing.doneShown, ) } - }.stateIn( + }.stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = null, ) @@ -244,9 +242,8 @@ class OnboardingViewModel @Inject constructor( prefs.monthViewStyle, prefs.weekStart, ::ViewChoice, - ).stateIn( + ).stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = null, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt index c4777e3..3caa4c0 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt @@ -1,7 +1,6 @@ package de.jeanlucmakiola.calendula.ui.search import android.Manifest -import android.content.pm.PackageManager import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -72,7 +71,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.calendula.R @@ -82,6 +80,7 @@ import de.jeanlucmakiola.calendula.domain.RecurringWriteScope import de.jeanlucmakiola.calendula.domain.SearchHit import de.jeanlucmakiola.calendula.domain.SearchMonth import de.jeanlucmakiola.calendula.domain.isDeclined +import de.jeanlucmakiola.floret.android.hasPermission import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme import de.jeanlucmakiola.floret.identity.animateItemMotion import de.jeanlucmakiola.floret.identity.fadeThrough @@ -166,10 +165,7 @@ fun SearchScreen( pendingWrite = null } val requireWrite: (() -> Unit) -> Unit = { action -> - val granted = ContextCompat.checkSelfPermission( - context, - Manifest.permission.WRITE_CALENDAR, - ) == PackageManager.PERMISSION_GRANTED + val granted = context.hasPermission(Manifest.permission.WRITE_CALENDAR) if (granted) { action() } else { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchViewModel.kt index 56486cf..76d697f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchViewModel.kt @@ -8,13 +8,13 @@ import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.EventSearch import de.jeanlucmakiola.calendula.domain.RecurringWriteScope import de.jeanlucmakiola.calendula.domain.SearchResults +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.TimeZone @@ -120,7 +119,7 @@ class SearchViewModel @Inject constructor( } .catch { emit(SearchUiState.Idle) } .flowOn(io) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), SearchUiState.Idle) + .stateWhileSubscribed(viewModelScope, SearchUiState.Idle) fun setQuery(value: String) { if (value != _query.value) _selection.value = emptySet() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/NotificationSettings.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/NotificationSettings.kt index 486715e..bea57ba 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/NotificationSettings.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/NotificationSettings.kt @@ -3,7 +3,6 @@ package de.jeanlucmakiola.calendula.ui.settings import android.Manifest import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.os.Build import android.os.PowerManager import android.provider.Settings @@ -27,7 +26,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -39,6 +37,7 @@ import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS import de.jeanlucmakiola.calendula.ui.common.ReminderDefaultPicker import de.jeanlucmakiola.calendula.ui.common.TimePickerAlert import de.jeanlucmakiola.calendula.ui.common.durationLabel +import de.jeanlucmakiola.floret.android.hasPermission import de.jeanlucmakiola.floret.components.CollapsingScaffold import de.jeanlucmakiola.floret.components.ExpandIcon import de.jeanlucmakiola.floret.components.GroupedRow @@ -73,9 +72,7 @@ internal fun NotificationsScreen( viewModel.setRemindersEnabled(enabled) val needsPermission = enabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - ContextCompat.checkSelfPermission( - context, Manifest.permission.POST_NOTIFICATIONS, - ) != PackageManager.PERMISSION_GRANTED + !context.hasPermission(Manifest.permission.POST_NOTIFICATIONS) if (needsPermission) { launcher.launch(Manifest.permission.POST_NOTIFICATIONS) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 2f396a9..e4c47f3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -18,6 +18,7 @@ import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncEngine import de.jeanlucmakiola.calendula.data.contacts.resolveTitleTemplate import de.jeanlucmakiola.calendula.data.fonts.CustomFontStore +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import de.jeanlucmakiola.floret.reminders.ReminderOverride import de.jeanlucmakiola.floret.reminders.reminderOverrideFor @@ -54,12 +55,10 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -230,9 +229,8 @@ class SettingsViewModel @Inject constructor( defaultEventDurationMinutes = overrides.durations.default, perCalendarEventDuration = overrides.durations.perCalendar, ) - }.stateIn( + }.stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = SettingsUiState(dynamicColorAvailable = dynamicColorAvailable), ) @@ -248,9 +246,8 @@ class SettingsViewModel @Inject constructor( prefs.plainFontStamp, ) { brand, plain, brandStamp, plainStamp -> AppFontSettings(brand = brand, plain = plain, brandStamp = brandStamp, plainStamp = plainStamp) - }.stateIn( + }.stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = AppFontSettings(), ) @@ -373,9 +370,8 @@ class SettingsViewModel @Inject constructor( allDayOverrides.reminderOverrideFor(calendarId) }, ) - }.stateIn( + }.stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = SpecialDatesUiState(), ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt index e1e7512..5fcbe92 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt @@ -15,6 +15,7 @@ import de.jeanlucmakiola.calendula.domain.displayPriority import de.jeanlucmakiola.calendula.domain.FailureReason import de.jeanlucmakiola.calendula.domain.calendarListFailure import de.jeanlucmakiola.calendula.ui.common.PageStateCache +import de.jeanlucmakiola.floret.android.stateWhileSubscribed import de.jeanlucmakiola.floret.di.IoDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -97,17 +98,15 @@ abstract class PagedTimelineViewModel( /** Whether to fade events that have already finished (display concern only). */ val dimCompletedEvents: StateFlow = settingsPrefs.dimCompletedEvents - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = false, ) /** How long a block drafted on an empty slot starts out (#153). */ val defaultEventDuration: StateFlow = settingsPrefs.defaultEventDurationMinutes - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = SettingsPrefs.DEFAULT_EVENT_DURATION, ) @@ -135,14 +134,13 @@ abstract class PagedTimelineViewModel( lastToday = date if (old != date) followToday(old, date) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), todayDate) + .stateWhileSubscribed(viewModelScope, todayDate) private val position: StateFlow = combine(_anchor, rule) { anchor, rule -> positionOf(anchor, rule) } .distinctUntilChanged() - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = positionOf(todayDate, initialRule), ) @@ -156,9 +154,8 @@ abstract class PagedTimelineViewModel( .distinctUntilChanged() .flatMapLatest { span -> span(span).filterNot { it is WeekUiState.Loading } } .catch { emit(WeekUiState.Failure(FailureReason.ProviderUnavailable)) } - .stateIn( + .stateWhileSubscribed( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), initialValue = WeekUiState.Loading, )