-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement the analytics SDK as a singleton (#424)
* 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
1 parent
a768c89
commit 99bed42
Showing
33 changed files
with
386 additions
and
620 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
275 changes: 51 additions & 224 deletions
275
android/src/main/java/com/rudderstack/android/RudderAnalytics.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.