Skip to content

Commit

Permalink
feat: implement the analytics SDK as a singleton (#424)
Browse files Browse the repository at this point in the history
* feat: make createInstance return a singleton analytics instance

* refactor: change createInstance to getInstance

* chore: remove `getInstance` method

* test: update the RudderAnalyticsTest test cases

* chore(sample app): remove secondary analytics instance setup

* chore: remove redundant else block

* feat: implement double-checked locking for singleton analytics instance

* chore: add custom annotation to indicate that AnalyticsRegistry could be used in the future

* chore: refactor RudderAnalyticsBuilderCompat

* chore(sample app): refactor sdk init snippet

* chore: remove unnecessary comments

* refactor: move FutureUse annotation inside `core` module

So that it could have a greater visibility from other modules.

* refactor: move AnalyticsUtil.kt into utilities package
  • Loading branch information
1abhishekpandey authored May 28, 2024
1 parent a768c89 commit 99bed42
Show file tree
Hide file tree
Showing 33 changed files with 386 additions and 620 deletions.
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.")
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? {
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

0 comments on commit 99bed42

Please sign in to comment.