From 04d10e64918bab05f00d67e550d45e0db2829542 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 21 Aug 2024 11:59:19 -0400 Subject: [PATCH] Updating Android SDK to version 32.1.0 --- CHANGELOG.md | 18 ++++ README.md | 6 +- .../src/main/AndroidManifest.xml | 2 +- .../braze/location/GooglePlayLocationUtils.kt | 63 +++++++---- .../BrazeActivityLifecycleCallbackListener.kt | 11 +- .../inappmessage/BrazeInAppMessageManager.kt | 76 +++++++++++-- .../DefaultInAppMessageViewWrapper.kt | 17 +++ build.gradle | 4 +- .../sample/activity/GeofencesMapActivity.java | 102 ------------------ .../sample/activity/GeofencesMapActivity.kt | 99 +++++++++++++++++ .../activity/InAppMessageSandboxActivity.kt | 12 ++- .../activity_in_app_message_sandbox.xml | 9 ++ gradle.properties | 4 +- samples/firebase-push/google-services.json | 2 +- .../google-tag-manager/google-services.json | 2 +- samples/hello-braze/proguard-rules.pro | 18 ++++ 16 files changed, 295 insertions(+), 150 deletions(-) delete mode 100644 droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.java create mode 100644 droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.kt create mode 100644 samples/hello-braze/proguard-rules.pro diff --git a/CHANGELOG.md b/CHANGELOG.md index d3cba33c0d..be56593893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `true`. + - 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) diff --git a/README.md b/README.md index bcfd8dc2ca..1045fcc90d 100644 --- a/README.md +++ b/README.md @@ -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.+' ... } ``` @@ -56,7 +56,7 @@ repositories { ``` dependencies { - implementation 'com.braze:android-sdk-ui:32.0.+' + implementation 'com.braze:android-sdk-ui:32.1.+' } ``` diff --git a/android-sdk-location/src/main/AndroidManifest.xml b/android-sdk-location/src/main/AndroidManifest.xml index 1759c1a896..dbfeac8e2f 100644 --- a/android-sdk-location/src/main/AndroidManifest.xml +++ b/android-sdk-location/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ + android:exported="true"> diff --git a/android-sdk-location/src/main/java/com/braze/location/GooglePlayLocationUtils.kt b/android-sdk-location/src/main/java/com/braze/location/GooglePlayLocationUtils.kt index a94b66ee7b..416ae83549 100644 --- a/android-sdk-location/src/main/java/com/braze/location/GooglePlayLocationUtils.kt +++ b/android-sdk-location/src/main/java/com/braze/location/GooglePlayLocationUtils.kt @@ -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 @@ -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, - geofenceRequestIntent: PendingIntent + desiredGeofencesToRegister: List, + geofenceRequestIntent: PendingIntent, + removalFunction: (List) -> Unit = { removeGeofencesRegisteredWithGeofencingClient(context, it) }, + registerFunction: (List) -> 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() + 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." } } @@ -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) { + @VisibleForTesting + internal fun removeGeofencesRegisteredWithGeofencingClient(context: Context, obsoleteGeofenceIds: List) { LocationServices.getGeofencingClient(context).removeGeofences(obsoleteGeofenceIds) .addOnSuccessListener { brazelog { "Geofences successfully un-registered with Google Play Services." } @@ -201,7 +222,8 @@ 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) /** @@ -209,7 +231,8 @@ object GooglePlayLocationUtils { * * @param newGeofencesToRegister List of [BrazeGeofence]s to store in SharedPreferences */ - private fun storeGeofencesToSharedPrefs(context: Context, newGeofencesToRegister: List) { + @VisibleForTesting + internal fun storeGeofencesToSharedPrefs(context: Context, newGeofencesToRegister: List) { val editor = getRegisteredGeofenceSharedPrefs(context).edit() for (brazeGeofence in newGeofencesToRegister) { editor.putString(brazeGeofence.id, brazeGeofence.forJsonPut().toString()) diff --git a/android-sdk-ui/src/main/java/com/braze/BrazeActivityLifecycleCallbackListener.kt b/android-sdk-ui/src/main/java/com/braze/BrazeActivityLifecycleCallbackListener.kt index 0934e04859..8111206eca 100644 --- a/android-sdk-ui/src/main/java/com/braze/BrazeActivityLifecycleCallbackListener.kt +++ b/android-sdk-ui/src/main/java/com/braze/BrazeActivityLifecycleCallbackListener.kt @@ -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) {} diff --git a/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/BrazeInAppMessageManager.kt b/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/BrazeInAppMessageManager.kt index e34e1b8533..d7182d2c4b 100644 --- a/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/BrazeInAppMessageManager.kt +++ b/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/BrazeInAppMessageManager.kt @@ -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 @@ -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() @@ -94,10 +96,16 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() { val inAppMessageEventMap = mutableMapOf() private var inAppMessageEventSubscriber: IEventSubscriber? = null private var sdkDataWipeEventSubscriber: IEventSubscriber? = null + private var brazeUserChangeEventSubscriber: IEventSubscriber? = 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] @@ -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 + ) + } } /** @@ -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) { @@ -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 ) @@ -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 @@ -635,6 +662,24 @@ open class BrazeInAppMessageManager : InAppMessageManagerBase() { } } + private fun createBrazeUserChangeEventSubscriber(context: Context): IEventSubscriber { + 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 @@ -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() diff --git a/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/DefaultInAppMessageViewWrapper.kt b/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/DefaultInAppMessageViewWrapper.kt index 40a215287d..b040071acd 100644 --- a/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/DefaultInAppMessageViewWrapper.kt +++ b/android-sdk-ui/src/main/java/com/braze/ui/inappmessage/DefaultInAppMessageViewWrapper.kt @@ -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 @@ -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 @@ -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() { diff --git a/build.gradle b/build.gradle index 63bbb36206..5580b178d7 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ ext { compileSdkVersion = 35 minSdkVersion = 21 targetSdkVersion = 35 - appVersionName = '32.0.0' + appVersionName = '32.1.0' } subprojects { @@ -33,5 +33,5 @@ subprojects { } group = 'com.braze' - version = '32.0.0' + version = '32.1.0' } diff --git a/droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.java b/droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.java deleted file mode 100644 index 98cf7272fc..0000000000 --- a/droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.appboy.sample.activity; - -import static com.appboy.sample.R.id.map; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Color; -import android.os.Bundle; - -import androidx.appcompat.app.AppCompatActivity; - -import com.appboy.sample.R; -import com.braze.models.BrazeGeofence; -import com.braze.support.BrazeLogger; -import com.braze.support.StringUtils; -import com.google.android.gms.maps.CameraUpdateFactory; -import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.OnMapReadyCallback; -import com.google.android.gms.maps.SupportMapFragment; -import com.google.android.gms.maps.model.CircleOptions; -import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.MarkerOptions; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class GeofencesMapActivity extends AppCompatActivity implements OnMapReadyCallback { - private static final String TAG = BrazeLogger.getBrazeLogTag(GeofencesMapActivity.class); - private static final String REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION = "com.appboy.support.geofences"; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.geofences_map); - SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() - .findFragmentById(map); - mapFragment.getMapAsync(this); - } - - @Override - public void onMapReady(GoogleMap googleMap) { - - // Note that this is for testing purposes only. This storage location and format are not a supported API. - SharedPreferences registeredGeofencePrefs = getApplicationContext() - .getSharedPreferences(REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION, Context.MODE_PRIVATE); - List registeredGeofences = retrieveBrazeGeofencesFromLocalStorage(registeredGeofencePrefs); - - int color = Color.BLUE; - if (registeredGeofences.size() > 0) { - for (BrazeGeofence registeredGeofence : registeredGeofences) { - googleMap.addCircle(new CircleOptions() - .center(new LatLng(registeredGeofence.getLatitude(), registeredGeofence.getLongitude())) - .radius(registeredGeofence.getRadiusMeters()) - .strokeColor(Color.RED) - .fillColor(Color.argb((int) Math.round(Color.alpha(color) * .20), Color.red(color), Color.green(color), Color.blue(color)))); - googleMap.addMarker(new MarkerOptions() - .position(new LatLng(registeredGeofence.getLatitude(), registeredGeofence.getLongitude())) - .title("Braze Geofence") - .snippet(registeredGeofence.getLatitude() + ", " + registeredGeofence.getLongitude() - + ", radius: " + registeredGeofence.getRadiusMeters() + "m")); - } - - BrazeGeofence firstGeofence = registeredGeofences.get(0); - googleMap.moveCamera(CameraUpdateFactory.newLatLng(new LatLng(firstGeofence.getLatitude(), firstGeofence.getLongitude()))); - googleMap.animateCamera(CameraUpdateFactory.zoomTo(10), null); - } - } - - // Note that this is for testing purposes only. This storage location and format are not a supported API. - private static List retrieveBrazeGeofencesFromLocalStorage(SharedPreferences sharedPreferences) { - List geofences = new ArrayList<>(); - Map storedGeofences = sharedPreferences.getAll(); - if (storedGeofences == null || storedGeofences.size() == 0) { - BrazeLogger.d(TAG, "Did not find stored geofences."); - return geofences; - } - Set keys = storedGeofences.keySet(); - for (String key : keys) { - String geofenceString = sharedPreferences.getString(key, null); - try { - if (StringUtils.isNullOrBlank(geofenceString)) { - BrazeLogger.w(TAG, String.format("Received null or blank serialized " - + " geofence string for geofence id %s from shared preferences. Not parsing.", key)); - continue; - } - JSONObject geofenceJson = new JSONObject(geofenceString); - BrazeGeofence brazeGeofence = new BrazeGeofence(geofenceJson); - geofences.add(brazeGeofence); - } catch (JSONException e) { - BrazeLogger.e(TAG, "Encountered Json exception while parsing stored geofence: " + geofenceString, e); - } catch (Exception e) { - BrazeLogger.e(TAG, "Encountered unexpected exception while parsing stored geofence: " + geofenceString, e); - } - } - return geofences; - } -} diff --git a/droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.kt b/droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.kt new file mode 100644 index 0000000000..16dd303de2 --- /dev/null +++ b/droidboy/src/main/java/com/appboy/sample/activity/GeofencesMapActivity.kt @@ -0,0 +1,99 @@ +package com.appboy.sample.activity + +import android.content.SharedPreferences +import android.graphics.Color +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.appboy.sample.R +import com.appboy.sample.R.id +import com.braze.models.BrazeGeofence +import com.braze.support.BrazeLogger.Priority.E +import com.braze.support.BrazeLogger.brazelog +import com.braze.support.BrazeLogger.getBrazeLogTag +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.CircleOptions +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MarkerOptions +import org.json.JSONException +import org.json.JSONObject + +class GeofencesMapActivity : AppCompatActivity(), OnMapReadyCallback { + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.geofences_map) + val mapFragment = supportFragmentManager + .findFragmentById(id.map) as SupportMapFragment? + mapFragment?.getMapAsync(this) + } + + override fun onMapReady(googleMap: GoogleMap) { + // Note that this is for testing purposes only. This storage location and format are not a supported API. + val registeredGeofencePrefs = applicationContext + .getSharedPreferences(REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION, MODE_PRIVATE) + val registeredGeofences = retrieveBrazeGeofencesFromLocalStorage(registeredGeofencePrefs) + val color = Color.BLUE + + val cameraPosition = CameraPosition.builder() + .zoom(12f) + + if (registeredGeofences.isNotEmpty()) { + for (registeredGeofence in registeredGeofences) { + googleMap.addCircle( + CircleOptions() + .center(LatLng(registeredGeofence.latitude, registeredGeofence.longitude)) + .radius(registeredGeofence.radiusMeters) + .strokeColor(Color.RED) + .fillColor(Color.argb(Math.round(Color.alpha(color) * .20).toInt(), Color.red(color), Color.green(color), Color.blue(color))) + ) + googleMap.addMarker( + MarkerOptions() + .position(LatLng(registeredGeofence.latitude, registeredGeofence.longitude)) + .title("Braze Geofence") + .snippet( + registeredGeofence.latitude.toString() + ", " + registeredGeofence.longitude + + ", radius: " + registeredGeofence.radiusMeters + "m" + ) + ) + } + val firstGeofence = registeredGeofences[0] + cameraPosition.target(LatLng(firstGeofence.latitude, firstGeofence.longitude)) + } else { + // NYC + cameraPosition.target(LatLng(40.730610, -73.935242)) + } + googleMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition.build())) + } + + companion object { + private val TAG = getBrazeLogTag(GeofencesMapActivity::class.java) + private const val REGISTERED_GEOFENCE_SHARED_PREFS_LOCATION = "com.appboy.support.geofences" + + // Note that this is for testing purposes only. This storage location and format are not a supported API. + private fun retrieveBrazeGeofencesFromLocalStorage(sharedPreferences: SharedPreferences): List { + val geofences: MutableList = ArrayList() + val storedGeofences = sharedPreferences.all + if (storedGeofences.isNullOrEmpty()) { + brazelog(TAG) { "Did not find stored geofences." } + return geofences + } + val keys: Set = storedGeofences.keys + for (key in keys) { + val geofenceString = sharedPreferences.getString(key, null) ?: continue + try { + val geofenceJson = JSONObject(geofenceString) + val brazeGeofence = BrazeGeofence(geofenceJson) + geofences.add(brazeGeofence) + } catch (e: JSONException) { + brazelog(TAG, E, e) { "Encountered Json exception while parsing stored geofence: $geofenceString" } + } catch (e: Exception) { + brazelog(TAG, E, e) { "Encountered unexpected exception while parsing stored geofence: $geofenceString" } + } + } + return geofences + } + } +} diff --git a/droidboy/src/main/java/com/appboy/sample/activity/InAppMessageSandboxActivity.kt b/droidboy/src/main/java/com/appboy/sample/activity/InAppMessageSandboxActivity.kt index bd5930b79d..701331a2d6 100644 --- a/droidboy/src/main/java/com/appboy/sample/activity/InAppMessageSandboxActivity.kt +++ b/droidboy/src/main/java/com/appboy/sample/activity/InAppMessageSandboxActivity.kt @@ -9,6 +9,7 @@ import com.appboy.sample.R import com.braze.enums.inappmessage.DismissType import com.braze.models.inappmessage.InAppMessageHtml import com.braze.models.inappmessage.InAppMessageModal +import com.braze.models.inappmessage.InAppMessageSlideup import com.braze.models.inappmessage.MessageButton import com.braze.ui.inappmessage.BrazeInAppMessageManager import java.util.Random @@ -26,7 +27,7 @@ class InAppMessageSandboxActivity : AppCompatActivity() { findViewById(R.id.bSandboxDisplayMessage1).setOnClickListener { this.displayMessage(1) } findViewById(R.id.bSandboxDisplayMessage0).setOnClickListener { this.displayMessage(0) } findViewById(R.id.bSandboxDummyButton).setOnClickListener { Toast.makeText(this, "dummy button pressed!", Toast.LENGTH_SHORT).show() } - + findViewById(R.id.bSandboxDisplaySlideup).setOnClickListener { displaySlideup() } findViewById(R.id.bSandboxHtmlInApp).setOnClickListener { displayHtmlMessage() } } @@ -60,6 +61,15 @@ class InAppMessageSandboxActivity : AppCompatActivity() { BrazeInAppMessageManager.getInstance().requestDisplayInAppMessage() } + private fun displaySlideup() { + val slideup = InAppMessageSlideup() + slideup.message = "Welcome to Braze! This is a slideup in-app message." + slideup.icon = "\uf091" + slideup.dismissType = DismissType.MANUAL + BrazeInAppMessageManager.getInstance().addInAppMessage(slideup) + BrazeInAppMessageManager.getInstance().requestDisplayInAppMessage() + } + private fun displayHtmlMessage() { val htmlString = this.assets.open(THE_WAY_HTML).bufferedReader().use { it.readText() diff --git a/droidboy/src/main/res/layout/activity_in_app_message_sandbox.xml b/droidboy/src/main/res/layout/activity_in_app_message_sandbox.xml index b5af57260a..424cfd50e4 100644 --- a/droidboy/src/main/res/layout/activity_in_app_message_sandbox.xml +++ b/droidboy/src/main/res/layout/activity_in_app_message_sandbox.xml @@ -51,4 +51,13 @@ android:text="HTML IAM" app:layout_constraintBottom_toTopOf="@+id/bSandboxDummyButton" app:layout_constraintStart_toStartOf="parent" /> +