diff --git a/android/build.gradle.kts b/android/build.gradle.kts index e9a0a68a..aa28ca65 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -120,12 +120,6 @@ dependencies { implementation(MVIKotlin.mvikotlinLogging) implementation(MVIKotlin.mvikotlinTimeTravel) - // Firebase - implementation(platform("com.google.firebase:firebase-bom:27.1.0")) - implementation("com.google.firebase:firebase-analytics-ktx") - implementation("com.google.firebase:firebase-crashlytics-ktx") - implementation("com.google.firebase:firebase-perf-ktx") - // Extras Extras.Android.apply { implementation(Acra.notification) @@ -134,6 +128,7 @@ dependencies { implementation(matomo) } + //implementation("com.jakewharton.timber:timber:4.7.1") implementation("dev.icerock.moko:parcelize:0.6.1") implementation("com.github.shabinder:storage-chooser:2.0.4.45") implementation("com.google.accompanist:accompanist-insets:0.9.1") diff --git a/android/src/main/java/com/shabinder/spotiflyer/App.kt b/android/src/main/java/com/shabinder/spotiflyer/App.kt index 36ffd831..c66d93c6 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/App.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/App.kt @@ -29,8 +29,26 @@ import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.component.KoinComponent import org.koin.core.logger.Level +import org.matomo.sdk.Matomo +import org.matomo.sdk.Tracker +import org.matomo.sdk.TrackerBuilder class App: Application(), KoinComponent { + + val tracker: Tracker by lazy { + TrackerBuilder.createDefault( + "https://kind-grasshopper-73.telebit.io/matomo/matomo.php", 1) + .build(Matomo.getInstance(this)).apply { + if (BuildConfig.DEBUG) { + /*Timber.plant(DebugTree()) + addTrackingCallback { + Timber.d(it.toMap().toString()) + it + }*/ + } + } + } + override fun onCreate() { super.onCreate() diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 72b148e7..9f2b2215 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -56,22 +56,22 @@ import com.shabinder.common.models.Actions import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey +import com.shabinder.common.models.Status import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.methods import com.shabinder.common.root.SpotiFlyerRoot +import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.uikit.* -import com.shabinder.spotiflyer.utils.* -import com.shabinder.common.models.Status -import com.shabinder.common.models.methods import com.shabinder.spotiflyer.ui.NetworkDialog import com.shabinder.spotiflyer.ui.PermissionDialog +import com.shabinder.spotiflyer.utils.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.koin.android.ext.android.inject +import org.matomo.sdk.extra.TrackHelper import java.io.File -const val disableDozeCode = 1223 - @ExperimentalAnimationApi class MainActivity : ComponentActivity() { @@ -85,6 +85,7 @@ class MainActivity : ComponentActivity() { private lateinit var updateUIReceiver: BroadcastReceiver private lateinit var queryReceiver: BroadcastReceiver private val internetAvailability by lazy { ConnectionLiveData(applicationContext) } + private val tracker get() = (application as App).tracker override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -118,7 +119,8 @@ class MainActivity : ComponentActivity() { PermissionDialog( permissionGranted.value, { requestStoragePermission() }, - { disableDozeMode(disableDozeCode) } + { disableDozeMode(disableDozeCode) }, + dir::enableAnalytics ) } } @@ -130,6 +132,10 @@ class MainActivity : ComponentActivity() { private fun initialise() { checkIfLatestVersion() handleIntentFromExternalActivity() + if(dir.isAnalyticsEnabled){ + // Download/App Install Event + TrackHelper.track().download().with(tracker) + } } @Composable @@ -380,4 +386,8 @@ class MainActivity : ComponentActivity() { } } } + + companion object { + const val disableDozeCode = 1223 + } } diff --git a/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt b/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt index a84efaca..41c6877b 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt @@ -8,11 +8,13 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.AlertDialog import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Insights import androidx.compose.material.icons.rounded.SdStorage import androidx.compose.material.icons.rounded.SystemSecurityUpdate import androidx.compose.runtime.Composable @@ -28,6 +30,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties import com.shabinder.common.uikit.SpotiFlyerShapes import com.shabinder.common.uikit.SpotiFlyerTypography import com.shabinder.common.uikit.colorPrimary @@ -38,13 +41,50 @@ import kotlinx.coroutines.delay fun PermissionDialog( permissionGranted: Boolean, requestStoragePermission:() -> Unit, - disableDozeMode:() -> Unit + disableDozeMode:() -> Unit, + enableAnalytics:() -> Unit ){ var askForPermission by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(2000) askForPermission = true } + + // Analytics Permission Dialog + var askForAnalyticsPermission by remember { mutableStateOf(false) } + AnimatedVisibility(askForAnalyticsPermission) { + AlertDialog( + onDismissRequest = { + askForAnalyticsPermission = false + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Rounded.Insights,"Analytics",Modifier.size(52.dp)) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text("Grant Analytics Access",style = SpotiFlyerTypography.h5,textAlign = TextAlign.Center) + } + }, + backgroundColor = Color.DarkGray, + buttons = { + TextButton( + { + askForAnalyticsPermission = false + enableAnalytics() + }, + Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth() + .background(colorPrimary, shape = SpotiFlyerShapes.medium) + .padding(horizontal = 8.dp), + ) { + Text("Sure!",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center) + } + }, + text = { + Text("Your Data is Anonymized and will never be shared with any 3rd party service",style = SpotiFlyerTypography.body2,textAlign = TextAlign.Center) + }, + properties = DialogProperties(dismissOnBackPress = true,dismissOnClickOutside = false) + ) + } + AnimatedVisibility( askForPermission && !permissionGranted ) { @@ -55,6 +95,7 @@ fun PermissionDialog( { requestStoragePermission() disableDozeMode() + askForAnalyticsPermission = true }, Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth() .background(colorPrimary, shape = SpotiFlyerShapes.medium) @@ -100,6 +141,23 @@ fun PermissionDialog( ) } } + Row( + modifier = Modifier.fillMaxWidth().padding(top = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Rounded.Insights,"Analytics") + Spacer(modifier = Modifier.padding(start = 16.dp)) + Column { + Text( + text = "Analytics", + style = SpotiFlyerTypography.h6.copy(fontWeight = FontWeight.SemiBold) + ) + Text( + text = "Share Analytics Data (optional) with App Devs (Self-Hosted), It will never be used/shared/sold to any third party service.", + style = SpotiFlyerTypography.subtitle2, + ) + } + } } } ) diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt index c6a3fa54..6dd8ec37 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt @@ -102,7 +102,7 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain) { ) when (model.selectedCategory) { - HomeCategory.About -> AboutColumn() + HomeCategory.About -> AboutColumn { component.analytics.donationDialogVisit() } HomeCategory.History -> HistoryColumn( model.records.sortedByDescending { it.id }, component::loadImage, @@ -221,7 +221,10 @@ fun SearchPanel( } @Composable -fun AboutColumn(modifier: Modifier = Modifier) { +fun AboutColumn( + modifier: Modifier = Modifier, + donationDialogOpenEvent:() -> Unit +) { Box { val stateVertical = rememberScrollState(0) @@ -331,14 +334,18 @@ fun AboutColumn(modifier: Modifier = Modifier) { var isDonationDialogVisible by remember { mutableStateOf(false) } DonationDialog( - isDonationDialogVisible - ) { - isDonationDialogVisible = false - } + isDonationDialogVisible, + onDismiss = { + isDonationDialogVisible = false + } + ) Row( modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) - .clickable(onClick = { isDonationDialogVisible = true }), + .clickable(onClick = { + isDonationDialogVisible = true + donationDialogOpenEvent() + }), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Rounded.CardGiftcard, "Support Developer") diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt index b077dd23..40a9eb12 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt @@ -41,7 +41,7 @@ import java.net.URL actual class Dir actual constructor( private val logger: Kermit, private val settings: Settings, - private val spotiFlyerDatabase: SpotiFlyerDatabase, + spotiFlyerDatabase: SpotiFlyerDatabase, ) { companion object { const val DirKey = "downloadDir" @@ -91,7 +91,7 @@ actual class Dir actual constructor( * */ if(!songFile.exists()) { /*Make intermediate Dirs if they don't exist yet*/ - songFile.parentFile.mkdirs() + songFile.parentFile?.mkdirs() } if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt index 7416fcf6..75974e63 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt @@ -17,7 +17,6 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit -import co.touchlab.stately.ensureNeverFrozen import com.russhwolf.settings.Settings import com.shabinder.common.database.databaseModule import com.shabinder.common.database.getLogger @@ -25,15 +24,10 @@ import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.providers.YoutubeMusic -import com.shabinder.common.models.NativeAtomicReference -import io.ktor.client.HttpClient -import io.ktor.client.features.HttpTimeout -import io.ktor.client.features.json.JsonFeature -import io.ktor.client.features.json.serializer.KotlinxSerializer -import io.ktor.client.features.logging.DEFAULT -import io.ktor.client.features.logging.LogLevel -import io.ktor.client.features.logging.Logger -import io.ktor.client.features.logging.Logging +import io.ktor.client.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.features.logging.* import kotlinx.serialization.json.Json import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration @@ -87,12 +81,6 @@ fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient { } ) }*/ - // Timeout - install(HttpTimeout) { - // requestTimeoutMillis = 20000L - connectTimeoutMillis = 15000L - socketTimeoutMillis = 15000L - } if (enableNetworkLogs) { install(Logging) { logger = Logger.DEFAULT diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt index 49ae8f9c..17f19a18 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt @@ -18,7 +18,6 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit import com.russhwolf.settings.Settings -import com.russhwolf.settings.SettingsListener import com.shabinder.common.database.SpotiFlyerDatabase import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadResult diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt index 58bea06e..594142ac 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt @@ -45,6 +45,13 @@ actual class Dir actual constructor( ) { companion object { const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" + } + + actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false + + actual fun enableAnalytics() { + settings.putBoolean(AnalyticsKey,true) } init { diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt index 056870f5..6e710ce6 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt @@ -21,7 +21,6 @@ import platform.Foundation.sendSynchronousRequest import platform.Foundation.writeToFile import platform.UIKit.UIImage import platform.UIKit.UIImageJPEGRepresentation -import java.lang.System actual class Dir actual constructor( val logger: Kermit, @@ -30,6 +29,13 @@ actual class Dir actual constructor( ) { companion object { const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" + } + + actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false + + actual fun enableAnalytics() { + settings.putBoolean(AnalyticsKey,true) } actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path) diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt index c6ea859c..711a331b 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt @@ -39,8 +39,17 @@ actual class Dir actual constructor( ) { companion object { const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" } + actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false + + actual fun enableAnalytics() { + settings.putBoolean(AnalyticsKey,true) + } + + actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath) + /*init { createDirectories() }*/ diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index c36180b1..ff4e1980 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -65,6 +65,11 @@ interface SpotiFlyerList { val link: String val listOutput: Consumer val downloadProgressFlow: MutableSharedFlow> + val listAnalytics: Analytics + } + + interface Analytics { + } sealed class Output { diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt index 57a8cefe..babb9bcd 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt @@ -31,6 +31,8 @@ interface SpotiFlyerMain { val models: Value + val analytics: Analytics + /* * We Intend to Move to List Screen * Note: Implementation in Root @@ -57,6 +59,11 @@ interface SpotiFlyerMain { val storeFactory: StoreFactory val database: Database? val dir: Dir + val mainAnalytics: Analytics + } + + interface Analytics { + fun donationDialogVisit() } sealed class Output { diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt index 554991a2..4ba6d31a 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt @@ -40,9 +40,6 @@ internal class SpotiFlyerMainImpl( init { instanceKeeper.ensureNeverFrozen() - lifecycle.doOnDestroy { - cache.invalidateAll() - } } private val store = @@ -55,11 +52,13 @@ internal class SpotiFlyerMainImpl( private val cache = Cache.Builder .newBuilder() - .maximumCacheSize(20) + .maximumCacheSize(25) .build() override val models: Value = store.asValue() + override val analytics = mainAnalytics + override fun onLinkSearch(link: String) { if (methods.value.isInternetAvailable) mainOutput.callback(Output.Search(link = link)) else methods.value.showPopUpMessage("Check Network Connection Please") diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt index 23d57065..74ede10e 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt @@ -54,6 +54,14 @@ interface SpotiFlyerRoot { val directories: Dir val downloadProgressReport: MutableSharedFlow> val actions:Actions + val analytics: Analytics + } + + interface Analytics { + fun appLaunchEvent() + fun homeScreenVisit() + fun listScreenVisit() + fun donationDialogVisit() } } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index e093b6da..1354681f 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -37,6 +37,7 @@ import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.Consumer import com.shabinder.common.models.methods import com.shabinder.common.root.SpotiFlyerRoot +import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.root.SpotiFlyerRoot.Dependencies import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks @@ -49,7 +50,8 @@ internal class SpotiFlyerRootImpl( componentContext: ComponentContext, private val main: (ComponentContext, output:Consumer)->SpotiFlyerMain, private val list: (ComponentContext, link:String, output:Consumer)->SpotiFlyerList, - private val actions: Actions + private val actions: Actions, + private val analytics: Analytics ) : SpotiFlyerRoot, ComponentContext by componentContext { constructor( @@ -72,7 +74,8 @@ internal class SpotiFlyerRootImpl( dependencies ) }, - actions = dependencies.actions.freeze() + actions = dependencies.actions.freeze(), + analytics = dependencies.analytics ) { instanceKeeper.ensureNeverFrozen() methods.value = dependencies.actions.freeze() @@ -113,16 +116,23 @@ internal class SpotiFlyerRootImpl( private fun onMainOutput(output: SpotiFlyerMain.Output) = when (output) { - is SpotiFlyerMain.Output.Search -> router.push(Configuration.List(link = output.link)) + is SpotiFlyerMain.Output.Search -> { + router.push(Configuration.List(link = output.link)) + analytics.listScreenVisit() + } } private fun onListOutput(output: SpotiFlyerList.Output): Unit = when (output) { - is SpotiFlyerList.Output.Finished -> router.pop() + is SpotiFlyerList.Output.Finished -> { + router.pop() + analytics.homeScreenVisit() + } } private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override:Boolean){ GlobalScope.launch(Dispatchers.Default) { + analytics.appLaunchEvent() /*Authenticate Spotify Client*/ spotifyProvider.authenticateSpotifyClient(override) } @@ -143,6 +153,9 @@ private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer< dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies { override val mainOutput: Consumer = output override val dir: Dir = directories + override val mainAnalytics = object : SpotiFlyerMain.Analytics { + override fun donationDialogVisit() = analytics.donationDialogVisit() + } } ) @@ -155,5 +168,6 @@ private fun spotiFlyerList(componentContext: ComponentContext, link: String, out override val link: String = link override val listOutput: Consumer = output override val downloadProgressFlow = downloadProgressReport + override val listAnalytics = object : SpotiFlyerList.Analytics {} } )