Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix part of #42: Exploration player base (Part 1) #42 #100

Merged
merged 12 commits into from
Sep 18, 2019
15 changes: 3 additions & 12 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/OppiaTheme">
<activity android:name=".player.exploration.ExplorationActivity"/>
<activity android:name="org.oppia.app.home.HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dagger.BindsInstance
import dagger.Subcomponent
import org.oppia.app.fragment.FragmentComponent
import org.oppia.app.home.HomeActivity
import org.oppia.app.player.exploration.ExplorationActivity
import javax.inject.Provider

/** Root subcomponent for all activities. */
Expand All @@ -19,5 +20,6 @@ interface ActivityComponent {

fun getFragmentComponentBuilderProvider(): Provider<FragmentComponent.Builder>

fun inject(explorationActivity: ExplorationActivity)
fun inject(homeActivity: HomeActivity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import org.oppia.app.home.HomeFragment
import org.oppia.app.player.exploration.ExplorationFragment
import org.oppia.app.player.state.StateFragment

/** Root subcomponent for all fragments. */
@Subcomponent
Expand All @@ -15,5 +17,7 @@ interface FragmentComponent {
fun build(): FragmentComponent
}

fun inject(explorationFragment: ExplorationFragment)
fun inject(homeFragment: HomeFragment)
fun inject(stateFragment: StateFragment)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.oppia.app.player.exploration

import android.os.Bundle
import org.oppia.app.activity.InjectableAppCompatActivity
import javax.inject.Inject

/** The starting point for exploration. */
class ExplorationActivity : InjectableAppCompatActivity() {
@Inject lateinit var explorationActivityPresenter: ExplorationActivityPresenter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
explorationActivityPresenter.handleOnCreate()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.oppia.app.player.exploration

import androidx.appcompat.app.AppCompatActivity
import org.oppia.app.R
import org.oppia.app.activity.ActivityScope
import org.oppia.app.home.HomeFragment
import javax.inject.Inject

/** The controller for [ExplorationActivity]. */
@ActivityScope
class ExplorationActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
fun handleOnCreate() {
activity.setContentView(R.layout.exploration_activity)
if (getExplorationFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
R.id.exploration_fragment_placeholder,
ExplorationFragment()
).commitNow()
}
}

private fun getExplorationFragment(): ExplorationFragment? {
return activity.supportFragmentManager.findFragmentById(R.id.exploration_fragment_placeholder) as ExplorationFragment?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.oppia.app.player.exploration

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.app.fragment.InjectableFragment
import javax.inject.Inject

/** Fragment that contains displays single exploration. */
class ExplorationFragment : InjectableFragment() {
@Inject lateinit var explorationFragmentPresenter: ExplorationFragmentPresenter

override fun onAttach(context: Context?) {
super.onAttach(context)
fragmentComponent.inject(this)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return explorationFragmentPresenter.handleCreateView(inflater, container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.oppia.app.player.exploration

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import org.oppia.app.databinding.ExplorationFragmentBinding
import org.oppia.app.fragment.FragmentScope
import javax.inject.Inject

/** The controller for [ExplorationFragment]. */
@FragmentScope
class ExplorationFragmentPresenter @Inject constructor(
private val fragment: Fragment
) {
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
return ExplorationFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false).root
}
}
23 changes: 23 additions & 0 deletions app/src/main/java/org/oppia/app/player/state/StateFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.oppia.app.player.state

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.app.fragment.InjectableFragment
import javax.inject.Inject

/** Fragment that contains displays current state of exploration. */
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
class StateFragment : InjectableFragment() {
@Inject lateinit var stateFragmentPresenter: StateFragmentPresenter

override fun onAttach(context: Context?) {
super.onAttach(context)
fragmentComponent.inject(this)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return stateFragmentPresenter.handleCreateView(inflater, container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.oppia.app.player.state

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import org.oppia.app.databinding.StateFragmentBinding
import org.oppia.app.fragment.FragmentScope
import javax.inject.Inject

/** The controller for [StateFragment]. */
@FragmentScope
class StateFragmentPresenter @Inject constructor(
private val fragment: Fragment
) {
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
return StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false).root
}
}
8 changes: 8 additions & 0 deletions app/src/main/res/layout/exploration_activity.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/exploration_fragment_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".player.exploration.ExplorationActivity" />
19 changes: 19 additions & 0 deletions app/src/main/res/layout/exploration_fragment.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/dummy_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dummy_string"
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
10 changes: 10 additions & 0 deletions app/src/main/res/layout/state_fragment.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
<string name="app_name">Oppia</string>
<string name="welcome_text">Welcome to Oppia!</string>
<string name="welcome_back_text">Welcome back to Oppia!</string>
<string name="dummy_string">This is dummy TextView for testing</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.oppia.app.player.exploration

import android.app.Application
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
import org.oppia.util.threading.BackgroundDispatcher
import org.oppia.util.threading.BlockingDispatcher
import java.util.concurrent.AbstractExecutorService
import java.util.concurrent.TimeUnit
import javax.inject.Singleton

/** Tests for [ExplorationActivity]. */
@RunWith(AndroidJUnit4::class)
class ExplorationActivityTest {
@Before
fun setUp() {
IdlingRegistry.getInstance().register(MainThreadExecutor.countingResource)
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
}

@After
fun tearDown() {
IdlingRegistry.getInstance().unregister(MainThreadExecutor.countingResource)
}

@Test
fun testExplorationActivity_loadExplorationFragment_hasDummyString() {
ActivityScenario.launch(ExplorationActivity::class.java).use {
Espresso.onView(ViewMatchers.withId(R.id.dummy_text_view))
.check(ViewAssertions.matches(ViewMatchers.withText("This is dummy TextView for testing")))
}
}

@Module
class TestModule {
@Provides
@Singleton
fun provideContext(application: Application): Context {
return application
}

// TODO(#89): Introduce a proper IdlingResource for background dispatchers to ensure they all complete before
// proceeding in an Espresso test. This solution should also be interoperative with Robolectric contexts by using a
// test coroutine dispatcher.

@Singleton
@Provides
@BackgroundDispatcher
fun provideBackgroundDispatcher(@BlockingDispatcher blockingDispatcher: CoroutineDispatcher): CoroutineDispatcher {
return blockingDispatcher
}

@Singleton
@Provides
@BlockingDispatcher
fun provideBlockingDispatcher(): CoroutineDispatcher {
return MainThreadExecutor.asCoroutineDispatcher()
}
}

@Singleton
@Component(modules = [TestModule::class])
interface TestApplicationComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun setApplication(application: Application): Builder

fun build(): TestApplicationComponent
}
}

// TODO(#59): Move this to a general-purpose testing library that replaces all CoroutineExecutors with an
// Espresso-enabled executor service. This service should also allow for background threads to run in both Espresso
// and Robolectric to help catch potential race conditions, rather than forcing parallel execution to be sequential
// and immediate.
// NB: This also blocks on #59 to be able to actually create a test-only library.
/**
* An executor service that schedules all [Runnable]s to run asynchronously on the main thread. This is based on:
* https://android.googlesource.com/platform/packages/apps/TV/+/android-live-tv/src/com/android/tv/util/MainThreadExecutor.java.
*/
private object MainThreadExecutor : AbstractExecutorService() {
override fun isTerminated(): Boolean = false

private val handler = Handler(Looper.getMainLooper())
val countingResource = CountingIdlingResource("main_thread_executor_counting_idling_resource")

override fun execute(command: Runnable?) {
countingResource.increment()
handler.post {
try {
command?.run()
} finally {
countingResource.decrement()
}
}
}

override fun shutdown() {
throw UnsupportedOperationException()
}

override fun shutdownNow(): MutableList<Runnable> {
throw UnsupportedOperationException()
}

override fun isShutdown(): Boolean = false

override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean {
throw UnsupportedOperationException()
}
}
}