Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: modernization use case example (DRAFT-DO NOT MERGE) #367

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion android-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ task coreSdkJavadocs(type: Javadoc) {
exclude {
String filePath = it.toString()
filePath.contains('/com/mparticle/internal/') ||
filePath.contains('/com/mparticle/kits/')
filePath.contains('/com/mparticle/kits/') ||
filePath.contains('/com/mparticle/modernization/')
}
}
}
Expand All @@ -135,6 +136,8 @@ dependencies {
api 'androidx.annotation:annotation:[1.0.0,)'
compileOnly 'androidx.core:core:[1.3.2, )'

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'

api 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'

lintPublish project( path: ':tooling:custom-lint-rules', configuration: 'lintBuild')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mparticle.modernization
Copy link
Collaborator

Choose a reason for hiding this comment

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

Based on latest discussions, we should use the term "nextgen" instead of "modernization"

Copy link
Author

Choose a reason for hiding this comment

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

Sure


class BatchManager(private val api : MpApiClientImpl) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as other comment, this should take an ApiClient interface of which MpApiClientImpl extends right?


suspend fun createBatch() : MpBatch? = MpBatch("", Math.random().toLong())

suspend fun uploadBatch(batch : MpBatch) {
api.uploadBatch(batch)
}

}

class MpBatch(data : String, batchId : Long){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mparticle.modernization

internal abstract class MParticleCallback<S, E> {
var isSuccessFul: Boolean = false
var isCompleted: Boolean = false

fun onSuccess(result: S) {
isSuccessFul = true
isCompleted = true
}

fun onError(error: E) {
isSuccessFul = false
isCompleted = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mparticle.modernization

class MpApiClientImpl {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should have an ApiClient interface which this extends right?

Copy link
Author

Choose a reason for hiding this comment

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

Yup, just wanted to outline the idea of the data source itself. We might even have the ApiClient itself without interface if we don't intend to have multiple implementations of it (foe example multiple http clients as tools to resolve the implementation). In the future we could always create the abstraction and change this into an Impl.


suspend fun uploadBatch(data : MpBatch) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.mparticle.modernization

import com.mparticle.modernization.core.MParticleMediator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

internal fun MParticleMediator.launch(block: suspend CoroutineScope.() -> Unit) {
this.coroutineScope.launch(this.coroutineDispatcher) { block }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.mparticle.modernization

import android.content.Context
import com.mparticle.MPEvent
import com.mparticle.MParticleOptions
import com.mparticle.commerce.CommerceEvent
import com.mparticle.commerce.Product
import com.mparticle.modernization.core.MParticle
import com.mparticle.modernization.eventlogging.MParticleEventLogging


class WalkthroughTest(private val context: Context) {

private var eventLogging: MParticleEventLogging? = null

//Only setup
fun initialize() {
val options = MParticleOptions.builder(context).credentials("key", "secret").build()
MParticle.start(options)
eventLogging = MParticle.getInstance().EventLogging()
}

init {
initialize()
}

//===================

fun runTest() {
logCommerceEvent()
logNormalEvent()
}

fun logCommerceEvent() {
val product = Product.Builder("testName", "testSku", 100.0).build()
val commerceEvent = CommerceEvent.Builder(Product.CHECKOUT, product).build()
eventLogging?.logEvent(commerceEvent)
}
fun logNormalEvent() {
val customAttributes =
mutableMapOf(Pair("key1", "value1"), Pair("key2", 4), Pair("key3", null))
val event = MPEvent.Builder("myEvent").customAttributes(customAttributes).build()
eventLogging?.logEvent(event)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.mparticle.modernization.core

import com.mparticle.MParticleOptions
import com.mparticle.modernization.data.MParticleDataRepository
import com.mparticle.modernization.eventlogging.MParticleEventLogging
import com.mparticle.modernization.identity.MParticleIdentity
import com.mparticle.modernization.kit.MParticleKitManager
import com.mparticle.modernization.datahandler.MParticleDataHandler

internal class MParticle private constructor(private val options: MParticleOptions) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What exactly does private val do in this constructor parameter? Is it syntax sugar for creating a private instance property called options and assigning it this value?

Copy link
Author

Choose a reason for hiding this comment

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

This enables to use the dependency injection pattern, making the property accesible to be used in the class but restrict visibility from consumers. If options would have been public, MParticle.getInstance().options would have been valid.. Moreover by default properties have public setters/getters so, someone from the outside could eventually temper with the properties within MParticleOptions

private var mediator: MParticleMediator = MParticleMediator(MParticleDataRepository())

init {
mediator.configure(options)
}

companion object {
private var _instance: MParticle? = null

@Throws(Exception::class)
fun getInstance(): MParticle = _instance ?: throw Exception("MParticle must be started before getting the instance")

fun start(options: MParticleOptions) {
_instance = MParticle(options)
}
}

fun KitManager(): MParticleKitManager? = mediator.kitManager as MParticleKitManager?
fun Identity(): MParticleIdentity? = mediator.identity as MParticleIdentity?
fun EventLogging(): MParticleEventLogging? = mediator.eventLogging
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.mparticle.modernization.core

internal interface MParticleComponent
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.mparticle.modernization.core

import com.mparticle.MParticleOptions
import com.mparticle.modernization.BatchManager
import com.mparticle.modernization.MpApiClientImpl
import com.mparticle.modernization.data.MParticleDataRepository
import com.mparticle.modernization.eventlogging.MParticleEventLogging
import com.mparticle.modernization.eventlogging.example.MParticleEventLoggingImpl
import com.mparticle.modernization.identity.InternalIdentity
import com.mparticle.modernization.identity.example.MParticleIdentityImpl
import com.mparticle.modernization.kit.KitManagerInternal
import com.mparticle.modernization.kit.MParticleKit
import com.mparticle.modernization.kit.MParticleKitManagerImpl
import com.mparticle.modernization.kit.example.MpKit
import com.mparticle.modernization.datahandler.MParticleDataStrategyManagerImpl
import com.mparticle.modernization.datahandler.MParticleDataHandlerStrategy
import com.mparticle.modernization.datahandler.example.MParticleCommerceHandler
import kotlinx.coroutines.CloseableCoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors

internal class MParticleMediator(private val dataRepository: MParticleDataRepository) {
internal var eventLogging: MParticleEventLogging? = null
internal var identity: InternalIdentity? = null
internal var kitManager: KitManagerInternal? = null
internal var batchManager : BatchManager = BatchManager(MpApiClientImpl())

private var mParticleUploadingStrategies: List<MParticleDataHandlerStrategy<*,*>> = listOf(
MParticleCommerceHandler(dataRepository, batchManager)
)

internal lateinit var coroutineScope: CoroutineScope
internal lateinit var coroutineDispatcher: CloseableCoroutineDispatcher

/**
* Mediator register kits and components, acting as a "common layer" for the components internally,
* and also providing a single instance of the available and visible components to the MParticle
* facade. This will help also controlling which this we want to make accesible and which one we doesn't.
*/
fun configure(options: MParticleOptions) {
/**
* Creation of auto-cancellable thread-pool using coroutines.
* Using a utility, and due to the fact that the relationship between mediator-mPaticle instance
* is 1:1, we would be able to launch async operation managed by the thread pool of each mparticle
* instance
*/
coroutineScope = CoroutineScope(SupervisorJob())
coroutineDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher()

var kits: MutableList<MParticleKit> = registerKits(options)
/**
* Mediator has a reference to each component. The components mostly are instanciated with a
* dependency to the mediator, meaning that, if component A (running action 1) needs an action 3 from component B
* to be executed to complete action 1; component A can use the mediator to access component B
*/
registerComponent(MParticleKitManagerImpl(kits))
registerComponent(MParticleIdentityImpl(this))
registerComponent(MParticleEventLoggingImpl(this))
}

private fun registerKits(options: MParticleOptions): MutableList<MParticleKit> =
mutableListOf(MpKit(this, MParticleDataStrategyManagerImpl(mParticleUploadingStrategies)))

private fun registerComponent(component: MParticleComponent) {
when (component) {
is InternalIdentity -> identity = component
is KitManagerInternal -> kitManager = component
is MParticleEventLogging -> eventLogging = component
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mparticle.modernization.data

import com.mparticle.internal.messages.BaseMPMessage
import com.mparticle.modernization.MpBatch

/**
* This is based on the repository pattern to manage data sources (apiClients, daos,
* sharedPreferences, cache, memory, etc).
* Ideally we would inject our data sources here, and the repository would be incharge of preprocessing
* the data interacting with the data source, and post-process the data coming from data sources.
*/
internal class MParticleDataRepository {

suspend fun insertCommerceDTO(data: BaseMPMessage) {
//TODO parses the event and saves it into the db
}

suspend fun getEventsByType() : List<BaseMPMessage>? = emptyList()

suspend fun getBatch() : MpBatch? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mparticle.modernization.datahandler

enum class DataHandlerType {
MP_EVENT, COMMERCE_EVENT, BREADCRUMB;

companion object {
fun getType(clazz: Class<*>): DataHandlerType? {
return when (clazz.javaClass.simpleName) {
"com.mparticle.MpEvent" -> MP_EVENT
"com.mparticle.commerce.CommerceEvent" -> COMMERCE_EVENT
else -> null
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mparticle.modernization.datahandler

import com.mparticle.modernization.core.MParticleComponent
import org.jetbrains.annotations.NotNull

internal interface MParticleDataHandler : MParticleComponent {
/**
*Save set of data using provided strategies. Decision made base on [type]
*
* @param data any type of data. Each strategy is responsible of converting and handling the data
* @param immediateUpload true or false depending if we want to force immediate data upload. By
* default this is false
*/
suspend fun saveData(
@NotNull data: Any,
@NotNull immediateUpload: Boolean = false
)

suspend fun configure() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.mparticle.modernization.datahandler


import com.mparticle.modernization.BatchManager
import org.jetbrains.annotations.NotNull

/**
* Data uploading strategies for different data types
*/
internal interface MParticleDataHandlerStrategy<I, O> {
/** Upload set of data
*
* @param data any type of data to upload
* @param immediateUpload true or false depending if we want to force immediate data upload. By
* default this is false
* @param uploadingConfiguration to handle auto-data uploads or other custom implementation based
* on a configuration.
*/
suspend fun saveData(
@NotNull data: Any,
@NotNull immediateUpload: Boolean
)

suspend fun retrieveData(): List<O>

fun I.toDto(): O?

fun O.toModel(): I?

/**
* @return strategy id
*/
fun type(): DataHandlerType

}

abstract class BaseMParticleDataHandlerStrategy<I, O>(protected val batchManager: BatchManager) :
MParticleDataHandlerStrategy<I, O> {
protected open suspend fun createAndUploadBatch() {
with(batchManager) {
createBatch()?.let { this.uploadBatch(it) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.mparticle.modernization.datahandler

internal class MParticleDataStrategyManagerImpl(
private val strategies: List<MParticleDataHandlerStrategy<*, *>>,
) : MParticleDataHandler {

/**
* Based on the data type we will choose the corresponding strategy provided at config type, and
* execute an action on it.
*/
override suspend fun saveData(data: Any, immediateUpload: Boolean) {
data.javaClass.getStrategy()?.saveData(data, immediateUpload)
}

private fun Class<*>.getStrategy(): MParticleDataHandlerStrategy<*, *>? =
strategies.firstOrNull { it.type() == DataHandlerType.getType(this) }

}
Loading