Skip to content

Commit

Permalink
Persistence (#15)
Browse files Browse the repository at this point in the history
* Add persistence

* Add persistence test
  • Loading branch information
MrFang authored Apr 5, 2023
1 parent 642f8ac commit 413fb91
Show file tree
Hide file tree
Showing 26 changed files with 520 additions and 610 deletions.
8 changes: 8 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jlleitschuh.gradle.ktlint")
id("com.google.devtools.ksp")
}

android {
Expand Down Expand Up @@ -40,11 +41,18 @@ android {
}
}

val roomVersion = "2.5.1"

dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.room:room-runtime:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
56 changes: 48 additions & 8 deletions app/src/androidTest/java/com/ifmo/balda/MainActivityTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ifmo.balda

import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
Expand All @@ -9,12 +11,15 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withSpinnerText
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ifmo.balda.activity.HelpScreenActivity
import com.ifmo.balda.activity.MainActivity
import com.ifmo.balda.activity.StatScreenActivity
import com.ifmo.balda.model.Topic
import org.hamcrest.CoreMatchers.instanceOf
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
Expand All @@ -25,6 +30,12 @@ class MainActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)

private val buttonIdToResourceId = mapOf(
R.id.easyDifficultyButton to R.string.easy,
R.id.mediumDifficultyButton to R.string.medium,
R.id.hardDifficultyButton to R.string.hard
)

companion object {
@BeforeClass
@JvmStatic
Expand Down Expand Up @@ -63,20 +74,22 @@ class MainActivityTest {

@Test
fun testTopicSelector() {
onView(withId(R.id.topicSelector))
val topicSelector = onView(withId(R.id.topicSelector))
topicSelector
.check(matches(isDisplayed()))
.check(matches(isClickable()))
// TODO: probably find a way to check content

for ((idx, topic) in Topic.values().withIndex()) {
topicSelector.perform(click())
onData(instanceOf(MainActivity.TopicSelectorItem::class.java))
.atPosition(idx)
.perform(click())
topicSelector.check(matches(withSpinnerText(topic.resourceId)))
}
}

@Test
fun testDifficultyButtons() {
val buttonIdToResourceId = mapOf(
R.id.easyDifficultyButton to R.string.easy,
R.id.mediumDifficultyButton to R.string.medium,
R.id.hardDifficultyButton to R.string.hard
)

for ((viewId, resourceId) in buttonIdToResourceId) {
onView(withId(viewId))
.check(matches(isDisplayed()))
Expand Down Expand Up @@ -104,4 +117,31 @@ class MainActivityTest {
.check(matches(withTooltip(R.string.topic_help)))
.perform(click())
}

@Test
fun testTopicPersists() {
for ((idx, topic) in Topic.values().withIndex()) {
onView(withId(R.id.topicSelector)).perform(click())
onData(instanceOf(MainActivity.TopicSelectorItem::class.java))
.atPosition(idx)
.perform(click())
reopenApp()
onView(withId(R.id.topicSelector))
.check(matches(withSpinnerText(topic.resourceId)))
}
}

@Test
fun testDifficultyPersists() {
for ((buttonId, resourceId) in buttonIdToResourceId) {
onView(withId(buttonId)).perform(click())
reopenApp()
onView(withId(R.id.selectedDifficulty)).check(matches(withText(resourceId)))
}
}

private fun reopenApp() {
activityRule.scenario.close()
ActivityScenario.launch(MainActivity::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.ifmo.balda

import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isClickable
Expand All @@ -23,11 +25,11 @@ class NameChoosingActivityTest {

@Test
fun testSinglePlayer() {
onView(withId(R.id.startGame1PlayerButton)).perform(click())
openSinglePlayerScreen()

onView(withId(R.id.player1Name))
.check(matches(isDisplayed()))
.check(matches(withText(R.string.player)))
.check(matches(withAnyText()))
onView(withId(R.id.player2Name))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
onView(withId(R.id.playButton))
Expand All @@ -38,17 +40,69 @@ class NameChoosingActivityTest {

@Test
fun testMultiplayer() {
onView(withId(R.id.startGame2PlayerButton)).perform(click())
openMultiPlayerScreen()

onView(withId(R.id.player1Name))
.check(matches(isDisplayed()))
.check(matches(withText(R.string.player1)))
.check(matches(withAnyText()))
onView(withId(R.id.player2Name))
.check(matches(isDisplayed()))
.check(matches(withText(R.string.player2)))
.check(matches(withAnyText()))
onView(withId(R.id.playButton))
.check(matches(isDisplayed()))
.check(matches(isClickable()))
.perform(click())
}

@Test
fun testSinglePlayerNamePersists() {
val nameToCheck = "TestPersist"

openSinglePlayerScreen()
onView(withId(R.id.player1Name))
.perform(replaceText(nameToCheck))
onView(withId(R.id.playButton))
.perform(click())
reopenApp()
openSinglePlayerScreen()

onView(withId(R.id.player1Name))
.check(matches(withText(nameToCheck)))
}

@Test
fun testMultiPlayerNamePersists() {
val nameToCheck1 = "TestPersist1"
val nameToCheck2 = "TestPersist2"

openMultiPlayerScreen()
onView(withId(R.id.player1Name))
.perform(replaceText(nameToCheck1))
onView(withId(R.id.player2Name))
.perform(replaceText(nameToCheck2))
onView(withId(R.id.playButton))
.perform(click())
reopenApp()
openMultiPlayerScreen()

onView(withId(R.id.player1Name))
.check(matches(withText(nameToCheck1)))
onView(withId(R.id.player2Name))
.check(matches(withText(nameToCheck2)))
}

private fun openSinglePlayerScreen() {
onView(withId(R.id.startGame1PlayerButton))
.perform(click())
}

private fun openMultiPlayerScreen() {
onView(withId(R.id.startGame2PlayerButton))
.perform(click())
}

private fun reopenApp() {
activityRule.scenario.close()
ActivityScenario.launch(MainActivity::class.java)
}
}
6 changes: 6 additions & 0 deletions app/src/androidTest/java/com/ifmo/balda/TestUtils.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ifmo.balda

import android.view.View
import android.widget.TextView
import androidx.test.platform.app.InstrumentationRegistry
import org.hamcrest.Description
import org.hamcrest.Matcher
Expand All @@ -14,3 +15,8 @@ fun withTooltip(str: String): Matcher<View> = object : TypeSafeMatcher<View>() {
fun withTooltip(resourceId: Int): Matcher<View> = withTooltip(
InstrumentationRegistry.getInstrumentation().targetContext.getString(resourceId)
)

fun withAnyText(): Matcher<View> = object : TypeSafeMatcher<View> () {
override fun describeTo(description: Description) = Unit
override fun matchesSafely(item: View): Boolean = item is TextView && item.text.isNotEmpty()
}
21 changes: 17 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,30 @@
android:theme="@style/Theme.Adfmp1h23balda"
tools:targetApi="33">
<activity
android:screenOrientation="portrait"
android:name=".activity.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activity.HelpScreenActivity" android:parentActivityName=".activity.MainActivity" />
<activity android:name=".activity.StatScreenActivity" android:parentActivityName=".activity.MainActivity" />
<activity android:name=".activity.ChooseNameActivity" android:parentActivityName=".activity.MainActivity" />
<activity android:name=".activity.GameActivity" android:parentActivityName=".activity.ChooseNameActivity" />
<activity
android:screenOrientation="portrait"
android:name=".activity.HelpScreenActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:screenOrientation="portrait"
android:name=".activity.StatScreenActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:screenOrientation="portrait"
android:name=".activity.ChooseNameActivity"
android:parentActivityName=".activity.MainActivity" />
<activity
android:screenOrientation="portrait"
android:name=".activity.GameActivity"
android:parentActivityName=".activity.ChooseNameActivity" />
</application>

</manifest>
46 changes: 46 additions & 0 deletions app/src/main/java/com/ifmo/balda/AppDatabase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.ifmo.balda

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import com.ifmo.balda.model.Topic
import com.ifmo.balda.model.dao.StatDao
import com.ifmo.balda.model.dao.WordDao
import com.ifmo.balda.model.entity.Stat
import com.ifmo.balda.model.entity.Word
import kotlin.streams.asSequence

@Database(entities = [Word::class, Stat::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
abstract fun statDao(): StatDao
}

private fun initDb(ctx: Context): AppDatabase = Room.databaseBuilder(ctx, AppDatabase::class.java, "balda.db")
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
ctx.resources.openRawResource(R.raw.russian_nouns)
.bufferedReader()
.lines().asSequence()
.chunked(900) // SQLITE_MAX_VARIABLE_NUMBER is 999
.forEach { nouns ->
db.execSQL(
"insert into word (word, topic) values ${nouns.joinToString(", ") { "(?, '${Topic.COMMON.name}')" }}",
nouns.toTypedArray()
)
}
}

override fun onDestructiveMigration(db: SupportSQLiteDatabase) = onCreate(db)
})
.build()

@Volatile
private var dbInstance: AppDatabase? = null

val Context.db: AppDatabase
get() = dbInstance ?: synchronized(applicationContext) {
dbInstance ?: initDb(applicationContext).also { dbInstance = it }
}
10 changes: 4 additions & 6 deletions app/src/main/java/com/ifmo/balda/IntentExtraNames.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.ifmo.balda

class IntentExtraNames {
companion object {
const val GAME_MODE = "GAME_MODE"
const val PLAYER_1_NAME = "PLAYER_1_NAME"
const val PLAYER_2_NAME = "PLAYER_2_NAME"
}
object IntentExtraNames {
const val GAME_MODE = "GAME_MODE"
const val PLAYER_1_NAME = "PLAYER_1_NAME"
const val PLAYER_2_NAME = "PLAYER_2_NAME"
}
10 changes: 10 additions & 0 deletions app/src/main/java/com/ifmo/balda/PreferencesKeys.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ifmo.balda

object PreferencesKeys {
const val preferencesFileKey = "prefFile"
const val difficultyKey = "difficulty"
const val topicKey = "topic"
const val singlePlayerNameKey = "singlePlayerName"
const val multiPlayer1PlayerNameKey = "multiPlayer1PlayerName"
const val multiPlayer2PlayerNameKey = "multiPlayer2PlayerName"
}
Loading

0 comments on commit 413fb91

Please sign in to comment.