Analytics: ask user consent at startup (we may iterate later)

This commit is contained in:
Benoit Marty 2021-11-23 14:32:20 +01:00 committed by Benoit Marty
parent b68e9e1f7f
commit 5606a5bfe7
7 changed files with 174 additions and 1 deletions

View file

@ -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)

View file

@ -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()
}

View file

@ -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<AnalyticsConsentViewState, AnalyticsConsentViewActions, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<AnalyticsConsentViewModel, AnalyticsConsentViewState> {
override fun create(initialState: AnalyticsConsentViewState): AnalyticsConsentViewModel
}
companion object : MavericksViewModelFactory<AnalyticsConsentViewModel, AnalyticsConsentViewState> 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)
}
}
}

View file

@ -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
}
}

View file

@ -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<FragmentLoginSplashBinding>() {
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))
}

View file

@ -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" />
<CheckBox
android:id="@+id/loginSplashAnalyticsConsent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/analytics_consent_splash"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1377,6 +1377,9 @@
<string name="template_settings_opt_in_of_analytics_prompt">Please enable analytics to help us improve ${app_name}.</string>
<string name="settings_opt_in_of_analytics_ok">Yes, I want to help!</string>
<!-- analytics v2 -->
<string name="analytics_consent_splash">Send anonymous usage data to element.io</string>
<string name="settings_data_save_mode">Data save mode</string>
<string name="settings_data_save_mode_summary">Data save mode applies a specific filter so presence updates and typing notifications are filtered out.</string>