diff --git a/README.md b/README.md index 5da0c09..eaadfbf 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/android/build.gradle b/android/build.gradle index d707cf2..df5b447 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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 } @@ -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' diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java index 2e3bd69..8d0ca28 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java @@ -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. * @@ -46,6 +46,7 @@ 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; @@ -53,6 +54,11 @@ 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; @@ -127,16 +133,48 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N notificationIdsTracker.remove(sdkKey); List defaultDecideOptions = argumentsParser.getDecideOptions(); + + // SDK Settings Default Values + int segmentsCacheSize = 100; + int segmentsCacheTimeoutInSecs = 600; + int timeoutForSegmentFetchInSecs = 10; + int timeoutForOdpEventInSecs = 10; + boolean disableOdp = false; + Map 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()) { @@ -158,14 +196,14 @@ protected void createUserContext(ArgumentsParser argumentsParser, @NonNull Resul String userId = argumentsParser.getUserId(); Map 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); @@ -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 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 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 identifiers = argumentsParser.getIdentifiers(); + if (identifiers == null) { + identifiers = new HashMap<>(); + } + Map 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 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); diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java index edc49c7..89f787c 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java @@ -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. * @@ -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; diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java index d726f21..605b0aa 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java @@ -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. * @@ -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; @@ -110,4 +111,36 @@ public String getDatafileHostPrefix() { public String getExperimentKey() { return (String) arguments.get(Constants.RequestParameterKey.EXPERIMENT_KEY); } + + public List getQualifiedSegments() { + return (List) 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 getIdentifiers() { + return (Map) arguments.get(Constants.RequestParameterKey.IDENTIFIERS); + } + + public Map getData() { + return (Map) arguments.get(Constants.RequestParameterKey.DATA); + } + + public List getSegmentOptions() { + return Utils.getSegmentOptions((List) arguments.get(Constants.RequestParameterKey.OPTIMIZELY_SEGMENT_OPTION)); + } + + public Map getOptimizelySdkSettings() { + return (Map) arguments.get(Constants.RequestParameterKey.OPTIMIZELY_SDK_SETTINGS); + } } diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java index 8e1b40b..2f200db 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java @@ -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. * @@ -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 { @@ -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 { @@ -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 { @@ -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"; + } } diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java index 7a4fb5d..76ec8c8 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java @@ -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. * @@ -29,6 +29,7 @@ import com.optimizely.ab.notification.DecisionNotification; import com.optimizely.ab.notification.TrackNotification; import com.optimizely.ab.notification.UpdateConfigNotification; +import com.optimizely.ab.odp.ODPSegmentOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; public class Utils { @@ -66,6 +67,26 @@ public static List getDecideOptions(List options return convertedOptions; } + public static List getSegmentOptions(List options) { + if(options == null || options.isEmpty()) { + return null; + } + List convertedOptions = new ArrayList<>(); + for(String option: options) { + switch(option) { + case Constants.SegmentOption.IGNORE_CACHE: + convertedOptions.add(ODPSegmentOption.IGNORE_CACHE); + break; + case Constants.SegmentOption.RESET_CACHE: + convertedOptions.add(ODPSegmentOption.RESET_CACHE); + break; + default: + break; + } + } + return convertedOptions; + } + public static Class getNotificationListenerType(String notificationType) { if (notificationType == null || notificationType.isEmpty()) { return null; diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index e643787..ceec9a6 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -39,7 +39,7 @@ android { applicationId "com.optimizely.optimizely_flutter_sdk_example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/build.gradle b/example/android/build.gradle index fbba653..57c3218 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -11,7 +11,7 @@ buildscript { } } ext { - android_sdk_version = "3.13.2" + android_sdk_version = "4.0.0-beta2" } allprojects { repositories {