diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 64fad1e5..7b864994 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -139,11 +139,27 @@ + android:theme="@style/AppTheme"> + + + android:theme="@style/AppTheme"> + + + + + + openRateUsLink("Drawer") R.id.share_now -> shareCurrentGame() R.id.previous_games -> openSaveHistory() + R.id.stats -> openStats() R.id.install_new -> installFromInstantApp() else -> handled = false } @@ -373,6 +375,13 @@ class GameActivity : DaggerAppCompatActivity() { } } + private fun openStats() { + analyticsManager.sentEvent(Analytics.OpenStats()) + Intent(this, StatsActivity::class.java).apply { + startActivity(this) + } + } + private fun showSettings() { analyticsManager.sentEvent(Analytics.OpenSettings()) Intent(this, PreferencesActivity::class.java).apply { diff --git a/app/src/main/java/dev/lucasnlm/antimine/about/AboutActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/about/AboutActivity.kt index 3640c1c7..b99236b3 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/about/AboutActivity.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/about/AboutActivity.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import android.view.MenuItem import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.Observer @@ -22,7 +21,7 @@ class AboutActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_empty) - bindToolbar() + setTitle(R.string.about) aboutViewModel = ViewModelProviders.of(this).get(AboutViewModel::class.java) @@ -60,23 +59,6 @@ class AboutActivity : AppCompatActivity() { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(SOURCE_CODE))) } - override fun onOptionsItemSelected(item: MenuItem): Boolean = - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } - - private fun bindToolbar() { - supportActionBar?.apply { - setTitle(R.string.about) - setDisplayHomeAsUpEnabled(true) - setHomeButtonEnabled(true) - } - } - companion object { private const val SOURCE_CODE = "https://github.com/lucasnlm/antimine-android" } diff --git a/app/src/main/java/dev/lucasnlm/antimine/about/TextActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/about/TextActivity.kt index 6b2005ef..203f4ef0 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/about/TextActivity.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/about/TextActivity.kt @@ -2,7 +2,6 @@ package dev.lucasnlm.antimine.about import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import android.view.MenuItem import android.view.View import dev.lucasnlm.antimine.R @@ -16,8 +15,8 @@ class TextActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + title = intent.getStringExtra(Constants.TEXT_TITLE) setContentView(R.layout.activity_text) - bindToolbar() GlobalScope.launch { withContext(Dispatchers.Main) { @@ -41,23 +40,4 @@ class TextActivity : AppCompatActivity() { } } } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - var handled = false - - if (item.itemId == android.R.id.home) { - onBackPressed() - handled = true - } - - return handled || super.onOptionsItemSelected(item) - } - - private fun bindToolbar() { - supportActionBar?.apply { - title = intent.getStringExtra(Constants.TEXT_TITLE) - setDisplayHomeAsUpEnabled(true) - setHomeButtonEnabled(true) - } - } } diff --git a/app/src/main/java/dev/lucasnlm/antimine/di/ActivityModule.kt b/app/src/main/java/dev/lucasnlm/antimine/di/ActivityModule.kt index 830267ae..ef39ba9d 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/di/ActivityModule.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/di/ActivityModule.kt @@ -6,6 +6,7 @@ import dagger.android.ContributesAndroidInjector import dev.lucasnlm.antimine.TvGameActivity import dev.lucasnlm.antimine.core.scope.ActivityScope import dev.lucasnlm.antimine.history.views.HistoryFragment +import dev.lucasnlm.antimine.stats.StatsActivity @Module interface ActivityModule { @@ -17,6 +18,10 @@ interface ActivityModule { @ContributesAndroidInjector fun contributeHistoryFragmentInjector(): HistoryFragment + @ActivityScope + @ContributesAndroidInjector + fun contributeStatsActivityInjector(): StatsActivity + @ActivityScope @ContributesAndroidInjector fun contributeTvGameActivityInjector(): TvGameActivity diff --git a/app/src/main/java/dev/lucasnlm/antimine/stats/StatsActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/stats/StatsActivity.kt new file mode 100644 index 00000000..b5afd585 --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/stats/StatsActivity.kt @@ -0,0 +1,46 @@ +package dev.lucasnlm.antimine.stats + +import android.os.Bundle +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import dagger.android.support.DaggerAppCompatActivity +import dev.lucasnlm.antimine.R +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository +import dev.lucasnlm.antimine.stats.viewmodel.StatsViewModel +import kotlinx.android.synthetic.main.activity_stats.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class StatsActivity : DaggerAppCompatActivity() { + @Inject + lateinit var statsRepository: IStatsRepository + + private lateinit var viewModel: StatsViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_stats) + setTitle(R.string.events) + + viewModel = ViewModelProviders.of(this).get(StatsViewModel::class.java) + viewModel.statsObserver.observe(this, Observer { + minesCount.text = it.mines.toString() + totalTime.text = formatTime(it.duration) + averageTime.text = formatTime(it.averageDuration) + totalGames.text = it.totalGames.toString() + performance.text = formatPercentage(100.0 * it.victory / it.totalGames) + openAreas.text = it.openArea.toString() + }) + + GlobalScope.launch { + viewModel.loadStats(statsRepository) + } + } + + private fun formatPercentage(value: Double) = + String.format("%.2f%%", value) + + private fun formatTime(durationSecs: Long) = + String.format("%02d:%02d:%02d", durationSecs / 3600, durationSecs % 3600 / 60, durationSecs % 60) +} diff --git a/app/src/main/java/dev/lucasnlm/antimine/stats/model/StatsModel.kt b/app/src/main/java/dev/lucasnlm/antimine/stats/model/StatsModel.kt new file mode 100644 index 00000000..6ec23f64 --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/stats/model/StatsModel.kt @@ -0,0 +1,10 @@ +package dev.lucasnlm.antimine.stats.model + +data class StatsModel( + val totalGames: Int, + val duration: Long, + val averageDuration: Long, + val mines: Int, + val victory: Int, + val openArea: Int +) diff --git a/app/src/main/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModel.kt b/app/src/main/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModel.kt new file mode 100644 index 00000000..dde6b28a --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModel.kt @@ -0,0 +1,41 @@ +package dev.lucasnlm.antimine.stats.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository +import dev.lucasnlm.antimine.stats.model.StatsModel + +class StatsViewModel : ViewModel() { + val statsObserver = MutableLiveData() + + suspend fun getStatsModel(statsRepository: IStatsRepository): StatsModel? { + val stats = statsRepository.getAllStats() + val statsCount = stats.count() + + return if (statsCount > 0) { + val result = stats.fold( + StatsModel(statsCount, 0L, 0L, 0, 0, 0) + ) { acc, value -> + StatsModel( + acc.totalGames, + acc.duration + value.duration, + 0, + acc.mines + value.mines, + acc.victory + value.victory, + acc.openArea + value.openArea + ) + } + result.copy(averageDuration = result.duration / result.totalGames) + } else { + StatsModel(0, 0, 0, 0, 0, 0) + } + } + + suspend fun loadStats(statsRepository: IStatsRepository) { + getStatsModel(statsRepository)?.let { + if (it.totalGames > 0) { + statsObserver.postValue(it) + } + } + } +} diff --git a/app/src/main/res/layout/activity_stats.xml b/app/src/main/res/layout/activity_stats.xml new file mode 100644 index 00000000..5b79b190 --- /dev/null +++ b/app/src/main/res/layout/activity_stats.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_history.xml b/app/src/main/res/layout/fragment_history.xml index 569c9360..9904ea16 100644 --- a/app/src/main/res/layout/fragment_history.xml +++ b/app/src/main/res/layout/fragment_history.xml @@ -8,6 +8,34 @@ android:fitsSystemWindows="false" tools:context="dev.lucasnlm.antimine.history.views.HistoryFragment"> + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/divider" /> diff --git a/app/src/main/res/menu/nav_menu.xml b/app/src/main/res/menu/nav_menu.xml index 2bd92f29..6a979a10 100644 --- a/app/src/main/res/menu/nav_menu.xml +++ b/app/src/main/res/menu/nav_menu.xml @@ -45,6 +45,11 @@ android:checkable="false" android:title="@string/previous_games" /> + + , savesRepository: ISavesRepository, + statsRepository: IStatsRepository, dimensionRepository: IDimensionRepository, preferencesRepository: IPreferencesRepository, hapticFeedbackInteractor: IHapticFeedbackInteractor, @@ -45,6 +48,7 @@ class TestLevelModule( application, eventObserver, savesRepository, + statsRepository, dimensionRepository, preferencesRepository, hapticFeedbackInteractor, @@ -62,6 +66,9 @@ class TestLevelModule( @Provides override fun provideSavesRepository(): ISavesRepository = MockSavesRepository() + @Provides + override fun provideStatsRepository(): IStatsRepository = MockStatsRepository(listOf()) + @Provides override fun provideMinefieldRepository(): IMinefieldRepository = MockMinefieldRepository() diff --git a/app/src/test/java/dev/lucasnlm/antimine/mocks/MockStatsRepository.kt b/app/src/test/java/dev/lucasnlm/antimine/mocks/MockStatsRepository.kt new file mode 100644 index 00000000..814d0a0e --- /dev/null +++ b/app/src/test/java/dev/lucasnlm/antimine/mocks/MockStatsRepository.kt @@ -0,0 +1,14 @@ +package dev.lucasnlm.antimine.mocks + +import dev.lucasnlm.antimine.common.level.database.models.Stats +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository + +class MockStatsRepository( + private val list: List +) : IStatsRepository { + override suspend fun getAllStats(): List = list + + override suspend fun addStats(stats: Stats): Long? { + return null + } +} diff --git a/app/src/test/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModelTest.kt b/app/src/test/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModelTest.kt new file mode 100644 index 00000000..11c39216 --- /dev/null +++ b/app/src/test/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModelTest.kt @@ -0,0 +1,76 @@ +package dev.lucasnlm.antimine.stats.viewmodel + +import dev.lucasnlm.antimine.common.level.database.models.Stats +import dev.lucasnlm.antimine.mocks.MockStatsRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class StatsViewModelTest { + private val listOfStats = listOf( + Stats(0, 1000, 10, 1, 10, 10, 90), + Stats(1, 1200, 24, 0, 10, 10, 20) + ) + + @Test + fun testStatsTotalGames() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(2, statsModel?.totalGames) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.totalGames) + } + + @Test + fun testStatsDuration() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(2200L, statsModel?.duration) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0L, emptyStatsModel?.duration) + } + + @Test + fun testStatsAverageDuration() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(1100L, statsModel?.averageDuration) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0L, emptyStatsModel?.averageDuration) + } + + @Test + fun testStatsMines() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(34, statsModel?.mines) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.mines) + } + + @Test + fun testVictory() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(1, statsModel?.victory) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.victory) + } + + @Test + fun testOpenArea() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(110, statsModel?.openArea) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.openArea) + } +} diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt index cdd09f4f..8a99c5d1 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt @@ -2,6 +2,7 @@ package dev.lucasnlm.antimine.common.level import dev.lucasnlm.antimine.common.level.database.models.Save import dev.lucasnlm.antimine.common.level.database.models.SaveStatus +import dev.lucasnlm.antimine.common.level.database.models.Stats import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Mark @@ -354,6 +355,27 @@ class LevelFacade { ) } + fun getStats(duration: Long): Stats? { + val gameStatus: SaveStatus = when { + checkVictory() -> SaveStatus.VICTORY + hasAnyMineExploded() -> SaveStatus.DEFEAT + else -> SaveStatus.ON_GOING + } + return if (gameStatus == SaveStatus.ON_GOING) { + null + } else { + Stats( + 0, + duration, + mines.count(), + if (gameStatus == SaveStatus.VICTORY) 1 else 0, + minefield.width, + minefield.height, + mines.count { !it.isCovered } + ) + } + } + fun setCurrentSaveId(id: Int) { this.saveId = id.coerceAtLeast(0) } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/database/AppDataBase.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/AppDataBase.kt index 3be4a191..c6711788 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/database/AppDataBase.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/AppDataBase.kt @@ -8,12 +8,15 @@ import dev.lucasnlm.antimine.common.level.database.converters.FieldConverter import dev.lucasnlm.antimine.common.level.database.converters.MinefieldConverter import dev.lucasnlm.antimine.common.level.database.converters.SaveStatusConverter import dev.lucasnlm.antimine.common.level.database.dao.SaveDao +import dev.lucasnlm.antimine.common.level.database.dao.StatsDao import dev.lucasnlm.antimine.common.level.database.models.Save +import dev.lucasnlm.antimine.common.level.database.models.Stats @Database( entities = [ - Save::class - ], version = 2, exportSchema = false + Save::class, + Stats::class + ], version = 3, exportSchema = false ) @TypeConverters( FieldConverter::class, @@ -23,4 +26,5 @@ import dev.lucasnlm.antimine.common.level.database.models.Save ) abstract class AppDataBase : RoomDatabase() { abstract fun saveDao(): SaveDao + abstract fun statsDao(): StatsDao } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/database/dao/StatsDao.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/dao/StatsDao.kt new file mode 100644 index 00000000..62414a7c --- /dev/null +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/dao/StatsDao.kt @@ -0,0 +1,16 @@ +package dev.lucasnlm.antimine.common.level.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import dev.lucasnlm.antimine.common.level.database.models.Stats + +@Dao +interface StatsDao { + @Query("SELECT * FROM stats") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(vararg stats: Stats): Array +} diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/database/models/Stats.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/models/Stats.kt new file mode 100644 index 00000000..49b8a378 --- /dev/null +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/models/Stats.kt @@ -0,0 +1,29 @@ +package dev.lucasnlm.antimine.common.level.database.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class Stats( + @PrimaryKey(autoGenerate = true) + val uid: Int, + + @ColumnInfo(name = "duration") + val duration: Long, + + @ColumnInfo(name = "mines") + val mines: Int, + + @ColumnInfo(name = "victory") + val victory: Int, + + @ColumnInfo(name = "width") + val width: Int, + + @ColumnInfo(name = "height") + val height: Int, + + @ColumnInfo(name = "openArea") + val openArea: Int +) diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/di/LevelModule.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/di/LevelModule.kt index 0d0dc70a..bceeb7ea 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/di/LevelModule.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/di/LevelModule.kt @@ -12,8 +12,10 @@ import dev.lucasnlm.antimine.common.level.repository.DimensionRepository import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository import dev.lucasnlm.antimine.common.level.repository.SavesRepository +import dev.lucasnlm.antimine.common.level.repository.StatsRepository import dev.lucasnlm.antimine.common.level.utils.Clock import dev.lucasnlm.antimine.common.level.utils.HapticFeedbackInteractor import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor @@ -35,10 +37,18 @@ open class LevelModule( appDataBase.saveDao() } + private val statsDao by lazy { + appDataBase.statsDao() + } + private val savesRepository by lazy { SavesRepository(savesDao) } + private val statsRepository by lazy { + StatsRepository(statsDao) + } + @Provides open fun provideGameEventObserver(): MutableLiveData = MutableLiveData() @@ -50,6 +60,7 @@ open class LevelModule( application: Application, eventObserver: MutableLiveData, savesRepository: ISavesRepository, + statsRepository: IStatsRepository, dimensionRepository: IDimensionRepository, preferencesRepository: IPreferencesRepository, hapticFeedbackInteractor: IHapticFeedbackInteractor, @@ -60,6 +71,7 @@ open class LevelModule( application, eventObserver, savesRepository, + statsRepository, dimensionRepository, preferencesRepository, hapticFeedbackInteractor, @@ -78,6 +90,9 @@ open class LevelModule( @Provides open fun provideSavesRepository(): ISavesRepository = savesRepository + @Provides + open fun provideStatsRepository(): IStatsRepository = statsRepository + @Provides open fun provideMinefieldRepository(): IMinefieldRepository = MinefieldRepository() diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/repository/StatsRepository.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/repository/StatsRepository.kt new file mode 100644 index 00000000..e038c618 --- /dev/null +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/repository/StatsRepository.kt @@ -0,0 +1,21 @@ +package dev.lucasnlm.antimine.common.level.repository + +import dev.lucasnlm.antimine.common.level.database.dao.StatsDao +import dev.lucasnlm.antimine.common.level.database.models.Stats + +interface IStatsRepository { + suspend fun getAllStats(): List + suspend fun addStats(stats: Stats): Long? +} + +class StatsRepository( + private val statsDao: StatsDao +) : IStatsRepository { + override suspend fun getAllStats(): List { + return statsDao.getAll() + } + + override suspend fun addStats(stats: Stats): Long? { + return statsDao.insertAll(stats).firstOrNull() + } +} diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt index 09f87271..e33a986a 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt @@ -13,6 +13,7 @@ import dev.lucasnlm.antimine.common.level.database.models.Save import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository import dev.lucasnlm.antimine.common.level.utils.Clock import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor import dev.lucasnlm.antimine.core.analytics.AnalyticsManager @@ -28,6 +29,7 @@ class GameViewModel( val application: Application, val eventObserver: MutableLiveData, private val savesRepository: ISavesRepository, + private val statsRepository: IStatsRepository, private val dimensionRepository: IDimensionRepository, private val preferencesRepository: IPreferencesRepository, private val hapticFeedbackInteractor: IHapticFeedbackInteractor, @@ -146,6 +148,14 @@ class GameViewModel( } } + private suspend fun saveStats() { + if (initialized && levelFacade.hasMines) { + levelFacade.getStats(elapsedTimeSeconds.value ?: 0L)?.let { + statsRepository.addStats(it) + } + } + } + fun resumeGame() { if (initialized && levelFacade.hasMines && !levelFacade.isGameOver()) { eventObserver.postValue(Event.Resume) @@ -303,6 +313,7 @@ class GameViewModel( } GlobalScope.launch { + saveStats() saveGame() } } @@ -321,6 +332,7 @@ class GameViewModel( } GlobalScope.launch { + saveStats() saveGame() } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModelFactory.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModelFactory.kt index 694e3043..70e5f015 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModelFactory.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModelFactory.kt @@ -8,6 +8,7 @@ import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository import dev.lucasnlm.antimine.common.level.utils.Clock import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor import dev.lucasnlm.antimine.core.analytics.AnalyticsManager @@ -18,6 +19,7 @@ class GameViewModelFactory @Inject constructor( private val application: Application, private val eventObserver: MutableLiveData, private val savesRepository: ISavesRepository, + private val statsRepository: IStatsRepository, private val dimensionRepository: IDimensionRepository, private val preferencesRepository: IPreferencesRepository, private val hapticFeedbackInteractor: IHapticFeedbackInteractor, @@ -33,6 +35,7 @@ class GameViewModelFactory @Inject constructor( application, eventObserver, savesRepository, + statsRepository, dimensionRepository, preferencesRepository, hapticFeedbackInteractor, diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt index e09285ba..aa155f1b 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt @@ -65,6 +65,8 @@ sealed class Analytics( class OpenAbout : Analytics("Open About") + class OpenStats : Analytics("Open Stats") + class OpenSettings : Analytics("Open Settings") class OpenSaveHistory : Analytics("Open Save History") diff --git a/common/src/main/res/values-cs/strings.xml b/common/src/main/res/values-cs/strings.xml index c4c57ffe..08496b46 100644 --- a/common/src/main/res/values-cs/strings.xml +++ b/common/src/main/res/values-cs/strings.xml @@ -3,6 +3,7 @@ Anti-Mine Musíte vyčistit obdélníkovou desku obsahující skryté miny, aniž by kterákoliv z nich vybuchla. Zbývající miny + Hry Předchozí hry Instalovat Obtížnost @@ -94,4 +95,8 @@ Pokud se vám tato hra líbí, dejte nám prosím zpětnou vazbu. Hodně nám to pomůže. Ano ❤️️️ Ne + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-de/strings.xml b/common/src/main/res/values-de/strings.xml index 929ce62e..701ab10e 100644 --- a/common/src/main/res/values-de/strings.xml +++ b/common/src/main/res/values-de/strings.xml @@ -3,6 +3,7 @@ Anti-Mine Du musst eine rechteckige Tafel mit versteckten minen räumen, ohne dass eine davon explodiert. Verbleibende Minen + Games Vorherige Spiele Installieren Schwierigkeitsgrad @@ -94,4 +95,8 @@ Wenn dir dieses Spiel gefällt, gib uns bitte eine Rückmeldung. Es wird uns sehr helfen. Ja ❤️️️ Nein + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-el/strings.xml b/common/src/main/res/values-el/strings.xml index fd64362c..eec2abe2 100644 --- a/common/src/main/res/values-el/strings.xml +++ b/common/src/main/res/values-el/strings.xml @@ -3,6 +3,7 @@ Antimine Πρέπει να καθαρίσετε μια ορθογώνια πλακέτα που περιέχει κρυμμένες \"νάρκες\" χωρίς να πυροδοτήσετε καμία από αυτές. Υπόλοιπες νάρκες + Games Προηγούμενα Παιχνίδια Εγκατάσταση Δυσκολία @@ -94,4 +95,8 @@ Αν σας αρέσει αυτό το παιχνίδι, παρακαλούμε δώστε μας τα σχόλιά σας. Θα μας βοηθήσει πολύ. Ναι ❤️️️ Όχι + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index 50c9612f..c4f337a1 100644 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -3,6 +3,7 @@ Anti-Mina Usted tiene que limpiar un tablero cuadrado que contiene minas escondidas sin detornarlas. Minas restantes + Games Juegos anteriores Instalar Dificultad @@ -78,7 +79,7 @@ Asistente de Juego Área cubierta Área marcada - Area dudosa + Área dudosa Área marcada incorrectamente General Vibrar al activar la explosión o la bandera @@ -94,4 +95,8 @@ Si te gusta este juego, por favor danos un comentario. Nos ayudará mucho. Sí ❤️️️ No + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-fr/strings.xml b/common/src/main/res/values-fr/strings.xml index 47283c3b..a0d3751f 100644 --- a/common/src/main/res/values-fr/strings.xml +++ b/common/src/main/res/values-fr/strings.xml @@ -3,6 +3,7 @@ Anti-Mine Vous devez vider un tableau rectangulaire contenant des mines cachées sans en détonner. Mines restantes + Games Parties précédentes Installer Difficulté @@ -94,4 +95,8 @@ Si vous aimez ce jeu, n\'hésitez pas à jour donner un retour. Ça nous serait très utile. Oui ❤️ Non + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml index 850e8273..50ea3cac 100644 --- a/common/src/main/res/values-it/strings.xml +++ b/common/src/main/res/values-it/strings.xml @@ -3,6 +3,7 @@ Antimine L\'obbiettivo del gioco è ripulire un campo rettangolare che contiene mine nascoste senza detonarne nessuna. Mine rimanenti + Games Previous Games Installare Difficoltà @@ -94,4 +95,8 @@ Se ti piace questo gioco, per favore inviaci suggerimenti. Puoi aiutare a migliorarlo. Sì ❤️️️ No + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index ad51ece7..e05ee0f7 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -3,6 +3,7 @@ Anti-Mine Encontre todas as minas escondidas no campo minado. Minas Restantes + Games Jogos Anteriores Instalar Dificuldade @@ -94,4 +95,8 @@ Se você está gostando do jogo, por favor deixe um comentário! Isso nos ajuda muito. Sim ❤️️️ Não + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index 56199323..fdbfc807 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -3,6 +3,7 @@ Anti-Mine Вам необходимо расчистить прямоугольную площадь со спрятанными минами, не взорвав ни одну из них. Мин осталось + Games Предыдущая партия Установить Сложность @@ -94,4 +95,8 @@ Нравится игра? Оставьте отзыв, пожалуйста. Это нам очень поможет. Да ❤️️️ Нет + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index 9236c6ae..7d3ec8d7 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -3,6 +3,7 @@ Anti-Mine Hiçbirini patlatmadan gizli mayın içeren dikdörtgen bir tahtayı temizlemelisiniz. Kalan mayınlar + Games Önceki Oyun Yükle Zorluk @@ -94,4 +95,8 @@ Bu oyunu beğendiyseniz, lütfen bize bir geri bildirim verin. Bize çok yardımcı olacak. Evet ❤️️️ Hayır + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-uk/strings.xml b/common/src/main/res/values-uk/strings.xml index aacaddb4..a64ba993 100644 --- a/common/src/main/res/values-uk/strings.xml +++ b/common/src/main/res/values-uk/strings.xml @@ -3,6 +3,7 @@ Сапер Вам потрібно очистити поле від схованих мін, не детонуючи їх. Залишилося мін + Games Попередня гра Встановити Складність @@ -94,4 +95,8 @@ Якщо вам подобається ця гра, то залиште нам свій відгук. Це нам дуже допоможе. Так ❤️️️ Ні + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-vi/strings.xml b/common/src/main/res/values-vi/strings.xml index 854671ed..33e5cc05 100644 --- a/common/src/main/res/values-vi/strings.xml +++ b/common/src/main/res/values-vi/strings.xml @@ -3,12 +3,15 @@ Dò mìn Bạn phải mở tất cả các ô trên một bãi mìn mà không làm nổ cục mìn nào. Số mìn còn lại + Games + Previous Games Cài đặt Độ khó Tiêu chuẩn Dễ Trung bình Khó + Mở Mở menu Đóng menu Cài đặt @@ -92,4 +95,8 @@ Nếu bạn thích trò chơi này, hãy gửi phản hồi cho chúng tôi. Nhận xét của bạn sẽ giúp chúng tôi rất nhiều. Có ❤️️️ Không + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values-zh/strings.xml b/common/src/main/res/values-zh/strings.xml index d907a2e2..d4238311 100644 --- a/common/src/main/res/values-zh/strings.xml +++ b/common/src/main/res/values-zh/strings.xml @@ -3,6 +3,7 @@ 反雷 - 扫雷 你需要清除一个隐藏着地雷的矩形面板,不能使任何地雷爆炸。 剩余地雷 + Games 前一盘棋 安装 难度 @@ -94,4 +95,8 @@ 如果你喜欢这个游戏,请给我们反馈。这将对我们有很多帮助。 是 ❤️️️ 取消 + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index a3bacd6b..a6e4aff7 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,8 +1,9 @@ Antimine - You have to clear a rectangular board containing hidden \"mines\" without detonating any of them. + You have to clear a rectangular board containing hidden mines without detonating any of them. Remaining mines + Games Previous Games Install Difficulty @@ -94,4 +95,8 @@ If you like this game, please give us a feedback. It will help us a lot. Yes ❤️️️ No + Open Areas + Total Time + Average Time + Performance diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/di/TestLevelModule.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/di/TestLevelModule.kt index 70035fd9..ab9e8d08 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/di/TestLevelModule.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/di/TestLevelModule.kt @@ -6,12 +6,14 @@ import androidx.lifecycle.MutableLiveData import dagger.Module import dagger.Provides import dev.lucasnlm.antimine.common.level.database.models.Save +import dev.lucasnlm.antimine.common.level.database.models.Stats import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository import dev.lucasnlm.antimine.common.level.repository.Size import dev.lucasnlm.antimine.common.level.utils.Clock @@ -35,6 +37,7 @@ class TestLevelModule( application: Application, eventObserver: MutableLiveData, savesRepository: ISavesRepository, + statsRepository: IStatsRepository, dimensionRepository: IDimensionRepository, preferencesRepository: IPreferencesRepository, hapticFeedbackInteractor: IHapticFeedbackInteractor, @@ -45,6 +48,7 @@ class TestLevelModule( application, eventObserver, savesRepository, + statsRepository, dimensionRepository, preferencesRepository, hapticFeedbackInteractor, @@ -91,6 +95,13 @@ class TestLevelModule( override fun randomSeed(): Long = 200 } + @Provides + fun provideStatsRepository(): IStatsRepository = object : IStatsRepository { + override suspend fun getAllStats(): List = listOf() + + override suspend fun addStats(stats: Stats): Long? = null + } + @Provides fun provideHapticFeedbackInteractor( application: Application,