diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b40913b276f..675725b3fe7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -194,6 +194,7 @@
android:name=".app.testing.TopicTestActivityForStory"
android:theme="@style/OppiaThemeWithoutActionBar" />
+
+
+
+
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt
new file mode 100644
index 00000000000..74d3f3754f1
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt
@@ -0,0 +1,246 @@
+package org.oppia.android.app.databinding
+
+import android.app.Activity
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import android.widget.ImageView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.intent.Intents
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.oppia.android.R
+import org.oppia.android.app.activity.ActivityComponent
+import org.oppia.android.app.application.ActivityComponentFactory
+import org.oppia.android.app.application.ApplicationComponent
+import org.oppia.android.app.application.ApplicationInjector
+import org.oppia.android.app.application.ApplicationInjectorProvider
+import org.oppia.android.app.application.ApplicationModule
+import org.oppia.android.app.application.ApplicationStartupListenerModule
+import org.oppia.android.app.databinding.ViewBindingAdapters.setRotationAnimation
+import org.oppia.android.app.devoptions.DeveloperOptionsModule
+import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+import org.oppia.android.app.testing.ViewBindingAdaptersTestActivity
+import org.oppia.android.app.topic.PracticeTabModule
+import org.oppia.android.domain.classify.InteractionsModule
+import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule
+import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.android.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.android.domain.platformparameter.PlatformParameterModule
+import org.oppia.android.domain.question.QuestionModule
+import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.android.testing.TestImageLoaderModule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.robolectric.RobolectricModule
+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.caching.testing.CachingTestModule
+import org.oppia.android.util.gcsresource.GcsResourceModule
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
+import org.oppia.android.util.parser.image.ImageParsingModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Default value for float comparison. */
+private const val TOLERANCE = 1e-5f
+
+/** Tests for [MarginBindingAdapters]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = ViewBindingAdaptersTest.TestApplication::class, qualifiers = "port-xxhdpi")
+class ViewBindingAdaptersTest {
+
+ @Inject
+ lateinit var context: Context
+
+ @get:Rule
+ var activityRule: ActivityScenarioRule =
+ ActivityScenarioRule(
+ Intent(
+ ApplicationProvider.getApplicationContext(),
+ ViewBindingAdaptersTestActivity::class.java
+ )
+ )
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ }
+
+ @Config(qualifiers = "port")
+ @Test
+ fun testViewBindingAdapters_ltrIsEnabled_antiClockwise_rotationAngleForLtrIsCorrect() {
+ val imageViewDropDown = activityRule.scenario.runWithActivity {
+ val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
+ setRotationAnimation(
+ imageViewDropDown,
+ /* isClockwise= */ false,
+ /* angle= */ 180f
+ )
+ return@runWithActivity imageViewDropDown
+ }
+ assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(180f)
+ }
+
+ @Config(qualifiers = "port")
+ @Test
+ fun testViewBindingAdapters_ltrIsEnabled_clockwise_rotationAngleForLtrIsCorrect() {
+ val imageViewDropDown = activityRule.scenario.runWithActivity {
+ val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
+ setRotationAnimation(
+ imageViewDropDown,
+ /* isClockwise= */ true,
+ /* angle= */ 180f
+ )
+ return@runWithActivity imageViewDropDown
+ }
+ assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(0f)
+ }
+
+ @Config(qualifiers = "port")
+ @Test
+ fun testViewBindingAdapters_rtlIsEnabled_clockwise_rotationAngleForRtlIsCorrect() {
+ val imageViewDropDown = activityRule.scenario.runWithActivity {
+ val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
+ ViewCompat.setLayoutDirection(imageViewDropDown, ViewCompat.LAYOUT_DIRECTION_RTL)
+ setRotationAnimation(
+ imageViewDropDown,
+ /* isClockwise= */ true,
+ /* angle= */ 180f
+ )
+ return@runWithActivity imageViewDropDown
+ }
+ assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(360f)
+ }
+
+ @Config(qualifiers = "port")
+ @Test
+ fun testViewBindingAdapters_rtlIsEnabled_antiClockwise_rotationAngleForRtlIsCorrect() {
+ val imageViewDropDown = activityRule.scenario.runWithActivity {
+ val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon)
+ ViewCompat.setLayoutDirection(imageViewDropDown, ViewCompat.LAYOUT_DIRECTION_RTL)
+ setRotationAnimation(
+ imageViewDropDown,
+ /* isClockwise= */ false,
+ /* angle= */ 180f
+ )
+ return@runWithActivity imageViewDropDown
+ }
+ assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(180f)
+ }
+
+ private inline fun ActivityScenario.runWithActivity(
+ crossinline action: (A) -> V
+ ): V {
+ // Use Mockito to ensure the routine is actually executed before returning the result.
+ @Suppress("UNCHECKED_CAST") // The unsafe cast is necessary to make the routine generic.
+ val fakeMock: Consumer = mock(Consumer::class.java) as Consumer
+ val valueCaptor = ArgumentCaptor.forClass(V::class.java)
+ onActivity { fakeMock.consume(action(it)) }
+ verify(fakeMock).consume(valueCaptor.capture())
+ return valueCaptor.value
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ @Singleton
+ @Component(
+ modules = [
+ RobolectricModule::class,
+ PlatformParameterModule::class,
+ TestDispatcherModule::class, ApplicationModule::class,
+ LoggerModule::class, ContinueModule::class, FractionInputModule::class,
+ ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
+ NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
+ DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class,
+ GcsResourceModule::class, TestImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
+ DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
+ ExplorationStorageModule::class
+ ]
+ )
+ /** Create a TestApplicationComponent. */
+ interface TestApplicationComponent : ApplicationComponent {
+ /** Build the TestApplicationComponent. */
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ /** Inject [ViewBindingAdaptersTest] in TestApplicationComponent . */
+ fun inject(ViewBindingAdaptersTest: ViewBindingAdaptersTest)
+ }
+
+ /**
+ * Class to override a dependency throughout the test application, instead of overriding the
+ * dependencies in every test class, we can just do it once by extending the Application class.
+ */
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerViewBindingAdaptersTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ /** Inject [ViewBindingAdaptersTest] in TestApplicationComponent . */
+ fun inject(viewBindingAdapters: ViewBindingAdaptersTest) {
+ component.inject(viewBindingAdapters)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
+
+ private interface Consumer {
+ /** Represents an operation that accepts a single input argument and returns no result. */
+ fun consume(value: T)
+ }
+}
diff --git a/scripts/assets/accessibility_label_exemptions.textproto b/scripts/assets/accessibility_label_exemptions.textproto
index 751cdc1c57b..b91017e02a2 100644
--- a/scripts/assets/accessibility_label_exemptions.textproto
+++ b/scripts/assets/accessibility_label_exemptions.textproto
@@ -32,4 +32,5 @@ exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TestFontScal
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivityForStory"
+exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ViewBindingAdaptersTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/walkthrough/WalkthroughActivity"
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 5db0c9b03a1..48148d020be 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -385,6 +385,7 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TestFontSca
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivity.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivityPresenter.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivity.kt"
+exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ViewBindingAdaptersTestActivity.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/EnablePracticeTab.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/PracticeTabModule.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/RouteToConceptCardListener.kt"