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

@ -71,3 +71,26 @@ jobs:
- Update SAS Strings from matrix-doc.
branch: sync-sas-strings
base: develop
sync-analytics-plan:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'vector-im/element-android'
steps:
- uses: actions/checkout@v2
- name: Run analytics import script
run: ./tools/import_analytic_plan.sh
- name: Create Pull Request for analytics plan
uses: peter-evans/create-pull-request@v3
with:
commit-message: Sync analytics plan
title: Sync analytics plan
body: |
### Update analytics plan
Reviewers:
- [ ] Please remove usage of Event or Enum which may have been removed or updated
- [ ] please ensure new Events or new Enums are used to send analytics by pushing new commit(s) to this PR.
*Note*: Change are coming from [this project](https://github.com/matrix-org/matrix-analytics-events)
branch: sync-analytics-plan
base: develop

View File

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

View File

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

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
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===110
enum class===114
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3

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
kapt libs.dagger.hiltCompiler
// Analytics
implementation 'com.posthog.android:posthog:1.1.2'
// gplay flavor only
gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'

View File

@ -5,6 +5,7 @@
<application>
<activity android:name=".features.debug.TestLinkifyActivity" />
<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.sas.DebugSasEmojiActivity" />
<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.toast
import im.vector.app.databinding.ActivityDebugMenuBinding
import im.vector.app.features.debug.analytics.DebugAnalyticsActivity
import im.vector.app.features.debug.features.DebugFeaturesSettingsActivity
import im.vector.app.features.debug.sas.DebugSasEmojiActivity
import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity
@ -79,6 +80,9 @@ class DebugMenuActivity : VectorBaseActivity<ActivityDebugMenuBinding>() {
private fun setupViews() {
views.debugFeatures.setOnClickListener { startActivity(Intent(this, DebugFeaturesSettingsActivity::class.java)) }
views.debugPrivateSetting.setOnClickListener { openPrivateSettings() }
views.debugAnalytics.setOnClickListener {
startActivity(Intent(this, DebugAnalyticsActivity::class.java))
}
views.debugTestTextViewLink.setOnClickListener { testTextViewLink() }
views.debugOpenButtonStylesLight.setOnClickListener {
startActivity(Intent(this, DebugVectorButtonStylesLightActivity::class.java))

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.MavericksViewModelComponent
import im.vector.app.core.di.MavericksViewModelKey
import im.vector.app.features.debug.analytics.DebugAnalyticsViewModel
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewModel
@InstallIn(MavericksViewModelComponent::class)
@Module
interface MavericksViewModelDebugModule {
@Binds
@IntoMap
@MavericksViewModelKey(DebugAnalyticsViewModel::class)
fun debugAnalyticsViewModelFactory(factory: DebugAnalyticsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(DebugPrivateSettingsViewModel::class)

View File

@ -32,6 +32,12 @@
android:layout_height="wrap_content"
android:text="Private settings" />
<Button
android:id="@+id/debug_analytics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Analytics" />
<Button
android:id="@+id/debug_test_text_view_link"
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.widgets.WidgetActivity" />
<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.usercode.UserCodeActivity" />
<activity android:name=".features.call.transfer.CallTransferActivity" />

View File

@ -262,6 +262,15 @@ SOFTWARE.
</li>
</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>
Apache License
<br/>

View File

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

View File

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

View File

@ -20,6 +20,8 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.IntoMap
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel
import im.vector.app.features.auth.ReAuthViewModel
import im.vector.app.features.call.VectorCallViewModel
import im.vector.app.features.call.conference.JitsiCallViewModel
@ -455,6 +457,16 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(LoginViewModel::class)
fun loginViewModelFactory(factory: LoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(AnalyticsConsentViewModel::class)
fun analyticsConsentViewModelFactory(factory: AnalyticsConsentViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(AnalyticsAccountDataViewModel::class)
fun analyticsAccountDataViewModelFactory(factory: AnalyticsAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(HomeServerCapabilitiesViewModel::class)

View File

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

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

View File

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

View File

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

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

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.utils.deleteAllFiles
import im.vector.app.databinding.ActivityMainBinding
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.ShortcutsHandler
import im.vector.app.features.notifications.NotificationDrawerManager
@ -96,6 +97,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
@Inject lateinit var pinCodeStore: PinCodeStore
@Inject lateinit var pinLocker: PinLocker
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var vectorAnalytics: VectorAnalytics
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -190,6 +192,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
uiStateRepository.reset()
pinLocker.unlock()
pinCodeStore.deleteEncodedPin()
vectorAnalytics.onSignOut()
}
withContext(Dispatchers.IO) {
// 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.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.navigation.Navigator
@ -103,6 +104,8 @@ class HomeActivity :
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Suppress("UNUSED")
private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel()
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel()
@ -243,6 +246,7 @@ class HomeActivity :
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
}.exhaustive
}
homeActivityViewModel.onEach { renderState(it) }
@ -267,6 +271,11 @@ class HomeActivity :
if (isFirstCreation()) {
handleIntent(intent)
}
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
}
private fun handleShowAnalyticsOptIn() {
navigator.openAnalyticsOptIn(this)
}
private fun handleIntent(intent: Intent?) {

View File

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

View File

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

View File

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

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.utils.toast
import im.vector.app.features.VectorFeatures
import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity
import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.call.conference.VectorJitsiActivity
import im.vector.app.features.call.transfer.CallTransferActivity
@ -424,6 +425,10 @@ class DefaultNavigator @Inject constructor(
}
}
override fun openAnalyticsOptIn(context: Context) {
context.startActivity(Intent(context, AnalyticsOptInActivity::class.java))
}
override fun openTerms(context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
serviceType: TermsService.ServiceType,

View File

@ -110,6 +110,8 @@ interface Navigator {
fun openBigImageViewer(activity: Activity, sharedElement: View?, mxcUrl: String?, title: String?)
fun openAnalyticsOptIn(context: Context)
fun openPinCode(context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
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"
const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
// analytics
const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
// Rageshake
const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
@ -818,15 +815,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
}
}
/**
* Tells if the analytics tracking is authorized (piwik, matomo, etc.).
*
* @return true if the analytics tracking is authorized
*/
fun useAnalytics(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false)
}
/**
* Tells if the user wants to see URL previews in the timeline
*
@ -836,17 +824,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_SHOW_URL_PREVIEW_KEY, true)
}
/**
* Enable or disable the analytics tracking.
*
* @param useAnalytics true to enable the analytics tracking
*/
fun setUseAnalytics(useAnalytics: Boolean) {
defaultPrefs.edit {
putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics)
}
}
/**
* Tells if media should be previewed before sending
*

View File

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

View File

@ -23,6 +23,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@ -31,8 +32,10 @@ import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.fragmentViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.dialogs.ExportKeysDialog
import im.vector.app.core.extensions.queryExportKeys
@ -43,10 +46,14 @@ import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.openFileSelection
import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogImportE2eKeysBinding
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState
import im.vector.app.features.crypto.keys.KeysExporter
import im.vector.app.features.crypto.keys.KeysImporter
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
@ -71,7 +78,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder,
private val pinCodeStore: PinCodeStore,
private val keysExporter: KeysExporter,
@ -83,6 +89,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_privacy
override val preferenceXmlRes = R.xml.vector_settings_security_privacy
private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel()
// cryptography
private val mCryptographyCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
@ -129,6 +137,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
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 {
return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also {
// Insert animation are really annoying the first time the list is shown
@ -238,18 +254,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
refreshKeysManagementSection()
// Analytics
setUpAnalytics()
// Analytics tracking management
findPreference<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
}
}
// Pin code
openPinCodeSettingsPref.setOnPreferenceClickListener {
openPinCodePreferenceScreen()
true
@ -274,6 +281,34 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeAnalyticsState()
}
private fun observeAnalyticsState() {
analyticsConsentViewModel.onEach(AnalyticsConsentViewState::userConsent) {
analyticsConsent.isChecked = it
}
}
private fun setUpAnalytics() {
analyticsCategory.isVisible = analyticsConfig.isEnabled
analyticsConsent.setOnPreferenceChangeListener { _, newValue ->
val newValueBool = newValue as? Boolean ?: false
if (newValueBool) {
// User wants to enable analytics, display the opt in screen
navigator.openAnalyticsOptIn(requireContext())
} else {
// Just disable analytics
analyticsConsentViewModel.handle(AnalyticsConsentViewActions.SetUserConsent(false))
}
true
}
}
// Todo this should be refactored and use same state as 4S section
private fun refreshXSigningStatus() {
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()

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="disable">Disable</string>
<string name="action_return">Return</string>
<string name="action_enable">Enable</string>
<string name="action_not_now">Not now</string>
<!-- 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="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_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
android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY"
android:title="@string/settings_analytics"
app:isPreferenceVisible="@bool/false_not_implemented">
android:title="@string/settings_analytics">
<im.vector.app.core.preference.VectorSwitchPreference
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: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"
}