Skip to content

Commit 11b402d

Browse files
brismithersjinliu9508
authored andcommitted
initWithContext synchronization fix (#1903)
1 parent 79c2ac9 commit 11b402d

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
@@ -130,6 +130,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
130130
private var _consentRequired: Boolean? = null
131131
private var _consentGiven: Boolean? = null
132132
private var _disableGMSMissingPrompt: Boolean? = null
133+
private val initLock: Any = Any()
133134
private val loginLock: Any = Any()
134135

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

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

178-
PreferenceStoreFix.ensureNoObfuscatedPrefStore(context)
181+
Logging.log(LogLevel.DEBUG, "initWithContext: SDK initializing")
179182

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

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

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

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

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

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

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

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

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

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

292-
startupService!!.start()
320+
startupService!!.start()
293321

294-
isInitialized = true
322+
isInitialized = true
295323

296-
return true
324+
return true
325+
}
297326
}
298327

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

305334
if (!isInitialized) {
306-
Logging.log(LogLevel.ERROR, "Must call 'initWithContext' before using Login")
335+
throw Exception("Must call 'initWithContext' before 'login'")
307336
}
308337

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

379408
if (!isInitialized) {
380-
Logging.log(LogLevel.ERROR, "Must call 'initWithContext' before using Login")
381-
return
409+
throw Exception("Must call 'initWithContext' before 'logout'")
382410
}
383411

384412
// 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)