diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml index 2da8e10542..5a5d8152ff 100644 --- a/.github/workflows/sync-from-external-sources.yml +++ b/.github/workflows/sync-from-external-sources.yml @@ -70,4 +70,27 @@ jobs: body: | - Update SAS Strings from matrix-doc. branch: sync-sas-strings + base: develop + + sync-analytics-plan: + runs-on: ubuntu-latest + # Skip in forks + if: github.repository == 'vector-im/element-android' + steps: + - uses: actions/checkout@v2 + - name: Run analytics import script + run: ./tools/import_analytic_plan.sh + - name: Create Pull Request for analytics plan + uses: peter-evans/create-pull-request@v3 + with: + commit-message: Sync analytics plan + title: Sync analytics plan + body: | + ### Update analytics plan + Reviewers: + - [ ] Please remove usage of Event or Enum which may have been removed or updated + - [ ] please ensure new Events or new Enums are used to send analytics by pushing new commit(s) to this PR. + + *Note*: Change are coming from [this project](https://github.com/matrix-org/matrix-analytics-events) + branch: sync-analytics-plan base: develop \ No newline at end of file diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index e143720aa9..a2e408b50d 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -24,6 +24,7 @@ pbkdf pids pkcs + posthog previewable previewables pstn diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index c677290adc..25a78bc0c3 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -96,6 +96,7 @@ ext.groups = [ 'com.parse.bolts', 'com.pinterest', 'com.pinterest.ktlint', + 'com.posthog.android', 'com.squareup', 'com.squareup.duktape', 'com.squareup.moshi', diff --git a/docs/analytics.md b/docs/analytics.md new file mode 100644 index 0000000000..135ace81b0 --- /dev/null +++ b/docs/analytics.md @@ -0,0 +1,16 @@ +# Analytics in Element + +## Solution + +Element is using PostHog to send analytics event. +We ask for the user to give consent before sending any analytics data. + +## How to add a new Event + +The analytics plan is shared between all Element clients. To add an Event, please open a PR to this project: https://github.com/matrix-org/matrix-analytics-events + +Then, once the PR has been merged, you can run the tool `import_analytic_plan.sh` to import the plan to Element, and then you can use the new Event. Note that this tool is run by Github action once a week. + +## Forks of Element + +Analytics on forks are disabled by default. Please refer to AnalyticsConfig and there implementation to setup analytics on your project. diff --git a/library/ui-styles/src/main/res/values-sw600dp/tablet.xml b/library/ui-styles/src/main/res/values-sw600dp/tablet.xml new file mode 100644 index 0000000000..39f467cf0d --- /dev/null +++ b/library/ui-styles/src/main/res/values-sw600dp/tablet.xml @@ -0,0 +1,6 @@ + + + + 0.6 + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values-sw720dp/tablet.xml b/library/ui-styles/src/main/res/values-sw720dp/tablet.xml new file mode 100644 index 0000000000..4afbe6c773 --- /dev/null +++ b/library/ui-styles/src/main/res/values-sw720dp/tablet.xml @@ -0,0 +1,6 @@ + + + + 0.5 + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/tablet.xml b/library/ui-styles/src/main/res/values/tablet.xml new file mode 100644 index 0000000000..a5df8fe17c --- /dev/null +++ b/library/ui-styles/src/main/res/values/tablet.xml @@ -0,0 +1,6 @@ + + + + 1 + + \ No newline at end of file diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index bbd6105b15..6ca86be095 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===110 +enum class===114 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/tools/import_analytic_plan.sh b/tools/import_analytic_plan.sh new file mode 100755 index 0000000000..9c020a8e37 --- /dev/null +++ b/tools/import_analytic_plan.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +echo "Deleted existing plan..." +rm vector/src/main/java/im/vector/app/features/analytics/plan/*.* + +echo "Cloning analytics project..." +mkdir analytics_tmp +cd analytics_tmp +git clone https://github.com/matrix-org/matrix-analytics-events.git + +echo "Copy plan..." +cp matrix-analytics-events/types/kotlin2/* ../vector/src/main/java/im/vector/app/features/analytics/plan/ + +echo "Cleanup." +cd .. +rm -rf analytics_tmp + +echo "Done." diff --git a/vector/build.gradle b/vector/build.gradle index 4c864f9309..a578fdb52f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -439,6 +439,9 @@ dependencies { implementation libs.dagger.hilt kapt libs.dagger.hiltCompiler + // Analytics + implementation 'com.posthog.android:posthog:1.1.2' + // gplay flavor only gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml index ceeb0353db..0b2b5cf90f 100644 --- a/vector/src/debug/AndroidManifest.xml +++ b/vector/src/debug/AndroidManifest.xml @@ -5,6 +5,7 @@ + diff --git a/vector/src/debug/java/im/vector/app/config/AnalyticsConfig.kt b/vector/src/debug/java/im/vector/app/config/AnalyticsConfig.kt new file mode 100644 index 0000000000..34f2d4f92b --- /dev/null +++ b/vector/src/debug/java/im/vector/app/config/AnalyticsConfig.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.config + +import im.vector.app.BuildConfig +import im.vector.app.features.analytics.AnalyticsConfig + +val analyticsConfig: AnalyticsConfig = object : AnalyticsConfig { + override val isEnabled = BuildConfig.APPLICATION_ID == "im.vector.app.debug" + override val postHogHost = "https://posthog-poc.lab.element.dev" + override val postHogApiKey = "rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8" + override val policyLink = "https://element.io/cookie-policy" +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index 4916ab1e5d..a2b2b44ce3 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -34,6 +34,7 @@ import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityDebugMenuBinding +import im.vector.app.features.debug.analytics.DebugAnalyticsActivity import im.vector.app.features.debug.features.DebugFeaturesSettingsActivity import im.vector.app.features.debug.sas.DebugSasEmojiActivity import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity @@ -79,6 +80,9 @@ class DebugMenuActivity : VectorBaseActivity() { private fun setupViews() { views.debugFeatures.setOnClickListener { startActivity(Intent(this, DebugFeaturesSettingsActivity::class.java)) } views.debugPrivateSetting.setOnClickListener { openPrivateSettings() } + views.debugAnalytics.setOnClickListener { + startActivity(Intent(this, DebugAnalyticsActivity::class.java)) + } views.debugTestTextViewLink.setOnClickListener { testTextViewLink() } views.debugOpenButtonStylesLight.setOnClickListener { startActivity(Intent(this, DebugVectorButtonStylesLightActivity::class.java)) diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt new file mode 100644 index 0000000000..61883251ce --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsActivity.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.debug.analytics + +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding + +@AndroidEntryPoint +class DebugAnalyticsActivity : VectorBaseActivity() { + + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + + override fun initUiAndData() { + if (isFirstCreation()) { + addFragment( + views.simpleFragmentContainer, + DebugAnalyticsFragment::class.java + ) + } + } +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt new file mode 100644 index 0000000000..eb23fe6383 --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsFragment.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.debug.analytics + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.toOnOff +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentDebugAnalyticsBinding +import me.gujun.android.span.span + +class DebugAnalyticsFragment : VectorBaseFragment() { + + private val viewModel: DebugAnalyticsViewModel by fragmentViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDebugAnalyticsBinding { + return FragmentDebugAnalyticsBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setViewListeners() + } + + private fun setViewListeners() { + views.showAnalyticsOptIn.onClick { + navigator.openAnalyticsOptIn(requireContext()) + } + views.resetAnalyticsOptInDisplayed.onClick { + viewModel.handle(DebugAnalyticsViewActions.ResetAnalyticsOptInDisplayed) + } + } + + override fun invalidate() = withState(viewModel) { state -> + views.analyticsStoreContent.text = span { + +"AnalyticsId: " + span { + textStyle = "bold" + text = state.analyticsId.orEmpty() + } + +"\nOptIn: " + span { + textStyle = "bold" + text = state.userConsent.toOnOff() + } + +"\nDidAsk: " + span { + textStyle = "bold" + text = state.didAskUserConsent.toString() + } + } + } +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt new file mode 100644 index 0000000000..e1a7ce36fd --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewActions.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.debug.analytics + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface DebugAnalyticsViewActions : VectorViewModelAction { + object ResetAnalyticsOptInDisplayed : DebugAnalyticsViewActions +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt new file mode 100644 index 0000000000..03e416813a --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewModel.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.debug.analytics + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.store.AnalyticsStore +import kotlinx.coroutines.launch + +class DebugAnalyticsViewModel @AssistedInject constructor( + @Assisted initialState: DebugAnalyticsViewState, + private val analyticsStore: AnalyticsStore +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DebugAnalyticsViewState): DebugAnalyticsViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + observerStore() + } + + private fun observerStore() { + analyticsStore.analyticsIdFlow.setOnEach { copy(analyticsId = it) } + analyticsStore.userConsentFlow.setOnEach { copy(userConsent = it) } + analyticsStore.didAskUserConsentFlow.setOnEach { copy(didAskUserConsent = it) } + } + + override fun handle(action: DebugAnalyticsViewActions) { + when (action) { + DebugAnalyticsViewActions.ResetAnalyticsOptInDisplayed -> handleResetAnalyticsOptInDisplayed() + }.exhaustive + } + + private fun handleResetAnalyticsOptInDisplayed() { + viewModelScope.launch { + analyticsStore.setDidAskUserConsent(false) + } + } +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt new file mode 100644 index 0000000000..8e7afb39ef --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/analytics/DebugAnalyticsViewState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.debug.analytics + +import com.airbnb.mvrx.MavericksState + +data class DebugAnalyticsViewState( + val analyticsId: String? = null, + val userConsent: Boolean = false, + val didAskUserConsent: Boolean = false +) : MavericksState diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt b/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt index 8be4470b3f..6ef7fe441a 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/di/MavericksViewModelDebugModule.kt @@ -23,12 +23,18 @@ import dagger.multibindings.IntoMap import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksViewModelComponent import im.vector.app.core.di.MavericksViewModelKey +import im.vector.app.features.debug.analytics.DebugAnalyticsViewModel import im.vector.app.features.debug.settings.DebugPrivateSettingsViewModel @InstallIn(MavericksViewModelComponent::class) @Module interface MavericksViewModelDebugModule { + @Binds + @IntoMap + @MavericksViewModelKey(DebugAnalyticsViewModel::class) + fun debugAnalyticsViewModelFactory(factory: DebugAnalyticsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(DebugPrivateSettingsViewModel::class) diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml index 7aa69becde..8b38c17b35 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -32,6 +32,12 @@ android:layout_height="wrap_content" android:text="Private settings" /> +