Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,15 @@ object AnalyticsConstants {
const val PORTAL_ATM = "explore_portal_atm"
const val LEARN_MORE = "explore_info_learn_more"
const val CONTINUE = "explore_info_continue"
const val ONLINE_MERCHANTS = "explore_online_merchants"
const val NEARBY_MERCHANTS = "explore_nearby_merchants"
const val ALL_MERCHANTS = "explore_all_merchants"
const val FILTER_MERCHANTS_TOP = "explore_filter_merchants_top"
const val FILTER_MERCHANTS_BOTTOM = "explore_filter_merchants_bottom"
const val SELECT_MERCHANT_LOCATION = "explore_select_merchant_location"
const val SELECT_MERCHANT_MARKER = "explore_select_merchant_marker"
const val INFO_EXPLORE_MERCHANT = "explore_info_search"
const val PAN_MERCHANT_MAP = "explore_pan_merchant_map"
const val ZOOM_MERCHANT_MAP = "explore_zoom_merchant_map"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class ExploreMapFragment : SupportMapFragment() {

@Inject
lateinit var userLocationState: UserLocationStateInt

private var cameraMovementReason: Int = -1
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Expand All @@ -96,16 +96,16 @@ class ExploreMapFragment : SupportMapFragment() {
showMap()
googleMap?.let { map ->
map.setOnCameraIdleListener {
val bounds = map.projection.visibleRegion.latLngBounds
viewModel.searchBounds = GeoBounds(
bounds.northeast.latitude,
bounds.northeast.longitude,
bounds.southwest.latitude,
bounds.southwest.longitude,
bounds.center.latitude,
bounds.center.longitude,
map.cameraPosition.zoom
)
viewModel.searchBounds = getGeoBounds(map)
if (cameraMovementReason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE){
viewModel.triggerPanAndZoomEvents(map.cameraPosition.zoom, getGeoBounds(map))
}
viewModel.previousZoomLevel = map.cameraPosition.zoom
viewModel.previousCameraGeoBounds = getGeoBounds(map)
}

map.setOnCameraMoveStartedListener { reason ->

@ClaudeHangui ClaudeHangui Mar 18, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are various reasons that can cause the camera to be in motion in a map: however our tracking only focuses on the Camera motion initiated in response to user gestures on the map including pinch to zoom and pan gestures. All other causes/reasons initiated or not by the user are to be ignored for the tracking

cameraMovementReason = reason
}
}
}
Expand Down Expand Up @@ -227,6 +227,7 @@ class ExploreMapFragment : SupportMapFragment() {
if (isGooglePlayServicesAvailable()) {
markerCollection = MarkerManager(googleMap).newCollection()
markerCollection?.setOnMarkerClickListener { marker ->
viewModel.triggerMarkerClickEvent()
viewModel.onMapMarkerSelected(marker.tag as Int)
true
}
Expand Down Expand Up @@ -351,6 +352,8 @@ class ExploreMapFragment : SupportMapFragment() {
val radiusBounds = getRadiusBounds(mCurrentUserLocation, radius)
map.moveCamera(radiusBounds)
lastFocusedUserLocation = mCurrentUserLocation
viewModel.previousZoomLevel = map.cameraPosition.zoom
viewModel.previousCameraGeoBounds = getGeoBounds(map)
}
}

Expand Down Expand Up @@ -485,4 +488,16 @@ class ExploreMapFragment : SupportMapFragment() {
canvas?.let { drawable.draw(it) }
return bitmap
}

private fun getGeoBounds(map: GoogleMap): GeoBounds {
val bounds = map.projection.visibleRegion.latLngBounds
return GeoBounds(
bounds.northeast.latitude,
bounds.northeast.longitude,
bounds.southwest.latitude,
bounds.southwest.longitude,
bounds.center.latitude,
bounds.center.longitude,
map.cameraPosition.zoom)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ package org.dash.wallet.features.exploredash.ui

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.core.os.bundleOf
import androidx.lifecycle.*
import androidx.paging.*
import androidx.paging.PagingData
import com.google.android.gms.maps.model.LatLng

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The viewModel should not reference LatLng since it's implementation-dependent and we're working with an interface in case of UserLocationState. If the implementation of UserLocationState changed, you'll have to change a lot of related code in the viewModel as well. Please use GeoBounds instead.

import com.google.firebase.FirebaseNetworkException
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -31,6 +33,8 @@ import org.dash.wallet.common.data.Resource
import org.dash.wallet.common.data.SingleLiveEvent
import org.dash.wallet.common.data.Status
import org.dash.wallet.common.livedata.ConnectionLiveData
import org.dash.wallet.common.services.analytics.AnalyticsConstants
import org.dash.wallet.common.services.analytics.AnalyticsService
import org.dash.wallet.features.exploredash.data.ExploreDataSource
import org.dash.wallet.features.exploredash.data.model.*
import org.dash.wallet.features.exploredash.data.model.GeoBounds
Expand Down Expand Up @@ -77,7 +81,8 @@ class ExploreViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val exploreData: ExploreDataSource,
private val locationProvider: UserLocationStateInt,
private val syncStatusService: DataSyncStatusService
private val syncStatusService: DataSyncStatusService,
private val analyticsService: AnalyticsService
) : ViewModel() {
companion object {
const val QUERY_DEBOUNCE_VALUE = 300L
Expand All @@ -97,7 +102,6 @@ class ExploreViewModel @Inject constructor(
private var boundedFilterJob: Job? = null
private var pagingFilterJob: Job? = null
private var allMerchantLocationsJob: Job? = null

val isMetric = Locale.getDefault().isMetric

private val _searchQuery = MutableStateFlow("")
Expand All @@ -106,7 +110,8 @@ class ExploreViewModel @Inject constructor(

var exploreTopic = ExploreTopic.Merchants
private set

var previousCameraGeoBounds = GeoBounds.noBounds
var previousZoomLevel: Float = -1.0f
private var lastResolvedAddress: GeoBounds? = null
private var _currentUserLocation: MutableStateFlow<UserLocation?> = MutableStateFlow(null)
val currentUserLocation = _currentUserLocation.asLiveData()
Expand Down Expand Up @@ -703,4 +708,30 @@ class ExploreViewModel @Inject constructor(
syncStatusService.setObservedLastError()
}
}

fun triggerPanAndZoomEvents(currentZoomLevel: Float, currentGeoBounds: GeoBounds){
when {
hasZoomLevelChanged(currentZoomLevel) -> {
if (exploreTopic == ExploreTopic.Merchants){
analyticsService.logEvent(AnalyticsConstants.ExploreDash.ZOOM_MERCHANT_MAP, bundleOf())
}
}
hasCameraCenterChanged(currentGeoBounds) -> {
if (exploreTopic == ExploreTopic.Merchants){
analyticsService.logEvent(AnalyticsConstants.ExploreDash.PAN_MERCHANT_MAP, bundleOf())
}
}
}
}
private fun hasZoomLevelChanged(currentZoomLevel: Float): Boolean =
previousZoomLevel != currentZoomLevel

private fun hasCameraCenterChanged(currentCenterPosition: GeoBounds): Boolean =
locationProvider.distanceBetweenCenters(previousCameraGeoBounds, currentCenterPosition) != 0.0

fun triggerMarkerClickEvent(){
if (exploreTopic == ExploreTopic.Merchants){
analyticsService.logEvent(AnalyticsConstants.ExploreDash.SELECT_MERCHANT_MARKER, bundleOf())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
import androidx.core.view.*
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
Expand Down Expand Up @@ -59,6 +60,8 @@ import org.dash.wallet.features.exploredash.ui.extensions.*
import org.dash.wallet.common.Configuration
import org.dash.wallet.common.data.Resource
import org.dash.wallet.common.data.Status
import org.dash.wallet.common.services.analytics.AnalyticsConstants
import org.dash.wallet.common.services.analytics.AnalyticsService
import org.dash.wallet.features.exploredash.ui.adapters.MerchantLocationsHeaderAdapter
import org.dash.wallet.features.exploredash.ui.adapters.MerchantsLocationsAdapter
import javax.inject.Inject
Expand All @@ -73,7 +76,8 @@ class SearchFragment : Fragment(R.layout.fragment_search) {

@Inject
lateinit var configuration: Configuration

@Inject
lateinit var analyticsService: AnalyticsService
private val binding by viewBinding(FragmentSearchBinding::bind)
private val viewModel: ExploreViewModel by activityViewModels()
private val args by navArgs<SearchFragmentArgs>()
Expand Down Expand Up @@ -107,6 +111,7 @@ class SearchFragment : Fragment(R.layout.fragment_search) {
hideKeyboard()

if (item is Merchant) {
analyticsService.logEvent(AnalyticsConstants.ExploreDash.SELECT_MERCHANT_LOCATION, bundleOf())
viewModel.openMerchantDetails(item, true)
} else if (item is Atm) {
viewModel.openAtmDetails(item)
Expand Down Expand Up @@ -150,6 +155,9 @@ class SearchFragment : Fragment(R.layout.fragment_search) {

binding.toolbar.setOnMenuItemClickListener {
if (it.itemId == R.id.menu_info) {
if (args.type == ExploreTopic.Merchants){
analyticsService.logEvent(AnalyticsConstants.ExploreDash.INFO_EXPLORE_MERCHANT, bundleOf())
}
safeNavigate(SearchFragmentDirections.exploreToInfo())
}
true
Expand Down Expand Up @@ -304,14 +312,27 @@ class SearchFragment : Fragment(R.layout.fragment_search) {
viewModel.setFilterMode(defaultMode)

searchHeaderAdapter.setOnFilterOptionChosen { mode ->
if (topic == ExploreTopic.Merchants){
when(mode){
FilterMode.Online -> analyticsService.logEvent(AnalyticsConstants.ExploreDash.ONLINE_MERCHANTS, bundleOf())
FilterMode.Nearby -> analyticsService.logEvent(AnalyticsConstants.ExploreDash.NEARBY_MERCHANTS, bundleOf())
else -> analyticsService.logEvent(AnalyticsConstants.ExploreDash.ALL_MERCHANTS, bundleOf())
}
}
viewModel.setFilterMode(mode)
}

searchHeaderAdapter.setOnFilterButtonClicked {
if (topic == ExploreTopic.Merchants){
analyticsService.logEvent(AnalyticsConstants.ExploreDash.FILTER_MERCHANTS_TOP, bundleOf())
}
openFilters()
}

binding.filterPanel.setOnClickListener {
if (topic == ExploreTopic.Merchants){
analyticsService.logEvent(AnalyticsConstants.ExploreDash.FILTER_MERCHANTS_BOTTOM, bundleOf())
}
openFilters()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import org.dash.wallet.common.data.Resource
import org.dash.wallet.common.services.analytics.AnalyticsService
import org.dash.wallet.features.exploredash.data.ExploreDataSource
import org.dash.wallet.features.exploredash.data.model.GeoBounds
import org.dash.wallet.features.exploredash.data.model.Merchant
Expand Down Expand Up @@ -124,8 +125,8 @@ class ExploreViewModelTest {
on { getSyncProgressFlow() } doReturn flow { emit(Resource.loading(50.0))}
on { hasObservedLastError() } doReturn flow { emit(false) }
}

val viewModel = ExploreViewModel(context, dataSource, locationState, dataSyncStatus)
val analyticsService = mock<AnalyticsService>()
val viewModel = ExploreViewModel(context, dataSource, locationState, dataSyncStatus, analyticsService)
viewModel.setSelectedTerritory(territory)
viewModel.setFilterMode(FilterMode.All)
viewModel.searchBounds = GeoBounds.noBounds
Expand Down Expand Up @@ -169,8 +170,8 @@ class ExploreViewModelTest {
on { getSyncProgressFlow() } doReturn flow { emit(Resource.loading(50.0))}
on { hasObservedLastError() } doReturn flow { emit(false) }
}

val viewModel = ExploreViewModel(context, dataSource, locationState, dataSyncStatus)
val analyticsService = mock<AnalyticsService>()
val viewModel = ExploreViewModel(context, dataSource, locationState, dataSyncStatus, analyticsService)
viewModel.setFilterMode(FilterMode.Nearby)
viewModel.searchBounds = bounds
viewModel.paymentMethodFilter = PaymentMethod.DASH
Expand Down Expand Up @@ -215,8 +216,9 @@ class ExploreViewModelTest {
on { getSyncProgressFlow() } doReturn flow { emit(Resource.loading(50.0))}
on { hasObservedLastError() } doReturn flow { emit(false) }
}
val analyticsService = mock<AnalyticsService>()

val viewModel = ExploreViewModel(context, dataSource, locationState, dataSyncStatus)
val viewModel = ExploreViewModel(context, dataSource, locationState, dataSyncStatus, analyticsService)
viewModel.setSelectedTerritory(territory)
viewModel.searchBounds = GeoBounds.noBounds
viewModel.setFilterMode(FilterMode.All)
Expand Down Expand Up @@ -265,7 +267,9 @@ class ExploreViewModelTest {
on { getSyncProgressFlow() } doReturn flow { emit(Resource.loading(50.0))}
on { hasObservedLastError() } doReturn flow { emit(false) }
}
val viewModel = ExploreViewModel(context, dataSource, locationMock, dataSyncStatus)
val analyticsService = mock<AnalyticsService>()

val viewModel = ExploreViewModel(context, dataSource, locationMock, dataSyncStatus, analyticsService)
viewModel.searchBounds = GeoBounds(90.0, 180.0, -90.0, -180.0, userLat, userLng)
.apply { zoomLevel = ExploreViewModel.MIN_ZOOM_LEVEL + 1 }
viewModel.setFilterMode(FilterMode.Nearby)
Expand Down Expand Up @@ -297,7 +301,9 @@ class ExploreViewModelTest {
on { getSyncProgressFlow() } doReturn flow { emit(Resource.loading(50.0))}
on { hasObservedLastError() } doReturn flow { emit(false) }
}
val viewModel = ExploreViewModel(context, dataSource, locationMock, dataSyncStatus)
val analyticsService = mock<AnalyticsService>()

val viewModel = ExploreViewModel(context, dataSource, locationMock, dataSyncStatus,analyticsService)
viewModel.setPhysicalResults(merchants)
viewModel.onMapMarkerSelected(5)

Expand Down