1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Countly reporting fundation

This commit is contained in:
Valere
2020-10-23 09:53:10 +02:00
parent 45edf6025e
commit 9c932549b9
15 changed files with 409 additions and 27 deletions

View File

@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.util package org.matrix.android.sdk.internal.util
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Locale
/** /**
* Compute a Hash of a String, using md5 algorithm * Compute a Hash of a String, using md5 algorithm
@@ -31,3 +32,14 @@ fun String.md5() = try {
// Should not happen, but just in case // Should not happen, but just in case
hashCode().toString() hashCode().toString()
} }
fun String.sha256() = try {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(toByteArray())
digest.digest()
.joinToString("") { String.format("%02X", it) }
.toLowerCase(Locale.getDefault())
} catch (exc: Exception) {
// Should not happen, but just in case
hashCode().toString()
}

View File

@@ -441,6 +441,9 @@ dependencies {
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.3'
implementation 'me.dm7.barcodescanner:zxing:1.9.13' implementation 'me.dm7.barcodescanner:zxing:1.9.13'
// Analytics
implementation 'ly.count.android:sdk:20.04.2'
// TESTS // TESTS
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
testImplementation "org.amshove.kluent:kluent-android:$kluent_version" testImplementation "org.amshove.kluent:kluent-android:$kluent_version"

View File

@@ -43,6 +43,7 @@ import im.vector.app.core.di.VectorComponent
import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.rx.RxConfig import im.vector.app.core.rx.RxConfig
import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.call.WebRtcPeerConnectionManager
import im.vector.app.core.utils.AnalyticsEngine
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
@@ -91,6 +92,7 @@ class VectorApplication :
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker @Inject lateinit var pinLocker: PinLocker
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
@Inject lateinit var analyticsEngine: AnalyticsEngine
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
@@ -114,7 +116,6 @@ class VectorApplication :
vectorComponent.inject(this) vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this) vectorUncaughtExceptionHandler.activate(this)
rxConfig.setupRxPlugin() rxConfig.setupRxPlugin()
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
} }
@@ -128,7 +129,7 @@ class VectorApplication :
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager, analyticsEngine))
val fontRequest = FontRequest( val fontRequest = FontRequest(
"com.google.android.gms.fonts", "com.google.android.gms.fonts",
"com.google.android.gms", "com.google.android.gms",
@@ -155,6 +156,7 @@ class VectorApplication :
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession) activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(applicationContext) lastAuthenticatedSession.configureAndStart(applicationContext)
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.Init(lastAuthenticatedSession))
} }
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver { ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)

View File

@@ -28,6 +28,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.AssetReader
import im.vector.app.core.utils.AnalyticsEngine
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.call.WebRtcPeerConnectionManager
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
@@ -151,6 +152,8 @@ interface VectorComponent {
fun reAuthHelper(): ReAuthHelper fun reAuthHelper(): ReAuthHelper
fun countlyProvider(): AnalyticsEngine
fun pinLocker(): PinLocker fun pinLocker(): PinLocker
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager

View File

@@ -0,0 +1,246 @@
/*
* 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.core.utils
import android.app.Activity
import android.content.Context
import im.vector.app.BuildConfig
import im.vector.app.core.extensions.exhaustive
import im.vector.app.features.home.RoomListDisplayMode
import ly.count.android.sdk.Countly
import ly.count.android.sdk.CountlyConfig
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.internal.util.sha256
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AnalyticsEngine @Inject constructor(private val context: Context) {
sealed class AnalyticEvent {
data class Init(val session: Session) : AnalyticEvent()
// Used for session detection
data class StartActivity(val activity: Activity) : AnalyticEvent()
data class EndActivity(val activity: Activity) : AnalyticEvent()
data class RoomView(val roomId: String, val isEncrypted: Boolean, val memberCount: Int, val isPublic: Boolean) : AnalyticEvent()
data class HomeView(val mode: RoomListDisplayMode) : AnalyticEvent()
data class StartCall(val roomId: String, val isVideo: Boolean) : AnalyticEvent()
data class JoinCall(val roomId: String, val isVideo: Boolean) : AnalyticEvent()
data class StartJitsiConf(val roomId: String, val isVideo: Boolean) : AnalyticEvent()
data class JoinConference(val roomId: String, val isVideo: Boolean) : AnalyticEvent()
data class SendText(val roomId: String) : AnalyticEvent()
data class SendReply(val roomId: String) : AnalyticEvent()
data class SendQuote(val roomId: String) : AnalyticEvent()
data class SendEdit(val roomId: String) : AnalyticEvent()
data class SendMedia(val roomId: String, val type: ContentAttachmentData.Type) : AnalyticEvent()
object StartRoomDirectory : AnalyticEvent()
object EndRoomDirectory : AnalyticEvent()
object StartRoomDirectorySearch : AnalyticEvent()
data class EndRoomDirectorySearch(val resultCount: Int) : AnalyticEvent()
object StartJoinRoomEvent : AnalyticEvent()
data class EndJoinRoomEvent(val roomId: String, val isEncrypted: Boolean, val memberCount: Int, val isPublic: Boolean): AnalyticEvent()
object CancelJoinRoomEvent : AnalyticEvent()
}
private var countly: Countly? = null
fun report(event: AnalyticEvent) {
when (event) {
is AnalyticEvent.Init -> {
val session = event.session
CountlyConfig(context, "8abf1ee15646bc884556b82e5053857904264b66", "https://try.count.ly/").let {
if (BuildConfig.DEBUG) {
it.setLoggingEnabled(true)
}
it.setDeviceId(session.sessionId)
Countly.sharedInstance().init(it).also {
countly = it
}
Countly.userData.setUserData(
mapOf("name" to session.myUserId),
mapOf("home_server" to session.sessionParams.homeServerHost)
)
Countly.userData.save()
}
}
is AnalyticEvent.RoomView -> {
countly?.views()?.recordView(
"view_room",
mapOf(
"room_id" to event.roomId.sha256(),
"is_encrypted" to event.isEncrypted,
"num_users" to event.memberCount,
"is_public" to event.isPublic
)
)
Unit
}
is AnalyticEvent.HomeView -> {
countly?.views()?.recordView(
"view_home",
mapOf<String, Any>("filter" to event.mode.name)
)
Unit
}
is AnalyticEvent.StartCall -> {
countly?.events()?.recordEvent(
"start_call",
mapOf<String, Any>(
"room_id" to event.roomId.sha256(),
"is_video" to event.isVideo,
"is_jitsi" to false
)
)
}
is AnalyticEvent.StartJitsiConf -> {
countly?.events()?.recordEvent(
"start_call",
mapOf<String, Any>(
"room_id" to event.roomId.sha256(),
"is_video" to event.isVideo,
"is_jitsi" to true
)
)
}
is AnalyticEvent.SendText -> {
countly?.events()?.recordEvent(
"send_message",
mapOf<String, Any>(
"room_id" to event.roomId.sha256(),
"is_edit" to false,
"is_reply" to false,
"message_type" to "Text"
)
)
}
is AnalyticEvent.SendReply -> {
countly?.events()?.recordEvent(
"send_message",
mapOf<String, Any>(
"room_id" to event.roomId.sha256(),
"is_edit" to false,
"is_reply" to true,
"message_type" to "Text"
)
)
}
is AnalyticEvent.SendQuote -> {
countly?.events()?.recordEvent(
"send_message",
mapOf<String, Any>(
"room_id" to event.roomId.sha256(),
"is_edit" to false,
"is_reply" to false,
"message_type" to "Text"
)
)
}
is AnalyticEvent.SendMedia -> {
countly?.events()?.recordEvent(
"send_message",
mapOf<String, Any>(
"room_id" to event.roomId.sha256(),
"is_edit" to false,
"is_reply" to false,
"message_type" to when (event.type) {
ContentAttachmentData.Type.FILE -> "File"
ContentAttachmentData.Type.IMAGE -> "Image"
ContentAttachmentData.Type.AUDIO -> "Audio"
ContentAttachmentData.Type.VIDEO -> "Video"
}
)
)
}
is AnalyticEvent.SendEdit -> {
countly?.events()?.recordEvent(
"send_message",
mapOf<String, Any>(
"room_id" to event.roomId.sha256(),
"is_edit" to true,
"is_reply" to false,
"message_type" to "Text"
)
)
}
is AnalyticEvent.JoinCall -> {
countly?.events()?.recordEvent(
"join_call",
mapOf<String, Any> (
"room_id" to event.roomId.sha256(),
"is_video" to event.isVideo,
"is_jitsi" to false
)
)
}
is AnalyticEvent.JoinConference -> {
countly?.events()?.recordEvent(
"join_call",
mapOf<String, Any> (
"room_id" to event.roomId.sha256(),
"is_video" to event.isVideo,
"is_jitsi" to true
)
)
}
is AnalyticEvent.StartActivity -> {
countly?.onStart(event.activity)
}
is AnalyticEvent.EndActivity -> {
countly?.onStop()
}
AnalyticEvent.StartRoomDirectory -> {
countly?.events()?.startEvent("room_directory")
Unit
}
AnalyticEvent.EndRoomDirectory -> {
countly?.events()?.endEvent("room_directory")
Unit
}
AnalyticEvent.StartRoomDirectorySearch -> {
countly?.events()?.startEvent("room_directory_search")
Unit
}
is AnalyticEvent.EndRoomDirectorySearch -> {
countly?.events()?.endEvent("room_directory_search", mapOf("result_count" to event.resultCount), 1, 0.0)
Unit
}
AnalyticEvent.StartJoinRoomEvent -> {
countly?.events()?.startEvent("join_room")
Unit
}
is AnalyticEvent.EndJoinRoomEvent -> {
countly?.events()?.endEvent(
"join_room",
mapOf<String, Any>("room_id" to event.roomId.sha256()),
1,
0.0
)
Unit
}
AnalyticEvent.CancelJoinRoomEvent -> {
countly?.events()?.cancelEvent("join_room")
Unit
}
}.exhaustive
}
}

View File

@@ -27,6 +27,7 @@ import im.vector.app.core.services.BluetoothHeadsetReceiver
import im.vector.app.core.services.CallService import im.vector.app.core.services.CallService
import im.vector.app.core.services.WiredHeadsetStateReceiver import im.vector.app.core.services.WiredHeadsetStateReceiver
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import im.vector.app.core.utils.AnalyticsEngine
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject import io.reactivex.subjects.ReplaySubject
@@ -75,7 +76,8 @@ import javax.inject.Singleton
@Singleton @Singleton
class WebRtcPeerConnectionManager @Inject constructor( class WebRtcPeerConnectionManager @Inject constructor(
private val context: Context, private val context: Context,
private val activeSessionDataSource: ActiveSessionDataSource private val activeSessionDataSource: ActiveSessionDataSource,
private val analyticsEngine: AnalyticsEngine
) : CallsListener, LifecycleObserver { ) : CallsListener, LifecycleObserver {
private val currentSession: Session? private val currentSession: Session?
@@ -543,6 +545,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
getTurnServer { turnServer -> getTurnServer { turnServer ->
internalAcceptIncomingCall(currentCall!!, turnServer) internalAcceptIncomingCall(currentCall!!, turnServer)
} }
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.JoinCall(mxCall.roomId, mxCall.isVideoCall))
} }
} }

View File

@@ -24,7 +24,7 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.di.HasScreenInjector
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.AnalyticsEngine
import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
@@ -41,7 +41,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val selectedGroupStore: SelectedGroupDataSource, private val selectedGroupStore: SelectedGroupDataSource,
private val homeRoomListStore: HomeRoomListDataSource, private val homeRoomListStore: HomeRoomListDataSource,
private val stringProvider: StringProvider) private val analyticsEngine: AnalyticsEngine)
: VectorViewModel<HomeDetailViewState, HomeDetailAction, EmptyViewEvents>(initialState) { : VectorViewModel<HomeDetailViewState, HomeDetailAction, EmptyViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
@@ -82,7 +82,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
setState { setState {
copy(displayMode = action.displayMode) copy(displayMode = action.displayMode)
} }
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.HomeView(action.displayMode))
uiStateRepository.storeDisplayMode(action.displayMode) uiStateRepository.storeDisplayMode(action.displayMode)
} }
} }

View File

@@ -90,4 +90,5 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
object ReportView: RoomDetailAction()
} }

View File

@@ -90,6 +90,7 @@ import im.vector.app.core.ui.views.ActiveCallViewHolder
import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.ActiveConferenceView
import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.AnalyticsEngine
import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.Debouncer
import im.vector.app.core.utils.KeyboardStateUtils import im.vector.app.core.utils.KeyboardStateUtils
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
@@ -221,7 +222,8 @@ class RoomDetailFragment @Inject constructor(
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val matrixItemColorProvider: MatrixItemColorProvider, private val matrixItemColorProvider: MatrixItemColorProvider,
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
private val analyticsEngine: AnalyticsEngine
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
@@ -372,6 +374,11 @@ class RoomDetailFragment @Inject constructor(
} }
} }
override fun onStart() {
super.onStart()
roomDetailViewModel.handle(RoomDetailAction.ReportView)
}
private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) { private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
navigator.openRoom(requireContext(), openRoom.roomId, null) navigator.openRoom(requireContext(), openRoom.roomId, null)
} }
@@ -445,6 +452,7 @@ class RoomDetailFragment @Inject constructor(
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.JoinConference(roomId = roomDetailArgs.roomId, isVideo = enableVideo))
} }
private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) { private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) {
@@ -653,36 +661,36 @@ class RoomDetailFragment @Inject constructor(
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.invite -> { R.id.invite -> {
navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId) navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId)
true true
} }
R.id.timeline_setting -> { R.id.timeline_setting -> {
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
true true
} }
R.id.resend_all -> { R.id.resend_all -> {
roomDetailViewModel.handle(RoomDetailAction.ResendAll) roomDetailViewModel.handle(RoomDetailAction.ResendAll)
true true
} }
R.id.open_matrix_apps -> { R.id.open_matrix_apps -> {
roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations) roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
true true
} }
R.id.voice_call, R.id.voice_call,
R.id.video_call -> { R.id.video_call -> {
handleCallRequest(item) handleCallRequest(item)
true true
} }
R.id.hangup_call -> { R.id.hangup_call -> {
roomDetailViewModel.handle(RoomDetailAction.EndCall) roomDetailViewModel.handle(RoomDetailAction.EndCall)
true true
} }
R.id.search -> { R.id.search -> {
handleSearchAction() handleSearchAction()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View File

@@ -31,6 +31,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.AnalyticsEngine
import im.vector.app.core.utils.subscribeLogError import im.vector.app.core.utils.subscribeLogError
import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.call.WebRtcPeerConnectionManager
import im.vector.app.features.command.CommandParser import im.vector.app.features.command.CommandParser
@@ -74,6 +75,8 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -115,7 +118,8 @@ class RoomDetailViewModel @AssistedInject constructor(
private val roomSummaryHolder: RoomSummaryHolder, private val roomSummaryHolder: RoomSummaryHolder,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
timelineSettingsFactory: TimelineSettingsFactory timelineSettingsFactory: TimelineSettingsFactory,
private val analyticsEngine: AnalyticsEngine
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener { ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
@@ -275,6 +279,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.ReportView -> handleReportView()
}.exhaustive }.exhaustive
} }
@@ -295,6 +300,23 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleReportView() {
viewModelScope.launch {
val roomSummary = room.roomSummary()
val joinRules = room.getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.IsNotEmpty)
?.content.toModel<RoomJoinRulesContent>()
?.joinRules
analyticsEngine.report(
AnalyticsEngine.AnalyticEvent.RoomView(
roomId = room.roomId,
isEncrypted = roomSummary?.isEncrypted ?: false,
memberCount = roomSummary?.joinedMembersCount ?: 0,
isPublic = joinRules == RoomJoinRules.PUBLIC
)
)
}
}
private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) {
room.getUserReadReceipt(action.userId) room.getUserReadReceipt(action.userId)
?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) }
@@ -308,6 +330,12 @@ class RoomDetailViewModel @AssistedInject constructor(
room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { room.roomSummary()?.otherMemberIds?.firstOrNull()?.let {
webRtcPeerConnectionManager.startOutgoingCall(room.roomId, it, action.isVideo) webRtcPeerConnectionManager.startOutgoingCall(room.roomId, it, action.isVideo)
} }
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartCall(
roomId = room.roomId,
isVideo = action.isVideo
))
}
} }
private fun handleEndCall() { private fun handleEndCall() {
@@ -403,6 +431,15 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.HideWaitingView) _viewEvents.post(RoomDetailViewEvents.HideWaitingView)
} }
} }
viewModelScope.launch {
analyticsEngine.report(
AnalyticsEngine.AnalyticEvent.StartJitsiConf(
roomId = room.roomId,
isVideo = action.withVideo
)
)
}
} }
private fun handleDeleteWidget(widgetId: String) { private fun handleDeleteWidget(widgetId: String) {
@@ -559,16 +596,16 @@ class RoomDetailViewModel @AssistedInject constructor(
return@withState false return@withState false
} }
when (itemId) { when (itemId) {
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.timeline_setting -> true R.id.timeline_setting -> true
R.id.invite -> state.canInvite R.id.invite -> state.canInvite
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, R.id.voice_call,
R.id.video_call -> true // always show for discoverability R.id.video_call -> true // always show for discoverability
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
R.id.search -> true R.id.search -> true
else -> false else -> false
} }
} }
@@ -582,6 +619,9 @@ class RoomDetailViewModel @AssistedInject constructor(
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId))
}
_viewEvents.post(RoomDetailViewEvents.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
@@ -598,6 +638,9 @@ class RoomDetailViewModel @AssistedInject constructor(
// Send the text message to the room, without markdown // Send the text message to the room, without markdown
room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
_viewEvents.post(RoomDetailViewEvents.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId))
}
popDraft() popDraft()
} }
is ParsedCommand.Invite -> { is ParsedCommand.Invite -> {
@@ -650,6 +693,9 @@ class RoomDetailViewModel @AssistedInject constructor(
is ParsedCommand.SendRainbow -> { is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let { slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId))
}
} }
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
@@ -657,6 +703,9 @@ class RoomDetailViewModel @AssistedInject constructor(
is ParsedCommand.SendRainbowEmote -> { is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let { slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId))
}
} }
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
@@ -728,6 +777,9 @@ class RoomDetailViewModel @AssistedInject constructor(
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
action.text, action.text,
action.autoMarkdown) action.autoMarkdown)
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendEdit(room.roomId))
}
} else { } else {
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
@@ -755,12 +807,19 @@ class RoomDetailViewModel @AssistedInject constructor(
} else { } else {
room.sendFormattedTextMessage(finalText, htmlText) room.sendFormattedTextMessage(finalText, htmlText)
} }
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId))
}
_viewEvents.post(RoomDetailViewEvents.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
is SendMode.REPLY -> { is SendMode.REPLY -> {
state.sendMode.timelineEvent.let { state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown) room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
viewModelScope.launch {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendReply(room.roomId))
}
_viewEvents.post(RoomDetailViewEvents.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
@@ -922,6 +981,11 @@ class RoomDetailViewModel @AssistedInject constructor(
)) ))
} }
} }
viewModelScope.launch {
attachments.forEach {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendMedia(roomId = room.roomId, type = it.type))
}
}
} }
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {

View File

@@ -19,9 +19,12 @@ package im.vector.app.features.lifecycle
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
import im.vector.app.core.utils.AnalyticsEngine
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager: PopupAlertManager) : Application.ActivityLifecycleCallbacks { class VectorActivityLifecycleCallbacks constructor(
private val popupAlertManager: PopupAlertManager,
private val analyticsEngine: AnalyticsEngine) : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) { override fun onActivityPaused(activity: Activity) {
} }
@@ -30,6 +33,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager
} }
override fun onActivityStarted(activity: Activity) { override fun onActivityStarted(activity: Activity) {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartActivity(activity))
} }
override fun onActivityDestroyed(activity: Activity) { override fun onActivityDestroyed(activity: Activity) {
@@ -39,6 +43,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager
} }
override fun onActivityStopped(activity: Activity) { override fun onActivityStopped(activity: Activity) {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndActivity(activity))
} }
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {

View File

@@ -34,6 +34,7 @@ import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.AnalyticsEngine
import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.core.utils.ensureTrailingSlash
import im.vector.app.features.signout.soft.SoftLogoutActivity import im.vector.app.features.signout.soft.SoftLogoutActivity
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
@@ -63,7 +64,8 @@ class LoginViewModel @AssistedInject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider private val stringProvider: StringProvider,
private val analyticsEngine: AnalyticsEngine
) : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) { ) : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
@@ -686,6 +688,7 @@ class LoginViewModel @AssistedInject constructor(
activeSessionHolder.setActiveSession(session) activeSessionHolder.setActiveSession(session)
authenticationService.reset() authenticationService.reset()
session.configureAndStart(applicationContext) session.configureAndStart(applicationContext)
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.Init(session))
setState { setState {
copy( copy(
asyncLoginAction = Success(Unit) asyncLoginAction = Success(Unit)

View File

@@ -95,6 +95,16 @@ class PublicRoomsFragment @Inject constructor(
}.exhaustive }.exhaustive
} }
override fun onStart() {
super.onStart()
viewModel.handle(RoomDirectoryAction.StartReport)
}
override fun onStop() {
super.onStop()
viewModel.handle(RoomDirectoryAction.EndReport)
}
override fun onDestroyView() { override fun onDestroyView() {
publicRoomsController.callback = null publicRoomsController.callback = null
publicRoomsList.cleanup() publicRoomsList.cleanup()

View File

@@ -24,4 +24,6 @@ sealed class RoomDirectoryAction : VectorViewModelAction {
data class FilterWith(val filter: String) : RoomDirectoryAction() data class FilterWith(val filter: String) : RoomDirectoryAction()
object LoadMore : RoomDirectoryAction() object LoadMore : RoomDirectoryAction()
data class JoinRoom(val roomId: String) : RoomDirectoryAction() data class JoinRoom(val roomId: String) : RoomDirectoryAction()
object StartReport : RoomDirectoryAction()
object EndReport : RoomDirectoryAction()
} }

View File

@@ -26,6 +26,7 @@ import com.airbnb.mvrx.appendAt
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.AnalyticsEngine
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
@@ -43,6 +44,7 @@ import timber.log.Timber
private const val PUBLIC_ROOMS_LIMIT = 20 private const val PUBLIC_ROOMS_LIMIT = 20
class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState, class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState,
private val analyticsEngine: AnalyticsEngine,
private val session: Session) private val session: Session)
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, RoomDirectoryViewEvents>(initialState) { : VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, RoomDirectoryViewEvents>(initialState) {
@@ -105,6 +107,12 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
is RoomDirectoryAction.FilterWith -> filterWith(action) is RoomDirectoryAction.FilterWith -> filterWith(action)
RoomDirectoryAction.LoadMore -> loadMore() RoomDirectoryAction.LoadMore -> loadMore()
is RoomDirectoryAction.JoinRoom -> joinRoom(action) is RoomDirectoryAction.JoinRoom -> joinRoom(action)
RoomDirectoryAction.StartReport -> {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartRoomDirectory)
}
RoomDirectoryAction.EndReport -> {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndRoomDirectory)
}
} }
} }
@@ -154,6 +162,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
} }
private fun load(filter: String, roomDirectoryData: RoomDirectoryData) { private fun load(filter: String, roomDirectoryData: RoomDirectoryData) {
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartRoomDirectorySearch)
currentTask = session.getPublicRooms(roomDirectoryData.homeServer, currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
PublicRoomsParams( PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT, limit = PUBLIC_ROOMS_LIMIT,
@@ -178,11 +187,13 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
hasMore = since != null hasMore = since != null
) )
} }
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndRoomDirectorySearch(data.chunk?.size ?: 0))
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.Cancelled) { if (failure is Failure.Cancelled) {
// Ignore, another request should be already started // Ignore, another request should be already started
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndRoomDirectorySearch(0))
return return
} }
@@ -193,6 +204,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
asyncPublicRoomsRequest = Fail(failure) asyncPublicRoomsRequest = Fail(failure)
) )
} }
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndRoomDirectorySearch(0))
} }
}) })
} }
@@ -207,15 +219,23 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
val viaServers = state.roomDirectoryData.homeServer?.let { val viaServers = state.roomDirectoryData.homeServer?.let {
listOf(it) listOf(it)
} ?: emptyList() } ?: emptyList()
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartJoinRoomEvent)
session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback<Unit> { session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined // Instead, we wait for the room to be joined
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndJoinRoomEvent(
roomId = action.roomId,
isPublic = true,
memberCount = 0,
isEncrypted = false
))
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// Notify the user // Notify the user
_viewEvents.post(RoomDirectoryViewEvents.Failure(failure)) _viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
analyticsEngine.report(AnalyticsEngine.AnalyticEvent.CancelJoinRoomEvent)
} }
}) })
} }