Skip to content

Commit 9b064ee

Browse files
PM-15380 Track user interactions which would trigger a potential showing of the app review prompt. (#4415)
1 parent d9ef87e commit 9b064ee

File tree

22 files changed

+512
-19
lines changed

22 files changed

+512
-19
lines changed

app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,35 @@ interface SettingsDiskSource {
325325
* Emits updates that track [getVaultRegisteredForExport] for the given [userId].
326326
*/
327327
fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?>
328+
329+
/**
330+
* Gets the number of qualifying add cipher actions for the device.
331+
*/
332+
fun getAddCipherActionCount(): Int?
333+
334+
/**
335+
* Stores the given [count] completed "add" cipher actions taken place on the device.
336+
*/
337+
fun storeAddCipherActionCount(count: Int?)
338+
339+
/**
340+
* Gets the number of qualifying generated result actions for the device.
341+
*/
342+
fun getGeneratedResultActionCount(): Int?
343+
344+
/**
345+
* Stores the given [count] completed generated password or username result actions taken
346+
* for the device.
347+
*/
348+
fun storeGeneratedResultActionCount(count: Int?)
349+
350+
/**
351+
* Gets the number of qualifying create send actions for the device.
352+
*/
353+
fun getCreateSendActionCount(): Int?
354+
355+
/**
356+
* Stores the given [count] completed create send actions for the device.
357+
*/
358+
fun storeCreateSendActionCount(count: Int?)
328359
}

app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
3737
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
3838
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
3939
private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
40+
private const val ADD_ACTION_COUNT = "addActionCount"
41+
private const val COPY_ACTION_COUNT = "copyActionCount"
42+
private const val CREATE_ACTION_COUNT = "createActionCount"
4043

4144
/**
4245
* Primary implementation of [SettingsDiskSource].
@@ -446,6 +449,39 @@ class SettingsDiskSourceImpl(
446449
getMutableVaultRegisteredForExportFlow(userId)
447450
.onSubscription { emit(getVaultRegisteredForExport(userId)) }
448451

452+
override fun getAddCipherActionCount(): Int? = getInt(
453+
key = ADD_ACTION_COUNT,
454+
)
455+
456+
override fun storeAddCipherActionCount(count: Int?) {
457+
putInt(
458+
key = ADD_ACTION_COUNT,
459+
value = count,
460+
)
461+
}
462+
463+
override fun getGeneratedResultActionCount(): Int? = getInt(
464+
key = COPY_ACTION_COUNT,
465+
)
466+
467+
override fun storeGeneratedResultActionCount(count: Int?) {
468+
putInt(
469+
key = COPY_ACTION_COUNT,
470+
value = count,
471+
)
472+
}
473+
474+
override fun getCreateSendActionCount(): Int? = getInt(
475+
key = CREATE_ACTION_COUNT,
476+
)
477+
478+
override fun storeCreateSendActionCount(count: Int?) {
479+
putInt(
480+
key = CREATE_ACTION_COUNT,
481+
value = count,
482+
)
483+
}
484+
449485
private fun getMutableLastSyncFlow(
450486
userId: String,
451487
): MutableSharedFlow<Instant?> =
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.x8bit.bitwarden.data.platform.manager
2+
3+
/**
4+
* Responsible for managing whether or not the app review prompt should be shown.
5+
*/
6+
interface ReviewPromptManager {
7+
/**
8+
* Register an add cipher item action.
9+
*/
10+
fun registerAddCipherAction()
11+
12+
/**
13+
* Register a generated result action.
14+
*/
15+
fun registerGeneratedResultAction()
16+
17+
/**
18+
* Register a create send action.
19+
*/
20+
fun registerCreateSendAction()
21+
22+
/**
23+
* Returns a boolean value indicating whether or not the user should be prompted to
24+
* review the app.
25+
*/
26+
fun shouldPromptForAppReview(): Boolean
27+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.x8bit.bitwarden.data.platform.manager
2+
3+
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
4+
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
5+
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
6+
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
7+
import com.x8bit.bitwarden.ui.platform.util.orZero
8+
9+
private const val ADD_ACTION_REQUIREMENT = 3
10+
private const val COPY_ACTION_REQUIREMENT = 3
11+
private const val CREATE_ACTION_REQUIREMENT = 3
12+
13+
/**
14+
* Default implementation of [ReviewPromptManager].
15+
*/
16+
class ReviewPromptManagerImpl(
17+
private val authDiskSource: AuthDiskSource,
18+
private val settingsDiskSource: SettingsDiskSource,
19+
private val autofillEnabledManager: AutofillEnabledManager,
20+
private val accessibilityEnabledManager: AccessibilityEnabledManager,
21+
) : ReviewPromptManager {
22+
23+
override fun registerAddCipherAction() {
24+
authDiskSource.userState?.activeUserId ?: return
25+
if (isMinimumAddActionsMet()) return
26+
val currentValue = settingsDiskSource.getAddCipherActionCount().orZero()
27+
settingsDiskSource.storeAddCipherActionCount(
28+
count = currentValue + 1,
29+
)
30+
}
31+
32+
override fun registerGeneratedResultAction() {
33+
authDiskSource.userState?.activeUserId ?: return
34+
if (isMinimumCopyActionsMet()) return
35+
val currentValue = settingsDiskSource
36+
.getGeneratedResultActionCount()
37+
.orZero()
38+
settingsDiskSource.storeGeneratedResultActionCount(
39+
count = currentValue + 1,
40+
)
41+
}
42+
43+
override fun registerCreateSendAction() {
44+
authDiskSource.userState?.activeUserId ?: return
45+
if (isMinimumCreateActionsMet()) return
46+
val currentValue = settingsDiskSource.getCreateSendActionCount().orZero()
47+
settingsDiskSource.storeCreateSendActionCount(
48+
count = currentValue + 1,
49+
)
50+
}
51+
52+
override fun shouldPromptForAppReview(): Boolean {
53+
authDiskSource.userState?.activeUserId ?: return false
54+
val autofillEnabled = autofillEnabledManager.isAutofillEnabledStateFlow.value
55+
val accessibilityEnabled = accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value
56+
val minAddActionsMet = isMinimumAddActionsMet()
57+
val minCopyActionsMet = isMinimumCopyActionsMet()
58+
val minCreateActionsMet = isMinimumCreateActionsMet()
59+
return (autofillEnabled || accessibilityEnabled) &&
60+
(minAddActionsMet || minCopyActionsMet || minCreateActionsMet)
61+
}
62+
63+
private fun isMinimumAddActionsMet(): Boolean =
64+
settingsDiskSource.getAddCipherActionCount().orZero() >= ADD_ACTION_REQUIREMENT
65+
66+
private fun isMinimumCopyActionsMet(): Boolean =
67+
settingsDiskSource
68+
.getGeneratedResultActionCount()
69+
.orZero() >= COPY_ACTION_REQUIREMENT
70+
71+
private fun isMinimumCreateActionsMet(): Boolean =
72+
settingsDiskSource.getCreateSendActionCount().orZero() >= CREATE_ACTION_REQUIREMENT
73+
}

app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.core.content.getSystemService
66
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
77
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
88
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
9+
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
910
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
1011
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
1112
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
@@ -39,6 +40,8 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
3940
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
4041
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
4142
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManagerImpl
43+
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
44+
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManagerImpl
4245
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
4346
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
4447
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
@@ -310,4 +313,18 @@ object PlatformManagerModule {
310313
authDiskSource = authDiskSource,
311314
settingsDiskSource = settingsDiskSource,
312315
)
316+
317+
@Provides
318+
@Singleton
319+
fun provideReviewPromptManager(
320+
authDiskSource: AuthDiskSource,
321+
settingsDiskSource: SettingsDiskSource,
322+
autofillEnabledManager: AutofillEnabledManager,
323+
accessibilityEnabledManager: AccessibilityEnabledManager,
324+
): ReviewPromptManager = ReviewPromptManagerImpl(
325+
authDiskSource = authDiskSource,
326+
settingsDiskSource = settingsDiskSource,
327+
autofillEnabledManager = autofillEnabledManager,
328+
accessibilityEnabledManager = accessibilityEnabledManager,
329+
)
313330
}

app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryImpl.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.bitwarden.generators.PasswordGeneratorRequest
77
import com.bitwarden.generators.UsernameGeneratorRequest
88
import com.bitwarden.vault.PasswordHistoryView
99
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
10-
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
10+
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
1111
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
1212
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
1313
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
@@ -54,7 +54,7 @@ class GeneratorRepositoryImpl(
5454
private val authDiskSource: AuthDiskSource,
5555
private val vaultSdkSource: VaultSdkSource,
5656
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
57-
private val policyManager: PolicyManager,
57+
private val reviewPromptManager: ReviewPromptManager,
5858
dispatcherManager: DispatcherManager,
5959
) : GeneratorRepository {
6060

@@ -69,7 +69,9 @@ class GeneratorRepositoryImpl(
6969
get() = mutablePasswordHistoryStateFlow.asStateFlow()
7070

7171
override val generatorResultFlow: Flow<GeneratorResult>
72-
get() = generatorResultChannel.receiveAsFlow()
72+
get() = generatorResultChannel
73+
.receiveAsFlow()
74+
.onEach { reviewPromptManager.registerGeneratedResultAction() }
7375

7476
init {
7577
mutablePasswordHistoryStateFlow

app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.x8bit.bitwarden.data.tools.generator.repository.di
22

33
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
4-
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
4+
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
55
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
66
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
77
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
@@ -33,7 +33,7 @@ object GeneratorRepositoryModule {
3333
vaultSdkSource: VaultSdkSource,
3434
passwordHistoryDiskSource: PasswordHistoryDiskSource,
3535
dispatcherManager: DispatcherManager,
36-
policyManager: PolicyManager,
36+
reviewPromptManager: ReviewPromptManager,
3737
): GeneratorRepository = GeneratorRepositoryImpl(
3838
clock = clock,
3939
generatorSdkSource = generatorSdkSource,
@@ -42,6 +42,6 @@ object GeneratorRepositoryModule {
4242
vaultSdkSource = vaultSdkSource,
4343
passwordHistoryDiskSource = passwordHistoryDiskSource,
4444
dispatcherManager = dispatcherManager,
45-
policyManager = policyManager,
45+
reviewPromptManager = reviewPromptManager,
4646
)
4747
}

app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.bitwarden.vault.AttachmentView
66
import com.bitwarden.vault.Cipher
77
import com.bitwarden.vault.CipherView
88
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
9+
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
910
import com.x8bit.bitwarden.data.platform.util.asFailure
1011
import com.x8bit.bitwarden.data.platform.util.asSuccess
1112
import com.x8bit.bitwarden.data.platform.util.flatMap
@@ -35,14 +36,15 @@ import java.time.Clock
3536
/**
3637
* The default implementation of the [CipherManager].
3738
*/
38-
@Suppress("TooManyFunctions")
39+
@Suppress("TooManyFunctions", "LongParameterList")
3940
class CipherManagerImpl(
4041
private val fileManager: FileManager,
4142
private val authDiskSource: AuthDiskSource,
4243
private val ciphersService: CiphersService,
4344
private val vaultDiskSource: VaultDiskSource,
4445
private val vaultSdkSource: VaultSdkSource,
4546
private val clock: Clock,
47+
private val reviewPromptManager: ReviewPromptManager,
4648
) : CipherManager {
4749
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
4850

@@ -57,7 +59,10 @@ class CipherManagerImpl(
5759
.onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) }
5860
.fold(
5961
onFailure = { CreateCipherResult.Error },
60-
onSuccess = { CreateCipherResult.Success },
62+
onSuccess = {
63+
reviewPromptManager.registerAddCipherAction()
64+
CreateCipherResult.Success
65+
},
6166
)
6267
}
6368

@@ -87,7 +92,10 @@ class CipherManagerImpl(
8792
}
8893
.fold(
8994
onFailure = { CreateCipherResult.Error },
90-
onSuccess = { CreateCipherResult.Success },
95+
onSuccess = {
96+
reviewPromptManager.registerAddCipherAction()
97+
CreateCipherResult.Success
98+
},
9199
)
92100
}
93101

app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
66
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
77
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
88
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
9+
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
910
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
1011
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
1112
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@@ -44,13 +45,15 @@ object VaultManagerModule {
4445
authDiskSource: AuthDiskSource,
4546
fileManager: FileManager,
4647
clock: Clock,
48+
reviewPromptManager: ReviewPromptManager,
4749
): CipherManager = CipherManagerImpl(
4850
fileManager = fileManager,
4951
authDiskSource = authDiskSource,
5052
ciphersService = ciphersService,
5153
vaultDiskSource = vaultDiskSource,
5254
vaultSdkSource = vaultSdkSource,
5355
clock = clock,
56+
reviewPromptManager = reviewPromptManager,
5457
)
5558

5659
@Provides

app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
2323
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
2424
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
2525
import com.x8bit.bitwarden.data.platform.manager.PushManager
26+
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
2627
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
2728
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
2829
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
@@ -144,6 +145,7 @@ class VaultRepositoryImpl(
144145
pushManager: PushManager,
145146
private val clock: Clock,
146147
dispatcherManager: DispatcherManager,
148+
private val reviewPromptManager: ReviewPromptManager,
147149
) : VaultRepository,
148150
CipherManager by cipherManager,
149151
VaultLockManager by vaultLockManager {
@@ -632,7 +634,10 @@ class VaultRepositoryImpl(
632634
}
633635
.fold(
634636
onFailure = { CreateSendResult.Error(message = null) },
635-
onSuccess = { CreateSendResult.Success(it) },
637+
onSuccess = {
638+
reviewPromptManager.registerCreateSendAction()
639+
CreateSendResult.Success(it)
640+
},
636641
)
637642
}
638643

0 commit comments

Comments
 (0)