mirror of
https://github.com/rafaelvcaetano/melonDS-android.git
synced 2024-11-23 05:39:41 +00:00
Cache achievement data to reduce the number of requests
This commit is contained in:
parent
f334cf39be
commit
0eb2bdfea9
@ -132,6 +132,7 @@ dependencies {
|
||||
implementation(preference)
|
||||
implementation(recyclerView)
|
||||
implementation(room)
|
||||
implementation(roomKtx)
|
||||
implementation(roomRxJava)
|
||||
implementation(splashscreen)
|
||||
implementation(swipeRefreshLayout)
|
||||
|
@ -5,34 +5,50 @@ import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import me.magnum.melonds.database.daos.CheatFolderDao
|
||||
import me.magnum.melonds.database.daos.CheatDao
|
||||
import me.magnum.melonds.database.daos.CheatDatabaseDao
|
||||
import me.magnum.melonds.database.daos.GameDao
|
||||
import me.magnum.melonds.database.converters.InstantConverter
|
||||
import me.magnum.melonds.database.daos.*
|
||||
import me.magnum.melonds.database.entities.CheatDatabaseEntity
|
||||
import me.magnum.melonds.database.entities.CheatEntity
|
||||
import me.magnum.melonds.database.entities.CheatFolderEntity
|
||||
import me.magnum.melonds.database.entities.GameEntity
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAAchievementEntity
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAGameSetMetadata
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAUserAchievementEntity
|
||||
|
||||
@Database(
|
||||
version = 3,
|
||||
version = 4,
|
||||
exportSchema = true,
|
||||
entities = [CheatDatabaseEntity::class, GameEntity::class, CheatFolderEntity::class, CheatEntity::class],
|
||||
entities = [
|
||||
CheatDatabaseEntity::class,
|
||||
GameEntity::class,
|
||||
CheatFolderEntity::class,
|
||||
CheatEntity::class,
|
||||
RAAchievementEntity::class,
|
||||
RAUserAchievementEntity::class,
|
||||
RAGameSetMetadata::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(
|
||||
from = 2,
|
||||
to = 3,
|
||||
spec = MelonDatabase.Migration2to3Spec::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 3,
|
||||
to = 4,
|
||||
)
|
||||
]
|
||||
)
|
||||
@TypeConverters(InstantConverter::class)
|
||||
abstract class MelonDatabase : RoomDatabase() {
|
||||
abstract fun cheatDatabaseDao(): CheatDatabaseDao
|
||||
abstract fun gameDao(): GameDao
|
||||
abstract fun cheatFolderDao(): CheatFolderDao
|
||||
abstract fun cheatDao(): CheatDao
|
||||
abstract fun achievementsDao(): RAAchievementsDao
|
||||
|
||||
class Migration2to3Spec : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
|
@ -0,0 +1,19 @@
|
||||
package me.magnum.melonds.database.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.time.Instant
|
||||
|
||||
class InstantConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun instantToTimestamp(instant: Instant?): Long? {
|
||||
return instant?.toEpochMilli()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun timestampToInstant(timestamp: Long?): Instant? {
|
||||
return timestamp?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package me.magnum.melonds.database.daos
|
||||
|
||||
import androidx.room.*
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAAchievementEntity
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAGameSetMetadata
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAUserAchievementEntity
|
||||
|
||||
@Dao
|
||||
interface RAAchievementsDao {
|
||||
|
||||
@Query("SELECT * FROM ra_game_set_metadata WHERE game_id = :gameId")
|
||||
suspend fun getGameSetMetadata(gameId: Long): RAGameSetMetadata?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun updateGameSetMetadata(gameSetMetadata: RAGameSetMetadata)
|
||||
|
||||
@Query("SELECT * FROM ra_achievement WHERE game_id = :gameId")
|
||||
suspend fun getGameAchievements(gameId: Long): List<RAAchievementEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun updateGameAchievements(achievements: List<RAAchievementEntity>)
|
||||
|
||||
@Query("SELECT * FROM ra_user_achievement WHERE game_id = :gameId AND is_unlocked = 1")
|
||||
suspend fun getGameUserUnlockedAchievements(gameId: Long): List<RAUserAchievementEntity>
|
||||
|
||||
@Query("DELETE FROM ra_user_achievement WHERE game_id = :gameId")
|
||||
suspend fun deleteGameUserUnlockedAchievements(gameId: Long)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertGameUserUnlockedAchievements(userAchievements: List<RAUserAchievementEntity>)
|
||||
|
||||
@Transaction
|
||||
suspend fun updateGameUserUnlockedAchievements(gameId: Long, userAchievements: List<RAUserAchievementEntity>) {
|
||||
deleteGameUserUnlockedAchievements(gameId)
|
||||
insertGameUserUnlockedAchievements(userAchievements)
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package me.magnum.melonds.database.entities.retroachievements
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "ra_achievement",
|
||||
indices = [
|
||||
Index("game_id")
|
||||
]
|
||||
)
|
||||
data class RAAchievementEntity(
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: Long,
|
||||
@ColumnInfo(name = "game_id") val gameId: Long,
|
||||
@ColumnInfo(name = "total_awards_casual") val totalAwardsCasual: Int,
|
||||
@ColumnInfo(name = "total_awards_hardcore") val totalAwardsHardcore: Int,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "description") val description: String,
|
||||
@ColumnInfo(name = "points") val points: Int,
|
||||
@ColumnInfo(name = "display_order") val displayOrder: Int,
|
||||
@ColumnInfo(name = "badge_url_unlocked") val badgeUrlUnlocked: String,
|
||||
@ColumnInfo(name = "badge_url_locked") val badgeUrlLocked: String,
|
||||
@ColumnInfo(name = "memory_address") val memoryAddress: String,
|
||||
@ColumnInfo(name = "type") val type: Int,
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
package me.magnum.melonds.database.entities.retroachievements
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.Instant
|
||||
|
||||
@Entity(tableName = "ra_game_set_metadata")
|
||||
data class RAGameSetMetadata(
|
||||
@PrimaryKey @ColumnInfo(name = "game_id") val gameId: Long,
|
||||
@ColumnInfo(name = "last_achievement_set_updated") val lastAchievementSetUpdated: Instant?,
|
||||
@ColumnInfo(name = "last_user_data_updated") val lastUserDataUpdated: Instant?,
|
||||
)
|
@ -0,0 +1,23 @@
|
||||
package me.magnum.melonds.database.entities.retroachievements
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "ra_user_achievement",
|
||||
primaryKeys = ["game_id", "achievement_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = RAAchievementEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["achievement_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
)
|
||||
],
|
||||
)
|
||||
data class RAUserAchievementEntity(
|
||||
@ColumnInfo(name = "game_id") val gameId: Long,
|
||||
@ColumnInfo(name = "achievement_id") val achievementId: Long,
|
||||
@ColumnInfo(name = "is_unlocked") val isUnlocked: Boolean,
|
||||
)
|
@ -83,8 +83,8 @@ object MelonModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetroAchievementsRepository(raApi: RAApi, raUserAuthStore: RAUserAuthStore): RetroAchievementsRepository {
|
||||
return AndroidRetroAchievementsRepository(raApi, raUserAuthStore)
|
||||
fun provideRetroAchievementsRepository(raApi: RAApi, melonDatabase: MelonDatabase, raUserAuthStore: RAUserAuthStore): RetroAchievementsRepository {
|
||||
return AndroidRetroAchievementsRepository(raApi, melonDatabase.achievementsDao(), raUserAuthStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@ -1,17 +1,22 @@
|
||||
package me.magnum.melonds.impl
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import me.magnum.melonds.database.daos.RAAchievementsDao
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAGameSetMetadata
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAUserAchievementEntity
|
||||
import me.magnum.melonds.domain.model.retroachievements.RAUserAchievement
|
||||
import me.magnum.melonds.domain.repositories.RetroAchievementsRepository
|
||||
import me.magnum.melonds.impl.mappers.retroachievements.mapToEntity
|
||||
import me.magnum.melonds.impl.mappers.retroachievements.mapToModel
|
||||
import me.magnum.rcheevosapi.RAApi
|
||||
import me.magnum.rcheevosapi.RAUserAuthStore
|
||||
import me.magnum.rcheevosapi.model.RAAchievement
|
||||
import me.magnum.rcheevosapi.model.RAGameId
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
class AndroidRetroAchievementsRepository(
|
||||
private val raApi: RAApi,
|
||||
private val achievementsDao: RAAchievementsDao,
|
||||
private val raUserAuthStore: RAUserAuthStore,
|
||||
) : RetroAchievementsRepository {
|
||||
|
||||
@ -24,33 +29,21 @@ class AndroidRetroAchievementsRepository(
|
||||
}
|
||||
|
||||
override suspend fun getGameUserAchievements(gameId: RAGameId): Result<List<RAUserAchievement>> {
|
||||
val (gameAchievementsResult, userUnlocksResult) = coroutineScope {
|
||||
awaitAll(
|
||||
async {
|
||||
raApi.getGameInfo(gameId).map { game ->
|
||||
game.achievements
|
||||
.filter {
|
||||
it.type == RAAchievement.Type.CORE
|
||||
}
|
||||
}
|
||||
},
|
||||
async {
|
||||
raApi.getUserUnlockedAchievements(gameId, false)
|
||||
},
|
||||
)
|
||||
}
|
||||
val gameSetMetadata = achievementsDao.getGameSetMetadata(gameId.id)
|
||||
val currentMetadata = CurrentGameSetMetadata(gameId, gameSetMetadata)
|
||||
|
||||
val gameAchievementsResult = fetchGameAchievements(gameId, currentMetadata)
|
||||
if (gameAchievementsResult.isFailure) {
|
||||
return Result.failure(gameAchievementsResult.exceptionOrNull()!!)
|
||||
}
|
||||
|
||||
val userUnlocksResult = fetchGameUserUnlockedAchievements(gameId, currentMetadata)
|
||||
if (userUnlocksResult.isFailure) {
|
||||
return Result.failure(userUnlocksResult.exceptionOrNull()!!)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val gameAchievements = (gameAchievementsResult as Result<List<RAAchievement>>).getOrThrow()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val userUnlocks = (userUnlocksResult as Result<List<Long>>).getOrThrow()
|
||||
val gameAchievements = gameAchievementsResult.getOrThrow()
|
||||
val userUnlocks = userUnlocksResult.getOrThrow()
|
||||
|
||||
val userAchievements = gameAchievements.map {
|
||||
RAUserAchievement(
|
||||
@ -60,4 +53,87 @@ class AndroidRetroAchievementsRepository(
|
||||
}
|
||||
return Result.success(userAchievements)
|
||||
}
|
||||
|
||||
private suspend fun fetchGameAchievements(gameId: RAGameId, gameSetMetadata: CurrentGameSetMetadata): Result<List<RAAchievement>> {
|
||||
return if (mustRefreshAchievementSet(gameSetMetadata.currentMetadata)) {
|
||||
raApi.getGameInfo(gameId).map { game ->
|
||||
game.achievements
|
||||
}.onSuccess { achievements ->
|
||||
val achievementEntities = achievements.map {
|
||||
it.mapToEntity(gameId)
|
||||
}
|
||||
|
||||
val newMetadata = gameSetMetadata.withNewAchievementSetUpdate()
|
||||
achievementsDao.updateGameAchievements(achievementEntities)
|
||||
achievementsDao.updateGameSetMetadata(newMetadata)
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
achievementsDao.getGameAchievements(gameId.id).map {
|
||||
it.mapToModel()
|
||||
}
|
||||
}
|
||||
}.map { achievements ->
|
||||
achievements.filter { it.type == RAAchievement.Type.CORE }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchGameUserUnlockedAchievements(gameId: RAGameId, gameSetMetadata: CurrentGameSetMetadata): Result<List<Long>> {
|
||||
return if (mustRefreshUserData(gameSetMetadata.currentMetadata)) {
|
||||
raApi.getUserUnlockedAchievements(gameId, false).onSuccess { userUnlocks ->
|
||||
val userAchievementEntities = userUnlocks.map {
|
||||
RAUserAchievementEntity(
|
||||
gameId.id,
|
||||
it,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
val newMetadata = gameSetMetadata.withNewUserAchievementsUpdate()
|
||||
achievementsDao.updateGameUserUnlockedAchievements(gameId.id, userAchievementEntities)
|
||||
achievementsDao.updateGameSetMetadata(newMetadata)
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
achievementsDao.getGameUserUnlockedAchievements(gameId.id).map {
|
||||
it.achievementId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mustRefreshAchievementSet(gameSetMetadata: RAGameSetMetadata?): Boolean {
|
||||
if (gameSetMetadata?.lastAchievementSetUpdated == null) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Update the achievement set once a week
|
||||
return Duration.between(gameSetMetadata.lastAchievementSetUpdated, Instant.now()) >= Duration.ofDays(7)
|
||||
}
|
||||
|
||||
private fun mustRefreshUserData(gameSetMetadata: RAGameSetMetadata?): Boolean {
|
||||
if (gameSetMetadata?.lastUserDataUpdated == null) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Sync user achievement data once a day
|
||||
return Duration.between(gameSetMetadata.lastUserDataUpdated, Instant.now()) >= Duration.ofDays(1)
|
||||
}
|
||||
|
||||
private class CurrentGameSetMetadata(private val gameId: RAGameId, initialMetadata: RAGameSetMetadata?) {
|
||||
var currentMetadata = initialMetadata
|
||||
private set
|
||||
|
||||
fun withNewAchievementSetUpdate(): RAGameSetMetadata {
|
||||
return (currentMetadata?.copy(lastAchievementSetUpdated = Instant.now()) ?: RAGameSetMetadata(gameId.id, Instant.now(), null)).also {
|
||||
currentMetadata = it
|
||||
}
|
||||
}
|
||||
|
||||
fun withNewUserAchievementsUpdate(): RAGameSetMetadata {
|
||||
return (currentMetadata?.copy(lastUserDataUpdated = Instant.now()) ?: RAGameSetMetadata(gameId.id, null, Instant.now())).also {
|
||||
currentMetadata = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package me.magnum.melonds.impl.mappers.retroachievements
|
||||
|
||||
import me.magnum.melonds.database.entities.retroachievements.RAAchievementEntity
|
||||
import me.magnum.rcheevosapi.model.RAAchievement
|
||||
import me.magnum.rcheevosapi.model.RAGameId
|
||||
import java.net.URL
|
||||
|
||||
fun RAAchievement.mapToEntity(gameId: RAGameId): RAAchievementEntity {
|
||||
return RAAchievementEntity(
|
||||
id,
|
||||
gameId.id,
|
||||
totalAwardsCasual,
|
||||
totalAwardsHardcore,
|
||||
title,
|
||||
description,
|
||||
points,
|
||||
displayOrder,
|
||||
badgeUrlUnlocked.toString(),
|
||||
badgeUrlLocked.toString(),
|
||||
memoryAddress,
|
||||
type.ordinal,
|
||||
)
|
||||
}
|
||||
|
||||
fun RAAchievementEntity.mapToModel(): RAAchievement {
|
||||
return RAAchievement(
|
||||
id,
|
||||
totalAwardsCasual,
|
||||
totalAwardsHardcore,
|
||||
title,
|
||||
description,
|
||||
points,
|
||||
displayOrder,
|
||||
URL(badgeUrlUnlocked),
|
||||
URL(badgeUrlLocked),
|
||||
memoryAddress,
|
||||
RAAchievement.Type.values()[type],
|
||||
)
|
||||
}
|
@ -3,4 +3,11 @@
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">nus.cdn.t.shop.nintendowifi.net</domain>
|
||||
</domain-config>
|
||||
|
||||
<debug-overrides>
|
||||
<trust-anchors>
|
||||
<certificates src="user" />
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</debug-overrides>
|
||||
</network-security-config>
|
||||
|
@ -66,6 +66,7 @@ object Dependencies {
|
||||
const val preference = "androidx.preference:preference-ktx:${Versions.Preference}"
|
||||
const val recyclerView = "androidx.recyclerview:recyclerview:${Versions.RecyclerView}"
|
||||
const val room = "androidx.room:room-runtime:${Versions.Room}"
|
||||
const val roomKtx = "androidx.room:room-ktx:${Versions.Room}"
|
||||
const val roomRxJava = "androidx.room:room-rxjava2:${Versions.Room}"
|
||||
const val splashscreen = "androidx.core:core-splashscreen:${Versions.Splashscreen}"
|
||||
const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:${Versions.SwipeRefreshLayout}"
|
||||
|
Loading…
Reference in New Issue
Block a user