commit
f4cfb5d6d3
23
.github/workflows/sync-from-external-sources.yml
vendored
23
.github/workflows/sync-from-external-sources.yml
vendored
@ -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
|
1
.idea/dictionaries/bmarty.xml
generated
1
.idea/dictionaries/bmarty.xml
generated
@ -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>
|
||||
|
@ -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
16
docs/analytics.md
Normal 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.
|
6
library/ui-styles/src/main/res/values-sw600dp/tablet.xml
Normal file
6
library/ui-styles/src/main/res/values-sw600dp/tablet.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<dimen name="width_percent">0.6</dimen>
|
||||
|
||||
</resources>
|
6
library/ui-styles/src/main/res/values-sw720dp/tablet.xml
Normal file
6
library/ui-styles/src/main/res/values-sw720dp/tablet.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<dimen name="width_percent">0.5</dimen>
|
||||
|
||||
</resources>
|
6
library/ui-styles/src/main/res/values/tablet.xml
Normal file
6
library/ui-styles/src/main/res/values/tablet.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<dimen name="width_percent">1</dimen>
|
||||
|
||||
</resources>
|
@ -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
18
tools/import_analytic_plan.sh
Executable 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."
|
@ -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'
|
||||
|
@ -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" />
|
||||
|
@ -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"
|
||||
}
|
@ -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))
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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)
|
||||
|
@ -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"
|
||||
|
45
vector/src/debug/res/layout/fragment_debug_analytics.xml
Normal file
45
vector/src/debug/res/layout/fragment_debug_analytics.xml
Normal 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>
|
@ -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" />
|
||||
|
@ -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/>
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
)
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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>?
|
||||
}
|
@ -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>?
|
||||
}
|
@ -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")
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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?) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
57
vector/src/main/res/drawable/element_logo_stars.xml
Normal file
57
vector/src/main/res/drawable/element_logo_stars.xml
Normal 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>
|
20
vector/src/main/res/drawable/ic_list_item_bullet.xml
Normal file
20
vector/src/main/res/drawable/ic_list_item_bullet.xml
Normal 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>
|
143
vector/src/main/res/layout/fragment_analytics_optin.xml
Normal file
143
vector/src/main/res/layout/fragment_analytics_optin.xml
Normal 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>
|
@ -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, we’ll 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>
|
||||
|
||||
|
@ -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" />
|
||||
|
||||
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue
Block a user