diff --git a/android/app/build.gradle b/android/app/build.gradle index 7ce76a1a..2232f1ee 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -9,13 +9,17 @@ apply plugin: 'kotlin-kapt' apply plugin: 'androidx.navigation.safeargs' android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } compileSdkVersion 29 buildToolsVersion "29.0.3" dataBinding { enabled = true } defaultConfig { - applicationId "ru.spbstu.amd.learnbraille" + applicationId "com.github.braillesystems.learnbraille" minSdkVersion 19 targetSdkVersion 29 versionCode 1 @@ -44,9 +48,14 @@ dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.12' + // Usb serial + implementation 'com.github.felHR85:UsbSerial:6.1.0' + // Room implementation "androidx.room:room-runtime:$version_room" kapt "androidx.room:room-compiler:$version_room" + implementation "androidx.room:room-ktx:$version_room" + testImplementation "androidx.room:room-testing:$version_room" // Testing androidTestImplementation 'androidx.test.ext:junit:1.1.0' @@ -61,4 +70,8 @@ dependencies { // Lifecycle implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" + + //Design + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.appcompat:appcompat:1.1.0' } diff --git a/android/app/src/androidTest/java/com/github/braillesystems/learnbraille/LearnBrailleDatabaseTest.kt b/android/app/src/androidTest/java/com/github/braillesystems/learnbraille/LearnBrailleDatabaseTest.kt new file mode 100644 index 00000000..ae4512bf --- /dev/null +++ b/android/app/src/androidTest/java/com/github/braillesystems/learnbraille/LearnBrailleDatabaseTest.kt @@ -0,0 +1,60 @@ +package com.github.braillesystems.learnbraille + +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.github.braillesystems.learnbraille.database.LearnBrailleDatabase +import com.github.braillesystems.learnbraille.database.entities.StepDao +import com.github.braillesystems.learnbraille.database.entities.SymbolDao +import com.github.braillesystems.learnbraille.database.entities.UserDao +import com.github.braillesystems.learnbraille.database.entities.UserPassedStepDao +import com.github.braillesystems.learnbraille.res.russian.PREPOPULATE_USERS +import com.github.braillesystems.learnbraille.res.russian.steps.DEBUG_LESSONS +import com.github.braillesystems.learnbraille.res.russian.symbols.PREPOPULATE_SYMBOLS +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class LearnBrailleDatabaseTest { + + private lateinit var db: LearnBrailleDatabase + private lateinit var userDao: UserDao + private lateinit var stepDao: StepDao + private lateinit var symbolDao: SymbolDao + private lateinit var userPassedStepDao: UserPassedStepDao + + @Before + fun createDB() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room + .inMemoryDatabaseBuilder(context, LearnBrailleDatabase::class.java) + .allowMainThreadQueries() + .build().apply { + runBlocking { + userDao.insertUsers(PREPOPULATE_USERS) + stepDao.insertSteps(DEBUG_LESSONS) + symbolDao.insertSymbols(PREPOPULATE_SYMBOLS) + } + } + + userDao = db.userDao + stepDao = db.stepDao + symbolDao = db.symbolDao + userPassedStepDao = db.userPassedStepDao + } + + @After + @Throws(IOException::class) + fun closeDB() { + db.close() + } + + // TODO actualize + + companion object { + const val TEST_USER = 1L + } +} diff --git a/android/app/src/androidTest/java/ru/spbstu/amd/learnbraille/LearnBrailleDatabaseTest.kt b/android/app/src/androidTest/java/ru/spbstu/amd/learnbraille/LearnBrailleDatabaseTest.kt deleted file mode 100644 index cac2224b..00000000 --- a/android/app/src/androidTest/java/ru/spbstu/amd/learnbraille/LearnBrailleDatabaseTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package ru.spbstu.amd.learnbraille - -import androidx.room.Room -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import ru.spbstu.amd.learnbraille.database.* -import ru.spbstu.amd.learnbraille.database.BrailleDot.F -import ru.spbstu.amd.learnbraille.res.russian.PREPOPULATE_LESSONS -import ru.spbstu.amd.learnbraille.res.russian.PREPOPULATE_USERS -import ru.spbstu.amd.learnbraille.res.russian.steps.PREPOPULATE_STEPS -import ru.spbstu.amd.learnbraille.res.russian.symbols.PREPOPULATE_SYMBOLS -import java.io.IOException - -@RunWith(AndroidJUnit4::class) -class LearnBrailleDatabaseTest { - - private lateinit var db: LearnBrailleDatabase - private lateinit var userDao: UserDao - private lateinit var stepDao: StepDao - private lateinit var symbolDao: SymbolDao - private lateinit var userPassedStepDao: UserPassedStepDao - - @Before - fun createDB() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - db = Room - .inMemoryDatabaseBuilder(context, LearnBrailleDatabase::class.java) - .allowMainThreadQueries() - .build().apply { - userDao.insertUsers(PREPOPULATE_USERS) - lessonDao.insertLessons(PREPOPULATE_LESSONS) - stepDao.insertSteps(PREPOPULATE_STEPS) - symbolDao.insertSymbols(PREPOPULATE_SYMBOLS) - } - - userDao = db.userDao - stepDao = db.stepDao - symbolDao = db.symbolDao - userPassedStepDao = db.userPassedStepDao - } - - @After - @Throws(IOException::class) - fun closeDB() { - db.close() - } - - @Test - @Throws(Exception::class) - fun defaultUserIsThere() { - assertEquals(1L, userDao.getId("default")) - } - - @Test - @Throws(Exception::class) - fun ruLettersAreThere() { - val letter = symbolDao.getSymbol('А') - assertEquals('А', letter?.symbol) - } - - @Test - @Throws(Exception::class) - fun stepOrder() { - val (_, step1) = stepDao.getCurrentStepForUser(TEST_USER) ?: error("No first step") - assertEquals(1, step1.id) - assertTrue(step1.data is Info) - userPassedStepDao.insertPassedStep(UserPassedStep(1, 1)) - - val (_, step2) = stepDao.getCurrentStepForUser(TEST_USER) ?: error("No second step") - assertEquals(2, step2.id) - assertTrue(step2.data is ShowDots) - assertEquals(BrailleDots(F, F, F, F, F, F), (step2.data as ShowDots).dots) - } - - companion object { - const val TEST_USER = 1L - } -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4eba31e4..e27e805b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,18 +1,18 @@ + package="com.github.braillesystems.learnbraille"> - + diff --git a/android/app/src/main/icon_beta-playstore.png b/android/app/src/main/icon_beta-playstore.png new file mode 100644 index 00000000..c0c31266 Binary files /dev/null and b/android/app/src/main/icon_beta-playstore.png differ diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/LearnBrailleApplication.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/LearnBrailleApplication.kt new file mode 100644 index 00000000..d3314399 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/LearnBrailleApplication.kt @@ -0,0 +1,41 @@ +package com.github.braillesystems.learnbraille + +import android.app.Application +import com.github.braillesystems.learnbraille.database.LearnBrailleDatabase +import com.github.braillesystems.learnbraille.database.entities.Language +import com.github.braillesystems.learnbraille.util.scope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import timber.log.Timber + +class LearnBrailleApplication : Application() { + + lateinit var prepopulationJob: Job + private set + + override fun onCreate() { + super.onCreate() + Timber.plant(Timber.DebugTree()) + Timber.i("onCreate") + + // Force database prepopulation on first launch + LearnBrailleDatabase.getInstance(this).apply { + prepopulationJob = scope().launch { + if (userDao.getUser(defaultUser) == null) { + Timber.i("DB has been already initialized") + } else Timber.i("DB is not initialized yet") + } + } + } +} + +typealias BuzzPattern = LongArray + +val CORRECT_BUZZ_PATTERN: BuzzPattern = longArrayOf(100, 100, 100, 100, 100, 100) +val INCORRECT_BUZZ_PATTERN: BuzzPattern = longArrayOf(0, 200) + +// TODO move to settings +val language = Language.RU +const val defaultUser = 1L + +const val DEBUG = false diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/MainActivity.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/MainActivity.kt similarity index 81% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/MainActivity.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/MainActivity.kt index e37fcdb1..bae6cf62 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/MainActivity.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/MainActivity.kt @@ -1,4 +1,4 @@ -package ru.spbstu.amd.learnbraille +package com.github.braillesystems.learnbraille import android.content.pm.ActivityInfo import android.os.Bundle @@ -7,7 +7,8 @@ import androidx.databinding.DataBindingUtil import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.NavigationUI -import ru.spbstu.amd.learnbraille.databinding.ActivityMainBinding +import com.github.braillesystems.learnbraille.databinding.ActivityMainBinding +import timber.log.Timber class MainActivity : AppCompatActivity() { @@ -15,6 +16,9 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + Timber.i("onCreate") + DataBindingUtil.setContentView( this, R.layout.activity_main diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Database.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/Database.kt similarity index 56% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Database.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/database/Database.kt index 190bdfc8..261a787e 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Database.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/Database.kt @@ -1,25 +1,30 @@ -package ru.spbstu.amd.learnbraille.database +package com.github.braillesystems.learnbraille.database import android.annotation.SuppressLint import android.content.Context +import androidx.fragment.app.Fragment import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.sqlite.db.SupportSQLiteDatabase -import ru.spbstu.amd.learnbraille.res.russian.PREPOPULATE_LESSONS -import ru.spbstu.amd.learnbraille.res.russian.PREPOPULATE_USERS -import ru.spbstu.amd.learnbraille.res.russian.steps.PREPOPULATE_STEPS -import ru.spbstu.amd.learnbraille.res.russian.symbols.PREPOPULATE_SYMBOLS +import com.github.braillesystems.learnbraille.database.entities.* +import com.github.braillesystems.learnbraille.res.russian.PREPOPULATE_LESSONS +import com.github.braillesystems.learnbraille.res.russian.PREPOPULATE_USERS +import com.github.braillesystems.learnbraille.res.russian.steps.PREPOPULATE_STEPS +import com.github.braillesystems.learnbraille.res.russian.symbols.PREPOPULATE_SYMBOLS +import com.github.braillesystems.learnbraille.util.application +import com.github.braillesystems.learnbraille.util.scope +import kotlinx.coroutines.launch import timber.log.Timber @Database( entities = [ User::class, Lesson::class, Step::class, Symbol::class, - UserKnowsSymbol::class, UserPassedStep::class + UserKnowsSymbol::class, UserPassedStep::class, UserLastStep::class ], - version = 1, + version = 4, exportSchema = false ) @TypeConverters( @@ -35,11 +40,16 @@ abstract class LearnBrailleDatabase : RoomDatabase() { abstract val symbolDao: SymbolDao abstract val userKnowsSymbolDao: UserKnowsSymbolDao abstract val userPassedStepDao: UserPassedStepDao + abstract val userLastStep: UserLastStepDao companion object { const val name = "braille_lessons_database" + @Volatile + var prepopulationFinished = true + private set + @Volatile private var INSTANCE: LearnBrailleDatabase? = null @@ -49,17 +59,31 @@ abstract class LearnBrailleDatabase : RoomDatabase() { INSTANCE ?: buildDatabase(context).also { INSTANCE = it } } - private fun buildDatabase(context: Context) = Room.databaseBuilder( + private fun buildDatabase(context: Context) = Room + .databaseBuilder( context.applicationContext, LearnBrailleDatabase::class.java, name ) .addCallback(object : Callback() { + @SuppressLint("SyntheticAccessor") override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) - Timber.d("Start database callback") - ioThread { + Timber.d("onCreate") + prepopulate() + } + + override fun onDestructiveMigration(db: SupportSQLiteDatabase) { + super.onDestructiveMigration(db) + Timber.i("onDestructiveMigration") + prepopulate() + } + + private fun prepopulate() { + Timber.i("prepopulate") + prepopulationFinished = false + scope().launch { Timber.i("Start database prepopulation") getInstance(context).apply { userDao.insertUsers(PREPOPULATE_USERS) @@ -68,6 +92,7 @@ abstract class LearnBrailleDatabase : RoomDatabase() { symbolDao.insertSymbols(PREPOPULATE_SYMBOLS) } Timber.i("Finnish database prepopulation") + prepopulationFinished = true } } }) @@ -75,3 +100,5 @@ abstract class LearnBrailleDatabase : RoomDatabase() { .build() } } + +fun Fragment.getDBInstance() = LearnBrailleDatabase.getInstance(application) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/BrailleDots.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/BrailleDots.kt similarity index 51% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/database/BrailleDots.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/BrailleDots.kt index 9cb5f652..6beb0769 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/BrailleDots.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/BrailleDots.kt @@ -1,26 +1,31 @@ -package ru.spbstu.amd.learnbraille.database +package com.github.braillesystems.learnbraille.database.entities -import android.widget.Checkable import androidx.room.TypeConverter -import ru.spbstu.amd.learnbraille.database.BrailleDot.E +import com.github.braillesystems.learnbraille.database.entities.BrailleDot.E +/** + * State of one Braille dot. + */ enum class BrailleDot { E, // Empty F; // Filled - companion object { + companion object Factories { fun valueOf(b: Boolean) = if (b) F else E fun valueOf(c: Char) = valueOf(c.toString()) } } +/** + * Combination on Braille dots for one symbol in 6-dots notation. + */ data class BrailleDots( val b1: BrailleDot = E, val b2: BrailleDot = E, val b3: BrailleDot = E, val b4: BrailleDot = E, val b5: BrailleDot = E, val b6: BrailleDot = E ) { constructor(dots: BooleanArray) : this( - dots.map { BrailleDot.valueOf(it) } + dots.map(BrailleDot.Factories::valueOf) ) constructor(dots: List) : this( @@ -37,31 +42,34 @@ data class BrailleDots( } constructor(string: String) : this( - string.toCharArray().map { BrailleDot.valueOf(it) } + string + .toCharArray() + .map(BrailleDot.Factories::valueOf) ) override fun toString() = "$b1$b2$b3$b4$b5$b6" } +val BrailleDots.list: List + get() = listOf(b1, b2, b3, b4, b5, b6) + +val BrailleDots.spelling: String + get() = list + .mapIndexed { index, brailleDot -> + if (brailleDot == BrailleDot.F) { + (index + 1).toString() + } else { + null + } + } + .filterNotNull() + .joinToString(separator = " ") + class BrailleDotsConverters { @TypeConverter fun to(brailleDots: BrailleDots) = brailleDots.toString() @TypeConverter - fun from(data: String): BrailleDots = BrailleDots(data) -} - -class BrailleDotsState(private val states: Array) { - - val brailleDots - get() = BrailleDots( - states.map { it.isChecked }.toBooleanArray() - ) - - init { - require(states.size == 6) { - "Only 6 dots braille notation supported" - } - } + fun from(data: String) = BrailleDots(data) } diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Language.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Language.kt similarity index 75% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Language.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Language.kt index e13753de..f99cda84 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Language.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Language.kt @@ -1,4 +1,4 @@ -package ru.spbstu.amd.learnbraille.database +package com.github.braillesystems.learnbraille.database.entities import androidx.room.TypeConverter diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Lessons.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Lessons.kt new file mode 100644 index 00000000..7f2dd898 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Lessons.kt @@ -0,0 +1,22 @@ +package com.github.braillesystems.learnbraille.database.entities + +import androidx.room.* + +@Entity(tableName = "lesson") +data class Lesson( + + @PrimaryKey(autoGenerate = false) + val id: Long, + + val name: String +) + +@Dao +interface LessonDao { + + @Insert + suspend fun insertLessons(lessons: List) + + @Query("DELETE FROM lesson") + suspend fun deleteAll() +} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/StepData.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/StepData.kt similarity index 57% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/database/StepData.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/StepData.kt index fac1e7cb..ab138df7 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/StepData.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/StepData.kt @@ -1,154 +1,207 @@ -package ru.spbstu.amd.learnbraille.database - -import androidx.room.TypeConverter - -/** - * There are specific step types in the app. - * They differs by visual representation and by the data they contain. - * - * Step type can be determined by `is` check. - */ -sealed class StepData { - - protected abstract val name: String - protected abstract val data: String - - override fun toString() = "$name $data" -} - -sealed class BaseInfo : StepData() - -/** - * Step displays information text for the user. - */ -class Info( - text: String -) : BaseInfo() { - - private val text = text.stepFormat() - - override val name = Companion.name - override val data = this.text - - companion object { - val name = Info::class.java.name - } -} - -class LastInfo( - text: String -) : BaseInfo() { - - private val text = text.stepFormat() - - override val name = Companion.name - override val data = this.text - - companion object { - val name = LastInfo::class.java.name - } -} - -sealed class BaseInput : StepData() - -/** - * Step prompts the user to enter Braille dots corresponding to the printed symbol. - */ -class InputSymbol( - val symbol: Symbol -) : BaseInput() { - - override val name = Companion.name - override val data = symbol.toString() - - constructor(data: String) : this(symbolOf(data)) - - companion object { - val name = InputSymbol::class.java.name - } -} - -/** - * Step prompts the user to enter dots with specific numbers. - */ -class InputDots( - val dots: BrailleDots -) : BaseInput() { - - override val name = Companion.name - override val data = dots.toString() - - constructor(data: String) : this(BrailleDots(data)) - - companion object { - val name = InputDots::class.java.name - } -} - -sealed class BaseShow : StepData() - -/** - * Step shows symbol and it's Braille representation. - */ -class ShowSymbol( - val symbol: Symbol -) : BaseShow() { - - override val name = Companion.name - override val data = symbol.toString() - - constructor(data: String) : this(symbolOf(data)) - - companion object { - val name = ShowSymbol::class.java.name - } -} - -/** - * Step shows Braille dots with specific numbers. - */ -class ShowDots( - val dots: BrailleDots -) : BaseShow() { - - override val name = Companion.name - override val data = dots.toString() - - constructor(data: String) : this(BrailleDots(data)) - - companion object { - val name = ShowDots::class.java.name - } -} - -/** - * Add new StepData types to the when, it is only one place not checked in compile time. - */ -fun stepDataOf(string: String): StepData = string - .split(' ', limit = 2) - .let { (type, data) -> - when (type) { - Info.name -> Info(data) - LastInfo.name -> LastInfo(data) - InputSymbol.name -> InputSymbol(data) - InputDots.name -> InputDots(data) - ShowSymbol.name -> ShowSymbol(data) - ShowDots.name -> ShowDots(data) - else -> error("No such step type: $type") - } - } - -class StepDataConverters { - - @TypeConverter - fun to(stepData: StepData) = stepData.toString() - - @TypeConverter - fun from(string: String): StepData = stepDataOf(string) -} - -/** - * Use with raw strings to format text for info steps. - */ -fun String.stepFormat(): String = this - .trimMargin() +package com.github.braillesystems.learnbraille.database.entities + +import androidx.room.TypeConverter + +/** + * There are specific step types in the app. + * They differs by visual representation and by the data they contain. + * + * Step type can be determined by `is` check. + */ +sealed class StepData { + + protected abstract val name: String + protected abstract val data: String + + override fun toString() = "$name $data" +} + +/** + * Add new StepData types to the when expression, + * it is only one place not checked in compile time. + */ +fun stepDataOf(string: String): StepData = string + .split(' ', limit = 2) + .let { (type, data) -> + when (type) { + Info.name -> Info(data) + FirstInfo.name -> FirstInfo(data) + LastInfo.name -> LastInfo(data) + InputSymbol.name -> InputSymbol(data) + InputDots.name -> inputDotsOf(data) + ShowSymbol.name -> ShowSymbol(data) + ShowDots.name -> showDotsOf(data) + else -> error("No such step type: $type") + } + } + +/** + * Represent step types with information. + */ +sealed class BaseInfo : StepData() + +/** + * Step displays information text for the user. + */ +class Info( + text: String +) : BaseInfo() { + + val text = text.stepFormat() + + override val name = Companion.name + override val data = this.text + + companion object { + val name = Info::class.java.name + } +} + +/** + * Info step without `prev` button. + */ +class FirstInfo( + text: String +) : BaseInfo() { + + val text = text.stepFormat() + + override val name = Companion.name + override val data = this.text + + companion object { + val name = FirstInfo::class.java.name + } +} + +/** + * Info step without `next` button. + */ +class LastInfo( + text: String +) : BaseInfo() { + + val text = text.stepFormat() + + override val name = Companion.name + override val data = this.text + + companion object { + val name = LastInfo::class.java.name + } +} + +sealed class BaseInput : StepData() + +/** + * Step prompts the user to enter Braille dots corresponding to the printed symbol. + */ +class InputSymbol( + val symbol: Symbol +) : BaseInput() { + + override val name = Companion.name + override val data = symbol.toString() + + constructor(data: String) : this( + symbolOf(data) + ) + + companion object { + val name = InputSymbol::class.java.name + } +} + +/** + * Step prompts the user to enter dots with specific numbers. + * + * @param text Special text of default braille dots spelling. + * Generated one will be displayed if `null`. + */ +class InputDots( + val text: String?, + val dots: BrailleDots +) : BaseInput() { + + override val name = Companion.name + override val data = text + delimiter + dots.toString() + + companion object { + val name = InputDots::class.java.name + const val delimiter = Char.MAX_VALUE + } +} + +fun inputDotsOf(string: String): InputDots = string + .split(InputDots.delimiter, limit = 2) + .let { (text, dots) -> + InputDots( + text = if (text == null.toString()) null else text, + dots = BrailleDots(dots) + ) + } + +sealed class BaseShow : StepData() + +/** + * Step shows symbol and it's Braille representation. + */ +class ShowSymbol( + val symbol: Symbol +) : BaseShow() { + + override val name = Companion.name + override val data = symbol.toString() + + constructor(data: String) : this( + symbolOf(data) + ) + + companion object { + val name = ShowSymbol::class.java.name + } +} + +/** + * Step shows Braille dots with specific numbers. + * + * @param text Special text of default braille dots spelling. + * Generated one will be displayed if `null`. + */ +class ShowDots( + val text: String?, + val dots: BrailleDots +) : BaseShow() { + + override val name = Companion.name + override val data = text + delimiter + dots.toString() + + companion object { + val name = ShowDots::class.java.name + const val delimiter = Char.MAX_VALUE + } +} + +fun showDotsOf(string: String): ShowDots = string + .split(ShowDots.delimiter, limit = 2) + .let { (text, dots) -> + ShowDots( + text = if (text == null.toString()) null else text, + dots = BrailleDots(dots) + ) + } + +class StepDataConverters { + + @TypeConverter + fun to(stepData: StepData) = stepData.toString() + + @TypeConverter + fun from(string: String) = stepDataOf(string) +} + +/** + * Use with raw strings to format text for info steps. + */ +fun String.stepFormat(): String = this + .trimMargin() diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Steps.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Steps.kt new file mode 100644 index 00000000..734ae1be --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Steps.kt @@ -0,0 +1,84 @@ +package com.github.braillesystems.learnbraille.database.entities + +import androidx.room.* +import com.github.braillesystems.learnbraille.util.* + +@Entity(tableName = "step") +data class Step( + + @PrimaryKey(autoGenerate = true) + var id: Long = 0, + + val title: String, + + @ColumnInfo(name = "lesson_id") + val lessonId: Long, + + val data: StepData +) { + companion object { + val pattern = Regex( + """Step\(id=(\d+), title=((?:.|\n)*), lessonId=(\d+), data=((?:.|\n)+)\)""" + ) + } +} + +fun stepOf(string: String) = Step.pattern.matchEntire(string)?.groups + ?.let { (_, id, title, lessonId, data) -> + Step( + id = id?.value?.toLong() ?: error("No id here $string"), + title = title?.value ?: error("No title here $string"), + lessonId = lessonId?.value?.toLong() ?: error("No lessonId here $string"), + data = stepDataOf(data?.value ?: error("No data here $string")) + ) + } ?: error("$string does not match symbol structure") + +@Dao +interface StepDao { + + @Insert + suspend fun insertSteps(steps: List) + + @Query( + """ + SELECT * FROM step + WHERE NOT EXISTS ( + SELECT * FROM user_passed_step AS ups + WHERE ups.user_id = :userId AND ups.step_id = step.id + ) + ORDER BY step.id ASC + LIMIT 1 + """ + ) + suspend fun getCurrentStepForUser(userId: Long): Step? + + @Query( + """ + SELECT * FROM step + WHERE EXISTS ( + SELECT * FROM user_passed_step AS ups + WHERE ups.user_id = :userId AND ups.step_id = :currentStepId + ) AND step.id > :currentStepId + ORDER BY step.id ASC + LIMIT 1 + """ + ) + suspend fun getNextStepForUser(userId: Long, currentStepId: Long): Step? + + @Query( + """ + SELECT * FROM step + WHERE EXISTS ( + SELECT * FROM user_last_step AS uls + WHERE uls.step_id = step.id AND uls.user_id = :userId + ) + """ + ) + suspend fun getLastStepForUser(userId: Long): Step? + + @Query("SELECT * FROM step WHERE step.id = :id") + suspend fun getStep(id: Long): Step? + + @Query("DELETE FROM step") + suspend fun deleteAll() +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Symbols.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Symbols.kt new file mode 100644 index 00000000..fcd7cf08 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/Symbols.kt @@ -0,0 +1,74 @@ +package com.github.braillesystems.learnbraille.database.entities + +import androidx.room.* +import com.github.braillesystems.learnbraille.util.* + +@Entity(tableName = "symbol") +data class Symbol( + + @PrimaryKey(autoGenerate = true) + var id: Long = 0, + + val symbol: Char, + + val language: Language = Language.NONE, + + @ColumnInfo(name = "braille_dots") + val brailleDots: BrailleDots +) { + companion object { + val pattern = Regex( + """Symbol\(id=(\d+), symbol=(.), language=(\w{2,4}), brailleDots=([E|F]{6})\)""" + ) + } +} + +fun symbolOf(data: String) = Symbol.pattern.matchEntire(data)?.groups + ?.let { (_, id, symbol, language, brailleDots) -> + Symbol( + id = id?.value?.toLong() ?: error("No id here $data"), + symbol = symbol?.value?.first() ?: error("No symbol here $data"), + language = Language.valueOf( + language?.value ?: error("No language here $data") + ), + brailleDots = BrailleDots( + brailleDots?.value ?: error("No braille dots here $data") + ) + ) + } ?: error("$data does not match Symbol structure") + +@Dao +interface SymbolDao { + + @Insert + suspend fun insertSymbols(symbols: List) + + @Query( + """ + SELECT * + FROM symbol + WHERE language = :language + ORDER BY RANDOM() + LIMIT 1 + """ + ) + suspend fun getRandomSymbol(language: Language): Symbol? + + @Query("SELECT * FROM symbol WHERE symbol = :char LIMIT 1") + suspend fun getSymbol(char: Char): Symbol? + + @Query( + """ + SELECT * + FROM symbol + WHERE language = :language AND braille_dots = :brailleDots + LIMIT 1 + """ + ) + suspend fun getSymbol(language: Language, brailleDots: BrailleDots): Symbol? + + @Query("DELETE FROM symbol") + suspend fun deleteAll() +} + + diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/User.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/User.kt similarity index 66% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/database/User.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/User.kt index ca89bb6c..848b59b8 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/User.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/User.kt @@ -1,4 +1,4 @@ -package ru.spbstu.amd.learnbraille.database +package com.github.braillesystems.learnbraille.database.entities import androidx.room.* @@ -21,11 +21,11 @@ data class User( interface UserDao { @Insert - fun insertUsers(users: List) + suspend fun insertUsers(users: List) @Query("SELECT * FROM user WHERE :login = login LIMIT 1") - fun getUser(login: String): User? + suspend fun getUser(login: String): User? @Query("SELECT * FROM user WHERE :id = id LIMIT 1") - fun getUser(id: Long): User? + suspend fun getUser(id: Long): User? } diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/UserKnowsSymbol.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserKnowsSymbol.kt similarity index 56% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/database/UserKnowsSymbol.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserKnowsSymbol.kt index 704e6824..1bbca8eb 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/UserKnowsSymbol.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserKnowsSymbol.kt @@ -1,11 +1,10 @@ -package ru.spbstu.amd.learnbraille.database +package com.github.braillesystems.learnbraille.database.entities import androidx.room.* -@Entity(tableName = "user_knows_symbol") +@Entity(tableName = "user_knows_symbol", primaryKeys = ["user_id", "symbol_id"]) data class UserKnowsSymbol( - @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "user_id") val userId: Long, @@ -16,8 +15,8 @@ data class UserKnowsSymbol( @Dao interface UserKnowsSymbolDao { - @Insert - fun insertKnowledge(knowledge: UserKnowsSymbol) + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertKnowledge(knowledge: UserKnowsSymbol) @Query( """ @@ -29,5 +28,5 @@ interface UserKnowsSymbolDao { LIMIT 1 """ ) - fun getRandomKnownSymbol(userId: Long): Symbol? + suspend fun getRandomKnownSymbol(userId: Long): Symbol? } diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserLastStep.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserLastStep.kt new file mode 100644 index 00000000..1f5688ca --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserLastStep.kt @@ -0,0 +1,20 @@ +package com.github.braillesystems.learnbraille.database.entities + +import androidx.room.* + +@Entity(tableName = "user_last_step", primaryKeys = ["user_id"]) +data class UserLastStep( + + @ColumnInfo(name = "user_id") + val userId: Long, + + @ColumnInfo(name = "step_id") + val stepId: Long +) + +@Dao +interface UserLastStepDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertLastStep(currentStep: UserLastStep) +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserPassedStep.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserPassedStep.kt new file mode 100644 index 00000000..5c23ff8b --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/database/entities/UserPassedStep.kt @@ -0,0 +1,20 @@ +package com.github.braillesystems.learnbraille.database.entities + +import androidx.room.* + +@Entity(tableName = "user_passed_step", primaryKeys = ["user_id", "step_id"]) +data class UserPassedStep( + + @ColumnInfo(name = "user_id") + val userId: Long, + + @ColumnInfo(name = "step_id") + val stepId: Long +) + +@Dao +interface UserPassedStepDao { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertPassedStep(passedStep: UserPassedStep) +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/Lessons.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/Lessons.kt new file mode 100644 index 00000000..fad2c30b --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/Lessons.kt @@ -0,0 +1,15 @@ +package com.github.braillesystems.learnbraille.res.russian + +import com.github.braillesystems.learnbraille.database.entities.Lesson + +val PREPOPULATE_LESSONS + get() = listOf( + Lesson( + id = 1, + name = "Знакомство с шеститочием" + ), + Lesson( + id = 2, + name = "Русские буквы А, Б, Ц" + ) + ) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/Users.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/Users.kt similarity index 51% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/Users.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/Users.kt index 740a43b5..508ec3c0 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/Users.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/Users.kt @@ -1,6 +1,6 @@ -package ru.spbstu.amd.learnbraille.res.russian +package com.github.braillesystems.learnbraille.res.russian -import ru.spbstu.amd.learnbraille.database.User +import com.github.braillesystems.learnbraille.database.entities.User val PREPOPULATE_USERS = listOf( User( diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/AllSteps.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/AllSteps.kt new file mode 100644 index 00000000..d103c94c --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/AllSteps.kt @@ -0,0 +1,13 @@ +package com.github.braillesystems.learnbraille.res.russian.steps + +import com.github.braillesystems.learnbraille.DEBUG + +/** + * Add via plus steps of all lessons. + * + * Keep VERY_LAST member last. + */ +val PREPOPULATE_STEPS + get() = + if (DEBUG) DEBUG_LESSONS + else listOf(VERY_FIRST) + LESSON_1_STEPS + LESSON_2_STEPS + VERY_LAST diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/DebugLessons.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/DebugLessons.kt new file mode 100644 index 00000000..3f30bd38 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/DebugLessons.kt @@ -0,0 +1,68 @@ +package com.github.braillesystems.learnbraille.res.russian.steps + +import com.github.braillesystems.learnbraille.database.entities.* +import com.github.braillesystems.learnbraille.database.entities.BrailleDot.E +import com.github.braillesystems.learnbraille.database.entities.BrailleDot.F +import com.github.braillesystems.learnbraille.res.russian.symbols.symbolMap + +val DEBUG_LESSONS + get() = listOf( + + Step( + title = "0 First", + lessonId = 1, + data = FirstInfo( + text = "I am so first" + ) + ), + + Step( + title = "1 Info", + lessonId = 1, + data = Info( + text = "I am best info step!" + ) + ), + + Step( + title = "2 Show dots, null text", + lessonId = 1, + data = ShowDots( + text = null, + dots = BrailleDots(F, F, E, E, F, F) + ) + ), + + Step( + title = "3 Show symbol", + lessonId = 1, + data = ShowSymbol( + symbol = symbolMap['Ф'] ?: error("All RU letters are supported") + ) + ), + + Step( + title = "4 Input dots, some text :)", + lessonId = 1, + data = InputDots( + text = "E E F F E E", + dots = BrailleDots(E, E, F, F, E, E) + ) + ), + + Step( + title = "5, Input symbol", + lessonId = 1, + data = InputSymbol( + symbol = symbolMap['Й'] ?: error("All RU letters are supported") + ) + ), + + Step( + title = "6, Last info", + lessonId = 1, + data = LastInfo( + text = "Veeery last one!!!" + ) + ) + ) diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/Lesson1.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/Lesson1.kt new file mode 100644 index 00000000..4cd15e9b --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/Lesson1.kt @@ -0,0 +1,75 @@ +package com.github.braillesystems.learnbraille.res.russian.steps + +import com.github.braillesystems.learnbraille.database.entities.* +import com.github.braillesystems.learnbraille.database.entities.BrailleDot.F +import com.github.braillesystems.learnbraille.res.russian.PREPOPULATE_LESSONS + +/** + * Automatically inserts proper lessonID. + * + * Cannot be replaced with factory returning lambda for each step because named parameters are + * not supported for functional types in kotlin 1.3. + */ +private fun Step(title: String, data: StepData) = + Step( + title = title, + lessonId = 1L, + data = data + ) + +/** + * List of steps for first lesson. + * + * Do not create symbols manually, always look them up in `symbolMap`. + */ +val LESSON_1_STEPS + get() = listOf( + + Step( + title = "Урок первый", + data = Info(PREPOPULATE_LESSONS[0].name) + ), + + Step( + title = "Знакомство с шеститочием", + data = Info( + """В рельефной азбуке Брайля любой символ - это шеститочие. + |Каждая точка из шести может быть выдавлена или пропущена. + |В следующем шаге все 6 точек выведены на экран.""" + ) + ), + + Step( + title = "Шеститочие", + data = ShowDots( + text = "Перед Вами полное шеститочие", + dots = BrailleDots(F, F, F, F, F, F) + ) + ), + + Step( + title = "Полное шеститочие", + data = InputDots( + text = "Введите все шесть точек", + dots = BrailleDots(F, F, F, F, F, F) + ) + ), + + Step( + title = "Работа с букварём", + data = Info( + """Откройте букварь на странице 12. + |В верхней строке 14 раз повторён символ полного шеститочия.""" + ) + ), + + Step( + title = "Комментарий", + data = Info( + """Точки расположены в два столбца по три. + |Точки в первом столбце имеют номера 1, 2, 3 сверху вниз. + |Точки во втором столбце - 4, 5, 6 сверху вниз. + |Важно выучить, где какая точка.""" + ) + ) + ) diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/Lesson2.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/Lesson2.kt new file mode 100644 index 00000000..e591d056 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/Lesson2.kt @@ -0,0 +1,108 @@ +package com.github.braillesystems.learnbraille.res.russian.steps + +import com.github.braillesystems.learnbraille.database.entities.* +import com.github.braillesystems.learnbraille.res.russian.symbols.symbolMap + +private fun Step(title: String, data: StepData) = + Step( + title = title, + lessonId = 2L, + data = data + ) + + +val LESSON_2_STEPS + get() = listOf( + + Step( + title = "Урок 2. Вступление", + data = Info( + """Некоторые символы Брайля обозначают как букву, так и цифру. Это буквы А, Б, + |Ц и так далее. С добавлением цифрового знака, который мы изучим в следующем + |уроке, из них получаются цифры 1, 2, 3 и так далее.""" + ) + ), + + Step( + title = "Буква А. Комментарий", + data = Info( + """Буква А обозначается одной точкой, точкой номер один. + |Ознакомьтесь с ней.""" + ) + ), + + Step( + title = "Точечный состав буквы А.", + data = ShowSymbol( + symbolMap['А'] ?: error("A russian not found") + ) + ), + + Step( + title = "Работа с букварём", + data = Info( + """Откройте букварь на странице 13. Вверху слева рельефно-грфаическое + |изображение буквы А. Рядом после полного шеститочия пять раз повторена + |буква А точечным шрифтом.""" + ) + ), + + Step( + title = "Введите букву А.", + data = InputSymbol( + symbolMap['А'] ?: error("A russian not found") + ) + ), + + Step( + title = "Точечный состав буквы Б.", + data = ShowSymbol( + symbolMap['Б'] ?: error("Б russian not found") + ) + ), + + Step( + title = "Работа с букварём", + data = Info( + """Снова изучим страницу 13 в букваре. Под строкой с буквой А - + |такая же с буквой Б.""" + ) + ), + + Step( + title = "Введите букву Б.", + data = InputSymbol( + symbolMap['Б'] ?: error("Б russian not found") + ) + ), + + Step( + title = "Буква Ц. Точечный состав.", + data = ShowSymbol( + symbolMap['Ц'] ?: error("Ц russian not found") + ) + ), + + Step( + title = "Работа с букварём", + data = Info( + """Ознакомьтесь с буквой Ц на странице 13 букваря. + |Строка с буквой Ц находится под строкой с буквой Б.""" + ) + ), + + Step( + title = "Введите букву Ц.", + data = InputSymbol( + symbolMap['Б'] ?: error("Ц russian not found") + ) + ), + + Step( + title = "В завершение второго урока", + data = Info( + """Поздравляем! Второй урок пройден. В следующем занятии мы узнаем, + |как с помощью букв А, Б и Ц составить цифры 1, 2 и 3.""" + ) + ) + ) diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/LessonFirst.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/LessonFirst.kt new file mode 100644 index 00000000..374a10f1 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/LessonFirst.kt @@ -0,0 +1,19 @@ +package com.github.braillesystems.learnbraille.res.russian.steps + +import com.github.braillesystems.learnbraille.database.entities.FirstInfo +import com.github.braillesystems.learnbraille.database.entities.Step +import com.github.braillesystems.learnbraille.res.russian.PREPOPULATE_LESSONS + +val VERY_FIRST = Step( + title = "О курсе", + lessonId = PREPOPULATE_LESSONS.first().id, + data = FirstInfo( + """Перед Вами пошаговый курс для обучения с нуля системе Луи Брайля. + |Курс построен на основе книги В. В. Голубиной "Пособие по изучению системы Л. Брайля". + |В течение курса Вы ознакомитесь с обозначениями букв, цифр и специальных символов. + |В некоторых шагах предлагается выполнять задания, используя книгу Голубиной. + |Если Вам будет непонятно, как пользоваться курсом, Вы в любой момент можете вызвать справку + |по разделу, нажав кнопку "справка" в правом верхнем углу экрана. + """ + ) +) diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/LessonLast.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/LessonLast.kt new file mode 100644 index 00000000..c84dd422 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/steps/LessonLast.kt @@ -0,0 +1,15 @@ +package com.github.braillesystems.learnbraille.res.russian.steps + +import com.github.braillesystems.learnbraille.database.entities.LastInfo +import com.github.braillesystems.learnbraille.database.entities.Step +import com.github.braillesystems.learnbraille.res.russian.PREPOPULATE_LESSONS + +val VERY_LAST = Step( + title = "Курс окончен", + lessonId = PREPOPULATE_LESSONS.last().id, + data = LastInfo( + """Вы дошли до конца курса. Спасибо, что воспользовались нашим обучающим + |приложением! Вы всегда можете вернутся к ранее пройденному материалу и повторить его. + """ + ) +) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/AllSymbols.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/AllSymbols.kt similarity index 55% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/AllSymbols.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/AllSymbols.kt index 90de1e2f..0361943c 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/AllSymbols.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/AllSymbols.kt @@ -1,11 +1,11 @@ -package ru.spbstu.amd.learnbraille.res.russian.symbols +package com.github.braillesystems.learnbraille.res.russian.symbols -import ru.spbstu.amd.learnbraille.database.Symbol +import com.github.braillesystems.learnbraille.database.entities.Symbol /** * Add via plus lists of symbols */ -val PREPOPULATE_SYMBOLS = PUNCTUATION + DIGITS + RU_LETTERS +val PREPOPULATE_SYMBOLS get() = PUNCTUATION + DIGITS + RU_LETTERS val symbolMap: Map = PREPOPULATE_SYMBOLS .groupBy { it.symbol } diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/NonLetters.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/NonLetters.kt new file mode 100644 index 00000000..e0e438c6 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/NonLetters.kt @@ -0,0 +1,9 @@ +package com.github.braillesystems.learnbraille.res.russian.symbols + +import com.github.braillesystems.learnbraille.database.entities.Symbol + +// TODO fill punctuation +val PUNCTUATION get() = listOf() + +// TODO fill digits +val DIGITS get() = listOf() diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/RuLetters.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/RuLetters.kt new file mode 100644 index 00000000..8a3d6e18 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/res/russian/symbols/RuLetters.kt @@ -0,0 +1,44 @@ +package com.github.braillesystems.learnbraille.res.russian.symbols + +import com.github.braillesystems.learnbraille.database.entities.BrailleDot.E +import com.github.braillesystems.learnbraille.database.entities.BrailleDot.F +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.database.entities.Language.RU +import com.github.braillesystems.learnbraille.database.entities.Symbol + +val RU_LETTERS + get() = listOf( + Symbol(symbol = 'А', language = RU, brailleDots = BrailleDots(F, E, E, E, E, E)), + Symbol(symbol = 'Б', language = RU, brailleDots = BrailleDots(F, F, E, E, E, E)), + Symbol(symbol = 'В', language = RU, brailleDots = BrailleDots(E, F, E, F, F, F)), + Symbol(symbol = 'Г', language = RU, brailleDots = BrailleDots(F, F, E, F, F, E)), + Symbol(symbol = 'Д', language = RU, brailleDots = BrailleDots(F, E, E, F, F, E)), + Symbol(symbol = 'Е', language = RU, brailleDots = BrailleDots(F, E, E, E, F, E)), + Symbol(symbol = 'Ё', language = RU, brailleDots = BrailleDots(F, E, E, E, E, F)), + Symbol(symbol = 'Ж', language = RU, brailleDots = BrailleDots(E, F, E, F, F, E)), + Symbol(symbol = 'З', language = RU, brailleDots = BrailleDots(F, E, F, E, F, F)), + Symbol(symbol = 'И', language = RU, brailleDots = BrailleDots(E, F, E, F, E, E)), + Symbol(symbol = 'Й', language = RU, brailleDots = BrailleDots(F, F, F, F, E, F)), + Symbol(symbol = 'К', language = RU, brailleDots = BrailleDots(F, E, F, E, E, E)), + Symbol(symbol = 'Л', language = RU, brailleDots = BrailleDots(F, F, F, E, E, E)), + Symbol(symbol = 'М', language = RU, brailleDots = BrailleDots(F, E, F, F, E, E)), + Symbol(symbol = 'Н', language = RU, brailleDots = BrailleDots(F, E, F, F, F, E)), + Symbol(symbol = 'О', language = RU, brailleDots = BrailleDots(F, E, F, E, F, E)), + Symbol(symbol = 'П', language = RU, brailleDots = BrailleDots(F, F, F, F, E, E)), + Symbol(symbol = 'Р', language = RU, brailleDots = BrailleDots(F, F, F, E, F, E)), + Symbol(symbol = 'С', language = RU, brailleDots = BrailleDots(E, F, F, F, E, E)), + Symbol(symbol = 'Т', language = RU, brailleDots = BrailleDots(E, F, F, F, F, E)), + Symbol(symbol = 'У', language = RU, brailleDots = BrailleDots(F, E, F, E, E, F)), + Symbol(symbol = 'Ф', language = RU, brailleDots = BrailleDots(F, F, E, F, E, E)), + Symbol(symbol = 'Х', language = RU, brailleDots = BrailleDots(F, F, E, E, F, E)), + Symbol(symbol = 'Ц', language = RU, brailleDots = BrailleDots(F, E, E, F, E, E)), + Symbol(symbol = 'Ч', language = RU, brailleDots = BrailleDots(F, F, F, F, F, E)), + Symbol(symbol = 'Ш', language = RU, brailleDots = BrailleDots(F, E, E, E, F, F)), + Symbol(symbol = 'Щ', language = RU, brailleDots = BrailleDots(F, E, F, F, E, F)), + Symbol(symbol = 'Ъ', language = RU, brailleDots = BrailleDots(F, F, F, E, F, F)), + Symbol(symbol = 'Ы', language = RU, brailleDots = BrailleDots(E, F, F, F, E, F)), + Symbol(symbol = 'Ь', language = RU, brailleDots = BrailleDots(E, F, F, F, F, F)), + Symbol(symbol = 'Э', language = RU, brailleDots = BrailleDots(E, F, E, F, E, F)), + Symbol(symbol = 'Ю', language = RU, brailleDots = BrailleDots(F, F, E, E, F, F)), + Symbol(symbol = 'Я', language = RU, brailleDots = BrailleDots(F, F, E, F, E, F)) + ) diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/AbstractFragmentWithHelp.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/AbstractFragmentWithHelp.kt new file mode 100644 index 00000000..2c16eac1 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/AbstractFragmentWithHelp.kt @@ -0,0 +1,35 @@ +package com.github.braillesystems.learnbraille.screens + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.screens.practice.PracticeFragmentDirections +import timber.log.Timber + +typealias HelpMsgId = Int + +/** + * Do not forget to add in onCreate `setHasOptionsMenu(true)` + */ +abstract class AbstractFragmentWithHelp(private val helpMsgId: HelpMsgId) : Fragment() { + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.help_menu, menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = + super.onOptionsItemSelected(item).also { + if (item.itemId == R.id.help) navigateToHelp() + } + + protected fun navigateToHelp() { + Timber.i("Navigate to help") + val action = PracticeFragmentDirections.actionGlobalHelpFragment() + action.helpMessage = getString(helpMsgId) + findNavController().navigate(action) + } +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/DotsChecker.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/DotsChecker.kt new file mode 100644 index 00000000..4558a664 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/DotsChecker.kt @@ -0,0 +1,219 @@ +package com.github.braillesystems.learnbraille.screens + +import android.os.Vibrator +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.github.braillesystems.learnbraille.CORRECT_BUZZ_PATTERN +import com.github.braillesystems.learnbraille.INCORRECT_BUZZ_PATTERN +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.serial.UsbSerial +import com.github.braillesystems.learnbraille.util.buzz +import com.github.braillesystems.learnbraille.util.side +import com.github.braillesystems.learnbraille.views.BrailleDotsState +import com.github.braillesystems.learnbraille.views.clickable +import com.github.braillesystems.learnbraille.views.display +import com.github.braillesystems.learnbraille.views.uncheck +import timber.log.Timber + +/** + * Represents state machine that serves input tasks. + */ +interface DotsChecker { + + val eventCorrect: LiveData + fun onCorrectComplete() + + val eventIncorrect: LiveData + fun onIncorrectComplete() + + val eventHint: LiveData + fun onHintComplete() + + val eventPassHint: LiveData + fun onPassHintComplete() + + val state: State + + fun onCheck() + fun onHint() + + enum class State { + INPUT, HINT + } +} + +interface MutableDotsChecker : DotsChecker { + + var getEnteredDots: () -> BrailleDots + var getExpectedDots: () -> BrailleDots? + + var onCheckHandler: () -> Unit + var onCorrectHandler: () -> Unit + var onIncorrectHandler: () -> Unit + var onHintHandler: () -> Unit + var onPassHintHandler: () -> Unit + + companion object { + fun create(): MutableDotsChecker = DotsCheckerImpl() + } +} + +/** + * Initialize lateinit callbacks firstly + */ +private class DotsCheckerImpl : MutableDotsChecker { + + override lateinit var getEnteredDots: () -> BrailleDots + override lateinit var getExpectedDots: () -> BrailleDots? + + override var onCheckHandler: () -> Unit = {} + override var onCorrectHandler: () -> Unit = {} + override var onIncorrectHandler: () -> Unit = {} + override var onHintHandler: () -> Unit = {} + override var onPassHintHandler: () -> Unit = {} + + private val _eventCorrect = MutableLiveData() + override val eventCorrect: LiveData + get() = _eventCorrect + + private val _eventIncorrect = MutableLiveData() + override val eventIncorrect: LiveData + get() = _eventIncorrect + + private val _eventHint = MutableLiveData() + override val eventHint: LiveData + get() = _eventHint + + private val _eventPassHint = MutableLiveData() + override val eventPassHint: LiveData + get() = _eventPassHint + + private val enteredDots: BrailleDots + get() = + if (::getEnteredDots.isInitialized) getEnteredDots() + else error("getEnteredDots property should be initialized") + + private val expectedDots: BrailleDots? + get() = + if (::getExpectedDots.isInitialized) getExpectedDots() + else error("getExpectedDots should be initialized") + + private val isCorrect: Boolean + get() = (enteredDots == expectedDots).also { + Timber.i( + if (it) "Correct: " else "Incorrect: " + + "entered = ${enteredDots}, expected = $expectedDots" + ) + } + + override var state = DotsChecker.State.INPUT + private set + + override fun onCheck() = onCheckHandler().side { + if (state == DotsChecker.State.HINT) { + state = DotsChecker.State.INPUT + onPassHint() + } else { + if (isCorrect) onCorrect() + else onIncorrect() + } + } + + private fun onCorrect() = onCorrectHandler().side { + _eventCorrect.value = true + } + + private fun onIncorrect() = onIncorrectHandler().side { + _eventIncorrect.value = true + } + + private fun onPassHint() = onPassHintHandler().side { + _eventPassHint.value = true + } + + override fun onHint() = onHintHandler().side { + if (state == DotsChecker.State.HINT) { + state = DotsChecker.State.INPUT + onPassHint() + } else { + state = DotsChecker.State.HINT + _eventHint.value = expectedDots + } + } + + override fun onCorrectComplete() { + _eventCorrect.value = false + } + + override fun onHintComplete() { + _eventHint.value = null + } + + override fun onPassHintComplete() { + _eventPassHint.value = false + } + + override fun onIncorrectComplete() { + _eventIncorrect.value = false + } +} + +/** + * Return observer with default behaviour. + */ +fun DotsChecker.getEventCorrectObserver( + dots: BrailleDotsState, + buzzer: Vibrator? = null, + block: () -> Unit = {} +) = Observer { + if (!it) return@Observer + buzzer.buzz(CORRECT_BUZZ_PATTERN) + dots.uncheck() + block() + onCorrectComplete() +} + +/** + * Return observer with default behaviour. + */ +fun DotsChecker.getEventIncorrectObserver( + dots: BrailleDotsState, + buzzer: Vibrator? = null, + block: () -> Unit = {} +) = Observer { + if (!it) return@Observer + buzzer.buzz(INCORRECT_BUZZ_PATTERN) + dots.uncheck() + block() + onIncorrectComplete() +} + +/** + * Return observer with default behaviour. + */ +fun DotsChecker.getEventHintObserver( + dots: BrailleDotsState, + serial: UsbSerial? = null, + block: (BrailleDots) -> Unit +) = Observer { expectedDots -> + if (expectedDots == null) return@Observer + dots.display(expectedDots) + serial?.trySend(expectedDots) + block(expectedDots) + onHintComplete() +} + +/** + * Return observer with default behaviour. + */ +fun DotsChecker.getEventPassHintObserver( + dots: BrailleDotsState, + block: () -> Unit +) = Observer { + if (!it) return@Observer + dots.uncheck() + dots.clickable(true) + block() + onPassHintComplete() +} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/exit/ExitFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/exit/ExitFragment.kt similarity index 72% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/exit/ExitFragment.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/screens/exit/ExitFragment.kt index 7bc9090c..00365f84 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/exit/ExitFragment.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/exit/ExitFragment.kt @@ -1,4 +1,4 @@ -package ru.spbstu.amd.learnbraille.screens.exit +package com.github.braillesystems.learnbraille.screens.exit import android.os.Bundle import android.view.LayoutInflater @@ -6,9 +6,9 @@ import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.navigation.Navigation -import ru.spbstu.amd.learnbraille.R -import ru.spbstu.amd.learnbraille.databinding.FragmentExitBinding -import ru.spbstu.amd.learnbraille.screens.updateTitle +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.databinding.FragmentExitBinding +import com.github.braillesystems.learnbraille.util.updateTitle import kotlin.system.exitProcess class ExitFragment : Fragment() { @@ -31,7 +31,7 @@ class ExitFragment : Fragment() { } continueButton.setOnClickListener( - Navigation.createNavigateOnClickListener(R.id.action_exitFragment_to_menuFragment) + Navigation.createNavigateOnClickListener(R.id.action_global_menuFragment) ) }.root diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/greeting/GreetingFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/greeting/GreetingFragment.kt similarity index 69% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/greeting/GreetingFragment.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/screens/greeting/GreetingFragment.kt index 31effffa..4fef69b8 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/greeting/GreetingFragment.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/greeting/GreetingFragment.kt @@ -1,4 +1,4 @@ -package ru.spbstu.amd.learnbraille.screens.greeting +package com.github.braillesystems.learnbraille.screens.greeting import android.os.Bundle import android.view.LayoutInflater @@ -6,10 +6,11 @@ import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.navigation.Navigation -import ru.spbstu.amd.learnbraille.R -import ru.spbstu.amd.learnbraille.databinding.FragmentGreetingBinding -import ru.spbstu.amd.learnbraille.screens.updateTitle +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.databinding.FragmentGreetingBinding +import com.github.braillesystems.learnbraille.util.updateTitle +// TODO decide what to do with this functionality class GreetingFragment : Fragment() { override fun onCreateView( diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/help/HelpFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/help/HelpFragment.kt similarity index 59% rename from android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/help/HelpFragment.kt rename to android/app/src/main/java/com/github/braillesystems/learnbraille/screens/help/HelpFragment.kt index a70cecf9..cb676be6 100644 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/help/HelpFragment.kt +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/help/HelpFragment.kt @@ -1,14 +1,14 @@ -package ru.spbstu.amd.learnbraille.screens.help - +package com.github.braillesystems.learnbraille.screens.help import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment -import ru.spbstu.amd.learnbraille.R -import ru.spbstu.amd.learnbraille.databinding.FragmentHelpBinding -import ru.spbstu.amd.learnbraille.screens.updateTitle +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.databinding.FragmentHelpBinding +import com.github.braillesystems.learnbraille.util.getFormattedArg +import com.github.braillesystems.learnbraille.util.updateTitle class HelpFragment : Fragment() { @@ -25,10 +25,7 @@ class HelpFragment : Fragment() { ).apply { updateTitle(getString(R.string.help_title)) - - helpMessage.text = arguments - ?.getString(helpMessageArgName) - ?: error("Unable to get help message arg") + helpMessage.text = getFormattedArg(helpMessageArgName) }.root } diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/AbstractInputLesson.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/AbstractInputLesson.kt new file mode 100644 index 00000000..101d1424 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/AbstractInputLesson.kt @@ -0,0 +1,102 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.view.View +import android.widget.Toast +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.LearnBrailleDatabase +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.database.entities.Step +import com.github.braillesystems.learnbraille.database.entities.spelling +import com.github.braillesystems.learnbraille.screens.HelpMsgId +import com.github.braillesystems.learnbraille.views.BrailleDotsState +import com.github.braillesystems.learnbraille.views.spelling +import timber.log.Timber + +/** + * Set `userTouchedDots` to false in `onCreateView` + */ +abstract class AbstractInputLesson(helpMsgId: HelpMsgId) : AbstractLesson(helpMsgId) { + + protected var userTouchedDots: Boolean = false + + protected fun getPrevButtonListener(step: Step, userId: Long, database: LearnBrailleDatabase) = + View.OnClickListener { + database.apply { + navigateToPrevStep( + current = step, + userId = userId, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + } + + protected fun getToCurrStepListener(userId: Long, database: LearnBrailleDatabase) = + View.OnClickListener { + database.apply { + navigateToCurrentStep( + userId = userId, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + } + + protected fun getEventCorrectObserverBlock( + step: Step, + userId: Long, + database: LearnBrailleDatabase + ): () -> Unit = { + database.apply { + Timber.i("Handle correct") + Toast.makeText( + context, getString(R.string.msg_correct), Toast.LENGTH_SHORT + ).show() + navigateToNextStep( + current = step, + userId = userId, + stepDao = stepDao, + lastStepDao = userLastStep, + upsd = userPassedStepDao + ) + } + } + + protected fun getEventIncorrectObserverBlock( + step: Step, + userId: Long, + database: LearnBrailleDatabase, + dots: BrailleDotsState + ): () -> Unit = { + database.apply { + Timber.i("Handle incorrect: entered = ${dots.spelling}") + if (userTouchedDots) { + Toast.makeText( + context, getString(R.string.msg_incorrect), Toast.LENGTH_SHORT + ).show() + } else { + navigateToNextStep( + current = step, + userId = userId, + stepDao = stepDao, + lastStepDao = userLastStep + ) { + Toast.makeText( + context, getString(R.string.msg_incorrect), Toast.LENGTH_SHORT + ).show() + } + } + } + } + + protected fun getEventHintObserverBlock(): (BrailleDots) -> Unit = { expectedDots -> + Timber.i("Handle hint") + val toast = getString(R.string.practice_hint_template) + .format(expectedDots.spelling) + Toast.makeText(context, toast, Toast.LENGTH_LONG).show() + } + + protected fun getEventPassHintObserverBlock(): () -> Unit = { + Timber.i("Handle pass hint") + } +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/AbstractLesson.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/AbstractLesson.kt new file mode 100644 index 00000000..25e4c3ee --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/AbstractLesson.kt @@ -0,0 +1,126 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.github.braillesystems.learnbraille.database.entities.* +import com.github.braillesystems.learnbraille.screens.AbstractFragmentWithHelp +import com.github.braillesystems.learnbraille.screens.HelpMsgId +import com.github.braillesystems.learnbraille.screens.lessons.AbstractLesson.Companion.stepArgName +import com.github.braillesystems.learnbraille.screens.menu.MenuFragment +import com.github.braillesystems.learnbraille.screens.menu.MenuFragmentDirections +import com.github.braillesystems.learnbraille.util.devnull +import com.github.braillesystems.learnbraille.util.getStringArg +import com.github.braillesystems.learnbraille.util.scope +import com.github.braillesystems.learnbraille.util.side +import kotlinx.coroutines.launch +import timber.log.Timber + +/** + * Base class for all lessons. + */ +abstract class AbstractLesson(helpMsgId: HelpMsgId) : AbstractFragmentWithHelp(helpMsgId) { + companion object { + const val stepArgName = "step" + } +} + +fun AbstractLesson.getStepArg() = stepOf(getStringArg(stepArgName)) + +private fun Fragment.navigateToStepHelper( + nextStep: Step, + userId: Long, + lastStepDao: UserLastStepDao +): Unit = + nextStep.toString().let { step -> + when (nextStep.data) { + is Info -> MenuFragmentDirections.actionGlobalInfoFragment(step) + is FirstInfo -> MenuFragmentDirections.actionGlobalFirstInfoFragment(step) + is LastInfo -> MenuFragmentDirections.actionGlobalLastInfoFragment(step) + is InputSymbol -> MenuFragmentDirections.actionGlobalInputSymbolFragment(step) + is InputDots -> MenuFragmentDirections.actionGlobalInputDotsFragment(step) + is ShowSymbol -> MenuFragmentDirections.actionGlobalShowSymbolFragment(step) + is ShowDots -> MenuFragmentDirections.actionGlobalShowDotsFragment(step) + } + }.side { action -> + Timber.i("Navigating to step with id = ${nextStep.id}") + val lastStep = UserLastStep(userId, nextStep.id) + scope().launch { lastStepDao.insertLastStep(lastStep) } + findNavController().navigate(action) + } + +fun AbstractLesson.navigateToStep(nextStep: Step, userId: Long, lastStepDao: UserLastStepDao) = + navigateToStepHelper(nextStep, userId, lastStepDao) + +fun MenuFragment.navigateToStep(nextStep: Step, userId: Long, lastStepDao: UserLastStepDao) = + navigateToStepHelper(nextStep, userId, lastStepDao) + +fun AbstractLesson.navigateToPrevStep( + current: Step, + userId: Long, + stepDao: StepDao, + lastStepDao: UserLastStepDao +): Unit = + if (current.data is FirstInfo) Timber.w("Trying to get step before first") + else scope().launch { + stepDao.getStep(current.id - 1)?.let { step -> + navigateToStep(step, userId, lastStepDao) + } ?: error("No step with less id") + }.devnull + +/** + * Navigate to the next step if current is already passes. + * + * @param upsd Add current step to the UserPassedStep before navigation if not null. + * @param onNextNotAvailable Is called when current step is not passed and `upsd` is `null`. + */ +fun AbstractLesson.navigateToNextStep( + current: Step, + userId: Long, + stepDao: StepDao, + lastStepDao: UserLastStepDao, + upsd: UserPassedStepDao? = null, + onNextNotAvailable: AbstractLesson.() -> Unit = {} +): Unit = + if (current.data is LastInfo) Timber.w("Trying to get step after last") + else scope().launch { + upsd?.insertPassedStep(UserPassedStep(userId, current.id)) + stepDao.getNextStepForUser(userId, current.id)?.let { step -> + navigateToStep(step, userId, lastStepDao) + } ?: Timber.i("On next step not available call").side { + onNextNotAvailable() + } + }.devnull + +/** + * Navigate to the last passed step in course progress. + */ +fun AbstractLesson.navigateToCurrentStep( + userId: Long, + stepDao: StepDao, + lastStepDao: UserLastStepDao +): Unit = + scope().launch { + navigateToStep( + stepDao.getCurrentStepForUser(userId) + ?: error("User ($userId) should always have at least one last step"), + userId, lastStepDao + ) + }.devnull + +/** + * Navigate to last step visited by user, or to the current step that always exists. + */ +fun MenuFragment.navigateToLastStep( + userId: Long, + stepDao: StepDao, + lastStepDao: UserLastStepDao +): Unit = + scope().launch { + stepDao.getLastStepForUser(userId)?.let { step -> + navigateToStep(step, userId, lastStepDao) + } ?: navigateToStep( + stepDao.getCurrentStepForUser(userId) + ?: error("$userId does not have current step"), + userId, lastStepDao + ) + }.devnull diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/FirstInfoFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/FirstInfoFragment.kt new file mode 100644 index 00000000..f5b55691 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/FirstInfoFragment.kt @@ -0,0 +1,55 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.FirstInfo +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentLessonFirstInfoBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.util.updateTitle + +class FirstInfoFragment : AbstractLesson(R.string.lessons_help_info) { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_lesson_first_info, + container, + false + ).apply { + + updateTitle(getString(R.string.lessons_title_info)) + setHasOptionsMenu(true) + + val step = getStepArg() + require(step.data is FirstInfo) + titleTextView.text = step.title + infoTextView.text = step.data.text + + getDBInstance().apply { + nextButton.setOnClickListener { + navigateToNextStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep, + upsd = userPassedStepDao + ) + } + toCurrStepButton.setOnClickListener { + navigateToCurrentStep( + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + } + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InfoFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InfoFragment.kt new file mode 100644 index 00000000..4cf4e595 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InfoFragment.kt @@ -0,0 +1,63 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.Info +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentLessonsInfoBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.util.updateTitle + +class InfoFragment : AbstractLesson(R.string.lessons_help_info) { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_lessons_info, + container, + false + ).apply { + + updateTitle(getString(R.string.lessons_title_info)) + setHasOptionsMenu(true) + + val step = getStepArg() + require(step.data is Info) + titleTextView.text = step.title + infoTextView.text = step.data.text + + getDBInstance().apply { + prevButton.setOnClickListener { + navigateToPrevStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + nextButton.setOnClickListener { + navigateToNextStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep, + upsd = userPassedStepDao + ) + } + toCurrStepButton.setOnClickListener { + navigateToCurrentStep( + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + } + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputDotsFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputDotsFragment.kt new file mode 100644 index 00000000..e8cdbf10 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputDotsFragment.kt @@ -0,0 +1,118 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.os.Bundle +import android.os.Vibrator +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.getSystemService +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.database.entities.InputDots +import com.github.braillesystems.learnbraille.database.entities.spelling +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentLessonsInputDotsBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.screens.getEventCorrectObserver +import com.github.braillesystems.learnbraille.screens.getEventHintObserver +import com.github.braillesystems.learnbraille.screens.getEventIncorrectObserver +import com.github.braillesystems.learnbraille.screens.getEventPassHintObserver +import com.github.braillesystems.learnbraille.util.application +import com.github.braillesystems.learnbraille.util.updateTitle +import com.github.braillesystems.learnbraille.views.* +import timber.log.Timber + +class InputDotsFragment : AbstractInputLesson(R.string.lessons_help_input_dots) { + + private lateinit var viewModel: InputViewModel + private lateinit var expectedDots: BrailleDots + private lateinit var dots: BrailleDotsState + private var buzzer: Vibrator? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_lessons_input_dots, + container, + false + ).apply { + + Timber.i("Start initialize input dots fragment") + + updateTitle(getString(R.string.lessons_title_input_dots)) + setHasOptionsMenu(true) + + val step = getStepArg() + require(step.data is InputDots) + titleTextView.text = step.title + infoTextView.text = step.data.text + ?: getString(R.string.lessons_input_dots_info_template) + .format(step.data.dots.spelling) + brailleDots.dots.display(step.data.dots) + + expectedDots = step.data.dots + userTouchedDots = false + dots = brailleDots.dots.apply { + uncheck() + clickable(true) + checkBoxes.forEach { checkBox -> + checkBox.setOnClickListener { + userTouchedDots = true + } + } + } + + + val viewModelFactory = InputViewModelFactory(application, expectedDots) { + dots.brailleDots + } + viewModel = ViewModelProvider( + this@InputDotsFragment, viewModelFactory + ).get(InputViewModel::class.java) + buzzer = activity?.getSystemService() + + + inputViewModel = viewModel + lifecycleOwner = this@InputDotsFragment + + + val database = getDBInstance() + + prevButton.setOnClickListener(getPrevButtonListener(step, defaultUser, database)) + toCurrStepButton.setOnClickListener(getToCurrStepListener(defaultUser, database)) + + viewModel.eventCorrect.observe( + viewLifecycleOwner, + viewModel.getEventCorrectObserver( + dots, buzzer, + getEventCorrectObserverBlock(step, defaultUser, database) + ) + ) + + viewModel.eventIncorrect.observe( + viewLifecycleOwner, + viewModel.getEventIncorrectObserver( + dots, buzzer, + getEventIncorrectObserverBlock(step, defaultUser, database, dots) + ) + ) + + viewModel.eventHint.observe( + viewLifecycleOwner, + viewModel.getEventHintObserver( + dots, null, /*TODO serial */ + getEventHintObserverBlock() + ) + ) + + viewModel.eventPassHint.observe( + viewLifecycleOwner, + viewModel.getEventPassHintObserver(dots, getEventPassHintObserverBlock()) + ) + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputSymbolFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputSymbolFragment.kt new file mode 100644 index 00000000..4fa5ea15 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputSymbolFragment.kt @@ -0,0 +1,115 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.os.Bundle +import android.os.Vibrator +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.getSystemService +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.database.entities.InputSymbol +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentLessonsInputSymbolBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.screens.getEventCorrectObserver +import com.github.braillesystems.learnbraille.screens.getEventHintObserver +import com.github.braillesystems.learnbraille.screens.getEventIncorrectObserver +import com.github.braillesystems.learnbraille.screens.getEventPassHintObserver +import com.github.braillesystems.learnbraille.util.application +import com.github.braillesystems.learnbraille.util.updateTitle +import com.github.braillesystems.learnbraille.views.* +import timber.log.Timber + +class InputSymbolFragment : AbstractInputLesson(R.string.lessons_help_input_symbol) { + + private lateinit var viewModel: InputViewModel + private lateinit var expectedDots: BrailleDots + private lateinit var dots: BrailleDotsState + private var buzzer: Vibrator? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_lessons_input_symbol, + container, + false + ).apply { + + Timber.i("Initialize input symbol fragment") + + updateTitle(getString(R.string.lessons_title_input_symbol)) + setHasOptionsMenu(true) + + val step = getStepArg() + require(step.data is InputSymbol) + titleTextView.text = step.title + letter.text = step.data.symbol.symbol.toString() + brailleDots.dots.display(step.data.symbol.brailleDots) + + expectedDots = step.data.symbol.brailleDots + userTouchedDots = false + dots = brailleDots.dots.apply { + uncheck() + clickable(true) + checkBoxes.forEach { checkBox -> + checkBox.setOnClickListener { + userTouchedDots = true + } + } + } + + + val viewModelFactory = InputViewModelFactory(application, expectedDots) { + dots.brailleDots + } + viewModel = ViewModelProvider( + this@InputSymbolFragment, viewModelFactory + ).get(InputViewModel::class.java) + buzzer = activity?.getSystemService() + + + inputViewModel = viewModel + lifecycleOwner = this@InputSymbolFragment + + + val database = getDBInstance() + + prevButton.setOnClickListener(getPrevButtonListener(step, defaultUser, database)) + toCurrStepButton.setOnClickListener(getToCurrStepListener(defaultUser, database)) + + viewModel.eventCorrect.observe( + viewLifecycleOwner, + viewModel.getEventCorrectObserver( + dots, buzzer, + getEventCorrectObserverBlock(step, defaultUser, database) + ) + ) + + viewModel.eventIncorrect.observe( + viewLifecycleOwner, + viewModel.getEventIncorrectObserver( + dots, buzzer, + getEventIncorrectObserverBlock(step, defaultUser, database, dots) + ) + ) + + viewModel.eventHint.observe( + viewLifecycleOwner, + viewModel.getEventHintObserver( + dots, null, /*TODO serial */ + getEventHintObserverBlock() + ) + ) + + viewModel.eventPassHint.observe( + viewLifecycleOwner, + viewModel.getEventPassHintObserver(dots, getEventPassHintObserverBlock()) + ) + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputViewModel.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputViewModel.kt new file mode 100644 index 00000000..fb74b3d9 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/InputViewModel.kt @@ -0,0 +1,40 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.screens.DotsChecker +import com.github.braillesystems.learnbraille.screens.MutableDotsChecker +import timber.log.Timber + +class InputViewModelFactory( + private val application: Application, + private val expectedDots: BrailleDots, + private val getEnteredDots: () -> BrailleDots +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T = + if (modelClass.isAssignableFrom(InputViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + InputViewModel(application, getEnteredDots, expectedDots) as T + } else { + throw IllegalArgumentException("Unknown ViewModel class") + } +} + +class InputViewModel( + application: Application, + getEnteredDots: () -> BrailleDots, + private val expectedDots: BrailleDots, + private val dotsChecker: MutableDotsChecker = MutableDotsChecker.create() +) : AndroidViewModel(application), + DotsChecker by dotsChecker { + + init { + Timber.i("Initialize input view model") + dotsChecker.getEnteredDots = getEnteredDots + dotsChecker.getExpectedDots = { expectedDots } + } +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/LastInfoFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/LastInfoFragment.kt new file mode 100644 index 00000000..cb4937ae --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/LastInfoFragment.kt @@ -0,0 +1,47 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.LastInfo +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentLessonLastInfoBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.util.updateTitle + +class LastInfoFragment : AbstractLesson(R.string.lessons_help_last_info) { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_lesson_last_info, + container, + false + ).apply { + + updateTitle(getString(R.string.lessons_title_last_info)) + setHasOptionsMenu(true) + + val step = getStepArg() + require(step.data is LastInfo) + titleTextView.text = step.title + infoTextView.text = step.data.text + + getDBInstance().run { + prevButton.setOnClickListener { + navigateToPrevStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + } + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/ShowDotsFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/ShowDotsFragment.kt new file mode 100644 index 00000000..2b80e626 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/ShowDotsFragment.kt @@ -0,0 +1,72 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.ShowDots +import com.github.braillesystems.learnbraille.database.entities.spelling +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentLessonsShowDotsBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.util.updateTitle +import com.github.braillesystems.learnbraille.views.display +import com.github.braillesystems.learnbraille.views.dots +import timber.log.Timber + +class ShowDotsFragment : AbstractLesson(R.string.lessons_help_show_dots) { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_lessons_show_dots, + container, + false + ).apply { + + Timber.i("Initialize show dots fragment") + + updateTitle(getString(R.string.lessons_title_show_dots)) + setHasOptionsMenu(true) + + val step = getStepArg() + require(step.data is ShowDots) + titleTextView.text = step.title + infoTextView.text = step.data.text + ?: getString(R.string.lessons_show_dots_info_template) + .format(step.data.dots.spelling) + brailleDots.dots.display(step.data.dots) + + getDBInstance().apply { + prevButton.setOnClickListener { + navigateToPrevStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + nextButton.setOnClickListener { + navigateToNextStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep, + upsd = userPassedStepDao + ) + } + toCurrStepButton.setOnClickListener { + navigateToCurrentStep( + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + } + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/ShowSymbolFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/ShowSymbolFragment.kt new file mode 100644 index 00000000..59c698cf --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/lessons/ShowSymbolFragment.kt @@ -0,0 +1,69 @@ +package com.github.braillesystems.learnbraille.screens.lessons + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.ShowSymbol +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentLessonsShowSymbolBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.util.updateTitle +import com.github.braillesystems.learnbraille.views.display +import com.github.braillesystems.learnbraille.views.dots +import timber.log.Timber + +class ShowSymbolFragment : AbstractLesson(R.string.lessons_help_show_symbol) { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_lessons_show_symbol, + container, + false + ).apply { + + Timber.i("Initialize show symbol fragment") + + updateTitle(getString(R.string.lessons_title_show_symbol)) + setHasOptionsMenu(true) + + val step = getStepArg() + require(step.data is ShowSymbol) + titleTextView.text = step.title + letter.text = step.data.symbol.symbol.toString() + brailleDots.dots.display(step.data.symbol.brailleDots) + + getDBInstance().apply { + prevButton.setOnClickListener { + navigateToPrevStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + nextButton.setOnClickListener { + navigateToNextStep( + current = step, + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep, + upsd = userPassedStepDao + ) + } + toCurrStepButton.setOnClickListener { + navigateToCurrentStep( + userId = defaultUser, + stepDao = stepDao, + lastStepDao = userLastStep + ) + } + } + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/menu/MenuFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/menu/MenuFragment.kt new file mode 100644 index 00000000..80b0c2bb --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/menu/MenuFragment.kt @@ -0,0 +1,110 @@ +package com.github.braillesystems.learnbraille.screens.menu + +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.LearnBrailleDatabase +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentMenuBinding +import com.github.braillesystems.learnbraille.defaultUser +import com.github.braillesystems.learnbraille.screens.AbstractFragmentWithHelp +import com.github.braillesystems.learnbraille.screens.lessons.navigateToLastStep +import com.github.braillesystems.learnbraille.util.application +import com.github.braillesystems.learnbraille.util.updateTitle +import timber.log.Timber + +class MenuFragment : AbstractFragmentWithHelp(R.string.menu_help) { + + private val isDbPrepopulated: Boolean + get() = (application.prepopulationJob.isCompleted && + LearnBrailleDatabase.prepopulationFinished).also { + if (it) Timber.i("DB has been prepopulated") + else Timber.i("DB has not been prepopulated") + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_menu, + container, + false + ).apply { + + updateTitle(getString(R.string.menu_actionbar_text)) + + setHasOptionsMenu(true) + + lessonsButton.setOnClickListener(interruptingOnClickListener { + getDBInstance().apply { + navigateToLastStep(defaultUser, stepDao, userLastStep) + } + }) + + practiceButton.setOnClickListener(interruptingOnClickListener { + findNavController().navigate(R.id.action_menuFragment_to_practiceFragment) + }) + + offlinePracticeButton.setOnClickListener { + try { + val intent = Intent("com.google.zxing.client.android.SCAN") + intent.putExtra("SCAN_MODE", "QR_CODE_MODE") + startActivityForResult(intent, 0) + } catch (e: Exception) { + val marketUri = Uri.parse("market://details?id=com.google.zxing.client.android") + val marketIntent = Intent(Intent.ACTION_VIEW, marketUri) + startActivity(marketIntent) + } + } + + stackedHelpButton.setOnClickListener { + navigateToHelp() + } + + exitButton.setOnClickListener( + Navigation.createNavigateOnClickListener(R.id.action_menuFragment_to_exitFragment) + ) + + }.root + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == qtResultCode) { + if (resultCode == RESULT_OK) { + val contents = data?.getStringExtra("SCAN_RESULT") + Toast.makeText(context, contents, Toast.LENGTH_SHORT).show() + } + if (resultCode == RESULT_CANCELED) { + Toast.makeText( + context, getString(R.string.msg_cancelled), Toast.LENGTH_SHORT + ).show() + } + } + } + + private fun interruptingOnClickListener(block: (View) -> Unit) = View.OnClickListener { + if (!isDbPrepopulated) { + Toast.makeText( + context, getString(R.string.menu_db_not_initialized_warning), Toast.LENGTH_LONG + ).show() + return@OnClickListener + } + block(it) + } + + companion object { + const val qtResultCode = 0 + } +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/practice/PracticeFragment.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/practice/PracticeFragment.kt new file mode 100644 index 00000000..0ec98457 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/practice/PracticeFragment.kt @@ -0,0 +1,127 @@ +package com.github.braillesystems.learnbraille.screens.practice + +import android.annotation.SuppressLint +import android.content.IntentFilter +import android.hardware.usb.UsbManager +import android.os.Bundle +import android.os.Vibrator +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.getSystemService +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.spelling +import com.github.braillesystems.learnbraille.database.getDBInstance +import com.github.braillesystems.learnbraille.databinding.FragmentPracticeBinding +import com.github.braillesystems.learnbraille.screens.* +import com.github.braillesystems.learnbraille.serial.UsbSerial +import com.github.braillesystems.learnbraille.util.application +import com.github.braillesystems.learnbraille.util.updateTitle +import com.github.braillesystems.learnbraille.views.BrailleDotsState +import com.github.braillesystems.learnbraille.views.brailleDots +import com.github.braillesystems.learnbraille.views.dots +import com.github.braillesystems.learnbraille.views.spelling +import timber.log.Timber + +class PracticeFragment : AbstractFragmentWithHelp(R.string.practice_help) { + + private lateinit var viewModel: PracticeViewModel + private lateinit var dots: BrailleDotsState + private var buzzer: Vibrator? = null + + private val title: String + get() = getString(R.string.practice_actionbar_title_template).let { + if (::viewModel.isInitialized) it.format( + viewModel.nCorrect, + viewModel.nTries + ) + else it.format(0, 0) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DataBindingUtil.inflate( + inflater, + R.layout.fragment_practice, + container, + false + ).apply { + + Timber.i("Start initialize practice fragment") + + updateTitle(title) + setHasOptionsMenu(true) + + val dataSource = getDBInstance().symbolDao + dots = brailleDots.dots + + val viewModelFactory = PracticeViewModelFactory(dataSource, application) { + dots.brailleDots + } + viewModel = ViewModelProvider( + this@PracticeFragment, viewModelFactory + ).get(PracticeViewModel::class.java) + buzzer = activity?.getSystemService() + + + practiceViewModel = viewModel + lifecycleOwner = this@PracticeFragment + + + // Init serial connection with Braille Trainer + // TODO extract initialization to factory + // TODO use application.usbManager + @SuppressLint("WrongConstant") // permit `application.getSystemService(USB_SERVICE)` + val usbManager = application.getSystemService("usb") as UsbManager + val filter = IntentFilter().apply { + addAction(UsbSerial.ACTION_USB_PERMISSION) + addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED) + addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) + } + val serial = UsbSerial(usbManager, application) + application.registerReceiver(serial.broadcastReceiver, filter) + + + viewModel.eventCorrect.observe( + viewLifecycleOwner, + viewModel.getEventCorrectObserver(dots, buzzer) { + Timber.i("Handle correct") + Toast.makeText(context, getString(R.string.msg_correct), Toast.LENGTH_SHORT).show() + updateTitle(title) + } + ) + + viewModel.eventIncorrect.observe( + viewLifecycleOwner, + viewModel.getEventIncorrectObserver(dots, buzzer) { + Timber.i("Handle incorrect: entered = ${dots.spelling}") + Toast.makeText( + context, getString(R.string.msg_incorrect), Toast.LENGTH_SHORT + ).show() + updateTitle(title) + } + ) + + viewModel.eventHint.observe( + viewLifecycleOwner, + viewModel.getEventHintObserver(dots, serial) { expectedDots -> + Timber.i("Handle hint") + val toast = getString(R.string.practice_hint_template) + .format(expectedDots.spelling) + Toast.makeText(context, toast, Toast.LENGTH_LONG).show() + } + ) + + viewModel.eventPassHint.observe( + viewLifecycleOwner, + viewModel.getEventPassHintObserver(dots) { + Timber.i("Handle pass hint") + } + ) + + }.root +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/practice/PracticeViewModel.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/practice/PracticeViewModel.kt new file mode 100644 index 00000000..0ea744d3 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/screens/practice/PracticeViewModel.kt @@ -0,0 +1,82 @@ +package com.github.braillesystems.learnbraille.screens.practice + +import android.app.Application +import androidx.lifecycle.* +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.database.entities.SymbolDao +import com.github.braillesystems.learnbraille.language +import com.github.braillesystems.learnbraille.screens.DotsChecker +import com.github.braillesystems.learnbraille.screens.MutableDotsChecker +import com.github.braillesystems.learnbraille.util.scope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import timber.log.Timber + +class PracticeViewModelFactory( + private val dataSource: SymbolDao, + private val application: Application, + private val getEnteredDots: () -> BrailleDots +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T = + if (modelClass.isAssignableFrom(PracticeViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + PracticeViewModel(dataSource, application, getEnteredDots) as T + } else { + throw IllegalArgumentException("Unknown ViewModel class") + } +} + +class PracticeViewModel( + private val database: SymbolDao, + application: Application, + private val getEnteredDots: () -> BrailleDots, + private val dotsChecker: MutableDotsChecker = MutableDotsChecker.create() +) : AndroidViewModel(application), + DotsChecker by dotsChecker { + + private var _symbol = MutableLiveData() + val symbol: LiveData + get() = _symbol + + var nTries: Int = 0 + private set + + var nCorrect: Int = 0 + private set + + private var expectedDots: BrailleDots? = null + + private val job = Job() + private val uiScope = scope(job) + + init { + Timber.i("Initialize practice view model") + initializeCard() + + dotsChecker.apply { + getEnteredDots = this@PracticeViewModel.getEnteredDots + getExpectedDots = { expectedDots } + onCheckHandler = { + if (dotsChecker.state == DotsChecker.State.INPUT) { + nTries++ + } + } + onCorrectHandler = { + initializeCard() + nCorrect++ + } + } + } + + override fun onCleared() { + super.onCleared() + job.cancel() + } + + private fun initializeCard() = uiScope.launch { + val entry = database.getRandomSymbol(language) ?: error("DB is not initialized") + _symbol.value = entry.symbol.toString() + expectedDots = entry.brailleDots + } +} diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/serial/UsbSerial.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/serial/UsbSerial.kt new file mode 100644 index 00000000..6892d774 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/serial/UsbSerial.kt @@ -0,0 +1,131 @@ +package com.github.braillesystems.learnbraille.serial + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbDeviceConnection +import android.hardware.usb.UsbManager +import com.felhr.usbserial.UsbSerialDevice +import com.felhr.usbserial.UsbSerialInterface +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import timber.log.Timber + + +/**********************************/ +/********** TODO refactor *********/ +/**********************************/ + + +class UsbSerial(usbManager: UsbManager, context: Context) { + private var mUsbManager: UsbManager = usbManager + private var invokingContext: Context = context + var mDevice: UsbDevice? = null + var mSerial: UsbSerialDevice? = null + var mConnection: UsbDeviceConnection? = null + private var birthTime: Long = System.currentTimeMillis() + + companion object { + const val arduinoNanoVendorId = 6790 + const val ACTION_USB_PERMISSION = "permission" + const val USB_SERVICE = "usb" // TODO why error: USB_SERVICE = Context.USB_SERVICE + } + + init { + startUsbConnection() + } + + fun trySend(brailleDots: BrailleDots) { + val setupTimeMillis = 5_000 + val data = brailleDots.toString().replace("F", "1").replace("E", "0") + if (System.currentTimeMillis() - birthTime > setupTimeMillis) { + sendData(data) + } + } + + + private fun startUsbConnection() { + val usbDevices: HashMap? = mUsbManager.deviceList + if (!usbDevices?.isEmpty()!!) { + var keep = true + usbDevices.forEach { entry -> + mDevice = entry.value + val deviceVendorId: Int? = mDevice?.vendorId + if (deviceVendorId == arduinoNanoVendorId) { + val intent: PendingIntent = + PendingIntent.getBroadcast( + invokingContext, + 0, + Intent(ACTION_USB_PERMISSION), + 0 + ) + mUsbManager.requestPermission(mDevice, intent) + keep = false + debugLog("connection successful") + } else { + mConnection = null + mDevice = null + debugLog("unable to connect " + deviceVendorId.toString()) + } + } + if (!keep) { + return + } + } else { + debugLog("no usb connected") + } + } + + private fun sendData(input: String) { + mSerial?.write(input.toByteArray()) + debugLog("sending data: $input") + } + + private fun disconnectUsb() { + mSerial?.close() + } + + private fun debugLog(msg: String) { + Timber.i(msg) + } + + val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + UsbManager.ACTION_USB_ACCESSORY_ATTACHED -> { + startUsbConnection() + return + } + UsbManager.ACTION_USB_ACCESSORY_DETACHED -> { + disconnectUsb() + return + } + ACTION_USB_PERMISSION -> { + } + else -> return + } + val granted: Boolean = + intent.extras!!.getBoolean(UsbManager.EXTRA_PERMISSION_GRANTED) + if (!granted) { + debugLog("permission is not granted") + return + } + mConnection = mUsbManager.openDevice(mDevice) + mSerial = UsbSerialDevice.createUsbSerialDevice(mDevice, mConnection) + if (mSerial == null) { + debugLog("port is none") + return + } + if (!mSerial!!.open()) { + debugLog("port not open") + return + } + mSerial!!.setBaudRate(9600) + mSerial!!.setDataBits(UsbSerialInterface.DATA_BITS_8) + mSerial!!.setStopBits(UsbSerialInterface.STOP_BITS_1) + mSerial!!.setParity(UsbSerialInterface.PARITY_NONE) + mSerial!!.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/util/ScreensUtil.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/util/ScreensUtil.kt new file mode 100644 index 00000000..b3198275 --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/util/ScreensUtil.kt @@ -0,0 +1,33 @@ +package com.github.braillesystems.learnbraille.util + +import android.os.Vibrator +import android.text.Html +import android.text.Spanned +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.github.braillesystems.learnbraille.BuzzPattern +import com.github.braillesystems.learnbraille.LearnBrailleApplication +import timber.log.Timber + +fun Fragment.updateTitle(title: String) { + (activity as AppCompatActivity) + .supportActionBar + ?.title = title +} + +fun Fragment.getStringArg(name: String): String = + arguments?.getString(name) ?: error("No $name found in args") + +@Suppress("DEPRECATION") // Provides low API level compatibility +fun Fragment.getFormattedArg(name: String): Spanned = Html.fromHtml(getStringArg(name)) + +fun Vibrator?.buzz(pattern: BuzzPattern): Unit = + if (this == null) Timber.i("Vibrator is not available") + else { + // Use deprecated API to be compatible with old android API levels + @Suppress("DEPRECATION") + vibrate(pattern, -1) + } + +val Fragment.application: LearnBrailleApplication + get() = requireNotNull(activity).application as LearnBrailleApplication diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/util/Util.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/util/Util.kt new file mode 100644 index 00000000..eed3188a --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/util/Util.kt @@ -0,0 +1,25 @@ +package com.github.braillesystems.learnbraille.util + +import android.app.Application +import android.content.Context.USB_SERVICE +import android.hardware.usb.UsbManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job + +fun T?.side(block: (T) -> R) { + if (this == null) return + block(this) +} + +val Application.usbManager get() = getSystemService(USB_SERVICE) as UsbManager + +operator fun MatchGroupCollection.component1() = get(0) +operator fun MatchGroupCollection.component2() = get(1) +operator fun MatchGroupCollection.component3() = get(2) +operator fun MatchGroupCollection.component4() = get(3) +operator fun MatchGroupCollection.component5() = get(4) + +val Any?.devnull: Unit get() {} + +fun scope(job: Job = Job()) = CoroutineScope(Dispatchers.Main + job) diff --git a/android/app/src/main/java/com/github/braillesystems/learnbraille/views/BrailleDotsView.kt b/android/app/src/main/java/com/github/braillesystems/learnbraille/views/BrailleDotsView.kt new file mode 100644 index 00000000..c669314c --- /dev/null +++ b/android/app/src/main/java/com/github/braillesystems/learnbraille/views/BrailleDotsView.kt @@ -0,0 +1,66 @@ +package com.github.braillesystems.learnbraille.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.CheckBox +import androidx.constraintlayout.widget.ConstraintLayout +import com.github.braillesystems.learnbraille.R +import com.github.braillesystems.learnbraille.database.entities.BrailleDot +import com.github.braillesystems.learnbraille.database.entities.BrailleDots +import com.github.braillesystems.learnbraille.database.entities.list +import com.github.braillesystems.learnbraille.database.entities.spelling +import kotlinx.android.synthetic.main.braille_dots.view.* + +/** + * Represents six Braille dots view. + */ +class BrailleDotsView : ConstraintLayout { + + constructor(context: Context) : super(context) + + constructor(context: Context, attrSet: AttributeSet) : super(context, attrSet) + + constructor( + context: Context, attrSet: AttributeSet, defStyleAttr: Int + ) : super( + context, attrSet, defStyleAttr + ) + + init { + LayoutInflater + .from(context) + .inflate(R.layout.braille_dots, this, true) + } +} + +class BrailleDotsState(val checkBoxes: Array) + +val BrailleDotsView.dots: BrailleDotsState + get() = BrailleDotsState( + arrayOf( + dotButton1, dotButton2, dotButton3, + dotButton4, dotButton5, dotButton6 + ) + ) + +fun BrailleDotsState.uncheck() = checkBoxes.forEach { + it.isChecked = false +} + +fun BrailleDotsState.clickable(isClickable: Boolean) = checkBoxes.forEach { + it.isClickable = isClickable +} + +val BrailleDotsState.spelling: String + get() = brailleDots.spelling + +val BrailleDotsState.brailleDots: BrailleDots + get() = BrailleDots( + checkBoxes.map(CheckBox::isChecked).toBooleanArray() + ) + +fun BrailleDotsState.display(brailleDots: BrailleDots): Unit = + (checkBoxes zip brailleDots.list).forEach { (checkBox, dot) -> + checkBox.isChecked = dot == BrailleDot.F + }.also { clickable(false) } diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/LearnBrailleApplication.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/LearnBrailleApplication.kt deleted file mode 100644 index 0b520964..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/LearnBrailleApplication.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.spbstu.amd.learnbraille - -import android.app.Application -import timber.log.Timber - -class LearnBrailleApplication : Application() { - - override fun onCreate() { - super.onCreate() - Timber.plant(Timber.DebugTree()) - } -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Executors.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Executors.kt deleted file mode 100644 index 198c37a6..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Executors.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.spbstu.amd.learnbraille.database - -import java.util.concurrent.Executors - -private val IO_EXECUTOR = Executors.newSingleThreadExecutor() - -/** - * Utility method to run blocks on a dedicated background thread, used for io/database work. - */ -fun ioThread(f: () -> Unit) { - IO_EXECUTOR.execute(f) -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Lessons.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Lessons.kt deleted file mode 100644 index 1b30de2f..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Lessons.kt +++ /dev/null @@ -1,22 +0,0 @@ -package ru.spbstu.amd.learnbraille.database - -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.Insert -import androidx.room.PrimaryKey - -@Entity(tableName = "lesson") -data class Lesson( - - @PrimaryKey(autoGenerate = false) - val id: Long, - - val name: String -) - -@Dao -interface LessonDao { - - @Insert - fun insertLessons(lessons: List) -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Steps.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Steps.kt deleted file mode 100644 index 9099fa67..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Steps.kt +++ /dev/null @@ -1,63 +0,0 @@ -package ru.spbstu.amd.learnbraille.database - -import androidx.room.* - -@Entity(tableName = "step") -data class Step( - - @PrimaryKey(autoGenerate = true) - var id: Long = 0, - - val title: String, - - @ColumnInfo(name = "lesson_id") - val lessonId: Long, - - val data: StepData -) - -data class LessonWithStep( - - @Embedded(prefix = "lesson_embedding_") - val lesson: Lesson, - - @Embedded - val step: Step -) - -@Dao -interface StepDao { - - @Insert - fun insertSteps(steps: List) - - @Query( - """ - SELECT step.*, - lesson.id AS 'lesson_embedding_id', - lesson.name AS 'lesson_embedding_name' - FROM step - INNER JOIN lesson on lesson_id = lesson.id - WHERE NOT EXISTS ( - SELECT * - FROM user_passed_step AS ups - WHERE ups.user_id = :userId AND ups.step_id = step.id - ) - ORDER BY step.id ASC - LIMIT 1 - """ - ) - fun getCurrentStepForUser(userId: Long): LessonWithStep? - - @Query( - """ - SELECT step.*, - lesson.id AS 'lesson_embedding_id', - lesson.name AS 'lesson_embedding_name' - FROM step - INNER JOIN lesson on lesson_id = lesson.id - WHERE step.id = :id - """ - ) - fun getStep(id: Long): LessonWithStep? -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Symbols.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Symbols.kt deleted file mode 100644 index ab8aff30..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Symbols.kt +++ /dev/null @@ -1,71 +0,0 @@ -package ru.spbstu.amd.learnbraille.database - -import androidx.room.* - -@Entity(tableName = "symbol") -data class Symbol( - - @PrimaryKey(autoGenerate = true) - var id: Long = 0, - - val symbol: Char, - - val language: Language = Language.NONE, - - @ColumnInfo(name = "braille_dots") - val brailleDots: BrailleDots -) { - companion object { - val pattern = Regex( - """Symbol\(id=(\d+), symbol=(.), language=(\w{2,4}), brailleDots=([E|F]{6})\)""" - ) - } -} - -fun symbolOf(data: String) = Symbol.pattern - .matchEntire(data) - ?.groups?.let { (_, id, symbol, language, brailleDots) -> - Symbol( - id = id?.value?.toLong() ?: error("No id here $data"), - symbol = symbol?.value?.first() ?: error("No symbol here $data"), - language = Language.valueOf( - language?.value ?: error("No language here $data") - ), - brailleDots = BrailleDots( - brailleDots?.value ?: error("No braille dots here $data") - ) - ) -} ?: error("$data does not match Symbol structure") - -@Dao -interface SymbolDao { - - @Insert - fun insertSymbols(symbols: List) - - @Query( - """ - SELECT * - FROM symbol - WHERE language = :language - ORDER BY RANDOM() - LIMIT 1 - """ - ) - fun getRandomSymbol(language: Language): Symbol? - - @Query("SELECT * FROM symbol WHERE symbol = :char LIMIT 1") - fun getSymbol(char: Char): Symbol? - - @Query( - """ - SELECT * - FROM symbol - WHERE language = :language AND braille_dots = :brailleDots - LIMIT 1 - """ - ) - fun getSymbol(language: Language, brailleDots: BrailleDots): Symbol? -} - - diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/UserPassedStep.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/UserPassedStep.kt deleted file mode 100644 index 0e1c1d9c..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/UserPassedStep.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ru.spbstu.amd.learnbraille.database - -import androidx.room.* - -@Entity(tableName = "user_passed_step") -data class UserPassedStep( - - @PrimaryKey(autoGenerate = false) - @ColumnInfo(name = "user_id") - val userId: Long, - - @ColumnInfo(name = "step_id") - val stepId: Long -) - -@Dao -interface UserPassedStepDao { - - @Insert - fun insertPassedStep(passedStep: UserPassedStep) -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Util.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Util.kt deleted file mode 100644 index 707aa260..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/database/Util.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.spbstu.amd.learnbraille.database - -operator fun MatchGroupCollection.component1() = get(0) -operator fun MatchGroupCollection.component2() = get(1) -operator fun MatchGroupCollection.component3() = get(2) -operator fun MatchGroupCollection.component4() = get(3) -operator fun MatchGroupCollection.component5() = get(4) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/Lessons.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/Lessons.kt deleted file mode 100644 index e4e3daf0..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/Lessons.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.spbstu.amd.learnbraille.res.russian - -import ru.spbstu.amd.learnbraille.database.Lesson - -// TODO fill lessons -val PREPOPULATE_LESSONS = listOf( - Lesson(id = 1, name = "Знакомство с шеститочием"), - Lesson(id = 2, name = "Русские буквы А, Б, Ц") -) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/AllSteps.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/AllSteps.kt deleted file mode 100644 index f076e685..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/AllSteps.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ru.spbstu.amd.learnbraille.res.russian.steps - -/** - * Add via plus steps of all lessons. - * - * Keep VERY_LAST member last. - */ -val PREPOPULATE_STEPS = LESSON_1_STEPS + LESSON_2_STEPS + VERY_LAST diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/Lesson1.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/Lesson1.kt deleted file mode 100644 index 50a8fd0b..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/Lesson1.kt +++ /dev/null @@ -1,81 +0,0 @@ -package ru.spbstu.amd.learnbraille.res.russian.steps - -import ru.spbstu.amd.learnbraille.database.* -import ru.spbstu.amd.learnbraille.database.BrailleDot.F -import ru.spbstu.amd.learnbraille.res.russian.PREPOPULATE_LESSONS -import ru.spbstu.amd.learnbraille.res.russian.symbols.symbolMap - -/** - * Automatically inserts proper lessonID. - * - * Cannot be replaced with factory returning lambda for each step because named parameters are - * not supported for functional types in kotlin 1.3. - */ -private fun Step(title: String, data: StepData) = - Step( - title = title, - lessonId = 1L, - data = data - ) - -// TODO fill steps -/** - * List of steps for first lesson. - * - * Do not create symbols manually, always look them up in `symbolMap`. - * - * First two steps are used for database testing. - */ -val LESSON_1_STEPS = listOf( - - // TODO intro steps in all lessons - Step( - title = "Урок первый", - data = Info(PREPOPULATE_LESSONS[0].name) - ), - - Step( - title = "Знакомство с шеститочием", - data = Info( - """В рельефной азбуке Брайля любой символ - это шеститочие. - |Каждая точка из шести может быть выдавлена или пропущена. - |В следующем шаге все 6 точек выведены на экран.""" - ) - ), - - Step( - title = "Шеститочие", - data = ShowDots(BrailleDots(F, F, F, F, F, F)) - ), - - Step( - title = "Введите все шесть точек", - data = InputDots(BrailleDots(F, F, F, F, F, F)) - ), - - Step( - title = "Работа с букварём", - data = Info( - """Откройте букварь на странице 12. - |В верхней строке 14 раз повторён символ полного шеститочия.""" - ) - ), - - Step( - title = "Работа с букварём", - data = Info( - """Точки расположены в два столбца по три. - |Точки в первом столбце имеют номера 1, 2, 3 сверху вниз. - |Точки во втором столбце - 4, 5, 6 сверху вниз. - |Важно выучить, где какая точка.""" - ) - ), - - // TODO replace - Step( - title = "Пример с символом", - data = ShowSymbol( - symbolMap['А'] ?: error("A russian not found") - ) - ) -) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/Lesson2.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/Lesson2.kt deleted file mode 100644 index c899d5dd..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/Lesson2.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.spbstu.amd.learnbraille.res.russian.steps - -import ru.spbstu.amd.learnbraille.database.Step -import ru.spbstu.amd.learnbraille.database.StepData - -private fun Step(title: String, data: StepData) = - Step( - title = title, - lessonId = 2L, - data = data - ) - -val LESSON_2_STEPS = listOf() diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/LessonLast.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/LessonLast.kt deleted file mode 100644 index c9078dd0..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/steps/LessonLast.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.spbstu.amd.learnbraille.res.russian.steps - -import ru.spbstu.amd.learnbraille.database.LastInfo -import ru.spbstu.amd.learnbraille.database.Step -import ru.spbstu.amd.learnbraille.res.russian.PREPOPULATE_LESSONS - -val VERY_LAST = listOf( - Step( - title = "Последний шаг", - lessonId = PREPOPULATE_LESSONS.last().id, - data = LastInfo("""Это самый последний шаг! Теперь вы владеете азбукой Брайля!""") - ) -) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/NonLetters.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/NonLetters.kt deleted file mode 100644 index aace3614..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/NonLetters.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.spbstu.amd.learnbraille.res.russian.symbols - -import ru.spbstu.amd.learnbraille.database.Symbol - -// TODO fill punctuation -val PUNCTUATION = listOf() - -// TODO fill digits -val DIGITS = listOf() diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/RuLetters.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/RuLetters.kt deleted file mode 100644 index 0015cf08..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/res/russian/symbols/RuLetters.kt +++ /dev/null @@ -1,43 +0,0 @@ -package ru.spbstu.amd.learnbraille.res.russian.symbols - -import ru.spbstu.amd.learnbraille.database.BrailleDot.E -import ru.spbstu.amd.learnbraille.database.BrailleDot.F -import ru.spbstu.amd.learnbraille.database.BrailleDots -import ru.spbstu.amd.learnbraille.database.Language.RU -import ru.spbstu.amd.learnbraille.database.Symbol - -val RU_LETTERS = listOf( - Symbol(symbol = 'А', language = RU, brailleDots = BrailleDots(F, E, E, E, E, E)), - Symbol(symbol = 'Б', language = RU, brailleDots = BrailleDots(F, F, E, E, E, E)), - Symbol(symbol = 'В', language = RU, brailleDots = BrailleDots(E, F, E, F, F, F)), - Symbol(symbol = 'Г', language = RU, brailleDots = BrailleDots(F, F, E, F, F, E)), - Symbol(symbol = 'Д', language = RU, brailleDots = BrailleDots(F, E, E, F, F, E)), - Symbol(symbol = 'Е', language = RU, brailleDots = BrailleDots(F, E, E, E, F, E)), - Symbol(symbol = 'Ё', language = RU, brailleDots = BrailleDots(F, E, E, E, E, F)), - Symbol(symbol = 'Ж', language = RU, brailleDots = BrailleDots(E, F, E, F, F, E)), - Symbol(symbol = 'З', language = RU, brailleDots = BrailleDots(F, E, F, E, F, F)), - Symbol(symbol = 'И', language = RU, brailleDots = BrailleDots(E, F, E, F, E, E)), - Symbol(symbol = 'Й', language = RU, brailleDots = BrailleDots(F, F, F, F, E, F)), - Symbol(symbol = 'К', language = RU, brailleDots = BrailleDots(F, E, F, E, E, E)), - Symbol(symbol = 'Л', language = RU, brailleDots = BrailleDots(F, F, F, E, E, E)), - Symbol(symbol = 'М', language = RU, brailleDots = BrailleDots(F, E, F, F, E, E)), - Symbol(symbol = 'Н', language = RU, brailleDots = BrailleDots(F, E, F, F, F, E)), - Symbol(symbol = 'О', language = RU, brailleDots = BrailleDots(F, E, F, E, F, E)), - Symbol(symbol = 'П', language = RU, brailleDots = BrailleDots(F, F, F, F, E, E)), - Symbol(symbol = 'Р', language = RU, brailleDots = BrailleDots(F, F, F, E, F, E)), - Symbol(symbol = 'С', language = RU, brailleDots = BrailleDots(E, F, F, F, E, E)), - Symbol(symbol = 'Т', language = RU, brailleDots = BrailleDots(E, F, F, F, F, E)), - Symbol(symbol = 'У', language = RU, brailleDots = BrailleDots(F, E, F, E, E, F)), - Symbol(symbol = 'Ф', language = RU, brailleDots = BrailleDots(F, F, E, F, E, E)), - Symbol(symbol = 'Х', language = RU, brailleDots = BrailleDots(F, F, E, E, F, E)), - Symbol(symbol = 'Ц', language = RU, brailleDots = BrailleDots(F, E, E, F, E, E)), - Symbol(symbol = 'Ч', language = RU, brailleDots = BrailleDots(F, F, F, F, F, E)), - Symbol(symbol = 'Ш', language = RU, brailleDots = BrailleDots(F, E, E, E, F, F)), - Symbol(symbol = 'Щ', language = RU, brailleDots = BrailleDots(F, E, F, F, E, F)), - Symbol(symbol = 'Ъ', language = RU, brailleDots = BrailleDots(F, F, F, E, F, F)), - Symbol(symbol = 'Ы', language = RU, brailleDots = BrailleDots(E, F, F, F, E, F)), - Symbol(symbol = 'Ь', language = RU, brailleDots = BrailleDots(E, F, F, F, F, F)), - Symbol(symbol = 'Э', language = RU, brailleDots = BrailleDots(E, F, E, F, E, F)), - Symbol(symbol = 'Ю', language = RU, brailleDots = BrailleDots(F, F, E, E, F, F)), - Symbol(symbol = 'Я', language = RU, brailleDots = BrailleDots(F, F, E, F, E, F)) -) diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/Util.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/Util.kt deleted file mode 100644 index 0215ee62..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/Util.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.spbstu.amd.learnbraille.screens - -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment - -fun Fragment.updateTitle(title: String) { - (activity as AppCompatActivity) - .supportActionBar - ?.title = title -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/lessons/LessonStepFragment.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/lessons/LessonStepFragment.kt deleted file mode 100644 index b4c863a2..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/lessons/LessonStepFragment.kt +++ /dev/null @@ -1,92 +0,0 @@ -package ru.spbstu.amd.learnbraille.screens.lessons - -import android.os.Bundle -import android.view.* -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController -import ru.spbstu.amd.learnbraille.R -import ru.spbstu.amd.learnbraille.database.* -import ru.spbstu.amd.learnbraille.databinding.FragmentLessonStepBinding -import ru.spbstu.amd.learnbraille.screens.updateTitle - -class LessonStepFragment : Fragment() { - - private lateinit var viewModel: LessonStepViewModel - - // TODO replace - private val userId = 1L - - private val title: String - get() = when (viewModel.currentLessonStep.value!!.step.data) { - is Info -> getString(R.string.lessons_title_info) - is LastInfo -> getString(R.string.lessons_title_last_info) - is InputSymbol -> getString(R.string.lessons_title_input_symbol) - is InputDots -> getString(R.string.lessons_title_input_dots) - is ShowSymbol -> getString(R.string.lessons_title_show_symbol) - is ShowDots -> getString(R.string.lessons_title_show_dots) - } - - private val helpMessage: String - get() = getString(R.string.lessons_help_template).format( - getString(R.string.lessons_help_common), - when (viewModel.currentLessonStep.value!!.step.data) { - is Info -> getString(R.string.lessons_help_info) - is LastInfo -> getString(R.string.lessons_help_last_info) - is InputSymbol -> getString(R.string.lessons_help_input_symbol) - is InputDots -> getString(R.string.lessons_help_input_dots) - is ShowSymbol -> getString(R.string.lessons_help_show_symbol) - is ShowDots -> getString(R.string.lessons_help_show_dots) - } - ) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DataBindingUtil.inflate( - inflater, - R.layout.fragment_lesson_step, - container, - false - ).apply { - - val application = requireNotNull(activity).application - val database = LearnBrailleDatabase.getInstance(application) - val viewModelFactory = LessonStepViewModelFactory( - application, userId, database.stepDao, database.userPassedStepDao - ) { - null - } - viewModel = ViewModelProvider( - this@LessonStepFragment, viewModelFactory - ).get(LessonStepViewModel::class.java) - - lessonStepViewModel = viewModel - lifecycleOwner = this@LessonStepFragment - - viewModel.currentLessonStep.observe(this@LessonStepFragment, Observer { - updateTitle(title) - }) - - setHasOptionsMenu(true) - - }.root - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.help_menu, menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item).also { - when (item.itemId) { - R.id.help -> { - val action = LessonStepFragmentDirections.actionLessonStepFragmentToHelpFragment() - action.helpMessage = helpMessage - findNavController().navigate(action) - } - } - } -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/lessons/LessonStepViewModel.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/lessons/LessonStepViewModel.kt deleted file mode 100644 index 295446e6..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/lessons/LessonStepViewModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -package ru.spbstu.amd.learnbraille.screens.lessons - -import android.app.Application -import androidx.lifecycle.* -import kotlinx.coroutines.* -import ru.spbstu.amd.learnbraille.database.* -import timber.log.Timber - -class LessonStepViewModelFactory( - private val application: Application, - private val userId: Long, - private val stepDao: StepDao, - private val userPassedStepDao: UserPassedStepDao, - private val dotsStateGetter: () -> BrailleDotsState? -) : ViewModelProvider.Factory { - - override fun create(modelClass: Class): T = - if (modelClass.isAssignableFrom(LessonStepViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - LessonStepViewModel( - application, userId, - stepDao, userPassedStepDao, - dotsStateGetter - ) as T - } else { - throw IllegalArgumentException("Unknown ViewModel class") - } -} - -class LessonStepViewModel( - application: Application, - private val userId: Long, - private val stepDao: StepDao, - private val userPassedStepDao: UserPassedStepDao, - private val dotsStateGetter: () -> BrailleDotsState? -) : AndroidViewModel(application) { - - private val _backingCurrentLessonStep = MutableLiveData() - private var _currentLessonStep: LessonWithStep - get() = _backingCurrentLessonStep.value ?: error("Current step should be initialized") - set(value) { - _backingCurrentLessonStep.value = value - } - val currentLessonStep: LiveData - get() = _backingCurrentLessonStep - - private val _eventCorrect = MutableLiveData() - val eventCorrect: LiveData - get() = _eventCorrect - - private val _eventIncorrect = MutableLiveData() - val eventIncorrect: LiveData - get() = _eventIncorrect - - private var stepsPassedUntil = 0L - - private val job = Job() - private val uiScope = CoroutineScope(Dispatchers.Main + job) - - init { - initializeCurrentStep() - } - - private fun initializeCurrentStep() = uiScope.launch { - _currentLessonStep = getCurrentStepFromDatabase(userId) - ?: error("User should always have at least one not passed step") - stepsPassedUntil = _currentLessonStep.step.id - } - - private suspend fun getCurrentStepFromDatabase(userId: Long) = withContext(Dispatchers.IO) { - stepDao.getCurrentStepForUser(userId) - } - - fun prevStep(): Unit = setPrevStep() - - fun nextStep(): Unit = if (_currentLessonStep.step.id < stepsPassedUntil) { - setNextStep() - } else { - when (val data = _currentLessonStep.step.data) { - is LastInfo -> Timber.w("No next step: last step reached, remove next button from ui") - is InputSymbol -> checkInput(data.symbol.brailleDots) - is InputDots -> checkInput(data.dots) - else -> { - markPassed() - setNextStep() - } - } - } - - private fun checkInput(input: BrailleDots) = dotsStateGetter() - ?.brailleDots - ?.equals(input) - ?.let { if (it) onCorrect() else onIncorrect() } - ?: error("Dots state is needed to check input correctness") - - private fun onCorrect() { - markPassed() - _eventCorrect.value = true - } - - private fun onIncorrect() { - _eventIncorrect.value = true - } - - fun onCorrectComplete() { - _eventCorrect.value = false - } - - fun onIncorrectComplete() { - _eventIncorrect.value = false - } - - private fun markPassed() = uiScope.launch { - insertPassedStepToDatabase() - stepsPassedUntil = _currentLessonStep.step.id - } - - private suspend fun insertPassedStepToDatabase() = withContext(Dispatchers.IO) { - userPassedStepDao.insertPassedStep( - UserPassedStep(userId, _currentLessonStep.step.id) - ) - } - - private fun setStep(id: Long) = uiScope.launch { - _currentLessonStep = getStepFromDatabase(id) ?: return@launch - } - - private suspend fun getStepFromDatabase(id: Long) = withContext(Dispatchers.IO) { - stepDao.getStep(id) - } - - private fun setNextStep() { - setStep(_currentLessonStep.step.id + 1) - } - - private fun setPrevStep() { - setStep(_currentLessonStep.step.id - 1) - } -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/menu/MenuFragment.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/menu/MenuFragment.kt deleted file mode 100644 index fb4da073..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/menu/MenuFragment.kt +++ /dev/null @@ -1,93 +0,0 @@ -package ru.spbstu.amd.learnbraille.screens.menu - -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.* -import android.widget.Toast -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import ru.spbstu.amd.learnbraille.R -import ru.spbstu.amd.learnbraille.databinding.FragmentMenuBinding -import ru.spbstu.amd.learnbraille.screens.updateTitle - - -class MenuFragment : Fragment() { - - companion object { - const val qtResultCode = 0 - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DataBindingUtil.inflate( - inflater, - R.layout.fragment_menu, - container, - false - ).apply { - - updateTitle(getString(R.string.menu_actionbar_text)) - - setHasOptionsMenu(true) - - lessonsButton.setOnClickListener( - Navigation.createNavigateOnClickListener(R.id.action_menuFragment_to_lessonFragment) - ) - - practiceButton.setOnClickListener( - Navigation.createNavigateOnClickListener(R.id.action_menuFragment_to_practiceFragment) - ) - - offlinePracticeButton.setOnClickListener { - try { - val intent = Intent("com.google.zxing.client.android.SCAN") - intent.putExtra("SCAN_MODE", "QR_CODE_MODE") - startActivityForResult(intent, 0) - } catch (e: Exception) { - val marketUri = Uri.parse("market://details?id=com.google.zxing.client.android") - val marketIntent = Intent(Intent.ACTION_VIEW, marketUri) - startActivity(marketIntent) - } - } - - exitButton.setOnClickListener( - Navigation.createNavigateOnClickListener(R.id.action_menuFragment_to_exitFragment) - ) - - }.root - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == qtResultCode) { - if (resultCode == RESULT_OK) { - val contents = data?.getStringExtra("SCAN_RESULT") - Toast.makeText(context, contents, Toast.LENGTH_SHORT).show() - } - if (resultCode == RESULT_CANCELED) { - Toast.makeText(context, "Try again", Toast.LENGTH_SHORT).show() - } - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.help_menu, menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item).also { - when (item.itemId) { - R.id.help -> { - val action = MenuFragmentDirections.actionMenuFragmentToHelpFragment() - action.helpMessage = getString(R.string.menu_help) - findNavController().navigate(action) - } - } - } -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/practice/PracticeFragment.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/practice/PracticeFragment.kt deleted file mode 100644 index da1c0dad..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/practice/PracticeFragment.kt +++ /dev/null @@ -1,153 +0,0 @@ -package ru.spbstu.amd.learnbraille.screens.practice - -import android.app.Application -import android.os.Bundle -import android.os.Vibrator -import android.view.* -import android.widget.CheckBox -import android.widget.Toast -import androidx.core.content.getSystemService -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController -import kotlinx.android.synthetic.main.braille_dots.view.* -import ru.spbstu.amd.learnbraille.R -import ru.spbstu.amd.learnbraille.database.BrailleDotsState -import ru.spbstu.amd.learnbraille.database.LearnBrailleDatabase -import ru.spbstu.amd.learnbraille.databinding.FragmentPracticeBinding -import ru.spbstu.amd.learnbraille.screens.updateTitle -import timber.log.Timber - -class PracticeFragment : Fragment() { - - private lateinit var viewModel: PracticeViewModel - private lateinit var viewModelFactory: PracticeViewModelFactory - private var buzzer: Vibrator? = null - - private val title: String - get() = getString(R.string.practice_actionbar_title).format( - viewModel.nCorrect.value, - viewModel.nLettersFaced.value - ) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DataBindingUtil.inflate( - inflater, - R.layout.fragment_practice, - container, - false - ).apply { - - val application: Application = requireNotNull(activity).application - val dataSource = LearnBrailleDatabase.getInstance(application).symbolDao - val dotCheckBoxes = practiceButtons.run { - arrayOf( - dotButton1, dotButton2, dotButton3, - dotButton4, dotButton5, dotButton6 - ) - } - - viewModelFactory = PracticeViewModelFactory( - dataSource, application, BrailleDotsState(dotCheckBoxes) - ) - viewModel = ViewModelProvider( - this@PracticeFragment, viewModelFactory - ).get(PracticeViewModel::class.java) - - buzzer = activity?.getSystemService() - - - practiceViewModel = viewModel - lifecycleOwner = this@PracticeFragment - - - viewModel.eventCorrect.observe(this@PracticeFragment, Observer { - if (!it) { - return@Observer - } - - Toast.makeText(context, "Correct!", Toast.LENGTH_SHORT).show() - Timber.i("Handle correct") - - // Use deprecated API to be compatible with old android API levels - @Suppress("DEPRECATION") - buzzer?.vibrate(CORRECT_BUZZ_PATTERN, -1) - - makeUnchecked(dotCheckBoxes) - viewModel.onCorrectComplete() - }) - - viewModel.eventIncorrect.observe(this@PracticeFragment, Observer { - if (!it) { - return@Observer - } - - Toast.makeText(context, "Incorrect!", Toast.LENGTH_SHORT).show() - Timber.i("Handle incorrect") - - // Use deprecated API to be compatible with old android API levels - @Suppress("DEPRECATION") - buzzer?.vibrate(INCORRECT_BUZZ_PATTERN, -1) - - makeUnchecked(dotCheckBoxes) - viewModel.onIncorrectComplete() - }) - - - viewModel.nCorrect.observe(this@PracticeFragment, Observer { - updateTitle(title) - }) - - viewModel.nLettersFaced.observe(this@PracticeFragment, Observer { - updateTitle(title) - }) - - setHasOptionsMenu(true) - - - viewModel.eventWaitDBInit.observe(this@PracticeFragment, Observer { - if (!it) { - return@Observer - } - Toast.makeText( - context, - getString(R.string.practice_db_not_initialized_warning), - Toast.LENGTH_LONG - ).show() - findNavController().navigate(R.id.action_practiceFragment_to_menuFragment) - }) - - }.root - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.help_menu, menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item).also { - when (item.itemId) { - R.id.help -> { - val action = PracticeFragmentDirections.actionPracticeFragmentToHelpFragment() - action.helpMessage = getString(R.string.practice_help) - findNavController().navigate(action) - } - } - } - - private fun makeUnchecked(checkBoxes: Array) = checkBoxes - .forEach { - if (it.isChecked) { - it.toggle() - } - } - - companion object { - val CORRECT_BUZZ_PATTERN = longArrayOf(100, 100, 100, 100, 100, 100) - val INCORRECT_BUZZ_PATTERN = longArrayOf(0, 200) - } -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/practice/PracticeViewModel.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/practice/PracticeViewModel.kt deleted file mode 100644 index 624ce9ea..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/screens/practice/PracticeViewModel.kt +++ /dev/null @@ -1,142 +0,0 @@ -package ru.spbstu.amd.learnbraille.screens.practice - -import android.app.Application -import androidx.lifecycle.* -import kotlinx.coroutines.* -import ru.spbstu.amd.learnbraille.database.BrailleDots -import ru.spbstu.amd.learnbraille.database.BrailleDotsState -import ru.spbstu.amd.learnbraille.database.Language -import ru.spbstu.amd.learnbraille.database.SymbolDao -import timber.log.Timber - -class PracticeViewModelFactory( - private val dataSource: SymbolDao, - private val application: Application, - private val dotsState: BrailleDotsState -) : ViewModelProvider.Factory { - - override fun create(modelClass: Class): T = - if (modelClass.isAssignableFrom(PracticeViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - PracticeViewModel(dataSource, application, dotsState) as T - } else { - throw IllegalArgumentException("Unknown ViewModel class") - } -} - -class PracticeViewModel( - private val database: SymbolDao, - application: Application, - private val dotsState: BrailleDotsState -) : AndroidViewModel(application) { - - private val _backingSymbol = MutableLiveData() // True backing field - private var _symbol: Char // This class interface to the true backing filed - get() = _backingSymbol.value!!.first() - set(value) { - _backingSymbol.value = value.toString() - } - val symbol: LiveData - get() = _backingSymbol - - private val _backingNLettersFaced = MutableLiveData() - private var _nLettersFaced: Int - get() = _backingNLettersFaced.value ?: error("nLettersFaced should be initialized") - set(value) { - _backingNLettersFaced.value = value - } - val nLettersFaced: LiveData - get() = _backingNLettersFaced - - private val _backingNCorrect = MutableLiveData() - private var _nCorrect: Int - get() = _backingNCorrect.value ?: error("nCorrect should be initialized") - set(value) { - _backingNCorrect.value = value - } - val nCorrect: LiveData - get() = _backingNCorrect - - private val _eventCorrect = MutableLiveData() - val eventCorrect: LiveData - get() = _eventCorrect - - private val _eventIncorrect = MutableLiveData() - val eventIncorrect: LiveData - get() = _eventIncorrect - - private val _eventWaitDBInit = MutableLiveData() - val eventWaitDBInit: LiveData - get() = _eventWaitDBInit - - private var expectedDots: BrailleDots? = null - private val enteredDots get() = dotsState.brailleDots - - private val isCorrect: Boolean - get() = (enteredDots == expectedDots).also { - Timber.i( - if (it) "Correct: " else "Incorrect: " + - "entered = $enteredDots, expected = $expectedDots" - ) - } - - private val language = Language.RU // Temporary field, will move to settings - - private val job = Job() - private val uiScope = CoroutineScope(Dispatchers.Main + job) - - init { - initializeCard() - _nLettersFaced = 0 - _nCorrect = 0 - } - - override fun onCleared() { - super.onCleared() - job.cancel() - } - - fun onNext() { - _nLettersFaced++ - if (isCorrect) { - onCorrect() - } else { - onIncorrect() - } - } - - fun onCorrectComplete() { - _eventCorrect.value = false - } - - fun onIncorrectComplete() { - _eventIncorrect.value = false - } - - fun onEventWaitDBInitComplete() { - _eventWaitDBInit.value = false - } - - private fun onCorrect() = initializeCard().also { - _nCorrect++ - _eventCorrect.value = true - } - - private fun onIncorrect() { - _eventIncorrect.value = true - } - - private fun onWaitDBInit() { - _eventWaitDBInit.value = true - } - - private fun initializeCard() = uiScope.launch { - val entry = getEntryFromDatabase(language) ?: onWaitDBInit().run { return@launch } - _symbol = entry.symbol - expectedDots = entry.brailleDots - } - - private suspend fun getEntryFromDatabase(language: Language) = withContext(Dispatchers.IO) { - database.getRandomSymbol(language) - } -} diff --git a/android/app/src/main/java/ru/spbstu/amd/learnbraille/views/ButtonsView.kt b/android/app/src/main/java/ru/spbstu/amd/learnbraille/views/ButtonsView.kt deleted file mode 100644 index 597f2be4..00000000 --- a/android/app/src/main/java/ru/spbstu/amd/learnbraille/views/ButtonsView.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ru.spbstu.amd.learnbraille.views - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import androidx.constraintlayout.widget.ConstraintLayout -import ru.spbstu.amd.learnbraille.R - -class ButtonsView : ConstraintLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrSet: AttributeSet) : super(context, attrSet) - - constructor( - context: Context, attrSet: AttributeSet, defStyleAttr: Int - ) : super( - context, attrSet, defStyleAttr - ) - - init { - LayoutInflater - .from(context) - .inflate(R.layout.braille_dots, this, true) - } -} diff --git a/android/app/src/main/res/drawable/action_menu_help_button.xml b/android/app/src/main/res/drawable/action_menu_help_button.xml new file mode 100644 index 00000000..2a326d16 --- /dev/null +++ b/android/app/src/main/res/drawable/action_menu_help_button.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/checked_round_checkbox.xml b/android/app/src/main/res/drawable/checked_round_checkbox.xml index f018736f..281e0730 100644 --- a/android/app/src/main/res/drawable/checked_round_checkbox.xml +++ b/android/app/src/main/res/drawable/checked_round_checkbox.xml @@ -1,7 +1,7 @@ - + diff --git a/android/app/src/main/res/drawable/navigate_left.xml b/android/app/src/main/res/drawable/navigate_left.xml new file mode 100644 index 00000000..91131f16 --- /dev/null +++ b/android/app/src/main/res/drawable/navigate_left.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/navigate_right.xml b/android/app/src/main/res/drawable/navigate_right.xml new file mode 100644 index 00000000..a477b2d6 --- /dev/null +++ b/android/app/src/main/res/drawable/navigate_right.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/practice_btn_shape.xml b/android/app/src/main/res/drawable/practice_btn_shape.xml new file mode 100644 index 00000000..8c1d5797 --- /dev/null +++ b/android/app/src/main/res/drawable/practice_btn_shape.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/practice_hint_btn_shape.xml b/android/app/src/main/res/drawable/practice_hint_btn_shape.xml new file mode 100644 index 00000000..e2257ac0 --- /dev/null +++ b/android/app/src/main/res/drawable/practice_hint_btn_shape.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/tmp_help_button.xml b/android/app/src/main/res/drawable/tmp_help_button.xml deleted file mode 100644 index 784b4f89..00000000 --- a/android/app/src/main/res/drawable/tmp_help_button.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/drawable/to_progress.xml b/android/app/src/main/res/drawable/to_progress.xml new file mode 100644 index 00000000..fd4c2672 --- /dev/null +++ b/android/app/src/main/res/drawable/to_progress.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/unchecked_round_checkbox.xml b/android/app/src/main/res/drawable/unchecked_round_checkbox.xml index 26d8acdb..5909773d 100644 --- a/android/app/src/main/res/drawable/unchecked_round_checkbox.xml +++ b/android/app/src/main/res/drawable/unchecked_round_checkbox.xml @@ -3,7 +3,7 @@ android:shape="oval"> + android:color="#263238" /> diff --git a/android/app/src/main/res/layout/fragment_exit.xml b/android/app/src/main/res/layout/fragment_exit.xml index b8c50891..693849d3 100644 --- a/android/app/src/main/res/layout/fragment_exit.xml +++ b/android/app/src/main/res/layout/fragment_exit.xml @@ -1,5 +1,6 @@ - + -