From f97ee286cb3763c23e05f34c4666731ee02373b9 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:30:49 +0200 Subject: [PATCH] DROID-2450 Membership | Analytics (#1284) --- .../anytype/analytics/props/UserProperty.kt | 11 ++ .../analytics/tracker/AmplitudeTracker.kt | 5 + .../anytype/di/feature/MainEntryDI.kt | 7 +- .../anytype/di/main/MembershipModule.kt | 7 +- .../anytype/domain/misc/DateProvider.kt | 4 + .../payments/DefaultCoroutineTestRule.kt | 29 ++++ .../anytype/payments/StubMembership.kt | 24 +++ .../provider/MembershipProviderTest.kt | 142 ++++++++++++++++++ .../presentation/main/MainViewModel.kt | 12 +- .../presentation/main/MainViewModelFactory.kt | 7 +- .../membership/provider/MembershipProvider.kt | 43 +++++- .../widgets/collection/DateProviderImpl.kt | 15 ++ 12 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 payments/src/test/java/com/anytypeio/anytype/payments/DefaultCoroutineTestRule.kt create mode 100644 payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt diff --git a/analytics/src/main/java/com/anytypeio/anytype/analytics/props/UserProperty.kt b/analytics/src/main/java/com/anytypeio/anytype/analytics/props/UserProperty.kt index 514a5cb64a..348c6af763 100644 --- a/analytics/src/main/java/com/anytypeio/anytype/analytics/props/UserProperty.kt +++ b/analytics/src/main/java/com/anytypeio/anytype/analytics/props/UserProperty.kt @@ -3,8 +3,19 @@ package com.anytypeio.anytype.analytics.props sealed class UserProperty { data class AccountId(val id: String?) : UserProperty() data class InterfaceLanguage(val lang: String) : UserProperty() + data class Tier(val tierId: Int) : UserProperty() { + val tier: String + get() = when (tierId) { + 0 -> "None" + 1 -> "Explorer" + 4 -> "Builder" + 5 -> "Co-Creator" + else -> "Custom" + } + } companion object { const val INTERFACE_LANG_KEY = "interfaceLang" + const val TIER_KEY = "tier" } } \ No newline at end of file diff --git a/analytics/src/main/java/com/anytypeio/anytype/analytics/tracker/AmplitudeTracker.kt b/analytics/src/main/java/com/anytypeio/anytype/analytics/tracker/AmplitudeTracker.kt index 74d40b1c0e..cedcf10c61 100644 --- a/analytics/src/main/java/com/anytypeio/anytype/analytics/tracker/AmplitudeTracker.kt +++ b/analytics/src/main/java/com/anytypeio/anytype/analytics/tracker/AmplitudeTracker.kt @@ -63,6 +63,11 @@ class AmplitudeTracker( JSONObject(mapOf(UserProperty.INTERFACE_LANG_KEY to prop.lang)) ) } + is UserProperty.Tier -> { + tracker.setUserProperties( + JSONObject(mapOf(UserProperty.TIER_KEY to prop.tier)) + ) + } } } } diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt index 0fb537dd4c..4a6b1aa1e4 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt @@ -26,6 +26,7 @@ import com.anytypeio.anytype.domain.wallpaper.RestoreWallpaper import com.anytypeio.anytype.domain.wallpaper.WallpaperStore import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.main.MainViewModelFactory +import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate import com.anytypeio.anytype.presentation.notifications.NotificationActionDelegate import com.anytypeio.anytype.presentation.notifications.NotificationsProvider @@ -74,7 +75,8 @@ object MainEntryModule { notificator: SystemNotificationService, notificationActionDelegate: NotificationActionDelegate, deepLinkToObjectDelegate: DeepLinkToObjectDelegate, - awaitAccountStartManager: AwaitAccountStartManager + awaitAccountStartManager: AwaitAccountStartManager, + membershipProvider: MembershipProvider ): MainViewModelFactory = MainViewModelFactory( resumeAccount = resumeAccount, analytics = analytics, @@ -93,7 +95,8 @@ object MainEntryModule { notificator = notificator, notificationActionDelegate = notificationActionDelegate, deepLinkToObjectDelegate = deepLinkToObjectDelegate, - awaitAccountStartManager = awaitAccountStartManager + awaitAccountStartManager = awaitAccountStartManager, + membershipProvider = membershipProvider ) @JvmStatic diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/MembershipModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/MembershipModule.kt index 3806d0b1e7..f023be0da8 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/MembershipModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/MembershipModule.kt @@ -6,6 +6,7 @@ import com.anytypeio.anytype.data.auth.event.MembershipRemoteChannel import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.LocaleProvider import com.anytypeio.anytype.domain.workspace.MembershipChannel import com.anytypeio.anytype.middleware.EventProxy @@ -61,12 +62,14 @@ object MembershipModule { awaitAccountStartManager: AwaitAccountStartManager, membershipChannel: MembershipChannel, localeProvider: LocaleProvider, - repo: BlockRepository + repo: BlockRepository, + dateProvider: DateProvider ): MembershipProvider = MembershipProvider.Default( dispatchers = dispatchers, membershipChannel = membershipChannel, awaitAccountStartManager = awaitAccountStartManager, localeProvider = localeProvider, - repo = repo + repo = repo, + dateProvider = dateProvider ) } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt b/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt index 3dee2bf191..6b82e39e78 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt @@ -2,6 +2,9 @@ package com.anytypeio.anytype.domain.misc import com.anytypeio.anytype.core_models.TimeInMillis import com.anytypeio.anytype.core_models.TimeInSeconds +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale /** @@ -18,6 +21,7 @@ interface DateProvider { fun getTimestampForWeekAgoAtStartOfDay(): TimeInSeconds fun adjustToStartOfDayInUserTimeZone(timestamp: TimeInSeconds): TimeInMillis fun adjustFromStartOfDayInUserTimeZoneToUTC(timestamp: TimeInMillis): TimeInSeconds + fun formatToDateString(timestamp: Long, pattern: String, locale: Locale): String } interface DateTypeNameProvider { diff --git a/payments/src/test/java/com/anytypeio/anytype/payments/DefaultCoroutineTestRule.kt b/payments/src/test/java/com/anytypeio/anytype/payments/DefaultCoroutineTestRule.kt new file mode 100644 index 0000000000..6656bdb3ac --- /dev/null +++ b/payments/src/test/java/com/anytypeio/anytype/payments/DefaultCoroutineTestRule.kt @@ -0,0 +1,29 @@ +package com.anytypeio.anytype.payments + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultCoroutineTestRule() : TestWatcher() { + + val dispatcher = StandardTestDispatcher(name = "Default test dispatcher") + + override fun starting(description: Description) { + super.starting(description) + Dispatchers.setMain(dispatcher) + } + + override fun finished(description: Description) { + super.finished(description) + Dispatchers.resetMain() + } + + fun advanceTime(millis: Long = 100L) { + dispatcher.scheduler.advanceTimeBy(millis) + } +} \ No newline at end of file diff --git a/payments/src/test/java/com/anytypeio/anytype/payments/StubMembership.kt b/payments/src/test/java/com/anytypeio/anytype/payments/StubMembership.kt index 4658da7ce8..4b188ebc41 100644 --- a/payments/src/test/java/com/anytypeio/anytype/payments/StubMembership.kt +++ b/payments/src/test/java/com/anytypeio/anytype/payments/StubMembership.kt @@ -1,10 +1,34 @@ package com.anytypeio.anytype.payments +import com.anytypeio.anytype.core_models.membership.Membership +import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod import com.anytypeio.anytype.core_models.membership.MembershipPeriodType import com.anytypeio.anytype.core_models.membership.MembershipTierData +import com.anytypeio.anytype.core_models.membership.NameServiceNameType import kotlin.random.Random import net.bytebuddy.utility.RandomString +fun StubMembership( + tier: Int = Random.nextInt(), + status: Membership.Status = Membership.Status.STATUS_ACTIVE, + dateEnds: Long = 432331231L, + paymentMethod: MembershipPaymentMethod = MembershipPaymentMethod.METHOD_CRYPTO, + anyName: String = "anyName-${RandomString.make()}", + dateStarted: Long = Random.nextLong() +): Membership { + return Membership( + tier = tier, + membershipStatusModel = status, + dateEnds = dateEnds, + paymentMethod = paymentMethod, + userEmail = "", + nameServiceName = anyName, + nameServiceType = NameServiceNameType.ANY_NAME, + subscribeToNewsletter = false, + isAutoRenew = false, + dateStarted = dateStarted + ) +} fun StubMembershipTierData( id: Int = Random.nextInt(), diff --git a/payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt b/payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt new file mode 100644 index 0000000000..2856dda108 --- /dev/null +++ b/payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt @@ -0,0 +1,142 @@ +package com.anytypeio.anytype.payments.provider + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import app.cash.turbine.turbineScope +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.membership.Membership +import com.anytypeio.anytype.domain.account.AwaitAccountStartManager +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.misc.DateProvider +import com.anytypeio.anytype.domain.misc.LocaleProvider +import com.anytypeio.anytype.domain.workspace.MembershipChannel +import com.anytypeio.anytype.payments.DefaultCoroutineTestRule +import com.anytypeio.anytype.payments.StubMembership +import com.anytypeio.anytype.payments.StubMembershipTierData +import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider +import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider.Default.Companion.DATE_FORMAT +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class MembershipProviderTest { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val coroutineTestRule = DefaultCoroutineTestRule() + + private val dispatchers = AppCoroutineDispatchers( + io = coroutineTestRule.dispatcher, + main = coroutineTestRule.dispatcher, + computation = coroutineTestRule.dispatcher + ) + + @Mock + lateinit var localeProvider: LocaleProvider + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var dateProvider: DateProvider + + @Mock + lateinit var membershipChannel: MembershipChannel + + private val awaitAccountStartManager: AwaitAccountStartManager = + AwaitAccountStartManager.Default + + private lateinit var provider: MembershipProvider + + @Before + fun before() { + MockitoAnnotations.openMocks(this) + provider = MembershipProvider.Default( + dispatchers = dispatchers, + membershipChannel = membershipChannel, + awaitAccountStartManager = awaitAccountStartManager, + localeProvider = localeProvider, + repo = repo, + dateProvider = dateProvider + ) + } + + @Test + fun `test membership status and tiers fetched successfully plus updated after event`() = + runTest { + turbineScope { + + val dateEnds = 432331231L + val membership = StubMembership(dateEnds = dateEnds) + val tierData = StubMembershipTierData() + val tierData2 = StubMembershipTierData(isTest = true) + + val event1 = Membership.Event(StubMembership(dateEnds = dateEnds)) + val event2 = Membership.Event(StubMembership(dateEnds = dateEnds)) + val eventList = flow { + emit(listOf(event1)) + emit(listOf(event2)) + } + + membershipChannel.stub { + on { observe() } doReturn eventList + } + whenever(repo.membershipStatus(any())).thenReturn(membership) + whenever( + dateProvider.formatToDateString( + dateEnds, + DATE_FORMAT, + localeProvider.locale() + ) + ).thenReturn("01-01-1970") + val command = Command.Membership.GetTiers( + noCache = true, + locale = "en" + ) + repo.stub { + onBlocking { membershipGetTiers(command) } doReturn listOf(tierData2, tierData) + + } + whenever(localeProvider.language()).thenReturn("en") + + awaitAccountStartManager.setIsStarted(true) + val membershipProviderFlow = provider.status().testIn(backgroundScope) + val membershipProviderFlow1 = provider.status().testIn(backgroundScope) + val membershipProviderFlow2 = provider.activeTier().testIn(backgroundScope) + + val initialStatus = membershipProviderFlow.awaitItem() + assertEquals(membership.tier, initialStatus.activeTier.value) + assertEquals(tierData.id, initialStatus.tiers.first().id) + + val status1 = membershipProviderFlow.awaitItem() + assertEquals(event1.membership.tier, status1.activeTier.value) + assertEquals(tierData.id, status1.tiers.first().id) + + val status2 = membershipProviderFlow.awaitItem() + assertEquals(event2.membership.tier, status2.activeTier.value) + assertEquals(tierData.id, status2.tiers.first().id) + + membershipProviderFlow1.awaitItem() + membershipProviderFlow1.awaitItem() + membershipProviderFlow1.awaitItem() + membershipProviderFlow2.awaitItem() + membershipProviderFlow2.awaitItem() + membershipProviderFlow2.awaitItem() + + verify(repo, times(6)).membershipGetTiers(command) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt index e40c351f4c..833f01fbdb 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt @@ -32,6 +32,7 @@ import com.anytypeio.anytype.domain.wallpaper.ObserveWallpaper import com.anytypeio.anytype.domain.wallpaper.RestoreWallpaper import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.home.navigation +import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate import com.anytypeio.anytype.presentation.notifications.NotificationAction import com.anytypeio.anytype.presentation.notifications.NotificationActionDelegate @@ -66,7 +67,8 @@ class MainViewModel( private val notificator: SystemNotificationService, private val notificationActionDelegate: NotificationActionDelegate, private val deepLinkToObjectDelegate: DeepLinkToObjectDelegate, - private val awaitAccountStartManager: AwaitAccountStartManager + private val awaitAccountStartManager: AwaitAccountStartManager, + private val membershipProvider: MembershipProvider ) : ViewModel(), NotificationActionDelegate by notificationActionDelegate, DeepLinkToObjectDelegate by deepLinkToObjectDelegate { @@ -114,6 +116,14 @@ class MainViewModel( } } } + viewModelScope.launch { + membershipProvider.activeTier().collect { result -> + updateUserProperties( + analytics = analytics, + userProperty = UserProperty.Tier(result.value) + ) + } + } } private suspend fun handleNotification(event: Notification.Event) { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt index 89f2c7444f..d92ea9a3b1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModelFactory.kt @@ -17,6 +17,7 @@ import com.anytypeio.anytype.domain.search.RelationsSubscriptionManager import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher import com.anytypeio.anytype.domain.wallpaper.ObserveWallpaper import com.anytypeio.anytype.domain.wallpaper.RestoreWallpaper +import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate import com.anytypeio.anytype.presentation.notifications.NotificationActionDelegate import com.anytypeio.anytype.presentation.notifications.NotificationsProvider @@ -40,7 +41,8 @@ class MainViewModelFactory @Inject constructor( private val notificator: SystemNotificationService, private val notificationActionDelegate: NotificationActionDelegate, private val deepLinkToObjectDelegate: DeepLinkToObjectDelegate, - private val awaitAccountStartManager: AwaitAccountStartManager + private val awaitAccountStartManager: AwaitAccountStartManager, + private val membershipProvider: MembershipProvider ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -63,6 +65,7 @@ class MainViewModelFactory @Inject constructor( notificator = notificator, notificationActionDelegate = notificationActionDelegate, deepLinkToObjectDelegate = deepLinkToObjectDelegate, - awaitAccountStartManager = awaitAccountStartManager + awaitAccountStartManager = awaitAccountStartManager, + membershipProvider = membershipProvider ) as T } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt index 94002fbab9..1971a5abbb 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt @@ -3,10 +3,10 @@ package com.anytypeio.anytype.presentation.membership.provider import com.anytypeio.anytype.core_models.Command import com.anytypeio.anytype.core_models.membership.Membership import com.anytypeio.anytype.core_models.membership.MembershipTierData -import com.anytypeio.anytype.core_utils.ext.formatToDateString import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.LocaleProvider import com.anytypeio.anytype.domain.workspace.MembershipChannel import com.anytypeio.anytype.presentation.membership.models.MembershipStatus @@ -25,13 +25,15 @@ import timber.log.Timber interface MembershipProvider { fun status(): Flow + fun activeTier(): Flow class Default( private val dispatchers: AppCoroutineDispatchers, private val membershipChannel: MembershipChannel, private val awaitAccountStartManager: AwaitAccountStartManager, private val localeProvider: LocaleProvider, - private val repo: BlockRepository + private val repo: BlockRepository, + private val dateProvider: DateProvider ) : MembershipProvider { @OptIn(ExperimentalCoroutinesApi::class) @@ -48,6 +50,33 @@ interface MembershipProvider { .flowOn(dispatchers.io) } + @OptIn(ExperimentalCoroutinesApi::class) + override fun activeTier(): Flow { + return awaitAccountStartManager.isStarted().flatMapLatest { isStarted -> + if (isStarted) { + buildActiveTierFlow( + initial = proceedWithGettingMembership() + ) + } else { + emptyFlow() + } + }.catch { e -> Timber.e(e) } + .flowOn(dispatchers.io) + } + + private fun buildActiveTierFlow( + initial: Membership? + ): Flow { + return membershipChannel + .observe() + .scan(initial) { _, events -> + events.lastOrNull()?.membership + }.filterNotNull() + .map { membership -> + TierId(membership.tier) + } + } + private fun buildStatusFlow( initial: Membership? ): Flow { @@ -86,6 +115,11 @@ interface MembershipProvider { membership: Membership, tiers: List ): MembershipStatus { + val formattedDateEnds = dateProvider.formatToDateString( + timestamp = membership.dateEnds, + pattern = DATE_FORMAT, + locale = localeProvider.locale() + ) return MembershipStatus( activeTier = TierId(membership.tier), status = membership.membershipStatusModel, @@ -93,10 +127,7 @@ interface MembershipProvider { paymentMethod = membership.paymentMethod, anyName = membership.nameServiceName, tiers = tiers, - formattedDateEnds = membership.dateEnds.formatToDateString( - pattern = DATE_FORMAT, - locale = localeProvider.locale() - ), + formattedDateEnds = formattedDateEnds, userEmail = membership.userEmail ) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/DateProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/DateProviderImpl.kt index eea0885206..e5f1f923c3 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/DateProviderImpl.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/DateProviderImpl.kt @@ -3,15 +3,20 @@ package com.anytypeio.anytype.presentation.widgets.collection import android.text.format.DateUtils import com.anytypeio.anytype.core_models.TimeInMillis import com.anytypeio.anytype.core_models.TimeInSeconds +import com.anytypeio.anytype.core_utils.ext.formatToDateString import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.DateType +import java.text.SimpleDateFormat import java.time.Instant import java.time.LocalDate import java.time.ZoneId import java.time.ZoneOffset import java.util.Calendar +import java.util.Date +import java.util.Locale import java.util.TimeZone import javax.inject.Inject +import timber.log.Timber class DateProviderImpl @Inject constructor() : DateProvider { @@ -112,6 +117,16 @@ class DateProviderImpl @Inject constructor() : DateProvider { // Convert the local start of the day back to seconds return calendarLocal.timeInMillis / 1000 } + + override fun formatToDateString(timestamp: Long, pattern: String, locale: Locale): String { + try { + val formatter = SimpleDateFormat(pattern, locale) + return formatter.format(Date(timestamp)) + } catch (e: Exception) { + Timber.e(e,"Error formatting timestamp to date string") + return "" + } + } }