diff --git a/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt b/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt index b06019d9da4..bb42f0ba1a3 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt @@ -17,7 +17,7 @@ import org.oppia.android.app.recyclerview.OnItemDragListener import org.oppia.android.app.shim.ViewBindingShim import org.oppia.android.app.view.ViewComponentFactory import org.oppia.android.app.view.ViewComponentImpl -import org.oppia.android.util.accessibility.AccessibilityChecker +import org.oppia.android.util.accessibility.AccessibilityService import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.parser.html.HtmlParser @@ -40,7 +40,7 @@ class DragDropSortInteractionView @JvmOverloads constructor( lateinit var htmlParserFactory: HtmlParser.Factory @Inject - lateinit var accessibilityChecker: AccessibilityChecker + lateinit var accessibilityService: AccessibilityService @Inject @field:ExplorationHtmlParserEntityType @@ -64,7 +64,7 @@ class DragDropSortInteractionView @JvmOverloads constructor( val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl viewComponent.inject(this) - isAccessibilityEnabled = accessibilityChecker.isScreenReaderEnabled() + isAccessibilityEnabled = accessibilityService.isScreenReaderEnabled() } fun allowMultipleItemsInSamePosition(isAllowed: Boolean) { diff --git a/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt b/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt index db93ff3f0f3..24d845c32c3 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt @@ -14,7 +14,7 @@ import org.oppia.android.app.utility.ClickableAreasImage import org.oppia.android.app.utility.OnClickableAreaClickedListener import org.oppia.android.app.view.ViewComponentFactory import org.oppia.android.app.view.ViewComponentImpl -import org.oppia.android.util.accessibility.AccessibilityChecker +import org.oppia.android.util.accessibility.AccessibilityService import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.locale.OppiaLocale import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType @@ -43,7 +43,7 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( private lateinit var listener: OnClickableAreaClickedListener @Inject - lateinit var accessibilityChecker: AccessibilityChecker + lateinit var accessibilityService: AccessibilityService @Inject lateinit var imageLoader: ImageLoader @@ -141,7 +141,7 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl viewComponent.inject(this) - isAccessibilityEnabled = accessibilityChecker.isScreenReaderEnabled() + isAccessibilityEnabled = accessibilityService.isScreenReaderEnabled() } fun setOnRegionClicked(onRegionClicked: OnClickableAreaClickedListener) { diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index 61bd4999f07..4af453b01d8 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -368,7 +368,7 @@ class StateFragmentPresenter @Inject constructor( moveToNextState() } else { if (result.labelledAsCorrectAnswer) { - recyclerViewAssembler.showCelebrationOnCorrectAnswer() + recyclerViewAssembler.showCelebrationOnCorrectAnswer(result.feedback) } else { viewModel.setCanSubmitAnswer(canSubmitAnswer = false) } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 5d0ec6d654d..8375b12f475 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -18,6 +18,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import kotlinx.coroutines.CoroutineDispatcher import nl.dionsegijn.konfetti.KonfettiView +import org.oppia.android.R import org.oppia.android.app.model.AnswerAndResponse import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.EphemeralState.StateTypeCase @@ -87,6 +88,7 @@ import org.oppia.android.databinding.SubmittedAnswerListItemBinding import org.oppia.android.databinding.SubmittedHtmlAnswerItemBinding import org.oppia.android.databinding.TextInputInteractionItemBinding import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.accessibility.AccessibilityService import org.oppia.android.util.parser.html.HtmlParser import org.oppia.android.util.threading.BackgroundDispatcher import javax.inject.Inject @@ -119,6 +121,7 @@ private const val CONGRATULATIONS_TEXT_VIEW_VISIBLE_MILLIS: Long = 800 * - [ReturnToTopicNavigationButtonListener] if the return to topic button is enabled */ class StatePlayerRecyclerViewAssembler private constructor( + private val accessibilityService: AccessibilityService, val adapter: BindableAdapter, val rhsAdapter: BindableAdapter, private val playerFeatureSet: PlayerFeatureSet, @@ -457,8 +460,11 @@ class StatePlayerRecyclerViewAssembler private constructor( /** * Shows a celebratory animation with a congratulations message and confetti when the learner submits * a correct answer. + * + * @param feedback Oppia's feedback to the learner's most recent answer. If the feedback is empty, + * talkback confirms correct answers by a separate announcement. */ - fun showCelebrationOnCorrectAnswer() { + fun showCelebrationOnCorrectAnswer(feedback: SubtitledHtml) { check(playerFeatureSet.showCelebrationOnCorrectAnswer) { "Cannot show congratulations message for assembler that doesn't support it" } @@ -471,9 +477,15 @@ class StatePlayerRecyclerViewAssembler private constructor( val confettiConfig = checkNotNull(congratulationsTextConfettiConfig) { "Expected non-null reference to confetti animation configuration" } - createBannerConfetti(confettiView, confettiConfig) animateCongratulationsTextView(textView) + + if (feedback.html.isBlank()) { + accessibilityService.announceForAccessibilityForView( + textView, + resourceHandler.getStringInLocale(R.string.correct) + ) + } } /** Shows confetti when the learner reaches the end of an exploration session. */ @@ -864,6 +876,7 @@ class StatePlayerRecyclerViewAssembler private constructor( * using its injectable [Factory]. */ class Builder private constructor( + private var accessibilityService: AccessibilityService, private val htmlParserFactory: HtmlParser.Factory, private val resourceBucketName: String, private val entityType: String, @@ -1317,6 +1330,7 @@ class StatePlayerRecyclerViewAssembler private constructor( fun build(): StatePlayerRecyclerViewAssembler { val playerFeatureSet = featureSets.reduce(PlayerFeatureSet::union) val assembler = StatePlayerRecyclerViewAssembler( + accessibilityService, /* adapter= */ adapterBuilder.build(), /* rhsAdapter= */ adapterBuilder.build(), playerFeatureSet, @@ -1347,6 +1361,7 @@ class StatePlayerRecyclerViewAssembler private constructor( /** Fragment injectable factory to create new [Builder]s. */ class Factory @Inject constructor( + private val accessibilityService: AccessibilityService, private val htmlParserFactory: HtmlParser.Factory, private val fragment: Fragment, private val context: Context, @@ -1362,6 +1377,7 @@ class StatePlayerRecyclerViewAssembler private constructor( */ fun create(resourceBucketName: String, entityType: String, profileId: ProfileId): Builder { return Builder( + accessibilityService, htmlParserFactory, resourceBucketName, entityType, diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt index 81aaf6894ab..a6db374baf8 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt @@ -269,7 +269,7 @@ class QuestionPlayerFragmentPresenter @Inject constructor( recyclerViewAssembler.isCorrectAnswer.set(result.isCorrectAnswer) if (result.isCorrectAnswer) { questionViewModel.setHintBulbVisibility(false) - recyclerViewAssembler.showCelebrationOnCorrectAnswer() + recyclerViewAssembler.showCelebrationOnCorrectAnswer(result.feedback) } else { questionViewModel.setCanSubmitAnswer(canSubmitAnswer = false) } diff --git a/app/src/test/java/org/oppia/android/app/accessibility/FakeAccessibilityServiceTest.kt b/app/src/test/java/org/oppia/android/app/accessibility/FakeAccessibilityServiceTest.kt new file mode 100644 index 00000000000..e77ded760d9 --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/accessibility/FakeAccessibilityServiceTest.kt @@ -0,0 +1,96 @@ +package org.oppia.android.app.accessibility + +import android.app.Application +import android.view.View +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.accessibility.FakeAccessibilityService +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [FakeAccessibilityService]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = FakeAccessibilityServiceTest.TestApplication::class) +class FakeAccessibilityServiceTest { + @Inject + lateinit var accessibilityService: FakeAccessibilityService + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testFakeAccessibilityService_initialState_isScreenReaderEnabled_isFalse() { + assertThat(accessibilityService.isScreenReaderEnabled()).isFalse() + } + + @Test + fun testFakeAccessibilityService_initialState_getLatestAnnouncement_isNull() { + assertThat(accessibilityService.getLatestAnnouncement()).isNull() + } + + @Test + fun testFakeAccessibilityService_setScreenReaderEnabledTrue_isTrue() { + accessibilityService.setScreenReaderEnabled(true) + assertThat(accessibilityService.isScreenReaderEnabled()).isTrue() + } + + @Test + fun testFakeAccessibilityService_setScreenReaderEnabledFalse_isFalse() { + accessibilityService.setScreenReaderEnabled(false) + assertThat(accessibilityService.isScreenReaderEnabled()).isFalse() + } + + @Test + fun testFakeAccessibilityService_announceForAccessibilityForView_latestAnnouncementIsSet() { + accessibilityService.announceForAccessibilityForView(mock(View::class.java), "test") + assertThat(accessibilityService.getLatestAnnouncement()).isEqualTo("test") + } + + @Test + fun testFakeAccessibilityService_resetLatestAnnouncement_latestAnnouncementIsNull() { + accessibilityService.resetLatestAnnouncement() + assertThat(accessibilityService.getLatestAnnouncement()).isNull() + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + @Singleton + @Component(modules = [AccessibilityTestModule::class]) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + fun build(): TestApplicationComponent + } + + fun inject(fakeAccessibilityServiceTest: FakeAccessibilityServiceTest) + } + + class TestApplication : Application() { + private val component: TestApplicationComponent by lazy { + DaggerFakeAccessibilityServiceTest_TestApplicationComponent.builder() + .setApplication(ApplicationProvider.getApplicationContext()) + .build() + } + + fun inject(fakeAccessibilityServiceTest: FakeAccessibilityServiceTest) { + component.inject(fakeAccessibilityServiceTest) + } + } +} diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index dc0ff28e020..a21685af4d4 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -131,6 +131,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.accessibility.FakeAccessibilityService import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadImagesFromAssets @@ -191,6 +192,9 @@ class StateFragmentLocalTest { @Inject lateinit var editTextInputAction: EditTextInputAction + @Inject + lateinit var accessibilityManager: FakeAccessibilityService + @Inject lateinit var translationController: TranslationController @@ -216,7 +220,6 @@ class StateFragmentLocalTest { .setAnimationExecutor(executorService) .setSourceExecutor(executorService) ) - profileTestHelper.initializeProfiles() ShadowMediaPlayer.addException(audioDataSource1, IOException("Test does not have networking")) } @@ -352,6 +355,58 @@ class StateFragmentLocalTest { } } + @Test + @Config(qualifiers = "+port") + fun testStateFragment_portrait_submitCorrectAnswerWithFeedback_correctIsNotAnnounced() { + launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { + startPlayingExploration() + playThroughFractionsState1() + playThroughFractionsState2() + accessibilityManager.resetLatestAnnouncement() + playThroughFractionsState3() + + assertThat(accessibilityManager.getLatestAnnouncement()).isNull() + } + } + + @Test + @Config(qualifiers = "+land") + fun testStateFragment_landscape_submitCorrectAnswerWithFeedback_correctIsNotAnnounced() { + launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { + startPlayingExploration() + playThroughFractionsState1() + playThroughFractionsState2() + accessibilityManager.resetLatestAnnouncement() + playThroughFractionsState3() + + assertThat(accessibilityManager.getLatestAnnouncement()).isNull() + } + } + + @Test + @Config(qualifiers = "+port") + fun testStateFragment_portrait_submitCorrectAnswer_correctIsAnnounced() { + launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { + startPlayingExploration() + playThroughFractionsState1() + playThroughFractionsState2() + + assertThat(accessibilityManager.getLatestAnnouncement()).isEqualTo("Correct!") + } + } + + @Test + @Config(qualifiers = "+land") + fun testStateFragment_landscape_submitCorrectAnswer_correctIsAnnounced() { + launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { + startPlayingExploration() + playThroughFractionsState1() + playThroughFractionsState2() + + assertThat(accessibilityManager.getLatestAnnouncement()).isEqualTo("Correct!") + } + } + @Test @Config(qualifiers = "+port") fun testStateFragment_portrait_submitCorrectAnswer_confettiIsActive() { diff --git a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt index b198682e10c..7cc6dcad16b 100644 --- a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt @@ -69,7 +69,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule -import org.oppia.android.util.accessibility.FakeAccessibilityChecker +import org.oppia.android.util.accessibility.FakeAccessibilityService import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule import org.oppia.android.util.gcsresource.GcsResourceModule @@ -105,7 +105,7 @@ class StateFragmentAccessibilityTest { lateinit var context: Context @Inject - lateinit var fakeAccessibilityManager: FakeAccessibilityChecker + lateinit var fakeAccessibilityManager: FakeAccessibilityService private val internalProfileId: Int = 1 diff --git a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt index 71f96f4660d..bb2e6d4cdff 100644 --- a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt @@ -17,6 +17,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import dagger.Component import dagger.Module import dagger.Provides @@ -87,6 +88,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.accessibility.FakeAccessibilityService import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule import org.oppia.android.util.gcsresource.GcsResourceModule @@ -117,6 +119,9 @@ class QuestionPlayerActivityLocalTest { @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @Inject + lateinit var accessibilityManager: FakeAccessibilityService + @Inject lateinit var profileTestHelper: ProfileTestHelper @@ -166,6 +171,64 @@ class QuestionPlayerActivityLocalTest { } } + @Test + @Config(qualifiers = "+port") + fun testQuestionPlayer_portrait_submitCorrectAnswerWithFeedback_correctIsNotAnnounced() { + launchForQuestionPlayer(SKILL_ID_LIST).use { + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.question_recycler_view)).check(matches(isDisplayed())) + + submitCorrectAnswerToQuestionPlayerFractionInput() + clickContinueNavigationButton() + accessibilityManager.resetLatestAnnouncement() + submitCorrectAnswerToQuestion2PlayerFractionInput() + + assertThat(accessibilityManager.getLatestAnnouncement()).isNull() + } + } + + @Test + @Config(qualifiers = "+land") + fun testQuestionPlayer_landscape_submitCorrectAnswerWithFeedback_correctIsNotAnnounced() { + launchForQuestionPlayer(SKILL_ID_LIST).use { + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.question_recycler_view)).check(matches(isDisplayed())) + + submitCorrectAnswerToQuestionPlayerFractionInput() + clickContinueNavigationButton() + accessibilityManager.resetLatestAnnouncement() + submitCorrectAnswerToQuestion2PlayerFractionInput() + + assertThat(accessibilityManager.getLatestAnnouncement()).isNull() + } + } + + @Test + @Config(qualifiers = "port") + fun testQuestionPlayer_portrait_submitCorrectAnswer_correctIsAnnounced() { + launchForQuestionPlayer(SKILL_ID_LIST).use { + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.question_recycler_view)).check(matches(isDisplayed())) + + submitCorrectAnswerToQuestionPlayerFractionInput() + + assertThat(accessibilityManager.getLatestAnnouncement()).isEqualTo("Correct!") + } + } + + @Test + @Config(qualifiers = "land") + fun testQuestionPlayer_landscape_submitCorrectAnswer_correctIsAnnounced() { + launchForQuestionPlayer(SKILL_ID_LIST).use { + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.question_recycler_view)).check(matches(isDisplayed())) + + submitCorrectAnswerToQuestionPlayerFractionInput() + + assertThat(accessibilityManager.getLatestAnnouncement()).isEqualTo("Correct!") + } + } + @Test @Config(qualifiers = "port") fun testQuestionPlayer_portrait_submitCorrectAnswer_confettiIsActive() { @@ -299,6 +362,29 @@ class QuestionPlayerActivityLocalTest { testCoroutineDispatchers.runCurrent() } + private fun submitCorrectAnswerToQuestion2PlayerFractionInput() { + onView(withId(R.id.question_recycler_view)) + .perform(scrollToViewType(StateItemViewModel.ViewType.TEXT_INPUT_INTERACTION)) + onView(withId(R.id.text_input_interaction_view)).perform( + editTextInputAction.appendText("1/4"), + closeSoftKeyboard() + ) + testCoroutineDispatchers.runCurrent() + + onView(withId(R.id.question_recycler_view)) + .perform(scrollToViewType(StateItemViewModel.ViewType.SUBMIT_ANSWER_BUTTON)) + onView(withId(R.id.submit_answer_button)).perform(click()) + testCoroutineDispatchers.runCurrent() + } + + private fun clickContinueNavigationButton() { + onView(withId(R.id.question_recycler_view)) + .perform(scrollToViewType(StateItemViewModel.ViewType.CONTINUE_NAVIGATION_BUTTON)) + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.continue_navigation_button)).perform(click()) + testCoroutineDispatchers.runCurrent() + } + private fun submitTwoWrongAnswersToQuestionPlayer() { submitWrongAnswerToQuestionPlayerFractionInput() submitWrongAnswerToQuestionPlayerFractionInput() diff --git a/domain/src/main/assets/MjZzEVOG47_1.json b/domain/src/main/assets/MjZzEVOG47_1.json index 6c0bb50e580..01f230ea14b 100644 --- a/domain/src/main/assets/MjZzEVOG47_1.json +++ b/domain/src/main/assets/MjZzEVOG47_1.json @@ -2694,7 +2694,7 @@ "dest": "Matthew gets conned", "feedback": { "content_id": "feedback_1", - "html": "

Yes, that's right -- congratulations, you figured out Crumb's trick!

Let's see what happens to Matthew, though...

" + "html": "" }, "labelled_as_correct": true, "param_changes": [], diff --git a/domain/src/main/assets/MjZzEVOG47_1.textproto b/domain/src/main/assets/MjZzEVOG47_1.textproto index b7c510c8db6..35e6e01f034 100644 --- a/domain/src/main/assets/MjZzEVOG47_1.textproto +++ b/domain/src/main/assets/MjZzEVOG47_1.textproto @@ -2955,7 +2955,7 @@ states { outcome { dest_state_name: "Question 1" feedback { - html: "

Yes, that\'s right! Well done.

" + html: "" content_id: "feedback_1" } labelled_as_correct: true diff --git a/domain/src/main/assets/questions.json b/domain/src/main/assets/questions.json index a1b0e374f2b..fcb17c4b94b 100644 --- a/domain/src/main/assets/questions.json +++ b/domain/src/main/assets/questions.json @@ -2015,7 +2015,7 @@ "dest": "", "feedback": { "content_id": "feedback_1", - "html": "

That's correct!

" + "html": "" }, "labelled_as_correct": true, "param_changes": [], diff --git a/domain/src/main/assets/questions.textproto b/domain/src/main/assets/questions.textproto index 5f23c2ce74d..a7edf7fa1a3 100644 --- a/domain/src/main/assets/questions.textproto +++ b/domain/src/main/assets/questions.textproto @@ -2722,7 +2722,7 @@ questions { answer_groups { outcome { feedback { - html: "

That\'s correct!

" + html: "" content_id: "feedback_1" } labelled_as_correct: true diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 17f2abb6059..f3adfd60ef4 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -3,6 +3,24 @@ file_content_checks { prohibited_content_regex: "^import .+?support.+?$" failure_message: "AndroidX should be used instead of the support library" } +file_content_checks { + file_path_regex: ".+?.kt" + prohibited_content_regex: "announceForAccessibility\\(" + failure_message: "Please use AccessibilityService instead." + exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityServiceImpl.kt" +} +file_content_checks { + file_path_regex: ".+?.kt" + prohibited_content_regex: "announceForAccessibilityForView\\(" + failure_message: "When using announceForAccessibility, please add an exempt file in file_content_validation_checks.textproto." + exempted_file_name: "app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt" + exempted_file_name: "app/src/test/java/org/oppia/android/app/accessibility/FakeAccessibilityServiceTest.kt" + exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityService.kt" + exempted_file_name: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityServiceImpl.kt" + exempted_file_name: "utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityService.kt" +} file_content_checks { file_path_regex: ".+?.kt" prohibited_content_regex: "CoroutineWorker" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 8233c66c0d6..71cac4aa32c 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -674,11 +674,10 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/threading/T exempted_file_path: "testing/src/main/java/org/oppia/android/testing/time/FakeOppiaClockModule.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/time/FakeSystemClock.kt" exempted_file_path: "testing/src/test/java/org/oppia/android/testing/threading/TestCoroutineDispatcherTestBase.kt" -exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityChecker.kt" -exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityCheckerImpl.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityService.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityServiceImpl.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityProdModule.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityTestModule.kt" -exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityChecker.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/AssetRepository.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/AssetRepositoryImpl.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/CacheAssetsLocally.kt" diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 3f576389c91..52fac0b873a 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -26,6 +26,11 @@ class RegexPatternValidationCheckTest { "AndroidX should be used instead of the support library" private val coroutineWorkerUsageErrorMessage = "For stable tests, prefer using ListenableWorker with an Oppia-managed dispatcher." + private val announceForAccessibilityUsageErrorMessage = + "Please use AccessibilityService instead." + private val announceForAccessibilityForViewUsageErrorMessage = + "When using announceForAccessibility, please add an exempt file in " + + "file_content_validation_checks.textproto." private val settableFutureUsageErrorMessage = "SettableFuture should only be used in pre-approved locations since it's easy to potentially " + "mess up & lead to a hanging ListenableFuture." @@ -277,6 +282,46 @@ class RegexPatternValidationCheckTest { ) } + @Test + fun testFileContent_announceForAccessibilityUsageErrorMessage_fileContentIsNotCorrect() { + val prohibitedContent = "announceForAccessibility(" + val fileContainsSupportLibraryImport = tempFolder.newFile("testfiles/TestFile.kt") + fileContainsSupportLibraryImport.writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + TestFile.kt:1: $announceForAccessibilityUsageErrorMessage + $wikiReferenceNote + """.trimIndent() + ) + } + + @Test + fun testFileContent_announceForAccessibilityForViewUsageErrorMessage_fileContentIsNotCorrect() { + val prohibitedContent = "announceForAccessibilityForView(" + val fileContainsSupportLibraryImport = tempFolder.newFile("testfiles/TestFile.kt") + fileContainsSupportLibraryImport.writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + TestFile.kt:1: $announceForAccessibilityForViewUsageErrorMessage + $wikiReferenceNote + """.trimIndent() + ) + } + @Test fun testFileContent_settableFuture_fileContentIsNotCorrect() { val prohibitedContent = "SettableFuture.create()" diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityChecker.kt b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityChecker.kt deleted file mode 100644 index 414865db699..00000000000 --- a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityChecker.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.oppia.android.util.accessibility - -/** Utility for determining various properties of the system's accessibility manager(s). */ -interface AccessibilityChecker { - /** Returns whether a screen reader (such as TalkBack) is currently enabled. */ - fun isScreenReaderEnabled(): Boolean -} diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityProdModule.kt b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityProdModule.kt index cb6d3adf349..d626e9e5948 100644 --- a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityProdModule.kt +++ b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityProdModule.kt @@ -7,5 +7,5 @@ import dagger.Module @Module interface AccessibilityProdModule { @Binds - fun provideProductionAccessibilityChecker(impl: AccessibilityCheckerImpl): AccessibilityChecker + fun provideProductionAccessibilityService(impl: AccessibilityServiceImpl): AccessibilityService } diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityService.kt b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityService.kt new file mode 100644 index 00000000000..ccde150eecd --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityService.kt @@ -0,0 +1,16 @@ +package org.oppia.android.util.accessibility + +import android.view.View + +/** Utility for determining various properties of the system's accessibility manager(s). */ +interface AccessibilityService { + /** Returns whether a screen reader (such as TalkBack) is currently enabled. */ + fun isScreenReaderEnabled(): Boolean + + /** + * Suggests to the screen reader (if any is installed/enabled) to announce the specified text for + * the given view. This does not guarantee the text will actually be read, and it may interrupt + * existing text being spoken. + */ + fun announceForAccessibilityForView(view: View, text: CharSequence) +} diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityCheckerImpl.kt b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityServiceImpl.kt similarity index 60% rename from utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityCheckerImpl.kt rename to utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityServiceImpl.kt index a1bd104b47e..892c3e3f154 100644 --- a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityCheckerImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityServiceImpl.kt @@ -1,13 +1,14 @@ package org.oppia.android.util.accessibility import android.content.Context +import android.view.View import android.view.accessibility.AccessibilityManager import javax.inject.Inject -/** Implementation of [AccessibilityChecker]. */ -class AccessibilityCheckerImpl @Inject constructor( +/** Implementation of [AccessibilityService]. */ +class AccessibilityServiceImpl @Inject constructor( private val context: Context -) : AccessibilityChecker { +) : AccessibilityService { private val accessibilityManager by lazy { context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager } @@ -15,4 +16,8 @@ class AccessibilityCheckerImpl @Inject constructor( override fun isScreenReaderEnabled(): Boolean { return accessibilityManager.isEnabled } + + override fun announceForAccessibilityForView(view: View, text: CharSequence) { + view.announceForAccessibility(text) + } } diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityTestModule.kt b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityTestModule.kt index 118253b7847..1e184fe24af 100644 --- a/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityTestModule.kt +++ b/utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityTestModule.kt @@ -7,5 +7,5 @@ import dagger.Module @Module interface AccessibilityTestModule { @Binds - fun provideFakeAccessibilityChecker(impl: FakeAccessibilityChecker): AccessibilityChecker + fun provideFakeAccessibilityService(impl: FakeAccessibilityService): AccessibilityService } diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/accessibility/BUILD.bazel index 2036d03d9eb..334c946e4a9 100644 --- a/utility/src/main/java/org/oppia/android/util/accessibility/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/accessibility/BUILD.bazel @@ -8,7 +8,7 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") kt_android_library( name = "accessibility", srcs = [ - "AccessibilityChecker.kt", + "AccessibilityService.kt", ], visibility = ["//:oppia_api_visibility"], ) @@ -16,7 +16,7 @@ kt_android_library( kt_android_library( name = "impl", srcs = [ - "AccessibilityCheckerImpl.kt", + "AccessibilityServiceImpl.kt", ], deps = [ ":accessibility", @@ -28,7 +28,7 @@ kt_android_library( name = "testing", testonly = True, srcs = [ - "FakeAccessibilityChecker.kt", + "FakeAccessibilityService.kt", ], visibility = ["//:oppia_testing_visibility"], deps = [ diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityChecker.kt b/utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityChecker.kt deleted file mode 100644 index 91a469111e3..00000000000 --- a/utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityChecker.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.oppia.android.util.accessibility - -import javax.inject.Inject -import javax.inject.Singleton - -/** Fake implementation of [AccessibilityChecker] which should be used in tests. */ -@Singleton -class FakeAccessibilityChecker @Inject constructor() : AccessibilityChecker { - private var isScreenReaderEnabled = false - - override fun isScreenReaderEnabled(): Boolean = isScreenReaderEnabled - - /** - * Sets whether a screen reader should be considered currently enabled. This will change the - * return value of [isScreenReaderEnabled]. - */ - fun setScreenReaderEnabled(isEnabled: Boolean) { - isScreenReaderEnabled = isEnabled - } -} diff --git a/utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityService.kt b/utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityService.kt new file mode 100644 index 00000000000..4aa422c586d --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityService.kt @@ -0,0 +1,37 @@ +package org.oppia.android.util.accessibility + +import android.view.View +import javax.inject.Inject +import javax.inject.Singleton + +/** Fake implementation of [AccessibilityService] which should be used in tests. */ +@Singleton +class FakeAccessibilityService @Inject constructor() : AccessibilityService { + private var isScreenReaderEnabled = false + private var announcement: CharSequence? = null + + override fun isScreenReaderEnabled(): Boolean = isScreenReaderEnabled + + /** + * Returns latest announcement. Note that the announcement gets overwritten each time + * announceForAccessibilityForView is called. + */ + fun getLatestAnnouncement(): CharSequence? = announcement + + /** Resets latest announcement. */ + fun resetLatestAnnouncement() { + announcement = null + } + + /** + * Sets whether a screen reader should be considered currently enabled. This will change the + * return value of [isScreenReaderEnabled]. + */ + fun setScreenReaderEnabled(isEnabled: Boolean) { + isScreenReaderEnabled = isEnabled + } + + override fun announceForAccessibilityForView(view: View, text: CharSequence) { + announcement = text + } +}