--- a/geekdroid/build.gradle.kts Tue Feb 18 18:08:10 2025 -0400
+++ b/geekdroid/build.gradle.kts Mon Feb 17 21:58:32 2025 -0400
@@ -107,6 +107,9 @@
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.activity.compose)
+ testImplementation(libs.kotlinx.coroutines.test)
+ testImplementation(libs.turbine)
+
}
--- a/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsLiveData.kt Tue Feb 18 18:08:10 2025 -0400
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsLiveData.kt Mon Feb 17 21:58:32 2025 -0400
@@ -27,10 +27,14 @@
import android.annotation.SuppressLint
import android.os.Build
import androidx.lifecycle.LiveData
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
/**
* Allows to observe the list of [Account] for a specified Account type.
*/
+@Deprecated("Use AccountManager.accountsFlow()")
class AccountsLiveData(
private val accountManager: AccountManager,
vararg accountType: String?
@@ -64,3 +68,43 @@
}
}
+
+/**
+ * Allows to observe the list of [Account] for a specified Account type.
+ */
+fun AccountManager.accountsFlow(vararg accountType: String): Flow<List<Account>> = callbackFlow {
+ val listener = object : OnAccountsUpdateListener {
+ override fun onAccountsUpdated(accounts: Array<out Account?>?) {
+ val forTypes = accounts?.filter { it?.type in accountType }?.filterNotNull() ?: emptyList()
+ trySend(forTypes)
+ }
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ addOnAccountsUpdatedListener(
+ listener, null, true,
+ accountType
+ )
+ } else {
+ addOnAccountsUpdatedListener(listener, null, true)
+ }
+
+ awaitClose {
+ removeOnAccountsUpdatedListener(listener)
+ }
+}
+
+/**
+ * Allows to observe the list of [Account]
+ */
+fun AccountManager.accountsFlow(): Flow<List<Account>> = callbackFlow {
+ val listener = object : OnAccountsUpdateListener {
+ override fun onAccountsUpdated(accounts: Array<out Account?>?) {
+ trySend(accounts?.filterNotNull() ?: emptyList())
+ }
+ }
+ addOnAccountsUpdatedListener(listener, null, true)
+
+ awaitClose {
+ removeOnAccountsUpdatedListener(listener)
+ }
+}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/accounts/AccountsFlowTest.kt Mon Feb 17 21:58:32 2025 -0400
@@ -0,0 +1,110 @@
+/*
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2025 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowAccountManager
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+
+@RunWith(AndroidJUnit4::class)
+@Config(shadows = [com.geekorum.geekdroid.shadows.ShadowAccountManager::class], sdk = [Build.VERSION_CODES.Q])
+class AccountsFlowTest {
+
+ private lateinit var shadowAccountManager: ShadowAccountManager
+ private lateinit var accountManager: AccountManager
+
+ private val TEST_ACCOUNT_TYPE: String = "test.account"
+ private val testAccounts = listOf(
+ Account("test1", TEST_ACCOUNT_TYPE),
+ Account("test2", TEST_ACCOUNT_TYPE),
+ Account("test3", TEST_ACCOUNT_TYPE),
+ )
+ private val testAccountMore = Account("test4 more", TEST_ACCOUNT_TYPE)
+
+
+ private val OTHER_ACCOUNT_TYPE: String = "other"
+ private val otherAccount = Account("test1", OTHER_ACCOUNT_TYPE)
+ private val otherAccount2 = Account("test2", OTHER_ACCOUNT_TYPE)
+
+ @BeforeTest
+ fun setUp() {
+ accountManager = AccountManager.get(RuntimeEnvironment.getApplication())
+ shadowAccountManager = Shadows.shadowOf(accountManager)
+ for (account in testAccounts) {
+ shadowAccountManager.addAccount(account)
+ }
+ shadowAccountManager.addAccount(otherAccount)
+
+ }
+
+ @Test
+ fun testWhenActiveGetTheCorrectData() = runTest {
+ accountManager.accountsFlow(TEST_ACCOUNT_TYPE)
+ .test {
+ assertThat(awaitItem()).isEqualTo(testAccounts)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun testThatWhenActiveGetCorrectUpdates() = runTest {
+ accountManager.accountsFlow(TEST_ACCOUNT_TYPE)
+ .test {
+ var expected = testAccounts
+ assertThat(awaitItem()).isEqualTo(expected)
+
+ // add some accounts
+ shadowAccountManager.addAccount(otherAccount2)
+ shadowAccountManager.addAccount(testAccountMore)
+ expected = testAccounts + testAccountMore
+
+ assertThat(awaitItem()).isEqualTo(expected)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun testThatWhenInactiveGetLaterUpdates() = runTest {
+ // add some accounts
+ shadowAccountManager.addAccount(testAccountMore)
+ shadowAccountManager.addAccount(otherAccount2)
+
+ accountManager.accountsFlow(TEST_ACCOUNT_TYPE)
+ .test {
+ var expected = testAccounts + testAccountMore
+
+ assertThat(awaitItem()).isEqualTo(expected)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+}
\ No newline at end of file
--- a/gradle/libs.versions.toml Tue Feb 18 18:08:10 2025 -0400
+++ b/gradle/libs.versions.toml Mon Feb 17 21:58:32 2025 -0400
@@ -22,6 +22,7 @@
recyclerview = "1.4.0"
room = "2.6.1"
timber = "5.0.1"
+turbine = "1.0.0"
workmanager = "2.10.0"
[plugins]
@@ -54,6 +55,7 @@
kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinx-coroutines-bom" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" }
lifecycle-livedata-core-ktx = { module = "androidx.lifecycle:lifecycle-livedata-core-ktx", version.ref = "lifecycle" }
lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" }
material = { module = "com.google.android.material:material", version.ref = "material" }
@@ -64,5 +66,6 @@
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
+turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
work-runtime = { module = "androidx.work:work-runtime", version.ref = "workmanager" }