From 5440860340f5c69a20f641897298b990549ef3a8 Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Fri, 31 Dec 2021 18:01:18 +0100 Subject: [PATCH] Replace Date and Calendar classes with Java 8 time APIs (#74) Enable multidex and Java 8 APIs desugaring in the project. Also bump minSDK version to 18 to allow calling DateFormat.getBestDateTimePattern(). --- app/build.gradle | 11 ++++-- .../be/digitalia/fosdem/FosdemApplication.kt | 4 +- .../fosdem/activities/MainActivity.kt | 29 ++++++++------ .../fosdem/activities/PersonInfoActivity.kt | 4 +- .../fosdem/adapters/BookmarksAdapter.kt | 14 +++---- .../fosdem/adapters/EventsAdapter.kt | 14 +++---- .../fosdem/adapters/TrackScheduleAdapter.kt | 11 +++--- .../java/be/digitalia/fosdem/api/FosdemApi.kt | 26 +++++++------ .../be/digitalia/fosdem/db/BookmarksDao.kt | 11 ++++-- .../be/digitalia/fosdem/db/ScheduleDao.kt | 33 +++++++--------- .../converters/NonNullDateTypeConverters.kt | 14 ------- .../NonNullInstantTypeConverters.kt | 14 +++++++ .../NonNullLocalDateTypeConverters.kt | 14 +++++++ .../converters/NullableDateTypeConverters.kt | 14 ------- .../NullableInstantTypeConverters.kt | 14 +++++++ .../fosdem/db/entities/EventEntity.kt | 12 +++--- .../fosdem/fragments/EventDetailsFragment.kt | 10 ++--- .../fosdem/livedata/LiveDataFactory.kt | 5 +-- .../be/digitalia/fosdem/model/AlarmInfo.kt | 10 ++--- .../java/be/digitalia/fosdem/model/Day.kt | 15 ++++--- .../java/be/digitalia/fosdem/model/Event.kt | 33 ++++++++-------- .../java/be/digitalia/fosdem/model/Person.kt | 3 +- .../digitalia/fosdem/parsers/EventsParser.kt | 34 ++++++---------- .../providers/BookmarksExportProvider.kt | 21 ++++------ .../fosdem/services/AlarmIntentService.kt | 16 +++++--- .../fosdem/settings/UserSettingsProvider.kt | 4 +- .../be/digitalia/fosdem/utils/DateUtils.kt | 28 ++++++------- .../be/digitalia/fosdem/utils/Parcelers.kt | 39 ++++++++++++++++--- .../fosdem/viewmodels/BookmarksViewModel.kt | 12 +++--- .../fosdem/viewmodels/LiveViewModel.kt | 12 +++--- .../viewmodels/TrackScheduleListViewModel.kt | 23 ++++++----- 31 files changed, 267 insertions(+), 237 deletions(-) delete mode 100644 app/src/main/java/be/digitalia/fosdem/db/converters/NonNullDateTypeConverters.kt create mode 100644 app/src/main/java/be/digitalia/fosdem/db/converters/NonNullInstantTypeConverters.kt create mode 100644 app/src/main/java/be/digitalia/fosdem/db/converters/NonNullLocalDateTypeConverters.kt delete mode 100644 app/src/main/java/be/digitalia/fosdem/db/converters/NullableDateTypeConverters.kt create mode 100644 app/src/main/java/be/digitalia/fosdem/db/converters/NullableInstantTypeConverters.kt diff --git a/app/build.gradle b/app/build.gradle index 23aab96..5899e7f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,9 @@ android { defaultConfig { applicationId "be.digitalia.fosdem" - minSdkVersion 17 + minSdkVersion 18 targetSdkVersion 31 + multiDexEnabled = true versionCode 1700205 versionName "2.0.5" // Supported languages @@ -48,15 +49,15 @@ android { } packagingOptions { + exclude 'androidsupportmultidexversion.txt' exclude 'DebugProbesKt.bin' + exclude 'kotlin-tooling-metadata.json' } } - debug { - multiDexEnabled = true - } } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -80,6 +81,8 @@ dependencies { def room_version = "2.4.0" def okhttp_version = "3.12.13" + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation "androidx.multidex:multidex:2.0.1" implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" diff --git a/app/src/main/java/be/digitalia/fosdem/FosdemApplication.kt b/app/src/main/java/be/digitalia/fosdem/FosdemApplication.kt index 08b3b55..e5c896b 100644 --- a/app/src/main/java/be/digitalia/fosdem/FosdemApplication.kt +++ b/app/src/main/java/be/digitalia/fosdem/FosdemApplication.kt @@ -28,8 +28,6 @@ class FosdemApplication : Application() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) - if (BuildConfig.DEBUG) { - MultiDex.install(this) - } + MultiDex.install(this) } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.kt b/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.kt index 8751afd..cde4747 100644 --- a/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.kt +++ b/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.kt @@ -9,8 +9,6 @@ import android.net.Uri import android.nfc.NdefRecord import android.os.Build import android.os.Bundle -import android.text.format.DateFormat -import android.text.format.DateUtils import android.view.Menu import android.view.MenuItem import android.view.View @@ -47,9 +45,12 @@ import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter import javax.inject.Inject import javax.inject.Named @@ -92,6 +93,8 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback @Inject lateinit var scheduleDao: ScheduleDao + private val latestUpdateDateTimeFormatter = DateTimeFormatter.ofPattern(LATEST_UPDATE_DATE_TIME_FORMAT) + private lateinit var holder: ViewHolder private lateinit var drawerToggle: ActionBarDrawerToggle @@ -188,7 +191,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update) lifecycleScope.launch { scheduleDao.latestUpdateTime.collect { time -> - val timeString = time?.let { DateFormat.format(LATEST_UPDATE_DATE_FORMAT, it) } + val timeString = time?.atZone(ZoneId.systemDefault())?.format(latestUpdateDateTimeFormatter) ?: getString(R.string.never) latestUpdateTextView.text = getString(R.string.last_update, timeString) } @@ -251,13 +254,15 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback // Scheduled database update lifecycleScope.launch { - val now = System.currentTimeMillis() + val now = Instant.now() val latestUpdateTime = scheduleDao.latestUpdateTime.first() - if (latestUpdateTime == null || latestUpdateTime.time < now - DATABASE_VALIDITY_DURATION) { - val latestAttemptTime = preferences.getLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, -1L) - if (latestAttemptTime == -1L || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) { + if (latestUpdateTime == null || latestUpdateTime < now - DATABASE_VALIDITY_DURATION) { + val latestAttemptTime = Instant.ofEpochMilli( + preferences.getLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, 0L) + ) + if (latestAttemptTime == Instant.EPOCH || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) { preferences.edit { - putLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, now) + putLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, now.toEpochMilli()) } // Try to update immediately. If it fails, the user gets a message and a retry button. api.downloadSchedule() @@ -358,9 +363,9 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback const val ACTION_SHORTCUT_LIVE = "${BuildConfig.APPLICATION_ID}.intent.action.SHORTCUT_LIVE" private const val ERROR_MESSAGE_DISPLAY_DURATION = 5000 - private const val DATABASE_VALIDITY_DURATION = DateUtils.DAY_IN_MILLIS - private const val AUTO_UPDATE_SNOOZE_DURATION = DateUtils.DAY_IN_MILLIS + private val DATABASE_VALIDITY_DURATION = Duration.ofDays(1L) + private val AUTO_UPDATE_SNOOZE_DURATION = Duration.ofDays(1L) private const val LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY = "latest_update_attempt_time" - private const val LATEST_UPDATE_DATE_FORMAT = "d MMM yyyy kk:mm:ss" + private const val LATEST_UPDATE_DATE_TIME_FORMAT = "d MMM yyyy kk:mm:ss" } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/activities/PersonInfoActivity.kt b/app/src/main/java/be/digitalia/fosdem/activities/PersonInfoActivity.kt index f5afdfd..16201f0 100644 --- a/app/src/main/java/be/digitalia/fosdem/activities/PersonInfoActivity.kt +++ b/app/src/main/java/be/digitalia/fosdem/activities/PersonInfoActivity.kt @@ -12,7 +12,6 @@ import androidx.fragment.app.commit import be.digitalia.fosdem.R import be.digitalia.fosdem.fragments.PersonInfoListFragment import be.digitalia.fosdem.model.Person -import be.digitalia.fosdem.utils.DateUtils import be.digitalia.fosdem.utils.configureToolbarColors import be.digitalia.fosdem.viewmodels.PersonInfoViewModel import dagger.hilt.android.AndroidEntryPoint @@ -36,8 +35,7 @@ class PersonInfoActivity : AppCompatActivity(R.layout.person_info) { // Look for the first non-placeholder event in the paged list val statusEvent = viewModel.events.value?.firstOrNull { it != null } if (statusEvent != null) { - val year = DateUtils.getYear(statusEvent.event.day.date.time) - val url = person.getUrl(year) + val url = person.getUrl(statusEvent.event.day.date.year) if (url != null) { try { CustomTabsIntent.Builder() diff --git a/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.kt b/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.kt index b3a0fab..917bcc7 100644 --- a/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.kt +++ b/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.kt @@ -24,12 +24,12 @@ import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.RoomStatus import be.digitalia.fosdem.utils.DateUtils import be.digitalia.fosdem.widgets.MultiChoiceHelper -import java.text.DateFormat +import java.time.format.DateTimeFormatter class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiChoiceHelper) : ListAdapter(DIFF_CALLBACK) { - private val timeDateFormat = DateUtils.getTimeDateFormat(context) + private val timeFormatter = DateUtils.getTimeFormatter(context) @ColorInt private val errorColor: Int @@ -53,7 +53,7 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_event, parent, false) - return ViewHolder(view, multiChoiceHelper, timeDateFormat, errorColor) + return ViewHolder(view, multiChoiceHelper, timeFormatter, errorColor) } private fun getRoomStatus(event: Event): RoomStatus? { @@ -101,7 +101,7 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho } class ViewHolder(itemView: View, helper: MultiChoiceHelper, - private val timeDateFormat: DateFormat, @ColorInt private val errorColor: Int) + private val timeFormatter: DateTimeFormatter, @ColorInt private val errorColor: Int) : MultiChoiceHelper.ViewHolder(itemView, helper), View.OnClickListener { private val title: TextView = itemView.findViewById(R.id.title) private val persons: TextView = itemView.findViewById(R.id.persons) @@ -130,10 +130,8 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho fun bindDetails(event: Event, previous: Event?, next: Event?, roomStatus: RoomStatus?) { val context = details.context - val startTime = event.startTime - val endTime = event.endTime - val startTimeString = if (startTime != null) timeDateFormat.format(startTime) else "?" - val endTimeString = if (endTime != null) timeDateFormat.format(endTime) else "?" + val startTimeString = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?" + val endTimeString = event.endTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?" val roomName = event.roomName.orEmpty() val detailsText: CharSequence = "${event.day.shortName}, $startTimeString ― $endTimeString | $roomName" val detailsSpannable = SpannableString(detailsText) diff --git a/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.kt b/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.kt index 0100686..a13cb14 100644 --- a/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.kt +++ b/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.kt @@ -21,12 +21,12 @@ import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.RoomStatus import be.digitalia.fosdem.model.StatusEvent import be.digitalia.fosdem.utils.DateUtils -import java.text.DateFormat +import java.time.format.DateTimeFormatter class EventsAdapter constructor(context: Context, private val showDay: Boolean = true) : PagedListAdapter(DIFF_CALLBACK) { - private val timeDateFormat = DateUtils.getTimeDateFormat(context) + private val timeFormatter = DateUtils.getTimeFormatter(context) var roomStatuses: Map? = null set(value) { @@ -38,7 +38,7 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_event, parent, false) - return ViewHolder(view, timeDateFormat) + return ViewHolder(view, timeFormatter) } private fun getRoomStatus(event: Event): RoomStatus? { @@ -70,7 +70,7 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean = } } - class ViewHolder(itemView: View, private val timeDateFormat: DateFormat) + class ViewHolder(itemView: View, private val timeFormatter: DateTimeFormatter) : RecyclerView.ViewHolder(itemView), View.OnClickListener { private val title: TextView = itemView.findViewById(R.id.title) private val persons: TextView = itemView.findViewById(R.id.persons) @@ -112,10 +112,8 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean = fun bindDetails(event: Event, showDay: Boolean, roomStatus: RoomStatus?) { val context = details.context - val startTime = event.startTime - val endTime = event.endTime - val startTimeString = if (startTime != null) timeDateFormat.format(startTime) else "?" - val endTimeString = if (endTime != null) timeDateFormat.format(endTime) else "?" + val startTimeString = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?" + val endTimeString = event.endTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?" val roomName = event.roomName.orEmpty() var detailsText: CharSequence = if (showDay) { "${event.day.shortName}, $startTimeString ― $endTimeString | $roomName" diff --git a/app/src/main/java/be/digitalia/fosdem/adapters/TrackScheduleAdapter.kt b/app/src/main/java/be/digitalia/fosdem/adapters/TrackScheduleAdapter.kt index 959a834..9a1ed55 100644 --- a/app/src/main/java/be/digitalia/fosdem/adapters/TrackScheduleAdapter.kt +++ b/app/src/main/java/be/digitalia/fosdem/adapters/TrackScheduleAdapter.kt @@ -18,6 +18,7 @@ import be.digitalia.fosdem.R import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.StatusEvent import be.digitalia.fosdem.utils.DateUtils +import java.time.Instant class TrackScheduleAdapter(context: Context, private val listener: EventClickListener? = null) : ListAdapter(EventsAdapter.DIFF_CALLBACK) { @@ -26,7 +27,7 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis fun onEventClick(event: Event) } - private val timeDateFormat = DateUtils.getTimeDateFormat(context) + private val timeFormatter = DateUtils.getTimeFormatter(context) @ColorInt private val timeBackgroundColor: Int = ContextCompat.getColor(context, R.color.schedule_time_background) @ColorInt @@ -46,7 +47,7 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis } } - var currentTime: Long = -1L + var currentTime: Instant? = null set(value) { if (field != value) { field = value @@ -127,7 +128,7 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis val context = itemView.context this.event = event - time.text = event.startTime?.let { timeDateFormat.format(it) } + time.text = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) title.text = event.title val bookmarkDrawable = if (isBookmarked) AppCompatResources.getDrawable(context, R.drawable.ic_bookmark_white_24dp) else null TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(title, null, null, bookmarkDrawable, null) @@ -141,8 +142,8 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis room.contentDescription = context.getString(R.string.room_content_description, event.roomName.orEmpty()) } - fun bindTimeColors(event: Event, currentTime: Long) { - if (currentTime != -1L && event.isRunningAtTime(currentTime)) { + fun bindTimeColors(event: Event, currentTime: Instant?) { + if (currentTime != null && event.isRunningAtTime(currentTime)) { // Contrast colors for running event time.setBackgroundColor(timeRunningBackgroundColor) time.setTextColor(timeRunningForegroundColor) diff --git a/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.kt b/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.kt index 1442e5a..ea38485 100644 --- a/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.kt +++ b/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.kt @@ -1,7 +1,6 @@ package be.digitalia.fosdem.api import android.os.SystemClock -import android.text.format.DateUtils import androidx.annotation.MainThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -20,6 +19,7 @@ import be.digitalia.fosdem.parsers.EventsParser import be.digitalia.fosdem.parsers.RoomStatusesParser import be.digitalia.fosdem.utils.BackgroundWorkScope import be.digitalia.fosdem.utils.ByteCountSource +import be.digitalia.fosdem.utils.DateUtils import be.digitalia.fosdem.utils.network.HttpClient import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -27,6 +27,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import okio.buffer +import java.time.LocalTime +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton import kotlin.math.pow @@ -103,9 +105,12 @@ class FosdemApi @Inject constructor( val startEndTimestamps = LongArray(days.size * 2) var index = 0 for (day in days) { - val dayStart = day.date.time - startEndTimestamps[index++] = dayStart + DAY_START_TIME - startEndTimestamps[index++] = dayStart + DAY_END_TIME + startEndTimestamps[index++] = day.date.atTime(DAY_START_TIME) + .atZone(DateUtils.conferenceZoneId) + .toEpochSecond() * 1000L + startEndTimestamps[index++] = day.date.atTime(DAY_END_TIME) + .atZone(DateUtils.conferenceZoneId) + .toEpochSecond() * 1000L } scheduler(*startEndTimestamps) } @@ -170,13 +175,10 @@ class FosdemApi @Inject constructor( } companion object { - // 8:30 (local time) - private const val DAY_START_TIME = 8 * DateUtils.HOUR_IN_MILLIS + 30 * DateUtils.MINUTE_IN_MILLIS - - // 19:00 (local time) - private const val DAY_END_TIME = 19 * DateUtils.HOUR_IN_MILLIS - private const val ROOM_STATUS_REFRESH_DELAY = 90L * DateUtils.SECOND_IN_MILLIS - private const val ROOM_STATUS_FIRST_RETRY_DELAY = 30L * DateUtils.SECOND_IN_MILLIS - private const val ROOM_STATUS_EXPIRATION_DELAY = 6L * DateUtils.MINUTE_IN_MILLIS + private val DAY_START_TIME = LocalTime.of(8, 30) + private val DAY_END_TIME = LocalTime.of(19, 0) + private val ROOM_STATUS_REFRESH_DELAY = TimeUnit.SECONDS.toMillis(90L) + private val ROOM_STATUS_FIRST_RETRY_DELAY = TimeUnit.SECONDS.toMillis(30L) + private val ROOM_STATUS_EXPIRATION_DELAY = TimeUnit.MINUTES.toMillis(6L) } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/db/BookmarksDao.kt b/app/src/main/java/be/digitalia/fosdem/db/BookmarksDao.kt index 377047a..80d7935 100644 --- a/app/src/main/java/be/digitalia/fosdem/db/BookmarksDao.kt +++ b/app/src/main/java/be/digitalia/fosdem/db/BookmarksDao.kt @@ -6,19 +6,22 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.TypeConverters import androidx.room.withTransaction +import be.digitalia.fosdem.db.converters.NonNullInstantTypeConverters import be.digitalia.fosdem.db.entities.Bookmark import be.digitalia.fosdem.model.AlarmInfo import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.utils.BackgroundWorkScope import kotlinx.coroutines.launch +import java.time.Instant @Dao abstract class BookmarksDao(private val appDatabase: AppDatabase) { /** * Returns the bookmarks. * - * @param minStartTime When greater than 0, only return the events starting after this time. + * @param minStartTime When greater than Instant.EPOCH, only return the events starting after this time. */ @Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type @@ -32,7 +35,8 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) { WHERE e.start_time > :minStartTime GROUP BY e.id ORDER BY e.start_time ASC""") - abstract fun getBookmarks(minStartTime: Long): LiveData> + @TypeConverters(NonNullInstantTypeConverters::class) + abstract fun getBookmarks(minStartTime: Instant): LiveData> @Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type @@ -54,7 +58,8 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) { WHERE e.start_time > :minStartTime ORDER BY e.start_time ASC""") @WorkerThread - abstract fun getBookmarksAlarmInfo(minStartTime: Long): Array + @TypeConverters(NonNullInstantTypeConverters::class) + abstract fun getBookmarksAlarmInfo(minStartTime: Instant): Array @Query("SELECT COUNT(*) FROM bookmarks WHERE event_id = :event") abstract fun getBookmarkStatus(event: Event): LiveData diff --git a/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt b/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt index ce62b71..8e54276 100644 --- a/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt +++ b/app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.kt @@ -12,6 +12,8 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction +import androidx.room.TypeConverters +import be.digitalia.fosdem.db.converters.NonNullInstantTypeConverters import be.digitalia.fosdem.db.entities.EventEntity import be.digitalia.fosdem.db.entities.EventTitles import be.digitalia.fosdem.db.entities.EventToPerson @@ -24,7 +26,6 @@ import be.digitalia.fosdem.model.Person import be.digitalia.fosdem.model.StatusEvent import be.digitalia.fosdem.model.Track import be.digitalia.fosdem.utils.BackgroundWorkScope -import be.digitalia.fosdem.utils.DateUtils import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -33,7 +34,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.runBlocking -import java.util.Date +import java.time.Instant +import java.time.LocalDate import java.util.HashSet @Dao @@ -42,8 +44,8 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) { /** * @return The latest update time, or null if not available. */ - val latestUpdateTime: Flow = appDatabase.dataStore.data.map { prefs -> - prefs[LATEST_UPDATE_TIME_PREF_KEY]?.let { Date(it) } + val latestUpdateTime: Flow = appDatabase.dataStore.data.map { prefs -> + prefs[LATEST_UPDATE_TIME_PREF_KEY]?.let { Instant.ofEpochMilli(it) } } /** @@ -69,11 +71,11 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) { 0 } if (totalEvents > 0) { // Set last update time and server's last modified tag - val now = System.currentTimeMillis() + val now = Instant.now() runBlocking { appDatabase.dataStore.edit { prefs -> prefs.clear() - prefs[LATEST_UPDATE_TIME_PREF_KEY] = now + prefs[LATEST_UPDATE_TIME_PREF_KEY] = now.toEpochMilli() if (lastModifiedTag != null) { prefs[LAST_MODIFIED_TAG_PREF] = lastModifiedTag } @@ -230,16 +232,9 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) { protected abstract fun getDaysInternal(): Flow> suspend fun getYear(): Int { - // Compute from days if available - val days = days.first() - val date = if (days.isNotEmpty()) { - days[0].date.time - } else { - // Use the current year by default - System.currentTimeMillis() - } - - return DateUtils.getYear(date) + // Compute from days if available, fall back to current year + val date = days.first().firstOrNull()?.date ?: LocalDate.now() + return date.year } @Query("""SELECT t.id, t.name, t.type FROM tracks t @@ -332,7 +327,8 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) { WHERE e.start_time BETWEEN :minStartTime AND :maxStartTime GROUP BY e.id ORDER BY e.start_time ASC""") - abstract fun getEventsWithStartTime(minStartTime: Long, maxStartTime: Long): DataSource.Factory + @TypeConverters(NonNullInstantTypeConverters::class) + abstract fun getEventsWithStartTime(minStartTime: Instant, maxStartTime: Instant): DataSource.Factory /** * Returns events in progress at the specified time, ordered by descending start time. @@ -350,7 +346,8 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) { WHERE e.start_time <= :time AND :time < e.end_time GROUP BY e.id ORDER BY e.start_time DESC""") - abstract fun getEventsInProgress(time: Long): DataSource.Factory + @TypeConverters(NonNullInstantTypeConverters::class) + abstract fun getEventsInProgress(time: Instant): DataSource.Factory /** * Returns the events presented by the specified person. diff --git a/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullDateTypeConverters.kt b/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullDateTypeConverters.kt deleted file mode 100644 index 5bab516..0000000 --- a/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullDateTypeConverters.kt +++ /dev/null @@ -1,14 +0,0 @@ -package be.digitalia.fosdem.db.converters - -import androidx.room.TypeConverter -import java.util.Date - -object NonNullDateTypeConverters { - @JvmStatic - @TypeConverter - fun toDate(value: Long): Date = Date(value) - - @JvmStatic - @TypeConverter - fun fromDate(value: Date): Long = value.time -} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullInstantTypeConverters.kt b/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullInstantTypeConverters.kt new file mode 100644 index 0000000..c53204a --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullInstantTypeConverters.kt @@ -0,0 +1,14 @@ +package be.digitalia.fosdem.db.converters + +import androidx.room.TypeConverter +import java.time.Instant + +object NonNullInstantTypeConverters { + @JvmStatic + @TypeConverter + fun toInstant(value: Long): Instant = Instant.ofEpochSecond(value) + + @JvmStatic + @TypeConverter + fun fromInstant(value: Instant): Long = value.epochSecond +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullLocalDateTypeConverters.kt b/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullLocalDateTypeConverters.kt new file mode 100644 index 0000000..ac108cd --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/db/converters/NonNullLocalDateTypeConverters.kt @@ -0,0 +1,14 @@ +package be.digitalia.fosdem.db.converters + +import androidx.room.TypeConverter +import java.time.LocalDate + +object NonNullLocalDateTypeConverters { + @JvmStatic + @TypeConverter + fun toLocalDate(value: Long): LocalDate = LocalDate.ofEpochDay(value) + + @JvmStatic + @TypeConverter + fun fromLocalDate(value: LocalDate): Long = value.toEpochDay() +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/db/converters/NullableDateTypeConverters.kt b/app/src/main/java/be/digitalia/fosdem/db/converters/NullableDateTypeConverters.kt deleted file mode 100644 index 1df997b..0000000 --- a/app/src/main/java/be/digitalia/fosdem/db/converters/NullableDateTypeConverters.kt +++ /dev/null @@ -1,14 +0,0 @@ -package be.digitalia.fosdem.db.converters - -import androidx.room.TypeConverter -import java.util.Date - -object NullableDateTypeConverters { - @JvmStatic - @TypeConverter - fun toDate(value: Long?): Date? = value?.let { Date(it) } - - @JvmStatic - @TypeConverter - fun fromDate(value: Date?): Long? = value?.time -} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/db/converters/NullableInstantTypeConverters.kt b/app/src/main/java/be/digitalia/fosdem/db/converters/NullableInstantTypeConverters.kt new file mode 100644 index 0000000..b93005e --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/db/converters/NullableInstantTypeConverters.kt @@ -0,0 +1,14 @@ +package be.digitalia.fosdem.db.converters + +import androidx.room.TypeConverter +import java.time.Instant + +object NullableInstantTypeConverters { + @JvmStatic + @TypeConverter + fun toInstant(value: Long?): Instant? = value?.let { Instant.ofEpochSecond(it) } + + @JvmStatic + @TypeConverter + fun fromInstant(value: Instant?): Long? = value?.epochSecond +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/db/entities/EventEntity.kt b/app/src/main/java/be/digitalia/fosdem/db/entities/EventEntity.kt index d0e7507..64ad1d9 100644 --- a/app/src/main/java/be/digitalia/fosdem/db/entities/EventEntity.kt +++ b/app/src/main/java/be/digitalia/fosdem/db/entities/EventEntity.kt @@ -5,8 +5,8 @@ import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey import androidx.room.TypeConverters -import be.digitalia.fosdem.db.converters.NullableDateTypeConverters -import java.util.Date +import be.digitalia.fosdem.db.converters.NullableInstantTypeConverters +import java.time.Instant @Entity(tableName = EventEntity.TABLE_NAME, indices = [ Index(value = ["day_index"], name = "event_day_index_idx"), @@ -20,11 +20,11 @@ class EventEntity( @ColumnInfo(name = "day_index") val dayIndex: Int, @ColumnInfo(name = "start_time") - @field:TypeConverters(NullableDateTypeConverters::class) - val startTime: Date?, + @field:TypeConverters(NullableInstantTypeConverters::class) + val startTime: Instant?, @ColumnInfo(name = "end_time") - @field:TypeConverters(NullableDateTypeConverters::class) - val endTime: Date?, + @field:TypeConverters(NullableInstantTypeConverters::class) + val endTime: Instant?, @ColumnInfo(name = "room_name") val roomName: String?, val slug: String?, diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.kt b/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.kt index 83a9da3..04a1bf2 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.kt +++ b/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.kt @@ -99,9 +99,9 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) { } view.findViewById(R.id.time).apply { - val timeDateFormat = DateUtils.getTimeDateFormat(context) - val startTime = event.startTime?.let { timeDateFormat.format(it) } ?: "?" - val endTime = event.endTime?.let { timeDateFormat.format(it) } ?: "?" + val timeFormatter = DateUtils.getTimeFormatter(context) + val startTime = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?" + val endTime = event.endTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?" text = "${event.day}, $startTime ― $endTime" contentDescription = getString(R.string.time_content_description, text) } @@ -226,8 +226,8 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) { description = "$speakersLabel: $personsSummary\n\n$description" } putExtra(CalendarContract.Events.DESCRIPTION, description) - event.startTime?.let { putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time) } - event.endTime?.let { putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.time) } + event.startTime?.let { putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.toEpochMilli()) } + event.endTime?.let { putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.toEpochMilli()) } } try { diff --git a/app/src/main/java/be/digitalia/fosdem/livedata/LiveDataFactory.kt b/app/src/main/java/be/digitalia/fosdem/livedata/LiveDataFactory.kt index 9e4f068..8d479ba 100644 --- a/app/src/main/java/be/digitalia/fosdem/livedata/LiveDataFactory.kt +++ b/app/src/main/java/be/digitalia/fosdem/livedata/LiveDataFactory.kt @@ -5,14 +5,13 @@ import android.os.SystemClock import androidx.core.os.HandlerCompat import androidx.lifecycle.LiveData import java.util.Arrays -import java.util.concurrent.TimeUnit object LiveDataFactory { private val handler = HandlerCompat.createAsync(Looper.getMainLooper()) - fun interval(period: Long, unit: TimeUnit): LiveData { - return IntervalLiveData(unit.toMillis(period)) + fun interval(periodInMillis: Long): LiveData { + return IntervalLiveData(periodInMillis) } /** diff --git a/app/src/main/java/be/digitalia/fosdem/model/AlarmInfo.kt b/app/src/main/java/be/digitalia/fosdem/model/AlarmInfo.kt index 112296c..ffc726a 100644 --- a/app/src/main/java/be/digitalia/fosdem/model/AlarmInfo.kt +++ b/app/src/main/java/be/digitalia/fosdem/model/AlarmInfo.kt @@ -3,17 +3,17 @@ package be.digitalia.fosdem.model import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.TypeConverters -import be.digitalia.fosdem.db.converters.NullableDateTypeConverters -import be.digitalia.fosdem.utils.DateParceler +import be.digitalia.fosdem.db.converters.NullableInstantTypeConverters +import be.digitalia.fosdem.utils.InstantParceler import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith -import java.util.Date +import java.time.Instant @Parcelize data class AlarmInfo( @ColumnInfo(name = "event_id") val eventId: Long, @ColumnInfo(name = "start_time") - @field:TypeConverters(NullableDateTypeConverters::class) - val startTime: @WriteWith Date? + @field:TypeConverters(NullableInstantTypeConverters::class) + val startTime: @WriteWith Instant? ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/model/Day.kt b/app/src/main/java/be/digitalia/fosdem/model/Day.kt index ec54001..a004735 100644 --- a/app/src/main/java/be/digitalia/fosdem/model/Day.kt +++ b/app/src/main/java/be/digitalia/fosdem/model/Day.kt @@ -4,13 +4,12 @@ import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverters -import be.digitalia.fosdem.db.converters.NonNullDateTypeConverters -import be.digitalia.fosdem.utils.DateParceler -import be.digitalia.fosdem.utils.DateUtils.withBelgiumTimeZone +import be.digitalia.fosdem.db.converters.NonNullLocalDateTypeConverters +import be.digitalia.fosdem.utils.LocalDateParceler import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith -import java.text.SimpleDateFormat -import java.util.Date +import java.time.LocalDate +import java.time.format.DateTimeFormatter import java.util.Locale @Entity(tableName = Day.TABLE_NAME) @@ -18,8 +17,8 @@ import java.util.Locale data class Day( @PrimaryKey val index: Int, - @field:TypeConverters(NonNullDateTypeConverters::class) - val date: @WriteWith Date + @field:TypeConverters(NonNullLocalDateTypeConverters::class) + val date: @WriteWith LocalDate ) : Comparable, Parcelable { val name: String @@ -37,6 +36,6 @@ data class Day( companion object { const val TABLE_NAME = "days" - private val DAY_DATE_FORMAT = SimpleDateFormat("EEEE", Locale.US).withBelgiumTimeZone() + private val DAY_DATE_FORMAT = DateTimeFormatter.ofPattern("EEEE", Locale.US) } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/model/Event.kt b/app/src/main/java/be/digitalia/fosdem/model/Event.kt index e7cfc41..c569bb5 100644 --- a/app/src/main/java/be/digitalia/fosdem/model/Event.kt +++ b/app/src/main/java/be/digitalia/fosdem/model/Event.kt @@ -5,12 +5,12 @@ import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.TypeConverters import be.digitalia.fosdem.api.FosdemUrls -import be.digitalia.fosdem.db.converters.NullableDateTypeConverters -import be.digitalia.fosdem.utils.DateParceler -import be.digitalia.fosdem.utils.DateUtils +import be.digitalia.fosdem.db.converters.NullableInstantTypeConverters +import be.digitalia.fosdem.utils.InstantParceler import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith -import java.util.Date +import java.time.Duration +import java.time.Instant @Parcelize data class Event( @@ -18,11 +18,11 @@ data class Event( @Embedded(prefix = "day_") val day: Day, @ColumnInfo(name = "start_time") - @field:TypeConverters(NullableDateTypeConverters::class) - val startTime: @WriteWith Date? = null, + @field:TypeConverters(NullableInstantTypeConverters::class) + val startTime: @WriteWith Instant? = null, @ColumnInfo(name = "end_time") - @field:TypeConverters(NullableDateTypeConverters::class) - val endTime: @WriteWith Date? = null, + @field:TypeConverters(NullableInstantTypeConverters::class) + val endTime: @WriteWith Instant? = null, @ColumnInfo(name = "room_name") val roomName: String?, val slug: String?, @@ -38,22 +38,21 @@ data class Event( val personsSummary: String? ) : Parcelable { - fun isRunningAtTime(time: Long): Boolean { - return startTime != null && endTime != null && time in startTime.time..endTime.time + fun isRunningAtTime(time: Instant): Boolean { + return startTime != null && endTime != null && time in startTime..endTime } - /** - * @return The event duration in minutes - */ - val duration: Int + val duration: Duration get() = if (startTime == null || endTime == null) { - 0 - } else ((endTime.time - startTime.time) / android.text.format.DateUtils.MINUTE_IN_MILLIS).toInt() + Duration.ZERO + } else { + Duration.between(startTime, endTime) + } val url: String? get() { val s = slug ?: return null - return FosdemUrls.getEvent(s, DateUtils.getYear(day.date.time)) + return FosdemUrls.getEvent(s, day.date.year) } override fun toString(): String = title.orEmpty() diff --git a/app/src/main/java/be/digitalia/fosdem/model/Person.kt b/app/src/main/java/be/digitalia/fosdem/model/Person.kt index 5a0004d..c748bf5 100644 --- a/app/src/main/java/be/digitalia/fosdem/model/Person.kt +++ b/app/src/main/java/be/digitalia/fosdem/model/Person.kt @@ -20,8 +20,7 @@ data class Person( ) : Parcelable { fun getUrl(year: Int): String? { - val n = name ?: return null - return FosdemUrls.getPerson(n.toSlug(), year) + return name?.let { FosdemUrls.getPerson(it.toSlug(), year) } } override fun toString(): String = name.orEmpty() diff --git a/app/src/main/java/be/digitalia/fosdem/parsers/EventsParser.kt b/app/src/main/java/be/digitalia/fosdem/parsers/EventsParser.kt index 28a6f6d..930d9aa 100644 --- a/app/src/main/java/be/digitalia/fosdem/parsers/EventsParser.kt +++ b/app/src/main/java/be/digitalia/fosdem/parsers/EventsParser.kt @@ -7,8 +7,7 @@ import be.digitalia.fosdem.model.EventDetails import be.digitalia.fosdem.model.Link import be.digitalia.fosdem.model.Person import be.digitalia.fosdem.model.Track -import be.digitalia.fosdem.utils.DateUtils.belgiumTimeZone -import be.digitalia.fosdem.utils.DateUtils.withBelgiumTimeZone +import be.digitalia.fosdem.utils.DateUtils import be.digitalia.fosdem.utils.isEndDocument import be.digitalia.fosdem.utils.isNextEndTag import be.digitalia.fosdem.utils.isStartTag @@ -16,10 +15,9 @@ import be.digitalia.fosdem.utils.skipToEndTag import be.digitalia.fosdem.utils.xmlPullParserFactory import okio.BufferedSource import org.xmlpull.v1.XmlPullParser -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale +import java.time.Duration +import java.time.Instant +import java.time.LocalDate /** * Main parser for FOSDEM schedule data in pentabarf XML format. @@ -28,10 +26,6 @@ import java.util.Locale */ class EventsParser : Parser> { - private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US).withBelgiumTimeZone() - // Calendar used to compute the events time, according to Belgium timezone - private val calendar = Calendar.getInstance(belgiumTimeZone, Locale.US) - override fun parse(source: BufferedSource): Sequence { val parser: XmlPullParser = xmlPullParserFactory.newPullParser().apply { setInput(source.inputStream(), null) @@ -48,7 +42,7 @@ class EventsParser : Parser> { "day" -> { currentDay = Day( index = parser.getAttributeValue(null, "index")!!.toInt(), - date = dateFormat.parse(parser.getAttributeValue(null, "date"))!! + date = LocalDate.parse(parser.getAttributeValue(null, "date")) ) } "room" -> currentRoomName = parser.getAttributeValue(null, "name") @@ -65,7 +59,7 @@ class EventsParser : Parser> { private fun parseEvent(parser: XmlPullParser, day: Day, roomName: String?): DetailedEvent { val id = parser.getAttributeValue(null, "id")!!.toLong() - var startTime: Date? = null + var startTime: Instant? = null var duration: String? = null var slug: String? = null var title: String? = null @@ -83,12 +77,10 @@ class EventsParser : Parser> { "start" -> { val timeString = parser.nextText() if (!timeString.isNullOrEmpty()) { - startTime = with(calendar) { - time = day.date - set(Calendar.HOUR_OF_DAY, getHours(timeString)) - set(Calendar.MINUTE, getMinutes(timeString)) - time - } + startTime = day.date + .atTime(getHours(timeString), getMinutes(timeString)) + .atZone(DateUtils.conferenceZoneId) + .toInstant() } } "duration" -> duration = parser.nextText() @@ -128,11 +120,7 @@ class EventsParser : Parser> { } val endTime = if (startTime != null && !duration.isNullOrEmpty()) { - with(calendar) { - add(Calendar.HOUR_OF_DAY, getHours(duration)) - add(Calendar.MINUTE, getMinutes(duration)) - time - } + startTime + Duration.ofMinutes(getHours(duration) * 60L + getMinutes(duration)) } else null val event = Event( diff --git a/app/src/main/java/be/digitalia/fosdem/providers/BookmarksExportProvider.kt b/app/src/main/java/be/digitalia/fosdem/providers/BookmarksExportProvider.kt index 572b94d..154cd4b 100644 --- a/app/src/main/java/be/digitalia/fosdem/providers/BookmarksExportProvider.kt +++ b/app/src/main/java/be/digitalia/fosdem/providers/BookmarksExportProvider.kt @@ -18,7 +18,6 @@ import be.digitalia.fosdem.db.BookmarksDao import be.digitalia.fosdem.db.ScheduleDao import be.digitalia.fosdem.ical.ICalendarWriter import be.digitalia.fosdem.model.Event -import be.digitalia.fosdem.utils.DateUtils import be.digitalia.fosdem.utils.stripHtml import be.digitalia.fosdem.utils.toSlug import dagger.hilt.EntryPoint @@ -31,10 +30,10 @@ import okio.sink import java.io.FileNotFoundException import java.io.IOException import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Calendar +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import java.util.Locale -import java.util.TimeZone /** * Content Provider generating the current bookmarks list in iCalendar format. @@ -101,12 +100,7 @@ class BookmarksExportProvider : ContentProvider() { } private class DownloadThread(private val outputStream: OutputStream, private val bookmarksDao: BookmarksDao) : Thread() { - private val calendar = Calendar.getInstance(DateUtils.belgiumTimeZone, Locale.US) - // Format all times in GMT - private val dateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("GMT+0") - } - private val dtStamp = dateFormat.format(System.currentTimeMillis()) + private val dtStamp = LocalDateTime.now(ZoneOffset.UTC).format(DATE_TIME_FORMAT) override fun run() { try { @@ -130,11 +124,11 @@ class BookmarksExportProvider : ContentProvider() { private fun writeEvent(writer: ICalendarWriter, event: Event) = with(writer) { write("BEGIN", "VEVENT") - val year = DateUtils.getYear(event.day.date.time, calendar) + val year = event.day.date.year write("UID", "${event.id}@$year@${BuildConfig.APPLICATION_ID}") write("DTSTAMP", dtStamp) - event.startTime?.let { write("DTSTART", dateFormat.format(it)) } - event.endTime?.let { write("DTEND", dateFormat.format(it)) } + event.startTime?.let { write("DTSTART", it.atOffset(ZoneOffset.UTC).format(DATE_TIME_FORMAT)) } + event.endTime?.let { write("DTEND", it.atOffset(ZoneOffset.UTC).format(DATE_TIME_FORMAT)) } write("SUMMARY", event.title) var description = event.abstractText if (description.isNullOrEmpty()) { @@ -176,6 +170,7 @@ class BookmarksExportProvider : ContentProvider() { .appendPath("bookmarks.ics") .build() private val COLUMNS = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE) + private val DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US) fun getIntent(activity: Activity): Intent { // Supports granting read permission for the attached shared file diff --git a/app/src/main/java/be/digitalia/fosdem/services/AlarmIntentService.kt b/app/src/main/java/be/digitalia/fosdem/services/AlarmIntentService.kt index b677c98..ba38614 100644 --- a/app/src/main/java/be/digitalia/fosdem/services/AlarmIntentService.kt +++ b/app/src/main/java/be/digitalia/fosdem/services/AlarmIntentService.kt @@ -40,6 +40,7 @@ import be.digitalia.fosdem.utils.roomNameToResourceName import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import java.time.Instant import javax.inject.Inject /** @@ -75,9 +76,9 @@ class AlarmIntentService : JobIntentService() { val delay = runBlocking { userSettingsProvider.notificationsDelayInMillis.first() } val now = System.currentTimeMillis() var hasAlarms = false - for (info in bookmarksDao.getBookmarksAlarmInfo(0L)) { + for (info in bookmarksDao.getBookmarksAlarmInfo(Instant.EPOCH)) { val startTime = info.startTime - val notificationTime = if (startTime == null) -1L else startTime.time - delay + val notificationTime = if (startTime == null) -1L else startTime.toEpochMilli() - delay val pi = getAlarmPendingIntent(info.eventId) if (notificationTime < now) { // Cancel pending alarms that are now scheduled in the past, if any @@ -94,7 +95,7 @@ class AlarmIntentService : JobIntentService() { } ACTION_DISABLE_ALARMS -> { // Cancel alarms of every bookmark in the future - for (info in bookmarksDao.getBookmarksAlarmInfo(System.currentTimeMillis())) { + for (info in bookmarksDao.getBookmarksAlarmInfo(Instant.now())) { alarmManager.cancel(getAlarmPendingIntent(info.eventId)) } setAlarmReceiverEnabled(false) @@ -107,7 +108,7 @@ class AlarmIntentService : JobIntentService() { var isFirstAlarm = true for ((eventId, startTime) in alarmInfos) { // Only schedule future events. If they start before the delay, the alarm will go off immediately - if (startTime != null && startTime.time >= now) { + if (startTime != null && startTime.toEpochMilli() >= now) { if (isFirstAlarm) { setAlarmReceiverEnabled(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -115,7 +116,10 @@ class AlarmIntentService : JobIntentService() { } isFirstAlarm = false } - AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, startTime.time - delay, getAlarmPendingIntent(eventId)) + AlarmManagerCompat.setExactAndAllowWhileIdle( + alarmManager, AlarmManager.RTC_WAKEUP, + startTime.toEpochMilli() - delay, getAlarmPendingIntent(eventId) + ) } } } @@ -187,7 +191,7 @@ class AlarmIntentService : JobIntentService() { val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL) .setSmallIcon(R.drawable.ic_stat_fosdem) .setColor(notificationColor) - .setWhen(event.startTime?.time ?: System.currentTimeMillis()) + .setWhen(event.startTime?.toEpochMilli() ?: System.currentTimeMillis()) .setContentTitle(event.title) .setContentText(contentText) .setStyle(NotificationCompat.BigTextStyle().bigText(bigText).setSummaryText(trackName)) diff --git a/app/src/main/java/be/digitalia/fosdem/settings/UserSettingsProvider.kt b/app/src/main/java/be/digitalia/fosdem/settings/UserSettingsProvider.kt index 0a26abc..8b01a07 100644 --- a/app/src/main/java/be/digitalia/fosdem/settings/UserSettingsProvider.kt +++ b/app/src/main/java/be/digitalia/fosdem/settings/UserSettingsProvider.kt @@ -1,12 +1,12 @@ package be.digitalia.fosdem.settings import android.content.Context -import android.text.format.DateUtils import androidx.preference.PreferenceManager import be.digitalia.fosdem.R import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton @@ -35,6 +35,6 @@ class UserSettingsProvider @Inject constructor(@ApplicationContext context: Cont get() = sharedPreferences.getStringAsFlow(PreferenceKeys.NOTIFICATIONS_DELAY) .map { // Convert from minutes to milliseconds - (it?.toLong() ?: 0L) * DateUtils.MINUTE_IN_MILLIS + TimeUnit.MINUTES.toMillis(it?.toLong() ?: 0L) } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/utils/DateUtils.kt b/app/src/main/java/be/digitalia/fosdem/utils/DateUtils.kt index 5c9a25f..ed9028f 100644 --- a/app/src/main/java/be/digitalia/fosdem/utils/DateUtils.kt +++ b/app/src/main/java/be/digitalia/fosdem/utils/DateUtils.kt @@ -1,25 +1,19 @@ package be.digitalia.fosdem.utils import android.content.Context -import java.text.DateFormat -import java.util.Calendar -import java.util.Locale -import java.util.TimeZone +import android.text.format.DateFormat +import androidx.core.os.ConfigurationCompat +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter object DateUtils { - val belgiumTimeZone: TimeZone = TimeZone.getTimeZone("GMT+1") + val conferenceZoneId: ZoneId = ZoneOffset.ofHours(1) - fun DateFormat.withBelgiumTimeZone(): DateFormat { - timeZone = belgiumTimeZone - return this - } - - fun getTimeDateFormat(context: Context): DateFormat { - return android.text.format.DateFormat.getTimeFormat(context).withBelgiumTimeZone() - } - - fun getYear(timestamp: Long, calendar: Calendar = Calendar.getInstance(belgiumTimeZone, Locale.US)): Int { - calendar.timeInMillis = timestamp - return calendar.get(Calendar.YEAR) + fun getTimeFormatter(context: Context): DateTimeFormatter { + val primaryLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0] + val basePattern = if (DateFormat.is24HourFormat(context)) "Hm" else "hm" + val bestPattern = DateFormat.getBestDateTimePattern(primaryLocale, basePattern) + return DateTimeFormatter.ofPattern(bestPattern, primaryLocale) } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/utils/Parcelers.kt b/app/src/main/java/be/digitalia/fosdem/utils/Parcelers.kt index e05380f..5fd0b9c 100644 --- a/app/src/main/java/be/digitalia/fosdem/utils/Parcelers.kt +++ b/app/src/main/java/be/digitalia/fosdem/utils/Parcelers.kt @@ -2,14 +2,41 @@ package be.digitalia.fosdem.utils import android.os.Parcel import kotlinx.parcelize.Parceler -import java.util.Date +import java.time.Instant +import java.time.LocalDate -object DateParceler : Parceler { +object InstantParceler : Parceler { - override fun create(parcel: Parcel): Date? { - val value = parcel.readLong() - return if (value == -1L) null else Date(value) + override fun create(parcel: Parcel): Instant? { + val nanoAdjustment = parcel.readInt() + if (nanoAdjustment == Int.MIN_VALUE) { + return null + } + val epochSecond = parcel.readLong() + return Instant.ofEpochSecond(epochSecond, nanoAdjustment.toLong()) } - override fun Date?.write(parcel: Parcel, flags: Int) = parcel.writeLong(this?.time ?: -1L) + override fun Instant?.write(parcel: Parcel, flags: Int) { + if (this == null) { + parcel.writeInt(Int.MIN_VALUE) + } else { + parcel.writeInt(nano) + parcel.writeLong(epochSecond) + } + } +} + +object LocalDateParceler : Parceler { + + override fun create(parcel: Parcel): LocalDate { + val year = parcel.readInt() + val monthDay = parcel.readInt() + return LocalDate.of(year, monthDay shr 16, monthDay and 0xFFFF) + } + + override fun LocalDate.write(parcel: Parcel, flags: Int) { + parcel.writeInt(year) + // pack month and day into a single int + parcel.writeInt((monthValue shl 16) or dayOfMonth) + } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/viewmodels/BookmarksViewModel.kt b/app/src/main/java/be/digitalia/fosdem/viewmodels/BookmarksViewModel.kt index df51025..e312e8b 100644 --- a/app/src/main/java/be/digitalia/fosdem/viewmodels/BookmarksViewModel.kt +++ b/app/src/main/java/be/digitalia/fosdem/viewmodels/BookmarksViewModel.kt @@ -2,7 +2,6 @@ package be.digitalia.fosdem.viewmodels import android.app.Application import android.net.Uri -import android.text.format.DateUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -18,6 +17,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.buffer import okio.source +import java.time.Duration +import java.time.Instant import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -33,12 +34,12 @@ class BookmarksViewModel @Inject constructor( val bookmarks: LiveData> = upcomingOnlyLiveData.switchMap { upcomingOnly: Boolean -> if (upcomingOnly) { // Refresh upcoming bookmarks every 2 minutes - LiveDataFactory.interval(2L, TimeUnit.MINUTES) + LiveDataFactory.interval(REFRESH_PERIOD) .switchMap { - bookmarksDao.getBookmarks(System.currentTimeMillis() - TIME_OFFSET) + bookmarksDao.getBookmarks(Instant.now() - TIME_OFFSET) } } else { - bookmarksDao.getBookmarks(-1L) + bookmarksDao.getBookmarks(Instant.EPOCH) } } @@ -63,7 +64,8 @@ class BookmarksViewModel @Inject constructor( } companion object { + private val REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(2L) // In upcomingOnly mode, events that just started are still shown for 5 minutes - private const val TIME_OFFSET = 5L * DateUtils.MINUTE_IN_MILLIS + private val TIME_OFFSET = Duration.ofMinutes(5L) } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/viewmodels/LiveViewModel.kt b/app/src/main/java/be/digitalia/fosdem/viewmodels/LiveViewModel.kt index a02668a..72ba46c 100644 --- a/app/src/main/java/be/digitalia/fosdem/viewmodels/LiveViewModel.kt +++ b/app/src/main/java/be/digitalia/fosdem/viewmodels/LiveViewModel.kt @@ -1,6 +1,5 @@ package be.digitalia.fosdem.viewmodels -import android.text.format.DateUtils import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.switchMap @@ -10,24 +9,27 @@ import be.digitalia.fosdem.db.ScheduleDao import be.digitalia.fosdem.livedata.LiveDataFactory import be.digitalia.fosdem.model.StatusEvent import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.Duration +import java.time.Instant import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel class LiveViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() { - private val heartbeat = LiveDataFactory.interval(1L, TimeUnit.MINUTES) + private val heartbeat = LiveDataFactory.interval(REFRESH_PERIOD) val nextEvents: LiveData> = heartbeat.switchMap { - val now = System.currentTimeMillis() + val now = Instant.now() scheduleDao.getEventsWithStartTime(now, now + NEXT_EVENTS_INTERVAL).toLiveData(20) } val eventsInProgress: LiveData> = heartbeat.switchMap { - scheduleDao.getEventsInProgress(System.currentTimeMillis()).toLiveData(20) + scheduleDao.getEventsInProgress(Instant.now()).toLiveData(20) } companion object { - private const val NEXT_EVENTS_INTERVAL = 30L * DateUtils.MINUTE_IN_MILLIS + private val REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(1L) + private val NEXT_EVENTS_INTERVAL = Duration.ofMinutes(30L) } } \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/viewmodels/TrackScheduleListViewModel.kt b/app/src/main/java/be/digitalia/fosdem/viewmodels/TrackScheduleListViewModel.kt index 295b2a5..ca39ed2 100644 --- a/app/src/main/java/be/digitalia/fosdem/viewmodels/TrackScheduleListViewModel.kt +++ b/app/src/main/java/be/digitalia/fosdem/viewmodels/TrackScheduleListViewModel.kt @@ -1,6 +1,5 @@ package be.digitalia.fosdem.viewmodels -import android.text.format.DateUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -11,7 +10,10 @@ import be.digitalia.fosdem.livedata.LiveDataFactory import be.digitalia.fosdem.model.Day import be.digitalia.fosdem.model.StatusEvent import be.digitalia.fosdem.model.Track +import be.digitalia.fosdem.utils.DateUtils import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.Duration +import java.time.Instant import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -25,21 +27,22 @@ class TrackScheduleListViewModel @Inject constructor(scheduleDao: ScheduleDao) : } /** - * @return The current time during the target day, or -1 outside of the target day. + * @return The current time during the target day, or null outside of the target day. */ - val currentTime: LiveData = dayTrackLiveData + val currentTime: LiveData = dayTrackLiveData .switchMap { (day, _) -> // Auto refresh during the day passed as argument - val dayStart = day.date.time - LiveDataFactory.scheduler(dayStart, dayStart + DateUtils.DAY_IN_MILLIS) + val dayStart = day.date.atStartOfDay(DateUtils.conferenceZoneId).toInstant() + LiveDataFactory.scheduler( + dayStart.toEpochMilli(), + (dayStart + Duration.ofDays(1L)).toEpochMilli() + ) } .switchMap { isOn -> if (isOn) { - LiveDataFactory.interval(REFRESH_TIME_INTERVAL, TimeUnit.MILLISECONDS).map { - System.currentTimeMillis() - } + LiveDataFactory.interval(TIME_REFRESH_PERIOD).map { Instant.now() } } else { - MutableLiveData(-1L) + MutableLiveData(null) } } @@ -51,6 +54,6 @@ class TrackScheduleListViewModel @Inject constructor(scheduleDao: ScheduleDao) : } companion object { - private const val REFRESH_TIME_INTERVAL = DateUtils.MINUTE_IN_MILLIS + private val TIME_REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(1L) } } \ No newline at end of file