From 9c932549b917d02edabd806f185628c8d3aae7a0 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 23 Oct 2020 09:53:10 +0200 Subject: [PATCH] Countly reporting fundation --- .../matrix/android/sdk/internal/util/Hash.kt | 12 + vector/build.gradle | 3 + .../java/im/vector/app/VectorApplication.kt | 6 +- .../im/vector/app/core/di/VectorComponent.kt | 3 + .../vector/app/core/utils/AnalyticsEngine.kt | 246 ++++++++++++++++++ .../call/WebRtcPeerConnectionManager.kt | 5 +- .../app/features/home/HomeDetailViewModel.kt | 6 +- .../home/room/detail/RoomDetailAction.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 26 +- .../home/room/detail/RoomDetailViewModel.kt | 84 +++++- .../VectorActivityLifecycleCallbacks.kt | 7 +- .../app/features/login/LoginViewModel.kt | 5 +- .../roomdirectory/PublicRoomsFragment.kt | 10 + .../roomdirectory/RoomDirectoryAction.kt | 2 + .../roomdirectory/RoomDirectoryViewModel.kt | 20 ++ 15 files changed, 409 insertions(+), 27 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/utils/AnalyticsEngine.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt index 3d80ad01d5..bd9ad33a8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import java.security.MessageDigest +import java.util.Locale /** * Compute a Hash of a String, using md5 algorithm @@ -31,3 +32,14 @@ fun String.md5() = try { // Should not happen, but just in case 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() +} diff --git a/vector/build.gradle b/vector/build.gradle index ca7cb12e31..55bbae28ac 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -441,6 +441,9 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' implementation 'me.dm7.barcodescanner:zxing:1.9.13' + // Analytics + implementation 'ly.count.android:sdk:20.04.2' + // TESTS testImplementation 'junit:junit:4.13' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 5be313d719..f23851a79f 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -43,6 +43,7 @@ import im.vector.app.core.di.VectorComponent import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.rx.RxConfig 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.disclaimer.doNotShowDisclaimerDialog import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks @@ -91,6 +92,7 @@ class VectorApplication : @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var pinLocker: PinLocker @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager + @Inject lateinit var analyticsEngine: AnalyticsEngine lateinit var vectorComponent: VectorComponent @@ -114,7 +116,6 @@ class VectorApplication : vectorComponent.inject(this) vectorUncaughtExceptionHandler.activate(this) rxConfig.setupRxPlugin() - if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } @@ -128,7 +129,7 @@ class VectorApplication : EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() - registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) + registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager, analyticsEngine)) val fontRequest = FontRequest( "com.google.android.gms.fonts", "com.google.android.gms", @@ -155,6 +156,7 @@ class VectorApplication : val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) lastAuthenticatedSession.configureAndStart(applicationContext) + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.Init(lastAuthenticatedSession)) } ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 28f3a52efa..81206a0782 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -28,6 +28,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.pushers.PushersManager 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.features.call.WebRtcPeerConnectionManager import im.vector.app.features.configuration.VectorConfiguration @@ -151,6 +152,8 @@ interface VectorComponent { fun reAuthHelper(): ReAuthHelper + fun countlyProvider(): AnalyticsEngine + fun pinLocker(): PinLocker fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager diff --git a/vector/src/main/java/im/vector/app/core/utils/AnalyticsEngine.kt b/vector/src/main/java/im/vector/app/core/utils/AnalyticsEngine.kt new file mode 100644 index 0000000000..feceb377b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/AnalyticsEngine.kt @@ -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("filter" to event.mode.name) + ) + Unit + } + is AnalyticEvent.StartCall -> { + countly?.events()?.recordEvent( + "start_call", + mapOf( + "room_id" to event.roomId.sha256(), + "is_video" to event.isVideo, + "is_jitsi" to false + ) + ) + } + is AnalyticEvent.StartJitsiConf -> { + countly?.events()?.recordEvent( + "start_call", + mapOf( + "room_id" to event.roomId.sha256(), + "is_video" to event.isVideo, + "is_jitsi" to true + ) + ) + } + is AnalyticEvent.SendText -> { + countly?.events()?.recordEvent( + "send_message", + mapOf( + "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( + "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( + "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( + "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( + "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 ( + "room_id" to event.roomId.sha256(), + "is_video" to event.isVideo, + "is_jitsi" to false + ) + ) + } + is AnalyticEvent.JoinConference -> { + countly?.events()?.recordEvent( + "join_call", + mapOf ( + "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("room_id" to event.roomId.sha256()), + 1, + 0.0 + ) + Unit + } + AnalyticEvent.CancelJoinRoomEvent -> { + countly?.events()?.cancelEvent("join_room") + Unit + } + }.exhaustive + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt index 86b38c1158..1725a76fe1 100644 --- a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt @@ -27,6 +27,7 @@ import im.vector.app.core.services.BluetoothHeadsetReceiver import im.vector.app.core.services.CallService import im.vector.app.core.services.WiredHeadsetStateReceiver import im.vector.app.push.fcm.FcmHelper +import im.vector.app.core.utils.AnalyticsEngine import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject @@ -75,7 +76,8 @@ import javax.inject.Singleton @Singleton class WebRtcPeerConnectionManager @Inject constructor( private val context: Context, - private val activeSessionDataSource: ActiveSessionDataSource + private val activeSessionDataSource: ActiveSessionDataSource, + private val analyticsEngine: AnalyticsEngine ) : CallsListener, LifecycleObserver { private val currentSession: Session? @@ -543,6 +545,7 @@ class WebRtcPeerConnectionManager @Inject constructor( getTurnServer { turnServer -> internalAcceptIncomingCall(currentCall!!, turnServer) } + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.JoinCall(mxCall.roomId, mxCall.isVideoCall)) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index 88c310fde8..4091de59bd 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -24,7 +24,7 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.platform.EmptyViewEvents 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.ui.UiStateRepository import io.reactivex.schedulers.Schedulers @@ -41,7 +41,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private val uiStateRepository: UiStateRepository, private val selectedGroupStore: SelectedGroupDataSource, private val homeRoomListStore: HomeRoomListDataSource, - private val stringProvider: StringProvider) + private val analyticsEngine: AnalyticsEngine) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -82,7 +82,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho setState { copy(displayMode = action.displayMode) } - + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.HomeView(action.displayMode)) uiStateRepository.storeDisplayMode(action.displayMode) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 99adc0bf83..3f53b6b591 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -90,4 +90,5 @@ sealed class RoomDetailAction : VectorViewModelAction { data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction() + object ReportView: RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9c6c473a7f..99d230b25f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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.JumpToReadMarkerView 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.KeyboardStateUtils 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 matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, - private val roomDetailPendingActionStore: RoomDetailPendingActionStore + private val roomDetailPendingActionStore: RoomDetailPendingActionStore, + private val analyticsEngine: AnalyticsEngine ) : VectorBaseFragment(), 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) { navigator.openRoom(requireContext(), openRoom.roomId, null) } @@ -445,6 +452,7 @@ class RoomDetailFragment @Inject constructor( private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { 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) { @@ -653,36 +661,36 @@ class RoomDetailFragment @Inject constructor( override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.invite -> { + R.id.invite -> { navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId) true } - R.id.timeline_setting -> { + R.id.timeline_setting -> { navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) true } - R.id.resend_all -> { + R.id.resend_all -> { roomDetailViewModel.handle(RoomDetailAction.ResendAll) true } - R.id.open_matrix_apps -> { + R.id.open_matrix_apps -> { roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations) true } R.id.voice_call, - R.id.video_call -> { + R.id.video_call -> { handleCallRequest(item) true } - R.id.hangup_call -> { + R.id.hangup_call -> { roomDetailViewModel.handle(RoomDetailAction.EndCall) true } - R.id.search -> { + R.id.search -> { handleSearchAction() true } - else -> super.onOptionsItemSelected(item) + else -> super.onOptionsItemSelected(item) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 9efad1081f..3734c5b6a0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -31,6 +31,7 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive 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.core.utils.subscribeLogError import im.vector.app.features.call.WebRtcPeerConnectionManager 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.model.Membership 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.RoomSummary 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 typingHelper: TypingHelper, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, - timelineSettingsFactory: TimelineSettingsFactory + timelineSettingsFactory: TimelineSettingsFactory, + private val analyticsEngine: AnalyticsEngine ) : VectorViewModel(initialState), Timeline.Listener { private val room = session.getRoom(initialState.roomId)!! @@ -275,6 +279,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.ReportView -> handleReportView() }.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() + ?.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) { room.getUserReadReceipt(action.userId) ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } @@ -308,6 +330,12 @@ class RoomDetailViewModel @AssistedInject constructor( room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { webRtcPeerConnectionManager.startOutgoingCall(room.roomId, it, action.isVideo) } + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartCall( + roomId = room.roomId, + isVideo = action.isVideo + )) + } } private fun handleEndCall() { @@ -403,6 +431,15 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.HideWaitingView) } } + + viewModelScope.launch { + analyticsEngine.report( + AnalyticsEngine.AnalyticEvent.StartJitsiConf( + roomId = room.roomId, + isVideo = action.withVideo + ) + ) + } } private fun handleDeleteWidget(widgetId: String) { @@ -559,16 +596,16 @@ class RoomDetailViewModel @AssistedInject constructor( return@withState false } when (itemId) { - R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true - R.id.timeline_setting -> true - R.id.invite -> state.canInvite - R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true - R.id.open_matrix_apps -> true + R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true + R.id.timeline_setting -> true + R.id.invite -> state.canInvite + R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true + R.id.open_matrix_apps -> true R.id.voice_call, - R.id.video_call -> true // always show for discoverability - R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null - R.id.search -> true - else -> false + R.id.video_call -> true // always show for discoverability + R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null + R.id.search -> true + else -> false } } @@ -582,6 +619,9 @@ class RoomDetailViewModel @AssistedInject constructor( is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId)) + } _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } @@ -598,6 +638,9 @@ class RoomDetailViewModel @AssistedInject constructor( // Send the text message to the room, without markdown room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) _viewEvents.post(RoomDetailViewEvents.MessageSent) + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId)) + } popDraft() } is ParsedCommand.Invite -> { @@ -650,6 +693,9 @@ class RoomDetailViewModel @AssistedInject constructor( is ParsedCommand.SendRainbow -> { slashCommandResult.message.toString().let { room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId)) + } } _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() @@ -657,6 +703,9 @@ class RoomDetailViewModel @AssistedInject constructor( is ParsedCommand.SendRainbowEmote -> { slashCommandResult.message.toString().let { room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId)) + } } _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() @@ -728,6 +777,9 @@ class RoomDetailViewModel @AssistedInject constructor( messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendEdit(room.roomId)) + } } else { Timber.w("Same message content, do not send edition") } @@ -755,12 +807,19 @@ class RoomDetailViewModel @AssistedInject constructor( } else { room.sendFormattedTextMessage(finalText, htmlText) } + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendText(room.roomId)) + } _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } is SendMode.REPLY -> { state.sendMode.timelineEvent.let { room.replyToMessage(it, action.text.toString(), action.autoMarkdown) + viewModelScope.launch { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.SendReply(room.roomId)) + } + _viewEvents.post(RoomDetailViewEvents.MessageSent) 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) { diff --git a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt index 084dd6349a..c413e04110 100644 --- a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt +++ b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt @@ -19,9 +19,12 @@ package im.vector.app.features.lifecycle import android.app.Activity import android.app.Application import android.os.Bundle +import im.vector.app.core.utils.AnalyticsEngine 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) { } @@ -30,6 +33,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager } override fun onActivityStarted(activity: Activity) { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartActivity(activity)) } override fun onActivityDestroyed(activity: Activity) { @@ -39,6 +43,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager } override fun onActivityStopped(activity: Activity) { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndActivity(activity)) } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 81d6a78123..3304c4e2aa 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -34,6 +34,7 @@ import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.exhaustive 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.core.utils.ensureTrailingSlash import im.vector.app.features.signout.soft.SoftLogoutActivity import org.matrix.android.sdk.api.MatrixCallback @@ -63,7 +64,8 @@ class LoginViewModel @AssistedInject constructor( private val activeSessionHolder: ActiveSessionHolder, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val reAuthHelper: ReAuthHelper, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val analyticsEngine: AnalyticsEngine ) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -686,6 +688,7 @@ class LoginViewModel @AssistedInject constructor( activeSessionHolder.setActiveSession(session) authenticationService.reset() session.configureAndStart(applicationContext) + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.Init(session)) setState { copy( asyncLoginAction = Success(Unit) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index b180018480..c9c936ebaa 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -95,6 +95,16 @@ class PublicRoomsFragment @Inject constructor( }.exhaustive } + override fun onStart() { + super.onStart() + viewModel.handle(RoomDirectoryAction.StartReport) + } + + override fun onStop() { + super.onStop() + viewModel.handle(RoomDirectoryAction.EndReport) + } + override fun onDestroyView() { publicRoomsController.callback = null publicRoomsList.cleanup() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt index a94cb7709f..397f2839ac 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt @@ -24,4 +24,6 @@ sealed class RoomDirectoryAction : VectorViewModelAction { data class FilterWith(val filter: String) : RoomDirectoryAction() object LoadMore : RoomDirectoryAction() data class JoinRoom(val roomId: String) : RoomDirectoryAction() + object StartReport : RoomDirectoryAction() + object EndReport : RoomDirectoryAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index 42b17b4dad..b5d477163b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -26,6 +26,7 @@ import com.airbnb.mvrx.appendAt import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject 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.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure @@ -43,6 +44,7 @@ import timber.log.Timber private const val PUBLIC_ROOMS_LIMIT = 20 class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState, + private val analyticsEngine: AnalyticsEngine, private val session: Session) : VectorViewModel(initialState) { @@ -105,6 +107,12 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: is RoomDirectoryAction.FilterWith -> filterWith(action) RoomDirectoryAction.LoadMore -> loadMore() 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) { + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartRoomDirectorySearch) currentTask = session.getPublicRooms(roomDirectoryData.homeServer, PublicRoomsParams( limit = PUBLIC_ROOMS_LIMIT, @@ -178,11 +187,13 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: hasMore = since != null ) } + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndRoomDirectorySearch(data.chunk?.size ?: 0)) } override fun onFailure(failure: Throwable) { if (failure is Failure.Cancelled) { // Ignore, another request should be already started + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndRoomDirectorySearch(0)) return } @@ -193,6 +204,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: 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 { listOf(it) } ?: emptyList() + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.StartJoinRoomEvent) session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // 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 + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.EndJoinRoomEvent( + roomId = action.roomId, + isPublic = true, + memberCount = 0, + isEncrypted = false + )) } override fun onFailure(failure: Throwable) { // Notify the user _viewEvents.post(RoomDirectoryViewEvents.Failure(failure)) + analyticsEngine.report(AnalyticsEngine.AnalyticEvent.CancelJoinRoomEvent) } }) }