Merge pull request #5009 from vector-im/feature/adm/storing-use-case

Storing and tracking the onboarding messaging use case
This commit is contained in:
Adam Brown 2022-01-31 14:15:39 +00:00 committed by GitHub
commit 986d9f92e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 142 additions and 14 deletions

View File

@ -21,6 +21,7 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import im.vector.app.core.services.VectorSyncService import im.vector.app.core.services.VectorSyncService
import im.vector.app.features.session.VectorSessionStore
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.FilterService
@ -76,3 +77,5 @@ fun Session.cannotLogoutSafely(): Boolean {
// That are not backed up // That are not backed up
!sharedSecretStorageService.isRecoverySetup()) !sharedSecretStorageService.isRecoverySetup())
} }
fun Session.vectorStore(context: Context) = VectorSessionStore(context, myUserId)

View File

@ -29,6 +29,7 @@ import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.extensions.vectorStore
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.deleteAllFiles import im.vector.app.core.utils.deleteAllFiles
import im.vector.app.databinding.ActivityMainBinding import im.vector.app.databinding.ActivityMainBinding
@ -40,6 +41,7 @@ import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.PinLocker import im.vector.app.features.pin.PinLocker
import im.vector.app.features.pin.UnlockedActivity import im.vector.app.features.pin.UnlockedActivity
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.session.VectorSessionStore
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.signout.hard.SignedOutActivity
import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ActivityOtherThemes
@ -143,13 +145,15 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
startNextActivityAndFinish() startNextActivityAndFinish()
return return
} }
val onboardingStore = session.vectorStore(this)
when { when {
args.isAccountDeactivated -> { args.isAccountDeactivated -> {
lifecycleScope.launch { lifecycleScope.launch {
// Just do the local cleanup // Just do the local cleanup
Timber.w("Account deactivated, start app") Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession() sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true) doLocalCleanup(clearPreferences = true, onboardingStore)
startNextActivityAndFinish() startNextActivityAndFinish()
} }
} }
@ -163,14 +167,14 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
} }
Timber.w("SIGN_OUT: success, start app") Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession() sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true) doLocalCleanup(clearPreferences = true, onboardingStore)
startNextActivityAndFinish() startNextActivityAndFinish()
} }
} }
args.clearCache -> { args.clearCache -> {
lifecycleScope.launch { lifecycleScope.launch {
session.clearCache() session.clearCache()
doLocalCleanup(clearPreferences = false) doLocalCleanup(clearPreferences = false, onboardingStore)
session.startSyncing(applicationContext) session.startSyncing(applicationContext)
startNextActivityAndFinish() startNextActivityAndFinish()
} }
@ -183,7 +187,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
Timber.w("Ignoring invalid token global error") Timber.w("Ignoring invalid token global error")
} }
private suspend fun doLocalCleanup(clearPreferences: Boolean) { private suspend fun doLocalCleanup(clearPreferences: Boolean, vectorSessionStore: VectorSessionStore) {
// On UI Thread // On UI Thread
Glide.get(this@MainActivity).clearMemory() Glide.get(this@MainActivity).clearMemory()
@ -193,6 +197,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
pinLocker.unlock() pinLocker.unlock()
pinCodeStore.deleteEncodedPin() pinCodeStore.deleteEncodedPin()
vectorAnalytics.onSignOut() vectorAnalytics.onSignOut()
vectorSessionStore.clear()
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// On BG thread // On BG thread

View File

@ -18,6 +18,7 @@ package im.vector.app.features.analytics
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.plan.Identity
interface AnalyticsTracker { interface AnalyticsTracker {
/** /**
@ -29,4 +30,9 @@ interface AnalyticsTracker {
* Track a displayed screen * Track a displayed screen
*/ */
fun screen(screen: VectorAnalyticsScreen) fun screen(screen: VectorAnalyticsScreen)
/**
* Update user specific properties
*/
fun updateUserProperties(identity: Identity)
} }

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 im.vector.app.features.analytics.extensions
import im.vector.app.features.analytics.plan.Identity
import im.vector.app.features.onboarding.FtueUseCase
fun FtueUseCase.toTrackingValue(): Identity.FtueUseCaseSelection {
return when (this) {
FtueUseCase.FRIENDS_FAMILY -> Identity.FtueUseCaseSelection.PersonalMessaging
FtueUseCase.TEAMS -> Identity.FtueUseCaseSelection.WorkMessaging
FtueUseCase.COMMUNITIES -> Identity.FtueUseCaseSelection.CommunityMessaging
FtueUseCase.SKIP -> Identity.FtueUseCaseSelection.Skip
}
}

View File

@ -17,6 +17,7 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import android.content.Context import android.content.Context
import com.posthog.android.Options
import com.posthog.android.PostHog import com.posthog.android.PostHog
import com.posthog.android.Properties import com.posthog.android.Properties
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
@ -25,6 +26,7 @@ import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.log.analyticsTag import im.vector.app.features.analytics.log.analyticsTag
import im.vector.app.features.analytics.plan.Identity
import im.vector.app.features.analytics.store.AnalyticsStore import im.vector.app.features.analytics.store.AnalyticsStore
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -34,6 +36,9 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private val REUSE_EXISTING_ID: String? = null
private val IGNORED_OPTIONS: Options? = null
@Singleton @Singleton
class DefaultVectorAnalytics @Inject constructor( class DefaultVectorAnalytics @Inject constructor(
private val context: Context, private val context: Context,
@ -170,6 +175,10 @@ class DefaultVectorAnalytics @Inject constructor(
?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties()) ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties())
} }
override fun updateUserProperties(identity: Identity) {
posthog?.identify(REUSE_EXISTING_ID, identity.getProperties().toPostHogProperties(), IGNORED_OPTIONS)
}
private fun Map<String, Any>?.toPostHogProperties(): Properties? { private fun Map<String, Any>?.toPostHogProperties(): Properties? {
if (this == null) return null if (this == null) return null

View File

@ -16,9 +16,13 @@
package im.vector.app.features.onboarding package im.vector.app.features.onboarding
enum class FtueUseCase { enum class FtueUseCase(val persistableValue: String) {
FRIENDS_FAMILY, FRIENDS_FAMILY("friends_family"),
TEAMS, TEAMS("teams"),
COMMUNITIES, COMMUNITIES("communities"),
SKIP SKIP("skip");
companion object {
fun from(persistedValue: String) = values().first { it.persistableValue == persistedValue }
}
} }

View File

@ -32,10 +32,14 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.vectorStore
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.core.utils.ensureTrailingSlash
import im.vector.app.features.VectorFeatures import im.vector.app.features.VectorFeatures
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toTrackingValue
import im.vector.app.features.analytics.plan.Identity
import im.vector.app.features.login.HomeServerConnectionConfigFactory import im.vector.app.features.login.HomeServerConnectionConfigFactory
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
@ -73,7 +77,8 @@ class OnboardingViewModel @AssistedInject constructor(
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService, private val homeServerHistoryService: HomeServerHistoryService,
private val vectorFeatures: VectorFeatures private val vectorFeatures: VectorFeatures,
private val analyticsTracker: AnalyticsTracker
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) { ) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -125,7 +130,7 @@ class OnboardingViewModel @AssistedInject constructor(
when (action) { when (action) {
is OnboardingAction.OnGetStarted -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow) is OnboardingAction.OnGetStarted -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow) is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
is OnboardingAction.UpdateUseCase -> handleUpdateUseCase() is OnboardingAction.UpdateUseCase -> handleUpdateUseCase(action)
OnboardingAction.ResetUseCase -> resetUseCase() OnboardingAction.ResetUseCase -> resetUseCase()
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action) is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action) is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
@ -458,13 +463,15 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun handleUpdateUseCase() { private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) {
// TODO act on the use case selection setState { copy(useCase = action.useCase) }
analyticsTracker.updateUserProperties(Identity(ftueUseCaseSelection = action.useCase.toTrackingValue()))
_viewEvents.post(OnboardingViewEvents.OpenServerSelection) _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
} }
private fun resetUseCase() { private fun resetUseCase() {
// TODO remove stored use case setState { copy(useCase = null) }
analyticsTracker.updateUserProperties(Identity(ftueUseCaseSelection = null))
} }
private fun handleUpdateServerType(action: OnboardingAction.UpdateServerType) { private fun handleUpdateServerType(action: OnboardingAction.UpdateServerType) {
@ -745,6 +752,9 @@ class OnboardingViewModel @AssistedInject constructor(
} }
private suspend fun onSessionCreated(session: Session) { private suspend fun onSessionCreated(session: Session) {
awaitState().useCase?.let { useCase ->
session.vectorStore(applicationContext).setUseCase(useCase)
}
activeSessionHolder.setActiveSession(session) activeSessionHolder.setActiveSession(session)
authenticationService.reset() authenticationService.reset()

View File

@ -40,6 +40,8 @@ data class OnboardingViewState(
@PersistState @PersistState
val serverType: ServerType = ServerType.Unknown, val serverType: ServerType = ServerType.Unknown,
@PersistState @PersistState
val useCase: FtueUseCase? = null,
@PersistState
val signMode: SignMode = SignMode.Unknown, val signMode: SignMode = SignMode.Unknown,
@PersistState @PersistState
val resetPasswordEmail: String? = null, val resetPasswordEmail: String? = null,

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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 im.vector.app.features.session
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import im.vector.app.features.onboarding.FtueUseCase
import kotlinx.coroutines.flow.first
import org.matrix.android.sdk.internal.util.md5
/**
* Local storage for:
* - messaging use case (Enum/String)
*/
class VectorSessionStore constructor(
private val context: Context,
myUserId: String
) {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_session_store_${myUserId.md5()}")
private val useCaseKey = stringPreferencesKey("use_case")
suspend fun readUseCase() = context.dataStore.data.first().let { preferences ->
preferences[useCaseKey]?.let { FtueUseCase.from(it) }
}
suspend fun setUseCase(useCase: FtueUseCase) {
context.dataStore.edit { settings ->
settings[useCaseKey] = useCase.persistableValue
}
}
suspend fun resetUseCase() {
context.dataStore.edit { settings ->
settings.remove(useCaseKey)
}
}
suspend fun clear() {
context.dataStore.edit { settings -> settings.clear() }
}
}