From 6b89c6f6a0fc1c1e460a0e3a31f7eafca61c7896 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 7 Aug 2019 13:08:31 -0700 Subject: [PATCH 01/10] Migrate ListenableFuture to a Kotlin coroutine, and split HomeActivity into both an activity and a fragment. --- app/build.gradle | 2 +- .../main/java/org/oppia/app/HomeActivity.kt | 42 +--------------- .../main/java/org/oppia/app/HomeFragment.kt | 49 +++++++++++++++++++ app/src/main/res/layout/home_activity.xml | 18 ++----- app/src/main/res/layout/home_fragment.xml | 18 +++++++ utility/build.gradle | 1 - .../org/oppia/util/data/AsyncDataSource.kt | 4 +- 7 files changed, 74 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/org/oppia/app/HomeFragment.kt create mode 100644 app/src/main/res/layout/home_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index 41e3e08da0c..43d8122615b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - api 'com.google.guava:guava:28.0-android' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03' testImplementation 'androidx.test:core:1.2.0' testImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/app/src/main/java/org/oppia/app/HomeActivity.kt b/app/src/main/java/org/oppia/app/HomeActivity.kt index 562ff10cf0d..0fda85beb37 100644 --- a/app/src/main/java/org/oppia/app/HomeActivity.kt +++ b/app/src/main/java/org/oppia/app/HomeActivity.kt @@ -1,15 +1,7 @@ package org.oppia.app -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import android.util.Log -import android.widget.TextView -import com.google.common.util.concurrent.FutureCallback -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.MoreExecutors.directExecutor -import org.oppia.app.model.UserAppHistory -import org.oppia.util.data.AsyncDataSource +import androidx.appcompat.app.AppCompatActivity /** The central activity for all users entering the app. */ class HomeActivity : AppCompatActivity() { @@ -17,36 +9,6 @@ class HomeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.home_activity) - } - - override fun onStart() { - super.onStart() - // TODO(BenHenning): Convert this to LiveData rather than risk an async operation completing outside of the - // activity's lifecycle. Also, a directExecutor() in this context could be a really bad idea if it isn't the main - // thread. - Futures.addCallback(getUserAppHistory().pendingOperation, object: FutureCallback { - override fun onSuccess(result: UserAppHistory?) { - if (result != null && result.alreadyOpenedApp) { - getWelcomeTextView().setText(R.string.welcome_back_text) - } - } - - override fun onFailure(t: Throwable) { - // TODO(BenHenning): Replace this log statement with a clearer logging API. - Log.e("HomeActivity", "Failed to retrieve user app history", t) - } - }, directExecutor()) - } - - private fun getWelcomeTextView(): TextView { - return findViewById(R.id.welcome_text_view) - } - - private fun getUserAppHistory(): AsyncDataSource { - // TODO(BenHenning): Retrieve this from a domain provider. - return object: AsyncDataSource { - override val pendingOperation: ListenableFuture - get() = Futures.immediateFuture(UserAppHistory.newBuilder().setAlreadyOpenedApp(false).build()) - } + supportFragmentManager.beginTransaction().add(R.id.home_fragment_placeholder, HomeFragment()).commitNow() } } diff --git a/app/src/main/java/org/oppia/app/HomeFragment.kt b/app/src/main/java/org/oppia/app/HomeFragment.kt new file mode 100644 index 00000000000..a27851abbed --- /dev/null +++ b/app/src/main/java/org/oppia/app/HomeFragment.kt @@ -0,0 +1,49 @@ +package org.oppia.app + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.oppia.app.model.UserAppHistory +import org.oppia.util.data.AsyncDataSource + +class HomeFragment: Fragment() { + private var userHistoryRetrievalJob: Job? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.home_fragment, container, /* attachToRoot= */ false) + } + + override fun onStart() { + if (userHistoryRetrievalJob == null) { + val userAppHistoryDataSource = getUserAppHistory() + userHistoryRetrievalJob = viewLifecycleOwner.lifecycleScope.launch { + // TODO(BenHenning): Convert this to LiveData rather than risk an async operation. + val appHistory = userAppHistoryDataSource.executePendingOperation() + if (appHistory.alreadyOpenedApp) { + getWelcomeTextView()?.setText(R.string.welcome_back_text) + } + } + } + + super.onStart() + } + + private fun getWelcomeTextView(): TextView? { + return view?.findViewById(R.id.welcome_text_view) + } + + private fun getUserAppHistory(): AsyncDataSource { + // TODO(BenHenning): Retrieve this from a domain provider. + return object: AsyncDataSource { + override suspend fun executePendingOperation(): UserAppHistory { + return UserAppHistory.newBuilder().setAlreadyOpenedApp(false).build() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/home_activity.xml b/app/src/main/res/layout/home_activity.xml index c8687eceb33..a538db5ad3a 100644 --- a/app/src/main/res/layout/home_activity.xml +++ b/app/src/main/res/layout/home_activity.xml @@ -1,20 +1,8 @@ - - - - - + tools:context=".HomeActivity" /> \ No newline at end of file diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml new file mode 100644 index 00000000000..b05a946dccb --- /dev/null +++ b/app/src/main/res/layout/home_fragment.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/utility/build.gradle b/utility/build.gradle index 1be4e9df225..921dc581b72 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -31,7 +31,6 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - api 'com.google.guava:guava:28.0-android' implementation 'androidx.appcompat:appcompat:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/utility/src/main/java/org/oppia/util/data/AsyncDataSource.kt b/utility/src/main/java/org/oppia/util/data/AsyncDataSource.kt index 86c3f8084ee..21eb18c8b9a 100644 --- a/utility/src/main/java/org/oppia/util/data/AsyncDataSource.kt +++ b/utility/src/main/java/org/oppia/util/data/AsyncDataSource.kt @@ -1,7 +1,5 @@ package org.oppia.util.data -import com.google.common.util.concurrent.ListenableFuture - /** * Represents a source of data that can be delivered and changed asynchronously. * @@ -10,5 +8,5 @@ import com.google.common.util.concurrent.ListenableFuture interface AsyncDataSource { // TODO(BenHenning): Finalize the interfaces for this API beyond a basic prototype for the initial project intro. - val pendingOperation: ListenableFuture + suspend fun executePendingOperation(): T } From 975bc353da65c39e2f3aac29782b0b533b2b664f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 14 Aug 2019 21:13:27 -0700 Subject: [PATCH 02/10] Introduce the domain and testsupport modules. The domain module includes a user app history controller that provides instances of a new AsyncResult interface that's meant to be a potential bridge between Kotlin coroutines and LiveData. The exact design of how this should work needs to be determined as part of #6. This also includes a new testsupport module that's required due to https://github.com/robolectric/robolectric/pull/4736. This is supporting a new test for the app history controller that leverages an AndroidX activity scenario to test the live data. Note that the test does not yet work since there's a race condition between the LiveData's coroutines completing and the test continuing to verify the state of the activity. This needs to be resolved, likely by waiting for test visual elements to change based on the LiveData result. Additional tests need to be added for other new components, and some slight cleaning up may be necessary. --- .gitignore | 2 + .idea/gradle.xml | 4 +- app/build.gradle | 7 +- .../main/java/org/oppia/app/HomeFragment.kt | 57 ++++++----- .../org/oppia/app/UserAppHistoryViewModel.kt | 10 ++ app/src/main/res/layout/home_fragment.xml | 38 +++++--- domain/build.gradle | 52 ++++++++++ domain/proguard-rules.pro | 21 ++++ domain/src/main/AndroidManifest.xml | 1 + .../oppia/domain/UserAppHistoryController.kt | 36 +++++++ .../domain/UserAppHistoryControllerTest.kt | 95 +++++++++++++++++++ gradle.properties | 2 + model/build.gradle | 1 - settings.gradle | 2 +- testsupport/build.gradle | 29 ++++++ testsupport/src/main/AndroidManifest.xml | 13 +++ .../TestUserAppHistoryControllerActivity.kt | 30 ++++++ testsupport/src/main/res/values/styles.xml | 4 + utility/build.gradle | 4 - .../java/org/oppia/util/data/AsyncResult.kt | 70 ++++++++++++++ 20 files changed, 426 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/org/oppia/app/UserAppHistoryViewModel.kt create mode 100644 domain/build.gradle create mode 100644 domain/proguard-rules.pro create mode 100644 domain/src/main/AndroidManifest.xml create mode 100644 domain/src/main/java/org/oppia/domain/UserAppHistoryController.kt create mode 100644 domain/src/test/java/org/oppia/domain/UserAppHistoryControllerTest.kt create mode 100644 testsupport/build.gradle create mode 100644 testsupport/src/main/AndroidManifest.xml create mode 100644 testsupport/src/main/java/org/oppia/test/TestUserAppHistoryControllerActivity.kt create mode 100644 testsupport/src/main/res/values/styles.xml create mode 100644 utility/src/main/java/org/oppia/util/data/AsyncResult.kt diff --git a/.gitignore b/.gitignore index 5a181851a8f..a376e7f4a31 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,9 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml app/build +domain/build model/build +testsupport/build utility/build .DS_Store /build diff --git a/.idea/gradle.xml b/.idea/gradle.xml index b24ba3a9a74..c96be2ae83a 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,7 +9,9 @@ @@ -17,4 +19,4 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 43d8122615b..b805cab6e92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,5 @@ apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - apply plugin: 'kotlin-android-extensions' android { @@ -21,6 +19,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + dataBinding { + enabled = true + } testOptions { unitTests { includeAndroidResources = true @@ -47,6 +48,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03' + implementation 'android.arch.lifecycle:extensions:1.1.1' testImplementation 'androidx.test:core:1.2.0' testImplementation 'androidx.test.ext:junit:1.1.1' @@ -61,5 +63,6 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation project(":model") + implementation project(":domain") implementation project(":utility") } diff --git a/app/src/main/java/org/oppia/app/HomeFragment.kt b/app/src/main/java/org/oppia/app/HomeFragment.kt index a27851abbed..4e4e9ed3ac5 100644 --- a/app/src/main/java/org/oppia/app/HomeFragment.kt +++ b/app/src/main/java/org/oppia/app/HomeFragment.kt @@ -1,49 +1,48 @@ package org.oppia.app import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModelProviders +import org.oppia.app.databinding.HomeFragmentBinding import org.oppia.app.model.UserAppHistory -import org.oppia.util.data.AsyncDataSource +import org.oppia.domain.UserAppHistoryController +import org.oppia.util.data.AsyncResult -class HomeFragment: Fragment() { - private var userHistoryRetrievalJob: Job? = null +/** Fragment that contains an introduction to the app. */ +class HomeFragment : Fragment() { + private val userAppHistoryController = UserAppHistoryController() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.home_fragment, container, /* attachToRoot= */ false) - } + val binding = HomeFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) + getUserAppHistoryViewModel().userAppHistoryLiveData = getUserAppHistory() + binding.lifecycleOwner = this - override fun onStart() { - if (userHistoryRetrievalJob == null) { - val userAppHistoryDataSource = getUserAppHistory() - userHistoryRetrievalJob = viewLifecycleOwner.lifecycleScope.launch { - // TODO(BenHenning): Convert this to LiveData rather than risk an async operation. - val appHistory = userAppHistoryDataSource.executePendingOperation() - if (appHistory.alreadyOpenedApp) { - getWelcomeTextView()?.setText(R.string.welcome_back_text) - } - } - } + userAppHistoryController.markUserOpenedApp() + + return binding.root + } - super.onStart() + private fun getUserAppHistoryViewModel(): UserAppHistoryViewModel { + return ViewModelProviders.of(this).get(UserAppHistoryViewModel::class.java) } - private fun getWelcomeTextView(): TextView? { - return view?.findViewById(R.id.welcome_text_view) + private fun getUserAppHistory(): LiveData { + // If there's an error loading the data, assume the default. + return Transformations.map( + userAppHistoryController.getUserAppHistory() + ) { result: AsyncResult -> processUserAppHistoryResult(result) } } - private fun getUserAppHistory(): AsyncDataSource { - // TODO(BenHenning): Retrieve this from a domain provider. - return object: AsyncDataSource { - override suspend fun executePendingOperation(): UserAppHistory { - return UserAppHistory.newBuilder().setAlreadyOpenedApp(false).build() - } + private fun processUserAppHistoryResult(appHistoryResult: AsyncResult): UserAppHistory { + if (appHistoryResult.isFailure()) { + Log.e("HomeFragment", "Failed to retrieve user app history", appHistoryResult.error) } + return appHistoryResult.getOrDefault(UserAppHistory.getDefaultInstance()) } } \ No newline at end of file diff --git a/app/src/main/java/org/oppia/app/UserAppHistoryViewModel.kt b/app/src/main/java/org/oppia/app/UserAppHistoryViewModel.kt new file mode 100644 index 00000000000..9b9eeb6e39f --- /dev/null +++ b/app/src/main/java/org/oppia/app/UserAppHistoryViewModel.kt @@ -0,0 +1,10 @@ +package org.oppia.app + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import org.oppia.app.model.UserAppHistory + +/** [ViewModel] for user app usage history. */ +class UserAppHistoryViewModel: ViewModel() { + var userAppHistoryLiveData: LiveData? = null +} \ No newline at end of file diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml index b05a946dccb..eb316d8abf7 100644 --- a/app/src/main/res/layout/home_fragment.xml +++ b/app/src/main/res/layout/home_fragment.xml @@ -1,18 +1,28 @@ - + xmlns:app="http://schemas.android.com/apk/res-auto"> - + + + + - + + + + + + \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100644 index 00000000000..e7cf57f782b --- /dev/null +++ b/domain/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03' + + testImplementation 'android.arch.core:core-testing:1.1.1' + testImplementation 'androidx.test.ext:junit:1.1.1' + testImplementation 'com.google.truth:truth:0.43' + testImplementation 'junit:junit:4.12' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-RC' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0-M2' + testImplementation 'org.mockito:mockito-core:2.19.0' + testImplementation 'org.robolectric:robolectric:4.3' + + implementation project(":model") + implementation project(":testsupport") + implementation project(":utility") +} +repositories { + mavenCentral() +} diff --git a/domain/proguard-rules.pro b/domain/proguard-rules.pro new file mode 100644 index 00000000000..f1b424510da --- /dev/null +++ b/domain/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..7d8459d5279 --- /dev/null +++ b/domain/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/domain/src/main/java/org/oppia/domain/UserAppHistoryController.kt b/domain/src/main/java/org/oppia/domain/UserAppHistoryController.kt new file mode 100644 index 00000000000..fea14908191 --- /dev/null +++ b/domain/src/main/java/org/oppia/domain/UserAppHistoryController.kt @@ -0,0 +1,36 @@ +package org.oppia.domain + +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import org.oppia.app.model.UserAppHistory +import org.oppia.util.data.AsyncDataSource +import org.oppia.util.data.AsyncResult + +/** Controller for persisting and retrieving the previous user history of using the app. */ +class UserAppHistoryController { + // TODO(BenHenning): Persist this value. + private var userOpenedApp = false + + private val userAppHistoryData: LiveData> = liveData { + emit(AsyncResult.success(createUserAppHistoryDataSource().executePendingOperation())) + } + + /** Saves that the user has opened the app. */ + fun markUserOpenedApp() { + userOpenedApp = true + } + + /** Returns a [LiveData] result indicating whether the user has previously opened the app. */ + fun getUserAppHistory(): LiveData> { + return userAppHistoryData + } + + // TODO(BenHenning): Move this to a data source within the data source module. + private fun createUserAppHistoryDataSource(): AsyncDataSource { + return object : AsyncDataSource { + override suspend fun executePendingOperation(): UserAppHistory { + return UserAppHistory.newBuilder().setAlreadyOpenedApp(userOpenedApp).build() + } + } + } +} \ No newline at end of file diff --git a/domain/src/test/java/org/oppia/domain/UserAppHistoryControllerTest.kt b/domain/src/test/java/org/oppia/domain/UserAppHistoryControllerTest.kt new file mode 100644 index 00000000000..21b74fd1c9a --- /dev/null +++ b/domain/src/test/java/org/oppia/domain/UserAppHistoryControllerTest.kt @@ -0,0 +1,95 @@ +package org.oppia.domain + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Observer +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +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.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.oppia.app.model.UserAppHistory +import org.oppia.test.TestUserAppHistoryControllerActivity +import org.oppia.test.TestUserAppHistoryControllerActivity.TestFragment +import org.oppia.util.data.AsyncResult + +/** Tests for [UserAppHistoryController]. */ +@RunWith(AndroidJUnit4::class) +class UserAppHistoryControllerTest { + @Rule + @JvmField + val mockitoRule = MockitoJUnit.rule() + + @Rule + @JvmField + val executorRule = InstantTaskExecutorRule() + + @get:Rule + val testActivityScenarioRule = ActivityScenarioRule(TestUserAppHistoryControllerActivity::class.java) + + @Mock + lateinit var mockAppHistoryObserver: Observer> + + @Captor + lateinit var appHistoryResultCaptor: ArgumentCaptor> + + private val userAppHistoryController = UserAppHistoryController() + + // https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/ + private val mainThreadSurrogate = newSingleThreadContext("UI thread") + + @Before + fun setUp() { + Dispatchers.setMain(mainThreadSurrogate) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + mainThreadSurrogate.close() + } + + @Test + fun testController_providesInitialLiveData_thatIndicatesUserHasNotOpenedTheApp() { + testActivityScenarioRule.scenario.onActivity { activity -> + getTestFragment(activity).observeUserAppHistory(userAppHistoryController.getUserAppHistory()) + } + + testActivityScenarioRule.scenario.moveToState(Lifecycle.State.RESUMED) + + testActivityScenarioRule.scenario.onActivity { activity -> + val appHistoryResult = getTestFragment(activity).userAppHistoryResult + assertThat(appHistoryResult).isNotNull() + assertThat(appHistoryResult!!.isSuccess()).isTrue() + assertThat(appHistoryResult.getOrThrow().alreadyOpenedApp).isFalse() + } + } + + @Test + fun testController_afterSettingAppOpened_providesLiveData_thatIndicatesUserHasOpenedTheApp() { + val appHistory = userAppHistoryController.getUserAppHistory() + appHistory.observeForever(mockAppHistoryObserver) + + userAppHistoryController.markUserOpenedApp() + + verify(mockAppHistoryObserver).onChanged(appHistoryResultCaptor.capture()) + assertThat(appHistoryResultCaptor.value.isSuccess()).isTrue() + assertThat(appHistoryResultCaptor.value.getOrThrow().alreadyOpenedApp).isTrue() + } + + private fun getTestFragment(testActivity: TestUserAppHistoryControllerActivity): TestFragment { + return testActivity.supportFragmentManager.findFragmentByTag("test_fragment") as TestFragment + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index dbe0688ed9f..c1aa0bdbbde 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,3 +25,5 @@ android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official + +android.databinding.enableV2=true diff --git a/model/build.gradle b/model/build.gradle index 0faea0981b3..c0cd1a66381 100644 --- a/model/build.gradle +++ b/model/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'java-library' - apply plugin: 'com.google.protobuf' protobuf { diff --git a/settings.gradle b/settings.gradle index 89a5f6ce1f7..c87f847a8fd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':model', ':utility' +include ':app', ':model', ':utility', ':domain', ':testsupport' diff --git a/testsupport/build.gradle b/testsupport/build.gradle new file mode 100644 index 00000000000..0d517e21249 --- /dev/null +++ b/testsupport/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.0.2' + + implementation project(":model") + implementation project(":utility") + compile "androidx.core:core-ktx:+" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() +} diff --git a/testsupport/src/main/AndroidManifest.xml b/testsupport/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..e9a331aeba9 --- /dev/null +++ b/testsupport/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/testsupport/src/main/java/org/oppia/test/TestUserAppHistoryControllerActivity.kt b/testsupport/src/main/java/org/oppia/test/TestUserAppHistoryControllerActivity.kt new file mode 100644 index 00000000000..6700ad42b2c --- /dev/null +++ b/testsupport/src/main/java/org/oppia/test/TestUserAppHistoryControllerActivity.kt @@ -0,0 +1,30 @@ +package org.oppia.test + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import org.oppia.app.model.UserAppHistory +import org.oppia.util.data.AsyncResult + +const val USER_APP_HISTORY_TEST_FRAGMENT_TAG = "test_fragment" + +/** A test-only activity used for verifying [org.oppia.domain.UserAppHistoryController]. */ +class TestUserAppHistoryControllerActivity: AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportFragmentManager.beginTransaction().add(TestFragment(), USER_APP_HISTORY_TEST_FRAGMENT_TAG).commitNow() + } + + /** The primary test fragment used within the outer test activity. */ + class TestFragment: Fragment() { + var userAppHistoryResult: AsyncResult? = null + + fun observeUserAppHistory(userAppHistoryLiveData: LiveData>) { + userAppHistoryLiveData.observe(this, Observer> { result -> + userAppHistoryResult = result + }) + } + } +} \ No newline at end of file diff --git a/testsupport/src/main/res/values/styles.xml b/testsupport/src/main/res/values/styles.xml new file mode 100644 index 00000000000..8dcedcf1fdb --- /dev/null +++ b/testsupport/src/main/res/values/styles.xml @@ -0,0 +1,4 @@ + + +