diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml
index 2da8e10542..5a5d8152ff 100644
--- a/.github/workflows/sync-from-external-sources.yml
+++ b/.github/workflows/sync-from-external-sources.yml
@@ -70,4 +70,27 @@ jobs:
body: |
- Update SAS Strings from matrix-doc.
branch: sync-sas-strings
+ base: develop
+
+ sync-analytics-plan:
+ runs-on: ubuntu-latest
+ # Skip in forks
+ if: github.repository == 'vector-im/element-android'
+ steps:
+ - uses: actions/checkout@v2
+ - name: Run analytics import script
+ run: ./tools/import_analytic_plan.sh
+ - name: Create Pull Request for analytics plan
+ uses: peter-evans/create-pull-request@v3
+ with:
+ commit-message: Sync analytics plan
+ title: Sync analytics plan
+ body: |
+ ### Update analytics plan
+ Reviewers:
+ - [ ] Please remove usage of Event or Enum which may have been removed or updated
+ - [ ] please ensure new Events or new Enums are used to send analytics by pushing new commit(s) to this PR.
+
+ *Note*: Change are coming from [this project](https://github.com/matrix-org/matrix-analytics-events)
+ branch: sync-analytics-plan
base: develop
\ No newline at end of file
diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index e143720aa9..a2e408b50d 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -24,6 +24,7 @@
pbkdf
pids
pkcs
+ posthog
previewable
previewables
pstn
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index c677290adc..25a78bc0c3 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -96,6 +96,7 @@ ext.groups = [
'com.parse.bolts',
'com.pinterest',
'com.pinterest.ktlint',
+ 'com.posthog.android',
'com.squareup',
'com.squareup.duktape',
'com.squareup.moshi',
diff --git a/docs/analytics.md b/docs/analytics.md
new file mode 100644
index 0000000000..135ace81b0
--- /dev/null
+++ b/docs/analytics.md
@@ -0,0 +1,16 @@
+# Analytics in Element
+
+## Solution
+
+Element is using PostHog to send analytics event.
+We ask for the user to give consent before sending any analytics data.
+
+## How to add a new Event
+
+The analytics plan is shared between all Element clients. To add an Event, please open a PR to this project: https://github.com/matrix-org/matrix-analytics-events
+
+Then, once the PR has been merged, you can run the tool `import_analytic_plan.sh` to import the plan to Element, and then you can use the new Event. Note that this tool is run by Github action once a week.
+
+## Forks of Element
+
+Analytics on forks are disabled by default. Please refer to AnalyticsConfig and there implementation to setup analytics on your project.
diff --git a/library/ui-styles/src/main/res/values-sw600dp/tablet.xml b/library/ui-styles/src/main/res/values-sw600dp/tablet.xml
new file mode 100644
index 0000000000..39f467cf0d
--- /dev/null
+++ b/library/ui-styles/src/main/res/values-sw600dp/tablet.xml
@@ -0,0 +1,6 @@
+
+
+
+ 0.6
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values-sw720dp/tablet.xml b/library/ui-styles/src/main/res/values-sw720dp/tablet.xml
new file mode 100644
index 0000000000..4afbe6c773
--- /dev/null
+++ b/library/ui-styles/src/main/res/values-sw720dp/tablet.xml
@@ -0,0 +1,6 @@
+
+
+
+ 0.5
+
+
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/tablet.xml b/library/ui-styles/src/main/res/values/tablet.xml
new file mode 100644
index 0000000000..a5df8fe17c
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/tablet.xml
@@ -0,0 +1,6 @@
+
+
+
+ 1
+
+
\ No newline at end of file
diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
index bbd6105b15..6ca86be095 100644
--- a/tools/check/forbidden_strings_in_code.txt
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
-enum class===110
+enum class===114
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3
diff --git a/tools/import_analytic_plan.sh b/tools/import_analytic_plan.sh
new file mode 100755
index 0000000000..9c020a8e37
--- /dev/null
+++ b/tools/import_analytic_plan.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+echo "Deleted existing plan..."
+rm vector/src/main/java/im/vector/app/features/analytics/plan/*.*
+
+echo "Cloning analytics project..."
+mkdir analytics_tmp
+cd analytics_tmp
+git clone https://github.com/matrix-org/matrix-analytics-events.git
+
+echo "Copy plan..."
+cp matrix-analytics-events/types/kotlin2/* ../vector/src/main/java/im/vector/app/features/analytics/plan/
+
+echo "Cleanup."
+cd ..
+rm -rf analytics_tmp
+
+echo "Done."
diff --git a/vector/build.gradle b/vector/build.gradle
index 4c864f9309..a578fdb52f 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -439,6 +439,9 @@ dependencies {
implementation libs.dagger.hilt
kapt libs.dagger.hiltCompiler
+ // Analytics
+ implementation 'com.posthog.android:posthog:1.1.2'
+
// gplay flavor only
gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml
index ceeb0353db..0b2b5cf90f 100644
--- a/vector/src/debug/AndroidManifest.xml
+++ b/vector/src/debug/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
diff --git a/vector/src/debug/java/im/vector/app/config/AnalyticsConfig.kt b/vector/src/debug/java/im/vector/app/config/AnalyticsConfig.kt
new file mode 100644
index 0000000000..34f2d4f92b
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/config/AnalyticsConfig.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.config
+
+import im.vector.app.BuildConfig
+import im.vector.app.features.analytics.AnalyticsConfig
+
+val analyticsConfig: AnalyticsConfig = object : AnalyticsConfig {
+ override val isEnabled = BuildConfig.APPLICATION_ID == "im.vector.app.debug"
+ override val postHogHost = "https://posthog-poc.lab.element.dev"
+ override val postHogApiKey = "rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8"
+ override val policyLink = "https://element.io/cookie-policy"
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt
index 4916ab1e5d..a2b2b44ce3 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt
@@ -34,6 +34,7 @@ import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityDebugMenuBinding
+import im.vector.app.features.debug.analytics.DebugAnalyticsActivity
import im.vector.app.features.debug.features.DebugFeaturesSettingsActivity
import im.vector.app.features.debug.sas.DebugSasEmojiActivity
import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity
@@ -79,6 +80,9 @@ class DebugMenuActivity : VectorBaseActivity() {
private fun setupViews() {
views.debugFeatures.setOnClickListener { startActivity(Intent(this, DebugFeaturesSettingsActivity::class.java)) }
views.debugPrivateSetting.setOnClickListener { openPrivateSettings() }
+ views.debugAnalytics.setOnClickListener {
+ startActivity(Intent(this, DebugAnalyticsActivity::class.java))
+ }
views.debugTestTextViewLink.setOnClickListener { testTextViewLink() }
views.debugOpenButtonStylesLight.setOnClickListener {
startActivity(Intent(this, DebugVectorButtonStylesLightActivity::class.java))
diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt
new file mode 100644
index 0000000000..61883251ce
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.debug.analytics
+
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.platform.VectorBaseActivity
+import im.vector.app.databinding.ActivitySimpleBinding
+
+@AndroidEntryPoint
+class DebugAnalyticsActivity : VectorBaseActivity() {
+
+ override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
+
+ override fun initUiAndData() {
+ if (isFirstCreation()) {
+ addFragment(
+ views.simpleFragmentContainer,
+ DebugAnalyticsFragment::class.java
+ )
+ }
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt
new file mode 100644
index 0000000000..eb23fe6383
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.debug.analytics
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
+import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.extensions.toOnOff
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentDebugAnalyticsBinding
+import me.gujun.android.span.span
+
+class DebugAnalyticsFragment : VectorBaseFragment() {
+
+ private val viewModel: DebugAnalyticsViewModel by fragmentViewModel()
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDebugAnalyticsBinding {
+ return FragmentDebugAnalyticsBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setViewListeners()
+ }
+
+ private fun setViewListeners() {
+ views.showAnalyticsOptIn.onClick {
+ navigator.openAnalyticsOptIn(requireContext())
+ }
+ views.resetAnalyticsOptInDisplayed.onClick {
+ viewModel.handle(DebugAnalyticsViewActions.ResetAnalyticsOptInDisplayed)
+ }
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ views.analyticsStoreContent.text = span {
+ +"AnalyticsId: "
+ span {
+ textStyle = "bold"
+ text = state.analyticsId.orEmpty()
+ }
+ +"\nOptIn: "
+ span {
+ textStyle = "bold"
+ text = state.userConsent.toOnOff()
+ }
+ +"\nDidAsk: "
+ span {
+ textStyle = "bold"
+ text = state.didAskUserConsent.toString()
+ }
+ }
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt
new file mode 100644
index 0000000000..e1a7ce36fd
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.debug.analytics
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed interface DebugAnalyticsViewActions : VectorViewModelAction {
+ object ResetAnalyticsOptInDisplayed : DebugAnalyticsViewActions
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt
new file mode 100644
index 0000000000..03e416813a
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.debug.analytics
+
+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.store.AnalyticsStore
+import kotlinx.coroutines.launch
+
+class DebugAnalyticsViewModel @AssistedInject constructor(
+ @Assisted initialState: DebugAnalyticsViewState,
+ private val analyticsStore: AnalyticsStore
+) : VectorViewModel(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: DebugAnalyticsViewState): DebugAnalyticsViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ init {
+ observerStore()
+ }
+
+ private fun observerStore() {
+ analyticsStore.analyticsIdFlow.setOnEach { copy(analyticsId = it) }
+ analyticsStore.userConsentFlow.setOnEach { copy(userConsent = it) }
+ analyticsStore.didAskUserConsentFlow.setOnEach { copy(didAskUserConsent = it) }
+ }
+
+ override fun handle(action: DebugAnalyticsViewActions) {
+ when (action) {
+ DebugAnalyticsViewActions.ResetAnalyticsOptInDisplayed -> handleResetAnalyticsOptInDisplayed()
+ }.exhaustive
+ }
+
+ private fun handleResetAnalyticsOptInDisplayed() {
+ viewModelScope.launch {
+ analyticsStore.setDidAskUserConsent(false)
+ }
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt
new file mode 100644
index 0000000000..8e7afb39ef
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.debug.analytics
+
+import com.airbnb.mvrx.MavericksState
+
+data class DebugAnalyticsViewState(
+ val analyticsId: String? = null,
+ val userConsent: Boolean = false,
+ val didAskUserConsent: Boolean = false
+) : MavericksState
diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt b/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt
index 8be4470b3f..6ef7fe441a 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt
@@ -23,12 +23,18 @@ import dagger.multibindings.IntoMap
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.MavericksViewModelComponent
import im.vector.app.core.di.MavericksViewModelKey
+import im.vector.app.features.debug.analytics.DebugAnalyticsViewModel
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewModel
@InstallIn(MavericksViewModelComponent::class)
@Module
interface MavericksViewModelDebugModule {
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(DebugAnalyticsViewModel::class)
+ fun debugAnalyticsViewModelFactory(factory: DebugAnalyticsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
@Binds
@IntoMap
@MavericksViewModelKey(DebugPrivateSettingsViewModel::class)
diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml
index 7aa69becde..8b38c17b35 100644
--- a/vector/src/debug/res/layout/activity_debug_menu.xml
+++ b/vector/src/debug/res/layout/activity_debug_menu.xml
@@ -32,6 +32,12 @@
android:layout_height="wrap_content"
android:text="Private settings" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 034f9c7619..93f4ac7632 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -298,6 +298,7 @@
+
diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html
index 5afee2389e..0eefa3b863 100755
--- a/vector/src/main/assets/open_source_licenses.html
+++ b/vector/src/main/assets/open_source_licenses.html
@@ -262,6 +262,15 @@ SOFTWARE.
+
+
+ posthog-android
+
+ https://github.com/PostHog/posthog-android
+ PostHog Android integration is licensed under the MIT License
+
+
+
Apache License
diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt
index 512f579785..400fb7eb89 100644
--- a/vector/src/main/java/im/vector/app/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/app/VectorApplication.kt
@@ -42,6 +42,7 @@ import dagger.hilt.android.HiltAndroidApp
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.startSyncing
+import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
@@ -96,6 +97,7 @@ class VectorApplication :
@Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var invitesAcceptor: InvitesAcceptor
@Inject lateinit var vectorFileLogger: VectorFileLogger
+ @Inject lateinit var vectorAnalytics: VectorAnalytics
// font thread handler
private var fontThreadHandler: Handler? = null
@@ -113,6 +115,7 @@ class VectorApplication :
enableStrictModeIfNeeded()
super.onCreate()
appContext = this
+ vectorAnalytics.init()
invitesAcceptor.initialize()
vectorUncaughtExceptionHandler.activate(this)
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index 43bb505a5e..ff84a46dab 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -24,6 +24,7 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.multibindings.IntoMap
+import im.vector.app.features.analytics.ui.consent.AnalyticsOptInFragment
import im.vector.app.features.attachments.preview.AttachmentsPreviewFragment
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
@@ -520,6 +521,11 @@ interface FragmentModule {
@FragmentKey(BreadcrumbsFragment::class)
fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment
+ @Binds
+ @IntoMap
+ @FragmentKey(AnalyticsOptInFragment::class)
+ fun bindAnalyticsOptInFragment(fragment: AnalyticsOptInFragment): Fragment
+
@Binds
@IntoMap
@FragmentKey(EmojiChooserFragment::class)
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 37721ca9f9..d09cd21d19 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,8 @@ 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
import im.vector.app.features.call.conference.JitsiCallViewModel
@@ -455,6 +457,16 @@ 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(AnalyticsAccountDataViewModel::class)
+ fun analyticsAccountDataViewModelFactory(factory: AnalyticsAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
@Binds
@IntoMap
@MavericksViewModelKey(HomeServerCapabilitiesViewModel::class)
diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt
index 52316751e6..0b9855ef56 100644
--- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt
+++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt
@@ -21,6 +21,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter
+import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator
@@ -55,6 +56,8 @@ interface SingletonEntryPoint {
fun pinLocker(): PinLocker
+ fun analytics(): VectorAnalytics
+
fun webRtcCallManager(): WebRtcCallManager
fun appCoroutineScope(): CoroutineScope
diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
index 350e1f6b7a..14ed17d0bb 100644
--- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
@@ -31,6 +31,8 @@ import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock
+import im.vector.app.features.analytics.VectorAnalytics
+import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
import im.vector.app.features.navigation.DefaultNavigator
@@ -57,6 +59,9 @@ abstract class VectorBindModule {
@Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
+ @Binds
+ abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
+
@Binds
abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
index 181bd8c6be..57a3f53373 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
@@ -65,6 +65,7 @@ import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
+import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.consent.ConsentNotGivenHelper
import im.vector.app.features.navigation.Navigator
@@ -132,6 +133,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
private lateinit var sessionListener: SessionListener
protected lateinit var bugReporter: BugReporter
private lateinit var pinLocker: PinLocker
+ protected lateinit var analytics: VectorAnalytics
@Inject
lateinit var rageShake: RageShake
@@ -187,6 +189,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java)
bugReporter = singletonEntryPoint.bugReporter()
pinLocker = singletonEntryPoint.pinLocker()
+ analytics = singletonEntryPoint.analytics()
navigator = singletonEntryPoint.navigator()
activeSessionHolder = singletonEntryPoint.activeSessionHolder()
vectorPreferences = singletonEntryPoint.vectorPreferences()
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
index e441efe684..69c525dbde 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
@@ -34,8 +34,10 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.EntryPointAccessors
import im.vector.app.core.di.ActivityEntryPoint
+import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.analytics.VectorAnalytics
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
@@ -82,6 +84,8 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe
open val showExpanded = false
+ protected lateinit var analytics: VectorAnalytics
+
interface ResultListener {
fun onBottomSheetResult(resultCode: Int, data: Any?)
@@ -119,6 +123,8 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe
override fun onAttach(context: Context) {
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
viewModelFactory = activityEntryPoint.viewModelFactory()
+ val singletonEntryPoint = context.singletonEntryPoint()
+ analytics = singletonEntryPoint.analytics()
super.onAttach(context)
}
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
index 9f156f937c..eab1101064 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
@@ -42,6 +42,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle
+import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
import kotlinx.coroutines.flow.launchIn
@@ -60,6 +61,7 @@ abstract class VectorBaseFragment : Fragment(), MavericksView
* ========================================================================================== */
protected lateinit var navigator: Navigator
+ protected lateinit var analytics: VectorAnalytics
protected lateinit var errorFormatter: ErrorFormatter
protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog
@@ -96,6 +98,7 @@ abstract class VectorBaseFragment : Fragment(), MavericksView
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
navigator = singletonEntryPoint.navigator()
errorFormatter = singletonEntryPoint.errorFormatter()
+ analytics = singletonEntryPoint.analytics()
unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog()
viewModelFactory = activityEntryPoint.viewModelFactory()
childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory()
diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt
index d3d51c3dbf..5f9c5433fe 100644
--- a/vector/src/main/java/im/vector/app/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt
@@ -32,6 +32,7 @@ import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.deleteAllFiles
import im.vector.app.databinding.ActivityMainBinding
+import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.ShortcutsHandler
import im.vector.app.features.notifications.NotificationDrawerManager
@@ -96,6 +97,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity
@Inject lateinit var pinCodeStore: PinCodeStore
@Inject lateinit var pinLocker: PinLocker
@Inject lateinit var popupAlertManager: PopupAlertManager
+ @Inject lateinit var vectorAnalytics: VectorAnalytics
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -190,6 +192,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity
uiStateRepository.reset()
pinLocker.unlock()
pinCodeStore.deleteEncodedPin()
+ vectorAnalytics.onSignOut()
}
withContext(Dispatchers.IO) {
// On BG thread
diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt
new file mode 100644
index 0000000000..46119f5563
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.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
+
+interface AnalyticsConfig {
+ val isEnabled: Boolean
+ val postHogHost: String
+ val postHogApiKey: String
+ val policyLink: String
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt
new file mode 100644
index 0000000000..476f5ade56
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt
@@ -0,0 +1,73 @@
+/*
+ * 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
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
+import kotlinx.coroutines.flow.Flow
+
+interface VectorAnalytics {
+ /**
+ * Return a Flow of Boolean, true if the user has given their consent
+ */
+ fun getUserConsent(): Flow
+
+ /**
+ * Update the user consent value
+ */
+ suspend fun setUserConsent(userConsent: Boolean)
+
+ /**
+ * Return a Flow of Boolean, true if the user has been asked for their consent
+ */
+ fun didAskUserConsent(): Flow
+
+ /**
+ * Store the fact that the user has been asked for their consent
+ */
+ suspend fun setDidAskUserConsent()
+
+ /**
+ * Return a Flow of String, used for analytics Id
+ */
+ fun getAnalyticsId(): Flow
+
+ /**
+ * Update analyticsId from the AccountData
+ */
+ suspend fun setAnalyticsId(analyticsId: String)
+
+ /**
+ * To be called when a session is destroyed
+ */
+ suspend fun onSignOut()
+
+ /**
+ * To be called when application is started
+ */
+ fun init()
+
+ /**
+ * Capture an Event
+ */
+ fun capture(event: VectorAnalyticsEvent)
+
+ /**
+ * Track a displayed screen
+ */
+ fun screen(screen: VectorAnalyticsScreen)
+}
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..5d65d7ea42
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt
@@ -0,0 +1,119 @@
+/*
+ * 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 im.vector.app.features.analytics.log.analyticsTag
+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.tag(analyticsTag.value).d("Consent revoked from Element Web?")
+ } else {
+ Timber.tag(analyticsTag.value).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/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
new file mode 100644
index 0000000000..eaf2e42705
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.impl
+
+import android.content.Context
+import com.posthog.android.PostHog
+import com.posthog.android.Properties
+import im.vector.app.BuildConfig
+import im.vector.app.config.analyticsConfig
+import im.vector.app.features.analytics.VectorAnalytics
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
+import im.vector.app.features.analytics.log.analyticsTag
+import im.vector.app.features.analytics.store.AnalyticsStore
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class DefaultVectorAnalytics @Inject constructor(
+ private val context: Context,
+ private val analyticsStore: AnalyticsStore
+) : VectorAnalytics {
+ private var posthog: PostHog? = null
+
+ // Cache for the store values
+ private var userConsent: Boolean? = null
+ private var analyticsId: String? = null
+
+ override fun getUserConsent(): Flow {
+ return analyticsStore.userConsentFlow
+ }
+
+ override suspend fun setUserConsent(userConsent: Boolean) {
+ Timber.tag(analyticsTag.value).d("setUserConsent($userConsent)")
+ analyticsStore.setUserConsent(userConsent)
+ }
+
+ override fun didAskUserConsent(): Flow {
+ return analyticsStore.didAskUserConsentFlow
+ }
+
+ override suspend fun setDidAskUserConsent() {
+ Timber.tag(analyticsTag.value).d("setDidAskUserConsent()")
+ analyticsStore.setDidAskUserConsent()
+ }
+
+ override fun getAnalyticsId(): Flow {
+ return analyticsStore.analyticsIdFlow
+ }
+
+ override suspend fun setAnalyticsId(analyticsId: String) {
+ Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)")
+ analyticsStore.setAnalyticsId(analyticsId)
+ }
+
+ override suspend fun onSignOut() {
+ // reset the analyticsId
+ setAnalyticsId("")
+ }
+
+ override fun init() {
+ observeUserConsent()
+ observeAnalyticsId()
+ createAnalyticsClient()
+ }
+
+ @Suppress("EXPERIMENTAL_API_USAGE")
+ private fun observeAnalyticsId() {
+ getAnalyticsId()
+ .onEach { id ->
+ Timber.tag(analyticsTag.value).d("Analytics Id updated to '$id'")
+ analyticsId = id
+ identifyPostHog()
+ }
+ .launchIn(GlobalScope)
+ }
+
+ private fun identifyPostHog() {
+ val id = analyticsId ?: return
+ if (id.isEmpty()) {
+ Timber.tag(analyticsTag.value).d("reset")
+ posthog?.reset()
+ } else {
+ Timber.tag(analyticsTag.value).d("identify")
+ posthog?.identify(id)
+ }
+ }
+
+ @Suppress("EXPERIMENTAL_API_USAGE")
+ private fun observeUserConsent() {
+ getUserConsent()
+ .onEach { consent ->
+ Timber.tag(analyticsTag.value).d("User consent updated to $consent")
+ userConsent = consent
+ optOutPostHog()
+ }
+ .launchIn(GlobalScope)
+ }
+
+ private fun optOutPostHog() {
+ userConsent?.let { posthog?.optOut(!it) }
+ }
+
+ private fun createAnalyticsClient() {
+ Timber.tag(analyticsTag.value).d("createAnalyticsClient()")
+
+ if (analyticsConfig.isEnabled.not()) {
+ Timber.tag(analyticsTag.value).w("Analytics is disabled")
+ return
+ }
+
+ posthog = PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
+ // Record certain application events automatically! (off/false by default)
+ // .captureApplicationLifecycleEvents()
+ // Record screen views automatically! (off/false by default)
+ // .recordScreenViews()
+ // Capture deep links as part of the screen call. (off by default)
+ // .captureDeepLinks()
+ // Maximum number of events to keep in queue before flushing (default 20)
+ // .flushQueueSize(20)
+ // Max delay before flushing the queue (30 seconds)
+ // .flushInterval(30, TimeUnit.SECONDS)
+ // Enable or disable collection of ANDROID_ID (true)
+ .collectDeviceId(false)
+ .logLevel(getLogLevel())
+ .build()
+
+ optOutPostHog()
+ identifyPostHog()
+ }
+
+ private fun getLogLevel(): PostHog.LogLevel {
+ return if (BuildConfig.DEBUG) {
+ PostHog.LogLevel.DEBUG
+ } else {
+ PostHog.LogLevel.INFO
+ }
+ }
+
+ override fun capture(event: VectorAnalyticsEvent) {
+ Timber.tag(analyticsTag.value).d("capture($event)")
+ posthog
+ ?.takeIf { userConsent == true }
+ ?.capture(event.getName(), event.getProperties()?.toPostHogProperties())
+ }
+
+ override fun screen(screen: VectorAnalyticsScreen) {
+ Timber.tag(analyticsTag.value).d("screen($screen)")
+ posthog
+ ?.takeIf { userConsent == true }
+ ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties())
+ }
+
+ private fun Map?.toPostHogProperties(): Properties? {
+ if (this == null) return null
+
+ return Properties().apply {
+ putAll(this@toPostHogProperties)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt
new file mode 100644
index 0000000000..c6acb3b87a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.itf
+
+interface VectorAnalyticsEvent {
+ fun getName(): String
+ fun getProperties(): Map?
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsScreen.kt b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsScreen.kt
new file mode 100644
index 0000000000..7056814aaf
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsScreen.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.itf
+
+interface VectorAnalyticsScreen {
+ fun getName(): String
+ fun getProperties(): Map?
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/log/AnalyticsLoggerTag.kt b/vector/src/main/java/im/vector/app/features/analytics/log/AnalyticsLoggerTag.kt
new file mode 100644
index 0000000000..360740b9ce
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/log/AnalyticsLoggerTag.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.log
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+
+val analyticsTag = LoggerTag("Analytics")
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt
new file mode 100644
index 0000000000..cd813325f1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when a call has ended.
+ */
+data class CallEnded(
+ /**
+ * The duration of the call in milliseconds.
+ */
+ val durationMs: Int,
+ /**
+ * Whether its a video call or not.
+ */
+ val isVideo: Boolean,
+ /**
+ * Number of participants in the call.
+ */
+ val numParticipants: Int,
+ /**
+ * Whether this user placed it.
+ */
+ val placed: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CallEnded"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("durationMs", durationMs)
+ put("isVideo", isVideo)
+ put("numParticipants", numParticipants)
+ put("placed", placed)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt
new file mode 100644
index 0000000000..18e77f9f1c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when an error occurred in a call.
+ */
+data class CallError(
+ /**
+ * Whether its a video call or not.
+ */
+ val isVideo: Boolean,
+ /**
+ * Number of participants in the call.
+ */
+ val numParticipants: Int,
+ /**
+ * Whether this user placed it.
+ */
+ val placed: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CallError"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isVideo", isVideo)
+ put("numParticipants", numParticipants)
+ put("placed", placed)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt
new file mode 100644
index 0000000000..81f4b6c194
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when a call is started.
+ */
+data class CallStarted(
+ /**
+ * Whether its a video call or not.
+ */
+ val isVideo: Boolean,
+ /**
+ * Number of participants in the call.
+ */
+ val numParticipants: Int,
+ /**
+ * Whether this user placed it.
+ */
+ val placed: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CallStarted"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isVideo", isVideo)
+ put("numParticipants", numParticipants)
+ put("placed", placed)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt
new file mode 100644
index 0000000000..fbc36a1195
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user clicks/taps on a UI element.
+ */
+data class Click(
+ /**
+ * The index of the element, if its in a list of elements.
+ */
+ val index: Int? = null,
+ /**
+ * The unique name of this element.
+ */
+ val name: Name,
+) : VectorAnalyticsEvent {
+
+ enum class Name {
+ SendMessageButton,
+ }
+
+ override fun getName() = "Click"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ index?.let { put("index", it) }
+ put("name", name.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt
new file mode 100644
index 0000000000..9562a6e735
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user creates a room.
+ */
+data class CreatedRoom(
+ /**
+ * Whether the room is a DM.
+ */
+ val isDM: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CreatedRoom"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isDM", isDM)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt
new file mode 100644
index 0000000000..988ad309b9
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when an error occurred
+ */
+data class Error(
+ /**
+ * Context - client defined, can be used for debugging
+ */
+ val context: String? = null,
+ val domain: Domain,
+ val name: Name,
+) : VectorAnalyticsEvent {
+
+ enum class Domain {
+ E2EE,
+ VOIP,
+ }
+
+ enum class Name {
+ OlmIndexError,
+ OlmKeysNotSentError,
+ OlmUnspecifiedError,
+ UnknownError,
+ VoipIceFailed,
+ VoipIceTimeout,
+ VoipInviteTimeout,
+ VoipUserHangup,
+ VoipUserMediaFailed,
+ }
+
+ override fun getName() = "Error"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ context?.let { put("context", it) }
+ put("domain", domain.name)
+ put("name", name.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt
new file mode 100644
index 0000000000..fc5f29bff1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user joins a room.
+ */
+data class JoinedRoom(
+ /**
+ * Whether the room is a DM.
+ */
+ val isDM: Boolean,
+ /**
+ * The size of the room.
+ */
+ val roomSize: RoomSize,
+) : VectorAnalyticsEvent {
+
+ enum class RoomSize {
+ ElevenToOneHundred,
+ MoreThanAThousand,
+ OneHundredAndOneToAThousand,
+ ThreeToTen,
+ Two,
+ }
+
+ override fun getName() = "JoinedRoom"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isDM", isDM)
+ put("roomSize", roomSize.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt
new file mode 100644
index 0000000000..34d0297f2d
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered after timing an operation in the app.
+ */
+data class PerformanceTimer(
+ /**
+ * Client defined, can be used for debugging.
+ */
+ val context: String? = null,
+ /**
+ * Client defined, an optional value to indicate how many items were handled during the operation.
+ */
+ val itemCount: Int? = null,
+ /**
+ * The timer that is being reported.
+ */
+ val name: Name,
+ /**
+ * The time reported by the timer in milliseconds.
+ */
+ val timeMs: Int,
+) : VectorAnalyticsEvent {
+
+ enum class Name {
+ /**
+ * The time spent parsing the response from an initial /sync request.
+ */
+ InitialSyncParsing,
+
+ /**
+ * The time spent waiting for a response to an initial /sync request.
+ */
+ InitialSyncRequest,
+
+ /**
+ * The time taken to display an event in the timeline that was opened from a notification.
+ */
+ NotificationsOpenEvent,
+
+ /**
+ * The duration of a regular /sync request when resuming the app.
+ */
+ StartupIncrementalSync,
+
+ /**
+ * The duration of an initial /sync request during startup (if the store has been wiped).
+ */
+ StartupInitialSync,
+
+ /**
+ * How long the app launch screen is displayed for.
+ */
+ StartupLaunchScreen,
+
+ /**
+ * The time to preload data in the MXStore on iOS.
+ */
+ StartupStorePreload,
+
+ /**
+ * The time to load all data from the store (including StartupStorePreload time).
+ */
+ StartupStoreReady,
+ }
+
+ override fun getName() = "PerformanceTimer"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ context?.let { put("context", it) }
+ itemCount?.let { put("itemCount", it) }
+ put("name", name.name)
+ put("timeMs", timeMs)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt
new file mode 100644
index 0000000000..1f18ceee00
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.plan
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user changed screen
+ */
+data class Screen(
+ /**
+ * How long the screen was displayed for in milliseconds.
+ */
+ val durationMs: Int? = null,
+ val screenName: ScreenName,
+) : VectorAnalyticsScreen {
+
+ enum class ScreenName {
+ Group,
+ Home,
+ MyGroups,
+ Room,
+ RoomDirectory,
+ User,
+ WebCompleteSecurity,
+ WebE2ESetup,
+ WebForgotPassword,
+ WebLoading,
+ WebLogin,
+ WebRegister,
+ WebSoftLogout,
+ WebWelcome,
+ }
+
+ override fun getName() = screenName.name
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ durationMs?.let { put("durationMs", it) }
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt
new file mode 100644
index 0000000000..d732e27a82
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.store
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import org.matrix.android.sdk.api.extensions.orFalse
+import javax.inject.Inject
+
+private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_analytics")
+
+/**
+ * Local storage for:
+ * - user consent (Boolean)
+ * - did ask user consent (Boolean)
+ * - analytics Id (String)
+ */
+class AnalyticsStore @Inject constructor(
+ private val context: Context
+) {
+ private val userConsent = booleanPreferencesKey("user_consent")
+ private val didAskUserConsent = booleanPreferencesKey("did_ask_user_consent")
+ private val analyticsId = stringPreferencesKey("analytics_id")
+
+ val userConsentFlow: Flow = context.dataStore.data
+ .map { preferences -> preferences[userConsent].orFalse() }
+ .distinctUntilChanged()
+
+ val didAskUserConsentFlow: Flow = context.dataStore.data
+ .map { preferences -> preferences[didAskUserConsent].orFalse() }
+ .distinctUntilChanged()
+
+ val analyticsIdFlow: Flow = context.dataStore.data
+ .map { preferences -> preferences[analyticsId].orEmpty() }
+ .distinctUntilChanged()
+
+ suspend fun setUserConsent(newUserConsent: Boolean) {
+ context.dataStore.edit { settings ->
+ settings[userConsent] = newUserConsent
+ }
+ }
+
+ suspend fun setDidAskUserConsent(newValue: Boolean = true) {
+ context.dataStore.edit { settings ->
+ settings[didAskUserConsent] = newValue
+ }
+ }
+
+ suspend fun setAnalyticsId(newAnalyticsId: String) {
+ context.dataStore.edit { settings ->
+ settings[analyticsId] = newAnalyticsId
+ }
+ }
+}
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..058ddcba27
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt
@@ -0,0 +1,23 @@
+/*
+ * 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()
+}
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..2c7a8ac9bc
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.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)
+ }.exhaustive
+ }
+
+ private fun handleSetUserConsent(action: AnalyticsConsentViewActions.SetUserConsent) {
+ viewModelScope.launch {
+ analytics.setUserConsent(action.userConsent)
+ analytics.setDidAskUserConsent()
+ _viewEvents.post(AnalyticsOptInViewEvents.OnDataSaved)
+ }
+ }
+}
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..46a21a400a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.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 com.airbnb.mvrx.MavericksState
+
+data class AnalyticsConsentViewState(
+ val userConsent: Boolean = false,
+ val didAskUserConsent: Boolean = false
+) : MavericksState
diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt
new file mode 100644
index 0000000000..f6a06ebdb7
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020 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.viewModel
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.extensions.exhaustive
+import im.vector.app.core.platform.VectorBaseActivity
+import im.vector.app.databinding.ActivitySimpleBinding
+
+/**
+ * Simple container for AnalyticsOptInFragment
+ */
+@AndroidEntryPoint
+class AnalyticsOptInActivity : VectorBaseActivity() {
+
+ private val viewModel: AnalyticsConsentViewModel by viewModel()
+
+ override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
+
+ override fun getCoordinatorLayout() = views.coordinatorLayout
+
+ override fun initUiAndData() {
+ if (isFirstCreation()) {
+ addFragment(views.simpleFragmentContainer, AnalyticsOptInFragment::class.java)
+ }
+
+ viewModel.observeViewEvents {
+ when (it) {
+ AnalyticsOptInViewEvents.OnDataSaved -> finish()
+ }.exhaustive
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInFragment.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInFragment.kt
new file mode 100644
index 0000000000..f112ba4659
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInFragment.kt
@@ -0,0 +1,76 @@
+/*
+ * 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 android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.mvrx.activityViewModel
+import im.vector.app.R
+import im.vector.app.config.analyticsConfig
+import im.vector.app.core.extensions.setTextWithColoredPart
+import im.vector.app.core.platform.OnBackPressed
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.utils.openUrlInChromeCustomTab
+import im.vector.app.databinding.FragmentAnalyticsOptinBinding
+import javax.inject.Inject
+
+class AnalyticsOptInFragment @Inject constructor() :
+ VectorBaseFragment(),
+ OnBackPressed {
+
+ // Share the view model with the Activity so that the Activity
+ // can decide what to do when the data has been saved
+ private val viewModel: AnalyticsConsentViewModel by activityViewModel()
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentAnalyticsOptinBinding {
+ return FragmentAnalyticsOptinBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupLink()
+ setupListeners()
+ }
+
+ private fun setupListeners() {
+ views.submit.debouncedClicks {
+ viewModel.handle(AnalyticsConsentViewActions.SetUserConsent(userConsent = true))
+ }
+ views.later.debouncedClicks {
+ viewModel.handle(AnalyticsConsentViewActions.SetUserConsent(userConsent = false))
+ }
+ }
+
+ private fun setupLink() {
+ views.subtitle.setTextWithColoredPart(
+ fullTextRes = R.string.analytics_opt_in_content,
+ coloredTextRes = R.string.analytics_opt_in_content_link,
+ onClick = {
+ openUrlInChromeCustomTab(requireContext(), null, analyticsConfig.policyLink)
+ }
+ )
+ }
+
+ override fun onBackPressed(toolbarButton: Boolean): Boolean {
+ // Consider user does not give consent
+ viewModel.handle(AnalyticsConsentViewActions.SetUserConsent(userConsent = false))
+ // And consume the event
+ return true
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInViewEvents.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInViewEvents.kt
new file mode 100644
index 0000000000..d73f472876
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInViewEvents.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.VectorViewEvents
+
+sealed interface AnalyticsOptInViewEvents : VectorViewEvents {
+ object OnDataSaved : AnalyticsOptInViewEvents
+}
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 04f3f7cc9c..e2696115f4 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()
@@ -243,6 +246,7 @@ class HomeActivity :
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
+ HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
}.exhaustive
}
homeActivityViewModel.onEach { renderState(it) }
@@ -267,6 +271,11 @@ class HomeActivity :
if (isFirstCreation()) {
handleIntent(intent)
}
+ homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
+ }
+
+ private fun handleShowAnalyticsOptIn() {
+ navigator.openAnalyticsOptIn(this)
}
private fun handleIntent(intent: Intent?) {
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt
index 9df45e4553..5f89c89bc9 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.home
import im.vector.app.core.platform.VectorViewModelAction
-sealed class HomeActivityViewActions : VectorViewModelAction {
- object PushPromptHasBeenReviewed : HomeActivityViewActions()
+sealed interface HomeActivityViewActions : VectorViewModelAction {
+ object ViewStarted : HomeActivityViewActions
+ object PushPromptHasBeenReviewed : HomeActivityViewActions
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
index 7753a7f58b..adc44a57bd 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
@@ -19,9 +19,10 @@ package im.vector.app.features.home
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.util.MatrixItem
-sealed class HomeActivityViewEvents : VectorViewEvents {
- data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
- data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents()
- data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents()
- object PromptToEnableSessionPush : HomeActivityViewEvents()
+sealed interface HomeActivityViewEvents : VectorViewEvents {
+ data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents
+ data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents
+ data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
+ object PromptToEnableSessionPush : HomeActivityViewEvents
+ object ShowAnalyticsOptIn : HomeActivityViewEvents
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 59b9cafd6e..35c112b63a 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -21,11 +21,13 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.ActiveSessionHolder
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.VectorViewModel
+import im.vector.app.features.analytics.store.AnalyticsStore
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
@@ -59,6 +61,7 @@ class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState,
private val activeSessionHolder: ActiveSessionHolder,
private val reAuthHelper: ReAuthHelper,
+ private val analyticsStore: AnalyticsStore,
private val vectorPreferences: VectorPreferences
) : VectorViewModel(initialState) {
@@ -69,14 +72,30 @@ class HomeActivityViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+ private var isInitialized = false
private var checkBootstrap = false
private var onceTrusted = false
- init {
+ private fun initialize() {
+ if (isInitialized) return
+ isInitialized = true
cleanupFiles()
observeInitialSync()
checkSessionPushIsOn()
observeCrossSigningReset()
+ observeAnalytics()
+ }
+
+ private fun observeAnalytics() {
+ if (analyticsConfig.isEnabled) {
+ analyticsStore.didAskUserConsentFlow
+ .onEach { didAskUser ->
+ if (!didAskUser) {
+ _viewEvents.post(HomeActivityViewEvents.ShowAnalyticsOptIn)
+ }
+ }
+ .launchIn(viewModelScope)
+ }
}
private fun cleanupFiles() {
@@ -241,6 +260,9 @@ class HomeActivityViewModel @AssistedInject constructor(
HomeActivityViewActions.PushPromptHasBeenReviewed -> {
vectorPreferences.setDidAskUserToEnableSessionPush()
}
+ HomeActivityViewActions.ViewStarted -> {
+ initialize()
+ }
}.exhaustive
}
}
diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
index eacd8523cf..6b035e7d49 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
@@ -38,6 +38,7 @@ import im.vector.app.core.error.fatalError
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
import im.vector.app.features.VectorFeatures
+import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity
import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.call.conference.VectorJitsiActivity
import im.vector.app.features.call.transfer.CallTransferActivity
@@ -424,6 +425,10 @@ class DefaultNavigator @Inject constructor(
}
}
+ override fun openAnalyticsOptIn(context: Context) {
+ context.startActivity(Intent(context, AnalyticsOptInActivity::class.java))
+ }
+
override fun openTerms(context: Context,
activityResultLauncher: ActivityResultLauncher,
serviceType: TermsService.ServiceType,
diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
index a1f40f07c0..6778c39a22 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
@@ -110,6 +110,8 @@ interface Navigator {
fun openBigImageViewer(activity: Activity, sharedElement: View?, mxcUrl: String?, title: String?)
+ fun openAnalyticsOptIn(context: Context)
+
fun openPinCode(context: Context,
activityResultLauncher: ActivityResultLauncher,
pinMode: PinMode)
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index a7df2deeeb..64561cbc12 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -162,9 +162,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM"
const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
- // analytics
- const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
-
// Rageshake
const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
@@ -818,15 +815,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
}
}
- /**
- * Tells if the analytics tracking is authorized (piwik, matomo, etc.).
- *
- * @return true if the analytics tracking is authorized
- */
- fun useAnalytics(): Boolean {
- return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false)
- }
-
/**
* Tells if the user wants to see URL previews in the timeline
*
@@ -836,17 +824,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_SHOW_URL_PREVIEW_KEY, true)
}
- /**
- * Enable or disable the analytics tracking.
- *
- * @param useAnalytics true to enable the analytics tracking
- */
- fun setUseAnalytics(useAnalytics: Boolean) {
- defaultPrefs.edit {
- putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics)
- }
- }
-
/**
* Tells if media should be previewed before sending
*
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
index cca4b72818..08d67067ec 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
@@ -22,19 +22,21 @@ import android.view.View
import androidx.annotation.CallSuper
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceFragmentCompat
+import com.airbnb.mvrx.MavericksView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
+import im.vector.app.features.analytics.VectorAnalytics
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.Session
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
-abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() {
+abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), MavericksView {
val vectorActivity: VectorBaseActivity<*> by lazy {
activity as VectorBaseActivity<*>
@@ -45,6 +47,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() {
// members
protected lateinit var session: Session
protected lateinit var errorFormatter: ErrorFormatter
+ protected lateinit var analytics: VectorAnalytics
/* ==========================================================================================
* Views
@@ -69,6 +72,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() {
super.onAttach(context)
session = singletonEntryPoint.activeSessionHolder().getActiveSession()
errorFormatter = singletonEntryPoint.errorFormatter()
+ analytics = singletonEntryPoint.analytics()
}
override fun onResume() {
@@ -159,4 +163,8 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() {
.setPositiveButton(R.string.ok, null)
.show()
}
+
+ override fun invalidate() {
+ // No op by default
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
index 3f25907be0..279499b7e9 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
@@ -23,6 +23,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@@ -31,8 +32,10 @@ import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference
import androidx.recyclerview.widget.RecyclerView
+import com.airbnb.mvrx.fragmentViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
+import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.dialogs.ExportKeysDialog
import im.vector.app.core.extensions.queryExportKeys
@@ -43,10 +46,14 @@ import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory
+import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.openFileSelection
import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogImportE2eKeysBinding
+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.crypto.keys.KeysExporter
import im.vector.app.features.crypto.keys.KeysImporter
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
@@ -71,7 +78,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
- private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder,
private val pinCodeStore: PinCodeStore,
private val keysExporter: KeysExporter,
@@ -83,6 +89,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_privacy
override val preferenceXmlRes = R.xml.vector_settings_security_privacy
+ private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel()
+
// cryptography
private val mCryptographyCategory by lazy {
findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
@@ -129,6 +137,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
findPreference("SETTINGS_SECURITY_PIN")!!
}
+ private val analyticsCategory by lazy {
+ findPreference("SETTINGS_ANALYTICS_PREFERENCE_KEY")!!
+ }
+
+ private val analyticsConsent by lazy {
+ findPreference("SETTINGS_USER_ANALYTICS_CONSENT_KEY")!!
+ }
+
override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView {
return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also {
// Insert animation are really annoying the first time the list is shown
@@ -238,18 +254,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
refreshKeysManagementSection()
// Analytics
+ setUpAnalytics()
- // Analytics tracking management
- findPreference(VectorPreferences.SETTINGS_USE_ANALYTICS_KEY)!!.let {
- // On if the analytics tracking is activated
- it.isChecked = vectorPreferences.useAnalytics()
-
- it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
- vectorPreferences.setUseAnalytics(newValue as Boolean)
- true
- }
- }
-
+ // Pin code
openPinCodeSettingsPref.setOnPreferenceClickListener {
openPinCodePreferenceScreen()
true
@@ -274,6 +281,34 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ observeAnalyticsState()
+ }
+
+ private fun observeAnalyticsState() {
+ analyticsConsentViewModel.onEach(AnalyticsConsentViewState::userConsent) {
+ analyticsConsent.isChecked = it
+ }
+ }
+
+ private fun setUpAnalytics() {
+ analyticsCategory.isVisible = analyticsConfig.isEnabled
+
+ analyticsConsent.setOnPreferenceChangeListener { _, newValue ->
+ val newValueBool = newValue as? Boolean ?: false
+ if (newValueBool) {
+ // User wants to enable analytics, display the opt in screen
+ navigator.openAnalyticsOptIn(requireContext())
+ } else {
+ // Just disable analytics
+ analyticsConsentViewModel.handle(AnalyticsConsentViewActions.SetUserConsent(false))
+ }
+ true
+ }
+ }
+
// Todo this should be refactored and use same state as 4S section
private fun refreshXSigningStatus() {
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
diff --git a/vector/src/main/res/drawable/element_logo_stars.xml b/vector/src/main/res/drawable/element_logo_stars.xml
new file mode 100644
index 0000000000..d982fbedc4
--- /dev/null
+++ b/vector/src/main/res/drawable/element_logo_stars.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_list_item_bullet.xml b/vector/src/main/res/drawable/ic_list_item_bullet.xml
new file mode 100644
index 0000000000..b4f13479f7
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_list_item_bullet.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_analytics_optin.xml b/vector/src/main/res/layout/fragment_analytics_optin.xml
new file mode 100644
index 0000000000..ab3df32cf9
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_analytics_optin.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index cbb37cdd64..34ac5fcddc 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -456,6 +456,7 @@
Copied to clipboard
Disable
Return
+ Enable
Not now
@@ -1377,6 +1378,15 @@
Please enable analytics to help us improve ${app_name}.
Yes, I want to help!
+
+ Help improve Element
+
+ Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices.\n\nYou can read all our terms %s.
+ here
+ We don\'t record or profile any account data
+ We don\'t share information with third parties
+ You can turn this off anytime in settings
+
Data save mode
Data save mode applies a specific filter so presence updates and typing notifications are filtered out.
diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml
index 7a301348ea..cd413e3fec 100644
--- a/vector/src/main/res/xml/vector_settings_security_privacy.xml
+++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml
@@ -103,12 +103,11 @@
+ android:title="@string/settings_analytics">
diff --git a/vector/src/release/java/im/vector/app/config/AnalyticsConfig.kt b/vector/src/release/java/im/vector/app/config/AnalyticsConfig.kt
new file mode 100644
index 0000000000..7f7ef1a54e
--- /dev/null
+++ b/vector/src/release/java/im/vector/app/config/AnalyticsConfig.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.config
+
+import im.vector.app.BuildConfig
+import im.vector.app.features.analytics.AnalyticsConfig
+
+val analyticsConfig: AnalyticsConfig = object : AnalyticsConfig {
+ override val isEnabled = BuildConfig.APPLICATION_ID == "im.vector.app"
+ override val postHogHost = "https://posthog.hss.element.io"
+ override val postHogApiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO"
+ override val policyLink = "https://element.io/cookie-policy"
+}