Skip to content

Commit

Permalink
Merge branch 'user-model/main' into update_between_sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
nan-li authored Dec 1, 2023
2 parents f16e039 + a5bfd21 commit 6328a5d
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 98 deletions.
2 changes: 1 addition & 1 deletion MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ The user name space is accessible via `OneSignal.User` (in Kotlin) or `OneSignal
| `fun addTags(tags: Map<String, String>)` | `void addTags(Map<String, String> tags)` | *Add multiple tags for the current user. Tags are key:value pairs used as building blocks for targeting specific users and/or personalizing messages. If the tag key already exists, it will be replaced with the value provided here.* |
| `fun removeTag(key: String)` | `void removeTag(String key)` | *Remove the data tag with the provided key from the current user.* |
| `fun removeTags(keys: Collection<String>)` | `void removeTags(Collection<String> keys)` | *Remove multiple tags from the current user.* |

| `fun getTags()` | `Map<String, String> getTags()` | *Return a copy of all local tags from the current user.*

**Session Namespace**
The session namespace is accessible via `OneSignal.Session` (in Kotlin) or `OneSignal.getSession()` (in Java) and provides access to session-scoped functionality.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.onesignal.core.internal.preferences

import android.content.Context
import android.os.Build
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
import java.io.File

object PreferenceStoreFix {
/**
* Ensure the OneSignal preference store is not using the v4 obfuscated version, if one
* exists.
*/
fun ensureNoObfuscatedPrefStore(context: Context) {
try {
// In the v4 version the OneSignal shared preference name was based on the OneSignal
// class name, which might be minimized/obfuscated if the app is using ProGuard or
// similar. In order for a device to successfully migrate from v4 to v5 picking
// up the subscription, we need to copy the shared preferences from the obfuscated
// version to the static "OneSignal" preference name. We only do this
// if there isn't already a "OneSignal" preference store.
val sharedPrefsDir =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
File(context.dataDir, "shared_prefs")
} else {
File(context.filesDir.parentFile, "shared_prefs")
}

val osPrefsFile = File(sharedPrefsDir, "OneSignal.xml")

if (!sharedPrefsDir.exists() || !sharedPrefsDir.isDirectory || osPrefsFile.exists()) {
return
}

val prefsFileList = sharedPrefsDir.listFiles() ?: return

// Go through every preference file, looking for the OneSignal preference store.
for (prefsFile in prefsFileList) {
val prefsStore =
context.getSharedPreferences(prefsFile.nameWithoutExtension, Context.MODE_PRIVATE)

if (prefsStore.contains(PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID)) {
prefsFile.renameTo(osPrefsFile)
return
}
}
} catch (e: Throwable) {
Logging.log(LogLevel.ERROR, "error attempting to fix obfuscated preference store", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.core.internal.operations.IOperationRepo
import com.onesignal.core.internal.preferences.IPreferencesService
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
import com.onesignal.core.internal.preferences.PreferenceStoreFix
import com.onesignal.core.internal.preferences.PreferenceStores
import com.onesignal.core.internal.startup.StartupService
import com.onesignal.debug.IDebugManager
Expand Down Expand Up @@ -133,6 +134,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 @@ -173,93 +175,140 @@ 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
}

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

// Give the logging singleton access to the application service to support visual logging.
Logging.applicationService = applicationService

// get the current config model, if there is one
configModel = services.getService<ConfigModelStore>().model
sessionModel = services.getService<SessionModelStore>().model

// 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
}
Logging.log(LogLevel.DEBUG, "initWithContext: SDK initializing")

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
PreferenceStoreFix.ensureNoObfuscatedPrefStore(context)

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

// Give the logging singleton access to the application service to support visual logging.
Logging.applicationService = applicationService

// get the current config model, if there is one
configModel = services.getService<ConfigModelStore>().model
sessionModel = services.getService<SessionModelStore>().model

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

Expand All @@ -282,24 +331,22 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
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 @@ -309,7 +356,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 @@ -383,8 +430,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
Expand Up @@ -133,4 +133,9 @@ interface IUserManager {
* @param keys The collection of keys, all of which will be removed from the current user.
*/
fun removeTags(keys: Collection<String>)

/**
* Return a copy of all local tags from the current user.
*/
fun getTags(): Map<String, String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ internal open class UserManager(
val externalId: String?
get() = _identityModel.externalId

val tags: Map<String, String>
get() = _propertiesModel.tags

val aliases: Map<String, String>
get() = _identityModel.filter { it.key != IdentityModel::id.name }.toMap()

Expand Down Expand Up @@ -218,4 +215,8 @@ internal open class UserManager(
_propertiesModel.tags.remove(it)
}
}

override fun getTags(): Map<String, String> {
return _propertiesModel.tags.toMap()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ internal class UserBackendService(
requestJSON.put("properties", JSONObject().putMap(properties))
}

requestJSON.put("refresh_device_metadata", true)

val response = _httpClient.post("apps/$appId/users", requestJSON)

if (!response.isSuccess) {
Expand Down
Loading

0 comments on commit 6328a5d

Please sign in to comment.