Skip to content

Commit

Permalink
Updating Android SDK to version 32.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
radixdev committed Aug 21, 2024
1 parent dd84c29 commit 04d10e6
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 150 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## 32.1.0

[Release Date](https://github.com/braze-inc/braze-android-sdk/releases/tag/v32.1.0)

##### Fixed
- Fixed an issue where geofence events could not be sent when the app is in the background.
- Fixed an issue where In-App Messages would fail to be dismissed when the host app is using the predictive back gesture.

##### Added
- Added support for an upcoming Braze SDK Debugging tool.
- Added the ability to prevent certain edge cases where the SDK could show In-App Messages to different users than the one that triggered the In-App Message.
- Configured via `braze.xml` through `<bool name="com_braze_prevent_in_app_message_display_for_different_user">true</bool>`.
- Can also be configured via runtime configuration through `BrazeConfig.setShouldPreventInAppMessageDisplayForDifferentUsers()`.
- Defaults to false. Note that even when false, the SDK will still prevent most cases of showing In-App Messages to different users. This configuration option is designed to prevent edge cases such as when the user changes while on a `BrazeActivityLifecycleCallbackListener` blocked Activity or when a mismatched message is still in the stack.

##### Changed
- Changed the behavior of the `Braze.getDeviceId()` method to return a different device ID based on the API key used to initialize the SDK.

## 32.0.0

[Release Date](https://github.com/braze-inc/braze-android-sdk/releases/tag/v32.0.0)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Our SDK is now hosted in Maven Central. You can remove `https://braze-inc.github

```
dependencies {
implementation 'com.braze:android-sdk-ui:32.0.+'
implementation 'com.braze:android-sdk-location:32.0.+'
implementation 'com.braze:android-sdk-ui:32.1.+'
implementation 'com.braze:android-sdk-location:32.1.+'
...
}
```
Expand All @@ -56,7 +56,7 @@ repositories {

```
dependencies {
implementation 'com.braze:android-sdk-ui:32.0.+'
implementation 'com.braze:android-sdk-ui:32.1.+'
}
```

Expand Down
2 changes: 1 addition & 1 deletion android-sdk-location/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<application>
<receiver
android:name="com.braze.location.BrazeActionReceiver"
android:exported="false">
android:exported="true">
</receiver>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting
import com.braze.managers.BrazeGeofenceManager
import com.braze.managers.IBrazeGeofenceLocationUpdateListener
import com.braze.models.BrazeGeofence
Expand All @@ -28,44 +29,63 @@ object GooglePlayLocationUtils {
*
* If a given geofence is already registered with Google Play Location Services, it will not be
* needlessly re-registered. Geofences that are registered with Google Play Location Services but
* not included in the given list of geofences will be un-registered.
* not included in [desiredGeofencesToRegister] will be un-registered.
*
* If the given geofence list is empty, geofences will be un-registered and deleted from local
* If [desiredGeofencesToRegister] is empty, all geofences will be un-registered and deleted from local
* storage.
* @param context used by shared preferences
* @param geofenceList list of [BrazeGeofence] objects
* @param geofenceRequestIntent pending intent to fire when geofences transition events occur
* @param desiredGeofencesToRegister list of [BrazeGeofence] objects to register if new or updated. Otherwise ignored.
* @param geofenceRequestIntent pending intent to fire when geofences transition events occur.
* @param removalFunction function to remove geofences from Google Play Location Services.
* @param registerFunction function to register geofences with Google Play Location Services.
*/
@JvmStatic
fun registerGeofencesWithGooglePlayIfNecessary(
context: Context,
geofenceList: List<BrazeGeofence>,
geofenceRequestIntent: PendingIntent
desiredGeofencesToRegister: List<BrazeGeofence>,
geofenceRequestIntent: PendingIntent,
removalFunction: (List<String>) -> Unit = { removeGeofencesRegisteredWithGeofencingClient(context, it) },
registerFunction: (List<BrazeGeofence>) -> Unit = { registerGeofencesWithGeofencingClient(context, it, geofenceRequestIntent) }
) {
brazelog(V) { "registerGeofencesWithGooglePlayIfNecessary called with $desiredGeofencesToRegister" }
try {
val prefs = getRegisteredGeofenceSharedPrefs(context)
val registeredGeofences = BrazeGeofenceManager.retrieveBrazeGeofencesFromLocalStorage(prefs)
val registeredGeofencesById = registeredGeofences.associateBy { it.id }

val newGeofencesToRegister = geofenceList.filter { newGeofence ->
registeredGeofences.none { registeredGeofence ->
registeredGeofence.id == newGeofence.id && registeredGeofence.equivalentServerData(newGeofence)
}
}
// Given the input [desiredGeofencesToRegister] and the [registeredGeofences], we need to determine
// which geofences are to be registered, which are obsolete, and which will be no-ops.
// We can do this by comparing the geofence data against the registered geofences.
// We only want to register geofences that are not already registered.
// An obsolete Geofence is one that is registered with Google Play Services but is not in the desired list.

// If any previously registered Geofence is missing from the desired list, it is obsolete.
val obsoleteGeofenceIds = registeredGeofences.filter { registeredGeofence ->
newGeofencesToRegister.none { newGeofence ->
newGeofence.id == registeredGeofence.id
desiredGeofencesToRegister.none { desiredGeofence ->
desiredGeofence.id == registeredGeofence.id
}
}.map { it.id }

// If any desired Geofence is not already registered, it is new and needs to be registered.
// Additionally, any previously registered geofence that has received updates should be re-registered.
val newGeofencesToRegister = mutableListOf<BrazeGeofence>()
for (desiredGeofence in desiredGeofencesToRegister) {
val registeredGeofenceWithSameId = registeredGeofencesById[desiredGeofence.id]
if (registeredGeofenceWithSameId == null || !desiredGeofence.equivalentServerData(registeredGeofenceWithSameId)) {
brazelog { "Geofence with id: ${desiredGeofence.id} is new or has been updated." }
newGeofencesToRegister.add(desiredGeofence)
}
}

if (obsoleteGeofenceIds.isNotEmpty()) {
brazelog { "Un-registering ${obsoleteGeofenceIds.size} obsolete geofences from Google Play Services." }
removeGeofencesRegisteredWithGeofencingClient(context, obsoleteGeofenceIds)
brazelog { "Un-registering $obsoleteGeofenceIds obsolete geofences from Google Play Services." }
removalFunction(obsoleteGeofenceIds)
} else {
brazelog { "No obsolete geofences need to be unregistered from Google Play Services." }
}
if (newGeofencesToRegister.isNotEmpty()) {
brazelog { "Registering ${newGeofencesToRegister.size} new geofences with Google Play Services." }
registerGeofencesWithGeofencingClient(context, newGeofencesToRegister, geofenceRequestIntent)
brazelog { "Registering $newGeofencesToRegister new geofences with Google Play Services." }
registerFunction(newGeofencesToRegister)
} else {
brazelog { "No new geofences need to be registered with Google Play Services." }
}
Expand Down Expand Up @@ -166,7 +186,8 @@ object GooglePlayLocationUtils {
*
* @param obsoleteGeofenceIds List of [String]s containing Geofence IDs that needs to be un-registered
*/
private fun removeGeofencesRegisteredWithGeofencingClient(context: Context, obsoleteGeofenceIds: List<String>) {
@VisibleForTesting
internal fun removeGeofencesRegisteredWithGeofencingClient(context: Context, obsoleteGeofenceIds: List<String>) {
LocationServices.getGeofencingClient(context).removeGeofences(obsoleteGeofenceIds)
.addOnSuccessListener {
brazelog { "Geofences successfully un-registered with Google Play Services." }
Expand Down Expand Up @@ -201,15 +222,17 @@ object GooglePlayLocationUtils {
/**
* Returns a [SharedPreferences] instance holding list of registered [BrazeGeofence]s.
*/
private fun getRegisteredGeofenceSharedPrefs(context: Context): SharedPreferences =
@VisibleForTesting
internal fun getRegisteredGeofenceSharedPrefs(context: Context): SharedPreferences =
context.getSharedPreferences(REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION, Context.MODE_PRIVATE)

/**
* Stores the list of [BrazeGeofence] which are successfully registered.
*
* @param newGeofencesToRegister List of [BrazeGeofence]s to store in SharedPreferences
*/
private fun storeGeofencesToSharedPrefs(context: Context, newGeofencesToRegister: List<BrazeGeofence>) {
@VisibleForTesting
internal fun storeGeofencesToSharedPrefs(context: Context, newGeofencesToRegister: List<BrazeGeofence>) {
val editor = getRegisteredGeofenceSharedPrefs(context).edit()
for (brazeGeofence in newGeofencesToRegister) {
editor.putString(brazeGeofence.id, brazeGeofence.forJsonPut().toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,10 @@ open class BrazeActivityLifecycleCallbackListener @JvmOverloads constructor(
}

override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
if (registerInAppMessageManager &&
shouldHandleLifecycleMethodsInActivity(activity, false)
) {
brazelog(V) {
"Automatically calling lifecycle method: ensureSubscribedToInAppMessageEvents for class: ${activity.javaClass}"
}
BrazeInAppMessageManager.getInstance()
.ensureSubscribedToInAppMessageEvents(activity.applicationContext)
brazelog(V) {
"Automatically calling lifecycle method: ensureSubscribedToInAppMessageEvents for class: ${activity.javaClass}"
}
BrazeInAppMessageManager.getInstance().ensureSubscribedToInAppMessageEvents(activity.applicationContext)
}

override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.braze.BrazeInternal
import com.braze.BrazeInternal.retryInAppMessage
import com.braze.configuration.BrazeConfigurationProvider
import com.braze.enums.inappmessage.Orientation
import com.braze.events.BrazeUserChangeEvent
import com.braze.events.IEventSubscriber
import com.braze.events.InAppMessageEvent
import com.braze.events.SdkDataWipeEvent
Expand Down Expand Up @@ -78,6 +79,7 @@ import kotlin.concurrent.withLock
*/
// Static field leak doesn't apply to this singleton since the activity is nullified after the manager is unregistered.
@SuppressLint("StaticFieldLeak")
@Suppress("TooManyFunctions")
open class BrazeInAppMessageManager : InAppMessageManagerBase() {
private val inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener =
DefaultInAppMessageViewLifecycleListener()
Expand All @@ -94,10 +96,16 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() {
val inAppMessageEventMap = mutableMapOf<IInAppMessage, InAppMessageEvent>()
private var inAppMessageEventSubscriber: IEventSubscriber<InAppMessageEvent>? = null
private var sdkDataWipeEventSubscriber: IEventSubscriber<SdkDataWipeEvent>? = null
private var brazeUserChangeEventSubscriber: IEventSubscriber<BrazeUserChangeEvent>? = null
private var originalOrientation: Int? = null
private var configurationProvider: BrazeConfigurationProvider? = null
private var inAppMessageViewWrapper: IInAppMessageViewWrapper? = null

/**
* The last seen user id from the [BrazeUserChangeEvent] subscriber.
*/
private var currentUserId: String = ""

/**
* An In-App Message being carried over during the
* [unregisterInAppMessageManager]
Expand Down Expand Up @@ -167,6 +175,20 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() {
it, SdkDataWipeEvent::class.java
)
}

if (brazeUserChangeEventSubscriber != null) {
brazelog(V) { "Removing existing user change event subscriber before subscribing a new one." }
getInstance(context).removeSingleSubscription(
brazeUserChangeEventSubscriber,
BrazeUserChangeEvent::class.java
)
}

brazeUserChangeEventSubscriber = createBrazeUserChangeEventSubscriber(context).also {
getInstance(context).addSingleSynchronousSubscription(
it, BrazeUserChangeEvent::class.java
)
}
}

/**
Expand Down Expand Up @@ -485,6 +507,19 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() {
throw Exception("Current orientation did not match specified orientation for in-app message. Doing nothing.")
}

// Verify this message is for the intended user
val configProvider = configurationProvider
?: throw Exception(
"configurationProvider is null. The in-app message will not be displayed and will not be" +
"put back on the stack."
)
if (configProvider.isPreventInAppMessageDisplayForDifferentUsersEnabled && !isInAppMessageForTheSameUser(inAppMessage, currentUserId)) {
throw Exception(
"The last identifier user $currentUserId does not match the in-app message's user. " +
"The in-app message will not be displayed and will not be put back on the stack."
)
}

// At this point, the only factors that would inhibit in-app message display are view creation issues.
// Since control in-app messages have no view, this is the end of execution for control in-app messages
if (inAppMessage.isControl) {
Expand Down Expand Up @@ -520,10 +555,7 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() {
resetAfterInAppMessageClose()
return
}
val inAppMessageViewFactory = getInAppMessageViewFactory(inAppMessage)
if (inAppMessageViewFactory == null) {
throw Exception("ViewFactory from getInAppMessageViewFactory was null.")
}
val inAppMessageViewFactory = getInAppMessageViewFactory(inAppMessage) ?: throw Exception("ViewFactory from getInAppMessageViewFactory was null.")
val inAppMessageView = inAppMessageViewFactory.createInAppMessageView(
activity, inAppMessage
)
Expand All @@ -542,11 +574,6 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() {
)
}

val configProvider = configurationProvider
?: throw Exception(
"configurationProvider is null. The in-app message will not be displayed and will not be" +
"put back on the stack."
)
val openingAnimation = inAppMessageAnimationFactory.getOpeningAnimation(inAppMessage)
val closingAnimation = inAppMessageAnimationFactory.getClosingAnimation(inAppMessage)
val viewWrapperFactory = inAppMessageViewWrapperFactory
Expand Down Expand Up @@ -635,6 +662,24 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() {
}
}

private fun createBrazeUserChangeEventSubscriber(context: Context): IEventSubscriber<BrazeUserChangeEvent> {
return IEventSubscriber { event: BrazeUserChangeEvent ->
brazelog(V) { "InAppMessage manager handling new current user id: '$event'" }
val configurationProvider = BrazeInternal.getConfigurationProvider(context)
if (!configurationProvider.isPreventInAppMessageDisplayForDifferentUsersEnabled) {
brazelog(V) { "Not cleansing in-app messages on user id change" }
return@IEventSubscriber
}
val currentUserId = event.currentUserId
this.currentUserId = currentUserId
brazelog { "Removing in-app messages not from user $currentUserId" }

inAppMessageStack.removeAll { !isInAppMessageForTheSameUser(it, currentUserId) }
if (!isInAppMessageForTheSameUser(carryoverInAppMessage, currentUserId)) carryoverInAppMessage = null
if (!isInAppMessageForTheSameUser(unregisteredInAppMessage, currentUserId)) unregisteredInAppMessage = null
}
}

/**
* For in-app messages that have a preferred orientation, locks the screen orientation and
* returns true if the screen is currently in the preferred orientation. If the screen is not
Expand Down Expand Up @@ -673,6 +718,19 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() {
return true
}

/**
* Determines whether the in-app message was triggered for the same user as the current user.
* @return true if the in-app message was triggered for the same user as the current
* user, false otherwise. Returns false if the message is null.
*/
@VisibleForTesting
open fun isInAppMessageForTheSameUser(inAppMessage: IInAppMessage?, currentUserId: String): Boolean {
if (inAppMessage == null) return true

val inAppMessageUserId = inAppMessageEventMap[inAppMessage]?.userId
return inAppMessageUserId == null || inAppMessageUserId == currentUserId
}

companion object {
private val instanceLock = ReentrantLock()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.braze.ui.inappmessage

import android.app.Activity
import android.os.Build
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.widget.FrameLayout
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.braze.configuration.BrazeConfigurationProvider
Expand All @@ -23,6 +26,7 @@ import com.braze.ui.inappmessage.listeners.IInAppMessageViewLifecycleListener
import com.braze.ui.inappmessage.listeners.SwipeDismissTouchListener.DismissCallbacks
import com.braze.ui.inappmessage.listeners.TouchAwareSwipeDismissTouchListener
import com.braze.ui.inappmessage.listeners.TouchAwareSwipeDismissTouchListener.ITouchListener
import com.braze.ui.inappmessage.utils.InAppMessageViewUtils
import com.braze.ui.inappmessage.views.IInAppMessageImmersiveView
import com.braze.ui.inappmessage.views.IInAppMessageView
import com.braze.ui.inappmessage.views.InAppMessageHtmlBaseView
Expand Down Expand Up @@ -154,6 +158,19 @@ open class DefaultInAppMessageViewWrapper @JvmOverloads constructor(
inAppMessageViewLifecycleListener
)
}

if (BrazeInAppMessageManager.getInstance().doesBackButtonDismissInAppMessageView && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.let {
val dismissInAppMessageCallback = object : OnBackInvokedCallback {
override fun onBackInvoked() {
InAppMessageViewUtils.closeInAppMessageOnKeycodeBack()
it.onBackInvokedDispatcher.unregisterOnBackInvokedCallback(this)
}
}

it.onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_OVERLAY, dismissInAppMessageCallback)
}
}
}

override fun close() {
Expand Down
Loading

0 comments on commit 04d10e6

Please sign in to comment.