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 c0a39e2c5a..adf1f63110 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.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel import im.vector.app.features.auth.ReAuthViewModel import im.vector.app.features.call.VectorCallViewModel @@ -460,6 +461,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(AnalyticsConsentViewModel::class) fun analyticsConsentViewModelFactory(factory: AnalyticsConsentViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(AnalyticsAccountDataViewModel::class) + fun analyticsAccountDataViewModelFactory(factory: AnalyticsAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataContent.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataContent.kt new file mode 100644 index 0000000000..ecb243dfc8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataContent.kt @@ -0,0 +1,35 @@ +/* + * 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.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AnalyticsAccountDataContent( + // A randomly generated analytics token for this user. + // This is suggested to be a 128-bit hex encoded string. + @Json(name = "id") + val id: String? = null, + // Boolean indicating whether the user has opted in. + // If null or not set, the user hasn't yet given consent either way + @Json(name = "pseudonymousAnalyticsOptIn") + val pseudonymousAnalyticsOptIn: Boolean? = null, + // Boolean indicating whether to show the analytics opt-in prompt. + @Json(name = "showPseudonymousAnalyticsPrompt") + val showPseudonymousAnalyticsPrompt: Boolean? = null +) diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt new file mode 100644 index 0000000000..665a26ce22 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt @@ -0,0 +1,118 @@ +/* + * 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.accountdata + +import androidx.lifecycle.asFlow +import com.airbnb.mvrx.MavericksState +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.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.VectorAnalytics +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.initsync.SyncStatusService +import org.matrix.android.sdk.flow.flow +import timber.log.Timber +import java.util.UUID + +data class DummyState( + val dummy: Boolean = false +) : MavericksState + +class AnalyticsAccountDataViewModel @AssistedInject constructor( + @Assisted initialState: DummyState, + private val session: Session, + private val analytics: VectorAnalytics +) : VectorViewModel(initialState) { + + private var checkDone: Boolean = false + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DummyState): AnalyticsAccountDataViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics"; + } + + init { + observeAccountData() + observeInitSync() + } + + private fun observeInitSync() { + combine( + session.getSyncStatusLive().asFlow(), + analytics.getUserConsent(), + analytics.getAnalyticsId() + ) { status, userConsent, analyticsId -> + if (status is SyncStatusService.Status.IncrementalSyncIdle && + userConsent && + analyticsId.isEmpty() && + !checkDone) { + // Initial sync is over, analytics Id from account data is missing and user has given consent to use analytics + checkDone = true + createAnalyticsAccountData() + } + }.launchIn(viewModelScope) + } + + private fun observeAccountData() { + session.flow() + .liveUserAccountData(setOf(ANALYTICS_EVENT_TYPE)) + .mapNotNull { it.firstOrNull() } + .mapNotNull { it.content.toModel() } + .onEach { analyticsAccountDataContent -> + if (analyticsAccountDataContent.id.isNullOrEmpty()) { + // Probably consent revoked from Element Web + // Ignore here + Timber.d("Consent revoked from Element Web?") + } else { + Timber.d("AnalyticsId has been retrieved") + analytics.setAnalyticsId(analyticsAccountDataContent.id) + } + } + .launchIn(viewModelScope) + } + + override fun handle(action: EmptyAction) { + // No op + } + + private fun createAnalyticsAccountData() { + val content = AnalyticsAccountDataContent( + id = UUID.randomUUID().toString() + ) + + viewModelScope.launch { + session.accountDataService().updateUserAccountData(ANALYTICS_EVENT_TYPE, content.toContent()) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 16c0655d85..b50a3a98a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -48,6 +48,7 @@ import im.vector.app.core.pushers.PushersManager import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.navigation.Navigator @@ -103,6 +104,8 @@ class HomeActivity : private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val homeActivityViewModel: HomeActivityViewModel by viewModel() + @Suppress("UNUSED") + private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel() private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel()