Skip to content

[FSSDK-8840]: Adds native Android implementation for ODP #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Refer to the [Flutter SDK's developer documentation](https://docs.developers.opt

See the [pubspec.yaml](https://github.com/optimizely/optimizely-flutter-sdk/blob/master/pubspec.yaml) file for Flutter version requirements.

On the Android platform, the SDK requires a minimum SDK version of 14 or higher and compile SDK version of 32.
On the Android platform, the SDK requires a minimum SDK version of 21 or higher and compile SDK version of 32.

On the iOS platform, the SDK requires a minimum version of 10.0.

Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ apply plugin: 'com.android.library'
ext {
compile_sdk_version = 32
build_tools_version = "30.0.3"
min_sdk_version = 14
min_sdk_version = 21
target_sdk_version = 29
}

Expand Down Expand Up @@ -77,7 +77,7 @@ dependencies {
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
implementation group: 'org.slf4j', name: 'slf4j-android', version: '1.7.25'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10"
implementation "com.optimizely.ab:android-sdk:3.13.2"
implementation "com.optimizely.ab:android-sdk:4.0.0-beta2"
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
implementation ('com.google.guava:guava:19.0') {
exclude group:'com.google.guava', module:'listenablefuture'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2023, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -46,13 +46,19 @@
import com.optimizely.ab.notification.NotificationCenter;
import com.optimizely.ab.notification.TrackNotification;
import com.optimizely.ab.notification.UpdateConfigNotification;
import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
import com.optimizely.optimizely_flutter_sdk.helper_classes.ArgumentsParser;
import com.optimizely.optimizely_flutter_sdk.helper_classes.Utils;

import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.*;
import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.DISABLE_ODP;
import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.SEGMENTS_CACHE_SIZE;
import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.SEGMENTS_CACHE_TIMEOUT_IN_SECONDS;
import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.TIMEOUT_FOR_ODP_EVENT_IN_SECONDS;
import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS;
import static com.optimizely.optimizely_flutter_sdk.helper_classes.Utils.getNotificationListenerType;

import java.util.Collections;
Expand Down Expand Up @@ -127,16 +133,48 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N
notificationIdsTracker.remove(sdkKey);

List<OptimizelyDecideOption> defaultDecideOptions = argumentsParser.getDecideOptions();

// SDK Settings Default Values
int segmentsCacheSize = 100;
int segmentsCacheTimeoutInSecs = 600;
int timeoutForSegmentFetchInSecs = 10;
int timeoutForOdpEventInSecs = 10;
boolean disableOdp = false;
Map<String, Object> sdkSettings = argumentsParser.getOptimizelySdkSettings();
if (sdkSettings != null) {
if (sdkSettings.containsKey(SEGMENTS_CACHE_SIZE)) {
segmentsCacheSize = (Integer) sdkSettings.get(SEGMENTS_CACHE_SIZE);
}
if (sdkSettings.containsKey(SEGMENTS_CACHE_TIMEOUT_IN_SECONDS)) {
segmentsCacheTimeoutInSecs = (Integer) sdkSettings.get(SEGMENTS_CACHE_TIMEOUT_IN_SECONDS);
}
if (sdkSettings.containsKey(TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS)) {
timeoutForSegmentFetchInSecs = (Integer) sdkSettings.get(TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS);
}
if (sdkSettings.containsKey(TIMEOUT_FOR_ODP_EVENT_IN_SECONDS)) {
timeoutForOdpEventInSecs = (Integer) sdkSettings.get(TIMEOUT_FOR_ODP_EVENT_IN_SECONDS);
}
if (sdkSettings.containsKey(DISABLE_ODP)) {
disableOdp = (boolean) sdkSettings.get(DISABLE_ODP);
}
}
// Creating new instance
OptimizelyManager optimizelyManager = OptimizelyManager.builder()
OptimizelyManager.Builder optimizelyManagerBuilder = OptimizelyManager.builder()
.withEventProcessor(batchProcessor)
.withEventHandler(eventHandler)
.withNotificationCenter(notificationCenter)
.withDatafileDownloadInterval(datafilePeriodicDownloadInterval, TimeUnit.SECONDS)
.withErrorHandler(new RaiseExceptionErrorHandler())
.withDefaultDecideOptions(defaultDecideOptions)
.withSDKKey(sdkKey)
.build(context);
.withODPSegmentCacheSize(segmentsCacheSize)
.withODPSegmentCacheTimeout(segmentsCacheTimeoutInSecs, TimeUnit.SECONDS)
.withTimeoutForODPSegmentFetch(timeoutForSegmentFetchInSecs)
.withTimeoutForODPEventDispatch(timeoutForOdpEventInSecs)
.withSDKKey(sdkKey);
if (disableOdp) {
optimizelyManagerBuilder.withODPDisabled();
}
OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context);

optimizelyManager.initialize(context, null, (OptimizelyClient client) -> {
if (client.isValid()) {
Expand All @@ -158,14 +196,14 @@ protected void createUserContext(ArgumentsParser argumentsParser, @NonNull Resul

String userId = argumentsParser.getUserId();
Map<String, Object> attributes = argumentsParser.getAttributes();
if (userId == null) {
result.success(createResponse(ErrorMessage.INVALID_PARAMS));
return;
}
try {
String userContextId = Utils.getRandomUUID();

OptimizelyUserContext optlyUserContext = optimizelyClient.createUserContext(userId, attributes);
OptimizelyUserContext optlyUserContext;
if (userId != null) {
optlyUserContext = optimizelyClient.createUserContext(userId, attributes);
} else {
optlyUserContext = optimizelyClient.createUserContext(attributes);
}
if (optlyUserContext != null) {
if (userContextsTracker.containsKey(sdkKey)) {
userContextsTracker.get(sdkKey).put(userContextId, optlyUserContext);
Expand Down Expand Up @@ -390,6 +428,109 @@ protected void removeAllForcedDecisions(ArgumentsParser argumentsParser, @NonNul
result.success(createResponse());
}

/// Returns an array of segments that the user is qualified for.
protected void getQualifiedSegments(ArgumentsParser argumentsParser, @NonNull Result result) {
String sdkKey = argumentsParser.getSdkKey();
OptimizelyUserContext userContext = getUserContext(argumentsParser);
if (!isUserContextValid(sdkKey, userContext, result)) {
return;
}
List<String> qualifiedSegments = userContext.getQualifiedSegments();
if (qualifiedSegments != null) {
result.success(createResponse(Collections.singletonMap(RequestParameterKey.QUALIFIED_SEGMENTS, qualifiedSegments)));
} else {
result.success(createResponse(ErrorMessage.QUALIFIED_SEGMENTS_NOT_FOUND));
}
}

/// Sets qualified segments for the user context.
protected void setQualifiedSegments(ArgumentsParser argumentsParser, @NonNull Result result) {
String sdkKey = argumentsParser.getSdkKey();
OptimizelyUserContext userContext = getUserContext(argumentsParser);
if (!isUserContextValid(sdkKey, userContext, result)) {
return;
}
List<String> qualifiedSegments = argumentsParser.getQualifiedSegments();
if (qualifiedSegments == null) {
result.success(createResponse(ErrorMessage.INVALID_PARAMS));
return;
}
userContext.setQualifiedSegments(qualifiedSegments);
result.success(createResponse());
}

/// Returns the device vuid.
protected void getVuid(ArgumentsParser argumentsParser, @NonNull Result result) {
String sdkKey = argumentsParser.getSdkKey();
OptimizelyClient optimizelyClient = getOptimizelyClient(sdkKey);
if (!isOptimizelyClientValid(sdkKey, optimizelyClient, result)) {
return;
}
result.success(createResponse(true, Collections.singletonMap(RequestParameterKey.VUID, optimizelyClient.getVuid()), ""));
}

/// Checks if the user is qualified for the given segment.
protected void isQualifiedFor(ArgumentsParser argumentsParser, @NonNull Result result) {
String sdkKey = argumentsParser.getSdkKey();
OptimizelyUserContext userContext = getUserContext(argumentsParser);
if (!isUserContextValid(sdkKey, userContext, result)) {
return;
}
String segment = argumentsParser.getSegment();
if (segment == null) {
result.success(createResponse(ErrorMessage.INVALID_PARAMS));
return;
}

result.success(createResponse(userContext.isQualifiedFor(segment)));
}

/// Send an event to the ODP server.
protected void sendODPEvent(ArgumentsParser argumentsParser, @NonNull Result result) {
String sdkKey = argumentsParser.getSdkKey();
OptimizelyClient optimizelyClient = getOptimizelyClient(sdkKey);
if (!isOptimizelyClientValid(sdkKey, optimizelyClient, result)) {
return;
}
String action = argumentsParser.getAction();
if (action == null || action.isEmpty()) {
result.success(createResponse(ErrorMessage.INVALID_PARAMS));
return;
}

String type = argumentsParser.getType();
Map<String, String> identifiers = argumentsParser.getIdentifiers();
if (identifiers == null) {
identifiers = new HashMap<>();
}
Map<String, Object> data = argumentsParser.getData();
if (data == null) {
data = new HashMap<>();
}

optimizelyClient.sendODPEvent(type, action, identifiers, data);
result.success(createResponse());
}

/// Fetch all qualified segments for the user context.
protected void fetchQualifiedSegments(ArgumentsParser argumentsParser, @NonNull Result result) {
String sdkKey = argumentsParser.getSdkKey();
OptimizelyUserContext userContext = getUserContext(argumentsParser);
if (!isUserContextValid(sdkKey, userContext, result)) {
return;
}
List<ODPSegmentOption> segmentOptions = argumentsParser.getSegmentOptions();

try {
userContext.fetchQualifiedSegments((fetchQualifiedResult) -> {
result.success(createResponse(fetchQualifiedResult));
},segmentOptions);

} catch (Exception ex) {
result.success(createResponse(ex.getMessage()));
}
}

protected void close(ArgumentsParser argumentsParser, @NonNull Result result) {
String sdkKey = argumentsParser.getSdkKey();
OptimizelyClient optimizelyClient = getOptimizelyClient(sdkKey);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2023, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -119,6 +119,30 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
removeAllForcedDecisions(argumentsParser, result);
break;
}
case APIs.GET_QUALIFIED_SEGMENTS: {
getQualifiedSegments(argumentsParser, result);
break;
}
case APIs.SET_QUALIFIED_SEGMENTS: {
setQualifiedSegments(argumentsParser, result);
break;
}
case APIs.GET_VUID: {
getVuid(argumentsParser, result);
break;
}
case APIs.IS_QUALIFIED_FOR: {
isQualifiedFor(argumentsParser, result);
break;
}
case APIs.SEND_ODP_EVENT: {
sendODPEvent(argumentsParser, result);
break;
}
case APIs.FETCH_QUALIFIED_SEGMENTS: {
fetchQualifiedSegments(argumentsParser, result);
break;
}
case APIs.CLOSE: {
close(argumentsParser, result);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2023, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand All @@ -15,6 +15,7 @@
***************************************************************************/
package com.optimizely.optimizely_flutter_sdk.helper_classes;

import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;

import java.util.List;
Expand Down Expand Up @@ -110,4 +111,36 @@ public String getDatafileHostPrefix() {
public String getExperimentKey() {
return (String) arguments.get(Constants.RequestParameterKey.EXPERIMENT_KEY);
}

public List<String> getQualifiedSegments() {
return (List<String>) arguments.get(Constants.RequestParameterKey.QUALIFIED_SEGMENTS);
}

public String getSegment() {
return (String) arguments.get(Constants.RequestParameterKey.SEGMENT);
}

public String getAction() {
return (String) arguments.get(Constants.RequestParameterKey.ACTION);
}

public String getType() {
return (String) arguments.get(Constants.RequestParameterKey.ODP_EVENT_TYPE);
}

public Map<String, String> getIdentifiers() {
return (Map<String, String>) arguments.get(Constants.RequestParameterKey.IDENTIFIERS);
}

public Map<String, Object> getData() {
return (Map<String, Object>) arguments.get(Constants.RequestParameterKey.DATA);
}

public List<ODPSegmentOption> getSegmentOptions() {
return Utils.getSegmentOptions((List<String>) arguments.get(Constants.RequestParameterKey.OPTIMIZELY_SEGMENT_OPTION));
}

public Map<String, Object> getOptimizelySdkSettings() {
return (Map<String, Object>) arguments.get(Constants.RequestParameterKey.OPTIMIZELY_SDK_SETTINGS);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2022, Optimizely, Inc. and contributors *
* Copyright 2022-2023, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -38,6 +38,14 @@ public static class APIs {
public static final String REMOVE_NOTIFICATION_LISTENER = "removeNotificationListener";
public static final String CLEAR_ALL_NOTIFICATION_LISTENERS = "clearAllNotificationListeners";
public static final String CLEAR_NOTIFICATION_LISTENERS = "clearNotificationListeners";

// ODP APIs constants
public static final String SEND_ODP_EVENT = "sendOdpEvent";
public static final String GET_VUID = "getVuid";
public static final String GET_QUALIFIED_SEGMENTS = "getQualifiedSegments";
public static final String SET_QUALIFIED_SEGMENTS = "setQualifiedSegments";
public static final String IS_QUALIFIED_FOR = "isQualifiedFor";
public static final String FETCH_QUALIFIED_SEGMENTS = "fetchQualifiedSegments";
}

public static class NotificationType {
Expand Down Expand Up @@ -71,6 +79,21 @@ public static class RequestParameterKey {
public static final String VARIATION_KEY = "variationKey";
public static final String DATAFILE_HOST_PREFIX = "datafileHostPrefix";
public static final String DATAFILE_HOST_SUFFIX = "datafileHostSuffix";

public static final String VUID = "vuid";
public static final String QUALIFIED_SEGMENTS = "qualifiedSegments";
public static final String SEGMENT = "segment";
public static final String ACTION = "action";
public static final String IDENTIFIERS = "identifiers";
public static final String DATA = "data";
public static final String ODP_EVENT_TYPE = "type";
public static final String OPTIMIZELY_SEGMENT_OPTION = "optimizelySegmentOption";
public static final String OPTIMIZELY_SDK_SETTINGS = "optimizelySdkSettings";
public static final String SEGMENTS_CACHE_SIZE = "segmentsCacheSize";
public static final String SEGMENTS_CACHE_TIMEOUT_IN_SECONDS = "segmentsCacheTimeoutInSecs";
public static final String TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS = "timeoutForSegmentFetchInSecs";
public static final String TIMEOUT_FOR_ODP_EVENT_IN_SECONDS = "timeoutForOdpEventInSecs";
public static final String DISABLE_ODP = "disableOdp";
}

public static class ErrorMessage {
Expand All @@ -80,6 +103,7 @@ public static class ErrorMessage {
public static final String OPTIMIZELY_CLIENT_NOT_FOUND = "Optimizely client not found.";
public static final String USER_CONTEXT_NOT_FOUND = "User context not found.";
public static final String USER_CONTEXT_NOT_CREATED = "User context not created.";
public static final String QUALIFIED_SEGMENTS_NOT_FOUND = "Qualified Segments not found.";
}

public static class DecisionListenerKeys {
Expand Down Expand Up @@ -124,4 +148,9 @@ public static class DecideOption {
public static final String INCLUDE_REASONS = "includeReasons";
public static final String EXCLUDE_VARIABLES = "excludeVariables";
}

public static class SegmentOption {
public static final String IGNORE_CACHE = "ignoreCache";
public static final String RESET_CACHE = "resetCache";
}
}
Loading