From 0df5efec5504ca46f7ef80f7b58f057e9712a2e1 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Wed, 29 Nov 2023 23:34:45 -0500 Subject: [PATCH] Use type converter that cannot return null for date to avoid crash (#1573) --- CHANGELOG.md | 2 + .../models/converter/DateTypeConverter.kt | 29 ++++++++++++++ .../pocketcasts/models/db/AppDatabase.kt | 2 + .../models/entity/PodcastEpisode.kt | 3 +- .../models/SafeDateTypeConverterTest.kt | 39 +++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 modules/services/model/src/test/java/au/com/shiftyjelly/pocketcasts/models/SafeDateTypeConverterTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e30325300..55849105ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * Bug Fixes: * Ensure we have the most up-to-date episode urls before attempting playback ([#1561](https://github.com/Automattic/pocket-casts-android/pull/1561)) + * Prevent crash if database is missing date episode is published + ([#1573](https://github.com/Automattic/pocket-casts-android/pull/1573)) 7.52 ----- diff --git a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/converter/DateTypeConverter.kt b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/converter/DateTypeConverter.kt index bc34733233f..16c5b49374e 100644 --- a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/converter/DateTypeConverter.kt +++ b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/converter/DateTypeConverter.kt @@ -1,6 +1,9 @@ package au.com.shiftyjelly.pocketcasts.models.converter import androidx.room.TypeConverter +import io.sentry.Sentry +import timber.log.Timber +import java.time.Instant import java.util.Date class DateTypeConverter { @@ -15,3 +18,29 @@ class DateTypeConverter { return value?.time } } + +typealias SafeDate = Date + +// Type converter for dates that will not return null even if a null parameter is passed in. +class SafeDateTypeConverter { + + @TypeConverter + fun toDate(value: Long?): SafeDate { + return if (value == null) { + "ShouldNotBeNullDateTypeConverter::toDate called with null parameter. Returning epoch date.".let { + Timber.w(it) + Sentry.addBreadcrumb(it) + } + EPOCH + } else { + Date(value) + } + } + + @TypeConverter + fun toLong(value: SafeDate?): Long = value?.time ?: 0L + + companion object { + private val EPOCH = Date(Instant.EPOCH.toEpochMilli()) + } +} diff --git a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/AppDatabase.kt b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/AppDatabase.kt index 954e33705dc..07e72918338 100644 --- a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/AppDatabase.kt +++ b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/db/AppDatabase.kt @@ -18,6 +18,7 @@ import au.com.shiftyjelly.pocketcasts.models.converter.EpisodesSortTypeConverter import au.com.shiftyjelly.pocketcasts.models.converter.PodcastAutoUpNextConverter import au.com.shiftyjelly.pocketcasts.models.converter.PodcastLicensingEnumConverter import au.com.shiftyjelly.pocketcasts.models.converter.PodcastsSortTypeConverter +import au.com.shiftyjelly.pocketcasts.models.converter.SafeDateTypeConverter import au.com.shiftyjelly.pocketcasts.models.converter.SyncStatusConverter import au.com.shiftyjelly.pocketcasts.models.converter.TrimModeTypeConverter import au.com.shiftyjelly.pocketcasts.models.converter.UserEpisodeServerStatusConverter @@ -69,6 +70,7 @@ import au.com.shiftyjelly.pocketcasts.localization.R as LR AnonymousBumpStat.CustomEventPropsTypeConverter::class, BundlePaidTypeConverter::class, DateTypeConverter::class, + SafeDateTypeConverter::class, EpisodePlayingStatusConverter::class, EpisodeStatusEnumConverter::class, EpisodesSortTypeConverter::class, diff --git a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/entity/PodcastEpisode.kt b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/entity/PodcastEpisode.kt index 39eef62c98d..e576d57274c 100644 --- a/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/entity/PodcastEpisode.kt +++ b/modules/services/model/src/main/java/au/com/shiftyjelly/pocketcasts/models/entity/PodcastEpisode.kt @@ -7,6 +7,7 @@ import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey import au.com.shiftyjelly.pocketcasts.localization.R +import au.com.shiftyjelly.pocketcasts.models.converter.SafeDate import au.com.shiftyjelly.pocketcasts.models.type.EpisodePlayingStatus import au.com.shiftyjelly.pocketcasts.models.type.EpisodeStatusEnum import java.io.Serializable @@ -23,7 +24,7 @@ import java.util.Date data class PodcastEpisode( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "uuid") override var uuid: String, @ColumnInfo(name = "episode_description") override var episodeDescription: String = "", - @ColumnInfo(name = "published_date") override var publishedDate: Date, + @ColumnInfo(name = "published_date") override var publishedDate: SafeDate, @ColumnInfo(name = "title") override var title: String = "", @ColumnInfo(name = "size_in_bytes") override var sizeInBytes: Long = 0, @ColumnInfo(name = "episode_status") override var episodeStatus: EpisodeStatusEnum = EpisodeStatusEnum.NOT_DOWNLOADED, diff --git a/modules/services/model/src/test/java/au/com/shiftyjelly/pocketcasts/models/SafeDateTypeConverterTest.kt b/modules/services/model/src/test/java/au/com/shiftyjelly/pocketcasts/models/SafeDateTypeConverterTest.kt new file mode 100644 index 00000000000..c7dd9745689 --- /dev/null +++ b/modules/services/model/src/test/java/au/com/shiftyjelly/pocketcasts/models/SafeDateTypeConverterTest.kt @@ -0,0 +1,39 @@ +package au.com.shiftyjelly.pocketcasts.models + +import au.com.shiftyjelly.pocketcasts.models.converter.SafeDateTypeConverter +import junit.framework.TestCase.assertEquals +import org.junit.Test +import java.time.Instant +import java.util.Date + +class SafeDateTypeConverterTest { + + @Test + fun `creates date from non-null long`() { + val l = 125542352L + val expected = Date(l) + val actual = SafeDateTypeConverter().toDate(l) + assertEquals(expected, actual) + } + + @Test + fun `creates date from null long`() { + val expected = Date(Instant.EPOCH.toEpochMilli()) + val actual = SafeDateTypeConverter().toDate(null) + assertEquals(expected, actual) + } + + @Test + fun `creates long from non-null date`() { + val expected = 125542352L + val d = Date(expected) + val actual = SafeDateTypeConverter().toLong(d) + assertEquals(expected, actual) + } + + @Test + fun `creates long from null date`() { + val actual = SafeDateTypeConverter().toLong(null) + assertEquals(0L, actual) + } +}