From ded54b4d2b314a13c8ef20ebe37edafe3bb1cf90 Mon Sep 17 00:00:00 2001 From: mark Date: Fri, 8 May 2026 06:38:30 +0100 Subject: [PATCH] Fix: handle unsupported locale in JokeSkill without NPE When the user changes language, there is a race condition where JokeSkill.generateOutput() can be called with a locale not in JOKE_SUPPORTED_LOCALES. This happened because the SkillRanker still holds the old JokeSkill instance while ctx.locale already reflects the new locale. The !! on resolveSupportedLocale() caused a NullPointerException. Replace with a safe return of JokeOutput.Failed. Fixes the crash reported in DicioTeam/dicio-android#412 where changing language on first install causes an NPE in JokeSkill. - Add JokeOutput.Failed variant with user-friendly message - Add regression test proving the NPE scenario --- .../stypox/dicio/skills/joke/JokeOutput.kt | 10 +++ .../org/stypox/dicio/skills/joke/JokeSkill.kt | 5 +- app/src/main/res/values/strings.xml | 1 + .../dicio/skills/joke/JokeSkillNpeTest.kt | 63 +++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 app/src/test/kotlin/org/stypox/dicio/skills/joke/JokeSkillNpeTest.kt diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeOutput.kt b/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeOutput.kt index 21d0bec94..5a0747cab 100644 --- a/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeOutput.kt +++ b/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeOutput.kt @@ -38,4 +38,14 @@ sealed interface JokeOutput : SkillOutput { val ENDS_WITH_PUNCTUATION_REGEX = ".*\\p{Punct}$".toRegex() } } + + class Failed : JokeOutput { + override fun getSpeechOutput(ctx: SkillContext): String = + ctx.getString(R.string.skill_joke_failed) + + @Composable + override fun GraphicalOutput(ctx: SkillContext) { + Body(text = getSpeechOutput(ctx)) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeSkill.kt b/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeSkill.kt index 9675244b6..7453278ac 100644 --- a/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeSkill.kt +++ b/app/src/main/kotlin/org/stypox/dicio/skills/joke/JokeSkill.kt @@ -13,9 +13,8 @@ import org.stypox.dicio.util.LocaleUtils class JokeSkill(correspondingSkillInfo: SkillInfo, data: StandardRecognizerData) : StandardRecognizerSkill(correspondingSkillInfo, data) { override suspend fun generateOutput(ctx: SkillContext, inputData: Joke): SkillOutput { - // we can use !! because the JokeInfo would have declared this skill unavailable - // if the current locale was not among the supported ones - val locale = LocaleUtils.resolveSupportedLocale(ctx.locale, JOKE_SUPPORTED_LOCALES)!! + val locale = LocaleUtils.resolveSupportedLocale(ctx.locale, JOKE_SUPPORTED_LOCALES) + ?: return JokeOutput.Failed() if (locale == "en") { val joke: JSONObject = ConnectionUtils.getPageJson(RANDOM_JOKE_URL_EN) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea8db6663..c05b0c82a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,6 +142,7 @@ Next song Joke Tell me a joke + Sorry, jokes are not available for this language Calculator What is five times four minus a million? Navigation diff --git a/app/src/test/kotlin/org/stypox/dicio/skills/joke/JokeSkillNpeTest.kt b/app/src/test/kotlin/org/stypox/dicio/skills/joke/JokeSkillNpeTest.kt new file mode 100644 index 000000000..ee8fe6400 --- /dev/null +++ b/app/src/test/kotlin/org/stypox/dicio/skills/joke/JokeSkillNpeTest.kt @@ -0,0 +1,63 @@ +package org.stypox.dicio.skills.joke + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.runBlocking +import org.dicio.skill.context.SkillContext +import org.dicio.skill.context.SpeechOutputDevice +import org.dicio.skill.skill.SkillOutput +import org.dicio.numbers.ParserFormatter +import org.dicio.skill.standard.util.MatchHelper +import org.stypox.dicio.sentences.Sentences +import java.util.Locale +import android.content.Context + +/** + * Regression test for https://github.com/DicioTeam/dicio-android/pull/412 + * + * When the user changes language, there is a race condition where JokeSkill.generateOutput() + * can be called with a locale not in JOKE_SUPPORTED_LOCALES. Previously this caused a NPE + * due to !! on resolveSupportedLocale(). The fix returns JokeOutput.Failed instead. + */ +class JokeSkillNpeTest : StringSpec({ + + "generateOutput returns Failed when ctx locale is not in JOKE_SUPPORTED_LOCALES" { + val data = Sentences.Joke["en"]!! + val skill = JokeSkill(JokeInfo, data) + + val ctx = object : SkillContext { + override val locale: Locale = Locale.ITALIAN + override val android: Context get() = throw NotImplementedError() + override val sentencesLanguage: String = "it" + override val parserFormatter: ParserFormatter? = null + override val speechOutputDevice: SpeechOutputDevice get() = throw NotImplementedError() + override val previousOutput: SkillOutput? = null + override val standardMatchHelper: MatchHelper? = null + } + + val result = runBlocking { + skill.generateOutput(ctx, Sentences.Joke.Command) + } + result.shouldBeInstanceOf() + } + + "generateOutput returns Failed when locale is Turkish" { + val data = Sentences.Joke["en"]!! + val skill = JokeSkill(JokeInfo, data) + + val ctx = object : SkillContext { + override val locale: Locale = Locale("tr") + override val android: Context get() = throw NotImplementedError() + override val sentencesLanguage: String = "tr" + override val parserFormatter: ParserFormatter? = null + override val speechOutputDevice: SpeechOutputDevice get() = throw NotImplementedError() + override val previousOutput: SkillOutput? = null + override val standardMatchHelper: MatchHelper? = null + } + + val result = runBlocking { + skill.generateOutput(ctx, Sentences.Joke.Command) + } + result.shouldBeInstanceOf() + } +})