Skip to content

Commit 39af8e8

Browse files
authored
initWithContext synchronization fix (#1903)
1 parent dcc3860 commit 39af8e8

File tree

2 files changed

+177
-105
lines changed

2 files changed

+177
-105
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt

Lines changed: 133 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
131131
private var _consentRequired: Boolean? = null
132132
private var _consentGiven: Boolean? = null
133133
private var _disableGMSMissingPrompt: Boolean? = null
134+
private val initLock: Any = Any()
134135
private val loginLock: Any = Any()
135136

136137
private val listOfModules =
@@ -171,130 +172,158 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
171172
): Boolean {
172173
Logging.log(LogLevel.DEBUG, "initWithContext(context: $context, appId: $appId)")
173174

174-
// do not do this again if already initialized
175-
if (isInitialized) {
176-
return true
177-
}
175+
synchronized(initLock) {
176+
// do not do this again if already initialized
177+
if (isInitialized) {
178+
Logging.log(LogLevel.DEBUG, "initWithContext: SDK already initialized")
179+
return true
180+
}
178181

179-
PreferenceStoreFix.ensureNoObfuscatedPrefStore(context)
182+
Logging.log(LogLevel.DEBUG, "initWithContext: SDK initializing")
180183

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

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

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

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

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

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

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

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

225-
// "Inject" the services required by this main class
226-
_location = services.getService()
227-
_user = services.getService()
228-
_session = services.getService()
229-
iam = services.getService()
230-
_notifications = services.getService()
231-
operationRepo = services.getService()
232-
propertiesModelStore = services.getService()
233-
identityModelStore = services.getService()
234-
subscriptionModelStore = services.getService()
235-
preferencesService = services.getService()
236-
237-
// Instantiate and call the IStartableServices
238-
startupService = services.getService()
239-
startupService!!.bootstrap()
240-
241-
if (forceCreateUser || !identityModelStore!!.model.hasProperty(IdentityConstants.ONESIGNAL_ID)) {
242-
val legacyPlayerId = preferencesService!!.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID)
243-
if (legacyPlayerId == null) {
244-
Logging.debug("initWithContext: creating new device-scoped user")
245-
createAndSwitchToNewUser()
246-
operationRepo!!.enqueue(
247-
LoginUserOperation(
248-
configModel!!.appId,
249-
identityModelStore!!.model.onesignalId,
250-
identityModelStore!!.model.externalId,
251-
),
252-
)
253-
} else {
254-
Logging.debug("initWithContext: creating user linked to subscription $legacyPlayerId")
226+
if (_disableGMSMissingPrompt != null) {
227+
configModel!!.disableGMSMissingPrompt = _disableGMSMissingPrompt!!
228+
}
255229

256-
// Converting a 4.x SDK to the 5.x SDK. We pull the legacy user sync values to create the subscription model, then enqueue
257-
// a specialized `LoginUserFromSubscriptionOperation`, which will drive fetching/refreshing of the local user
258-
// based on the subscription ID we do have.
259-
val legacyUserSyncString =
230+
// "Inject" the services required by this main class
231+
_location = services.getService()
232+
_user = services.getService()
233+
_session = services.getService()
234+
iam = services.getService()
235+
_notifications = services.getService()
236+
operationRepo = services.getService()
237+
propertiesModelStore = services.getService()
238+
identityModelStore = services.getService()
239+
subscriptionModelStore = services.getService()
240+
preferencesService = services.getService()
241+
242+
// Instantiate and call the IStartableServices
243+
startupService = services.getService()
244+
startupService!!.bootstrap()
245+
246+
if (forceCreateUser || !identityModelStore!!.model.hasProperty(IdentityConstants.ONESIGNAL_ID)) {
247+
val legacyPlayerId =
260248
preferencesService!!.getString(
261249
PreferenceStores.ONESIGNAL,
262-
PreferenceOneSignalKeys.PREFS_LEGACY_USER_SYNCVALUES,
250+
PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID,
251+
)
252+
if (legacyPlayerId == null) {
253+
Logging.debug("initWithContext: creating new device-scoped user")
254+
createAndSwitchToNewUser()
255+
operationRepo!!.enqueue(
256+
LoginUserOperation(
257+
configModel!!.appId,
258+
identityModelStore!!.model.onesignalId,
259+
identityModelStore!!.model.externalId,
260+
),
261+
)
262+
} else {
263+
Logging.debug("initWithContext: creating user linked to subscription $legacyPlayerId")
264+
265+
// Converting a 4.x SDK to the 5.x SDK. We pull the legacy user sync values to create the subscription model, then enqueue
266+
// a specialized `LoginUserFromSubscriptionOperation`, which will drive fetching/refreshing of the local user
267+
// based on the subscription ID we do have.
268+
val legacyUserSyncString =
269+
preferencesService!!.getString(
270+
PreferenceStores.ONESIGNAL,
271+
PreferenceOneSignalKeys.PREFS_LEGACY_USER_SYNCVALUES,
272+
)
273+
var suppressBackendOperation = false
274+
275+
if (legacyUserSyncString != null) {
276+
val legacyUserSyncJSON = JSONObject(legacyUserSyncString)
277+
val notificationTypes = legacyUserSyncJSON.getInt("notification_types")
278+
279+
val pushSubscriptionModel = SubscriptionModel()
280+
pushSubscriptionModel.id = legacyPlayerId
281+
pushSubscriptionModel.type = SubscriptionType.PUSH
282+
pushSubscriptionModel.optedIn =
283+
notificationTypes != SubscriptionStatus.NO_PERMISSION.value && notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value
284+
pushSubscriptionModel.address =
285+
legacyUserSyncJSON.safeString("identifier") ?: ""
286+
pushSubscriptionModel.status = SubscriptionStatus.fromInt(notificationTypes)
287+
?: SubscriptionStatus.NO_PERMISSION
288+
configModel!!.pushSubscriptionId = legacyPlayerId
289+
subscriptionModelStore!!.add(
290+
pushSubscriptionModel,
291+
ModelChangeTags.NO_PROPOGATE,
292+
)
293+
suppressBackendOperation = true
294+
}
295+
296+
createAndSwitchToNewUser(suppressBackendOperation = suppressBackendOperation)
297+
298+
operationRepo!!.enqueue(
299+
LoginUserFromSubscriptionOperation(
300+
configModel!!.appId,
301+
identityModelStore!!.model.onesignalId,
302+
legacyPlayerId,
303+
),
304+
)
305+
preferencesService!!.saveString(
306+
PreferenceStores.ONESIGNAL,
307+
PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID,
308+
null,
263309
)
264-
var suppressBackendOperation = false
265-
266-
if (legacyUserSyncString != null) {
267-
val legacyUserSyncJSON = JSONObject(legacyUserSyncString)
268-
val notificationTypes = legacyUserSyncJSON.getInt("notification_types")
269-
270-
val pushSubscriptionModel = SubscriptionModel()
271-
pushSubscriptionModel.id = legacyPlayerId
272-
pushSubscriptionModel.type = SubscriptionType.PUSH
273-
pushSubscriptionModel.optedIn = notificationTypes != SubscriptionStatus.NO_PERMISSION.value && notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value
274-
pushSubscriptionModel.address = legacyUserSyncJSON.safeString("identifier") ?: ""
275-
pushSubscriptionModel.status = SubscriptionStatus.fromInt(notificationTypes) ?: SubscriptionStatus.NO_PERMISSION
276-
configModel!!.pushSubscriptionId = legacyPlayerId
277-
subscriptionModelStore!!.add(pushSubscriptionModel, ModelChangeTags.NO_PROPOGATE)
278-
suppressBackendOperation = true
279310
}
280-
281-
createAndSwitchToNewUser(suppressBackendOperation = suppressBackendOperation)
282-
311+
} else {
312+
Logging.debug("initWithContext: using cached user ${identityModelStore!!.model.onesignalId}")
283313
operationRepo!!.enqueue(
284-
LoginUserFromSubscriptionOperation(configModel!!.appId, identityModelStore!!.model.onesignalId, legacyPlayerId),
314+
RefreshUserOperation(
315+
configModel!!.appId,
316+
identityModelStore!!.model.onesignalId,
317+
),
285318
)
286-
preferencesService!!.saveString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID, null)
287319
}
288-
} else {
289-
Logging.debug("initWithContext: using cached user ${identityModelStore!!.model.onesignalId}")
290-
operationRepo!!.enqueue(RefreshUserOperation(configModel!!.appId, identityModelStore!!.model.onesignalId))
291-
}
292320

293-
startupService!!.start()
321+
startupService!!.start()
294322

295-
isInitialized = true
323+
isInitialized = true
296324

297-
return true
325+
return true
326+
}
298327
}
299328

300329
override fun login(
@@ -304,7 +333,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
304333
Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)")
305334

306335
if (!isInitialized) {
307-
Logging.log(LogLevel.ERROR, "Must call 'initWithContext' before using Login")
336+
throw Exception("Must call 'initWithContext' before 'login'")
308337
}
309338

310339
var currentIdentityExternalId: String? = null
@@ -378,8 +407,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
378407
Logging.log(LogLevel.DEBUG, "logout()")
379408

380409
if (!isInitialized) {
381-
Logging.log(LogLevel.ERROR, "Must call 'initWithContext' before using Login")
382-
return
410+
throw Exception("Must call 'initWithContext' before 'logout'")
383411
}
384412

385413
// only allow one login/logout at a time
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.onesignal.internal
2+
3+
import com.onesignal.debug.LogLevel
4+
import com.onesignal.debug.internal.logging.Logging
5+
import io.kotest.assertions.throwables.shouldThrowUnit
6+
import io.kotest.core.spec.style.FunSpec
7+
import io.kotest.matchers.shouldBe
8+
import io.kotest.runner.junit4.KotestTestRunner
9+
import org.junit.runner.RunWith
10+
11+
@RunWith(KotestTestRunner::class)
12+
class OneSignalImpTests : FunSpec({
13+
beforeAny {
14+
Logging.logLevel = LogLevel.NONE
15+
}
16+
17+
test("attempting login before initWithContext throws exception") {
18+
// Given
19+
val os = OneSignalImp()
20+
21+
// When
22+
val exception =
23+
shouldThrowUnit<Exception> {
24+
os.login("login-id")
25+
}
26+
27+
// Then
28+
exception.message shouldBe "Must call 'initWithContext' before 'login'"
29+
}
30+
31+
test("attempting logout before initWithContext throws exception") {
32+
// Given
33+
val os = OneSignalImp()
34+
35+
// When
36+
val exception =
37+
shouldThrowUnit<Exception> {
38+
os.logout()
39+
}
40+
41+
// Then
42+
exception.message shouldBe "Must call 'initWithContext' before 'logout'"
43+
}
44+
})

0 commit comments

Comments
 (0)