diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/Optimove.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/Optimove.java index 8216980b..88a057a4 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/Optimove.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/Optimove.java @@ -11,6 +11,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.optimove.android.auth.AuthManager; import com.optimove.android.embeddedmessaging.OptimoveEmbeddedMessaging; import com.optimove.android.main.common.EventHandlerFactory; import com.optimove.android.main.common.EventHandlerProvider; @@ -72,6 +73,7 @@ final public class Optimove { private final LifecycleObserver lifecycleObserver; private static OptimoveConfig currentConfig; + private static @Nullable AuthManager authManager; public enum IBeaconProximity { UNKNOWN, @@ -110,6 +112,7 @@ private Optimove(@NonNull Context context, OptimoveConfig config) { .optistreamDbHelper(new OptistreamDbHelper(context)) .lifecycleObserver(lifecycleObserver) .context(context) + .authManager(authManager) .build()); this.optimoveLifecycleEventGenerator = new OptimoveLifecycleEventGenerator(eventHandlerProvider, userInfo, @@ -141,10 +144,17 @@ public static Optimove getInstance() { public static void initialize(@NonNull Application application, @NonNull OptimoveConfig config) { currentConfig = config; + if (config.getAuthTokenProvider() != null) { + authManager = new AuthManager(config.getAuthTokenProvider()); + } else { + authManager = null; + } + performSingletonInitialization(application.getApplicationContext(), config); if (config.isOptimobileConfigured()) { Optimobile.initialize(application, config, shared.userInfo.getInitialVisitorId(), shared.userInfo.getUserId()); + Optimobile.setAuthManager(authManager); } if (config.isOptimoveConfigured()) { @@ -285,6 +295,10 @@ public static OptimoveConfig getConfig() { return currentConfig; } + public static @Nullable AuthManager getAuthManager() { + return authManager; + } + /** * Enables remote logs for investigations. Don't call it unless we explicitly asked you to. */ diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java index daf9d5e8..523db1f7 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.optimove.android.auth.AuthTokenProvider; import com.optimove.android.embeddedmessaging.EmbeddedMessagingConfig; import com.optimove.android.main.tools.opti_logger.LogLevel; import com.optimove.android.optimobile.DeferredDeepLinkHandlerInterface; @@ -73,6 +74,8 @@ public final class OptimoveConfig { private @Nullable EmbeddedMessagingConfig embeddedMessagingConfig; + private @Nullable AuthTokenProvider authTokenProvider; + public enum InAppConsentStrategy { AUTO_ENROLL, EXPLICIT_BY_USER @@ -415,6 +418,14 @@ public boolean usesDelayedConfiguration() { return this.embeddedMessagingConfig; } + public @Nullable AuthTokenProvider getAuthTokenProvider() { + return authTokenProvider; + } + + private void setAuthTokenProvider(@Nullable AuthTokenProvider provider) { + this.authTokenProvider = provider; + } + private boolean hasFinishedInitialisation() { boolean hasOptimoveCreds = optimoveToken != null && configFileName != null; boolean hasOptimobileCreds = apiKey != null && secretKey != null; @@ -473,6 +484,7 @@ public static class Builder { private DeferredDeepLinkHandlerInterface deferredDeepLinkHandler; private @Nullable LogLevel minLogLevel; + private @Nullable AuthTokenProvider authTokenProvider; /** * @deprecated Use {@link Builder#Builder(FeatureSet)} instead @@ -575,6 +587,11 @@ public Builder enableEmbeddedMessaging(@NonNull String embeddedMessagingConfigur return this; } + public Builder enableAuth(@NonNull AuthTokenProvider provider) { + this.authTokenProvider = provider; + return this; + } + /** * The minimum amount of time the user has to have left the app for a session end event to be * recorded. @@ -664,6 +681,7 @@ public OptimoveConfig build() { newConfig.setDeferredDeepLinkHandler(this.deferredDeepLinkHandler); newConfig.setMinLogLevel(this.minLogLevel); + newConfig.setAuthTokenProvider(this.authTokenProvider); return newConfig; } diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/embeddedmessaging/OptimoveEmbeddedMessaging.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/embeddedmessaging/OptimoveEmbeddedMessaging.java index a32401dd..ca10bd72 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/embeddedmessaging/OptimoveEmbeddedMessaging.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/embeddedmessaging/OptimoveEmbeddedMessaging.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import com.optimove.android.Optimove; +import com.optimove.android.auth.AuthManager; import com.optimove.android.main.common.UserInfo; import com.optimove.android.main.tools.networking.HttpClient; @@ -17,6 +18,7 @@ import java.io.IOException; import java.net.URLEncoder; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -129,7 +131,7 @@ public void deleteMessageAsync(EmbeddedMessage message, @NonNull EmbeddedMessage new Date(), UUID.randomUUID().toString(), EventType.DELETED, context, userId, userInfo.getVisitorId()); - Runnable task = new EventReportEmbeddedMessagesRunnable(config, request, embeddedMessagesDeleteHandler); + Runnable task = new EventReportEmbeddedMessagesRunnable(config, userId, request, embeddedMessagesDeleteHandler); executorService.submit(task); } @@ -158,7 +160,7 @@ public void reportClickMetricAsync( EmbeddedMessageEventRequest request = new EmbeddedMessageEventRequest( new Date(), UUID.randomUUID().toString(), EventType.CLICKED, context, userId, userInfo.getVisitorId()); - Runnable task = new EventReportEmbeddedMessagesRunnable(config, request, embeddedMessagesMetricsHandler); + Runnable task = new EventReportEmbeddedMessagesRunnable(config, userId, request, embeddedMessagesMetricsHandler); executorService.submit(task); } @@ -188,7 +190,7 @@ public void setAsReadASync(EmbeddedMessage message, boolean isRead, new Date(), UUID.randomUUID().toString(), isRead ? EventType.READ : EventType.UNREAD, context, userId, userInfo.getVisitorId()); Runnable task = new EventReportEmbeddedMessagesRunnable( - config, request, embeddedMessagesStatusHandler); + config, userId, request, embeddedMessagesStatusHandler); executorService.submit(task); } @@ -210,7 +212,7 @@ class GetEmbeddedMessagesRunnable extends EmbeddedMessagesRunnableBase implement GetEmbeddedMessagesRunnable( EmbeddedMessagingConfig config, String customerId, EmbeddedMessagesGetHandler callback, ContainerRequestOptions[] requestBody) { - super(config); + super(config, customerId); this.customerId = customerId; this.callback = callback; this.requestBody = requestBody; @@ -250,9 +252,10 @@ class EventReportEmbeddedMessagesRunnable extends EmbeddedMessagesRunnableBase i EventReportEmbeddedMessagesRunnable( EmbeddedMessagingConfig config, + String customerId, EmbeddedMessageEventRequest request, EmbeddedMessagesSetHandler callback) { - super(config); + super(config, customerId); this.callback = callback; this.request = request; } @@ -281,15 +284,22 @@ private void fireCallback(ResultType result) { class EmbeddedMessagesRunnableBase { protected EmbeddedMessagingConfig config; + protected String customerId; - public EmbeddedMessagesRunnableBase(EmbeddedMessagingConfig config) { + public EmbeddedMessagesRunnableBase(EmbeddedMessagingConfig config, String customerId) { this.config = config; + this.customerId = customerId; } public EmbeddedMessagingResult postSync(String url, JSONArray postData, boolean expectResponse) { + Map extraHeaders = resolveAuthHeaders(); + if (extraHeaders == null) { + return new EmbeddedMessagingResult(ResultType.ERROR, null); + } + HttpClient httpClient = HttpClient.getInstance(); - try (Response response = httpClient.postSync(url, postData, config.getTenantId())) { + try (Response response = httpClient.postSync(url, postData, config.getTenantId(), extraHeaders)) { return handleResponse(response, expectResponse); } catch (Exception e) { Log.e(TAG, e.getMessage()); @@ -297,6 +307,21 @@ public EmbeddedMessagingResult postSync(String url, JSONArray postData, boolean return new EmbeddedMessagingResult(ResultType.ERROR, null); } } + + @Nullable + private Map resolveAuthHeaders() { + AuthManager authManager = Optimove.getAuthManager(); + if (authManager == null || customerId == null || customerId.isEmpty()) { + return Collections.emptyMap(); + } + + String jwt = AuthManager.getTokenBlocking(authManager, customerId); + if (jwt == null) { + Log.e(TAG, "Auth token fetch failed. Dropping request."); + return null; + } + return Collections.singletonMap("X-User-JWT", jwt); + } protected String getBaseUrl(String endpoint) { String region = config.getRegion(); diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/common/EventHandlerFactory.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/common/EventHandlerFactory.java index b80d3191..0845482c 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/common/EventHandlerFactory.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/common/EventHandlerFactory.java @@ -2,6 +2,9 @@ import android.content.Context; +import androidx.annotation.Nullable; + +import com.optimove.android.auth.AuthManager; import com.optimove.android.main.event_handlers.DestinationDecider; import com.optimove.android.main.event_handlers.EventDecorator; import com.optimove.android.main.event_handlers.EventMemoryBuffer; @@ -28,17 +31,19 @@ public class EventHandlerFactory { private OptistreamDbHelper optistreamDbHelper; private LifecycleObserver lifecycleObserver; private Context context; + private @Nullable AuthManager authManager; private EventHandlerFactory(HttpClient httpClient, UserInfo userInfo, int maximumBufferSize, OptistreamDbHelper optistreamDbHelper, - LifecycleObserver lifecycleObserver, Context context) { + LifecycleObserver lifecycleObserver, Context context, + @Nullable AuthManager authManager) { this.httpClient = httpClient; this.userInfo = userInfo; this.maximumBufferSize = maximumBufferSize; this.optistreamDbHelper = optistreamDbHelper; this.lifecycleObserver = lifecycleObserver; this.context = context; - + this.authManager = authManager; } public EventMemoryBuffer getEventBuffer() { @@ -58,11 +63,11 @@ public EventDecorator getEventDecorator(Map eventConfigs, } public RealtimeManager getRealtimeMananger(RealtimeConfigs realtimeConfigs) { - return new RealtimeManager(httpClient, realtimeConfigs, context); + return new RealtimeManager(httpClient, realtimeConfigs, context, authManager); } public OptistreamHandler getOptistreamHandler(OptitrackConfigs optitrackConfigs) { - return new OptistreamHandler(httpClient, lifecycleObserver, optistreamDbHelper, optitrackConfigs); + return new OptistreamHandler(httpClient, lifecycleObserver, optistreamDbHelper, optitrackConfigs, authManager); } public DestinationDecider getDestinationDecider(Map eventConfigs, @@ -109,6 +114,7 @@ public interface ContextStep { } public interface Build { + Build authManager(@Nullable AuthManager authManager); EventHandlerFactory build(); } @@ -121,6 +127,7 @@ public static class Builder implements OptistreamDbHelperStep, MaximumBufferSize private OptistreamDbHelper optistreamDbHelper; private LifecycleObserver lifecycleObserver; private Context context; + private @Nullable AuthManager authManager; @Override public HttpClientStep userInfo(UserInfo userInfo) { @@ -158,10 +165,16 @@ public Build context(Context context) { return this; } + @Override + public Build authManager(@Nullable AuthManager authManager) { + this.authManager = authManager; + return this; + } + @Override public EventHandlerFactory build() { return new EventHandlerFactory(httpClient, userInfo, maximumBufferSize, optistreamDbHelper, - lifecycleObserver, context); + lifecycleObserver, context, authManager); } } diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/tools/networking/HttpClient.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/tools/networking/HttpClient.java index 9a0b837b..d40378a4 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/tools/networking/HttpClient.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/main/tools/networking/HttpClient.java @@ -12,6 +12,8 @@ import org.json.JSONObject; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import okhttp3.Call; import okhttp3.Callback; @@ -92,12 +94,18 @@ public RequestBuilder errorListener(ErrorListener errorListener) { public class JsonRequestBuilder extends RequestBuilder { protected String json; + private final Map extraHeaders = new HashMap<>(); protected JsonRequestBuilder(String baseUrl, String json) { super(baseUrl); this.json = json; } + public JsonRequestBuilder addHeader(String name, String value) { + extraHeaders.put(name, value); + return this; + } + @Override public void send() { if (url == null) { @@ -107,7 +115,14 @@ public void send() { RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json); - Request request = new Request.Builder().url(url).post(body).build(); + Request.Builder reqBuilder = new Request.Builder().url(url).post(body) + .addHeader("X-Optimove-Auth-Capable", "1"); + + for (Map.Entry entry : extraHeaders.entrySet()) { + reqBuilder.addHeader(entry.getKey(), entry.getValue()); + } + + Request request = reqBuilder.build(); okHttpClient.newCall(request).enqueue(new Callback() { @Override @@ -191,70 +206,84 @@ public interface ErrorListener { } public Response getSync(String url, int tenantId) throws IOException { - Request.Builder builder = new Request.Builder().get(); - - Request request = this.buildRequest(builder, url, tenantId); + return getSync(url, tenantId, null); + } + public Response getSync(String url, int tenantId, @Nullable Map extraHeaders) throws IOException { + Request.Builder builder = new Request.Builder().get(); + Request request = this.buildRequest(builder, url, tenantId, extraHeaders); return this.doSyncRequest(request); } public Response putSync(String url, JSONArray data, int tenantId) throws IOException { - String dataStr = data.toString(); + return putSync(url, data, tenantId, null); + } + public Response putSync(String url, JSONArray data, int tenantId, @Nullable Map extraHeaders) throws IOException { + String dataStr = data.toString(); RequestBody body = RequestBody.create(dataStr, MediaType.parse("application/json; charset=utf-8")); - Request.Builder builder = new Request.Builder().put(body); - Request request = this.buildRequest(builder, url, tenantId); - + Request request = this.buildRequest(builder, url, tenantId, extraHeaders); return this.doSyncRequest(request); } public Response putSingleSync(String url, JSONObject data, int tenantId) throws IOException { - String dataStr = data.toString(); + return putSingleSync(url, data, tenantId, null); + } + public Response putSingleSync(String url, JSONObject data, int tenantId, @Nullable Map extraHeaders) throws IOException { + String dataStr = data.toString(); RequestBody body = RequestBody.create(dataStr, MediaType.parse("application/json; charset=utf-8")); - Request.Builder builder = new Request.Builder().put(body); - Request request = this.buildRequest(builder, url, tenantId); - + Request request = this.buildRequest(builder, url, tenantId, extraHeaders); return this.doSyncRequest(request); } public Response postSync(String url, JSONArray data, int tenantId) throws IOException { - String dataStr = data.toString(); + return postSync(url, data, tenantId, null); + } + public Response postSync(String url, JSONArray data, int tenantId, @Nullable Map extraHeaders) throws IOException { + String dataStr = data.toString(); RequestBody body = RequestBody.create(dataStr, MediaType.parse("application/json; charset=utf-8")); - Request.Builder builder = new Request.Builder().post(body); - Request request = this.buildRequest(builder, url, tenantId); - + Request request = this.buildRequest(builder, url, tenantId, extraHeaders); return this.doSyncRequest(request); } public Response postSingleSync(String url, JSONObject data, int tenantId) throws IOException { - String dataStr = data.toString(); + return postSingleSync(url, data, tenantId, null); + } + public Response postSingleSync(String url, JSONObject data, int tenantId, @Nullable Map extraHeaders) throws IOException { + String dataStr = data.toString(); RequestBody body = RequestBody.create(dataStr, MediaType.parse("application/json; charset=utf-8")); - Request.Builder builder = new Request.Builder().post(body); - Request request = this.buildRequest(builder, url, tenantId); - + Request request = this.buildRequest(builder, url, tenantId, extraHeaders); return this.doSyncRequest(request); } public Response deleteSync(String url, int tenantId) throws IOException { Request.Builder builder = new Request.Builder().delete(); - Request request = this.buildRequest(builder, url, tenantId); - + Request request = this.buildRequest(builder, url, tenantId, null); return this.doSyncRequest(request); } - private Request buildRequest(Request.Builder builder, String url, int tenantId) { - return builder.url(url) + private Request buildRequest(Request.Builder builder, String url, int tenantId, + @Nullable Map extraHeaders) { + builder.url(url) .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") .addHeader("X-Tenant-Id", String.valueOf(tenantId)) - .build(); + .addHeader("X-Optimove-Auth-Capable", "1"); + + if (extraHeaders != null) { + for (Map.Entry entry : extraHeaders.entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } + } + + return builder.build(); } private Response doSyncRequest(Request request) throws IOException { diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsUploadHelper.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsUploadHelper.java index a7a27fdd..8d782fd4 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsUploadHelper.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsUploadHelper.java @@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Pair; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.json.JSONArray; @@ -59,23 +60,32 @@ Result flushEvents(Context context) { } private boolean flushBatchToNetwork(Context context, ArrayList events, long maxEventId) throws Optimobile.PartialInitialisationException { - // Pack into JSON JSONArray data = new JSONArray(events); final OptimobileHttpClient httpClient = OptimobileHttpClient.getInstance(); final String url = Optimobile.urlForService(UrlBuilder.Service.EVENTS, "/v1/app-installs/" + Optimobile.getInstallId() + "/events"); + String batchUserId = extractUserIdFromBatch(events); + boolean isVisitorBatch = batchUserId != null && batchUserId.equals(Optimobile.getInstallId()); + String authUserId = isVisitorBatch ? null : batchUserId; + boolean result = false; - try (Response response = httpClient.postSync(url, data)) { + try (Response response = httpClient.postSync(url, data, authUserId)) { if (response.isSuccessful()) { result = true; + } else if (response.code() == 401) { + deletePersistedEvents(context, maxEventId); + return true; } } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().startsWith("Auth token fetch failed")) { + deletePersistedEvents(context, maxEventId); + return true; + } e.printStackTrace(); } - // Clean up batch from DB if (result) { deletePersistedEvents(context, maxEventId); } @@ -83,6 +93,11 @@ private boolean flushBatchToNetwork(Context context, ArrayList event return result; } + private static @Nullable String extractUserIdFromBatch(ArrayList events) { + if (events.isEmpty()) return null; + return events.get(0).optString("userId", null); + } + private void deletePersistedEvents(Context context, long maxEventId){ try (SQLiteOpenHelper dbHelper = new AnalyticsDbHelper(context)) { SQLiteDatabase db = dbHelper.getWritableDatabase(); @@ -100,6 +115,43 @@ private void deletePersistedEvents(Context context, long maxEventId){ } private Pair, Long> getBatchOfEvents(SQLiteDatabase db, long minEventId) { + String sortBy = AnalyticsContract.AnalyticsEvent.COL_ID + " ASC"; + + // Peek at the oldest event to determine which user_identifier to batch + String peekSelection = AnalyticsContract.AnalyticsEvent.COL_ID + " > ?"; + String[] peekParams = new String[]{String.valueOf(minEventId)}; + + Cursor peekCursor = db.query( + AnalyticsContract.AnalyticsEvent.TABLE_NAME, + new String[]{AnalyticsContract.AnalyticsEvent.COL_USER_IDENTIFIER}, + peekSelection, peekParams, null, null, sortBy, "1"); + + String batchUserIdentifier = null; + boolean hasPeek = false; + if (peekCursor.moveToFirst()) { + hasPeek = true; + int idx = peekCursor.getColumnIndex(AnalyticsContract.AnalyticsEvent.COL_USER_IDENTIFIER); + batchUserIdentifier = peekCursor.isNull(idx) ? null : peekCursor.getString(idx); + } + peekCursor.close(); + + if (!hasPeek) { + return new Pair<>(new ArrayList<>(), -1L); + } + + // Fetch up to 100 events with the same user_identifier + String selection; + String[] params; + if (batchUserIdentifier != null) { + selection = AnalyticsContract.AnalyticsEvent.COL_ID + " > ? AND " + + AnalyticsContract.AnalyticsEvent.COL_USER_IDENTIFIER + " = ?"; + params = new String[]{String.valueOf(minEventId), batchUserIdentifier}; + } else { + selection = AnalyticsContract.AnalyticsEvent.COL_ID + " > ? AND " + + AnalyticsContract.AnalyticsEvent.COL_USER_IDENTIFIER + " IS NULL"; + params = new String[]{String.valueOf(minEventId)}; + } + String[] projection = { AnalyticsContract.AnalyticsEvent.COL_ID, AnalyticsContract.AnalyticsEvent.COL_HAPPENED_AT_MILLIS, @@ -109,11 +161,6 @@ private Pair, Long> getBatchOfEvents(SQLiteDatabase db, lo AnalyticsContract.AnalyticsEvent.COL_USER_IDENTIFIER }; - String sortBy = AnalyticsContract.AnalyticsEvent.COL_ID + " ASC"; - - String selection = AnalyticsContract.AnalyticsEvent.COL_ID + " > ?"; - String[] params = new String[]{String.valueOf(minEventId)}; - Cursor cursor = db.query( AnalyticsContract.AnalyticsEvent.TABLE_NAME, projection, diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppRequestService.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppRequestService.java index 8d2b43f2..fa5f74e5 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppRequestService.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppRequestService.java @@ -33,6 +33,9 @@ static List readInAppMessages(Context c, Date lastSyncTime) { } String encodedIdentifier = Uri.encode(userIdentifier); + boolean isVisitor = userIdentifier != null && userIdentifier.equals(Optimobile.getInstallId()); + String authUserId = isVisitor ? null : userIdentifier; + List messages = null; try { String url = Optimobile.urlForService(UrlBuilder.Service.PUSH, "/v1/users/" + encodedIdentifier + "/messages" + params); diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java index 74a937a5..2fe65489 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java @@ -26,6 +26,7 @@ import com.optimove.android.BuildConfig; import com.optimove.android.Optimove; import com.optimove.android.OptimoveConfig; +import com.optimove.android.auth.AuthManager; import android.location.Location; @@ -142,6 +143,10 @@ private static void maybeMigrateUserAssociation(Application application, @Nullab } + public static void setAuthManager(@Nullable AuthManager authManager) { + OptimobileHttpClient.getInstance().setAuthManager(authManager); + } + //============================================================================================== //-- Getters/setters diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimobileHttpClient.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimobileHttpClient.java index 1e4ca006..832d402c 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimobileHttpClient.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimobileHttpClient.java @@ -7,6 +7,7 @@ import com.optimove.android.Optimove; import com.optimove.android.OptimoveConfig; +import com.optimove.android.auth.AuthManager; import org.json.JSONArray; @@ -28,6 +29,7 @@ class OptimobileHttpClient { private final OkHttpClient okHttpClient; private @Nullable String authHeader; + private @Nullable AuthManager authManager; static OptimobileHttpClient getInstance() { if (instance != null) { @@ -45,13 +47,15 @@ private OptimobileHttpClient() { okHttpClient = this.buildOkHttpClient(); } + void setAuthManager(@Nullable AuthManager authManager) { + this.authManager = authManager; + } + private OkHttpClient buildOkHttpClient() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return new OkHttpClient(); } - //ciphers available on Android 4.4 have intersections with the approved ones in MODERN_TLS, but the intersections are on bad cipher list, so, - //perhaps not supported by CloudFlare. On older devices allow all ciphers ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .allEnabledCipherSuites() .build(); @@ -62,31 +66,49 @@ private OkHttpClient buildOkHttpClient() { } Response postSync(String url, JSONArray data) throws IOException, Optimobile.PartialInitialisationException { - String dataStr = data.toString(); + return postSync(url, data, (String) null); + } + Response postSync(String url, JSONArray data, @Nullable String authUserId) throws IOException, Optimobile.PartialInitialisationException { + String dataStr = data.toString(); RequestBody body = RequestBody.create(dataStr, MediaType.parse("application/json; charset=utf-8")); - Request.Builder builder = new Request.Builder().post(body); - Request request = this.buildRequest(builder, url); - - return this.doSyncRequest(request); + return this.buildAndSend(builder, url, authUserId); } Response getSync(String url) throws IOException, Optimobile.PartialInitialisationException { - Request.Builder builder = new Request.Builder().get(); - - Request request = this.buildRequest(builder, url); + return getSync(url, (String) null); + } - return this.doSyncRequest(request); + Response getSync(String url, @Nullable String authUserId) throws IOException, Optimobile.PartialInitialisationException { + Request.Builder builder = new Request.Builder().get(); + return this.buildAndSend(builder, url, authUserId); } void getAsync(String url, Callback callback) throws Optimobile.PartialInitialisationException { Request.Builder builder = new Request.Builder().get(); Request request = this.buildRequest(builder, url); - this.doAsyncRequest(request, callback); } + private Response buildAndSend(Request.Builder builder, String url, + @Nullable String authUserId) + throws IOException, Optimobile.PartialInitialisationException { + Request request = this.buildRequest(builder, url); + + if (authManager != null && authUserId != null && !authUserId.isEmpty()) { + String jwt = AuthManager.getTokenBlocking(authManager, authUserId); + if (jwt == null) { + throw new IOException("Auth token fetch failed for userId: " + authUserId); + } + request = request.newBuilder() + .addHeader("X-User-JWT", jwt) + .build(); + } + + return this.doSyncRequest(request); + } + private Request buildRequest(Request.Builder builder, String url) throws Optimobile.PartialInitialisationException { if (this.authHeader == null) { OptimoveConfig config = Optimove.getConfig(); @@ -104,6 +126,7 @@ private Request buildRequest(Request.Builder builder, String url) throws Optimob .addHeader(Optimobile.KEY_AUTH_HEADER, this.authHeader) .addHeader("Accept", "application/json") .addHeader("Content-Type", "application/json") + .addHeader("X-Optimove-Auth-Capable", "1") .build(); } diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optistream/OptistreamHandler.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optistream/OptistreamHandler.java index 4dac8987..a541375b 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optistream/OptistreamHandler.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optistream/OptistreamHandler.java @@ -4,14 +4,15 @@ import androidx.annotation.Nullable; import com.google.gson.Gson; +import com.optimove.android.auth.AuthManager; import com.optimove.android.main.common.LifecycleObserver; import com.optimove.android.main.sdk_configs.configs.OptitrackConfigs; import com.optimove.android.main.tools.networking.HttpClient; import com.optimove.android.main.tools.opti_logger.OptiLoggerStreamsContainer; -import org.json.JSONArray; import org.json.JSONObject; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -21,41 +22,39 @@ public class OptistreamHandler implements LifecycleObserver.ActivityStopped { @NonNull - private HttpClient httpClient; + private final LifecycleObserver lifecycleObserver; @NonNull - private LifecycleObserver lifecycleObserver; + private final OptistreamPersistanceAdapter optistreamPersistanceAdapter; @NonNull - private OptistreamPersistanceAdapter optistreamPersistanceAdapter; + private final OptitrackConfigs optitrackConfigs; @NonNull - private OptitrackConfigs optitrackConfigs; + private final ScheduledExecutorService singleThreadScheduledExecutor; @NonNull - private ScheduledExecutorService singleThreadScheduledExecutor; + private final Gson optistreamGson; @NonNull - private Gson optistreamGson; - + private final OptistreamDispatcher dispatcher; @Nullable private ScheduledFuture timerDispatchFuture; - //accessed ONLY by the single thread of the executor private boolean dispatchRequestWaitsForResponse = false; private boolean initialized = false; public final static class Constants { public static final int EVENT_BATCH_LIMIT = 100; public static final int DISPATCH_INTERVAL_IN_SECONDS = 30; - } public OptistreamHandler(@NonNull HttpClient httpClient, @NonNull LifecycleObserver lifecycleObserver, @NonNull OptistreamPersistanceAdapter optistreamPersistanceAdapter, - @NonNull OptitrackConfigs optitrackConfigs) { - this.httpClient = httpClient; + @NonNull OptitrackConfigs optitrackConfigs, + @Nullable AuthManager authManager) { this.lifecycleObserver = lifecycleObserver; this.optistreamPersistanceAdapter = optistreamPersistanceAdapter; this.optitrackConfigs = optitrackConfigs; this.singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor(); this.optistreamGson = new Gson(); + this.dispatcher = new OptistreamDispatcher(httpClient, authManager); } private synchronized void ensureInit() { @@ -92,7 +91,7 @@ public void reportEvents(List optistreamEvents) { private void dispatchBulkIfExists(){ if (dispatchRequestWaitsForResponse) { - return; //protects from sending same events twice + return; } OptistreamDbHelper.EventsBulk eventsBulk = optistreamPersistanceAdapter.getFirstEvents(Constants.EVENT_BATCH_LIMIT); if (eventsBulk == null) { @@ -102,20 +101,23 @@ private void dispatchBulkIfExists(){ List eventJsons = eventsBulk.getEventJsons(); if (eventJsons != null && !eventJsons.isEmpty()) { try { - JSONArray jsonArrayToDispatch = new JSONArray(); - for (String eventJson: eventJsons) { - jsonArrayToDispatch.put(new JSONObject(eventJson)); + List parsedEvents = new ArrayList<>(); + for (String eventJson : eventJsons) { + parsedEvents.add(new JSONObject(eventJson)); } dispatchRequestWaitsForResponse = true; - httpClient.postJson(optitrackConfigs.getOptitrackEndpoint(), jsonArrayToDispatch.toString()) - .errorListener(error -> { - OptiLoggerStreamsContainer.error("Events dispatching failed - %s", - error.getMessage()); - // some error occurred, try again in the next dispatch - dispatchRequestWaitsForResponse = false; - scheduleTheNextDispatch(); - }) - .successListener(response -> { + + dispatcher.sendBatch( + parsedEvents, + optitrackConfigs.getOptitrackEndpoint(), + null, + (groupEvents, success, error) -> { + if (!success) { + OptiLoggerStreamsContainer.error("Events dispatch group failed - %s", + error != null ? error.getMessage() : "unknown"); + } + }, + () -> { try { singleThreadScheduledExecutor.submit(() -> { optistreamPersistanceAdapter.removeEvents(eventsBulk.getLastId()); @@ -123,17 +125,17 @@ private void dispatchBulkIfExists(){ dispatchBulkIfExists(); }); } catch (Throwable throwable) { + dispatchRequestWaitsForResponse = false; OptiLoggerStreamsContainer.error("Error while submitting a command - %s", throwable.getMessage()); } - }) - .send(); + } + ); } catch (Throwable e) { dispatchRequestWaitsForResponse = false; OptiLoggerStreamsContainer.error("Events dispatching failed - %s", e.getMessage()); } } else { - // Schedule another dispatch all if we are done dispatching dispatchRequestWaitsForResponse = false; scheduleTheNextDispatch(); } diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/preferencecenter/OptimovePreferenceCenter.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/preferencecenter/OptimovePreferenceCenter.java index fae27f5e..3c0afa75 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/preferencecenter/OptimovePreferenceCenter.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/preferencecenter/OptimovePreferenceCenter.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import com.optimove.android.Optimove; +import com.optimove.android.auth.AuthManager; import com.optimove.android.main.common.UserInfo; import com.optimove.android.main.tools.networking.HttpClient; @@ -22,6 +23,8 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; +import java.util.Collections; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -132,6 +135,12 @@ static class GetPreferencesRunnable implements Runnable { @Override public void run() { + Map extraHeaders = resolveAuthHeaders(this.customerId); + if (extraHeaders == null) { + this.fireCallback(ResultType.ERROR, null); + return; + } + Preferences preferences = null; ResultType resultType = ResultType.ERROR; @@ -142,7 +151,7 @@ public void run() { String encodedCustomerId = URLEncoder.encode(this.customerId, "UTF-8"); String url = "https://preference-center-" + region + ".optimove.net/api/v1/preferences?customerId=" + encodedCustomerId + "&brandGroupId=" + config.getBrandGroupId(); - try (Response response = httpClient.getSync(url, config.getTenantId())) { + try (Response response = httpClient.getSync(url, config.getTenantId(), extraHeaders)) { if (!response.isSuccessful()) { logFailedResponse(response); } else { @@ -178,6 +187,12 @@ static class SetPreferencesRunnable implements Runnable { @Override public void run() { + Map extraHeaders = resolveAuthHeaders(this.customerId); + if (extraHeaders == null) { + this.fireCallback(ResultType.ERROR); + return; + } + ResultType result = ResultType.ERROR; String region = config.getRegion(); @@ -188,7 +203,7 @@ public void run() { String url = "https://preference-center-" + region + ".optimove.net/api/v1/preferences?customerId=" + encodedCustomerId + "&brandGroupId=" + config.getBrandGroupId(); JSONArray data = mapPreferenceUpdatesToArray(updates); - try (Response response = httpClient.putSync(url, data, config.getTenantId())) { + try (Response response = httpClient.putSync(url, data, config.getTenantId(), extraHeaders)) { if (!response.isSuccessful()) { logFailedResponse(response); } else { @@ -208,6 +223,25 @@ private void fireCallback(ResultType result) { } } + /** + * Resolves JWT auth headers for a sync request. Returns empty map if no auth configured, + * a map with X-User-JWT if token was resolved, or null if token fetch failed (fail-closed). + */ + @Nullable + private static Map resolveAuthHeaders(@NonNull String customerId) { + AuthManager authManager = Optimove.getAuthManager(); + if (authManager == null) { + return Collections.emptyMap(); + } + + String jwt = AuthManager.getTokenBlocking(authManager, customerId); + if (jwt == null) { + Log.e(TAG, "Auth token fetch failed. Dropping request."); + return null; + } + return Collections.singletonMap("X-User-JWT", jwt); + } + private static JSONArray mapPreferenceUpdatesToArray(List updates) throws JSONException { JSONArray updatesArray = new JSONArray(); diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/realtime/RealtimeManager.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/realtime/RealtimeManager.java index a1d7339b..78ad0f0b 100644 --- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/realtime/RealtimeManager.java +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/realtime/RealtimeManager.java @@ -4,15 +4,21 @@ import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.gson.Gson; +import com.optimove.android.auth.AuthManager; import com.optimove.android.main.events.core_events.SetEmailEvent; import com.optimove.android.main.events.core_events.SetUserIdEvent; import com.optimove.android.main.sdk_configs.configs.RealtimeConfigs; import com.optimove.android.main.tools.networking.HttpClient; import com.optimove.android.main.tools.opti_logger.OptiLoggerStreamsContainer; +import com.optimove.android.optistream.OptistreamDispatcher; import com.optimove.android.optistream.OptistreamEvent; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.ArrayList; import java.util.List; @@ -25,18 +31,18 @@ public final class RealtimeManager { @NonNull private final SharedPreferences realtimePreferences; @NonNull - private final HttpClient httpClient; - @NonNull private final RealtimeConfigs realtimeConfigs; - + @NonNull private final Gson realtimeGson; + @NonNull + private final OptistreamDispatcher dispatcher; public RealtimeManager(@NonNull HttpClient httpClient, @NonNull RealtimeConfigs realtimeConfigs, - @NonNull Context context) { - this.httpClient = httpClient; + @NonNull Context context, @Nullable AuthManager authManager) { this.realtimePreferences = context.getSharedPreferences(REALTIME_SP_NAME, Context.MODE_PRIVATE); this.realtimeConfigs = realtimeConfigs; this.realtimeGson = new Gson(); + this.dispatcher = new OptistreamDispatcher(httpClient, authManager); } public void reportEvents(List optistreamEvents) { @@ -73,16 +79,37 @@ public void reportEvents(List optistreamEvents) { } private void dispatchEvents(List optistreamEvents) { - httpClient.postJson(realtimeConfigs.getRealtimeGateway(), new Gson().toJson(optistreamEvents)) - .successListener(jsonResponse -> + List parsedEvents = new ArrayList<>(); + try { + Gson gson = new Gson(); + for (OptistreamEvent event : optistreamEvents) { + parsedEvents.add(new JSONObject(gson.toJson(event))); + } + } catch (JSONException e) { + dispatchingFailed(e, optistreamEvents); + return; + } + + dispatcher.sendBatch( + parsedEvents, + realtimeConfigs.getRealtimeGateway(), + RealtimeConstants.REPORT_EVENT_REQUEST_ROUTE, + (groupEvents, success, error) -> { + if (success) { realtimePreferences.edit() .remove(FAILED_SET_USER_EVENT_KEY) .remove(FAILED_SET_EMAIL_EVENT_KEY) - .apply() - ) - .errorListener(e -> dispatchingFailed(e, optistreamEvents)) - .destination("%s", RealtimeConstants.REPORT_EVENT_REQUEST_ROUTE) - .send(); + .apply(); + } else { + OptiLoggerStreamsContainer.error("RT dispatch group failed - %s", + error != null ? error.getMessage() : "unknown"); + dispatchingFailed( + error != null ? error : new Exception("Unknown"), + optistreamEvents); + } + }, + () -> { } + ); } private void dispatchingFailed(Exception e, List optistreamEvents) {