From 2fa97f4bc057a314f6a292f5a3f832d3636878b6 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Fri, 29 Nov 2024 14:48:20 +0100 Subject: [PATCH] Move Android test as jvm test: TabDataRepository, and fix flaky test (#5316) Task/Issue URL: https://app.asana.com/0/1202552961248957/1208839731155341/f ### Description Fixes flaky test due time reference. Moves it as jvm. ### Steps to test this PR _Feature 1_ - [ ] CI is green - [ ] ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| --- .../java/com/duckduckgo/app/TestExtensions.kt | 19 ------ ...wserTabFireproofDialogsEventHandlerTest.kt | 2 +- .../FireproofWebsiteRepositoryImplTest.kt | 2 +- .../LocationPermissionsRepositoryImplTest.kt | 2 +- .../privacy/db/NetworkLeaderboardDaoTest.kt | 2 +- .../app/privacy/db/UserAllowListDaoTest.kt | 2 +- .../com/duckduckgo/app/tabs/db/TabsDao.kt | 3 +- .../app/tabs/model/TabDataRepository.kt | 8 +-- .../com/duckduckgo}/tabs/db/TabsDaoTest.kt | 8 ++- .../tabs/model/TabDataRepositoryTest.kt | 59 ++++++++++++++----- common/common-test/build.gradle | 1 + .../common/test/LiveDataTestExtensions.kt | 35 +++++++++++ 12 files changed, 97 insertions(+), 46 deletions(-) rename app/src/{androidTest/java/com/duckduckgo/app => test/java/com/duckduckgo}/tabs/db/TabsDaoTest.kt (98%) rename app/src/{androidTest/java/com/duckduckgo/app => test/java/com/duckduckgo}/tabs/model/TabDataRepositoryTest.kt (92%) create mode 100644 common/common-test/src/main/java/com/duckduckgo/common/test/LiveDataTestExtensions.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/TestExtensions.kt b/app/src/androidTest/java/com/duckduckgo/app/TestExtensions.kt index afa35bfc7e8f..8e16d24d8d9d 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/TestExtensions.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/TestExtensions.kt @@ -16,28 +16,9 @@ package com.duckduckgo.app -import androidx.annotation.UiThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.di.AppComponent import com.duckduckgo.app.global.DuckDuckGoApplication -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -// from https://stackoverflow.com/a/44991770/73479 -@UiThread -fun LiveData.blockingObserve(): T? { - var value: T? = null - val latch = CountDownLatch(1) - val innerObserver = Observer { - value = it - latch.countDown() - } - observeForever(innerObserver) - latch.await(2, TimeUnit.SECONDS) - return value -} fun getApp(): DuckDuckGoApplication { return InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as DuckDuckGoApplication diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/BrowserTabFireproofDialogsEventHandlerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/BrowserTabFireproofDialogsEventHandlerTest.kt index 2272e2d50997..7a79e415790e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/BrowserTabFireproofDialogsEventHandlerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/logindetection/BrowserTabFireproofDialogsEventHandlerTest.kt @@ -19,7 +19,6 @@ package com.duckduckgo.app.browser.logindetection import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.browser.logindetection.FireproofDialogsEventHandler.Event import com.duckduckgo.app.browser.logindetection.FireproofDialogsEventHandler.Event.FireproofWebSiteSuccess import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao @@ -33,6 +32,7 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.test.blockingObserve import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertNull diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepositoryImplTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepositoryImplTest.kt index 7037fe355408..476d2645be22 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepositoryImplTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/fireproofwebsite/data/FireproofWebsiteRepositoryImplTest.kt @@ -19,10 +19,10 @@ package com.duckduckgo.app.fire.fireproofwebsite.data import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.test.blockingObserve import dagger.Lazy import kotlinx.coroutines.test.runTest import org.junit.After diff --git a/app/src/androidTest/java/com/duckduckgo/app/location/data/LocationPermissionsRepositoryImplTest.kt b/app/src/androidTest/java/com/duckduckgo/app/location/data/LocationPermissionsRepositoryImplTest.kt index 2f2e66410a7c..e7b4d1d26665 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/location/data/LocationPermissionsRepositoryImplTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/location/data/LocationPermissionsRepositoryImplTest.kt @@ -19,10 +19,10 @@ package com.duckduckgo.app.location.data import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.test.blockingObserve import dagger.Lazy import kotlinx.coroutines.test.runTest import org.junit.After diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/db/NetworkLeaderboardDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/db/NetworkLeaderboardDaoTest.kt index 2b4b52b20175..095daaf0c8d4 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/privacy/db/NetworkLeaderboardDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/db/NetworkLeaderboardDaoTest.kt @@ -19,8 +19,8 @@ package com.duckduckgo.app.privacy.db import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.common.test.blockingObserve import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserAllowListDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserAllowListDaoTest.kt index 77094232ab3a..9e2784942785 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserAllowListDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserAllowListDaoTest.kt @@ -19,9 +19,9 @@ package com.duckduckgo.app.privacy.db import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.test.blockingObserve import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.After diff --git a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt index 32cedd2d886f..09cb34c742eb 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt @@ -24,7 +24,6 @@ import com.duckduckgo.common.utils.swap import com.duckduckgo.di.scopes.AppScope import dagger.SingleInstanceIn import java.time.LocalDateTime -import java.time.ZoneOffset import kotlinx.coroutines.flow.Flow @Dao @@ -170,7 +169,7 @@ abstract class TabsDao { @Query("update tabs set lastAccessTime=:lastAccessTime where tabId=:tabId") abstract fun updateTabLastAccess( tabId: String, - lastAccessTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC), + lastAccessTime: LocalDateTime, ) @Query("update tabs set url=:url, title=:title, viewed=:viewed where tabId=:tabId") diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index 6e35c1172fca..06e8cce5c1ad 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -30,13 +30,12 @@ import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType import com.duckduckgo.app.tabs.model.TabSwitcherData.UserState import com.duckduckgo.app.tabs.store.TabSwitcherDataStore import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.common.utils.CurrentTimeProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import dagger.SingleInstanceIn import io.reactivex.Scheduler import io.reactivex.schedulers.Schedulers -import java.time.LocalDateTime -import java.time.ZoneOffset import java.util.UUID import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -57,6 +56,7 @@ class TabDataRepository @Inject constructor( private val webViewPreviewPersister: WebViewPreviewPersister, private val faviconManager: FaviconManager, private val tabSwitcherDataStore: TabSwitcherDataStore, + private val timeProvider: CurrentTimeProvider, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatchers: DispatcherProvider, ) : TabRepository { @@ -199,7 +199,7 @@ class TabDataRepository @Inject constructor( } override fun countTabsAccessedWithinRange(accessOlderThan: Long, accessNotMoreThan: Long?): Int { - val now = LocalDateTime.now(ZoneOffset.UTC) + val now = timeProvider.localDateTimeNow() val start = now.minusDays(accessOlderThan) val end = accessNotMoreThan?.let { now.minusDays(it).minusSeconds(1) } // subtracted a second to make the end limit inclusive return tabsDao.tabs().filter { @@ -245,7 +245,7 @@ class TabDataRepository @Inject constructor( override suspend fun updateTabLastAccess(tabId: String) { databaseExecutor().scheduleDirect { - tabsDao.updateTabLastAccess(tabId) + tabsDao.updateTabLastAccess(tabId, timeProvider.localDateTimeNow()) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt b/app/src/test/java/com/duckduckgo/tabs/db/TabsDaoTest.kt similarity index 98% rename from app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt rename to app/src/test/java/com/duckduckgo/tabs/db/TabsDaoTest.kt index e9c6d1d1e72e..5aa1c47aa39a 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt +++ b/app/src/test/java/com/duckduckgo/tabs/db/TabsDaoTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,14 @@ * limitations under the License. */ -package com.duckduckgo.app.tabs.db +package com.duckduckgo.tabs.db import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.tabs.db.TabsDao import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabSelectionEntity import kotlinx.coroutines.test.runTest @@ -28,7 +30,9 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) class TabsDaoTest { @get:Rule diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt b/app/src/test/java/com/duckduckgo/tabs/model/TabDataRepositoryTest.kt similarity index 92% rename from app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt rename to app/src/test/java/com/duckduckgo/tabs/model/TabDataRepositoryTest.kt index 514ebe75a2c4..53113b90452a 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt +++ b/app/src/test/java/com/duckduckgo/tabs/model/TabDataRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,13 @@ * limitations under the License. */ -@file:Suppress("RemoveExplicitTypeArguments") - -package com.duckduckgo.app.tabs.model +package com.duckduckgo.tabs.model import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.browser.DuckDuckGoUrlDetector import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository import com.duckduckgo.app.browser.favicon.FaviconManager @@ -31,12 +29,18 @@ import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.model.SiteFactoryImpl import com.duckduckgo.app.privacy.db.UserAllowListRepository import com.duckduckgo.app.tabs.db.TabsDao +import com.duckduckgo.app.tabs.model.TabDataRepository +import com.duckduckgo.app.tabs.model.TabEntity +import com.duckduckgo.app.tabs.model.TabSelectionEntity import com.duckduckgo.app.tabs.store.TabSwitcherDataStore import com.duckduckgo.app.trackerdetection.EntityLookup import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.InstantSchedulersRule +import com.duckduckgo.common.test.blockingObserve +import com.duckduckgo.common.utils.CurrentTimeProvider import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.privacy.config.api.ContentBlocking +import java.time.Instant import java.time.LocalDateTime import java.time.ZoneOffset import kotlinx.coroutines.channels.Channel @@ -44,11 +48,23 @@ import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.After -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test -import org.mockito.kotlin.* - +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) class TabDataRepositoryTest { @get:Rule @@ -206,6 +222,7 @@ class TabDataRepositoryTest { val existingTabs = listOf(tab0) whenever(mockDao.tabs()).thenReturn(existingTabs) + whenever(mockDao.lastTab()).thenReturn(existingTabs.last()) testee.add("http://www.example.com") @@ -468,7 +485,7 @@ class TabDataRepositoryTest { @Test fun getActiveTabCountReturnsCorrectCountWhenTabsYoungerThanSpecifiedDay() = runTest { // Arrange: No tabs in the repository - val now = LocalDateTime.now(ZoneOffset.UTC) + val now = now() val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(6)) val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(8)) val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(10)) @@ -497,10 +514,10 @@ class TabDataRepositoryTest { @Test fun getInactiveTabCountReturnsCorrectCountWhenAllTabsOlderThanSpecifiedDay() = runTest { // Arrange: Add some tabs with different last access times - val now = LocalDateTime.now(ZoneOffset.UTC) + val now = now() val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(8)) val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(10)) - val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(9)) + val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(9).minusSeconds(1)) val tab4 = TabEntity(tabId = "tab4") whenever(mockDao.tabs()).thenReturn(listOf(tab1, tab2, tab3, tab4)) val testee = tabDataRepository() @@ -514,7 +531,7 @@ class TabDataRepositoryTest { @Test fun getInactiveTabCountReturnsCorrectCountWhenAllTabsInactiveWithinRange() = runTest { // Arrange: Add some tabs with different last access times - val now = LocalDateTime.now(ZoneOffset.UTC) + val now = now() val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(8)) val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(10)) val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(9)) @@ -531,7 +548,7 @@ class TabDataRepositoryTest { @Test fun getInactiveTabCountReturnsZeroWhenNoTabsInactiveWithinRange() = runTest { // Arrange: Add some tabs with different last access times - val now = LocalDateTime.now(ZoneOffset.UTC) + val now = now() val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(5)) val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(6)) val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(13)) @@ -548,7 +565,7 @@ class TabDataRepositoryTest { @Test fun getInactiveTabCountReturnsCorrectCountWhenSomeTabsInactiveWithinRange() = runTest { // Arrange: Add some tabs with different last access times - val now = LocalDateTime.now(ZoneOffset.UTC) + val now = now() val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(5)) val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(10)) val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(15)) @@ -572,6 +589,7 @@ class TabDataRepositoryTest { faviconManager: FaviconManager = mock(), tabSwitcherDataStore: TabSwitcherDataStore = mock(), duckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock(), + timeProvider: CurrentTimeProvider = FakeTimeProvider(), ): TabDataRepository { return TabDataRepository( dao, @@ -588,6 +606,7 @@ class TabDataRepositoryTest { webViewPreviewPersister, faviconManager, tabSwitcherDataStore, + timeProvider, coroutinesTestRule.testScope, coroutinesTestRule.testDispatcherProvider, ) @@ -608,7 +627,19 @@ class TabDataRepositoryTest { return mockDao } + private fun now(): LocalDateTime { + return FakeTimeProvider().localDateTimeNow() + } + companion object { const val TAB_ID = "abcd" } + + private class FakeTimeProvider : CurrentTimeProvider { + var currentTime: Instant = Instant.parse("2024-10-16T00:00:00.00Z") + + override fun currentTimeMillis(): Long = currentTime.toEpochMilli() + override fun elapsedRealtime(): Long = throw UnsupportedOperationException() + override fun localDateTimeNow(): LocalDateTime = currentTime.atZone(ZoneOffset.UTC).toLocalDateTime() + } } diff --git a/common/common-test/build.gradle b/common/common-test/build.gradle index 755ba72bccb8..cc1099219493 100644 --- a/common/common-test/build.gradle +++ b/common/common-test/build.gradle @@ -14,6 +14,7 @@ dependencies { // Dependencies for this Module implementation project(path: ':common-utils') + implementation AndroidX.lifecycle.liveDataKtx implementation "io.reactivex.rxjava2:rxjava:_" implementation "io.reactivex.rxjava2:rxandroid:_" implementation "com.squareup.moshi:moshi-kotlin:_" diff --git a/common/common-test/src/main/java/com/duckduckgo/common/test/LiveDataTestExtensions.kt b/common/common-test/src/main/java/com/duckduckgo/common/test/LiveDataTestExtensions.kt new file mode 100644 index 000000000000..81f4d9f298b1 --- /dev/null +++ b/common/common-test/src/main/java/com/duckduckgo/common/test/LiveDataTestExtensions.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.common.test + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +// https://stackoverflow.com/a/44991770/73479 +fun LiveData.blockingObserve(): T? { + var value: T? = null + val latch = CountDownLatch(1) + val innerObserver = Observer { + value = it + latch.countDown() + } + observeForever(innerObserver) + latch.await(2, TimeUnit.SECONDS) + return value +}