Merge pull request #130 from lucasnlm/add-clean-stats

Add clean stats
This commit is contained in:
Lucas Nunes 2020-07-10 00:21:09 -03:00 committed by GitHub
commit cd3d0008a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 241 additions and 59 deletions

View file

@ -9,8 +9,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 702051
versionName '7.2.5'
versionCode 702061
versionName '7.2.6'
minSdkVersion 16
targetSdkVersion 30
multiDexEnabled true

View file

@ -1,12 +1,16 @@
package dev.lucasnlm.antimine.stats
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.stats.model.StatsModel
import dev.lucasnlm.antimine.stats.viewmodel.StatsViewModel
import kotlinx.android.synthetic.main.activity_stats.*
import kotlinx.coroutines.GlobalScope
@ -22,25 +26,76 @@ class StatsActivity : AppCompatActivity(R.layout.activity_stats) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
refreshStats(StatsViewModel.emptyStats)
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()
victory.text = it.victory.toString()
defeat.text = (it.totalGames - it.victory).toString()
refreshStats(it)
}
)
GlobalScope.launch {
viewModel.loadStats(statsRepository)
viewModel.loadStats()
}
}
private fun refreshStats(stats: StatsModel) {
if (stats.totalGames > 0) {
minesCount.text = stats.mines.toString()
totalTime.text = formatTime(stats.duration)
averageTime.text = formatTime(stats.averageDuration)
totalGames.text = stats.totalGames.toString()
performance.text = formatPercentage(100.0 * stats.victory / stats.totalGames)
openAreas.text = stats.openArea.toString()
victory.text = stats.victory.toString()
defeat.text = (stats.totalGames - stats.victory).toString()
} else {
val emptyText = "-"
totalGames.text = "0"
minesCount.text = emptyText
totalTime.text = emptyText
averageTime.text = emptyText
performance.text = emptyText
openAreas.text = emptyText
victory.text = emptyText
defeat.text = emptyText
}
invalidateOptionsMenu()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
viewModel.statsObserver.value?.let {
if (it.totalGames > 0) {
menuInflater.inflate(R.menu.stats_menu, menu)
}
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.delete) {
confirmAndDelete()
true
} else {
super.onOptionsItemSelected(item)
}
}
private fun confirmAndDelete() {
AlertDialog.Builder(this)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.delete_all_message)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete_all) { _, _ ->
GlobalScope.launch {
viewModel.deleteAll()
}
}
.show()
}
companion object {
private fun formatPercentage(value: Double) =
String.format("%.2f%%", value)

View file

@ -1,15 +1,21 @@
package dev.lucasnlm.antimine.stats.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.stats.model.StatsModel
class StatsViewModel : ViewModel() {
class StatsViewModel @ViewModelInject constructor(
private val statsRepository: IStatsRepository,
private val preferenceRepository: IPreferencesRepository
) : ViewModel() {
val statsObserver = MutableLiveData<StatsModel>()
suspend fun getStatsModel(statsRepository: IStatsRepository): StatsModel? {
val stats = statsRepository.getAllStats()
suspend fun getStatsModel(): StatsModel? {
val minId = preferenceRepository.getStatsBase()
val stats = statsRepository.getAllStats(minId)
val statsCount = stats.count()
return if (statsCount > 0) {
@ -27,15 +33,27 @@ class StatsViewModel : ViewModel() {
}
result.copy(averageDuration = result.duration / result.totalGames)
} else {
StatsModel(0, 0, 0, 0, 0, 0)
emptyStats
}
}
suspend fun loadStats(statsRepository: IStatsRepository) {
getStatsModel(statsRepository)?.let {
suspend fun deleteAll() {
statsRepository.getAllStats(0).lastOrNull()?.let {
preferenceRepository.updateStatsBase(it.uid + 1)
}
statsObserver.postValue(emptyStats)
}
suspend fun loadStats() {
getStatsModel()?.let {
if (it.totalGames > 0) {
statsObserver.postValue(it)
}
}
}
companion object {
val emptyStats = StatsModel(0, 0, 0, 0, 0, 0)
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V9c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v10zM18,4h-2.5l-0.71,-0.71c-0.18,-0.18 -0.44,-0.29 -0.7,-0.29H9.91c-0.26,0 -0.52,0.11 -0.7,0.29L8.5,4H6c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h12c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z"/>
</vector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".level.LevelActivity">
<item
android:id="@+id/delete"
android:icon="@drawable/delete"
android:title="@string/delete_all"
app:showAsAction="ifRoom" />
</menu>

View file

@ -2,9 +2,13 @@ package dev.lucasnlm.antimine.stats.viewmodel
import dev.lucasnlm.antimine.common.level.database.models.Stats
import dev.lucasnlm.antimine.common.level.repository.MemoryStatsRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
@ -14,63 +18,132 @@ class StatsViewModelTest {
Stats(1, 1200, 24, 0, 10, 10, 20)
)
private val prefsRepository: IPreferencesRepository = mockk()
@Before
fun setup() {
every { prefsRepository.getStatsBase() } returns 0
}
@Test
fun testStatsTotalGames() = runBlockingTest {
val viewModel = StatsViewModel()
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(2, statsModel?.totalGames)
}
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository())
assertEquals(0, emptyStatsModel?.totalGames)
@Test
fun testStatsTotalGamesWithBase() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
every { prefsRepository.getStatsBase() } returns 0
val statsModelBase0 = viewModel.getStatsModel()
assertEquals(2, statsModelBase0?.totalGames)
every { prefsRepository.getStatsBase() } returns 1
val statsModelBase1 = viewModel.getStatsModel()
assertEquals(1, statsModelBase1?.totalGames)
every { prefsRepository.getStatsBase() } returns 2
val statsModelBase2 = viewModel.getStatsModel()
assertEquals(0, statsModelBase2?.totalGames)
}
@Test
fun testStatsTotalGamesEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(0, statsModel?.totalGames)
}
@Test
fun testStatsDuration() = runBlockingTest {
val viewModel = StatsViewModel()
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
assertEquals(2200L, statsModel?.duration)
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository())
assertEquals(0L, emptyStatsModel?.duration)
assertEquals(2200L, statsModel?.duration)
}
@Test
fun testStatsDurationEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(0L, statsModel?.duration)
}
@Test
fun testStatsAverageDuration() = runBlockingTest {
val viewModel = StatsViewModel()
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
assertEquals(1100L, statsModel?.averageDuration)
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
assertEquals(0L, emptyStatsModel?.averageDuration)
assertEquals(1100L, statsModel?.averageDuration)
}
@Test
fun testStatsAverageDurationEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(0L, statsModel?.averageDuration)
}
@Test
fun testStatsMines() = runBlockingTest {
val viewModel = StatsViewModel()
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(34, statsModel?.mines)
}
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
assertEquals(0, emptyStatsModel?.mines)
@Test
fun testStatsMinesEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(0, statsModel?.mines)
}
@Test
fun testVictory() = runBlockingTest {
val viewModel = StatsViewModel()
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
assertEquals(1, statsModel?.victory)
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
assertEquals(0, emptyStatsModel?.victory)
assertEquals(1, statsModel?.victory)
}
@Test
fun testVictoryEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(0, statsModel?.victory)
}
@Test
fun testOpenArea() = runBlockingTest {
val viewModel = StatsViewModel()
val statsModel = viewModel.getStatsModel(MemoryStatsRepository(listOfStats.toMutableList()))
assertEquals(110, statsModel?.openArea)
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
val emptyStatsModel = viewModel.getStatsModel(MemoryStatsRepository(mutableListOf()))
assertEquals(0, emptyStatsModel?.openArea)
assertEquals(110, statsModel?.openArea)
}
@Test
fun testOpenAreaEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
val statsModel = viewModel.getStatsModel()
assertEquals(0, statsModel?.openArea)
}
}

View file

@ -9,8 +9,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 702051
versionName '7.2.5'
versionCode 702061
versionName '7.2.6'
minSdkVersion 16
targetSdkVersion 30
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -8,8 +8,8 @@ import dev.lucasnlm.antimine.common.level.database.models.Stats
@Dao
interface StatsDao {
@Query("SELECT * FROM stats")
suspend fun getAll(): List<Stats>
@Query("SELECT * FROM stats WHERE stats.uid >= :minId")
suspend fun getAll(minId: Int = 0): List<Stats>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(vararg stats: Stats): LongArray

View file

@ -4,15 +4,15 @@ import dev.lucasnlm.antimine.common.level.database.dao.StatsDao
import dev.lucasnlm.antimine.common.level.database.models.Stats
interface IStatsRepository {
suspend fun getAllStats(): List<Stats>
suspend fun getAllStats(minId: Int): List<Stats>
suspend fun addStats(stats: Stats): Long?
}
class StatsRepository(
private val statsDao: StatsDao
) : IStatsRepository {
override suspend fun getAllStats(): List<Stats> {
return statsDao.getAll()
override suspend fun getAllStats(minId: Int): List<Stats> {
return statsDao.getAll(minId)
}
override suspend fun addStats(stats: Stats): Long? {
@ -23,7 +23,7 @@ class StatsRepository(
class MemoryStatsRepository(
private val memoryStats: MutableList<Stats> = mutableListOf()
) : IStatsRepository {
override suspend fun getAllStats(): List<Stats> = memoryStats.toList()
override suspend fun getAllStats(minId: Int): List<Stats> = memoryStats.filter { it.uid >= minId }
override suspend fun addStats(stats: Stats): Long? {
memoryStats.add(stats)

View file

@ -15,6 +15,9 @@ interface IPreferencesRepository {
fun controlStyle(): ControlStyle
fun useControlStyle(controlStyle: ControlStyle)
fun updateStatsBase(statsBase: Int)
fun getStatsBase(): Int
fun useFlagAssistant(): Boolean
fun useHapticFeedback(): Boolean
fun useLargeAreas(): Boolean
@ -83,6 +86,13 @@ class PreferencesRepository(
putInt(PREFERENCE_CONTROL_STYLE, controlStyle.ordinal)
}
override fun updateStatsBase(statsBase: Int) {
putInt(PREFERENCE_STATS_BASE, statsBase)
}
override fun getStatsBase(): Int =
getInt(PREFERENCE_STATS_BASE, 0)
private fun migrateOldPreferences() {
if (preferencesManager.contains(PREFERENCE_OLD_DOUBLE_CLICK)) {
if (getBoolean(PREFERENCE_OLD_DOUBLE_CLICK, false)) {
@ -105,5 +115,6 @@ class PreferencesRepository(
private const val PREFERENCE_CUSTOM_GAME_HEIGHT = "preference_custom_game_height"
private const val PREFERENCE_CUSTOM_GAME_MINES = "preference_custom_game_mines"
private const val PREFERENCE_SOUND_EFFECTS = "preference_sound"
private const val PREFERENCE_STATS_BASE = "preference_stats_base"
}
}

View file

@ -86,6 +86,8 @@
<string name="no_network">No internet connection.</string>
<string name="open_menu">Open Menu</string>
<string name="close_menu">Close Menu</string>
<string name="delete_all">Delete all</string>
<string name="delete_all_message">Delete all events permanently.</string>
<string name="all_mines_disabled">All mines were disabled.</string>
<string name="desc_convered_area">Covered area</string>
<string name="desc_marked_area">Marked area</string>

View file

@ -6,8 +6,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 702051 // MMmmPPv
versionName '7.2.5'
versionCode 702061 // MMmmPPv
versionName '7.2.6'
minSdkVersion 16
targetSdkVersion 30
}

View file

@ -6,8 +6,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 702051 // MMmmPPv
versionName '7.2.5'
versionCode 702061 // MMmmPPv
versionName '7.2.6'
minSdkVersion 16
targetSdkVersion 30
}

View file

@ -9,8 +9,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 702051
versionName '7.2.5'
versionCode 702061
versionName '7.2.6'
applicationId 'dev.lucasnlm.antimine'
minSdkVersion 23
targetSdkVersion 30