Skip to content

Commit

Permalink
initWithContext synchronization fix (#1903)
Browse files Browse the repository at this point in the history
  • Loading branch information
brismithers authored and jinliu9508 committed Feb 6, 2024
1 parent 79c2ac9 commit 11b402d
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
private var _consentRequired: Boolean? = null
private var _consentGiven: Boolean? = null
private var _disableGMSMissingPrompt: Boolean? = null
private val initLock: Any = Any()
private val loginLock: Any = Any()

private val listOfModules =
Expand Down Expand Up @@ -170,130 +171,158 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
): Boolean {
Logging.log(LogLevel.DEBUG, "initWithContext(context: $context, appId: $appId)")

// do not do this again if already initialized
if (isInitialized) {
return true
}
synchronized(initLock) {
// do not do this again if already initialized
if (isInitialized) {
Logging.log(LogLevel.DEBUG, "initWithContext: SDK already initialized")
return true
}

PreferenceStoreFix.ensureNoObfuscatedPrefStore(context)
Logging.log(LogLevel.DEBUG, "initWithContext: SDK initializing")

// start the application service. This is called explicitly first because we want
// to make sure it has the context provided on input, for all other startable services
// to depend on if needed.
val applicationService = services.getService<IApplicationService>()
(applicationService as ApplicationService).start(context)
PreferenceStoreFix.ensureNoObfuscatedPrefStore(context)

// Give the logging singleton access to the application service to support visual logging.
Logging.applicationService = applicationService
// start the application service. This is called explicitly first because we want
// to make sure it has the context provided on input, for all other startable services
// to depend on if needed.
val applicationService = services.getService<IApplicationService>()
(applicationService as ApplicationService).start(context)

// get the current config model, if there is one
configModel = services.getService<ConfigModelStore>().model
sessionModel = services.getService<SessionModelStore>().model
// Give the logging singleton access to the application service to support visual logging.
Logging.applicationService = applicationService

// initWithContext is called by our internal services/receivers/activites but they do not provide
// an appId (they don't know it). If the app has never called the external initWithContext
// prior to our services/receivers/activities we will blow up, as no appId has been established.
if (appId == null && !configModel!!.hasProperty(ConfigModel::appId.name)) {
Logging.warn("initWithContext called without providing appId, and no appId has been established!")
return false
}
// get the current config model, if there is one
configModel = services.getService<ConfigModelStore>().model
sessionModel = services.getService<SessionModelStore>().model

var forceCreateUser = false
// if the app id was specified as input, update the config model with it
if (appId != null) {
if (!configModel!!.hasProperty(ConfigModel::appId.name) || configModel!!.appId != appId) {
forceCreateUser = true
// initWithContext is called by our internal services/receivers/activites but they do not provide
// an appId (they don't know it). If the app has never called the external initWithContext
// prior to our services/receivers/activities we will blow up, as no appId has been established.
if (appId == null && !configModel!!.hasProperty(ConfigModel::appId.name)) {
Logging.warn("initWithContext called without providing appId, and no appId has been established!")
return false
}
configModel!!.appId = appId
}

// if requires privacy consent was set prior to init, set it in the model now
if (_consentRequired != null) {
configModel!!.consentRequired = _consentRequired!!
}
var forceCreateUser = false
// if the app id was specified as input, update the config model with it
if (appId != null) {
if (!configModel!!.hasProperty(ConfigModel::appId.name) || configModel!!.appId != appId) {
forceCreateUser = true
}
configModel!!.appId = appId
}

// if privacy consent was set prior to init, set it in the model now
if (_consentGiven != null) {
configModel!!.consentGiven = _consentGiven!!
}
// if requires privacy consent was set prior to init, set it in the model now
if (_consentRequired != null) {
configModel!!.consentRequired = _consentRequired!!
}

if (_disableGMSMissingPrompt != null) {
configModel!!.disableGMSMissingPrompt = _disableGMSMissingPrompt!!
}
// if privacy consent was set prior to init, set it in the model now
if (_consentGiven != null) {
configModel!!.consentGiven = _consentGiven!!
}

// "Inject" the services required by this main class
_location = services.getService()
_user = services.getService()
_session = services.getService()
iam = services.getService()
_notifications = services.getService()
operationRepo = services.getService()
propertiesModelStore = services.getService()
identityModelStore = services.getService()
subscriptionModelStore = services.getService()
preferencesService = services.getService()

// Instantiate and call the IStartableServices
startupService = services.getService()
startupService!!.bootstrap()

if (forceCreateUser || !identityModelStore!!.model.hasProperty(IdentityConstants.ONESIGNAL_ID)) {
val legacyPlayerId = preferencesService!!.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID)
if (legacyPlayerId == null) {
Logging.debug("initWithContext: creating new device-scoped user")
createAndSwitchToNewUser()
operationRepo!!.enqueue(
LoginUserOperation(
configModel!!.appId,
identityModelStore!!.model.onesignalId,
identityModelStore!!.model.externalId,
),
)
} else {
Logging.debug("initWithContext: creating user linked to subscription $legacyPlayerId")
if (_disableGMSMissingPrompt != null) {
configModel!!.disableGMSMissingPrompt = _disableGMSMissingPrompt!!
}

// Converting a 4.x SDK to the 5.x SDK. We pull the legacy user sync values to create the subscription model, then enqueue
// a specialized `LoginUserFromSubscriptionOperation`, which will drive fetching/refreshing of the local user
// based on the subscription ID we do have.
val legacyUserSyncString =
// "Inject" the services required by this main class
_location = services.getService()
_user = services.getService()
_session = services.getService()
iam = services.getService()
_notifications = services.getService()
operationRepo = services.getService()
propertiesModelStore = services.getService()
identityModelStore = services.getService()
subscriptionModelStore = services.getService()
preferencesService = services.getService()

// Instantiate and call the IStartableServices
startupService = services.getService()
startupService!!.bootstrap()

if (forceCreateUser || !identityModelStore!!.model.hasProperty(IdentityConstants.ONESIGNAL_ID)) {
val legacyPlayerId =
preferencesService!!.getString(
PreferenceStores.ONESIGNAL,
PreferenceOneSignalKeys.PREFS_LEGACY_USER_SYNCVALUES,
PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID,
)
if (legacyPlayerId == null) {
Logging.debug("initWithContext: creating new device-scoped user")
createAndSwitchToNewUser()
operationRepo!!.enqueue(
LoginUserOperation(
configModel!!.appId,
identityModelStore!!.model.onesignalId,
identityModelStore!!.model.externalId,
),
)
} else {
Logging.debug("initWithContext: creating user linked to subscription $legacyPlayerId")

// Converting a 4.x SDK to the 5.x SDK. We pull the legacy user sync values to create the subscription model, then enqueue
// a specialized `LoginUserFromSubscriptionOperation`, which will drive fetching/refreshing of the local user
// based on the subscription ID we do have.
val legacyUserSyncString =
preferencesService!!.getString(
PreferenceStores.ONESIGNAL,
PreferenceOneSignalKeys.PREFS_LEGACY_USER_SYNCVALUES,
)
var suppressBackendOperation = false

if (legacyUserSyncString != null) {
val legacyUserSyncJSON = JSONObject(legacyUserSyncString)
val notificationTypes = legacyUserSyncJSON.getInt("notification_types")

val pushSubscriptionModel = SubscriptionModel()
pushSubscriptionModel.id = legacyPlayerId
pushSubscriptionModel.type = SubscriptionType.PUSH
pushSubscriptionModel.optedIn =
notificationTypes != SubscriptionStatus.NO_PERMISSION.value && notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value
pushSubscriptionModel.address =
legacyUserSyncJSON.safeString("identifier") ?: ""
pushSubscriptionModel.status = SubscriptionStatus.fromInt(notificationTypes)
?: SubscriptionStatus.NO_PERMISSION
configModel!!.pushSubscriptionId = legacyPlayerId
subscriptionModelStore!!.add(
pushSubscriptionModel,
ModelChangeTags.NO_PROPOGATE,
)
suppressBackendOperation = true
}

createAndSwitchToNewUser(suppressBackendOperation = suppressBackendOperation)

operationRepo!!.enqueue(
LoginUserFromSubscriptionOperation(
configModel!!.appId,
identityModelStore!!.model.onesignalId,
legacyPlayerId,
),
)
preferencesService!!.saveString(
PreferenceStores.ONESIGNAL,
PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID,
null,
)
var suppressBackendOperation = false

if (legacyUserSyncString != null) {
val legacyUserSyncJSON = JSONObject(legacyUserSyncString)
val notificationTypes = legacyUserSyncJSON.getInt("notification_types")

val pushSubscriptionModel = SubscriptionModel()
pushSubscriptionModel.id = legacyPlayerId
pushSubscriptionModel.type = SubscriptionType.PUSH
pushSubscriptionModel.optedIn = notificationTypes != SubscriptionStatus.NO_PERMISSION.value && notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value
pushSubscriptionModel.address = legacyUserSyncJSON.safeString("identifier") ?: ""
pushSubscriptionModel.status = SubscriptionStatus.fromInt(notificationTypes) ?: SubscriptionStatus.NO_PERMISSION
configModel!!.pushSubscriptionId = legacyPlayerId
subscriptionModelStore!!.add(pushSubscriptionModel, ModelChangeTags.NO_PROPOGATE)
suppressBackendOperation = true
}

createAndSwitchToNewUser(suppressBackendOperation = suppressBackendOperation)

} else {
Logging.debug("initWithContext: using cached user ${identityModelStore!!.model.onesignalId}")
operationRepo!!.enqueue(
LoginUserFromSubscriptionOperation(configModel!!.appId, identityModelStore!!.model.onesignalId, legacyPlayerId),
RefreshUserOperation(
configModel!!.appId,
identityModelStore!!.model.onesignalId,
),
)
preferencesService!!.saveString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID, null)
}
} else {
Logging.debug("initWithContext: using cached user ${identityModelStore!!.model.onesignalId}")
operationRepo!!.enqueue(RefreshUserOperation(configModel!!.appId, identityModelStore!!.model.onesignalId))
}

startupService!!.start()
startupService!!.start()

isInitialized = true
isInitialized = true

return true
return true
}
}

override fun login(
Expand All @@ -303,7 +332,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)")

if (!isInitialized) {
Logging.log(LogLevel.ERROR, "Must call 'initWithContext' before using Login")
throw Exception("Must call 'initWithContext' before 'login'")
}

var currentIdentityExternalId: String? = null
Expand Down Expand Up @@ -377,8 +406,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
Logging.log(LogLevel.DEBUG, "logout()")

if (!isInitialized) {
Logging.log(LogLevel.ERROR, "Must call 'initWithContext' before using Login")
return
throw Exception("Must call 'initWithContext' before 'logout'")
}

// only allow one login/logout at a time
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.onesignal.internal

import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
import io.kotest.assertions.throwables.shouldThrowUnit
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.runner.junit4.KotestTestRunner
import org.junit.runner.RunWith

@RunWith(KotestTestRunner::class)
class OneSignalImpTests : FunSpec({
beforeAny {
Logging.logLevel = LogLevel.NONE
}

test("attempting login before initWithContext throws exception") {
// Given
val os = OneSignalImp()

// When
val exception =
shouldThrowUnit<Exception> {
os.login("login-id")
}

// Then
exception.message shouldBe "Must call 'initWithContext' before 'login'"
}

test("attempting logout before initWithContext throws exception") {
// Given
val os = OneSignalImp()

// When
val exception =
shouldThrowUnit<Exception> {
os.logout()
}

// Then
exception.message shouldBe "Must call 'initWithContext' before 'logout'"
}
})

0 comments on commit 11b402d

Please sign in to comment.