Move all database queries off the ui thread & add a ViewModel for MainActivity (#4786)

- Move all database queries off the ui thread - this is a massive
performance improvement
- ViewModel for MainActivity - this makes MainActivity smaller and
network requests won't be retried when rotating the screen
- removes the Push Notification Migration feature. We had it long
enough, all users who want push notifications should be migrated by now
- AccountEntity is now immutable
- converted BaseActivity to Kotlin
- The header image of Accounts is now cached as well
This commit is contained in:
Konrad Pozniak 2025-01-17 12:35:35 +01:00 committed by GitHub
commit 9e597800c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2421 additions and 1127 deletions

View file

@ -3,7 +3,10 @@ package com.keylesspalace.tusky
import android.app.Activity
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.viewpager2.widget.ViewPager2
@ -12,13 +15,15 @@ import at.connyduck.calladapter.networkresult.NetworkResult
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.getSerializableExtraCompat
import java.util.Date
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@ -128,17 +133,20 @@ class MainActivityTest {
private fun startMainActivity(intent: Intent): Activity {
val controller = Robolectric.buildActivity(MainActivity::class.java, intent)
val activity = controller.get()
activity.eventHub = EventHub()
activity.accountManager = mock {
val eventHub = EventHub()
activity.eventHub = eventHub
val accountManager: AccountManager = mock {
on { accounts } doReturn listOf(accountEntity)
on { accountsFlow } doReturn MutableStateFlow(listOf(accountEntity))
on { activeAccount } doReturn accountEntity
}
activity.draftsAlert = mock {}
activity.shareShortcutHelper = mock {}
activity.externalScope = TestScope()
activity.mastodonApi = mock {
activity.accountManager = accountManager
activity.draftsAlert = mock { }
val api: MastodonApi = mock {
onBlocking { accountVerifyCredentials() } doReturn NetworkResult.success(account)
onBlocking { announcements() } doReturn NetworkResult.success(emptyList())
}
activity.mastodonApi = api
activity.preferences = mock(defaultAnswer = {
when (it.method.returnType) {
String::class.java -> "test"
@ -146,6 +154,20 @@ class MainActivityTest {
else -> null
}
})
val viewModel = MainViewModel(
context = mock {
on { getSystemService(Context.NOTIFICATION_SERVICE) } doReturn mock<NotificationManager>()
},
api = api,
eventHub = eventHub,
accountManager = accountManager,
shareShortcutHelper = mock()
)
val testViewModelFactory = viewModelFactory {
initializer { viewModel }
}
activity.viewModelProviderFactory = testViewModelFactory
controller.create().start()
return activity
}

View file

@ -44,6 +44,7 @@ import com.squareup.moshi.adapter
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Assert.assertEquals
@ -108,6 +109,8 @@ class ComposeActivityTest {
activity = controller.get()
accountManagerMock = mock {
on { accounts } doReturn listOf(account)
on { accountsFlow } doReturn MutableStateFlow(listOf(account))
on { activeAccount } doReturn account
}

View file

@ -20,6 +20,7 @@ import com.keylesspalace.tusky.db.entity.NotificationDataEntity
import com.keylesspalace.tusky.di.NetworkModule
import java.io.IOException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.After
@ -42,15 +43,18 @@ import retrofit2.Response
@RunWith(AndroidJUnit4::class)
class NotificationsRemoteMediatorTest {
private val account = AccountEntity(
id = 1,
domain = "mastodon.example",
accessToken = "token",
clientId = "id",
clientSecret = "secret",
isActive = true
)
private val accountManager: AccountManager = mock {
on { activeAccount } doReturn AccountEntity(
id = 1,
domain = "mastodon.example",
accessToken = "token",
clientId = "id",
clientSecret = "secret",
isActive = true
)
on { activeAccount } doReturn account
on { accountsFlow } doReturn MutableStateFlow(listOf(account))
}
private lateinit var db: AppDatabase
@ -78,12 +82,12 @@ class NotificationsRemoteMediatorTest {
@ExperimentalPagingApi
fun `should return error when network call returns error code`() = runTest {
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
},
db = db,
excludes = emptySet()
db = db
)
val result = remoteMediator.load(LoadType.REFRESH, state())
@ -97,12 +101,12 @@ class NotificationsRemoteMediatorTest {
@ExperimentalPagingApi
fun `should return error when network call fails`() = runTest {
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
},
db = db,
excludes = emptySet()
db = db
)
val result = remoteMediator.load(LoadType.REFRESH, state())
@ -115,10 +119,10 @@ class NotificationsRemoteMediatorTest {
@ExperimentalPagingApi
fun `should not prepend notifications`() = runTest {
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock(),
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -151,6 +155,7 @@ class NotificationsRemoteMediatorTest {
db.insert(notificationsAlreadyInDb)
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success(
@ -168,8 +173,7 @@ class NotificationsRemoteMediatorTest {
)
)
},
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -212,6 +216,7 @@ class NotificationsRemoteMediatorTest {
db.insert(notificationsAlreadyInDb)
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(
@ -239,8 +244,7 @@ class NotificationsRemoteMediatorTest {
)
)
},
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -288,6 +292,7 @@ class NotificationsRemoteMediatorTest {
db.insert(notificationsAlreadyInDb)
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success(
@ -305,8 +310,7 @@ class NotificationsRemoteMediatorTest {
)
)
},
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -340,6 +344,7 @@ class NotificationsRemoteMediatorTest {
@ExperimentalPagingApi
fun `should not try to refresh already cached notifications when db is empty`() = runTest {
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(
@ -350,8 +355,7 @@ class NotificationsRemoteMediatorTest {
)
)
},
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -393,6 +397,7 @@ class NotificationsRemoteMediatorTest {
db.timelineStatusDao().setContentCollapsed(1, "1", false)
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(emptyList())
@ -404,8 +409,7 @@ class NotificationsRemoteMediatorTest {
)
)
},
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -449,6 +453,7 @@ class NotificationsRemoteMediatorTest {
db.notificationsDao().insertNotification(placeholder)
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(sinceId = "6", limit = 20, excludes = emptySet()) } doReturn Response.success(
@ -465,8 +470,7 @@ class NotificationsRemoteMediatorTest {
)
)
},
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -507,6 +511,7 @@ class NotificationsRemoteMediatorTest {
db.insert(notificationsAlreadyInDb)
val remoteMediator = NotificationsRemoteMediator(
viewModel = mockViewModel(),
accountManager = accountManager,
api = mock {
onBlocking { notifications(maxId = "5", limit = 20, excludes = emptySet()) } doReturn Response.success(
@ -517,8 +522,7 @@ class NotificationsRemoteMediatorTest {
)
)
},
db = db,
excludes = emptySet()
db = db
)
val state = state(
@ -558,4 +562,11 @@ class NotificationsRemoteMediatorTest {
),
leadingPlaceholderCount = 0
)
private fun mockViewModel(): NotificationsViewModel {
return mock {
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { excludes } doReturn MutableStateFlow(emptySet())
}
}
}

View file

@ -11,6 +11,7 @@ import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineRemoteMediator
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.Converters
@ -19,6 +20,7 @@ import com.keylesspalace.tusky.db.entity.HomeTimelineData
import com.keylesspalace.tusky.di.NetworkModule
import java.io.IOException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.After
@ -41,17 +43,6 @@ import retrofit2.Response
@RunWith(AndroidJUnit4::class)
class CachedTimelineRemoteMediatorTest {
private val accountManager: AccountManager = mock {
on { activeAccount } doReturn AccountEntity(
id = 1,
domain = "mastodon.example",
accessToken = "token",
clientId = "id",
clientSecret = "secret",
isActive = true
)
}
private lateinit var db: AppDatabase
private val moshi = NetworkModule.providesMoshi()
@ -77,7 +68,7 @@ class CachedTimelineRemoteMediatorTest {
@ExperimentalPagingApi
fun `should return error when network call returns error code`() = runTest {
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
},
@ -95,7 +86,7 @@ class CachedTimelineRemoteMediatorTest {
@ExperimentalPagingApi
fun `should return error when network call fails`() = runTest {
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
},
@ -112,7 +103,7 @@ class CachedTimelineRemoteMediatorTest {
@ExperimentalPagingApi
fun `should not prepend statuses`() = runTest {
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock(),
db = db,
)
@ -147,7 +138,7 @@ class CachedTimelineRemoteMediatorTest {
db.insert(statusesAlreadyInDb)
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(limit = 3) } doReturn Response.success(
listOf(
@ -207,7 +198,7 @@ class CachedTimelineRemoteMediatorTest {
db.insert(statusesAlreadyInDb)
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(limit = 20) } doReturn Response.success(
listOf(
@ -266,7 +257,7 @@ class CachedTimelineRemoteMediatorTest {
db.insert(statusesAlreadyInDb)
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(limit = 3) } doReturn Response.success(
listOf(
@ -317,7 +308,7 @@ class CachedTimelineRemoteMediatorTest {
@ExperimentalPagingApi
fun `should not try to refresh already cached statuses when db is empty`() = runTest {
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(limit = 20) } doReturn Response.success(
listOf(
@ -366,7 +357,7 @@ class CachedTimelineRemoteMediatorTest {
db.insert(statusesAlreadyInDb)
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(limit = 20) } doReturn Response.success(emptyList())
@ -416,7 +407,7 @@ class CachedTimelineRemoteMediatorTest {
db.insert(statusesAlreadyInDb)
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(sinceId = "6", limit = 20) } doReturn Response.success(
listOf(
@ -473,7 +464,7 @@ class CachedTimelineRemoteMediatorTest {
db.insert(statusesAlreadyInDb)
val remoteMediator = CachedTimelineRemoteMediator(
accountManager = accountManager,
viewModel = mockViewModel(),
api = mock {
onBlocking { homeTimeline(maxId = "5", limit = 20) } doReturn Response.success(
listOf(
@ -523,4 +514,23 @@ class CachedTimelineRemoteMediatorTest {
),
leadingPlaceholderCount = 0
)
private fun mockViewModel(): CachedTimelineViewModel {
val account = AccountEntity(
id = 1,
domain = "mastodon.example",
accessToken = "token",
clientId = "id",
clientSecret = "secret",
isActive = true
)
val accManager: AccountManager = mock {
on { activeAccount } doReturn account
on { accountsFlow } doReturn MutableStateFlow(listOf(account))
}
return mock {
on { accountManager } doReturn accManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
}
}
}

View file

@ -14,6 +14,7 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.viewdata.StatusViewData
import java.io.IOException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import okhttp3.Headers
import okhttp3.ResponseBody.Companion.toResponseBody
@ -34,15 +35,17 @@ import retrofit2.Response
@RunWith(AndroidJUnit4::class)
class NetworkTimelineRemoteMediatorTest {
private val account = AccountEntity(
id = 1,
domain = "mastodon.example",
accessToken = "token",
clientId = "id",
clientSecret = "secret",
isActive = true
)
private val accountManager: AccountManager = mock {
on { activeAccount } doReturn AccountEntity(
id = 1,
domain = "mastodon.example",
accessToken = "token",
clientId = "id",
clientSecret = "secret",
isActive = true
)
on { activeAccount } doReturn account
}
@Test
@ -53,7 +56,7 @@ class NetworkTimelineRemoteMediatorTest {
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val result = remoteMediator.load(LoadType.REFRESH, state())
@ -66,11 +69,13 @@ class NetworkTimelineRemoteMediatorTest {
@ExperimentalPagingApi
fun `should return error when network call fails`() = runTest {
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn mutableListOf()
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val result = remoteMediator.load(LoadType.REFRESH, state())
@ -84,6 +89,9 @@ class NetworkTimelineRemoteMediatorTest {
val statuses: MutableList<StatusViewData> = mutableListOf()
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn statuses
on { nextKey } doReturn null
onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success(
@ -99,7 +107,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val state = state(
listOf(
@ -135,6 +143,8 @@ class NetworkTimelineRemoteMediatorTest {
)
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn statuses
on { nextKey } doReturn "0"
onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success(
@ -146,7 +156,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val state = state(
listOf(
@ -187,6 +197,8 @@ class NetworkTimelineRemoteMediatorTest {
)
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn statuses
on { nextKey } doReturn "0"
onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success(
@ -198,7 +210,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val state = state(
listOf(
@ -240,6 +252,8 @@ class NetworkTimelineRemoteMediatorTest {
)
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn statuses
on { nextKey } doReturn "3"
onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success(
@ -251,7 +265,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val state = state(
listOf(
@ -293,6 +307,8 @@ class NetworkTimelineRemoteMediatorTest {
)
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn statuses
on { nextKey } doReturn "3"
onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success(
@ -308,7 +324,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val state = state(
listOf(
@ -350,11 +366,13 @@ class NetworkTimelineRemoteMediatorTest {
)
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn statuses
on { nextKey } doReturn null
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val state = state(
listOf(
@ -393,6 +411,8 @@ class NetworkTimelineRemoteMediatorTest {
)
val timelineViewModel: NetworkTimelineViewModel = mock {
on { accountManager } doReturn accountManager
on { activeAccountFlow } doReturn MutableStateFlow(account)
on { statusData } doReturn statuses
on { nextKey } doReturn "3"
on { kind } doReturn TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
@ -409,7 +429,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
val state = state(
listOf(

View file

@ -7,6 +7,8 @@ import com.keylesspalace.tusky.di.StorageModule
import com.keylesspalace.tusky.entity.Emoji
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@ -24,7 +26,7 @@ class MigrationsTest {
)
@Test
fun testMigrations() {
fun testMigrations() = runTest {
/** the db name must match the one in [StorageModule.providesDatabase] */
val db = migrationHelper.createDatabase("tuskyDB", 10)
val moshi = Moshi.Builder().build()
@ -73,7 +75,7 @@ class MigrationsTest {
Converters(moshi)
)
val account = roomDb.accountDao().loadAll().first()
val account = roomDb.accountDao().allAccounts().first().first()
roomDb.close()