From 5d73f97831f7f3653139545fbaba820ba9150fe3 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Sat, 13 Jan 2024 11:47:39 -0600 Subject: [PATCH] Add persistence for a user's "last active time" (#601) --- .../auth/datasource/disk/AuthDiskSource.kt | 19 ++++++ .../datasource/disk/AuthDiskSourceImpl.kt | 14 +++++ .../datasource/disk/AuthDiskSourceTest.kt | 59 +++++++++++++++++++ .../disk/util/FakeAuthDiskSource.kt | 11 ++++ 4 files changed, 103 insertions(+) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt index 0ab08153e9..2a2745b93b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt @@ -31,6 +31,25 @@ interface AuthDiskSource { */ val userStateFlow: Flow + /** + * Retrieves the "last active time" for the given [userId], in milliseconds. + * + * This time is intended to be derived from a call to + * [SystemClock.elapsedRealtime()](https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()) + */ + fun getLastActiveTimeMillis(userId: String): Long? + + /** + * Stores the [lastActiveTimeMillis] for the given [userId]. + * + * This time is intended to be derived from a call to + * [SystemClock.elapsedRealtime()](https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()) + */ + fun storeLastActiveTimeMillis( + userId: String, + lastActiveTimeMillis: Long?, + ) + /** * Retrieves a user key using a [userId]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index d32b040db5..e5ef1761e9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -19,6 +19,7 @@ private const val USER_AUTO_UNLOCK_KEY_KEY = "$ENCRYPTED_BASE_KEY:userKeyAutoUnl private const val UNIQUE_APP_ID_KEY = "$BASE_KEY:appId" private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail" private const val STATE_KEY = "$BASE_KEY:state" +private const val LAST_ACTIVE_TIME_KEY = "$BASE_KEY:lastActiveTime" private const val MASTER_KEY_ENCRYPTION_USER_KEY = "$BASE_KEY:masterKeyEncryptedUserKey" private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "$BASE_KEY:encPrivateKey" private const val ORGANIZATIONS_KEY = "$BASE_KEY:organizations" @@ -66,6 +67,19 @@ class AuthDiskSourceImpl( get() = mutableUserStateFlow .onSubscription { emit(userState) } + override fun getLastActiveTimeMillis(userId: String): Long? = + getLong(key = "${LAST_ACTIVE_TIME_KEY}_$userId") + + override fun storeLastActiveTimeMillis( + userId: String, + lastActiveTimeMillis: Long?, + ) { + putLong( + key = "${LAST_ACTIVE_TIME_KEY}_$userId", + value = lastActiveTimeMillis, + ) + } + private val mutableUserStateFlow = bufferedMutableSharedFlow(replay = 1) override fun getUserKey(userId: String): String? = diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index f3cf14701f..599d3fa513 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -16,7 +16,9 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.encodeToString import kotlinx.serialization.json.encodeToJsonElement import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class AuthDiskSourceTest { @@ -116,6 +118,63 @@ class AuthDiskSourceTest { } } + @Test + fun `getLastActiveTimeMillis should pull from SharedPreferences`() { + val lastActiveTimeBaseKey = "bwPreferencesStorage:lastActiveTime" + val mockUserId = "mockUserId" + val mockLastActiveTime = 123456789L + fakeSharedPreferences + .edit() + .putLong( + "${lastActiveTimeBaseKey}_$mockUserId", + mockLastActiveTime, + ) + .apply() + val actual = authDiskSource.getLastActiveTimeMillis(userId = mockUserId) + assertEquals( + mockLastActiveTime, + actual, + ) + } + + @Test + fun `storeLastActiveTimeMillis for non-null values should update SharedPreferences`() { + val lastActiveTimeBaseKey = "bwPreferencesStorage:lastActiveTime" + val mockUserId = "mockUserId" + val mockLastActiveTime = 123456789L + authDiskSource.storeLastActiveTimeMillis( + userId = mockUserId, + lastActiveTimeMillis = mockLastActiveTime, + ) + val actual = fakeSharedPreferences + .getLong( + "${lastActiveTimeBaseKey}_$mockUserId", + 0L, + ) + assertEquals( + mockLastActiveTime, + actual, + ) + } + + @Test + fun `storeLastActiveTimeMillis for null values should clear SharedPreferences`() { + val lastActiveTimeBaseKey = "bwPreferencesStorage:lastActiveTime" + val mockUserId = "mockUserId" + val mockLastActiveTime = 123456789L + val lastActiveTimeKey = "${lastActiveTimeBaseKey}_$mockUserId" + fakeSharedPreferences + .edit() + .putLong(lastActiveTimeKey, mockLastActiveTime) + .apply() + assertTrue(fakeSharedPreferences.contains(lastActiveTimeKey)) + authDiskSource.storeLastActiveTimeMillis( + userId = mockUserId, + lastActiveTimeMillis = null, + ) + assertFalse(fakeSharedPreferences.contains(lastActiveTimeKey)) + } + @Test fun `getUserKey should pull from SharedPreferences`() { val userKeyBaseKey = "bwPreferencesStorage:masterKeyEncryptedUserKey" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt index a2a4b72c17..1f33c63001 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -19,6 +19,7 @@ class FakeAuthDiskSource : AuthDiskSource { mutableMapOf?>>() private val mutableUserStateFlow = bufferedMutableSharedFlow(replay = 1) + private val storedLastActiveTimeMillis = mutableMapOf() private val storedUserKeys = mutableMapOf() private val storedPrivateKeys = mutableMapOf() private val storedUserAutoUnlockKeys = mutableMapOf() @@ -35,6 +36,16 @@ class FakeAuthDiskSource : AuthDiskSource { override val userStateFlow: Flow get() = mutableUserStateFlow.onSubscription { emit(userState) } + override fun getLastActiveTimeMillis(userId: String): Long? = + storedLastActiveTimeMillis[userId] + + override fun storeLastActiveTimeMillis( + userId: String, + lastActiveTimeMillis: Long?, + ) { + storedLastActiveTimeMillis[userId] = lastActiveTimeMillis + } + override fun getUserKey(userId: String): String? = storedUserKeys[userId] override fun storeUserKey(userId: String, userKey: String?) {