From 5606a5bfe7c975f45a26d3d181fe8af9006bb0b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 14:32:20 +0100 Subject: [PATCH] Analytics: ask user consent at startup (we may iterate later) --- .../app/core/di/MavericksViewModelModule.kt | 6 ++ .../ui/consent/AnalyticsConsentViewActions.kt | 24 ++++++ .../ui/consent/AnalyticsConsentViewModel.kt | 79 +++++++++++++++++++ .../ui/consent/AnalyticsConsentViewState.kt | 32 ++++++++ .../app/features/login/LoginSplashFragment.kt | 20 +++++ .../main/res/layout/fragment_login_splash.xml | 11 ++- vector/src/main/res/values/strings.xml | 3 + 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index cac694e84e..c0a39e2c5a 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -20,6 +20,7 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.multibindings.IntoMap +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel import im.vector.app.features.auth.ReAuthViewModel import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel @@ -454,6 +455,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(LoginViewModel::class) fun loginViewModelFactory(factory: LoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(AnalyticsConsentViewModel::class) + fun analyticsConsentViewModelFactory(factory: AnalyticsConsentViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt new file mode 100644 index 0000000000..b43dc4742a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt @@ -0,0 +1,24 @@ +/* + * 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.analytics.ui.consent + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class AnalyticsConsentViewActions : VectorViewModelAction { + data class SetUserConsent(val userConsent: Boolean) : AnalyticsConsentViewActions() + object OnGetStarted : AnalyticsConsentViewActions() +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt new file mode 100644 index 0000000000..00ed86b0ca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt @@ -0,0 +1,79 @@ +/* + * 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.analytics.ui.consent + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.VectorAnalytics +import kotlinx.coroutines.launch + +class AnalyticsConsentViewModel @AssistedInject constructor( + @Assisted initialState: AnalyticsConsentViewState, + private val analytics: VectorAnalytics +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: AnalyticsConsentViewState): AnalyticsConsentViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + observeAnalytics() + } + + private fun observeAnalytics() { + analytics.didAskUserConsent().setOnEach { + copy(didAskUserConsent = it) + } + analytics.getUserConsent().setOnEach { + copy(userConsent = it) + } + } + + override fun handle(action: AnalyticsConsentViewActions) { + when (action) { + is AnalyticsConsentViewActions.SetUserConsent -> handleSetUserConsent(action) + AnalyticsConsentViewActions.OnGetStarted -> handleOnScreenLeft() + }.exhaustive + } + + private fun handleSetUserConsent(action: AnalyticsConsentViewActions.SetUserConsent) { + viewModelScope.launch { + analytics.setUserConsent(action.userConsent) + if (!action.userConsent) { + // User explicitly changed the default value, let's avoid reverting to the default value + analytics.setDidAskUserConsent(true) + } + } + } + + private fun handleOnScreenLeft() { + // Whatever the state of the box, consider the user acknowledge it + viewModelScope.launch { + analytics.setDidAskUserConsent(true) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt new file mode 100644 index 0000000000..f7496710e2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt @@ -0,0 +1,32 @@ +/* + * 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.analytics.ui.consent + +import com.airbnb.mvrx.MavericksState + +data class AnalyticsConsentViewState( + val userConsent: Boolean = false, + val didAskUserConsent: Boolean = false +) : MavericksState { + val shouldCheckTheBox: Boolean = + if (didAskUserConsent) { + userConsent + } else { + // default value + true + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt index 527f9f99b3..d719ab6b31 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt @@ -22,10 +22,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import com.airbnb.mvrx.fragmentViewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.databinding.FragmentLoginSplashBinding +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.failure.Failure import java.net.UnknownHostException @@ -38,6 +42,8 @@ class LoginSplashFragment @Inject constructor( private val vectorPreferences: VectorPreferences ) : AbstractLoginFragment() { + private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel() + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplashBinding { return FragmentLoginSplashBinding.inflate(inflater, container, false) } @@ -46,10 +52,23 @@ class LoginSplashFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupViews() + observeAnalyticsState() + } + + private fun observeAnalyticsState() { + analyticsConsentViewModel.onEach(AnalyticsConsentViewState::shouldCheckTheBox) { + views.loginSplashAnalyticsConsent.isChecked = it + } } private fun setupViews() { views.loginSplashSubmit.debouncedClicks { getStarted() } + // setOnCheckedChangeListener is to annoying since it does not distinguish user changes and code changes + views.loginSplashAnalyticsConsent.setOnClickListener { + analyticsConsentViewModel.handle(AnalyticsConsentViewActions.SetUserConsent( + views.loginSplashAnalyticsConsent.isChecked + )) + } if (BuildConfig.DEBUG || vectorPreferences.developerMode()) { views.loginSplashVersion.isVisible = true @@ -61,6 +80,7 @@ class LoginSplashFragment @Inject constructor( } private fun getStarted() { + analyticsConsentViewModel.handle(AnalyticsConsentViewActions.OnGetStarted) loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = false)) } diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index 1d69cde723..90a33594c3 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -204,9 +204,18 @@ android:layout_height="wrap_content" android:textColor="?vctr_content_secondary" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/loginSplashAnalyticsConsent" app:layout_constraintStart_toStartOf="parent" tools:text="@string/settings_version" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ddb731a5e0..85e99edb1a 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1377,6 +1377,9 @@ Please enable analytics to help us improve ${app_name}. Yes, I want to help! + + Send anonymous usage data to element.io + Data save mode Data save mode applies a specific filter so presence updates and typing notifications are filtered out.