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: implement the analytics SDK as a singleton #424

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.rudderstack.android

import androidx.annotation.VisibleForTesting
import com.rudderstack.core.utilities.FutureUse
import com.rudderstack.core.Analytics
import java.util.concurrent.ConcurrentHashMap

Expand All @@ -18,6 +19,8 @@ import java.util.concurrent.ConcurrentHashMap
* Note: The class is marked as internal, indicating that it is intended for use within the same module and should not be accessed
* from outside the module.
*/

@FutureUse("This class will be utilized when multiple instances are implemented.")
1abhishekpandey marked this conversation as resolved.
Show resolved Hide resolved
internal object AnalyticsRegistry {

private val writeKeyToInstance: ConcurrentHashMap<String, Analytics> = ConcurrentHashMap()
Expand Down Expand Up @@ -53,4 +56,4 @@ internal object AnalyticsRegistry {
fun clear() {
writeKeyToInstance.clear()
}
}
}
275 changes: 51 additions & 224 deletions android/src/main/java/com/rudderstack/android/RudderAnalytics.kt
Original file line number Diff line number Diff line change
@@ -1,236 +1,63 @@
/*
* Creator: Debanjan Chatterjee on 26/04/22, 3:08 PM Last modified: 26/04/22, 3:08 PM
* Copyright: All rights reserved Ⓒ 2022 http://rudderstack.com
*
* 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.
*/
@file:JvmName("RudderAnalytics") @file:Suppress("FunctionName")

package com.rudderstack.android

import com.rudderstack.android.internal.infrastructure.ActivityBroadcasterPlugin
import com.rudderstack.android.internal.infrastructure.AnonymousIdHeaderPlugin
import com.rudderstack.android.internal.infrastructure.AppInstallUpdateTrackerPlugin
import com.rudderstack.android.internal.infrastructure.LifecycleObserverPlugin
import com.rudderstack.android.internal.infrastructure.ResetImplementationPlugin
import com.rudderstack.android.internal.plugins.ExtractStatePlugin
import com.rudderstack.android.internal.plugins.FillDefaultsPlugin
import com.rudderstack.android.internal.plugins.PlatformInputsPlugin
import com.rudderstack.android.internal.infrastructure.ReinstatePlugin
import com.rudderstack.android.internal.plugins.SessionPlugin
import com.rudderstack.android.internal.states.ContextState
import com.rudderstack.android.internal.states.UserSessionState
import com.rudderstack.android.storage.AndroidStorage
import com.rudderstack.android.storage.AndroidStorageImpl
import com.rudderstack.android.utilities.shutdownSessionManagement
import com.rudderstack.android.utilities.onShutdown
import com.rudderstack.android.utilities.startup
import com.rudderstack.core.Analytics
import com.rudderstack.core.ConfigDownloadService
import com.rudderstack.core.DataUploadService
import com.rudderstack.core.holder.associateState
import com.rudderstack.core.holder.retrieveState
import com.rudderstack.models.MessageContext
import com.rudderstack.models.createContext
import com.rudderstack.models.traits
import com.rudderstack.models.updateWith

//device info and stuff
//multi process
//bt stuff
//tv,
//work manager
private fun RudderAnalytics(
writeKey: String,
configuration: ConfigurationAndroid,
dataUploadService: DataUploadService? = null,
configDownloadService: ConfigDownloadService? = null,
storage: AndroidStorage = AndroidStorageImpl(
configuration.application,
writeKey = writeKey,
useContentProvider = ConfigurationAndroid.Defaults.USE_CONTENT_PROVIDER
),
initializationListener: ((success: Boolean, message: String?) -> Unit)? = null
): Analytics {
return Analytics(writeKey,
configuration,
dataUploadService,
configDownloadService,
storage,
initializationListener = initializationListener,
shutdownHook = {
onShutdown()
}).apply {
startup()
}
}

@JvmOverloads
fun createInstance(
writeKey: String,
configuration: ConfigurationAndroid,
dataUploadService: DataUploadService? = null,
configDownloadService: ConfigDownloadService? = null,
storage: AndroidStorage = AndroidStorageImpl(
configuration.application,
writeKey = writeKey,
useContentProvider = ConfigurationAndroid.Defaults.USE_CONTENT_PROVIDER
),
initializationListener: ((success: Boolean, message: String?) -> Unit)? = null
): Analytics {
return AnalyticsRegistry.getInstance(writeKey)
?: RudderAnalytics(
writeKey,
configuration,
dataUploadService,
configDownloadService,
storage,
initializationListener
).also { analyticsInstance ->
AnalyticsRegistry.register(writeKey, analyticsInstance)
}
}

fun getInstance(writeKey: String): Analytics? {
1abhishekpandey marked this conversation as resolved.
Show resolved Hide resolved
return AnalyticsRegistry.getInstance(writeKey)
}

internal val Analytics.contextState: ContextState?
get() = retrieveState<ContextState>()
val Analytics.androidStorage: AndroidStorage
get() = (storage as AndroidStorage)
import com.rudderstack.core.Storage

/**
* Set the AdvertisingId yourself. If set, SDK will not capture idfa automatically
* Singleton class for RudderAnalytics to manage the analytics instance.
*
* @param advertisingId IDFA for the device
* This class ensures that only one instance of the Analytics object is created.
*/
fun Analytics.putAdvertisingId(advertisingId: String) {

applyConfiguration {
if (this is ConfigurationAndroid) copy(
advertisingId = advertisingId
)
else this
}
}

/**
* Set the push token for the device to be passed to the downstream destinations
*
* @param deviceToken Push Token from FCM
*/
fun Analytics.putDeviceToken(deviceToken: String) {
applyConfiguration {
if (this is ConfigurationAndroid) copy(
deviceToken = deviceToken
)
else this
}
}

/**
* Anonymous id to be used for all consecutive calls.
* Anonymous id is mostly used for messages sent prior to user identification or in case of
* anonymous usage.
*
* @param anonymousId String to be used as anonymousId
*/
fun Analytics.setAnonymousId(anonymousId: String) {
androidStorage.setAnonymousId(anonymousId)
applyConfiguration {
if (this is ConfigurationAndroid) copy(
anonymousId = anonymousId
)
else this
}
val anonymousIdPair = ("anonymousId" to anonymousId)
val newContext = contextState?.value?.let {
it.updateWith(traits = (it.traits?: mapOf()) + anonymousIdPair)
}?: createContext(traits = mapOf(anonymousIdPair))
processNewContext(newContext)
}

/**
* Setting the [ConfigurationAndroid.userId] explicitly.
*
* @param userId String to be used as userId
*/
fun Analytics.setUserId(userId: String) {
androidStorage.setUserId(userId)
applyConfiguration {
if (this is ConfigurationAndroid) copy(
userId = userId
)
else this
}
}

private val infrastructurePlugins
get() = arrayOf(
ReinstatePlugin(),
AnonymousIdHeaderPlugin(),
AppInstallUpdateTrackerPlugin(),
LifecycleObserverPlugin(),
ActivityBroadcasterPlugin(),
ResetImplementationPlugin()
)
private val messagePlugins
get() = listOf(
ExtractStatePlugin(), FillDefaultsPlugin(), PlatformInputsPlugin(),
SessionPlugin()
)

private fun Analytics.startup() {
addPlugins()
associateStates()
}


private fun Analytics.associateStates() {
associateState(ContextState())
attachSavedContextIfAvailable()
associateState(UserSessionState())
}

private fun Analytics.attachSavedContextIfAvailable() {
androidStorage.context?.let {
processNewContext(it)
}
}

private fun Analytics.addPlugins() {
addInfrastructurePlugin(*infrastructurePlugins)
addPlugin(*messagePlugins.toTypedArray())
}

internal fun Analytics.processNewContext(
newContext: MessageContext
) {
androidStorage.cacheContext(newContext)
contextState?.update(newContext)
}

fun Analytics.applyConfigurationAndroid(
androidConfigurationScope: ConfigurationAndroid.() ->
ConfigurationAndroid
) {
applyConfiguration {
if (this is ConfigurationAndroid) androidConfigurationScope()
else this
class RudderAnalytics private constructor() {

companion object {

@Volatile
private var instance: Analytics? = null

/**
* Returns the singleton instance of [Analytics], creating it if necessary.
*
* @param writeKey The write key for authentication.
* @param configuration The configuration settings for Android.
* @param storage The storage implementation for storing data. Defaults to [AndroidStorageImpl].
* @param dataUploadService The service responsible for uploading data. Defaults to null.
* @param configDownloadService The service responsible for downloading configuration. Defaults to null.
* @param initializationListener A listener for initialization events. Defaults to null.
* @return The singleton instance of [Analytics].
*/
@JvmStatic
@JvmOverloads
fun getInstance(
writeKey: String,
configuration: ConfigurationAndroid,
storage: Storage = AndroidStorageImpl(
configuration.application,
writeKey = writeKey,
useContentProvider = ConfigurationAndroid.Defaults.USE_CONTENT_PROVIDER
),
dataUploadService: DataUploadService? = null,
configDownloadService: ConfigDownloadService? = null,
initializationListener: ((success: Boolean, message: String?) -> Unit)? = null,
) = instance ?: synchronized(this) {
instance ?: Analytics(
writeKey = writeKey,
configuration = configuration,
dataUploadService = dataUploadService,
configDownloadService = configDownloadService,
storage = storage,
initializationListener = initializationListener,
shutdownHook = { onShutdown() }
).apply {
startup()
}.also {
instance = it
}
}
}
}

val Analytics.currentConfigurationAndroid: ConfigurationAndroid?
get() = (currentConfiguration as? ConfigurationAndroid)

private fun Analytics.onShutdown() {
shutdownSessionManagement()
}


Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ public RudderAnalyticsBuilderCompat withInitializationListener(InitializationLis
}
public Analytics build() {

return RudderAnalytics.createInstance(
return RudderAnalytics.getInstance(
writeKey,
configuration,
storage,
dataUploadService,
configDownloadService,
storage,
(success, message) -> {
if (initializationListener != null) {
initializationListener.onInitialized(success, message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import android.app.Activity
import android.app.Application
import android.os.Bundle
import com.rudderstack.android.LifecycleListenerPlugin
import com.rudderstack.android.currentConfigurationAndroid
import com.rudderstack.android.utilities.currentConfigurationAndroid
import com.rudderstack.core.Analytics
import com.rudderstack.core.InfrastructurePlugin
import java.util.concurrent.atomic.AtomicInteger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ package com.rudderstack.android.internal.infrastructure

import com.rudderstack.android.AndroidUtils
import com.rudderstack.android.ConfigurationAndroid
import com.rudderstack.android.applyConfigurationAndroid
import com.rudderstack.android.utilities.applyConfigurationAndroid
import com.rudderstack.core.Analytics
import com.rudderstack.core.Configuration
import com.rudderstack.core.DataUploadService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.rudderstack.android.internal.infrastructure

import android.content.pm.PackageManager
import android.os.Build
import com.rudderstack.android.androidStorage
import com.rudderstack.android.currentConfigurationAndroid
import com.rudderstack.android.utilities.androidStorage
import com.rudderstack.android.utilities.currentConfigurationAndroid
import com.rudderstack.android.storage.AndroidStorage
import com.rudderstack.models.AppVersion
import com.rudderstack.core.Analytics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ package com.rudderstack.android.internal.infrastructure

import android.os.SystemClock
import com.rudderstack.android.LifecycleListenerPlugin
import com.rudderstack.android.androidStorage
import com.rudderstack.android.currentConfigurationAndroid
import com.rudderstack.android.utilities.androidStorage
import com.rudderstack.android.utilities.currentConfigurationAndroid
import com.rudderstack.core.Analytics
import com.rudderstack.core.ConfigDownloadService
import com.rudderstack.core.Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ package com.rudderstack.android.internal.infrastructure
import android.content.Context
import com.rudderstack.android.AndroidUtils
import com.rudderstack.android.ConfigurationAndroid
import com.rudderstack.android.androidStorage
import com.rudderstack.android.contextState
import com.rudderstack.android.currentConfigurationAndroid
import com.rudderstack.android.processNewContext
import com.rudderstack.android.setAnonymousId
import com.rudderstack.android.setUserId
import com.rudderstack.android.utilities.androidStorage
import com.rudderstack.android.utilities.contextState
import com.rudderstack.android.utilities.currentConfigurationAndroid
import com.rudderstack.android.utilities.processNewContext
import com.rudderstack.android.utilities.setAnonymousId
import com.rudderstack.android.utilities.setUserId
import com.rudderstack.android.utilities.initializeSessionManagement
import com.rudderstack.android.utilities.isV1SavedServerConfigContainsSourceId
import com.rudderstack.core.Analytics
Expand All @@ -31,7 +31,6 @@ import com.rudderstack.core.DataUploadService
import com.rudderstack.core.InfrastructurePlugin
import com.rudderstack.models.RudderServerConfig
import com.rudderstack.models.createContext
import com.rudderstack.models.traits
import java.util.concurrent.atomic.AtomicBoolean

/**
Expand Down
Loading