From e3732d641edc7b5d3511006a2c3523adbbf67701 Mon Sep 17 00:00:00 2001 From: Tyler Lopez <77797048+Tyler-Lopez@users.noreply.github.com> Date: Fri, 17 Feb 2023 23:32:00 -0500 Subject: [PATCH] Change methodology of fetching activities from year based queries to querying up to first 10 pages (#28) * This commit addresses a possible concern that Strava's backend is failing when `before` and `after` properties are specified by migrating the code to instead query up to the first 10 pages of activities. --- app/build.gradle | 4 +- .../com/activityartapp/data/Converters.kt | 20 --- .../data/cache/ActivitiesCache.kt | 6 +- .../activityartapp/data/dao/ActivityDao.kt | 14 ++ .../data/dao/AthleteCacheDictionaryDao.kt | 18 --- .../data/dao/{OAuth2Dao.kt => AthleteDao.kt} | 14 +- .../data/database/AthleteDatabase.kt | 16 +- .../entities/AthleteCacheDictionaryEntity.kt | 13 -- .../{OAuth2Entity.kt => AthleteEntity.kt} | 6 +- .../activityartapp/data/remote/AthleteApi.kt | 2 - .../data/remote/responses/ActivityResponse.kt | 2 +- .../data/remote/responses/AthleteResponse.kt | 5 +- .../responses/AthleteWithResourceState.kt | 8 - .../data/remote/responses/Bearer.kt | 5 +- .../remote/responses/BearerWithoutAthlete.kt | 4 +- .../data/remote/responses/Segment.kt | 22 --- .../java/com/activityartapp/di/AppModule.kt | 65 +------- .../activityartapp/domain/models/Activity.kt | 1 - .../activityartapp/domain/models/Athlete.kt | 15 +- .../domain/models/AthleteCacheDictionary.kt | 18 --- .../activityartapp/domain/models/OAuth2.kt | 1 - .../activityartapp/domain/models/OAuth2Ext.kt | 6 +- .../domain/models/OAuth2WithoutAthlete.kt | 7 - .../GetActivitiesByPageFromRemote.kt | 18 +-- .../GetActivitiesByYearFromDiskOrRemote.kt | 143 ------------------ .../GetActivitiesByYearFromMemory.kt | 10 -- .../GetActivitiesByYearFromRemote.kt | 84 ---------- .../GetActivitiesByYearMonthFromDisk.kt | 39 ----- ...arFromDisk.kt => GetActivitiesFromDisk.kt} | 6 +- .../GetActivitiesFromDiskAndRemote.kt | 114 ++++++++++++++ .../activities/GetActivitiesFromMemory.kt | 11 +- .../activities/InsertActivitiesIntoDisk.kt | 39 +---- .../activities/InsertActivitiesIntoMemory.kt | 8 +- .../GetAthleteCacheDictionaryFromDisk.kt | 15 -- .../InsertAthleteCacheDictionaryIntoDisk.kt | 19 --- ...kenFromDisk.kt => ClearAthleteFromDisk.kt} | 12 +- .../authentication/GetAccessTokenFromDisk.kt | 15 -- ...essTokenWithAuthorizationCodeFromRemote.kt | 14 +- .../authentication/GetAthleteFromDisk.kt | 15 ++ ...emote.kt => GetAthleteFromDiskOrRemote.kt} | 15 +- ...> GetAthleteWithTokenRefreshFromRemote.kt} | 20 +-- .../InsertAccessTokenIntoDisk.kt | 23 --- .../authentication/InsertAthleteIntoDisk.kt | 24 +++ .../presentation/MainViewModel.kt | 33 ++-- .../editArtScreen/EditArtViewModel.kt | 2 +- .../LoadActivitiesViewModel.kt | 54 +++---- .../saveArtScreen/SaveArtViewModel.kt | 2 +- .../welcomeScreen/WelcomeViewModel.kt | 7 +- 48 files changed, 302 insertions(+), 712 deletions(-) delete mode 100644 app/src/main/java/com/activityartapp/data/Converters.kt delete mode 100644 app/src/main/java/com/activityartapp/data/dao/AthleteCacheDictionaryDao.kt rename app/src/main/java/com/activityartapp/data/dao/{OAuth2Dao.kt => AthleteDao.kt} (50%) delete mode 100644 app/src/main/java/com/activityartapp/data/entities/AthleteCacheDictionaryEntity.kt rename app/src/main/java/com/activityartapp/data/entities/{OAuth2Entity.kt => AthleteEntity.kt} (71%) delete mode 100644 app/src/main/java/com/activityartapp/data/remote/responses/AthleteWithResourceState.kt delete mode 100644 app/src/main/java/com/activityartapp/data/remote/responses/Segment.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/models/AthleteCacheDictionary.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/models/OAuth2WithoutAthlete.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromDiskOrRemote.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromMemory.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromRemote.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearMonthFromDisk.kt rename app/src/main/java/com/activityartapp/domain/useCase/activities/{GetActivitiesByYearFromDisk.kt => GetActivitiesFromDisk.kt} (62%) create mode 100644 app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromDiskAndRemote.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/GetAthleteCacheDictionaryFromDisk.kt delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/InsertAthleteCacheDictionaryIntoDisk.kt rename app/src/main/java/com/activityartapp/domain/useCase/authentication/{ClearAccessTokenFromDisk.kt => ClearAthleteFromDisk.kt} (71%) delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenFromDisk.kt create mode 100644 app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteFromDisk.kt rename app/src/main/java/com/activityartapp/domain/useCase/authentication/{GetAccessTokenFromDiskOrRemote.kt => GetAthleteFromDiskOrRemote.kt} (70%) rename app/src/main/java/com/activityartapp/domain/useCase/authentication/{GetAccessTokenWithRefreshTokenFromRemote.kt => GetAthleteWithTokenRefreshFromRemote.kt} (78%) delete mode 100644 app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAccessTokenIntoDisk.kt create mode 100644 app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAthleteIntoDisk.kt diff --git a/app/build.gradle b/app/build.gradle index 9b57d166..d908f81f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "com.activityartapp" minSdk 26 targetSdk 33 - versionCode 11 - versionName "1.3.1" + versionCode 12 + versionName "1.3.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/activityartapp/data/Converters.kt b/app/src/main/java/com/activityartapp/data/Converters.kt deleted file mode 100644 index 4a6a7576..00000000 --- a/app/src/main/java/com/activityartapp/data/Converters.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.activityartapp.data - -import androidx.room.TypeConverter -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken - -class Converters { - @TypeConverter - fun stringToMap(value: String): Map { - return Gson().fromJson( - value, - object : TypeToken>() {}.type - ) - } - - @TypeConverter - fun mapToString(value: Map?): String { - return if (value == null) "" else Gson().toJson(value) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/cache/ActivitiesCache.kt b/app/src/main/java/com/activityartapp/data/cache/ActivitiesCache.kt index 5520a158..94dbfe74 100644 --- a/app/src/main/java/com/activityartapp/data/cache/ActivitiesCache.kt +++ b/app/src/main/java/com/activityartapp/data/cache/ActivitiesCache.kt @@ -2,10 +2,6 @@ package com.activityartapp.data.cache import com.activityartapp.domain.models.Activity -/** - * Global Singleton cache data-layer which is populated during the first data load and - * then never again. - */ class ActivitiesCache { - val cachedActivitiesByYear: MutableMap> = mutableMapOf() + var cachedActivities: List? = null } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/dao/ActivityDao.kt b/app/src/main/java/com/activityartapp/data/dao/ActivityDao.kt index eefca9a4..64be7947 100644 --- a/app/src/main/java/com/activityartapp/data/dao/ActivityDao.kt +++ b/app/src/main/java/com/activityartapp/data/dao/ActivityDao.kt @@ -12,6 +12,20 @@ interface ActivityDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAllActivities(vararg activityEntity: ActivityEntity) + + /** + * Retrieves all activities which match the year provided. + * Functions by pattern recognition of the iso8601 String. + */ + @Query( + "SELECT * " + + "FROM activityEntity " + + "WHERE athleteId = :athleteId " + + "AND summaryPolyline IS NOT NULL" + ) + suspend fun getActivities(athleteId: Long): List + + /** * Retrieves all activities which match the year provided. * Functions by pattern recognition of the iso8601 String. diff --git a/app/src/main/java/com/activityartapp/data/dao/AthleteCacheDictionaryDao.kt b/app/src/main/java/com/activityartapp/data/dao/AthleteCacheDictionaryDao.kt deleted file mode 100644 index ecc18d9e..00000000 --- a/app/src/main/java/com/activityartapp/data/dao/AthleteCacheDictionaryDao.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.activityartapp.data.dao - -import androidx.room.* -import com.activityartapp.data.entities.AthleteCacheDictionaryEntity - -@Dao -interface AthleteCacheDictionaryDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAthlete(athleteEntity: AthleteCacheDictionaryEntity) - - // https://stackoverflow.com/questions/44244508/room-persistance-library-delete-all - @Query("DELETE FROM athleteCacheDictionaryEntity") - suspend fun clearAthleteCacheDictionary() - - @Query("SELECT * FROM athleteCacheDictionaryEntity WHERE athleteCacheDictionaryEntity.id = :athleteId") - suspend fun getAthleteCacheDictionaryById(athleteId: Long): AthleteCacheDictionaryEntity? -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/dao/OAuth2Dao.kt b/app/src/main/java/com/activityartapp/data/dao/AthleteDao.kt similarity index 50% rename from app/src/main/java/com/activityartapp/data/dao/OAuth2Dao.kt rename to app/src/main/java/com/activityartapp/data/dao/AthleteDao.kt index cfd94616..bd9d8c81 100644 --- a/app/src/main/java/com/activityartapp/data/dao/OAuth2Dao.kt +++ b/app/src/main/java/com/activityartapp/data/dao/AthleteDao.kt @@ -4,18 +4,18 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import com.activityartapp.data.entities.OAuth2Entity +import com.activityartapp.data.entities.AthleteEntity @Dao -interface OAuth2Dao { +interface AthleteDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertOauth2(oAuth2Entity: OAuth2Entity) + suspend fun insertAthlete(AthleteEntity: AthleteEntity) // https://stackoverflow.com/questions/44244508/room-persistance-library-delete-all - @Query("DELETE FROM oAuth2Entity") - suspend fun clearOauth2() + @Query("DELETE FROM athleteEntity") + suspend fun clearAthlete() - @Query("SELECT * FROM oAuth2Entity") - suspend fun getCurrAuth(): OAuth2Entity? + @Query("SELECT * FROM athleteEntity") + suspend fun getCurrAthlete(): AthleteEntity? } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/database/AthleteDatabase.kt b/app/src/main/java/com/activityartapp/data/database/AthleteDatabase.kt index f6204a5e..25836d5a 100644 --- a/app/src/main/java/com/activityartapp/data/database/AthleteDatabase.kt +++ b/app/src/main/java/com/activityartapp/data/database/AthleteDatabase.kt @@ -2,14 +2,10 @@ package com.activityartapp.data.database import androidx.room.Database import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import com.activityartapp.data.Converters import com.activityartapp.data.dao.ActivityDao -import com.activityartapp.data.dao.AthleteCacheDictionaryDao -import com.activityartapp.data.dao.OAuth2Dao +import com.activityartapp.data.dao.AthleteDao import com.activityartapp.data.entities.ActivityEntity -import com.activityartapp.data.entities.AthleteCacheDictionaryEntity -import com.activityartapp.data.entities.OAuth2Entity +import com.activityartapp.data.entities.AthleteEntity /** * https://svvashishtha.medium.com/using-room-with-hilt-cb57a1bc32f @@ -17,12 +13,10 @@ import com.activityartapp.data.entities.OAuth2Entity */ @Database( - entities = [ActivityEntity::class, AthleteCacheDictionaryEntity::class, OAuth2Entity::class], - version = 3 + entities = [ActivityEntity::class, AthleteEntity::class], + version = 5 ) -@TypeConverters(Converters::class) abstract class AthleteDatabase : RoomDatabase() { abstract val activityDao: ActivityDao - abstract val athleteCacheDictionaryDao: AthleteCacheDictionaryDao - abstract val oAuth2Dao: OAuth2Dao + abstract val athleteDao: AthleteDao } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/entities/AthleteCacheDictionaryEntity.kt b/app/src/main/java/com/activityartapp/data/entities/AthleteCacheDictionaryEntity.kt deleted file mode 100644 index 82ee3649..00000000 --- a/app/src/main/java/com/activityartapp/data/entities/AthleteCacheDictionaryEntity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.activityartapp.data.entities - -import androidx.room.Entity -import androidx.room.PrimaryKey -import androidx.room.TypeConverters -import com.activityartapp.data.Converters -import com.activityartapp.domain.models.AthleteCacheDictionary - -@Entity -data class AthleteCacheDictionaryEntity( - @PrimaryKey override val id: Long, - @TypeConverters(Converters::class) override val lastCachedYearMonth: Map -) : AthleteCacheDictionary \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/entities/OAuth2Entity.kt b/app/src/main/java/com/activityartapp/data/entities/AthleteEntity.kt similarity index 71% rename from app/src/main/java/com/activityartapp/data/entities/OAuth2Entity.kt rename to app/src/main/java/com/activityartapp/data/entities/AthleteEntity.kt index 115b124b..7368e8b4 100644 --- a/app/src/main/java/com/activityartapp/data/entities/OAuth2Entity.kt +++ b/app/src/main/java/com/activityartapp/data/entities/AthleteEntity.kt @@ -2,13 +2,15 @@ package com.activityartapp.data.entities import androidx.room.Entity import androidx.room.PrimaryKey +import com.activityartapp.domain.models.Athlete import com.activityartapp.domain.models.OAuth2 @Entity -data class OAuth2Entity( +data class AthleteEntity( @PrimaryKey override val athleteId: Long, + override val lastCachedUnixMs: Long?, override val expiresAtUnixSeconds: Int, override val accessToken: String, override val refreshToken: String -) : OAuth2 +) : Athlete diff --git a/app/src/main/java/com/activityartapp/data/remote/AthleteApi.kt b/app/src/main/java/com/activityartapp/data/remote/AthleteApi.kt index 37e2fcc0..53b31692 100644 --- a/app/src/main/java/com/activityartapp/data/remote/AthleteApi.kt +++ b/app/src/main/java/com/activityartapp/data/remote/AthleteApi.kt @@ -24,8 +24,6 @@ interface AthleteApi { @GET("api/v3/athlete/activities?") suspend fun getActivities( @Header("Authorization") authHeader: String, - @Query("before") before: Int, - @Query("after") after: Int, @Query("page") page: Int, @Query("per_page") perPage: Int ): Activities diff --git a/app/src/main/java/com/activityartapp/data/remote/responses/ActivityResponse.kt b/app/src/main/java/com/activityartapp/data/remote/responses/ActivityResponse.kt index 23751ee4..e79e2b51 100644 --- a/app/src/main/java/com/activityartapp/data/remote/responses/ActivityResponse.kt +++ b/app/src/main/java/com/activityartapp/data/remote/responses/ActivityResponse.kt @@ -33,7 +33,7 @@ data class ActivityResponse( override val iso8601LocalDate: String, @SerializedName("suffer_score") override val sufferScore: Int?, - val athlete: AthleteWithResourceState, + val athlete: AthleteResponse, val map: Map? ) : Activity { override val athleteId: Long diff --git a/app/src/main/java/com/activityartapp/data/remote/responses/AthleteResponse.kt b/app/src/main/java/com/activityartapp/data/remote/responses/AthleteResponse.kt index f57370cc..1c21b288 100644 --- a/app/src/main/java/com/activityartapp/data/remote/responses/AthleteResponse.kt +++ b/app/src/main/java/com/activityartapp/data/remote/responses/AthleteResponse.kt @@ -1,9 +1,8 @@ package com.activityartapp.data.remote.responses -import com.activityartapp.domain.models.Athlete import com.google.gson.annotations.SerializedName data class AthleteResponse( @SerializedName("id") - override val id: Long, -) : Athlete \ No newline at end of file + val id: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/remote/responses/AthleteWithResourceState.kt b/app/src/main/java/com/activityartapp/data/remote/responses/AthleteWithResourceState.kt deleted file mode 100644 index 91532b08..00000000 --- a/app/src/main/java/com/activityartapp/data/remote/responses/AthleteWithResourceState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.activityartapp.data.remote.responses - -import com.activityartapp.domain.models.Athlete - -data class AthleteWithResourceState( - override val id: Long, - val resource_state: Int -) : Athlete \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/data/remote/responses/Bearer.kt b/app/src/main/java/com/activityartapp/data/remote/responses/Bearer.kt index fea219b2..4f77c769 100644 --- a/app/src/main/java/com/activityartapp/data/remote/responses/Bearer.kt +++ b/app/src/main/java/com/activityartapp/data/remote/responses/Bearer.kt @@ -14,7 +14,4 @@ data class Bearer( val athlete: AthleteResponse, val expires_in: Int, val token_type: String -) : OAuth2 { - override val athleteId: Long - get() = athlete.id -} +) : OAuth2 diff --git a/app/src/main/java/com/activityartapp/data/remote/responses/BearerWithoutAthlete.kt b/app/src/main/java/com/activityartapp/data/remote/responses/BearerWithoutAthlete.kt index 9fcfb3ab..2ad42a7d 100644 --- a/app/src/main/java/com/activityartapp/data/remote/responses/BearerWithoutAthlete.kt +++ b/app/src/main/java/com/activityartapp/data/remote/responses/BearerWithoutAthlete.kt @@ -1,6 +1,6 @@ package com.activityartapp.data.remote.responses -import com.activityartapp.domain.models.OAuth2WithoutAthlete +import com.activityartapp.domain.models.OAuth2 import com.google.gson.annotations.SerializedName data class BearerWithoutAthlete( @@ -12,4 +12,4 @@ data class BearerWithoutAthlete( override val refreshToken: String, val expires_in: Int, val token_type: String -) : OAuth2WithoutAthlete +) : OAuth2 diff --git a/app/src/main/java/com/activityartapp/data/remote/responses/Segment.kt b/app/src/main/java/com/activityartapp/data/remote/responses/Segment.kt deleted file mode 100644 index 4da1120a..00000000 --- a/app/src/main/java/com/activityartapp/data/remote/responses/Segment.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.activityartapp.data.remote.responses - -data class Segment( - val activity_type: String, - val average_grade: Double, - val city: String, - val climb_category: Int, - val country: String, - val distance: Double, - val elevation_high: Double, - val elevation_low: Double, - val end_latlng: List, - val hazardous: Boolean, - val id: Int, - val maximum_grade: Double, - val name: String, - val `private`: Boolean, - val resource_state: Int, - val starred: Boolean, - val start_latlng: List, - val state: String -) \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/di/AppModule.kt b/app/src/main/java/com/activityartapp/di/AppModule.kt index 6f97b431..f0573f31 100644 --- a/app/src/main/java/com/activityartapp/di/AppModule.kt +++ b/app/src/main/java/com/activityartapp/di/AppModule.kt @@ -8,16 +8,13 @@ import com.activityartapp.data.remote.AthleteApi import com.activityartapp.data.repository.AthleteUsageRepositoryImpl import com.activityartapp.data.repository.FileRepositoryImpl import com.activityartapp.data.repository.VersionRepositoryImpl +import com.activityartapp.domain.models.ResolutionListFactory import com.activityartapp.domain.repository.AthleteUsageRepository import com.activityartapp.domain.repository.FileRepository import com.activityartapp.domain.repository.VersionRepository -import com.activityartapp.domain.models.ResolutionListFactory -import com.activityartapp.domain.useCase.activities.* -import com.activityartapp.domain.useCase.athleteCacheDictionary.GetAthleteCacheDictionaryFromDisk -import com.activityartapp.domain.useCase.athleteCacheDictionary.InsertAthleteCacheDictionaryIntoDisk -import com.activityartapp.domain.useCase.athleteUsage.GetAthleteUsageFromRemote -import com.activityartapp.domain.useCase.athleteUsage.InsertAthleteUsageIntoRemote -import com.activityartapp.domain.useCase.authentication.ClearAccessTokenFromDisk +import com.activityartapp.domain.useCase.activities.GetActivitiesByPageFromRemote +import com.activityartapp.domain.useCase.activities.InsertActivitiesIntoMemory +import com.activityartapp.domain.useCase.authentication.ClearAthleteFromDisk import com.activityartapp.presentation.editArtScreen.subscreens.resize.ResolutionListFactoryImpl import com.activityartapp.util.* import com.activityartapp.util.constants.TokenConstants @@ -51,74 +48,22 @@ object AppModule { @Provides fun provideActivitiesCache() = ActivitiesCache() - @Provides - fun providesGetAthleteFromLocalUseCase(athleteDatabase: AthleteDatabase) = - GetAthleteCacheDictionaryFromDisk(athleteDatabase) - @Provides fun providesGetActivitiesByPageFromRemoteUseCase( api: AthleteApi ): GetActivitiesByPageFromRemote = GetActivitiesByPageFromRemote(api) - @Provides - fun providesGetActivitiesByYearFromRemoteUseCase( - getActivitiesByPageFromRemote: GetActivitiesByPageFromRemote, - getAthleteUsageFromRemote: GetAthleteUsageFromRemote, - insertAthleteUsageIntoRemote: InsertAthleteUsageIntoRemote, - timeUtils: TimeUtils - ) = GetActivitiesByYearFromRemote( - getActivitiesByPageFromRemote, - getAthleteUsageFromRemote, - insertAthleteUsageIntoRemote, - timeUtils - ) - - @Provides - fun providesGetActivitiesByYearFromLocalUseCase( - athleteDatabase: AthleteDatabase - ) = GetActivitiesByYearFromDisk(athleteDatabase) - - @Provides - fun providesGetActivitiesByYearMonthFromLocalUseCase( - athleteDatabase: AthleteDatabase - ) = GetActivitiesByYearMonthFromDisk(athleteDatabase) - - @Provides - fun providesGetActivitiesFromCacheUseCase(cache: ActivitiesCache) = - GetActivitiesByYearFromMemory(cache) - @Provides fun providesInsertActivitiesFromCacheUseCase(cache: ActivitiesCache) = InsertActivitiesIntoMemory(cache) - @Provides - fun providesGetActivitiesByYear( - getAthleteCacheDictionaryFromDisk: GetAthleteCacheDictionaryFromDisk, - getActivitiesByYearMonthFromDisk: GetActivitiesByYearMonthFromDisk, - getActivitiesByYearFromRemote: GetActivitiesByYearFromRemote, - insertActivitiesIntoDisk: InsertActivitiesIntoDisk, - insertActivitiesIntoMemory: InsertActivitiesIntoMemory, - timeUtils: TimeUtils - ) = GetActivitiesByYearFromDiskOrRemote( - getAthleteCacheDictionaryFromDisk, - getActivitiesByYearMonthFromDisk, - getActivitiesByYearFromRemote, - insertActivitiesIntoDisk, - insertActivitiesIntoMemory, - timeUtils - ) - - @Provides - fun providesInsertAthleteFromRemoteUseCase(athleteDatabase: AthleteDatabase) = - InsertAthleteCacheDictionaryIntoDisk(athleteDatabase) - @Provides fun clearAccessTokenUseCase( athleteDatabase: AthleteDatabase, cache: ActivitiesCache ) = - ClearAccessTokenFromDisk(athleteDatabase, cache) + ClearAthleteFromDisk(athleteDatabase, cache) @Singleton @Provides diff --git a/app/src/main/java/com/activityartapp/domain/models/Activity.kt b/app/src/main/java/com/activityartapp/domain/models/Activity.kt index 6b40f738..ec068c4f 100644 --- a/app/src/main/java/com/activityartapp/domain/models/Activity.kt +++ b/app/src/main/java/com/activityartapp/domain/models/Activity.kt @@ -19,5 +19,4 @@ interface Activity { val sufferScore: Int? val summaryPolyline: String? val sportType: SportType - } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/models/Athlete.kt b/app/src/main/java/com/activityartapp/domain/models/Athlete.kt index 44936563..f72b57cc 100644 --- a/app/src/main/java/com/activityartapp/domain/models/Athlete.kt +++ b/app/src/main/java/com/activityartapp/domain/models/Athlete.kt @@ -1,10 +1,9 @@ package com.activityartapp.domain.models -/** - * [Athlete] is a registered athlete on Strava. - * - * @property id [id] is an athlete's ID, as provided by Strava. - */ -interface Athlete { - val id: Long -} \ No newline at end of file +interface Athlete : OAuth2 { + val athleteId: Long + val lastCachedUnixMs: Long? + override val expiresAtUnixSeconds: Int + override val accessToken: String + override val refreshToken: String +} diff --git a/app/src/main/java/com/activityartapp/domain/models/AthleteCacheDictionary.kt b/app/src/main/java/com/activityartapp/domain/models/AthleteCacheDictionary.kt deleted file mode 100644 index 2d432c39..00000000 --- a/app/src/main/java/com/activityartapp/domain/models/AthleteCacheDictionary.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.activityartapp.domain.models - -/** - * Given an athlete's [id], [AthleteCacheDictionary] keeps track of, for each year, - * the last month successfully cached into ROOM persistent storage. - * [lastCachedYearMonth] provides information to make conservative usage of the Strava API. - * - * @property id [id] is an athlete's ID, as provided by Strava. - * @property lastCachedYearMonth [lastCachedYearMonth] is a key-value store. - * The keys are years, while the values are the last zero-indexed month cached. - * A year that is fully cached will have a value of 11. A cached year does not necessarily - * mean that there are any activities cached for this year. - * Instead, a cached year means that IF any activities exist, they are cached. - */ -interface AthleteCacheDictionary : Athlete { - override val id: Long - val lastCachedYearMonth: Map -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/models/OAuth2.kt b/app/src/main/java/com/activityartapp/domain/models/OAuth2.kt index d10499c5..5607192e 100644 --- a/app/src/main/java/com/activityartapp/domain/models/OAuth2.kt +++ b/app/src/main/java/com/activityartapp/domain/models/OAuth2.kt @@ -1,7 +1,6 @@ package com.activityartapp.domain.models interface OAuth2 { - val athleteId: Long val expiresAtUnixSeconds: Int val accessToken: String val refreshToken: String diff --git a/app/src/main/java/com/activityartapp/domain/models/OAuth2Ext.kt b/app/src/main/java/com/activityartapp/domain/models/OAuth2Ext.kt index d2d8d7d5..d8aba9d9 100644 --- a/app/src/main/java/com/activityartapp/domain/models/OAuth2Ext.kt +++ b/app/src/main/java/com/activityartapp/domain/models/OAuth2Ext.kt @@ -4,11 +4,11 @@ import java.util.concurrent.TimeUnit private const val EXPIRE_BUFFER_SECONDS = 1800 -val OAuth2.requiresRefresh: Boolean +val Athlete.requiresRefresh: Boolean get() { println("Determining if access token $this requires refresh.") val currSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) println("--> currSeconds: $currSeconds") - println("--> expiresAtUnixSeconds: $expiresAtUnixSeconds") - return expiresAtUnixSeconds < currSeconds + EXPIRE_BUFFER_SECONDS + println("--> expiresAtUnixSeconds: ${expiresAtUnixSeconds}") + return true } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/models/OAuth2WithoutAthlete.kt b/app/src/main/java/com/activityartapp/domain/models/OAuth2WithoutAthlete.kt deleted file mode 100644 index d44d28da..00000000 --- a/app/src/main/java/com/activityartapp/domain/models/OAuth2WithoutAthlete.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.activityartapp.domain.models - -interface OAuth2WithoutAthlete { - val expiresAtUnixSeconds: Int - val accessToken: String - val refreshToken: String -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByPageFromRemote.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByPageFromRemote.kt index 5896c74b..cd6125da 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByPageFromRemote.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByPageFromRemote.kt @@ -7,27 +7,26 @@ import com.google.maps.android.PolyUtil import java.util.concurrent.CancellationException import javax.inject.Inject -/** Retrieves a [List] of [Activity] by Strava's API which occurs within a window of time - * and is on a certain page of specified pagination. +/** Retrieves a [List] of [Activity] by Strava's API by page without specifying before and + * after epoch timestamp parameters. * * Intended for internal abstracted use within [GetActivitiesByYearFromRemote].*/ class GetActivitiesByPageFromRemote @Inject constructor( private val api: AthleteApi // Impl of API ) { + companion object { + private const val ACTIVITIES_PER_PAGE = 200 + } + suspend operator fun invoke( code: String, - page: Int, - activitiesPerPage: Int, - beforeUnixSeconds: Int, - afterUnixSeconds: Int + page: Int ): Response> { return try { Response.Success(data = api.getActivities( authHeader = "Bearer $code", page = page, - perPage = activitiesPerPage, - before = beforeUnixSeconds, - after = afterUnixSeconds + perPage = ACTIVITIES_PER_PAGE ) .toList() .filter { @@ -36,6 +35,7 @@ class GetActivitiesByPageFromRemote @Inject constructor( } == true }) } catch (e: Exception) { + println("Exception in by page for page $page and code was $code") /* When using try catch in a suspend block, ensure we do not catch CancellationException */ if (e is CancellationException) throw e diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromDiskOrRemote.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromDiskOrRemote.kt deleted file mode 100644 index d3109cd7..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromDiskOrRemote.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.activityartapp.domain.useCase.activities - -import com.activityartapp.domain.models.Activity -import com.activityartapp.domain.models.Athlete -import com.activityartapp.domain.models.AthleteCacheDictionary -import com.activityartapp.domain.useCase.athleteCacheDictionary.GetAthleteCacheDictionaryFromDisk -import com.activityartapp.util.Response -import com.activityartapp.util.Response.Success -import com.activityartapp.util.TimeUtils -import com.activityartapp.util.doOnError -import com.activityartapp.util.doOnSuccess -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import java.net.UnknownHostException -import java.util.* -import javax.inject.Inject - -/** - * Retrieves all [Activity] for an [Athlete] by their id and year. - * - * Activities will be sourced, in increasing preference, from the Strava API or disk storage. - * - * Activities are automatically stored on disk and in memory. - */ -class GetActivitiesByYearFromDiskOrRemote @Inject constructor( - private val getAthleteCacheDictionaryFromDisk: GetAthleteCacheDictionaryFromDisk, - private val getActivitiesByYearMonthFromDisk: GetActivitiesByYearMonthFromDisk, - private val getActivitiesByYearFromRemote: GetActivitiesByYearFromRemote, - private val insertActivitiesIntoDisk: InsertActivitiesIntoDisk, - private val insertActivitiesIntoMemory: InsertActivitiesIntoMemory, - private val timeUtils: TimeUtils -) { - companion object { - private const val FIRST_MONTH_OF_YEAR = 0 - private const val LAST_MONTH_OF_YEAR = 11 - private const val NO_CACHED_MONTHS = -1 - } - - /** - * @param accessToken A non-expired access token. - * @param athleteId The athlete's ID as provided by Strava. - * @param internetEnabled Whether or not we should include fetching from Strava API as - * an option. For example, we may not want to allow a remote query if unable to validate - * the version due to a connection issue. - * @param year The year we are returning associated activities for. - * **/ - suspend operator fun invoke( - accessToken: String, - athleteId: Long, - internetEnabled: Boolean, - year: Int, - ): Response> { - return run { - val activities = mutableListOf() - - /** Determine from the [AthleteCacheDictionary] associated with this athlete - * which months of this year have cached on disk storage. **/ - val cachedYearMonths = - getAthleteCacheDictionaryFromDisk(athleteId)?.lastCachedYearMonth ?: mapOf() - val lastCachedMonth = cachedYearMonths[year] ?: NO_CACHED_MONTHS - - activities += mutableListOf>>().apply { - coroutineScope { - (FIRST_MONTH_OF_YEAR..lastCachedMonth).forEach { - add(async { - getActivitiesByYearMonthFromDisk( - athleteId = athleteId, - month = it, - year = year - ) - }) - } - } - }.awaitAll().flatten() - - /** If we've reached this point and internet access is disabled, send back [UnknownHostException] **/ - if (!internetEnabled) { - return@run Response.Error( - data = activities.toList(), - exception = UnknownHostException() - ) - } - - /** If any months of this [year] have not been stored on disk storage, obtain activities - * of those months from a Strava query. */ - if (lastCachedMonth != LAST_MONTH_OF_YEAR) { - getActivitiesByYearFromRemote( - accessToken = accessToken, - athleteId = athleteId, - year = year, - startMonth = cachedYearMonths[year].takeIf { - it != NO_CACHED_MONTHS - }?.nextMonth ?: FIRST_MONTH_OF_YEAR - ) - .doOnSuccess { - val cal = Calendar.getInstance() - val currMonth = cal.get(Calendar.MONTH) - val currYear = cal.get(Calendar.YEAR) - - /** At this point, we have received from remote all [Activity] between the start - * month and the end of the year. However, we will never consider the current - * month of the current year cached. **/ - val storeUpToMonthOnDisk = if (currYear == year) { - currMonth.previousMonth - } else { - LAST_MONTH_OF_YEAR - } - - /** Prepare to return all activities, then filter out anything from the current month - * if this is the current year before caching on disk. **/ - activities += data - - /** Cache activities into disk **/ - val filteredData = data - .takeIf { currYear == year } - ?.filter { timeUtils.iso8601StringToMonth(it.iso8601LocalDate) != currMonth } - ?: data - - storeUpToMonthOnDisk.takeIf { it >= FIRST_MONTH_OF_YEAR }?.let { month -> - insertActivitiesIntoDisk(filteredData, athleteId, year, month) - } - } - .doOnError { - return@run Response.Error( - data = activities.toList(), - exception = exception - ) - } - } - - /** Return successful result **/ - return@run Success(data = activities.toList()) - }.also { - /** Insert activities into memory **/ - it.data?.let { activities -> insertActivitiesIntoMemory(year, activities) } - } - } - - private val Int.previousMonth get() = this - 1 - private val Int.nextMonth get() = this + 1 -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromMemory.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromMemory.kt deleted file mode 100644 index d10e7c10..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromMemory.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.activityartapp.domain.useCase.activities - -import com.activityartapp.data.cache.ActivitiesCache -import javax.inject.Inject - -class GetActivitiesByYearFromMemory @Inject constructor( - private val cache: ActivitiesCache -) { - operator fun invoke(year: Int) = cache.cachedActivitiesByYear[year] -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromRemote.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromRemote.kt deleted file mode 100644 index 7dc2971f..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromRemote.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.activityartapp.domain.useCase.activities - -import com.activityartapp.domain.models.Activity -import com.activityartapp.domain.useCase.athleteUsage.GetAthleteUsageFromRemote -import com.activityartapp.domain.useCase.athleteUsage.InsertAthleteUsageIntoRemote -import com.activityartapp.util.Response -import com.activityartapp.util.TimeUtils -import com.activityartapp.util.doOnError -import com.activityartapp.util.doOnSuccess -import com.activityartapp.util.errors.AthleteRateLimitedException -import javax.inject.Inject - -class GetActivitiesByYearFromRemote @Inject constructor( - private val getActivitiesByPageFromRemote: GetActivitiesByPageFromRemote, - private val getAthleteUsageFromRemote: GetAthleteUsageFromRemote, - private val insertAthleteUsageIntoRemote: InsertAthleteUsageIntoRemote, - private val timeUtils: TimeUtils -) { - companion object { - private const val ACTIVITIES_PER_PAGE = 200 - private const val FIRST_MONTH_OF_YEAR = 0 - private const val LAST_MONTH_OF_YEAR = 11 - private const val FIRST_PAGE = 1 - private const val MAXIMUM_USAGE = 25 - } - - /** - * @param startMonth Optional parameter to specify the first 0-indexed month which to read - * from remote. Activities in months which precede this parameter will be omitted. - */ - suspend operator fun invoke( - accessToken: String, - athleteId: Long, - year: Int, - startMonth: Int = FIRST_MONTH_OF_YEAR - ): Response> { - - var page = FIRST_PAGE - var activitiesInLastPage = ACTIVITIES_PER_PAGE - val activities = mutableListOf() - - var usage: Int - getAthleteUsageFromRemote(athleteId) - .run { - when (this) { - is Response.Success -> usage = data - is Response.Error -> return Response.Error( - activities, - exception - ) - } - } - println("GetActivitiesByYearFromRemote: usage is $usage") - - while (activitiesInLastPage >= ACTIVITIES_PER_PAGE) { - if (usage >= MAXIMUM_USAGE) { - println("$usage is more than max usage of $MAXIMUM_USAGE") - return Response.Error(exception = AthleteRateLimitedException) - } - getActivitiesByPageFromRemote( - code = accessToken, - page = page++, - activitiesPerPage = ACTIVITIES_PER_PAGE, - beforeUnixSeconds = timeUtils.firstUnixSecondAfterYearMonth( - year, LAST_MONTH_OF_YEAR - ), - afterUnixSeconds = timeUtils.lastUnixSecondBeforeYearMonth( - year, startMonth - ), - ) - .doOnSuccess { - insertAthleteUsageIntoRemote(athleteId, ++usage) - activitiesInLastPage = data.size - activities.addAll(data) - } - .doOnError { - println("Here, an error occurred, that error was ${this.exception}") - return Response.Error(data = activities, exception = this.exception) - } - } - - return Response.Success(activities) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearMonthFromDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearMonthFromDisk.kt deleted file mode 100644 index f861c6e6..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearMonthFromDisk.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.activityartapp.domain.useCase.activities - -import com.activityartapp.data.database.AthleteDatabase -import com.activityartapp.domain.models.Activity -import com.activityartapp.domain.models.AthleteCacheDictionary -import javax.inject.Inject - -/** - * Retrieves all [Activity] stored on disk associated with an - * [AthleteCacheDictionary] occurring on the given year and month. - */ -class GetActivitiesByYearMonthFromDisk @Inject constructor( - private val athleteDatabase: AthleteDatabase -) { - companion object { - private const val MONTH_QUERY_MIN_LENGTH = 2 - private const val PADDING_CHAR: Char = '0' - private const val DELIMITER_CHAR = '-' - } - - /** - * @param month A 0-indexed value representing the month ranging from 0 to 11. - */ - suspend operator fun invoke(athleteId: Long, month: Int, year: Int): List { - /** ISO8601 is NOT 0-indexed with respect to month and thus must be reversed. - * Month is a padded integer which ranges from 01 to 12. **/ - val paddedMonth = "${(month + 1)}".padStart( - MONTH_QUERY_MIN_LENGTH, - PADDING_CHAR - ) - return athleteDatabase - .activityDao - .getActivitiesByYearMonth( - athleteId = athleteId, - monthStringWithDelimiter = "$DELIMITER_CHAR$paddedMonth$DELIMITER_CHAR", - yearStringWithDelimiter = "$year$DELIMITER_CHAR" - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromDisk.kt similarity index 62% rename from app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromDisk.kt rename to app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromDisk.kt index e45cc54e..18271c92 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesByYearFromDisk.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromDisk.kt @@ -4,12 +4,12 @@ import com.activityartapp.data.database.AthleteDatabase import com.activityartapp.domain.models.Activity import javax.inject.Inject -class GetActivitiesByYearFromDisk @Inject constructor( +class GetActivitiesFromDisk @Inject constructor( private val athleteDatabase: AthleteDatabase ) { - suspend operator fun invoke(athleteId: Long, year: Int): List? { + suspend operator fun invoke(athleteId: Long): List { return athleteDatabase .activityDao - .getActivitiesByYear(athleteId, year) + .getActivities(athleteId) } } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromDiskAndRemote.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromDiskAndRemote.kt new file mode 100644 index 00000000..6cc305bb --- /dev/null +++ b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromDiskAndRemote.kt @@ -0,0 +1,114 @@ +package com.activityartapp.domain.useCase.activities + +import com.activityartapp.domain.models.Activity +import com.activityartapp.domain.models.Athlete +import com.activityartapp.domain.useCase.athleteUsage.GetAthleteUsageFromRemote +import com.activityartapp.domain.useCase.athleteUsage.InsertAthleteUsageIntoRemote +import com.activityartapp.util.Response +import com.activityartapp.util.doOnError +import com.activityartapp.util.doOnSuccess +import com.activityartapp.util.errors.AthleteRateLimitedException +import java.net.UnknownHostException +import javax.inject.Inject + +class GetActivitiesFromDiskAndRemote @Inject constructor( + private val getActivitiesByPageFromRemote: GetActivitiesByPageFromRemote, + private val getActivitiesFromDisk: GetActivitiesFromDisk, + private val getAthleteUsageFromRemote: GetAthleteUsageFromRemote, + private val insertAthleteUsageIntoRemote: InsertAthleteUsageIntoRemote, + private val insertActivitiesIntoDisk: InsertActivitiesIntoDisk, + private val insertActivitiesIntoMemory: InsertActivitiesIntoMemory +) { + companion object { + private const val MAXIMUM_USAGE = 25 + private const val MAXIMUM_PAGE = 10 + private const val PAGE_FIRST = 1 + } + + suspend operator fun invoke( + athlete: Athlete, + internetEnabled: Boolean, + onActivitiesLoaded: (Int) -> Unit + ): Response> { + return athlete.run { + /** If there are cached activities, add them to activities **/ + val cachedActivities = getActivitiesFromDisk(athleteId) + onActivitiesLoaded(cachedActivities.size) + val cachedActivitiesIds = cachedActivities.map { it.id }.toSet() + + /** If we've reached this point and internet access is disabled, send back [UnknownHostException] **/ + if (!internetEnabled) { + return@run Response.Error( + data = cachedActivities.toList(), + exception = UnknownHostException() + ) + } + + /** Get the athlete's usage from remote + * If we were unable to get it, return cached activities and an error **/ + var usage: Int + getAthleteUsageFromRemote(athleteId) + .run { + when (this) { + is Response.Success -> usage = data + is Response.Error -> return Response.Error( + data = cachedActivities, + exception = exception + ) + } + } + + println("got usage, it was $usage") + + var page = PAGE_FIRST + var activitiesFromRemoteUnique = true + val remoteActivities = mutableListOf() + while (usage < MAXIMUM_USAGE && page < MAXIMUM_PAGE && activitiesFromRemoteUnique) { + /** Load this page of activities from remote **/ + getActivitiesByPageFromRemote(code = accessToken, page = page) + .doOnSuccess { + page++ + insertAthleteUsageIntoRemote(athleteId, ++usage) + /** If there are no common activities between remote & cache, continue loading **/ + val newActivities = data.filter { + !cachedActivitiesIds.contains(it.id) + } + + activitiesFromRemoteUnique = newActivities.size == data.size + + /** Add all activities **/ + remoteActivities.addAll(newActivities) + onActivitiesLoaded(newActivities.size) + } + .doOnError { + return@run Response.Error( + data = cachedActivities, + exception = this.exception + ) + } + } + + /** Insert activities from remote into disk cache **/ + if (page == MAXIMUM_PAGE || usage < MAXIMUM_USAGE) { + insertActivitiesIntoDisk( + activities = remoteActivities, + athleteId = athleteId + ) + } + + val allActivities = cachedActivities.plus(remoteActivities) + + /** If the athlete has exceeded the maximum temporary usage, return an error */ + if (usage >= MAXIMUM_USAGE) { + return@run Response.Error( + data = allActivities, + exception = AthleteRateLimitedException + ) + } + + return@run Response.Success(data = allActivities) + }.also { response -> + response.data?.let { insertActivitiesIntoMemory(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromMemory.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromMemory.kt index 8fe0745b..45768deb 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromMemory.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/activities/GetActivitiesFromMemory.kt @@ -1,17 +1,10 @@ package com.activityartapp.domain.useCase.activities import com.activityartapp.data.cache.ActivitiesCache -import com.activityartapp.domain.models.Activity -import com.activityartapp.util.TimeUtils import javax.inject.Inject class GetActivitiesFromMemory @Inject constructor( - private val cache: ActivitiesCache, - private val timeUtils: TimeUtils + private val cache: ActivitiesCache ) { - operator fun invoke(): List { - return cache.cachedActivitiesByYear - .flatMap { it.value } - .sortedBy { timeUtils.iso8601StringToUnixSecond(it.iso8601LocalDate) } - } + operator fun invoke() = cache.cachedActivities } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoDisk.kt index c913af46..ef6e1ab3 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoDisk.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoDisk.kt @@ -2,28 +2,15 @@ package com.activityartapp.domain.useCase.activities import com.activityartapp.data.database.AthleteDatabase import com.activityartapp.data.entities.ActivityEntity -import com.activityartapp.data.entities.AthleteCacheDictionaryEntity import com.activityartapp.domain.models.Activity -import com.activityartapp.domain.models.AthleteCacheDictionary -import com.activityartapp.domain.useCase.athleteCacheDictionary.GetAthleteCacheDictionaryFromDisk -import com.activityartapp.domain.useCase.athleteCacheDictionary.InsertAthleteCacheDictionaryIntoDisk import javax.inject.Inject -class InsertActivitiesIntoDisk @Inject constructor( - private val athleteDatabase: AthleteDatabase, - private val getAthleteCacheDictionaryFromDisk: GetAthleteCacheDictionaryFromDisk, - private val insertAthleteCacheDictionaryIntoDisk: InsertAthleteCacheDictionaryIntoDisk -) { +class InsertActivitiesIntoDisk @Inject constructor(private val athleteDatabase: AthleteDatabase) { suspend operator fun invoke( activities: List, - athleteId: Long, - year: Int, - lastStableMonth: Int + athleteId: Long ) { - println("Insert activities use case invoked for year $year, last stable month $lastStableMonth") - val activityEntities = activities - // Todo, replace with general to ActivityEntity function .map { it.run { ActivityEntity( @@ -51,27 +38,5 @@ class InsertActivitiesIntoDisk @Inject constructor( athleteDatabase .activityDao .insertAllActivities(*activityEntities) - - /** Update athlete cache for inserted activities **/ - println("Trying to get previous athlete") - val athleteCacheDictionary = getAthleteCacheDictionaryFromDisk(athleteId) ?: object : AthleteCacheDictionary { - override val id: Long = athleteId - override val lastCachedYearMonth: Map = mapOf() - } - println("Prev athlete was $athleteCacheDictionary") - athleteCacheDictionary.run { - lastCachedYearMonth.let { prevCache -> - val newCache = prevCache.toMutableMap() - newCache[year] = lastStableMonth - apply { - insertAthleteCacheDictionaryIntoDisk( - AthleteCacheDictionaryEntity( - id = athleteId, - lastCachedYearMonth = newCache - ) - ) - } - } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoMemory.kt b/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoMemory.kt index 57ae71a4..f0af4c9b 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoMemory.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/activities/InsertActivitiesIntoMemory.kt @@ -4,10 +4,8 @@ import com.activityartapp.data.cache.ActivitiesCache import com.activityartapp.domain.models.Activity import javax.inject.Inject -class InsertActivitiesIntoMemory @Inject constructor( - private val cache: ActivitiesCache -) { - operator fun invoke(year: Int, activities: List) { - cache.cachedActivitiesByYear[year] = activities +class InsertActivitiesIntoMemory @Inject constructor(private val cache: ActivitiesCache) { + operator fun invoke(activities: List) { + cache.cachedActivities = activities } } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/GetAthleteCacheDictionaryFromDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/GetAthleteCacheDictionaryFromDisk.kt deleted file mode 100644 index d10f63a0..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/GetAthleteCacheDictionaryFromDisk.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.activityartapp.domain.useCase.athleteCacheDictionary - -import com.activityartapp.data.database.AthleteDatabase -import com.activityartapp.domain.models.AthleteCacheDictionary -import javax.inject.Inject - -class GetAthleteCacheDictionaryFromDisk @Inject constructor( - private val athleteDatabase: AthleteDatabase -) { - suspend operator fun invoke(athleteId: Long): AthleteCacheDictionary? { - return athleteDatabase - .athleteCacheDictionaryDao - .getAthleteCacheDictionaryById(athleteId) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/InsertAthleteCacheDictionaryIntoDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/InsertAthleteCacheDictionaryIntoDisk.kt deleted file mode 100644 index f4cb792f..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/athleteCacheDictionary/InsertAthleteCacheDictionaryIntoDisk.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.activityartapp.domain.useCase.athleteCacheDictionary - -import com.activityartapp.data.database.AthleteDatabase -import com.activityartapp.data.entities.AthleteCacheDictionaryEntity -import com.activityartapp.domain.models.AthleteCacheDictionary -import javax.inject.Inject - -class InsertAthleteCacheDictionaryIntoDisk @Inject constructor( - private val athleteDatabase: AthleteDatabase -) { - suspend operator fun invoke(athlete: AthleteCacheDictionary) { - athleteDatabase.athleteCacheDictionaryDao.insertAthlete(athlete.run { - AthleteCacheDictionaryEntity( - id = id, - lastCachedYearMonth = lastCachedYearMonth - ) - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/ClearAccessTokenFromDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/ClearAthleteFromDisk.kt similarity index 71% rename from app/src/main/java/com/activityartapp/domain/useCase/authentication/ClearAccessTokenFromDisk.kt rename to app/src/main/java/com/activityartapp/domain/useCase/authentication/ClearAthleteFromDisk.kt index dce642fa..2db55786 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/authentication/ClearAccessTokenFromDisk.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/authentication/ClearAthleteFromDisk.kt @@ -3,22 +3,22 @@ package com.activityartapp.domain.useCase.authentication import com.activityartapp.data.cache.ActivitiesCache import com.activityartapp.data.database.AthleteDatabase import com.activityartapp.domain.models.Activity -import com.activityartapp.domain.models.OAuth2 +import com.activityartapp.domain.models.Athlete import javax.inject.Inject -/** Clears all access tokens [OAuth2] which exist in on-disk storage +/** Clears all access tokens [Athlete] which exist in on-disk storage * and any [Activity] which exist in memory.**/ -class ClearAccessTokenFromDisk @Inject constructor( +class ClearAthleteFromDisk @Inject constructor( private val athleteDatabase: AthleteDatabase, private val cache: ActivitiesCache ) { suspend operator fun invoke() { /** Singleton cache is first cleared so that it will be fetched * again if a new athlete signs in */ - cache.cachedActivitiesByYear.clear() + cache.cachedActivities = null /** Clear ROOM storage entry containing current authentication **/ return athleteDatabase - .oAuth2Dao - .clearOauth2() + .athleteDao + .clearAthlete() } } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenFromDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenFromDisk.kt deleted file mode 100644 index 835bdfbc..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenFromDisk.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.activityartapp.domain.useCase.authentication - -import com.activityartapp.data.database.AthleteDatabase -import com.activityartapp.domain.models.OAuth2 -import javax.inject.Inject - -/** Retrieves the POTENTIALLY-EXPIRED [OAuth2] from on-disk storage - * or null if one does not exist. **/ -class GetAccessTokenFromDisk @Inject constructor(private val athleteDatabase: AthleteDatabase) { - suspend operator fun invoke(): OAuth2? { - return athleteDatabase - .oAuth2Dao - .getCurrAuth() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenWithAuthorizationCodeFromRemote.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenWithAuthorizationCodeFromRemote.kt index a60bee1f..db9a5d6f 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenWithAuthorizationCodeFromRemote.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenWithAuthorizationCodeFromRemote.kt @@ -1,14 +1,14 @@ package com.activityartapp.domain.useCase.authentication import com.activityartapp.data.remote.AthleteApi -import com.activityartapp.domain.models.OAuth2 +import com.activityartapp.domain.models.Athlete import com.activityartapp.util.Response import com.activityartapp.util.constants.CLIENT_SECRET import com.activityartapp.util.constants.TokenConstants.CLIENT_ID import java.util.concurrent.CancellationException import javax.inject.Inject -/** Retrieves an [OAuth2] by sending an authorization code to the Strava API. **/ +/** Retrieves an [Athlete] by sending an authorization code to the Strava API. **/ class GetAccessTokenWithAuthorizationCodeFromRemote @Inject constructor(private val api: AthleteApi) { companion object { private const val GRANT_TYPE = "authorization_code" @@ -16,7 +16,7 @@ class GetAccessTokenWithAuthorizationCodeFromRemote @Inject constructor(private suspend operator fun invoke( authorizationCode: String - ): Response { + ): Response { return try { val bearer = api.getAccessToken( clientId = CLIENT_ID, @@ -24,7 +24,13 @@ class GetAccessTokenWithAuthorizationCodeFromRemote @Inject constructor(private code = authorizationCode, grantType = GRANT_TYPE ) - Response.Success(bearer) + Response.Success(object : Athlete { + override val athleteId: Long = bearer.athlete.id + override val lastCachedUnixMs: Long? = null + override val expiresAtUnixSeconds = bearer.expiresAtUnixSeconds + override val accessToken: String = bearer.accessToken + override val refreshToken: String = bearer.refreshToken + }) } catch (e: Exception) { /* When using try catch in a suspend block, ensure we do not catch CancellationException */ diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteFromDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteFromDisk.kt new file mode 100644 index 00000000..ce71c165 --- /dev/null +++ b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteFromDisk.kt @@ -0,0 +1,15 @@ +package com.activityartapp.domain.useCase.authentication + +import com.activityartapp.data.database.AthleteDatabase +import com.activityartapp.domain.models.Athlete +import javax.inject.Inject + +/** Retrieves the POTENTIALLY-EXPIRED [Athlete] from on-disk storage + * or null if one does not exist. **/ +class GetAthleteFromDisk @Inject constructor(private val athleteDatabase: AthleteDatabase) { + suspend operator fun invoke(): Athlete? { + return athleteDatabase + .athleteDao + .getCurrAthlete() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenFromDiskOrRemote.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteFromDiskOrRemote.kt similarity index 70% rename from app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenFromDiskOrRemote.kt rename to app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteFromDiskOrRemote.kt index c113eb92..30b838fc 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenFromDiskOrRemote.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteFromDiskOrRemote.kt @@ -3,7 +3,6 @@ package com.activityartapp.domain.useCase.authentication import android.net.Uri import com.activityartapp.domain.models.Activity import com.activityartapp.domain.models.Athlete -import com.activityartapp.domain.models.OAuth2 import com.activityartapp.util.Response import com.activityartapp.util.UriUtils import com.activityartapp.util.doOnSuccess @@ -14,14 +13,14 @@ import javax.inject.Inject * storage or by sending an authorization code or an on-disk stored * refresh token to Strava API. * - * On success, the [OAuth2] is automatically inserted into on disk storage. **/ -class GetAccessTokenFromDiskOrRemote @Inject constructor( - private val getAccessTokenWithRefreshUseCase: GetAccessTokenWithRefreshTokenFromRemote, + * On success, the [Athlete] is automatically inserted into on disk storage. **/ +class GetAthleteFromDiskOrRemote @Inject constructor( + private val getAccessTokenWithRefreshUseCase: GetAthleteWithTokenRefreshFromRemote, private val getAccessTokenFromRemoteUseCase: GetAccessTokenWithAuthorizationCodeFromRemote, - private val insertAccessTokenIntoDisk: InsertAccessTokenIntoDisk, + private val insertAthleteIntoDisk: InsertAthleteIntoDisk, private val uriUtils: UriUtils ) { - suspend operator fun invoke(uri: Uri? = null): Response { + suspend operator fun invoke(uri: Uri? = null): Response { /** If URI was provided, parse out authorization code **/ val authCode: String? = uri?.let { uriUtils.parseUri(it) } return when { @@ -29,6 +28,8 @@ class GetAccessTokenFromDiskOrRemote @Inject constructor( authCode != null -> getAccessTokenFromRemoteUseCase(authCode) /** Read from locally-stored access token, refresh if needed **/ else -> getAccessTokenWithRefreshUseCase() - }.doOnSuccess { insertAccessTokenIntoDisk(data) } + }.doOnSuccess { + insertAthleteIntoDisk(data) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenWithRefreshTokenFromRemote.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteWithTokenRefreshFromRemote.kt similarity index 78% rename from app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenWithRefreshTokenFromRemote.kt rename to app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteWithTokenRefreshFromRemote.kt index 533165dd..11461724 100644 --- a/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAccessTokenWithRefreshTokenFromRemote.kt +++ b/app/src/main/java/com/activityartapp/domain/useCase/authentication/GetAthleteWithTokenRefreshFromRemote.kt @@ -1,7 +1,7 @@ package com.activityartapp.domain.useCase.authentication import com.activityartapp.data.remote.AthleteApi -import com.activityartapp.domain.models.OAuth2 +import com.activityartapp.domain.models.Athlete import com.activityartapp.domain.models.requiresRefresh import com.activityartapp.util.Response import com.activityartapp.util.Response.Error @@ -11,32 +11,33 @@ import com.activityartapp.util.constants.TokenConstants import java.util.concurrent.CancellationException import javax.inject.Inject -/** Retrieves a non-expired [OAuth2] from the potentially-expired access token +/** Retrieves a non-expired [Athlete] from the potentially-expired access token * and refresh token stored on disk. We refresh an expired access token by the * Strava API. Returns an [Error] if no token exists on disk or when unable * to refresh an expired token.**/ -class GetAccessTokenWithRefreshTokenFromRemote @Inject constructor( +class GetAthleteWithTokenRefreshFromRemote @Inject constructor( private val athleteApi: AthleteApi, - private val getAccessTokenFromLocalUseCase: GetAccessTokenFromDisk, + private val getAccessTokenFromLocalUseCase: GetAthleteFromDisk, ) { companion object { private const val GRANT_TYPE = "refresh_token" } - suspend operator fun invoke(): Response { + suspend operator fun invoke(): Response { getAccessTokenFromLocalUseCase().apply { return when { this == null -> Error() - /** On refresh, pass in local athlete id as refresh does not incl AthleteCacheDictionary **/ + /** On refresh, pass in local athlete id as refresh does not incl AthleteHasCached **/ requiresRefresh -> onRequiresRefresh(this) else -> Success(this) } } } - private suspend fun onRequiresRefresh(prevOauth: OAuth2): - Response { + private suspend fun onRequiresRefresh(prevOauth: Athlete): + Response { return try { + println("refresh required, refreshing...") Success( athleteApi.getAccessTokenFromRefresh( clientId = TokenConstants.CLIENT_ID, @@ -47,8 +48,9 @@ class GetAccessTokenWithRefreshTokenFromRemote @Inject constructor( val expiresAtUnixSeconds = expiresAtUnixSeconds val accessToken = accessToken val refreshToken = refreshToken - object : OAuth2 { + object : Athlete { override val athleteId: Long = prevOauth.athleteId + override val lastCachedUnixMs: Long? = null override val expiresAtUnixSeconds: Int = expiresAtUnixSeconds override val accessToken: String = accessToken override val refreshToken: String = refreshToken diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAccessTokenIntoDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAccessTokenIntoDisk.kt deleted file mode 100644 index 9eafdc19..00000000 --- a/app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAccessTokenIntoDisk.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.activityartapp.domain.useCase.authentication - -import com.activityartapp.data.database.AthleteDatabase -import com.activityartapp.data.entities.OAuth2Entity -import com.activityartapp.domain.models.OAuth2 -import javax.inject.Inject - -/** Inserts an [OAuth2] into on-disk storage. **/ -class InsertAccessTokenIntoDisk @Inject constructor( - private val athleteDatabase: AthleteDatabase -) { - suspend operator fun invoke(auth: OAuth2) { - val entity = auth.run { - OAuth2Entity( - athleteId, - expiresAtUnixSeconds, - accessToken, - refreshToken - ) - } - athleteDatabase.oAuth2Dao.insertOauth2(entity) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAthleteIntoDisk.kt b/app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAthleteIntoDisk.kt new file mode 100644 index 00000000..f69c26e0 --- /dev/null +++ b/app/src/main/java/com/activityartapp/domain/useCase/authentication/InsertAthleteIntoDisk.kt @@ -0,0 +1,24 @@ +package com.activityartapp.domain.useCase.authentication + +import com.activityartapp.data.database.AthleteDatabase +import com.activityartapp.data.entities.AthleteEntity +import com.activityartapp.domain.models.Athlete +import javax.inject.Inject + +/** Inserts an [Athlete] into on-disk storage. **/ +class InsertAthleteIntoDisk @Inject constructor( + private val athleteDatabase: AthleteDatabase +) { + suspend operator fun invoke(auth: Athlete) { + val entity = auth.run { + AthleteEntity( + athleteId = athleteId, + lastCachedUnixMs = null, + expiresAtUnixSeconds = auth.expiresAtUnixSeconds, + accessToken = auth.accessToken, + refreshToken = auth.refreshToken + ) + } + athleteDatabase.athleteDao.insertAthlete(entity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/presentation/MainViewModel.kt b/app/src/main/java/com/activityartapp/presentation/MainViewModel.kt index 89c8e752..fb0ec431 100644 --- a/app/src/main/java/com/activityartapp/presentation/MainViewModel.kt +++ b/app/src/main/java/com/activityartapp/presentation/MainViewModel.kt @@ -4,7 +4,8 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.activityartapp.architecture.BaseRoutingViewModel import com.activityartapp.data.cache.ActivitiesCache -import com.activityartapp.domain.useCase.authentication.GetAccessTokenFromDiskOrRemote +import com.activityartapp.domain.useCase.activities.InsertActivitiesIntoMemory +import com.activityartapp.domain.useCase.authentication.GetAthleteFromDiskOrRemote import com.activityartapp.presentation.MainViewState.* import com.activityartapp.presentation.MainViewEvent.* import com.activityartapp.util.ParcelableActivity @@ -12,33 +13,30 @@ import com.activityartapp.util.Response.* import com.activityartapp.util.parcelize import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import java.time.Year import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val activitiesCache: ActivitiesCache, - private val getAccessTokenFromDiskOrRemote: GetAccessTokenFromDiskOrRemote, - private val savedStateHandle: SavedStateHandle + private val getAthleteFromDiskOrRemote: GetAthleteFromDiskOrRemote, + private val savedStateHandle: SavedStateHandle, + insertActivitiesIntoMemory: InsertActivitiesIntoMemory ) : BaseRoutingViewModel< MainViewState, MainViewEvent, MainDestination>() { companion object { - private val YEAR_NOW = Year.now().value - private const val YEAR_EARLIEST = 2018 + private const val ACTIVITIES_KEY = "activities" } - + var athleteId: Long? = null var accessToken: String? = null init { - (YEAR_EARLIEST..YEAR_NOW).forEach { year -> - val yearActivities: List? = savedStateHandle["$year"] - yearActivities?.map { it }?.let { - activitiesCache.cachedActivitiesByYear[year] = it - } + val activities: List? = savedStateHandle[ACTIVITIES_KEY] + if (activities != null) { + insertActivitiesIntoMemory(activities) } } @@ -52,7 +50,7 @@ class MainViewModel @Inject constructor( private fun onLoadAuthentication(event: LoadAuthentication) { viewModelScope.launch { pushState( - when (val response = getAccessTokenFromDiskOrRemote(event.uri)) { + when (val response = getAthleteFromDiskOrRemote(event.uri)) { is Success -> Authenticated is Error -> if (response.data != null) { Authenticated @@ -65,10 +63,9 @@ class MainViewModel @Inject constructor( } private fun onLoadedActivities() { - (YEAR_EARLIEST..YEAR_NOW).forEach { year -> - (activitiesCache.cachedActivitiesByYear[year]) - ?.parcelize() - ?.let { savedStateHandle["$year"] = it } - } + activitiesCache + .cachedActivities + ?.parcelize() + ?.let { savedStateHandle[ACTIVITIES_KEY] = it } } } \ No newline at end of file diff --git a/app/src/main/java/com/activityartapp/presentation/editArtScreen/EditArtViewModel.kt b/app/src/main/java/com/activityartapp/presentation/editArtScreen/EditArtViewModel.kt index 23b543d0..8d8e4f7a 100644 --- a/app/src/main/java/com/activityartapp/presentation/editArtScreen/EditArtViewModel.kt +++ b/app/src/main/java/com/activityartapp/presentation/editArtScreen/EditArtViewModel.kt @@ -54,7 +54,7 @@ class EditArtViewModel @Inject constructor( } /** All activities cached in Singleton memory **/ - private val activities: List = getActivitiesFromMemory() + private val activities: List = getActivitiesFromMemory() ?: error("Activities missing!") /** The list of activities for each [EditArtFilterType] **/ private val activitiesFilteredByFilterType: MutableMap> = diff --git a/app/src/main/java/com/activityartapp/presentation/loadActivitiesScreen/LoadActivitiesViewModel.kt b/app/src/main/java/com/activityartapp/presentation/loadActivitiesScreen/LoadActivitiesViewModel.kt index fe889219..eb887ff9 100644 --- a/app/src/main/java/com/activityartapp/presentation/loadActivitiesScreen/LoadActivitiesViewModel.kt +++ b/app/src/main/java/com/activityartapp/presentation/loadActivitiesScreen/LoadActivitiesViewModel.kt @@ -2,10 +2,10 @@ package com.activityartapp.presentation.loadActivitiesScreen import androidx.lifecycle.viewModelScope import com.activityartapp.architecture.BaseRoutingViewModel -import com.activityartapp.domain.models.OAuth2 +import com.activityartapp.domain.models.Athlete import com.activityartapp.domain.models.Version -import com.activityartapp.domain.useCase.activities.GetActivitiesByYearFromDiskOrRemote -import com.activityartapp.domain.useCase.authentication.GetAccessTokenFromDiskOrRemote +import com.activityartapp.domain.useCase.activities.GetActivitiesFromDiskAndRemote +import com.activityartapp.domain.useCase.authentication.GetAthleteFromDiskOrRemote import com.activityartapp.domain.useCase.version.GetVersionFromRemote import com.activityartapp.presentation.MainDestination import com.activityartapp.presentation.MainDestination.* @@ -13,7 +13,6 @@ import com.activityartapp.presentation.errorScreen.ErrorScreenType import com.activityartapp.presentation.loadActivitiesScreen.LoadActivitiesViewEvent.* import com.activityartapp.presentation.loadActivitiesScreen.LoadActivitiesViewState.* import com.activityartapp.util.Response -import com.activityartapp.util.Response.Error import com.activityartapp.util.Response.Success import com.activityartapp.util.classes.ApiError import com.activityartapp.util.doOnError @@ -22,15 +21,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.time.Year import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @HiltViewModel class LoadActivitiesViewModel @Inject constructor( - private val getActivitiesByYearFromDiskOrRemote: GetActivitiesByYearFromDiskOrRemote, - private val getAccessTokenFromDiskOrRemote: GetAccessTokenFromDiskOrRemote, + private val getActivitiesFromDiskAndRemote: GetActivitiesFromDiskAndRemote, + private val getAthleteFromDiskOrRemote: GetAthleteFromDiskOrRemote, private val getVersionFromRemote: GetVersionFromRemote, ) : BaseRoutingViewModel() { @@ -40,8 +38,6 @@ class LoadActivitiesViewModel @Inject constructor( private const val DELAY_MS_SUCCESSFULLY_LOADED = 1000L private const val NO_ACTIVITIES_COUNT = 0 - private const val YEAR_START = 2018 - private val YEAR_NOW = Year.now().value } override fun onEvent(event: LoadActivitiesViewEvent) { @@ -104,28 +100,20 @@ class LoadActivitiesViewModel @Inject constructor( /** If we don't have internet to know if the version is supported, make sure we don't access the internet **/ val internetEnabled: Boolean = versionResponse is Success - /* OAuth2 should never return null here */ - val oAuth2 = getOAuth2() ?: error("OAuth2 is null for an unknown reason...") - val accessToken = oAuth2.accessToken - val athleteId = oAuth2.athleteId - - /** Load activities until complete or - * returned [Response] is an [Error] **/ - (YEAR_NOW downTo YEAR_START).forEach { year -> - getActivitiesByYearFromDiskOrRemote( - accessToken = accessToken, - athleteId = athleteId, - year = year, - internetEnabled = internetEnabled - ) - .doOnError { error = ApiError.valueOf(exception) } - .apply { - data?.let { activitiesCount += it.size } - activitiesCount - .takeIf { it > NO_ACTIVITIES_COUNT } - ?.let { Loading(activitiesCount).push() } - } - } + /* Athlete should never return null here */ + val oAuth2 = getOAuth2() ?: error("Athlete is null for an unknown reason...") + + getActivitiesFromDiskAndRemote( + athlete = oAuth2, + internetEnabled = internetEnabled, + onActivitiesLoaded = { newNumberOfActivities -> + activitiesCount += newNumberOfActivities + activitiesCount.takeIf { it > NO_ACTIVITIES_COUNT }?.let { Loading(it).push() } + } + ) + .doOnError { + error = ApiError.valueOf(exception) + } when { error != null -> error?.let { @@ -144,10 +132,10 @@ class LoadActivitiesViewModel @Inject constructor( } } - private suspend fun getOAuth2(): OAuth2? { + private suspend fun getOAuth2(): Athlete? { return suspendCoroutine { continuation -> viewModelScope.launch(Dispatchers.IO) { - getAccessTokenFromDiskOrRemote() + getAthleteFromDiskOrRemote() .doOnSuccess { continuation.resume(data) } diff --git a/app/src/main/java/com/activityartapp/presentation/saveArtScreen/SaveArtViewModel.kt b/app/src/main/java/com/activityartapp/presentation/saveArtScreen/SaveArtViewModel.kt index b379c1b3..bdef3ca6 100644 --- a/app/src/main/java/com/activityartapp/presentation/saveArtScreen/SaveArtViewModel.kt +++ b/app/src/main/java/com/activityartapp/presentation/saveArtScreen/SaveArtViewModel.kt @@ -163,7 +163,7 @@ class SaveArtViewModel @Inject constructor( println("here, activity types are $activityTypes") return visualizationUtils.createBitmap( activities = activityFilterUtils.filterActivities( - activities = activities, + activities = activities ?: error("Activities missing!"), includeActivityTypes = activityTypes.toSet(), unixMsRange = filterDateAfterMs..filterDateBeforeMs, distanceRange = filterDistanceMoreThanMeters..filterDistanceLessThanMeters diff --git a/app/src/main/java/com/activityartapp/presentation/welcomeScreen/WelcomeViewModel.kt b/app/src/main/java/com/activityartapp/presentation/welcomeScreen/WelcomeViewModel.kt index 0519eadd..7c71a6b4 100644 --- a/app/src/main/java/com/activityartapp/presentation/welcomeScreen/WelcomeViewModel.kt +++ b/app/src/main/java/com/activityartapp/presentation/welcomeScreen/WelcomeViewModel.kt @@ -2,7 +2,7 @@ package com.activityartapp.presentation.welcomeScreen import androidx.lifecycle.viewModelScope import com.activityartapp.architecture.BaseRoutingViewModel -import com.activityartapp.domain.useCase.authentication.ClearAccessTokenFromDisk +import com.activityartapp.domain.useCase.authentication.ClearAthleteFromDisk import com.activityartapp.domain.useCase.version.GetVersionFromRemote import com.activityartapp.presentation.MainDestination import com.activityartapp.presentation.MainDestination.* @@ -17,7 +17,7 @@ import javax.inject.Inject @HiltViewModel class WelcomeViewModel @Inject constructor( - private val clearAccessTokenFromDiskUseCase: ClearAccessTokenFromDisk, + private val clearAthleteFromDisk: ClearAthleteFromDisk, private val getVersionFromRemote: GetVersionFromRemote ) : BaseRoutingViewModel() { @@ -27,7 +27,6 @@ class WelcomeViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { getVersionFromRemote() .doOnSuccess { - println("Version received ${data.isLatest} ${data.isSupported}") if (!data.isSupported) { routeTo( NavigateError( @@ -71,7 +70,7 @@ class WelcomeViewModel @Inject constructor( private fun onClickedLogout() { viewModelScope.launch { - clearAccessTokenFromDiskUseCase() + clearAthleteFromDisk() routeTo(NavigateLogin) } }