Merge pull request #4559 from vector-im/feature/bma/posthog

Analytics
This commit is contained in:
Benoit Marty 2021-12-14 09:31:55 +01:00 committed by GitHub
commit f4cfb5d6d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 2166 additions and 48 deletions

View File

@ -70,4 +70,27 @@ jobs:
body: | body: |
- Update SAS Strings from matrix-doc. - Update SAS Strings from matrix-doc.
branch: sync-sas-strings 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 base: develop

View File

@ -24,6 +24,7 @@
<w>pbkdf</w> <w>pbkdf</w>
<w>pids</w> <w>pids</w>
<w>pkcs</w> <w>pkcs</w>
<w>posthog</w>
<w>previewable</w> <w>previewable</w>
<w>previewables</w> <w>previewables</w>
<w>pstn</w> <w>pstn</w>

View File

@ -96,6 +96,7 @@ ext.groups = [
'com.parse.bolts', 'com.parse.bolts',
'com.pinterest', 'com.pinterest',
'com.pinterest.ktlint', 'com.pinterest.ktlint',
'com.posthog.android',
'com.squareup', 'com.squareup',
'com.squareup.duktape', 'com.squareup.duktape',
'com.squareup.moshi', 'com.squareup.moshi',

16
docs/analytics.md Normal file
View File

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

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="width_percent">0.6</dimen>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="width_percent">0.5</dimen>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="width_percent">1</dimen>
</resources>

View File

@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # 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 ### 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 ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

18
tools/import_analytic_plan.sh Executable file
View File

@ -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."

View File

@ -439,6 +439,9 @@ dependencies {
implementation libs.dagger.hilt implementation libs.dagger.hilt
kapt libs.dagger.hiltCompiler kapt libs.dagger.hiltCompiler
// Analytics
implementation 'com.posthog.android:posthog:1.1.2'
// gplay flavor only // gplay flavor only
gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') { gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'

View File

@ -5,6 +5,7 @@
<application> <application>
<activity android:name=".features.debug.TestLinkifyActivity" /> <activity android:name=".features.debug.TestLinkifyActivity" />
<activity android:name=".features.debug.DebugPermissionActivity" /> <activity android:name=".features.debug.DebugPermissionActivity" />
<activity android:name=".features.debug.analytics.DebugAnalyticsActivity" />
<activity android:name=".features.debug.settings.DebugPrivateSettingsActivity" /> <activity android:name=".features.debug.settings.DebugPrivateSettingsActivity" />
<activity android:name=".features.debug.sas.DebugSasEmojiActivity" /> <activity android:name=".features.debug.sas.DebugSasEmojiActivity" />
<activity android:name=".features.debug.features.DebugFeaturesSettingsActivity" /> <activity android:name=".features.debug.features.DebugFeaturesSettingsActivity" />

View File

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

View File

@ -34,6 +34,7 @@ import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityDebugMenuBinding 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.features.DebugFeaturesSettingsActivity
import im.vector.app.features.debug.sas.DebugSasEmojiActivity import im.vector.app.features.debug.sas.DebugSasEmojiActivity
import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity
@ -79,6 +80,9 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() {
private fun setupViews() { private fun setupViews() {
views.debugFeatures.setOnClickListener { startActivity(Intent(this, DebugFeaturesSettingsActivity::class.java)) } views.debugFeatures.setOnClickListener { startActivity(Intent(this, DebugFeaturesSettingsActivity::class.java)) }
views.debugPrivateSetting.setOnClickListener { openPrivateSettings() } views.debugPrivateSetting.setOnClickListener { openPrivateSettings() }
views.debugAnalytics.setOnClickListener {
startActivity(Intent(this, DebugAnalyticsActivity::class.java))
}
views.debugTestTextViewLink.setOnClickListener { testTextViewLink() } views.debugTestTextViewLink.setOnClickListener { testTextViewLink() }
views.debugOpenButtonStylesLight.setOnClickListener { views.debugOpenButtonStylesLight.setOnClickListener {
startActivity(Intent(this, DebugVectorButtonStylesLightActivity::class.java)) startActivity(Intent(this, DebugVectorButtonStylesLightActivity::class.java))

View File

@ -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<ActivitySimpleBinding>() {
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(
views.simpleFragmentContainer,
DebugAnalyticsFragment::class.java
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -23,12 +23,18 @@ import dagger.multibindings.IntoMap
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.MavericksViewModelComponent import im.vector.app.core.di.MavericksViewModelComponent
import im.vector.app.core.di.MavericksViewModelKey import im.vector.app.core.di.MavericksViewModelKey
import im.vector.app.features.debug.analytics.DebugAnalyticsViewModel
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewModel import im.vector.app.features.debug.settings.DebugPrivateSettingsViewModel
@InstallIn(MavericksViewModelComponent::class) @InstallIn(MavericksViewModelComponent::class)
@Module @Module
interface MavericksViewModelDebugModule { interface MavericksViewModelDebugModule {
@Binds
@IntoMap
@MavericksViewModelKey(DebugAnalyticsViewModel::class)
fun debugAnalyticsViewModelFactory(factory: DebugAnalyticsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(DebugPrivateSettingsViewModel::class) @MavericksViewModelKey(DebugPrivateSettingsViewModel::class)

View File

@ -32,6 +32,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Private settings" /> android:text="Private settings" />
<Button
android:id="@+id/debug_analytics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Analytics" />
<Button <Button
android:id="@+id/debug_test_text_view_link" android:id="@+id/debug_test_text_view_link"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".features.debug.analytics.DebugAnalyticsActivity"
tools:ignore="HardcodedText">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/linear_divider"
android:orientation="vertical"
android:padding="@dimen/layout_horizontal_margin"
android:showDividers="middle">
<TextView
android:id="@+id/analytics_store_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Store content" />
<Button
android:id="@+id/reset_analytics_opt_in_displayed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reset Analytics Opt in Displayed" />
<Button
android:id="@+id/show_analytics_opt_in"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Show Analytics Opt in" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -298,6 +298,7 @@
<activity android:name=".features.terms.ReviewTermsActivity" /> <activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity" /> <activity android:name=".features.widgets.WidgetActivity" />
<activity android:name=".features.pin.PinActivity" /> <activity android:name=".features.pin.PinActivity" />
<activity android:name=".features.analytics.ui.consent.AnalyticsOptInActivity" />
<activity android:name=".features.home.room.detail.search.SearchActivity" /> <activity android:name=".features.home.room.detail.search.SearchActivity" />
<activity android:name=".features.usercode.UserCodeActivity" /> <activity android:name=".features.usercode.UserCodeActivity" />
<activity android:name=".features.call.transfer.CallTransferActivity" /> <activity android:name=".features.call.transfer.CallTransferActivity" />

View File

@ -262,6 +262,15 @@ SOFTWARE.
</li> </li>
</ul> </ul>
<ul>
<li>
<b>posthog-android</b>
<br/>
https://github.com/PostHog/posthog-android
PostHog Android integration is licensed under the MIT License
</li>
</ul>
<h3> <h3>
Apache License Apache License
<br/> <br/>

View File

@ -42,6 +42,7 @@ import dagger.hilt.android.HiltAndroidApp
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.startSyncing 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
@ -96,6 +97,7 @@ class VectorApplication :
@Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var invitesAcceptor: InvitesAcceptor @Inject lateinit var invitesAcceptor: InvitesAcceptor
@Inject lateinit var vectorFileLogger: VectorFileLogger @Inject lateinit var vectorFileLogger: VectorFileLogger
@Inject lateinit var vectorAnalytics: VectorAnalytics
// font thread handler // font thread handler
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
@ -113,6 +115,7 @@ class VectorApplication :
enableStrictModeIfNeeded() enableStrictModeIfNeeded()
super.onCreate() super.onCreate()
appContext = this appContext = this
vectorAnalytics.init()
invitesAcceptor.initialize() invitesAcceptor.initialize()
vectorUncaughtExceptionHandler.activate(this) vectorUncaughtExceptionHandler.activate(this)

View File

@ -24,6 +24,7 @@ import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.components.ActivityComponent
import dagger.multibindings.IntoMap 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.attachments.preview.AttachmentsPreviewFragment
import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragment import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
@ -520,6 +521,11 @@ interface FragmentModule {
@FragmentKey(BreadcrumbsFragment::class) @FragmentKey(BreadcrumbsFragment::class)
fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(AnalyticsOptInFragment::class)
fun bindAnalyticsOptInFragment(fragment: AnalyticsOptInFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(EmojiChooserFragment::class) @FragmentKey(EmojiChooserFragment::class)

View File

@ -20,6 +20,8 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.multibindings.IntoMap 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.auth.ReAuthViewModel
import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.VectorCallViewModel
import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel
@ -455,6 +457,16 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(LoginViewModel::class) @MavericksViewModelKey(LoginViewModel::class)
fun loginViewModelFactory(factory: LoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *> 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 @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class)

View File

@ -21,6 +21,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -55,6 +56,8 @@ interface SingletonEntryPoint {
fun pinLocker(): PinLocker fun pinLocker(): PinLocker
fun analytics(): VectorAnalytics
fun webRtcCallManager(): WebRtcCallManager fun webRtcCallManager(): WebRtcCallManager
fun appCoroutineScope(): CoroutineScope fun appCoroutineScope(): CoroutineScope

View File

@ -31,6 +31,8 @@ import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock 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.AutoAcceptInvites
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
import im.vector.app.features.navigation.DefaultNavigator import im.vector.app.features.navigation.DefaultNavigator
@ -57,6 +59,9 @@ abstract class VectorBindModule {
@Binds @Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
@Binds @Binds
abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter

View File

@ -65,6 +65,7 @@ import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs 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.configuration.VectorConfiguration
import im.vector.app.features.consent.ConsentNotGivenHelper import im.vector.app.features.consent.ConsentNotGivenHelper
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -132,6 +133,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
private lateinit var sessionListener: SessionListener private lateinit var sessionListener: SessionListener
protected lateinit var bugReporter: BugReporter protected lateinit var bugReporter: BugReporter
private lateinit var pinLocker: PinLocker private lateinit var pinLocker: PinLocker
protected lateinit var analytics: VectorAnalytics
@Inject @Inject
lateinit var rageShake: RageShake lateinit var rageShake: RageShake
@ -187,6 +189,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java)
bugReporter = singletonEntryPoint.bugReporter() bugReporter = singletonEntryPoint.bugReporter()
pinLocker = singletonEntryPoint.pinLocker() pinLocker = singletonEntryPoint.pinLocker()
analytics = singletonEntryPoint.analytics()
navigator = singletonEntryPoint.navigator() navigator = singletonEntryPoint.navigator()
activeSessionHolder = singletonEntryPoint.activeSessionHolder() activeSessionHolder = singletonEntryPoint.activeSessionHolder()
vectorPreferences = singletonEntryPoint.vectorPreferences() vectorPreferences = singletonEntryPoint.vectorPreferences()

View File

@ -34,8 +34,10 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import im.vector.app.core.di.ActivityEntryPoint 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.extensions.toMvRxBundle
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.analytics.VectorAnalytics
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
@ -82,6 +84,8 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
open val showExpanded = false open val showExpanded = false
protected lateinit var analytics: VectorAnalytics
interface ResultListener { interface ResultListener {
fun onBottomSheetResult(resultCode: Int, data: Any?) fun onBottomSheetResult(resultCode: Int, data: Any?)
@ -119,6 +123,8 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
viewModelFactory = activityEntryPoint.viewModelFactory() viewModelFactory = activityEntryPoint.viewModelFactory()
val singletonEntryPoint = context.singletonEntryPoint()
analytics = singletonEntryPoint.analytics()
super.onAttach(context) super.onAttach(context)
} }

View File

@ -42,6 +42,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -60,6 +61,7 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
* ========================================================================================== */ * ========================================================================================== */
protected lateinit var navigator: Navigator protected lateinit var navigator: Navigator
protected lateinit var analytics: VectorAnalytics
protected lateinit var errorFormatter: ErrorFormatter protected lateinit var errorFormatter: ErrorFormatter
protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog
@ -96,6 +98,7 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java)
navigator = singletonEntryPoint.navigator() navigator = singletonEntryPoint.navigator()
errorFormatter = singletonEntryPoint.errorFormatter() errorFormatter = singletonEntryPoint.errorFormatter()
analytics = singletonEntryPoint.analytics()
unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog() unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog()
viewModelFactory = activityEntryPoint.viewModelFactory() viewModelFactory = activityEntryPoint.viewModelFactory()
childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory() childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory()

View File

@ -32,6 +32,7 @@ import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.deleteAllFiles import im.vector.app.core.utils.deleteAllFiles
import im.vector.app.databinding.ActivityMainBinding import im.vector.app.databinding.ActivityMainBinding
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.ShortcutsHandler import im.vector.app.features.home.ShortcutsHandler
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
@ -96,6 +97,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
@Inject lateinit var pinCodeStore: PinCodeStore @Inject lateinit var pinCodeStore: PinCodeStore
@Inject lateinit var pinLocker: PinLocker @Inject lateinit var pinLocker: PinLocker
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var vectorAnalytics: VectorAnalytics
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -190,6 +192,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
uiStateRepository.reset() uiStateRepository.reset()
pinLocker.unlock() pinLocker.unlock()
pinCodeStore.deleteEncodedPin() pinCodeStore.deleteEncodedPin()
vectorAnalytics.onSignOut()
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// On BG thread // On BG thread

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics
interface AnalyticsConfig {
val isEnabled: Boolean
val postHogHost: String
val postHogApiKey: String
val policyLink: String
}

View File

@ -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<Boolean>
/**
* 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<Boolean>
/**
* 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<String>
/**
* 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)
}

View File

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

View File

@ -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<DummyState, EmptyAction, EmptyViewEvents>(initialState) {
private var checkDone: Boolean = false
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<AnalyticsAccountDataViewModel, DummyState> {
override fun create(initialState: DummyState): AnalyticsAccountDataViewModel
}
companion object : MavericksViewModelFactory<AnalyticsAccountDataViewModel, DummyState> 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<AnalyticsAccountDataContent>() }
.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())
}
}
}

View File

@ -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<Boolean> {
return analyticsStore.userConsentFlow
}
override suspend fun setUserConsent(userConsent: Boolean) {
Timber.tag(analyticsTag.value).d("setUserConsent($userConsent)")
analyticsStore.setUserConsent(userConsent)
}
override fun didAskUserConsent(): Flow<Boolean> {
return analyticsStore.didAskUserConsentFlow
}
override suspend fun setDidAskUserConsent() {
Timber.tag(analyticsTag.value).d("setDidAskUserConsent()")
analyticsStore.setDidAskUserConsent()
}
override fun getAnalyticsId(): Flow<String> {
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<String, Any>?.toPostHogProperties(): Properties? {
if (this == null) return null
return Properties().apply {
putAll(this@toPostHogProperties)
}
}
}

View File

@ -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<String, Any>?
}

View File

@ -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<String, Any>?
}

View File

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

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("durationMs", durationMs)
put("isVideo", isVideo)
put("numParticipants", numParticipants)
put("placed", placed)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isVideo", isVideo)
put("numParticipants", numParticipants)
put("placed", placed)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isVideo", isVideo)
put("numParticipants", numParticipants)
put("placed", placed)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
index?.let { put("index", it) }
put("name", name.name)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isDM", isDM)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
context?.let { put("context", it) }
put("domain", domain.name)
put("name", name.name)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isDM", isDM)
put("roomSize", roomSize.name)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
context?.let { put("context", it) }
itemCount?.let { put("itemCount", it) }
put("name", name.name)
put("timeMs", timeMs)
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<String, Any>? {
return mutableMapOf<String, Any>().apply {
durationMs?.let { put("durationMs", it) }
}.takeIf { it.isNotEmpty() }
}
}

View File

@ -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<Preferences> 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<Boolean> = context.dataStore.data
.map { preferences -> preferences[userConsent].orFalse() }
.distinctUntilChanged()
val didAskUserConsentFlow: Flow<Boolean> = context.dataStore.data
.map { preferences -> preferences[didAskUserConsent].orFalse() }
.distinctUntilChanged()
val analyticsIdFlow: Flow<String> = 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
}
}
}

View File

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

View File

@ -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<AnalyticsConsentViewState, AnalyticsConsentViewActions, AnalyticsOptInViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<AnalyticsConsentViewModel, AnalyticsConsentViewState> {
override fun create(initialState: AnalyticsConsentViewState): AnalyticsConsentViewModel
}
companion object : MavericksViewModelFactory<AnalyticsConsentViewModel, AnalyticsConsentViewState> by hiltMavericksViewModelFactory()
init {
observeAnalytics()
}
private fun observeAnalytics() {
analytics.didAskUserConsent().setOnEach {
copy(didAskUserConsent = it)
}
analytics.getUserConsent().setOnEach {
copy(userConsent = it)
}
}
override fun handle(action: AnalyticsConsentViewActions) {
when (action) {
is AnalyticsConsentViewActions.SetUserConsent -> handleSetUserConsent(action)
}.exhaustive
}
private fun handleSetUserConsent(action: AnalyticsConsentViewActions.SetUserConsent) {
viewModelScope.launch {
analytics.setUserConsent(action.userConsent)
analytics.setDidAskUserConsent()
_viewEvents.post(AnalyticsOptInViewEvents.OnDataSaved)
}
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics.ui.consent
import com.airbnb.mvrx.MavericksState
data class AnalyticsConsentViewState(
val userConsent: Boolean = false,
val didAskUserConsent: Boolean = false
) : MavericksState

View File

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

View File

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

View File

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

View File

@ -48,6 +48,7 @@ import im.vector.app.core.pushers.PushersManager
import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs 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.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -103,6 +104,8 @@ class HomeActivity :
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Suppress("UNUSED")
private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel()
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel() private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel()
@ -243,6 +246,7 @@ class HomeActivity :
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
}.exhaustive }.exhaustive
} }
homeActivityViewModel.onEach { renderState(it) } homeActivityViewModel.onEach { renderState(it) }
@ -267,6 +271,11 @@ class HomeActivity :
if (isFirstCreation()) { if (isFirstCreation()) {
handleIntent(intent) handleIntent(intent)
} }
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
}
private fun handleShowAnalyticsOptIn() {
navigator.openAnalyticsOptIn(this)
} }
private fun handleIntent(intent: Intent?) { private fun handleIntent(intent: Intent?) {

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class HomeActivityViewActions : VectorViewModelAction { sealed interface HomeActivityViewActions : VectorViewModelAction {
object PushPromptHasBeenReviewed : HomeActivityViewActions() object ViewStarted : HomeActivityViewActions
object PushPromptHasBeenReviewed : HomeActivityViewActions
} }

View File

@ -19,9 +19,10 @@ package im.vector.app.features.home
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
sealed class HomeActivityViewEvents : VectorViewEvents { sealed interface HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents() data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents
data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents() data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents() data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
object PromptToEnableSessionPush : HomeActivityViewEvents() object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents
} }

View File

@ -21,11 +21,13 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel 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.login.ReAuthHelper
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@ -59,6 +61,7 @@ class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState, @Assisted initialState: HomeActivityViewState,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val analyticsStore: AnalyticsStore,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) { ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@ -69,14 +72,30 @@ class HomeActivityViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<HomeActivityViewModel, HomeActivityViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<HomeActivityViewModel, HomeActivityViewState> by hiltMavericksViewModelFactory()
private var isInitialized = false
private var checkBootstrap = false private var checkBootstrap = false
private var onceTrusted = false private var onceTrusted = false
init { private fun initialize() {
if (isInitialized) return
isInitialized = true
cleanupFiles() cleanupFiles()
observeInitialSync() observeInitialSync()
checkSessionPushIsOn() checkSessionPushIsOn()
observeCrossSigningReset() observeCrossSigningReset()
observeAnalytics()
}
private fun observeAnalytics() {
if (analyticsConfig.isEnabled) {
analyticsStore.didAskUserConsentFlow
.onEach { didAskUser ->
if (!didAskUser) {
_viewEvents.post(HomeActivityViewEvents.ShowAnalyticsOptIn)
}
}
.launchIn(viewModelScope)
}
} }
private fun cleanupFiles() { private fun cleanupFiles() {
@ -241,6 +260,9 @@ class HomeActivityViewModel @AssistedInject constructor(
HomeActivityViewActions.PushPromptHasBeenReviewed -> { HomeActivityViewActions.PushPromptHasBeenReviewed -> {
vectorPreferences.setDidAskUserToEnableSessionPush() vectorPreferences.setDidAskUserToEnableSessionPush()
} }
HomeActivityViewActions.ViewStarted -> {
initialize()
}
}.exhaustive }.exhaustive
} }
} }

View File

@ -38,6 +38,7 @@ import im.vector.app.core.error.fatalError
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.VectorFeatures 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.JitsiCallViewModel
import im.vector.app.features.call.conference.VectorJitsiActivity import im.vector.app.features.call.conference.VectorJitsiActivity
import im.vector.app.features.call.transfer.CallTransferActivity 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, override fun openTerms(context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>, activityResultLauncher: ActivityResultLauncher<Intent>,
serviceType: TermsService.ServiceType, serviceType: TermsService.ServiceType,

View File

@ -110,6 +110,8 @@ interface Navigator {
fun openBigImageViewer(activity: Activity, sharedElement: View?, mxcUrl: String?, title: String?) fun openBigImageViewer(activity: Activity, sharedElement: View?, mxcUrl: String?, title: String?)
fun openAnalyticsOptIn(context: Context)
fun openPinCode(context: Context, fun openPinCode(context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>, activityResultLauncher: ActivityResultLauncher<Intent>,
pinMode: PinMode) pinMode: PinMode)

View File

@ -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" 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" 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 // Rageshake
const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" 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" 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 * 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) 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 * Tells if media should be previewed before sending
* *

View File

@ -22,19 +22,21 @@ import android.view.View
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.airbnb.mvrx.MavericksView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.analytics.VectorAnalytics
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber import timber.log.Timber
abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() { abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), MavericksView {
val vectorActivity: VectorBaseActivity<*> by lazy { val vectorActivity: VectorBaseActivity<*> by lazy {
activity as VectorBaseActivity<*> activity as VectorBaseActivity<*>
@ -45,6 +47,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() {
// members // members
protected lateinit var session: Session protected lateinit var session: Session
protected lateinit var errorFormatter: ErrorFormatter protected lateinit var errorFormatter: ErrorFormatter
protected lateinit var analytics: VectorAnalytics
/* ========================================================================================== /* ==========================================================================================
* Views * Views
@ -69,6 +72,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() {
super.onAttach(context) super.onAttach(context)
session = singletonEntryPoint.activeSessionHolder().getActiveSession() session = singletonEntryPoint.activeSessionHolder().getActiveSession()
errorFormatter = singletonEntryPoint.errorFormatter() errorFormatter = singletonEntryPoint.errorFormatter()
analytics = singletonEntryPoint.analytics()
} }
override fun onResume() { override fun onResume() {
@ -159,4 +163,8 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() {
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
override fun invalidate() {
// No op by default
}
} }

View File

@ -23,6 +23,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -31,8 +32,10 @@ import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.fragmentViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.dialogs.ExportKeysDialog import im.vector.app.core.dialogs.ExportKeysDialog
import im.vector.app.core.extensions.queryExportKeys 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.platform.SimpleTextWatcher
import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory 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.copyToClipboard
import im.vector.app.core.utils.openFileSelection import im.vector.app.core.utils.openFileSelection
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogImportE2eKeysBinding 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.KeysExporter
import im.vector.app.features.crypto.keys.KeysImporter import im.vector.app.features.crypto.keys.KeysImporter
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity 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 import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor( class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val pinCodeStore: PinCodeStore, private val pinCodeStore: PinCodeStore,
private val keysExporter: KeysExporter, private val keysExporter: KeysExporter,
@ -83,6 +89,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_privacy override var titleRes = R.string.settings_security_and_privacy
override val preferenceXmlRes = R.xml.vector_settings_security_privacy override val preferenceXmlRes = R.xml.vector_settings_security_privacy
private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel()
// cryptography // cryptography
private val mCryptographyCategory by lazy { private val mCryptographyCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
@ -129,6 +137,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
findPreference<VectorPreference>("SETTINGS_SECURITY_PIN")!! findPreference<VectorPreference>("SETTINGS_SECURITY_PIN")!!
} }
private val analyticsCategory by lazy {
findPreference<VectorPreferenceCategory>("SETTINGS_ANALYTICS_PREFERENCE_KEY")!!
}
private val analyticsConsent by lazy {
findPreference<VectorSwitchPreference>("SETTINGS_USER_ANALYTICS_CONSENT_KEY")!!
}
override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView { override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView {
return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also { return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also {
// Insert animation are really annoying the first time the list is shown // Insert animation are really annoying the first time the list is shown
@ -238,18 +254,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
refreshKeysManagementSection() refreshKeysManagementSection()
// Analytics // Analytics
setUpAnalytics()
// Analytics tracking management // Pin code
findPreference<SwitchPreference>(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
}
}
openPinCodeSettingsPref.setOnPreferenceClickListener { openPinCodeSettingsPref.setOnPreferenceClickListener {
openPinCodePreferenceScreen() openPinCodePreferenceScreen()
true 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 // Todo this should be refactored and use same state as 4S section
private fun refreshXSigningStatus() { private fun refreshXSigningStatus() {
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()

View File

@ -0,0 +1,57 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="94dp"
android:viewportWidth="120"
android:viewportHeight="94">
<path
android:pathData="M60.396,4.958L60.604,4.958A44.521,44.521 0,0 1,105.125 49.479L105.125,49.479A44.521,44.521 0,0 1,60.604 94L60.396,94A44.521,44.521 0,0 1,15.875 49.479L15.875,49.479A44.521,44.521 0,0 1,60.396 4.958z"
android:fillColor="#0DBD8B"/>
<path
android:pathData="M53.228,26.676C53.228,24.958 54.623,23.566 56.344,23.566C67.82,23.566 77.123,32.847 77.123,44.296C77.123,46.014 75.727,47.406 74.006,47.406C72.285,47.406 70.889,46.014 70.889,44.296C70.889,36.282 64.377,29.785 56.344,29.785C54.623,29.785 53.228,28.393 53.228,26.676Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M67.772,72.282C67.772,73.999 66.377,75.391 64.655,75.391C53.18,75.391 43.877,66.11 43.877,54.661C43.877,52.944 45.272,51.552 46.994,51.552C48.715,51.552 50.111,52.944 50.111,54.661C50.111,62.675 56.623,69.172 64.655,69.172C66.377,69.172 67.772,70.564 67.772,72.282Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M37.644,56.734C35.922,56.734 34.527,55.342 34.527,53.625C34.527,42.176 43.83,32.895 55.305,32.895C57.027,32.895 58.422,34.287 58.422,36.004C58.422,37.722 57.027,39.114 55.305,39.114C47.272,39.114 40.76,45.611 40.76,53.625C40.76,55.342 39.365,56.734 37.644,56.734Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M83.356,42.223C85.078,42.223 86.473,43.615 86.473,45.332C86.473,56.781 77.17,66.063 65.695,66.063C63.973,66.063 62.578,64.671 62.578,62.953C62.578,61.236 63.973,59.844 65.695,59.844C73.728,59.844 80.24,53.347 80.24,45.332C80.24,43.615 81.635,42.223 83.356,42.223Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M39.571,77.305C40.181,77.305 40.683,76.856 40.769,76.227C41.7,69.687 42.587,68.79 48.918,68.076C49.56,68.001 50.041,67.478 50.041,66.87C50.041,66.251 49.57,65.75 48.929,65.664C42.63,64.843 41.817,64.043 40.769,57.502C40.662,56.873 40.181,56.435 39.571,56.435C38.972,56.435 38.47,56.873 38.374,57.513C37.454,64.053 36.566,64.95 30.235,65.664C29.594,65.739 29.123,66.251 29.123,66.87C29.123,67.478 29.583,67.99 30.235,68.076C36.534,68.951 37.315,69.697 38.374,76.238C38.491,76.867 38.983,77.305 39.571,77.305Z"
android:strokeWidth="1.5"
android:fillColor="#ffffff"
android:strokeColor="#0DBD8B"/>
<path
android:strokeWidth="1"
android:pathData="M82.194,35.392C82.697,35.392 83.111,35.019 83.182,34.494C83.949,29.044 84.682,28.297 89.905,27.701C90.434,27.639 90.831,27.203 90.831,26.697C90.831,26.181 90.443,25.763 89.913,25.692C84.717,25.007 84.046,24.34 83.182,18.89C83.093,18.365 82.697,18.001 82.194,18.001C81.7,18.001 81.285,18.365 81.205,18.899C80.447,24.349 79.714,25.096 74.491,25.692C73.962,25.754 73.574,26.181 73.574,26.697C73.574,27.203 73.953,27.63 74.491,27.701C79.688,28.43 80.332,29.053 81.205,34.503C81.302,35.028 81.708,35.392 82.194,35.392Z"
android:fillColor="#ffffff"
android:strokeColor="#0DBD8B"/>
<path
android:pathData="M113.846,18.87C114.174,18.87 114.444,18.631 114.49,18.296C114.991,14.807 115.468,14.329 118.873,13.948C119.218,13.908 119.477,13.63 119.477,13.305C119.477,12.975 119.224,12.708 118.879,12.662C115.491,12.224 115.054,11.797 114.49,8.309C114.433,7.973 114.174,7.74 113.846,7.74C113.524,7.74 113.254,7.973 113.202,8.315C112.707,11.803 112.23,12.281 108.825,12.662C108.48,12.702 108.227,12.975 108.227,13.305C108.227,13.63 108.474,13.903 108.825,13.948C112.213,14.415 112.633,14.813 113.202,18.301C113.265,18.637 113.53,18.87 113.846,18.87Z"
android:fillColor="#0DBD8B"/>
<path
android:pathData="M107.169,9.131C107.354,9.131 107.506,8.997 107.531,8.808C107.813,6.846 108.081,6.577 109.997,6.363C110.191,6.34 110.336,6.183 110.336,6.001C110.336,5.815 110.194,5.665 110,5.639C108.094,5.393 107.849,5.153 107.531,3.191C107.499,3.002 107.354,2.871 107.169,2.871C106.988,2.871 106.836,3.002 106.807,3.194C106.529,5.156 106.26,5.425 104.345,5.639C104.151,5.662 104.008,5.815 104.008,6.001C104.008,6.183 104.147,6.337 104.345,6.363C106.25,6.625 106.486,6.849 106.807,8.811C106.842,9 106.991,9.131 107.169,9.131Z"
android:fillColor="#0DBD8B"/>
<path
android:pathData="M108.575,24.435C108.8,24.435 108.986,24.271 109.018,24.04C109.362,21.642 109.69,21.314 112.031,21.052C112.268,21.024 112.446,20.833 112.446,20.61C112.446,20.383 112.272,20.199 112.035,20.167C109.706,19.866 109.405,19.573 109.018,17.175C108.978,16.944 108.8,16.783 108.575,16.783C108.353,16.783 108.167,16.944 108.132,17.179C107.792,19.577 107.464,19.905 105.123,20.167C104.885,20.195 104.711,20.383 104.711,20.61C104.711,20.833 104.881,21.02 105.123,21.052C107.452,21.372 107.74,21.646 108.132,24.044C108.175,24.275 108.357,24.435 108.575,24.435Z"
android:fillColor="#0DBD8B"/>
<path
android:pathData="M6.197,15.392C6.504,15.392 6.758,15.168 6.801,14.853C7.27,11.583 7.718,11.135 10.91,10.778C11.233,10.74 11.476,10.479 11.476,10.175C11.476,9.865 11.239,9.615 10.915,9.572C7.739,9.161 7.329,8.761 6.801,5.491C6.747,5.176 6.504,4.958 6.197,4.958C5.895,4.958 5.642,5.176 5.593,5.496C5.129,8.767 4.682,9.215 1.49,9.572C1.166,9.609 0.929,9.865 0.929,10.175C0.929,10.479 1.161,10.735 1.49,10.778C4.666,11.215 5.059,11.589 5.593,14.859C5.652,15.174 5.9,15.392 6.197,15.392Z"
android:fillColor="#0DBD8B"/>
<path
android:pathData="M13.231,5.653C13.375,5.653 13.493,5.549 13.513,5.402C13.732,3.876 13.941,3.667 15.431,3.5C15.582,3.482 15.695,3.36 15.695,3.218C15.695,3.074 15.584,2.957 15.433,2.937C13.951,2.745 13.76,2.559 13.513,1.033C13.488,0.886 13.375,0.784 13.231,0.784C13.09,0.784 12.972,0.886 12.95,1.035C12.733,2.561 12.524,2.77 11.035,2.937C10.884,2.955 10.773,3.074 10.773,3.218C10.773,3.36 10.881,3.48 11.035,3.5C12.517,3.704 12.7,3.878 12.95,5.404C12.977,5.551 13.093,5.653 13.231,5.653Z"
android:strokeAlpha="0.4"
android:fillColor="#0DBD8B"
android:fillAlpha="0.4"/>
<path
android:pathData="M16.747,11.914C16.89,11.914 17.009,11.809 17.029,11.663C17.248,10.136 17.457,9.927 18.946,9.761C19.097,9.743 19.21,9.621 19.21,9.479C19.21,9.335 19.1,9.218 18.949,9.198C17.467,9.006 17.275,8.819 17.029,7.293C17.004,7.147 16.89,7.044 16.747,7.044C16.606,7.044 16.488,7.147 16.465,7.296C16.249,8.822 16.04,9.031 14.55,9.198C14.399,9.215 14.289,9.335 14.289,9.479C14.289,9.621 14.397,9.741 14.55,9.761C16.032,9.965 16.216,10.139 16.465,11.665C16.493,11.812 16.609,11.914 16.747,11.914Z"
android:strokeAlpha="0.4"
android:fillColor="#0DBD8B"
android:fillAlpha="0.4"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2V2Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#0DBD8B"
android:strokeLineCap="square"/>
<path
android:pathData="M6.5454,12.8885L9.803,16.2428L17.4545,8.364"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#0DBD8B"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin">
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flowMain"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:orientation="vertical"
app:constraint_referenced_ids="flowHeader,separator,flowItems,flowButtons"
app:flow_verticalStyle="spread"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="@dimen/width_percent" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flowHeader"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:orientation="vertical"
app:constraint_referenced_ids="logo,title,subtitle"
app:flow_verticalGap="20dp" />
<ImageView
android:id="@+id/logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/element_logo_stars" />
<TextView
android:id="@+id/title"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="@string/analytics_opt_in_title"
android:textColor="?vctr_content_primary" />
<TextView
android:id="@+id/subtitle"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:gravity="center"
android:textColor="?vctr_content_secondary"
tools:text="@string/analytics_opt_in_content" />
<View
android:id="@+id/separator"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?vctr_content_quinary" />
<!-- width of this block will be the width of the first referenced text,
which has wrap_content, the other have 0dp -->
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flowItems"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:constraint_referenced_ids="list_item_1,list_item_2,list_item_3"
app:flow_verticalGap="12dp" />
<TextView
android:id="@+id/list_item_1"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:text="@string/analytics_opt_in_list_item_1"
android:textColor="?vctr_content_secondary"
app:drawableStartCompat="@drawable/ic_list_item_bullet" />
<TextView
android:id="@+id/list_item_2"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:text="@string/analytics_opt_in_list_item_2"
android:textColor="?vctr_content_secondary"
app:drawableStartCompat="@drawable/ic_list_item_bullet" />
<TextView
android:id="@+id/list_item_3"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:text="@string/analytics_opt_in_list_item_3"
android:textColor="?vctr_content_secondary"
app:drawableStartCompat="@drawable/ic_list_item_bullet" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flowButtons"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:constraint_referenced_ids="submit,later" />
<Button
android:id="@+id/submit"
style="@style/Widget.Vector.Button.CallToAction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="@dimen/layout_touch_size"
android:text="@string/action_enable" />
<Button
android:id="@+id/later"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_not_now" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -456,6 +456,7 @@
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
<string name="disable">Disable</string> <string name="disable">Disable</string>
<string name="action_return">Return</string> <string name="action_return">Return</string>
<string name="action_enable">Enable</string>
<string name="action_not_now">Not now</string> <string name="action_not_now">Not now</string>
<!-- dialog titles --> <!-- dialog titles -->
@ -1377,6 +1378,15 @@
<string name="template_settings_opt_in_of_analytics_prompt">Please enable analytics to help us improve ${app_name}.</string> <string name="template_settings_opt_in_of_analytics_prompt">Please enable analytics to help us improve ${app_name}.</string>
<string name="settings_opt_in_of_analytics_ok">Yes, I want to help!</string> <string name="settings_opt_in_of_analytics_ok">Yes, I want to help!</string>
<!-- analytics v2 -->
<string name="analytics_opt_in_title">Help improve Element</string>
<!-- The template will be replaced by the value of the resource analytics_opt_in_content_link -->
<string name="analytics_opt_in_content">Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, well generate a random identifier, shared by your devices.\n\nYou can read all our terms %s.</string>
<string name="analytics_opt_in_content_link">here</string>
<string name="analytics_opt_in_list_item_1">We <b>don\'t</b> record or profile any account data</string>
<string name="analytics_opt_in_list_item_2">We <b>don\'t</b> share information with third parties</string>
<string name="analytics_opt_in_list_item_3">You can turn this off anytime in settings</string>
<string name="settings_data_save_mode">Data save mode</string> <string name="settings_data_save_mode">Data save mode</string>
<string name="settings_data_save_mode_summary">Data save mode applies a specific filter so presence updates and typing notifications are filtered out.</string> <string name="settings_data_save_mode_summary">Data save mode applies a specific filter so presence updates and typing notifications are filtered out.</string>

View File

@ -103,12 +103,11 @@
<im.vector.app.core.preference.VectorPreferenceCategory <im.vector.app.core.preference.VectorPreferenceCategory
android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY" android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY"
android:title="@string/settings_analytics" android:title="@string/settings_analytics">
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.app.core.preference.VectorSwitchPreference <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="SETTINGS_USE_ANALYTICS_KEY" android:key="SETTINGS_USER_ANALYTICS_CONSENT_KEY"
android:summary="@string/settings_opt_in_of_analytics_summary" android:summary="@string/settings_opt_in_of_analytics_summary"
android:title="@string/settings_opt_in_of_analytics" /> android:title="@string/settings_opt_in_of_analytics" />

View File

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