diff --git a/app/build.gradle b/app/build.gradle index fa7059e9b..c85cdac38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,6 @@ apply plugin: 'com.android.application' //apply plugin: 'com.huawei.agconnect' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' /* //for automatic symbol upload diff --git a/app/src/main/java/ly/count/android/demo/App.java b/app/src/main/java/ly/count/android/demo/App.java index 905ba0636..6e7e588b7 100644 --- a/app/src/main/java/ly/count/android/demo/App.java +++ b/app/src/main/java/ly/count/android/demo/App.java @@ -225,6 +225,8 @@ public void callback(String error) { .setEnableAttribution(true) + .enableServerConfiguration() + .setUserProperties(customUserProperties); Countly.sharedInstance().init(config); diff --git a/build.gradle b/build.gradle index 2f3c25a18..845759e71 100644 --- a/build.gradle +++ b/build.gradle @@ -1,31 +1,31 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.8.0' repositories { - maven { - url 'https://developer.huawei.com/repo/' - } - google() - mavenCentral() + maven { + url 'https://developer.huawei.com/repo/' } - dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' - classpath 'com.github.dcendents:android-maven-plugin:1.2' - classpath 'com.google.gms:google-services:4.3.10' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.huawei.agconnect:agcp:1.3.1.300' - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3" - } + classpath 'com.github.dcendents:android-maven-plugin:1.2' + classpath 'com.google.gms:google-services:4.3.15' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.huawei.agconnect:agcp:1.3.1.300' + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3" + } } allprojects { - tasks.withType(Javadoc) { - options.addStringOption('Xdoclint:none', '-quiet') - } - repositories { - google() - mavenCentral() - } + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + } + repositories { + google() + mavenCentral() + } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bec506d67..d92a9f707 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Apr 25 19:27:00 EEST 2021 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +#Sun Apr 23 22:46:09 EEST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/sdk-native/build.gradle b/sdk-native/build.gradle index 5013486b7..354366768 100644 --- a/sdk-native/build.gradle +++ b/sdk-native/build.gradle @@ -27,7 +27,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - buildToolsVersion '31.0.0' + buildToolsVersion '33.0.0' } dependencies { diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java index 11e01f818..a060685a3 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java @@ -57,15 +57,26 @@ public class ConnectionProcessorTests { DeviceIdProvider mockDeviceId; String testDeviceId; ModuleLog moduleLog; + ConfigurationProvider configurationProviderFake; @Before public void setUp() { + configurationProviderFake = new ConfigurationProvider() { + @Override public boolean getNetworkingEnabled() { + return true; + } + + @Override public boolean getTrackingEnabled() { + return true; + } + }; + Countly.sharedInstance().setLoggingEnabled(true); mockStore = mock(CountlyStore.class); mockDeviceId = mock(DeviceIdProvider.class); moduleLog = mock(ModuleLog.class); - connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, null, null, moduleLog); + connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, configurationProviderFake, null, null, moduleLog); testDeviceId = "123"; } @@ -74,7 +85,7 @@ public void testConstructorAndGetters() { final String serverURL = "https://secureserver"; final CountlyStore mockStore = mock(CountlyStore.class); final DeviceIdProvider mockDeviceId = mock(DeviceIdProvider.class); - final ConnectionProcessor connectionProcessor1 = new ConnectionProcessor(serverURL, mockStore, mockDeviceId, null, null, moduleLog); + final ConnectionProcessor connectionProcessor1 = new ConnectionProcessor(serverURL, mockStore, mockDeviceId, configurationProviderFake, null, null, moduleLog); assertEquals(serverURL, connectionProcessor1.getServerURL()); assertSame(mockStore, connectionProcessor1.getCountlyStore()); } @@ -143,7 +154,7 @@ public void urlConnectionCustomHeaderValues() throws IOException { customValues.put("5", ""); customValues.put("6", null); - ConnectionProcessor connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, null, customValues, moduleLog); + ConnectionProcessor connectionProcessor = new ConnectionProcessor("http://server", mockStore, mockDeviceId, configurationProviderFake, null, customValues, moduleLog); final URLConnection urlConnection = connectionProcessor.urlConnectionForServerRequest("eventData", null); assertEquals("bb", urlConnection.getRequestProperty("aa")); diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyStoreTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyStoreTests.java index a7a6af33b..08a6c4cfb 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyStoreTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyStoreTests.java @@ -636,6 +636,10 @@ public void validatingAnythingSetInStorageSeparate() { CountlyStore.cachePushData("mnc", null, getContext()); assertTrue(sp.anythingSetInStorage()); store.clear(); + + sp.setServerConfig("qwe"); + assertTrue(sp.anythingSetInStorage()); + store.clear(); } /** @@ -680,6 +684,9 @@ public void validatingAnythingSetInStorageAggregate() { CountlyStore.cachePushData("mnc", "uio", getContext()); assertTrue(sp.anythingSetInStorage()); + + sp.setServerConfig("qwe"); + assertTrue(sp.anythingSetInStorage()); } /** @@ -732,4 +739,12 @@ public void getEventsForRequestAndEmptyEventQueueWithSimpleEvents() throws Unsup assertEquals(expected, sp.getEventsForRequestAndEmptyEventQueue()); Assert.assertEquals(0, sp.getEventQueueSize()); } + + @Test + public void getSetServerConfig() { + store.clear(); + Assert.assertNull(sp.getServerConfig()); + sp.setServerConfig("qwe"); + Assert.assertEquals("qwe", sp.getServerConfig()); + } } diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleBaseTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleBaseTests.java index 6e55ebc1b..69afef45b 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleBaseTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleBaseTests.java @@ -30,7 +30,7 @@ public void tearDown() { //making sure all needed modules are added @Test public void checkup() { - Assert.assertEquals(14, mCountly.modules.size()); + Assert.assertEquals(15, mCountly.modules.size()); } //just making sure nothing throws exceptions diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java new file mode 100644 index 000000000..a57cd6da7 --- /dev/null +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java @@ -0,0 +1,284 @@ +package ly.count.android.sdk; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.HashMap; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static androidx.test.InstrumentationRegistry.getContext; +import static org.mockito.Mockito.mock; + +@RunWith(AndroidJUnit4.class) +public class ModuleConfigurationTests { + CountlyStore countlyStore; + + @Before + public void setUp() { + countlyStore = new CountlyStore(getContext(), mock(ModuleLog.class)); + countlyStore.clear(); + } + + @After + public void tearDown() { + } + + /** + * Default values when server config is disabled and storage is empty + * No server connection + */ + @Test + public void init_disabled_storageEmpty() { + countlyStore.clear(); + CountlyConfig config = TestUtils.createConfigurationConfig(false, null); + Countly countly = (new Countly()).init(config); + + Assert.assertFalse(countly.moduleConfiguration.serverConfigEnabled); + Assert.assertNull(countlyStore.getServerConfig()); + assertConfigDefault(countly); + } + + /** + * Default values when server config is enabled and storage is empty + * No server connection + */ + @Test + public void init_enabled_storageEmpty() { + CountlyConfig config = TestUtils.createConfigurationConfig(true, null); + Countly countly = (new Countly()).init(config); + + Assert.assertTrue(countly.moduleConfiguration.serverConfigEnabled); + Assert.assertNull(countlyStore.getServerConfig()); + assertConfigDefault(countly); + } + + /** + * Server config enabled + * All config properties are default/allowing + * No server connection + * + * @throws JSONException + */ + @Test + public void init_enabled_storageAllowing() throws JSONException { + countlyStore.setServerConfig(getStorageString(true, true)); + CountlyConfig config = TestUtils.createConfigurationConfig(true, null); + Countly countly = (new Countly()).init(config); + + Assert.assertTrue(countly.moduleConfiguration.serverConfigEnabled); + Assert.assertNotNull(countlyStore.getServerConfig()); + assertConfigDefault(countly); + } + + /** + * Server config enabled + * All config properties are off default/disabling + * No server connection + * + * @throws JSONException + */ + @Test + public void init_enabled_storageForbidding() throws JSONException { + countlyStore.setServerConfig(getStorageString(false, false)); + CountlyConfig config = TestUtils.createConfigurationConfig(true, null); + Countly countly = (new Countly()).init(config); + + Assert.assertTrue(countly.moduleConfiguration.serverConfigEnabled); + Assert.assertNotNull(countlyStore.getServerConfig()); + Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled()); + Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled()); + } + + /** + * Server config disabled + * All config properties are default/allowing + * No server connection + * + * @throws JSONException + */ + @Test + public void init_disabled_storageAllowing() throws JSONException { + countlyStore.setServerConfig(getStorageString(true, true)); + CountlyConfig config = TestUtils.createConfigurationConfig(false, null); + Countly countly = Countly.sharedInstance().init(config); + + Assert.assertFalse(countly.moduleConfiguration.serverConfigEnabled); + Assert.assertNotNull(countlyStore.getServerConfig()); + assertConfigDefault(countly); + } + + /** + * Server config disabled + * All config properties are off default/disabling + * No server connection + * + * @throws JSONException + */ + @Test + public void init_disabled_storageForbidding() throws JSONException { + countlyStore.setServerConfig(getStorageString(false, false)); + CountlyConfig config = TestUtils.createConfigurationConfig(false, null); + Countly countly = (new Countly()).init(config); + + Assert.assertFalse(countly.moduleConfiguration.serverConfigEnabled); + Assert.assertNotNull(countlyStore.getServerConfig()); + assertConfigDefault(countly); + } + + /** + * Making sure that a downloaded configuration is persistently stored across init's + */ + @Test + public void scenario_1() { + //initial state is fresh + Assert.assertNull(countlyStore.getServerConfig()); + + //first init fails receiving config, config getters return defaults, store is empty + initAndValidateConfigParsingResult(null, false); + + //second init succeeds receiving config + Countly countly = initAndValidateConfigParsingResult("{'v':1,'t':2,'c':{'tracking':false,'networking':false}}", true); + Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled()); + Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled()); + + //third init is lacking a connection but still has the previously saved values + CountlyConfig config = TestUtils.createConfigurationConfig(true, null); + countly = (new Countly()).init(config); + Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled()); + Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled()); + + //fourth init updates config values + countly = initAndValidateConfigParsingResult("{'v':1,'t':2,'c':{'tracking':true,'networking':false}}", true); + Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled()); + Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled()); + } + + /** + * With tracking disabled, nothing should be written to the request and event queues + */ + @Test + public void validatingTrackingConfig() throws JSONException { + //nothing in queues initially + Assert.assertEquals("", countlyStore.getRequestQueueRaw()); + Assert.assertEquals(0, countlyStore.getEvents().length); + + countlyStore.setServerConfig(getStorageString(false, false)); + + CountlyConfig config = TestUtils.createConfigurationConfig(true, null); + Countly countly = (new Countly()).init(config); + + Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled()); + Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled()); + + //try events + countly.events().recordEvent("d"); + countly.events().recordEvent("1"); + + //try a non event recording + countly.crashes().recordHandledException(new Exception()); + + //try a direct request + countly.requestQueue().addDirectRequest(new HashMap<>()); + + countly.requestQueue().attemptToSendStoredRequests(); + + Assert.assertEquals("", countlyStore.getRequestQueueRaw()); + Assert.assertEquals(0, countlyStore.getEvents().length); + } + + /** + * Making sure that bad config responses are rejected + */ + @Test + public void init_enabled_rejectingRequests() { + //{"v":1,"t":2,"c":{"aa":"bb"}} + Assert.assertNull(countlyStore.getServerConfig()); + + //return null object + initAndValidateConfigParsingResult(null, false); + + //return empty object + initAndValidateConfigParsingResult("{}", false); + + //returns all except 'v' + initAndValidateConfigParsingResult("{'t':2,'c':{'aa':'bb'}}", false); + + //returns all except 't' + initAndValidateConfigParsingResult("{'v':1,'c':{'aa':'bb'}}", false); + + //returns all except 'c' + initAndValidateConfigParsingResult("{'v':1,'t':2}", false); + + //returns all except 'c' wrong type (number) + initAndValidateConfigParsingResult("{'v':1,'t':2,'c':123}", false); + + //returns all except 'c' wrong type (bool) + initAndValidateConfigParsingResult("{'v':1,'t':2,'c':false}", false); + + //returns all except 'c' wrong type (string) + initAndValidateConfigParsingResult("{'v':1,'t':2,'c':'fdf'}", false); + } + + Countly initAndValidateConfigParsingResult(String targetResponse, boolean responseAccepted) { + CountlyConfig config = TestUtils.createConfigurationConfig(true, createIRGForSpecificResponse(targetResponse)); + Countly countly = (new Countly()).init(config); + + if (!responseAccepted) { + Assert.assertNull(countlyStore.getServerConfig()); + assertConfigDefault(countly); + } else { + Assert.assertNotNull(countlyStore.getServerConfig()); + } + + return countly; + } + + void assertConfigDefault(Countly countly) { + Assert.assertTrue(countly.moduleConfiguration.getNetworkingEnabled()); + Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled()); + } + + ImmediateRequestGenerator createIRGForSpecificResponse(final String targetResponse) { + return new ImmediateRequestGenerator() { + @Override public ImmediateRequestI CreateImmediateRequestMaker() { + return new ImmediateRequestI() { + @Override public void doWork(String requestData, String customEndpoint, ConnectionProcessor cp, boolean requestShouldBeDelayed, boolean networkingIsEnabled, ImmediateRequestMaker.InternalImmediateRequestCallback callback, ModuleLog log) { + if (targetResponse == null) { + callback.callback(null); + return; + } + + JSONObject jobj = null; + + try { + jobj = new JSONObject(targetResponse); + } catch (JSONException e) { + e.printStackTrace(); + } + + callback.callback(jobj); + } + }; + } + }; + } + + //creates the stringified storage object with all the required properties + String getStorageString(boolean tracking, boolean networking) throws JSONException { + JSONObject jsonObject = new JSONObject(); + JSONObject jsonObjectConfig = new JSONObject(); + + jsonObjectConfig.put("tracking", tracking); + jsonObjectConfig.put("networking", networking); + + jsonObject.put("v", 1); + jsonObject.put("t", 1681808287464L); + jsonObject.put("c", jsonObjectConfig); + + return jsonObject.toString(); + } +} diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java index 402aaeecf..2fc774d07 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java @@ -50,6 +50,21 @@ public static class Activity2 extends Activity { public static class Activity3 extends Activity { } + public static CountlyConfig createConfigurationConfig(boolean enableServerConfig, ImmediateRequestGenerator irGen) { + CountlyConfig cc = (new CountlyConfig((Application) ApplicationProvider.getApplicationContext(), commonAppKey, commonURL)) + .setDeviceId(commonDeviceId) + .setLoggingEnabled(true) + .enableCrashReporting(); + + cc.immediateRequestGenerator = irGen; + + if (enableServerConfig) { + cc.enableServerConfiguration(); + } + + return cc; + } + public static CountlyConfig createConsentCountlyConfig(boolean requiresConsent, String[] givenConsent, ModuleBase testModuleListener, RequestQueueProvider rqp) { CountlyConfig cc = (new CountlyConfig((Application) ApplicationProvider.getApplicationContext(), commonAppKey, commonURL)) .setDeviceId(commonDeviceId) diff --git a/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java new file mode 100644 index 000000000..b0134fec9 --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java @@ -0,0 +1,7 @@ +package ly.count.android.sdk; + +interface ConfigurationProvider { + boolean getNetworkingEnabled(); + + boolean getTrackingEnabled(); +} diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java index f554bd169..760a3badb 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java @@ -51,6 +51,7 @@ public class ConnectionProcessor implements Runnable { private final StorageProvider storageProvider_; private final DeviceIdProvider deviceIdProvider_; + final ConfigurationProvider configProvider_; private final String serverURL_; private final SSLContext sslContext_; @@ -65,10 +66,11 @@ private enum RequestResult { RETRY // retry MAX_RETRIES_BEFORE_SLEEP before switching to SLEEP } - ConnectionProcessor(final String serverURL, final StorageProvider storageProvider, final DeviceIdProvider deviceIdProvider, final SSLContext sslContext, final Map requestHeaderCustomValues, ModuleLog logModule) { + ConnectionProcessor(final String serverURL, final StorageProvider storageProvider, final DeviceIdProvider deviceIdProvider, final ConfigurationProvider configProvider, final SSLContext sslContext, final Map requestHeaderCustomValues, ModuleLog logModule) { serverURL_ = serverURL; storageProvider_ = storageProvider; deviceIdProvider_ = deviceIdProvider; + configProvider_ = configProvider; sslContext_ = sslContext; requestHeaderCustomValues_ = requestHeaderCustomValues; L = logModule; @@ -198,6 +200,11 @@ synchronized public URLConnection urlConnectionForServerRequest(String requestDa @Override public void run() { while (true) { + if (!configProvider_.getNetworkingEnabled()) { + L.w("[Connection Processor] run, Networking config is disabled, request queue skipped"); + break; + } + final String[] storedEvents = storageProvider_.getRequests(); int storedEventCount = storedEvents == null ? 0 : storedEvents.length; diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java index f05445b37..29cac1513 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java @@ -24,6 +24,7 @@ of this software and associated documentation files (the "Software"), to deal import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.time.OffsetDateTime; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -59,6 +60,7 @@ class ConnectionQueue implements RequestQueueProvider { protected ModuleRequestQueue moduleRequestQueue = null;//todo remove in the future protected DeviceInfo deviceInfo = null;//todo ?remove in the future? StorageProvider storageProvider; + ConfigurationProvider configProvider; void setBaseInfoProvider(BaseInfoProvider bip) { baseInfoProvider = bip; @@ -649,6 +651,15 @@ String prepareCommonRequestData() { + "&sdk_name=" + Countly.sharedInstance().COUNTLY_SDK_NAME; } + String prepareCommonRequestDataShort() { + UtilsTime.Instant instant = UtilsTime.getCurrentInstant(); + + return "app_key=" + UtilsNetworking.urlEncodeString(baseInfoProvider.getAppKey()) + + "×tamp=" + instant.timestampMs + + "&sdk_version=" + Countly.sharedInstance().COUNTLY_SDK_VERSION_STRING + + "&sdk_name=" + Countly.sharedInstance().COUNTLY_SDK_NAME; + } + private String prepareLocationData(boolean locationDisabled, String locationCountryCode, String locationCity, String locationGpsCoordinates, String locationIpAddress) { String data = ""; @@ -715,6 +726,14 @@ public String prepareFeedbackListRequest() { return data; } + @Override + public String prepareServerConfigRequest() { + String data = prepareCommonRequestDataShort() + + "&method=sc" + + "&device_id=" + UtilsNetworking.urlEncodeString(deviceIdProvider_.getDeviceId()); + return data; + } + /** * Ensures that an executor has been created for ConnectionProcessor instances to be submitted to. */ @@ -746,7 +765,7 @@ public void tick() { } public ConnectionProcessor createConnectionProcessor() { - return new ConnectionProcessor(baseInfoProvider.getServerURL(), storageProvider, deviceIdProvider_, sslContext_, requestHeaderCustomValues, L); + return new ConnectionProcessor(baseInfoProvider.getServerURL(), storageProvider, deviceIdProvider_, configProvider, sslContext_, requestHeaderCustomValues, L); } public boolean queueContainsTemporaryIdItems() { diff --git a/sdk/src/main/java/ly/count/android/sdk/Countly.java b/sdk/src/main/java/ly/count/android/sdk/Countly.java index facf05226..818474852 100644 --- a/sdk/src/main/java/ly/count/android/sdk/Countly.java +++ b/sdk/src/main/java/ly/count/android/sdk/Countly.java @@ -169,6 +169,7 @@ private static class SingletonHolder { ModuleRequestQueue moduleRequestQueue = null; ModuleAttribution moduleAttribution = null; ModuleUserProfile moduleUserProfile = null; + ModuleConfiguration moduleConfiguration = null; //reference to countly store CountlyStore countlyStore; @@ -492,6 +493,14 @@ public synchronized Countly init(CountlyConfig config) { }; } + if (config.immediateRequestGenerator == null) { + config.immediateRequestGenerator = new ImmediateRequestGenerator() { + @Override public ImmediateRequestI CreateImmediateRequestMaker() { + return (new ImmediateRequestMaker()); + } + }; + } + if (config.metricProviderOverride != null) { L.d("[Init] Custom metric provider was provided"); } @@ -529,6 +538,7 @@ public synchronized Countly init(CountlyConfig config) { } //initialise modules + moduleConfiguration = new ModuleConfiguration(this, config); moduleRequestQueue = new ModuleRequestQueue(this, config); moduleConsent = new ModuleConsent(this, config); moduleDeviceId = new ModuleDeviceId(this, config); @@ -545,6 +555,7 @@ public synchronized Countly init(CountlyConfig config) { moduleAttribution = new ModuleAttribution(this, config); modules.clear(); + modules.add(moduleConfiguration); modules.add(moduleRequestQueue); modules.add(moduleConsent); modules.add(moduleDeviceId); @@ -565,6 +576,7 @@ public synchronized Countly init(CountlyConfig config) { } //add missing providers + moduleConfiguration.consentProvider = config.consentProvider; moduleRequestQueue.consentProvider = config.consentProvider; moduleRequestQueue.deviceIdProvider = config.deviceIdProvider; moduleConsent.eventProvider = config.eventProvider; @@ -623,6 +635,7 @@ public synchronized Countly init(CountlyConfig config) { //initialize networking queues connectionQueue_.L = L; + connectionQueue_.configProvider = config.configProvider; connectionQueue_.consentProvider = moduleConsent; connectionQueue_.moduleRequestQueue = moduleRequestQueue; connectionQueue_.deviceInfo = config.deviceInfo; @@ -792,6 +805,7 @@ public synchronized void halt() { moduleLocation = null; moduleFeedback = null; moduleRequestQueue = null; + moduleConfiguration = null; COUNTLY_SDK_VERSION_STRING = DEFAULT_COUNTLY_SDK_VERSION_STRING; COUNTLY_SDK_NAME = DEFAULT_COUNTLY_SDK_NAME; diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java index a9dfaa3ed..a6872e8e9 100644 --- a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java @@ -33,10 +33,14 @@ public class CountlyConfig { protected BaseInfoProvider baseInfoProvider = null; + protected ConfigurationProvider configProvider = null; + protected SafeIDGenerator safeViewIDGenerator = null; protected SafeIDGenerator safeEventIDGenerator = null; + protected ImmediateRequestGenerator immediateRequestGenerator = null; + protected MetricProvider metricProviderOverride = null; protected DeviceInfo deviceInfo = null; @@ -194,7 +198,9 @@ public class CountlyConfig { String daCampaignData = null; Map iaAttributionValues = null; - public boolean explicitStorageModeEnabled = false; + boolean explicitStorageModeEnabled = false; + + boolean serverConfigurationEnabled = false; //SDK internal limits Integer maxKeyLength; @@ -914,4 +920,16 @@ public synchronized CountlyConfig enableExplicitStorageMode() { explicitStorageModeEnabled = true; return this; } + + /** + * This is an experimental feature and it can have breaking changes + * + * with this mode enable, the SDK will acquire additional configuration from it's Countly server + * + * @return + */ + public synchronized CountlyConfig enableServerConfiguration() { + serverConfigurationEnabled = true; + return this; + } } diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyStore.java b/sdk/src/main/java/ly/count/android/sdk/CountlyStore.java index f669a8954..367dd1913 100644 --- a/sdk/src/main/java/ly/count/android/sdk/CountlyStore.java +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyStore.java @@ -64,6 +64,7 @@ public class CountlyStore implements StorageProvider, EventQueueProvider { private static final String STORAGE_SCHEMA_VERSION = "SCHEMA_VERSION"; private static final String PREFERENCE_KEY_ID_ID = "ly.count.android.api.DeviceId.id"; private static final String PREFERENCE_KEY_ID_TYPE = "ly.count.android.api.DeviceId.type"; + private static final String PREFERENCE_SERVER_CONFIG = "SERVER_CONFIG"; private static final String CACHED_PUSH_ACTION_ID = "PUSH_ACTION_ID"; private static final String CACHED_PUSH_ACTION_INDEX = "PUSH_ACTION_INDEX"; @@ -78,6 +79,8 @@ public class CountlyStore implements StorageProvider, EventQueueProvider { ModuleLog L; + ConfigurationProvider configurationProvider; + int maxRequestQueueSize = 1000; //explicit storage fields @@ -109,6 +112,10 @@ public void setLimits(final int maxRequestQueueSize) { this.maxRequestQueueSize = maxRequestQueueSize; } + public void setConfigurationProvider(ConfigurationProvider configurationProvider) { + this.configurationProvider = configurationProvider; + } + static SharedPreferences createPreferencesPush(Context context) { return context.getSharedPreferences(PREFERENCES_PUSH, Context.MODE_PRIVATE); } @@ -222,6 +229,17 @@ public synchronized void esWriteCacheToStorage(@Nullable ExplicitStorageCallback } } + @Override + public void setServerConfig(String config) { + //PREFERENCE_SERVER_CONFIG + preferences_.edit().putString(PREFERENCE_SERVER_CONFIG, config).apply(); + } + + @Override + public String getServerConfig() { + return preferences_.getString(PREFERENCE_SERVER_CONFIG, null); + } + /** * Returns an unsorted array of the current stored connections. */ @@ -314,6 +332,11 @@ public synchronized String getEventsForRequestAndEmptyEventQueue() { */ @SuppressLint("ApplySharedPref") public synchronized void addRequest(@NonNull final String requestStr, final boolean writeInSync) { + if (configurationProvider != null && !configurationProvider.getTrackingEnabled()) { + L.w("[CountlyStore] addRequest, Tracking config is disabled, request will not be added to the request queue."); + return; + } + if (requestStr != null && requestStr.length() > 0) { final List connections = new ArrayList<>(Arrays.asList(getRequests())); @@ -375,6 +398,11 @@ public synchronized void replaceRequestList(final List newConns) { */ // TODO: void addEvent(final Event event) { + if (configurationProvider != null && !configurationProvider.getTrackingEnabled()) { + L.w("[CountlyStore] addEvent, Tracking config is disabled, event will not be added to the request queue."); + return; + } + final List events = getEventList(); if (events.size() < MAX_EVENTS) { events.add(event); @@ -648,6 +676,10 @@ public void setDataSchemaVersion(int version) { return true; } + if (preferences_.getString(PREFERENCE_SERVER_CONFIG, null) != null) { + return true; + } + if (preferencesPush_.getInt(CACHED_PUSH_MESSAGING_MODE, -100) != -100) { return true; } diff --git a/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestGenerator.java b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestGenerator.java new file mode 100644 index 000000000..0d4a48746 --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestGenerator.java @@ -0,0 +1,5 @@ +package ly.count.android.sdk; + +interface ImmediateRequestGenerator { + ImmediateRequestI CreateImmediateRequestMaker(); +} diff --git a/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestI.java b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestI.java new file mode 100644 index 000000000..b2dda5987 --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestI.java @@ -0,0 +1,5 @@ +package ly.count.android.sdk; + +interface ImmediateRequestI { + void doWork(String requestData, String customEndpoint, ConnectionProcessor cp, boolean requestShouldBeDelayed, boolean networkingIsEnabled, ImmediateRequestMaker.InternalImmediateRequestCallback callback, ModuleLog log); +} diff --git a/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestMaker.java b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestMaker.java index d571e700a..d09b8f788 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestMaker.java +++ b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestMaker.java @@ -11,33 +11,47 @@ /** * Async task for making immediate server requests */ -class ImmediateRequestMaker extends AsyncTask { +class ImmediateRequestMaker extends AsyncTask implements ImmediateRequestI { /** * Used for callback from async task */ - protected interface InternalFeedbackRatingCallback { + protected interface InternalImmediateRequestCallback { void callback(JSONObject checkResponse); } - InternalFeedbackRatingCallback callback; + InternalImmediateRequestCallback callback; ModuleLog L; + @Override + public void doWork(String requestData, String customEndpoint, ConnectionProcessor cp, boolean requestShouldBeDelayed, boolean networkingIsEnabled, InternalImmediateRequestCallback callback, ModuleLog log) { + this.execute(requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log); + } + /** * params fields: * 0 - requestData * 1 - custom endpoint * 2 - connection processor * 3 - requestShouldBeDelayed - * 4 - callback + * 4 - networkingIsEnabled + * 5 - callback + * 6 - log module */ protected JSONObject doInBackground(Object... params) { final String requestData = (String) params[0]; final String customEndpoint = (String) params[1]; final ConnectionProcessor cp = (ConnectionProcessor) params[2]; final boolean requestShouldBeDelayed = (boolean) params[3]; - callback = (InternalFeedbackRatingCallback) params[4]; - L = (ModuleLog) params[5]; + final boolean networkingIsEnabled = (boolean) params[4]; + callback = (InternalImmediateRequestCallback) params[5]; + L = (ModuleLog) params[6]; + + if (!networkingIsEnabled) { + L.w("[ImmediateRequestMaker] ImmediateRequestMaker, Networking config is disabled, request cancelled. Endpoint[" + customEndpoint + "] request[" + requestData + "]"); + + return null; + } L.v("[ImmediateRequestMaker] Starting request"); diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java index 65521d3c3..291dc2c5b 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java @@ -15,6 +15,7 @@ abstract class ModuleBase { DeviceIdProvider deviceIdProvider; BaseInfoProvider baseInfoProvider; ViewIdProvider viewIdProvider; + ConfigurationProvider configProvider; DeviceInfo deviceInfo; @@ -28,6 +29,7 @@ abstract class ModuleBase { deviceIdProvider = config.deviceIdProvider; baseInfoProvider = config.baseInfoProvider; viewIdProvider = config.viewIdProvider; + configProvider = config.configProvider; deviceInfo = config.deviceInfo; } @@ -77,12 +79,19 @@ void callbackOnActivitySaveInstanceState(Activity activity) { void callbackOnActivityDestroyed(Activity activity) { } + //notify the SDK modules that the device ID has changed void deviceIdChanged() { } + //notify the SDK modules that consent was updated void onConsentChanged(@NonNull final List consentChangeDelta, final boolean newConsent, @NonNull final ModuleConsent.ConsentChangeSource changeSource) { } + //notify the SDK modules that internal configuration was updated + void sdkConfigurationChanged() { + + } + void initFinished(@NonNull CountlyConfig config) { } } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java new file mode 100644 index 000000000..69f6daa8c --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java @@ -0,0 +1,214 @@ +package ly.count.android.sdk; + +import androidx.annotation.NonNull; +import org.json.JSONException; +import org.json.JSONObject; + +class ModuleConfiguration extends ModuleBase implements ConfigurationProvider { + ImmediateRequestGenerator immediateRequestGenerator; + + boolean serverConfigEnabled = false; + + JSONObject latestRetrievedConfigurationFull = null; + JSONObject latestRetrievedConfiguration = null; + + //config keys + final static String keyTracking = "tracking"; + final static String keyNetworking = "networking"; + + //request keys + final static String keyRTimestamp = "t"; + final static String keyRVersion = "v"; + final static String keyRConfig = "c"; + + final static boolean defaultVTracking = true; + final static boolean defaultVNetworking = true; + + boolean currentVTracking = true; + boolean currentVNetworking = true; + + ModuleConfiguration(@NonNull Countly cly, @NonNull CountlyConfig config) { + super(cly, config); + L.v("[ModuleConfiguration] Initialising"); + config.configProvider = this; + configProvider = this; + + serverConfigEnabled = config.serverConfigurationEnabled; + + immediateRequestGenerator = config.immediateRequestGenerator; + + config.countlyStore.setConfigurationProvider(this); + + if (serverConfigEnabled) { + //load the previously saved configuration + loadConfigFromStorage(); + + //update the config variables according to the new state + updateConfigVariables(); + } + } + + @Override + void initFinished(@NonNull final CountlyConfig config) { + if (serverConfigEnabled) { + //once the SDK has loaded, init fetching the server config + fetchConfigFromServer(); + } + } + + @Override + void halt() { + + } + + /** + * Reads from storage to local json objects + */ + void loadConfigFromStorage() { + String sConfig = storageProvider.getServerConfig(); + L.v("[ModuleConfiguration] loadConfigFromStorage, [" + sConfig + "]"); + + if (sConfig == null || sConfig.isEmpty()) { + L.d("[ModuleConfiguration] loadStoredConfig, no configs persistently stored"); + return; + } + + try { + latestRetrievedConfigurationFull = new JSONObject(sConfig); + latestRetrievedConfiguration = latestRetrievedConfigurationFull.getJSONObject(keyRConfig); + L.d("[ModuleConfiguration] loadStoredConfig, stored config loaded [" + sConfig + "]"); + } catch (JSONException e) { + L.w("[ModuleConfiguration] loadStoredConfig, failed to parse, " + e); + + latestRetrievedConfigurationFull = null; + latestRetrievedConfiguration = null; + } + } + + //update the config variables according to the current config obj state + void updateConfigVariables() { + L.v("[ModuleConfiguration] updateConfigVariables"); + //set all to defaults + currentVNetworking = defaultVNetworking; + currentVTracking = defaultVTracking; + + if (latestRetrievedConfiguration == null) { + //no config, don't continue + return; + } + + //networking + if (latestRetrievedConfiguration.has(keyNetworking)) { + try { + currentVNetworking = latestRetrievedConfiguration.getBoolean(keyNetworking); + } catch (JSONException e) { + L.w("[ModuleConfiguration] updateConfigs, failed to load 'networking', " + e); + } + } + + //tracking + if (latestRetrievedConfiguration.has(keyTracking)) { + try { + currentVTracking = latestRetrievedConfiguration.getBoolean(keyTracking); + } catch (JSONException e) { + L.w("[ModuleConfiguration] updateConfigs, failed to load 'tracking', " + e); + } + } + } + + void saveAndStoreDownloadedConfig(@NonNull JSONObject config) { + L.v("[ModuleConfiguration] saveAndStoreDownloadedConfig"); + if (!config.has(keyRVersion)) { + L.w("[ModuleConfiguration] saveAndStoreDownloadedConfig, Retrieved configuration does not has a 'version' field. Config will be ignored."); + return; + } + + if (!config.has(keyRTimestamp)) { + L.w("[ModuleConfiguration] saveAndStoreDownloadedConfig, Retrieved configuration does not has a 'timestamp' field. Config will be ignored."); + return; + } + + if (!config.has(keyRConfig)) { + L.w("[ModuleConfiguration] saveAndStoreDownloadedConfig, Retrieved configuration does not has a 'configuration' field. Config will be ignored."); + return; + } + + //at this point it is a valid response + latestRetrievedConfigurationFull = config; + String configAsString = null; + + try { + latestRetrievedConfiguration = config.getJSONObject(keyRConfig); + configAsString = config.toString(); + } catch (JSONException e) { + latestRetrievedConfigurationFull = null; + latestRetrievedConfiguration = null; + + L.w("[ModuleConfiguration] saveAndStoreDownloadedConfig, Failed retrieving internal config, " + e); + return; + } + + //save to storage + storageProvider.setServerConfig(configAsString); + + //update config variables + updateConfigVariables(); + } + + /** + * Perform network request for retrieving latest config + * If valid config is downloaded, save it, and update the values + * + * Example response: + * { + * "v":1, + * "t":1681808287464, + * "c":{ + * "tracking":false, + * "networking":false, + * "crashes":false, + * "views":false, + * "heartbeat":61, + * "event_queue":11, + * "request_queue":1001 + * } + * } + */ + void fetchConfigFromServer() { + L.v("[ModuleConfiguration] fetchConfigFromServer"); + String requestData = requestQueueProvider.prepareServerConfigRequest(); + ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor(); + + immediateRequestGenerator.CreateImmediateRequestMaker().doWork(requestData, "/o/sdk", cp, false, true, new ImmediateRequestMaker.InternalImmediateRequestCallback() { + @Override public void callback(JSONObject checkResponse) { + if (checkResponse == null) { + L.w("[ModuleConfiguration] Not possible to retrieve configuration data. Probably due to lack of connection to the server"); + return; + } + + L.d("[ModuleConfiguration] Retrieved configuration response: [" + checkResponse.toString() + "]"); + + saveAndStoreDownloadedConfig(checkResponse); + } + }, L); + } + + // configuration getters + + @Override + public boolean getNetworkingEnabled() { + if (!serverConfigEnabled) { + return defaultVNetworking; + } + + return currentVNetworking; + } + + @Override + public boolean getTrackingEnabled() { + if (!serverConfigEnabled) { + return defaultVTracking; + } + return currentVTracking; + } +} diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java index 5358cadae..55143a658 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java @@ -79,10 +79,11 @@ void getAvailableFeedbackWidgetsInternal(final RetrieveFeedbackWidgets devCallba } ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor(); + final boolean networkingIsEnabled = cp.configProvider_.getNetworkingEnabled(); String requestData = requestQueueProvider.prepareFeedbackListRequest(); - (new ImmediateRequestMaker()).execute(requestData, "/o/sdk", cp, false, new ImmediateRequestMaker.InternalFeedbackRatingCallback() { + (new ImmediateRequestMaker()).doWork(requestData, "/o/sdk", cp, false, networkingIsEnabled, new ImmediateRequestMaker.InternalImmediateRequestCallback() { @Override public void callback(JSONObject checkResponse) { if (checkResponse == null) { L.d("[ModuleFeedback] Not possible to retrieve widget list. Probably due to lack of connection to the server"); @@ -391,11 +392,12 @@ void getFeedbackWidgetDataInternal(@Nullable CountlyFeedbackWidget widgetInfo, @ requestData.append(cachedAppVersion); ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor(); + final boolean networkingIsEnabled = cp.configProvider_.getNetworkingEnabled(); String requestDataStr = requestData.toString(); L.d("[ModuleFeedback] Using following request params for retrieving widget data:[" + requestDataStr + "]"); - (new ImmediateRequestMaker()).execute(requestDataStr, widgetDataEndpoint, cp, false, new ImmediateRequestMaker.InternalFeedbackRatingCallback() { + (new ImmediateRequestMaker()).doWork(requestDataStr, widgetDataEndpoint, cp, false, networkingIsEnabled, new ImmediateRequestMaker.InternalImmediateRequestCallback() { @Override public void callback(JSONObject checkResponse) { if (checkResponse == null) { L.d("[ModuleFeedback] Not possible to retrieve widget data. Probably due to lack of connection to the server"); diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java b/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java index 8b9faac80..715c63999 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java @@ -473,8 +473,9 @@ synchronized void showFeedbackPopupInternal(@Nullable final String widgetId, @Nu L.d("[ModuleRatings] rating widget url :[" + ratingWidgetUrl + "]"); ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor(); + final boolean networkingIsEnabled = cp.configProvider_.getNetworkingEnabled(); - (new ImmediateRequestMaker()).execute(requestData, "/o/feedback/widget", cp, false, new ImmediateRequestMaker.InternalFeedbackRatingCallback() { + (new ImmediateRequestMaker()).doWork(requestData, "/o/feedback/widget", cp, false, networkingIsEnabled, new ImmediateRequestMaker.InternalImmediateRequestCallback() { @Override public void callback(JSONObject checkResponse) { if (checkResponse == null) { diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleRemoteConfig.java b/sdk/src/main/java/ly/count/android/sdk/ModuleRemoteConfig.java index c94585143..1e43a7377 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleRemoteConfig.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleRemoteConfig.java @@ -89,8 +89,9 @@ void updateRemoteConfigValues(@Nullable final String[] keysOnly, @Nullable final L.d("[ModuleRemoteConfig] RemoteConfig requestData:[" + requestData + "]"); ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor(); + final boolean networkingIsEnabled = cp.configProvider_.getNetworkingEnabled(); - (new ImmediateRequestMaker()).execute(requestData, "/o/sdk", cp, requestShouldBeDelayed, new ImmediateRequestMaker.InternalFeedbackRatingCallback() { + (new ImmediateRequestMaker()).doWork(requestData, "/o/sdk", cp, requestShouldBeDelayed, networkingIsEnabled, new ImmediateRequestMaker.InternalImmediateRequestCallback() { @Override public void callback(JSONObject checkResponse) { L.d("[ModuleRemoteConfig] Processing remote config received response, received response is null:[" + (checkResponse == null) + "]"); diff --git a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java index 006a042de..29eb4c14f 100644 --- a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java @@ -55,4 +55,6 @@ interface RequestQueueProvider { String prepareRatingWidgetRequest(String widgetId); String prepareFeedbackListRequest(); + + String prepareServerConfigRequest(); } diff --git a/sdk/src/main/java/ly/count/android/sdk/StorageProvider.java b/sdk/src/main/java/ly/count/android/sdk/StorageProvider.java index eec5ee8ae..41989b9b3 100644 --- a/sdk/src/main/java/ly/count/android/sdk/StorageProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/StorageProvider.java @@ -51,6 +51,10 @@ interface StorageProvider { void esWriteCacheToStorage(@Nullable ExplicitStorageCallback callback);//required for explicit storage + void setServerConfig(String config); + + String getServerConfig(); + //fields for data migration int getDataSchemaVersion();