diff --git a/build.gradle b/build.gradle index 7eb29e1da..5b9545e55 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.1.1' + splitVersion = '5.1.2-rc1' } android { @@ -25,6 +25,10 @@ android { targetCompatibility = '1.8' sourceCompatibility = '1.8' + buildFeatures { + buildConfig true + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/src/androidTest/java/tests/database/DatabaseInitializationTest.java b/src/androidTest/java/tests/database/DatabaseInitializationTest.java index a4201e1a9..a0cac7e48 100644 --- a/src/androidTest/java/tests/database/DatabaseInitializationTest.java +++ b/src/androidTest/java/tests/database/DatabaseInitializationTest.java @@ -187,6 +187,8 @@ private static String[] getDbList(Context context) { } catch (InterruptedException e) { throw new RuntimeException(e); } - return Arrays.stream(context.databaseList()).filter(db -> !db.endsWith("-journal")).toArray(String[]::new); + return Arrays.stream(context.databaseList()) + .filter(db -> !db.endsWith("-journal") && !db.endsWith("-wal") && !db.endsWith("-shm")) + .toArray(String[]::new); } } diff --git a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java index be821fde0..81cbc29de 100644 --- a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java +++ b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java @@ -181,6 +181,8 @@ private void test(long timestampDaysAgo, RolloutCacheConfiguration.Builder confi private void verify(SplitFactory factory, CountDownLatch readyLatch, List initialFlags, List initialSegments, List initialLargeSegments, long initialChangeNumber) throws InterruptedException { // Track final values + Thread.sleep(10000); + List finalFlags = mRoomDb.splitDao().getAll(); List finalSegments = mRoomDb.mySegmentDao().getAll(); List finalLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); diff --git a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java index 30b748d0d..3b70f4a82 100644 --- a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java +++ b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java @@ -153,7 +153,7 @@ public void testCleanUp() throws IOException, InterruptedException { mClient.on(SplitEvent.SDK_READY, readyTask); latch.await(40, TimeUnit.SECONDS); - Thread.sleep(1000); + Thread.sleep(8000); // Load all records again after cleanup List remainingKeys = mUniqueKeysDao.getBy(0, StorageRecordStatus.ACTIVE, 10); diff --git a/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java b/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java index 2066bb2cd..7af0d2802 100644 --- a/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java +++ b/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import helper.DatabaseHelper; import io.split.android.client.dtos.Split; @@ -32,6 +33,8 @@ public class PersistentSplitsStorageTest { SplitRoomDatabase mRoomDb; Context mContext; PersistentSplitsStorage mPersistentSplitsStorage; + private final Map> mFlagSets = new HashMap<>(); + private final Map mTrafficTypes = new HashMap<>(); @Before public void setUp() { @@ -94,7 +97,7 @@ public void addSplits() { split.status = Status.ACTIVE; splits.add(split); } - mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, new ArrayList<>(), 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, new ArrayList<>(), 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -114,7 +117,7 @@ public void addSplits() { @Test public void updateEmptySplit() { List splits = new ArrayList<>(); - mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, splits, 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, splits, 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -134,7 +137,7 @@ public void updateEmptySplit() { @Test public void addNullSplitList() { List splits = new ArrayList<>(); - boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits,1L, 0L)); + boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits,1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -155,7 +158,7 @@ public void addNullSplitList() { @Test public void deleteNullSplitList() { List splits = new ArrayList<>(); - boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, null,1L, 0L)); + boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, null,1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -183,7 +186,7 @@ public void deleteSplits() { split.status = Status.ARCHIVED; splits.add(split); } - mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -220,7 +223,7 @@ public void deleteAllSplits() { split.status = Status.ARCHIVED; splits.add(split); } - mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); List loadedSlits = snapshot.getSplits(); diff --git a/src/androidTest/java/tests/storage/SplitsStorageTest.java b/src/androidTest/java/tests/storage/SplitsStorageTest.java index d5370f911..d68127cdb 100644 --- a/src/androidTest/java/tests/storage/SplitsStorageTest.java +++ b/src/androidTest/java/tests/storage/SplitsStorageTest.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -35,6 +36,7 @@ import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.storage.splits.SplitsStorageImpl; import io.split.android.client.storage.splits.SqLitePersistentSplitsStorage; +import io.split.android.client.utils.Json; public class SplitsStorageTest { @@ -334,6 +336,11 @@ public void trafficTypesAreLoadedInMemoryWhenLoadingLocalSplits() { mRoomDb.clearAllTables(); mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type"), newSplitEntity("split_test_2", "test_type_2"))); + Map trafficTypes = new HashMap<>(); + trafficTypes.put("test_type", 1); + trafficTypes.put("test_type_2", 1); + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, Json.toJson(trafficTypes))); + mSplitsStorage.loadLocal(); assertTrue(mSplitsStorage.isValidTrafficType("test_type")); @@ -346,6 +353,11 @@ public void loadedFromStorageTrafficTypesAreCorrectlyUpdated() { mRoomDb.clearAllTables(); mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type"), newSplitEntity("split_test_2", "test_type_2"))); + Map trafficTypes = new HashMap<>(); + trafficTypes.put("test_type", 1); + trafficTypes.put("test_type_2", 1); + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, Json.toJson(trafficTypes))); + mSplitsStorage.loadLocal(); Split updatedSplit = newSplit("split_test", Status.ACTIVE, "new_type"); @@ -365,6 +377,12 @@ public void flagSetsAreUpdatedWhenCallingLoadLocal() { newSplitEntity("split_test_3", "test_type_2", Collections.singleton("set_2")), newSplitEntity("split_test_4", "test_type_2", Collections.singleton("set_1")))); + Map> flagSets = new HashMap<>(); + flagSets.put("set_1", new HashSet<>(Arrays.asList("split_test", "split_test_4"))); + flagSets.put("set_2", new HashSet<>(Arrays.asList("split_test_2", "split_test_3"))); + + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, Json.toJson(flagSets))); + mSplitsStorage.loadLocal(); Assert.assertEquals(new HashSet<>(Arrays.asList("split_test", "split_test_4")), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1"))); @@ -378,6 +396,13 @@ public void flagSetsAreRemovedWhenUpdating() { newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")), newSplitEntity("split_test_3", "test_type_2", Collections.singleton("set_2")))); + + Map> flagSets = new HashMap<>(); + flagSets.put("set_1", new HashSet<>(Arrays.asList("split_test"))); + flagSets.put("set_2", new HashSet<>(Arrays.asList("split_test_2", "split_test_3"))); + + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, Json.toJson(flagSets))); + mSplitsStorage.loadLocal(); Set initialSet1 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1")); @@ -396,6 +421,13 @@ public void flagSetsAreRemovedWhenUpdating() { public void updateWithoutChecksRemovesFromFlagSet() { mRoomDb.clearAllTables(); mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")))); + + Map> flagSets = new HashMap<>(); + flagSets.put("set_1", new HashSet<>(Arrays.asList("split_test"))); + flagSets.put("set_2", new HashSet<>(Arrays.asList("split_test_2"))); + + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, Json.toJson(flagSets))); + mSplitsStorage.loadLocal(); Set initialSet1 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1")); diff --git a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java index 544aab48b..2e7109b08 100644 --- a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java @@ -45,7 +45,7 @@ public class SplitClientFactoryImpl implements SplitClientFactory { private final TreatmentManagerFactory mTreatmentManagerFactory; private final ImpressionListener.FederatedImpressionListener mCustomerImpressionListener; private final SplitValidatorImpl mSplitValidator; - private final EventsTracker mEventsTracker; + private final SplitFactoryImpl.EventsTrackerProvider mEventsTrackerProvider; public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, @NonNull SplitClientContainer clientContainer, @@ -56,7 +56,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull ValidationMessageLogger validationLogger, @NonNull KeyValidator keyValidator, - @NonNull EventsTracker eventsTracker, + @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTrackerProvider, @NonNull ImpressionListener.FederatedImpressionListener customerImpressionListener, @Nullable FlagSetsFilter flagSetsFilter) { mSplitFactory = checkNotNull(splitFactory); @@ -67,7 +67,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, mStorageContainer = checkNotNull(storageContainer); mTelemetrySynchronizer = checkNotNull(telemetrySynchronizer); mCustomerImpressionListener = checkNotNull(customerImpressionListener); - mEventsTracker = checkNotNull(eventsTracker); + mEventsTrackerProvider = checkNotNull(eventsTrackerProvider); mAttributesManagerFactory = getAttributesManagerFactory(config.persistentAttributesEnabled(), validationLogger, @@ -106,7 +106,7 @@ public SplitClient getClient(@NonNull Key key, mCustomerImpressionListener, mConfig, eventsManager, - mEventsTracker, + mEventsTrackerProvider.getEventsTracker(), mAttributesManagerFactory.getManager(key.matchingKey(), attributesStorage), mSplitValidator, mTreatmentManagerFactory.getTreatmentManager(key, diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index be33b61c3..770cc3249 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -98,7 +98,6 @@ class SplitFactoryHelper { private static final int DB_MAGIC_CHARS_COUNT = 4; String getDatabaseName(SplitClientConfig config, String apiToken, Context context) { - String dbName = buildDatabaseName(config, apiToken); File dbPath = context.getDatabasePath(dbName); if (dbPath.exists()) { @@ -162,7 +161,8 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, SplitCipher splitCipher, TelemetryStorage telemetryStorage, long observerCacheExpirationPeriod, - ScheduledThreadPoolExecutor impressionsObserverExecutor) { + ScheduledThreadPoolExecutor impressionsObserverExecutor, + SplitsStorage splitsStorage) { boolean isPersistenceEnabled = userConsentStatus == UserConsent.GRANTED; PersistentEventsStorage persistentEventsStorage = @@ -170,7 +170,7 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, PersistentImpressionsStorage persistentImpressionsStorage = StorageFactory.getPersistentImpressionsStorage(splitRoomDatabase, splitCipher); return new SplitStorageContainer( - StorageFactory.getSplitsStorage(splitRoomDatabase, splitCipher), + splitsStorage, StorageFactory.getMySegmentsStorage(splitRoomDatabase, splitCipher), StorageFactory.getMyLargeSegmentsStorage(splitRoomDatabase, splitCipher), StorageFactory.getPersistentSplitsStorage(splitRoomDatabase, splitCipher), @@ -504,7 +504,6 @@ static class Initializer implements Runnable { this(new RolloutCacheManagerImpl(config, storageContainer, - splitTaskFactory.createCleanUpDatabaseTask(System.currentTimeMillis() / 1000), splitTaskFactory.createEncryptionMigrationTask(apiToken, splitDatabase, config.encryptionEnabled(), splitCipher)), new Listener(eventsManagerCoordinator, splitTaskExecutor, splitSingleThreadTaskExecutor, syncManager, lifecycleManager, initLock), initLock); @@ -550,13 +549,13 @@ static class Listener implements SplitTaskExecutionListener { @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { try { - mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - mSplitTaskExecutor.resume(); mSplitSingleThreadTaskExecutor.resume(); + mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); mSyncManager.start(); mLifecycleManager.register(mSyncManager); + Logger.i("Android SDK initialized!"); } catch (Exception e) { Logger.e("Error initializing Android SDK", e); diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index ea65577f1..9fb574d7f 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -32,6 +32,7 @@ import io.split.android.client.lifecycle.SplitLifecycleManagerImpl; import io.split.android.client.network.HttpClient; import io.split.android.client.network.HttpClientImpl; +import io.split.android.client.service.CleanUpDatabaseTask; import io.split.android.client.service.SplitApiFacade; import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor; import io.split.android.client.service.executor.SplitTaskExecutor; @@ -57,6 +58,8 @@ import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.StorageFactory; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.TelemetrySynchronizer; import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.utils.logger.Logger; @@ -88,9 +91,14 @@ public class SplitFactoryImpl implements SplitFactory { private final SplitStorageContainer mStorageContainer; private final SplitClientContainer mClientContainer; - private final UserConsentManager mUserConsentManager; + private volatile UserConsentManager mUserConsentManager; private final ReentrantLock mInitLock = new ReentrantLock(); + private final EventsTrackerProvider mEventsTrackerProvider; + private final StrategyImpressionManager mImpressionManager; + private final SplitTaskExecutor mSplitTaskExecutor; + private final SplitClientConfig mConfig; + public SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull SplitClientConfig config, @NonNull Context context) throws URISyntaxException { this(apiToken, key, config, context, @@ -113,23 +121,6 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp KeyValidator keyValidator = new KeyValidatorImpl(); ValidationMessageLogger validationLogger = new ValidationMessageLoggerImpl(); - HttpClient defaultHttpClient; - if (httpClient == null) { - HttpClientImpl.Builder builder = new HttpClientImpl.Builder() - .setConnectionTimeout(config.connectionTimeout()) - .setReadTimeout(config.readTimeout()) - .setProxy(config.proxy()) - .setDevelopmentSslConfig(config.developmentSslConfig()) - .setContext(context) - .setProxyAuthenticator(config.authenticator()); - if (config.certificatePinningConfiguration() != null) { - builder.setCertificatePinningConfiguration(config.certificatePinningConfiguration()); - } - - defaultHttpClient = builder.build(); - } else { - defaultHttpClient = httpClient; - } ValidationErrorInfo errorInfo = keyValidator.validate(key.matchingKey(), key.bucketingKey()); String validationTag = "factory instantiation"; if (errorInfo != null) { @@ -161,27 +152,46 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp splitDatabase = testDatabase; Logger.d("Using test database"); } - - defaultHttpClient.addHeaders(factoryHelper.buildHeaders(config, apiToken)); - defaultHttpClient.addStreamingHeaders(factoryHelper.buildStreamingHeaders(apiToken)); - - SplitTaskExecutor splitTaskExecutor = new SplitTaskExecutorImpl(); - splitTaskExecutor.pause(); - - EventsManagerCoordinator mEventsManagerCoordinator = new EventsManagerCoordinator(); - + mConfig = config; SplitCipher splitCipher = factoryHelper.getCipher(apiToken, config.encryptionEnabled()); + SplitsStorage splitsStorage = getSplitsStorage(splitDatabase, splitCipher); + ScheduledThreadPoolExecutor impressionsObserverExecutor = new ScheduledThreadPoolExecutor(1, new ThreadPoolExecutor.CallerRunsPolicy()); + mStorageContainer = factoryHelper.buildStorageContainer(config.userConsent(), - splitDatabase, config.shouldRecordTelemetry(), splitCipher, telemetryStorage, config.observerCacheExpirationPeriod(), impressionsObserverExecutor); + splitDatabase, config.shouldRecordTelemetry(), splitCipher, telemetryStorage, config.observerCacheExpirationPeriod(), impressionsObserverExecutor, splitsStorage); + + mSplitTaskExecutor = new SplitTaskExecutorImpl(); + mSplitTaskExecutor.pause(); + + EventsManagerCoordinator mEventsManagerCoordinator = new EventsManagerCoordinator(); Pair, String> filtersConfig = factoryHelper.getFilterConfiguration(config.syncConfig()); Map filters = filtersConfig.first; String splitsFilterQueryStringFromConfig = filtersConfig.second; String flagsSpec = getFlagsSpec(testingConfig); + HttpClient defaultHttpClient; + if (httpClient == null) { + HttpClientImpl.Builder builder = new HttpClientImpl.Builder() + .setConnectionTimeout(config.connectionTimeout()) + .setReadTimeout(config.readTimeout()) + .setProxy(config.proxy()) + .setDevelopmentSslConfig(config.developmentSslConfig()) + .setContext(context) + .setProxyAuthenticator(config.authenticator()); + if (config.certificatePinningConfiguration() != null) { + builder.setCertificatePinningConfiguration(config.certificatePinningConfiguration()); + } + + defaultHttpClient = builder.build(); + } else { + defaultHttpClient = httpClient; + } + defaultHttpClient.addHeaders(factoryHelper.buildHeaders(config, apiToken)); + defaultHttpClient.addStreamingHeaders(factoryHelper.buildStreamingHeaders(apiToken)); SplitApiFacade splitApiFacade = factoryHelper.buildApiFacade( config, defaultHttpClient, splitsFilterQueryStringFromConfig); @@ -192,19 +202,22 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp getFlagsSpec(testingConfig), mEventsManagerCoordinator, filters, flagSetsFilter, testingConfig); WorkManagerWrapper workManagerWrapper = factoryHelper.buildWorkManagerWrapper(context, config, apiToken, databaseName, filters); + SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor = new SplitSingleThreadTaskExecutor(); splitSingleThreadTaskExecutor.pause(); - ImpressionStrategyProvider impressionStrategyProvider = factoryHelper.getImpressionStrategyProvider(splitTaskExecutor, splitTaskFactory, mStorageContainer, config); + ImpressionStrategyProvider impressionStrategyProvider = factoryHelper.getImpressionStrategyProvider(mSplitTaskExecutor, splitTaskFactory, mStorageContainer, config); Pair noneComponents = impressionStrategyProvider.getNoneComponents(); - StrategyImpressionManager impressionManager = new StrategyImpressionManager(noneComponents, impressionStrategyProvider.getStrategy(config.impressionsMode())); + + mImpressionManager = new StrategyImpressionManager(noneComponents, impressionStrategyProvider.getStrategy(config.impressionsMode())); final RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory = new RetryBackoffCounterTimerFactory(); - StreamingComponents streamingComponents = factoryHelper.buildStreamingComponents(splitTaskExecutor, + StreamingComponents streamingComponents = factoryHelper.buildStreamingComponents(mSplitTaskExecutor, splitTaskFactory, config, defaultHttpClient, splitApiFacade, mStorageContainer, flagsSpec); + Synchronizer mSynchronizer = new SynchronizerImpl( config, - splitTaskExecutor, + mSplitTaskExecutor, splitSingleThreadTaskExecutor, splitTaskFactory, workManagerWrapper, @@ -212,7 +225,7 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp mStorageContainer.getTelemetryStorage(), new AttributesSynchronizerRegistryImpl(), new MySegmentsSynchronizerRegistryImpl(), - impressionManager, + mImpressionManager, mStorageContainer.getEventsStorage(), mEventsManagerCoordinator, streamingComponents.getPushManagerEventBroadcaster() @@ -225,18 +238,18 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp CompressionUtilProvider compressionProvider = new CompressionUtilProvider(); - TelemetrySynchronizer telemetrySynchronizer = factoryHelper.getTelemetrySynchronizer(splitTaskExecutor, + TelemetrySynchronizer telemetrySynchronizer = factoryHelper.getTelemetrySynchronizer(mSplitTaskExecutor, splitTaskFactory, config.telemetryRefreshRate(), config.shouldRecordTelemetry()); mSyncManager = factoryHelper.buildSyncManager( config, - splitTaskExecutor, + mSplitTaskExecutor, mSynchronizer, telemetrySynchronizer, streamingComponents.getPushNotificationManager(), streamingComponents.getPushManagerEventBroadcaster(), factoryHelper.getSplitUpdatesWorker(config, - splitTaskExecutor, + mSplitTaskExecutor, splitTaskFactory, mSynchronizer, streamingComponents.getSplitsUpdateNotificationQueue(), @@ -262,22 +275,22 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp } else { customerImpressionListener = new ImpressionListener.FederatedImpressionListener(splitImpressionListener, impressionListeners); } - EventsTracker eventsTracker = buildEventsTracker(); - mUserConsentManager = new UserConsentManagerImpl(config, - mStorageContainer.getImpressionsStorage(), - mStorageContainer.getEventsStorage(), - mSyncManager, eventsTracker, impressionManager, splitTaskExecutor); - ClientComponentsRegister componentsRegister = factoryHelper.getClientComponentsRegister(config, splitTaskExecutor, + mEventsTrackerProvider = new EventsTrackerProvider(mStorageContainer.getSplitsStorage(), + mStorageContainer.getTelemetryStorage(), mSyncManager); + + ClientComponentsRegister componentsRegister = factoryHelper.getClientComponentsRegister(config, mSplitTaskExecutor, mEventsManagerCoordinator, mSynchronizer, streamingComponents.getNotificationParser(), streamingComponents.getNotificationProcessor(), streamingComponents.getSseAuthenticator(), mStorageContainer, mSyncManager, compressionProvider); + mClientContainer = new SplitClientContainerImpl( mDefaultClientKey.matchingKey(), this, config, mSyncManager, - telemetrySynchronizer, mStorageContainer, splitTaskExecutor, splitApiFacade, + telemetrySynchronizer, mStorageContainer, mSplitTaskExecutor, splitApiFacade, validationLogger, keyValidator, customerImpressionListener, streamingComponents.getPushNotificationManager(), componentsRegister, workManagerWrapper, - eventsTracker, flagSetsFilter); + mEventsTrackerProvider, flagSetsFilter); + mDestroyer = new Runnable() { public void run() { mInitLock.lock(); @@ -307,7 +320,7 @@ public void run() { Logger.d("Successful shutdown of httpclient"); mManager.destroy(); Logger.d("Successful shutdown of manager"); - splitTaskExecutor.stop(); + mSplitTaskExecutor.stop(); splitSingleThreadTaskExecutor.stop(); Logger.d("Successful shutdown of task executor"); mStorageContainer.getAttributesStorageContainer().destroy(); @@ -329,7 +342,6 @@ public void run() { SplitFactoryImpl.this.destroy(); } }); - // Set up async initialization final SplitFactoryHelper.Initializer initializer = new SplitFactoryHelper.Initializer(apiToken, config, @@ -337,7 +349,7 @@ public void run() { splitDatabase, splitCipher, mEventsManagerCoordinator, - splitTaskExecutor, + mSplitTaskExecutor, splitSingleThreadTaskExecutor, mStorageContainer, mSyncManager, @@ -353,13 +365,20 @@ public void run() { // Run initializer new Thread(initializer).start(); + CleanUpDatabaseTask cleanUpDatabaseTask = splitTaskFactory.createCleanUpDatabaseTask(System.currentTimeMillis() / 1000); + mSplitTaskExecutor.schedule(cleanUpDatabaseTask, 5L, null); + // Initialize default client client(); SplitParser mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); mManager = new SplitManagerImpl( mStorageContainer.getSplitsStorage(), new SplitValidatorImpl(), mSplitParser); + } + @NonNull + private static SplitsStorage getSplitsStorage(SplitRoomDatabase splitDatabase, SplitCipher splitCipher) { + return StorageFactory.getSplitsStorage(splitDatabase, splitCipher); } private static String getFlagsSpec(@Nullable TestingConfig testingConfig) { @@ -427,16 +446,31 @@ public void flush() { @Override public void setUserConsent(boolean enabled) { UserConsent newMode = (enabled ? UserConsent.GRANTED : UserConsent.DECLINED); - if (mUserConsentManager == null) { + if (getUserConsentManager() == null) { Logger.e("User consent manager not initialized. Unable to set mode " + newMode.toString()); return; } - mUserConsentManager.setStatus(newMode); + getUserConsentManager().setStatus(newMode); + } + + private UserConsentManager getUserConsentManager() { + if (mUserConsentManager == null) { + synchronized (mConfig) { + if (mUserConsentManager == null) { + mUserConsentManager = new UserConsentManagerImpl(mConfig, + mStorageContainer.getImpressionsStorage(), + mStorageContainer.getEventsStorage(), + mSyncManager, mEventsTrackerProvider, mImpressionManager, mSplitTaskExecutor); + } + } + } + + return mUserConsentManager; } @Override public UserConsent getUserConsent() { - return mUserConsentManager.getStatus(); + return getUserConsentManager().getStatus(); } void checkClients() { @@ -444,14 +478,35 @@ void checkClients() { } private void setupValidations(SplitClientConfig splitClientConfig) { - ValidationConfig.getInstance().setMaximumKeyLength(splitClientConfig.maximumKeyLength()); ValidationConfig.getInstance().setTrackEventNamePattern(splitClientConfig.trackEventNamePattern()); } - private EventsTracker buildEventsTracker() { - EventValidator eventsValidator = new EventValidatorImpl(new KeyValidatorImpl(), mStorageContainer.getSplitsStorage()); - return new EventsTrackerImpl(eventsValidator, new ValidationMessageLoggerImpl(), mStorageContainer.getTelemetryStorage(), - new EventPropertiesProcessorImpl(), mSyncManager); + public static class EventsTrackerProvider { + + private final SplitsStorage mSplitsStorage; + private final TelemetryStorage mTelemetryStorage; + private final SyncManager mSyncManager; + private volatile EventsTracker mEventsTracker; + + public EventsTrackerProvider(SplitsStorage splitsStorage, TelemetryStorage telemetryStorage, SyncManager syncManager) { + mSplitsStorage = splitsStorage; + mTelemetryStorage = telemetryStorage; + mSyncManager = syncManager; + } + + public EventsTracker getEventsTracker() { + if (mEventsTracker == null) { + synchronized (this) { + if (mEventsTracker == null) { + EventValidator eventsValidator = new EventValidatorImpl(new KeyValidatorImpl(), mSplitsStorage); + mEventsTracker = new EventsTrackerImpl(eventsValidator, new ValidationMessageLoggerImpl(), mTelemetryStorage, + new EventPropertiesProcessorImpl(), mSyncManager); + } + } + } + + return mEventsTracker; + } } } diff --git a/src/main/java/io/split/android/client/UserConsentManagerImpl.java b/src/main/java/io/split/android/client/UserConsentManagerImpl.java index 7ebc7d03e..0a33e1304 100644 --- a/src/main/java/io/split/android/client/UserConsentManagerImpl.java +++ b/src/main/java/io/split/android/client/UserConsentManagerImpl.java @@ -20,7 +20,7 @@ public class UserConsentManagerImpl implements UserConsentManager { private final ImpressionsStorage mImpressionsStorage; private final EventsStorage mEventsStorage; private final SyncManager mSyncManager; - private final EventsTracker mEventsTracker; + private final SplitFactoryImpl.EventsTrackerProvider mEventsTracker; private final ImpressionManager mImpressionManager; private UserConsent mCurrentStatus; private final SplitTaskExecutor mTaskExecutor; @@ -30,7 +30,7 @@ public UserConsentManagerImpl(@NonNull SplitClientConfig splitConfig, @NonNull ImpressionsStorage impressionsStorage, @NonNull EventsStorage eventsStorage, @NonNull SyncManager syncManager, - @NonNull EventsTracker eventsTracker, + @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTracker, @NonNull ImpressionManager impressionManager, @NonNull SplitTaskExecutor taskExecutor) { mSplitConfig = checkNotNull(splitConfig); @@ -67,7 +67,7 @@ public UserConsent getStatus() { private void enableTracking(UserConsent status) { final boolean enable = (status != UserConsent.DECLINED); - mEventsTracker.enableTracking(enable); + mEventsTracker.getEventsTracker().enableTracking(enable); mImpressionManager.enableTracking(enable); Logger.d("Tracking has been set to " + enable ); } diff --git a/src/main/java/io/split/android/client/dtos/Split.java b/src/main/java/io/split/android/client/dtos/Split.java index 3af9030de..e22838048 100644 --- a/src/main/java/io/split/android/client/dtos/Split.java +++ b/src/main/java/io/split/android/client/dtos/Split.java @@ -52,4 +52,15 @@ public class Split { @SerializedName("impressionsDisabled") public boolean impressionsDisabled = false; + + public String json = null; + + public Split() { + + } + + public Split(String name, String json) { + this.name = name; + this.json = json; + } } diff --git a/src/main/java/io/split/android/client/events/BaseEventsManager.java b/src/main/java/io/split/android/client/events/BaseEventsManager.java index 32f591180..e22b9a7dc 100644 --- a/src/main/java/io/split/android/client/events/BaseEventsManager.java +++ b/src/main/java/io/split/android/client/events/BaseEventsManager.java @@ -7,8 +7,8 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; -import io.split.android.client.service.executor.ThreadFactoryBuilder; import io.split.android.client.utils.logger.Logger; import io.split.android.engine.scheduler.PausableThreadPoolExecutor; import io.split.android.engine.scheduler.PausableThreadPoolExecutorImpl; @@ -16,27 +16,35 @@ public abstract class BaseEventsManager implements Runnable { private final static int QUEUE_CAPACITY = 20; + // Shared thread factory for all instances + private static final ThreadFactory EVENTS_THREAD_FACTORY = createThreadFactory(); protected final ArrayBlockingQueue mQueue; protected final Set mTriggered; - public BaseEventsManager() { - - mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); - mTriggered = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - ThreadFactory threadFactory = new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("Split-FactoryEventsManager-%d") - .setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + private static ThreadFactory createThreadFactory() { + final AtomicInteger threadNumber = new AtomicInteger(1); + return new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "Split-FactoryEventsManager-" + threadNumber.getAndIncrement()); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { Logger.e("Unexpected error " + e.getLocalizedMessage()); } - }) - .build(); - launch(threadFactory); + }); + return thread; + } + }; + } + + public BaseEventsManager() { + mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + mTriggered = Collections.newSetFromMap(new ConcurrentHashMap<>()); + launch(EVENTS_THREAD_FACTORY); } @Override diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java b/src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java index d443661fd..b98ecb681 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java @@ -6,7 +6,7 @@ public class SplitTaskExecutorImpl extends SplitBaseTaskExecutor { - private static final int MIN_THREAD_POOL_SIZE_WHEN_IDLE = 6; + private static final int MIN_THREAD_POOL_SIZE_WHEN_IDLE = 2; private static final String THREAD_NAME_FORMAT = "split-taskExecutor-%d"; @NonNull diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java index 7b40dd611..20e7b9003 100644 --- a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; import io.split.android.client.service.executor.ThreadFactoryBuilder; import io.split.android.client.utils.logger.Logger; @@ -14,14 +16,22 @@ public class SplitParallelTaskExecutorFactoryImpl implements SplitParallelTaskEx private final int mThreads = Runtime.getRuntime().availableProcessors(); private final ExecutorService mScheduler = Executors.newFixedThreadPool(mThreads, - new ThreadFactoryBuilder() - .setNameFormat("Split-ParallelTaskExecutor-%d") - .setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { - Logger.e("Unexpected error " + e.getLocalizedMessage()); - } - }).build()); + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final Thread.UncaughtExceptionHandler exceptionHandler = new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { + Logger.e("Unexpected error " + e.getLocalizedMessage()); + } + }; + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setName("Split-ParallelTaskExecutor-" + threadNumber.getAndIncrement()); + thread.setUncaughtExceptionHandler(exceptionHandler); + return thread; + } + }); @Override public SplitParallelTaskExecutor> createForList(Class type) { diff --git a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java index b420833e1..f5546470e 100644 --- a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java +++ b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java @@ -33,6 +33,8 @@ public LoadSplitsTask(@NonNull SplitsStorage splitsStorage, @Nullable String spl @Override @NonNull public SplitTaskExecutionInfo execute() { + long startTime = System.currentTimeMillis(); + // This call loads the feature flags from the DB into memory, as well as the // filter and flags spec values mSplitsStorage.loadLocal(); @@ -48,9 +50,12 @@ public SplitTaskExecutionInfo execute() { } // If change number is not the initial one, and the filter and flags spec have not changed, we don't need to do anything - boolean isNotInitialChangeNumber = mSplitsStorage.getTill() > -1; + long till = mSplitsStorage.getTill(); + boolean isNotInitialChangeNumber = till > -1; + boolean filterHasNotChanged = mSplitsFilterQueryStringFromConfig.equals(queryStringFromStorage); boolean flagsSpecHasNotChanged = mFlagsSpecFromConfig.equals(flagsSpecFromStorage); + if (isNotInitialChangeNumber && filterHasNotChanged && flagsSpecHasNotChanged) { return SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java index 2c9b9db04..4f3f2a5d1 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java @@ -10,7 +10,6 @@ import io.split.android.client.RolloutCacheConfiguration; import io.split.android.client.SplitClientConfig; -import io.split.android.client.service.CleanUpDatabaseTask; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -23,7 +22,7 @@ public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { - public static final int MIN_CACHE_CLEAR_DAYS = 1; // TODO + public static final int MIN_CACHE_CLEAR_DAYS = 1; @NonNull private final GeneralInfoStorage mGeneralInfoStorage; @@ -32,17 +31,13 @@ public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { @NonNull private final RolloutDefinitionsCache[] mStorages; @NonNull - private final CleanUpDatabaseTask mCleanUpDatabaseTask; - @NonNull private final EncryptionMigrationTask mEncryptionMigrationTask; public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitStorageContainer storageContainer, - @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, @NonNull EncryptionMigrationTask encryptionMigrationTask) { this(storageContainer.getGeneralInfoStorage(), splitClientConfig.rolloutCacheConfiguration(), - cleanUpDatabaseTask, encryptionMigrationTask, storageContainer.getSplitsStorage(), storageContainer.getMySegmentsStorageContainer(), @@ -52,11 +47,9 @@ public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @VisibleForTesting RolloutCacheManagerImpl(@NonNull GeneralInfoStorage generalInfoStorage, @NonNull RolloutCacheConfiguration config, - @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, @NonNull EncryptionMigrationTask encryptionMigrationTask, @NonNull RolloutDefinitionsCache... storages) { mGeneralInfoStorage = checkNotNull(generalInfoStorage); - mCleanUpDatabaseTask = checkNotNull(cleanUpDatabaseTask); mEncryptionMigrationTask = checkNotNull(encryptionMigrationTask); mStorages = checkNotNull(storages); mConfig = checkNotNull(config); @@ -66,8 +59,6 @@ public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @Override public void validateCache(SplitTaskExecutionListener listener) { try { - Logger.v("Rollout cache manager: Executing clearing task"); - mCleanUpDatabaseTask.execute(); Logger.v("Rollout cache manager: Validating cache"); execute(); Logger.v("Rollout cache manager: Migrating encryption"); diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java index 43b91a593..6a4f28e97 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -8,7 +8,6 @@ import java.util.concurrent.atomic.AtomicBoolean; -import io.split.android.client.EventsTracker; import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; @@ -73,7 +72,7 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, @Nullable PushNotificationManager pushNotificationManager, @NonNull ClientComponentsRegister clientComponentsRegister, @NonNull MySegmentsWorkManagerWrapper workManagerWrapper, - @NonNull EventsTracker eventsTracker, + @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTrackerProvider, @Nullable FlagSetsFilter flagSetsFilter) { mDefaultMatchingKey = checkNotNull(defaultMatchingKey); mPushNotificationManager = pushNotificationManager; @@ -91,7 +90,7 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, splitTaskExecutor, validationLogger, keyValidator, - eventsTracker, + eventsTrackerProvider, customerImpressionListener, flagSetsFilter ); @@ -118,8 +117,7 @@ public SplitClientContainerImpl(String defaultMatchingKey, SplitClientConfig config, SplitClientFactory splitClientFactory, ClientComponentsRegister clientComponentsRegister, - MySegmentsWorkManagerWrapper workManagerWrapper, - EventsTracker eventsTracker) { + MySegmentsWorkManagerWrapper workManagerWrapper) { mDefaultMatchingKey = checkNotNull(defaultMatchingKey); mPushNotificationManager = pushNotificationManager; mStreamingEnabled = streamingEnabled; diff --git a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java index e1e5928a1..8efd4c2ef 100644 --- a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java +++ b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java @@ -9,6 +9,8 @@ import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.storage.db.EventDao; import io.split.android.client.storage.db.EventEntity; +import io.split.android.client.storage.db.GeneralInfoDao; +import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.ImpressionDao; import io.split.android.client.storage.db.ImpressionEntity; import io.split.android.client.storage.db.ImpressionsCountDao; @@ -50,7 +52,7 @@ public SplitTaskExecutionInfo execute() { @Override public void run() { updateAttributes(mSplitDatabase.attributesDao()); - updateSplits(mSplitDatabase.splitDao()); + updateSplits(mSplitDatabase.splitDao(), mSplitDatabase.generalInfoDao()); updateSegments(mSplitDatabase.mySegmentDao()); updateLargeSegments(mSplitDatabase.myLargeSegmentDao()); updateImpressions(mSplitDatabase.impressionDao()); @@ -187,7 +189,7 @@ private void updateEvents(EventDao eventDao) { } } - private void updateSplits(SplitDao dao) { + private void updateSplits(SplitDao dao, GeneralInfoDao generalInfoDao) { List items = dao.getAll(); for (SplitEntity item : items) { @@ -204,5 +206,27 @@ private void updateSplits(SplitDao dao) { Logger.e("Error applying cipher to split storage"); } } + + GeneralInfoEntity trafficTypesEntity = generalInfoDao.getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP); + if (trafficTypesEntity != null) { + String fromTrafficTypes = mFromCipher.decrypt(trafficTypesEntity.getStringValue()); + String toTrafficTypes = mToCipher.encrypt(fromTrafficTypes); + if (toTrafficTypes != null) { + generalInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, toTrafficTypes)); + } else { + Logger.e("Error applying cipher to traffic types"); + } + } + + GeneralInfoEntity flagSetsEntity = generalInfoDao.getByName(GeneralInfoEntity.FLAG_SETS_MAP); + if (flagSetsEntity != null) { + String fromFlagSets = mFromCipher.decrypt(flagSetsEntity.getStringValue()); + String toFlagSets = mToCipher.encrypt(fromFlagSets); + if (toFlagSets != null) { + generalInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, toFlagSets)); + } else { + Logger.e("Error applying cipher to flag sets"); + } + } } } diff --git a/src/main/java/io/split/android/client/storage/cipher/DBCipher.java b/src/main/java/io/split/android/client/storage/cipher/DBCipher.java index d5cd55eb7..6cd9a09a1 100644 --- a/src/main/java/io/split/android/client/storage/cipher/DBCipher.java +++ b/src/main/java/io/split/android/client/storage/cipher/DBCipher.java @@ -7,7 +7,6 @@ import androidx.annotation.WorkerThread; import io.split.android.client.storage.db.SplitRoomDatabase; -import io.split.android.client.utils.logger.Logger; public class DBCipher { @@ -41,6 +40,7 @@ public DBCipher(@NonNull SplitRoomDatabase splitDatabase, if (mMustApply) { mFromCipher = SplitCipherFactory.create(apiKey, fromLevel); + mToCipher = checkNotNull(toCipher); mSplitDatabase = checkNotNull(splitDatabase); mTaskProvider = checkNotNull(taskProvider); @@ -50,11 +50,9 @@ public DBCipher(@NonNull SplitRoomDatabase splitDatabase, @WorkerThread public void apply() { if (mMustApply) { - Logger.d("Migrating encryption mode"); - mTaskProvider.get(mSplitDatabase, mFromCipher, mToCipher).execute(); - Logger.d("Encryption mode migration done"); - } else { - Logger.d("No need to migrate encryption mode"); + ApplyCipherTask task = mTaskProvider.get(mSplitDatabase, mFromCipher, mToCipher); + + task.execute(); } } diff --git a/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java b/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java index 3a84d6be4..953147007 100644 --- a/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java +++ b/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java @@ -33,14 +33,18 @@ public EncryptionMigrationTask(String apiKey, @Override public SplitTaskExecutionInfo execute() { try { + long startTime = System.currentTimeMillis(); + // Get current encryption level SplitEncryptionLevel fromLevel = getFromLevel(mSplitDatabase.generalInfoDao(), mEncryptionEnabled); // Determine target encryption level SplitEncryptionLevel toLevel = getLevel(mEncryptionEnabled); + DBCipher dbCipher = new DBCipher(mApiKey, mSplitDatabase, fromLevel, toLevel, mToCipher); + // Apply encryption - new DBCipher(mApiKey, mSplitDatabase, fromLevel, toLevel, mToCipher).apply(); + dbCipher.apply(); // Update encryption level updateCurrentLevel(toLevel); @@ -65,8 +69,7 @@ private static SplitEncryptionLevel getFromLevel(GeneralInfoDao generalInfoDao, .getByName(GeneralInfoEntity.DATABASE_ENCRYPTION_MODE); if (entity != null) { - return SplitEncryptionLevel.fromString( - entity.getStringValue()); + return SplitEncryptionLevel.fromString(entity.getStringValue()); } return getLevel(encryptionEnabled); diff --git a/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java b/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java index ce7f25696..b5754bcdf 100644 --- a/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java +++ b/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java @@ -14,6 +14,8 @@ public class GeneralInfoEntity { public static final String SPLITS_FILTER_QUERY_STRING = "splitsFilterQueryString"; public static final String DATABASE_ENCRYPTION_MODE = "databaseEncryptionMode"; public static final String FLAGS_SPEC = "flagsSpec"; + public static final String TRAFFIC_TYPES_MAP = "trafficTypesMap"; + public static final String FLAG_SETS_MAP = "flagSetsMap"; @PrimaryKey() @NonNull diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java b/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java index 6d4ea5fd5..784e6f863 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java +++ b/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java @@ -1,7 +1,7 @@ package io.split.android.client.storage.db; -import java.util.List; +import java.util.Map; public interface SplitQueryDao { - public List get(long rowIdFrom, int maxRows); + Map getAllAsMap(); } \ No newline at end of file diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java b/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java index 68ecb7940..f0dcda0e3 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java +++ b/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java @@ -1,57 +1,146 @@ package io.split.android.client.storage.db; import android.database.Cursor; +import android.os.Process; import androidx.annotation.NonNull; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import io.split.android.client.SplitClientFactoryImpl; +import io.split.android.client.SplitFactoryImpl; import io.split.android.client.utils.logger.Logger; public class SplitQueryDaoImpl implements SplitQueryDao { private final SplitRoomDatabase mDatabase; + private volatile Map mCachedSplitsMap; + private final Object mLock = new Object(); + private boolean mIsInitialized = false; + private final Thread mInitializationThread; public SplitQueryDaoImpl(SplitRoomDatabase mDatabase) { this.mDatabase = mDatabase; + // Start prefilling the map in a background thread + mInitializationThread = new Thread(() -> { + try { + android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); + } catch (Exception ignore) { + // Ignore + } + long startTime = System.currentTimeMillis(); + + Map result = loadSplitsMap(); + + synchronized (mLock) { + mCachedSplitsMap = result; + mIsInitialized = true; + mLock.notifyAll(); // Notify any waiting threads + } + }); + mInitializationThread.setName("SplitMapPrefill"); + mInitializationThread.start(); } - public List get(long rowIdFrom, int maxRows) { + int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { + final int index = c.getColumnIndex(name); + if (index >= 0) { + return index; + } + return c.getColumnIndexOrThrow("`" + name + "`"); + } - String sql = "SELECT rowid, name, body, updated_at FROM splits WHERE rowId > ? ORDER BY rowId LIMIT ?"; - Object[] arguments = {rowIdFrom, maxRows}; - Cursor cursor = mDatabase.query(sql, arguments); + public Map getAllAsMap() { + // Fast path - if the map is already initialized, return it immediately + if (mIsInitialized) { + return new HashMap<>(mCachedSplitsMap); + } + + // Wait for initialization to complete if it's in progress + synchronized (mLock) { + if (mIsInitialized) { + return new HashMap<>(mCachedSplitsMap); + } + + // If initialization thread is running, wait for it + if (mInitializationThread != null && mInitializationThread.isAlive()) { + try { + mLock.wait(5000); // Wait up to 5 seconds + + if (mIsInitialized) { + return new HashMap<>(mCachedSplitsMap); + } + } catch (InterruptedException e) { + } + } + + // If we get here, either initialization failed or timed out + // Load the map directly + Map result = loadSplitsMap(); + + // Cache the result for future calls + mCachedSplitsMap = result; + mIsInitialized = true; + + return new HashMap<>(result); + } + } + + /** + * Internal method to load the splits map from the database. + * This contains the actual loading logic separated from the caching/synchronization. + */ + private Map loadSplitsMap() { + final String sql = "SELECT name, body FROM splits"; + + Cursor cursor = mDatabase.query(sql, null); + + final int ESTIMATED_CAPACITY = 2000; + Map result = new HashMap<>(ESTIMATED_CAPACITY); + try { - final int rowIdIndex = getColumnIndexOrThrow(cursor, "rowid"); final int nameIndex = getColumnIndexOrThrow(cursor, "name"); final int bodyIndex = getColumnIndexOrThrow(cursor, "body"); - final int updatedAtIndex = getColumnIndexOrThrow(cursor, "updated_at"); - final List entities = new ArrayList(cursor.getCount()); + + final int BATCH_SIZE = 100; + String[] names = new String[BATCH_SIZE]; + String[] bodies = new String[BATCH_SIZE]; + int batchCount = 0; + while (cursor.moveToNext()) { - final SplitEntity item; - item = new SplitEntity(); - item.setRowId(cursor.getLong(rowIdIndex)); - item.setName(cursor.getString(nameIndex)); - item.setBody(cursor.getString(bodyIndex)); - item.setUpdatedAt(cursor.getLong(updatedAtIndex)); - entities.add(item); + names[batchCount] = cursor.getString(nameIndex); + bodies[batchCount] = cursor.getString(bodyIndex); + batchCount++; + + // Process in batches + if (batchCount == BATCH_SIZE) { + for (int i = 0; i < BATCH_SIZE; i++) { + SplitEntity entity = new SplitEntity(); + entity.setName(names[i]); + entity.setBody(bodies[i]); + result.put(names[i], entity); + } + batchCount = 0; + } + } + + // Process any remaining items + for (int i = 0; i < batchCount; i++) { + SplitEntity entity = new SplitEntity(); + entity.setName(names[i]); + entity.setBody(bodies[i]); + result.put(names[i], entity); } - return entities; } catch (Exception e) { - Logger.e("Error executing splits query: " + e.getLocalizedMessage()); + Logger.e("Error executing loadSplitsMap query: " + e.getLocalizedMessage()); } finally { cursor.close(); } - return new ArrayList<>(); - } - int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { - final int index = c.getColumnIndex(name); - if (index >= 0) { - return index; - } - return c.getColumnIndexOrThrow("`" + name + "`"); + return result; } } diff --git a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java index 273208e52..e17ee3db9 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java +++ b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java @@ -5,12 +5,16 @@ import android.content.Context; +import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; +import androidx.sqlite.db.SupportSQLiteDatabase; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; import io.split.android.client.storage.db.attributes.AttributesDao; import io.split.android.client.storage.db.attributes.AttributesEntity; @@ -18,6 +22,7 @@ import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeyEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeysDao; +import io.split.android.client.utils.logger.Logger; @Database( entities = { @@ -54,6 +59,21 @@ public abstract class SplitRoomDatabase extends RoomDatabase { private static volatile Map mInstances = new ConcurrentHashMap<>(); + /** + * Get the SplitQueryDao instance for optimized split queries. + * This uses direct cursor access for better performance. + */ + public SplitQueryDao getSplitQueryDao() { + if (mSplitQueryDao == null) { + synchronized (this) { + if (mSplitQueryDao == null) { + mSplitQueryDao = new SplitQueryDaoImpl(this); + } + } + } + return mSplitQueryDao; + } + public static SplitRoomDatabase getDatabase(final Context context, final String databaseName) { checkNotNull(context); checkNotNull(databaseName); @@ -63,23 +83,32 @@ public static SplitRoomDatabase getDatabase(final Context context, final String instance = mInstances.get(databaseName); if (instance == null) { instance = Room.databaseBuilder(context.getApplicationContext(), - SplitRoomDatabase.class, databaseName) - .setJournalMode(JournalMode.TRUNCATE) + SplitRoomDatabase.class, databaseName) + .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) .fallbackToDestructiveMigration() .build(); + + try { + SupportSQLiteDatabase db = instance.getOpenHelper().getWritableDatabase(); + + db.execSQL("PRAGMA cache_size = -3000"); + db.execSQL("PRAGMA automatic_index = ON"); + db.execSQL("PRAGMA foreign_keys = OFF"); + } catch (Exception e) { + Logger.i("Failed to set optimized pragma"); + } + + mInstances.put(databaseName, instance); + new Thread(() -> { + try { + mInstances.get(databaseName).getSplitQueryDao(); + } catch (Exception e) { + Logger.i("Failed to preload query DAO"); + } + }).start(); } } return instance; } - - public SplitQueryDao splitQueryDao() { - if (mSplitQueryDao != null) { - return mSplitQueryDao; - } - synchronized (this) { - mSplitQueryDao = new SplitQueryDaoImpl(this); - return mSplitQueryDao; - } - } } diff --git a/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java b/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java new file mode 100644 index 000000000..743e07552 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java @@ -0,0 +1,89 @@ +package io.split.android.client.storage.splits; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.split.android.client.dtos.Split; + +class MetadataHelper { + + static void increaseTrafficTypeCount(@Nullable String name, Map outputTrafficTypes) { + if (name == null) { + return; + } + + String lowercaseName = name.toLowerCase(); + int count = countForTrafficType(lowercaseName, outputTrafficTypes); + outputTrafficTypes.put(lowercaseName, ++count); + } + + static void decreaseTrafficTypeCount(@Nullable String name, Map outputTrafficTypes) { + if (name == null) { + return; + } + String lowercaseName = name.toLowerCase(); + + int count = countForTrafficType(lowercaseName, outputTrafficTypes); + if (count > 1) { + outputTrafficTypes.put(lowercaseName, --count); + } else { + outputTrafficTypes.remove(lowercaseName); + } + } + + static int countForTrafficType(@NonNull String name, Map outputTrafficTypes) { + int count = 0; + Integer countValue = outputTrafficTypes.get(name); + if (countValue != null) { + count = countValue; + } + return count; + } + + static void addOrUpdateFlagSets(Split split, Map> outputFlagSets) { + if (split.sets == null) { + return; + } + + for (String set : split.sets) { + Set splitsForSet = outputFlagSets.get(set); + if (splitsForSet == null) { + splitsForSet = new HashSet<>(); + outputFlagSets.put(set, splitsForSet); + } + splitsForSet.add(split.name); + } + + deleteFromFlagSetsIfNecessary(split, outputFlagSets); + } + + static void deleteFromFlagSetsIfNecessary(Split featureFlag, Map> outputFlagSets) { + if (featureFlag.sets == null) { + return; + } + + for (String set : outputFlagSets.keySet()) { + if (featureFlag.sets.contains(set)) { + continue; + } + + Set flagsForSet = outputFlagSets.get(set); + if (flagsForSet != null) { + flagsForSet.remove(featureFlag.name); + } + } + } + + static void deleteFromFlagSets(Split featureFlag, Map> outputFlagSets) { + for (String set : outputFlagSets.keySet()) { + Set flagsForSet = outputFlagSets.get(set); + if (flagsForSet != null) { + flagsForSet.remove(featureFlag.name); + } + } + } +} diff --git a/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java index fc5fe1203..079eb7abf 100644 --- a/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java @@ -3,11 +3,13 @@ import androidx.annotation.Nullable; import java.util.List; +import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; public interface PersistentSplitsStorage { - boolean update(ProcessedSplitChange splitChange); + boolean update(ProcessedSplitChange splitChange, Map mTrafficTypes, Map> mFlagSets); SplitsSnapshot getSnapshot(); List getAll(); diff --git a/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java b/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java index 40ace1db2..9c4253139 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java @@ -1,7 +1,6 @@ package io.split.android.client.storage.splits; import static io.split.android.client.utils.Utils.checkNotNull; -import static io.split.android.client.utils.Utils.partition; import androidx.annotation.NonNull; @@ -9,92 +8,54 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; +import java.util.Map; import io.split.android.client.dtos.Split; -import io.split.android.client.service.executor.parallel.SplitDeferredTaskItem; -import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutor; import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.db.SplitEntity; -import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; public class SplitEntityToSplitTransformer implements SplitListTransformer { - private final SplitParallelTaskExecutor> mTaskExecutor; private final SplitCipher mSplitCipher; - public SplitEntityToSplitTransformer(@NonNull SplitParallelTaskExecutor> taskExecutor, - @NonNull SplitCipher splitCipher) { - mTaskExecutor = checkNotNull(taskExecutor); + public SplitEntityToSplitTransformer(@NonNull SplitCipher splitCipher) { mSplitCipher = checkNotNull(splitCipher); } @Override + @Deprecated public List transform(List entities) { - if (entities == null) { - return new ArrayList<>(); - } - - int entitiesCount = entities.size(); - - if (entitiesCount > mTaskExecutor.getAvailableThreads()) { - List> result = mTaskExecutor.execute(getSplitDeserializationTasks(entities, entitiesCount)); - List splits = new ArrayList<>(); - for (List subList : result) { - splits.addAll(subList); - } - - return splits; - } else { - return convertEntitiesToSplitList(entities, mSplitCipher); - } + return new ArrayList<>(); // no - op } - @NonNull - private List>> getSplitDeserializationTasks(List allEntities, int entitiesCount) { - int availableThreads = mTaskExecutor.getAvailableThreads(); - int partitionSize = availableThreads > 0 ? entitiesCount / availableThreads : 1; - List> partitions = partition(allEntities, partitionSize); - List>> taskList = new ArrayList<>(partitions.size()); - - for (List partition : partitions) { - taskList.add(new SplitDeferredTaskItem<>( - new Callable>() { - @Override - public List call() { - return convertEntitiesToSplitList(partition, mSplitCipher); - } - })); - } - - return taskList; - } - - @NonNull - private static List convertEntitiesToSplitList(List entities, - SplitCipher cipher) { - List splits = new ArrayList<>(); - - if (entities == null) { - return splits; + @Override + public List transform(Map allNamesAndBodies) { + if (allNamesAndBodies == null) { + return new ArrayList<>(); } - for (SplitEntity entity : entities) { - String name; - String json; + List splits = new ArrayList<>(allNamesAndBodies.size()); + for (Map.Entry entry : allNamesAndBodies.entrySet()) { + if (entry == null || entry.getValue() == null) { + continue; + } try { - name = cipher.decrypt(entity.getName()); - json = cipher.decrypt(entity.getBody()); - if (name != null && json != null) { - Split split = Json.fromJson(json, Split.class); - split.name = name; - splits.add(split); + String decryptedName = mSplitCipher.decrypt(entry.getKey()); + if (decryptedName == null) { + continue; + } + String decryptedBody = mSplitCipher.decrypt(entry.getValue().getBody()); + if (decryptedBody == null) { + continue; } + + splits.add(new Split(decryptedName, decryptedBody)); } catch (JsonSyntaxException e) { - Logger.e("Could not parse entity to split: " + entity.getName()); + Logger.e("Could not parse entity to split: " + entry.getKey()); } } + return splits; } } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java b/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java index 57a486d79..2b191d70d 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java @@ -1,6 +1,7 @@ package io.split.android.client.storage.splits; import java.util.List; +import java.util.Map; /** * Used to map a list of values of type {@link I} to a list of type {@link O} @@ -11,4 +12,6 @@ public interface SplitListTransformer { List transform(List inputList); + + List transform(Map allNamesAndBodies); } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java b/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java index f72b2f342..0c43d4056 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java @@ -6,7 +6,9 @@ import androidx.annotation.NonNull; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.Callable; import io.split.android.client.dtos.Split; @@ -21,7 +23,7 @@ public class SplitToSplitEntityTransformer implements SplitListTransformer> mTaskExecutor; private final SplitCipher mSplitCipher; - + public SplitToSplitEntityTransformer(@NonNull SplitParallelTaskExecutor> taskExecutor, @NonNull SplitCipher splitCipher) { mTaskExecutor = checkNotNull(taskExecutor); @@ -30,6 +32,7 @@ public SplitToSplitEntityTransformer(@NonNull SplitParallelTaskExecutor transform(List splits) { + List splitEntities = new ArrayList<>(); if (splits == null) { @@ -38,7 +41,8 @@ public List transform(List splits) { int splitsSize = splits.size(); if (splitsSize > mTaskExecutor.getAvailableThreads()) { - List> subLists = mTaskExecutor.execute(getSplitEntityTasks(splits, splitsSize)); + List>> tasks = getSplitEntityTasks(splits, splitsSize); + List> subLists = mTaskExecutor.execute(tasks); for (List subList : subLists) { splitEntities.addAll(subList); @@ -51,11 +55,18 @@ public List transform(List splits) { } } + @Override + @Deprecated + public List transform(Map allNamesAndBodies) { + return Collections.emptyList(); // to be removed + } + @NonNull - private static List getSplitEntities(List partition, SplitCipher cipher) { + private List getSplitEntities(List partition, SplitCipher cipher) { List result = new ArrayList<>(); for (Split split : partition) { + // Create entity String encryptedName = cipher.encrypt(split.name); String encryptedJson = cipher.encrypt(Json.toJson(split)); if (encryptedName == null || encryptedJson == null) { @@ -65,6 +76,7 @@ private static List getSplitEntities(List partition, SplitCi SplitEntity entity = new SplitEntity(); entity.setName(encryptedName); entity.setBody(encryptedJson); + entity.setUpdatedAt(System.currentTimeMillis() / 1000); result.add(entity); } @@ -91,4 +103,5 @@ public List call() { return taskList; } + } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java b/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java index c73ef1beb..19fe8c082 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java @@ -3,7 +3,10 @@ import androidx.annotation.NonNull; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; @@ -14,13 +17,19 @@ public class SplitsSnapshot { private final long mUpdateTimestamp; private final String mSplitsFilterQueryString; private final String mFlagsSpec; + private final Map mTrafficTypesMap; + private final Map> mFlagSetsMap; - public SplitsSnapshot(List splits, long changeNumber, long updateTimestamp, String splitsFilterQueryString, String flagsSpec) { + public SplitsSnapshot(List splits, long changeNumber, long updateTimestamp, + String splitsFilterQueryString, String flagsSpec, + Map trafficTypesMap, Map> flagSetsMap) { mChangeNumber = changeNumber; mSplits = splits; mUpdateTimestamp = updateTimestamp; mSplitsFilterQueryString = splitsFilterQueryString; mFlagsSpec = flagsSpec; + mTrafficTypesMap = trafficTypesMap != null ? trafficTypesMap : new HashMap<>(); + mFlagSetsMap = flagSetsMap != null ? flagSetsMap : new HashMap<>(); } public long getChangeNumber() { @@ -42,4 +51,12 @@ public String getSplitsFilterQueryString() { public String getFlagsSpec() { return mFlagsSpec; } + + public @NonNull Map getTrafficTypesMap() { + return mTrafficTypesMap; + } + + public @NonNull Map> getFlagSetsMap() { + return mFlagSetsMap; + } } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java index add0148ff..3c6d48b91 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java @@ -1,11 +1,18 @@ package io.split.android.client.storage.splits; +import static io.split.android.client.storage.splits.MetadataHelper.addOrUpdateFlagSets; +import static io.split.android.client.storage.splits.MetadataHelper.decreaseTrafficTypeCount; +import static io.split.android.client.storage.splits.MetadataHelper.deleteFromFlagSets; +import static io.split.android.client.storage.splits.MetadataHelper.deleteFromFlagSetsIfNecessary; +import static io.split.android.client.storage.splits.MetadataHelper.increaseTrafficTypeCount; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import com.google.gson.JsonSyntaxException; + import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -13,8 +20,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import io.split.android.client.dtos.Split; +import io.split.android.client.utils.Json; public class SplitsStorageImpl implements SplitsStorage { @@ -26,8 +35,10 @@ public class SplitsStorageImpl implements SplitsStorage { private String mSplitsFilterQueryString; private String mFlagsSpec; private final Map mTrafficTypes; + private final AtomicBoolean mInitialized; public SplitsStorageImpl(@NonNull PersistentSplitsStorage persistentStorage) { + mInitialized = new AtomicBoolean(false); mPersistentStorage = checkNotNull(persistentStorage); mInMemorySplits = new ConcurrentHashMap<>(); mTrafficTypes = new ConcurrentHashMap<>(); @@ -36,35 +47,69 @@ public SplitsStorageImpl(@NonNull PersistentSplitsStorage persistentStorage) { @Override @WorkerThread - public void loadLocal() { - SplitsSnapshot snapshot = mPersistentStorage.getSnapshot(); - List splits = snapshot.getSplits(); - mChangeNumber = snapshot.getChangeNumber(); - mUpdateTimestamp = snapshot.getUpdateTimestamp(); - mSplitsFilterQueryString = snapshot.getSplitsFilterQueryString(); - mFlagsSpec = snapshot.getFlagsSpec(); - for (Split split : splits) { - mInMemorySplits.put(split.name, split); - addOrUpdateFlagSets(split); - increaseTrafficTypeCount(split.trafficTypeName); + public synchronized void loadLocal() { + if (mInitialized.get()) { + return; + } + + try { + long startTime = System.currentTimeMillis(); + + SplitsSnapshot snapshot = mPersistentStorage.getSnapshot(); + List splits = snapshot.getSplits(); + + mChangeNumber = snapshot.getChangeNumber(); + mUpdateTimestamp = snapshot.getUpdateTimestamp(); + mSplitsFilterQueryString = snapshot.getSplitsFilterQueryString(); + mFlagsSpec = snapshot.getFlagsSpec(); + + // Populate traffic types and flag sets + mTrafficTypes.putAll(snapshot.getTrafficTypesMap()); + for (Map.Entry> entry : snapshot.getFlagSetsMap().entrySet()) { + mFlagSets.put(entry.getKey(), new HashSet<>(entry.getValue())); + } + + for (Split split : splits) { + mInMemorySplits.put(split.name, split); + } + } finally { + mInitialized.compareAndSet(false, true); } } @Override public Split get(@NonNull String name) { - return mInMemorySplits.get(name); + Split split = mInMemorySplits.get(name); + if (split == null) { + return null; + } + + if (split.json == null) { + return split; + } + + try { + Split parsedSplit = Json.fromJson(split.json, Split.class); + parsedSplit.json = null; + mInMemorySplits.put(name, parsedSplit); + return mInMemorySplits.get(name); + } catch (JsonSyntaxException e) { + return null; + } } @Override public Map getMany(@Nullable List splitNames) { Map splits = new HashMap<>(); if (splitNames == null || splitNames.isEmpty()) { - splits.putAll(mInMemorySplits); + for (String name : mInMemorySplits.keySet()) { + splits.put(name, get(name)); + } return splits; } for (String name : splitNames) { - Split split = mInMemorySplits.get(name); + Split split = get(name); if (split != null) { splits.put(name, split); } @@ -94,13 +139,13 @@ public boolean update(ProcessedSplitChange splitChange) { appliedUpdates = true; } for (Split split : activeSplits) { - Split loadedSplit = mInMemorySplits.get(split.name); + Split loadedSplit = get(split.name); if (loadedSplit != null && loadedSplit.trafficTypeName != null) { - decreaseTrafficTypeCount(loadedSplit.trafficTypeName); + decreaseTrafficTypeCount(loadedSplit.trafficTypeName, mTrafficTypes); } - increaseTrafficTypeCount(split.trafficTypeName); + increaseTrafficTypeCount(split.trafficTypeName, mTrafficTypes); mInMemorySplits.put(split.name, split); - addOrUpdateFlagSets(split); + addOrUpdateFlagSets(split, mFlagSets); } } @@ -109,15 +154,16 @@ public boolean update(ProcessedSplitChange splitChange) { if (mInMemorySplits.remove(split.name) != null) { // The flag was in memory, so it will be updated appliedUpdates = true; - decreaseTrafficTypeCount(split.trafficTypeName); - deleteFromFlagSetsIfNecessary(split); + decreaseTrafficTypeCount(split.trafficTypeName, mTrafficTypes); + deleteFromFlagSetsIfNecessary(split, mFlagSets); } } } mChangeNumber = splitChange.getChangeNumber(); mUpdateTimestamp = splitChange.getUpdateTimestamp(); - mPersistentStorage.update(splitChange); + + mPersistentStorage.update(splitChange, mTrafficTypes, mFlagSets); return appliedUpdates; } @@ -127,7 +173,7 @@ public boolean update(ProcessedSplitChange splitChange) { public void updateWithoutChecks(Split split) { mInMemorySplits.put(split.name, split); mPersistentStorage.update(split); - deleteFromFlagSets(split); + deleteFromFlagSets(split, mFlagSets); } @Override @@ -197,80 +243,4 @@ public boolean isValidTrafficType(@Nullable String name) { } return (mTrafficTypes.get(name.toLowerCase()) != null); } - - private void increaseTrafficTypeCount(@Nullable String name) { - if (name == null) { - return; - } - - String lowercaseName = name.toLowerCase(); - int count = countForTrafficType(lowercaseName); - mTrafficTypes.put(lowercaseName, ++count); - } - - private void decreaseTrafficTypeCount(@Nullable String name) { - if (name == null) { - return; - } - String lowercaseName = name.toLowerCase(); - - int count = countForTrafficType(lowercaseName); - if (count > 1) { - mTrafficTypes.put(lowercaseName, --count); - } else { - mTrafficTypes.remove(lowercaseName); - } - } - - private int countForTrafficType(@NonNull String name) { - int count = 0; - Integer countValue = mTrafficTypes.get(name); - if (countValue != null) { - count = countValue; - } - return count; - } - - private void addOrUpdateFlagSets(Split split) { - if (split.sets == null) { - return; - } - - for (String set : split.sets) { - Set splitsForSet = mFlagSets.get(set); - if (splitsForSet == null) { - splitsForSet = new HashSet<>(); - mFlagSets.put(set, splitsForSet); - } - splitsForSet.add(split.name); - } - - deleteFromFlagSetsIfNecessary(split); - } - - private void deleteFromFlagSetsIfNecessary(Split featureFlag) { - if (featureFlag.sets == null) { - return; - } - - for (String set : mFlagSets.keySet()) { - if (featureFlag.sets.contains(set)) { - continue; - } - - Set flagsForSet = mFlagSets.get(set); - if (flagsForSet != null) { - flagsForSet.remove(featureFlag.name); - } - } - } - - private void deleteFromFlagSets(Split featureFlag) { - for (String set : mFlagSets.keySet()) { - Set flagsForSet = mFlagSets.get(set); - if (flagsForSet != null) { - flagsForSet.remove(featureFlag.name); - } - } - } } diff --git a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java index 14aa16835..5bd250a14 100644 --- a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java @@ -1,5 +1,9 @@ package io.split.android.client.storage.splits; +import static io.split.android.client.storage.splits.MetadataHelper.addOrUpdateFlagSets; +import static io.split.android.client.storage.splits.MetadataHelper.decreaseTrafficTypeCount; +import static io.split.android.client.storage.splits.MetadataHelper.deleteFromFlagSetsIfNecessary; +import static io.split.android.client.storage.splits.MetadataHelper.increaseTrafficTypeCount; import static io.split.android.client.utils.Utils.checkNotNull; import static io.split.android.client.utils.Utils.partition; @@ -9,14 +13,24 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; +import io.split.android.client.SplitFactoryImpl; import io.split.android.client.dtos.Split; +import io.split.android.client.dtos.Status; import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutorFactory; import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutorFactoryImpl; import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; + +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.concurrent.ConcurrentHashMap; public class SqLitePersistentSplitsStorage implements PersistentSplitsStorage { @@ -45,17 +59,18 @@ private SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNull SplitParallelTaskExecutorFactory executorFactory, @NonNull SplitCipher splitCipher) { this(database, - new SplitEntityToSplitTransformer(executorFactory.createForList(Split.class), splitCipher), + new SplitEntityToSplitTransformer(splitCipher), new SplitToSplitEntityTransformer(executorFactory.createForList(SplitEntity.class), splitCipher), splitCipher); } @Override - public boolean update(ProcessedSplitChange splitChange) { + public boolean update(ProcessedSplitChange splitChange, Map mTrafficTypes, Map> mFlagSets) { if (splitChange == null) { return false; } + List removedSplits = splitNameList(splitChange.getArchivedSplits()); List splitEntities = convertSplitListToEntities(splitChange.getActiveSplits()); @@ -64,8 +79,20 @@ public boolean update(ProcessedSplitChange splitChange) { public void run() { mDatabase.generalInfoDao().update( new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, splitChange.getChangeNumber())); - mDatabase.splitDao().insert(splitEntities); - mDatabase.splitDao().delete(removedSplits); + if (!splitEntities.isEmpty()) { + mDatabase.splitDao().insert(splitEntities); + } + if (!removedSplits.isEmpty()) { + mDatabase.splitDao().delete(removedSplits); + } + if (!mTrafficTypes.isEmpty()) { + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, + Json.toJson(mTrafficTypes))); + } + if (!mFlagSets.isEmpty()) { + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, + Json.toJson(mFlagSets))); + } mDatabase.generalInfoDao().update( new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, splitChange.getUpdateTimestamp())); } @@ -76,10 +103,11 @@ public void run() { @Override public SplitsSnapshot getSnapshot() { - SplitsSnapshotLoader loader = new SplitsSnapshotLoader(mDatabase); - mDatabase.runInTransaction(loader); - return new SplitsSnapshot(loadSplits(), loader.getChangeNumber(), - loader.getUpdateTimestamp(), loader.getSplitsFilterQueryString(), loader.getFlagsSpec()); + SplitsSnapshotLoader loader = new SplitsSnapshotLoader(mDatabase, loadSplits(), mCipher); + loader.run(); + return new SplitsSnapshot(loader.getSplits(), loader.getChangeNumber(), + loader.getUpdateTimestamp(), loader.getSplitsFilterQueryString(), loader.getFlagsSpec(), + loader.getTrafficTypes(), loader.getFlagSets()); } @Override @@ -130,6 +158,8 @@ public void clear() { @Override public void run() { mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, -1)); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, "")); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, "")); mDatabase.splitDao().deleteAll(); } }); @@ -148,10 +178,17 @@ public String getFilterQueryString() { } private List loadSplits() { - return mEntityToSplitTransformer.transform(mDatabase.splitDao().getAll()); + Map allNamesAndBodies = mDatabase.getSplitQueryDao().getAllAsMap(); + long transformStartTime = System.currentTimeMillis(); + List splits = mEntityToSplitTransformer.transform(allNamesAndBodies); + + return splits; } private List convertSplitListToEntities(List splits) { + if (splits == null) { + return new ArrayList<>(); + } return mSplitToEntityTransformer.transform(splits); } @@ -172,9 +209,15 @@ private static class SplitsSnapshotLoader implements Runnable { private Long mUpdateTimestamp = 0L; private String mSplitsFilterQueryString = ""; private String mFlagsSpec = ""; + private Map mTrafficTypes = new ConcurrentHashMap<>(); + private Map> mFlagSets = new ConcurrentHashMap<>(); + private final List mSplits; + private final SplitCipher mCipher; - public SplitsSnapshotLoader(SplitRoomDatabase database) { + public SplitsSnapshotLoader(SplitRoomDatabase database, List splits, SplitCipher cipher) { mDatabase = database; + mSplits = splits; + mCipher = cipher; } @Override @@ -183,6 +226,8 @@ public void run() { GeneralInfoEntity changeNumberEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO); GeneralInfoEntity filterQueryStringEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING); GeneralInfoEntity flagsSpecEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.FLAGS_SPEC); + GeneralInfoEntity trafficTypesEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP); + GeneralInfoEntity flagSetsEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.FLAG_SETS_MAP); if (changeNumberEntity != null) { mChangeNumber = changeNumberEntity.getLongValue(); @@ -199,6 +244,67 @@ public void run() { if (flagsSpecEntity != null) { mFlagsSpec = flagsSpecEntity.getStringValue(); } + + boolean splitsAreNotEmpty = !mSplits.isEmpty(); + boolean trafficTypesEntityIsEmpty = trafficTypesEntity == null || trafficTypesEntity.getStringValue().isEmpty(); + boolean flagSetsEntityIsEmpty = flagSetsEntity == null || flagSetsEntity.getStringValue().isEmpty(); + boolean trafficTypesAndSetsMigrationRequired = splitsAreNotEmpty && + (trafficTypesEntityIsEmpty || flagSetsEntityIsEmpty); + if (trafficTypesAndSetsMigrationRequired) { + migrateTrafficTypesAndSetsFromStoredData(); + } + + parseTrafficTypesAndSets(trafficTypesEntity, flagSetsEntity); + } + + private synchronized void parseTrafficTypesAndSets(@Nullable GeneralInfoEntity trafficTypesEntity, @Nullable GeneralInfoEntity flagSetsEntity) { + Logger.v("Parsing traffic types and sets"); + if (trafficTypesEntity != null) { + Type mapType = new TypeToken>(){}.getType(); + String encryptedTrafficTypes = trafficTypesEntity.getStringValue(); + String decryptedTrafficTypes = mCipher.decrypt(encryptedTrafficTypes); + mTrafficTypes = Json.fromJson(decryptedTrafficTypes, mapType); + } + + if (flagSetsEntity != null) { + Type flagsMapType = new TypeToken>>(){}.getType(); + String encryptedFlagSets = flagSetsEntity.getStringValue(); + String decryptedFlagSets = mCipher.decrypt(encryptedFlagSets); + mFlagSets = Json.fromJson(decryptedFlagSets, flagsMapType); + } + } + + private void migrateTrafficTypesAndSetsFromStoredData() { + Logger.i("Migration required for cached traffic types and flag sets. Migrating now."); + try { + for (Split split : mSplits) { + Split parsedSplit = Json.fromJson(split.json, Split.class); + if (parsedSplit != null && parsedSplit.status == Status.ACTIVE) { + increaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); + addOrUpdateFlagSets(parsedSplit, mFlagSets); + } else { + decreaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); + deleteFromFlagSetsIfNecessary(parsedSplit, mFlagSets); + } + } + + // persist TTs + String decryptedTrafficTypes = Json.toJson(mTrafficTypes); + String encryptedTrafficTypes = mCipher.encrypt(decryptedTrafficTypes); + + // persist flag sets + String decryptedFlagSets = Json.toJson(mFlagSets); + String encryptedFlagSets = mCipher.encrypt(decryptedFlagSets); + + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, encryptedTrafficTypes)); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, encryptedFlagSets)); + } catch (Exception e) { + Logger.e("Failed to migrate traffic types and flag sets", e); + } + } + + public List getSplits() { + return mSplits; } public Long getChangeNumber() { @@ -216,5 +322,13 @@ public String getSplitsFilterQueryString() { public String getFlagsSpec() { return mFlagsSpec; } + + public Map getTrafficTypes() { + return mTrafficTypes; + } + + public Map> getFlagSets() { + return mFlagSets; + } } } diff --git a/src/main/java/io/split/android/client/utils/Json.java b/src/main/java/io/split/android/client/utils/Json.java index 029c84fdd..4505210e2 100644 --- a/src/main/java/io/split/android/client/utils/Json.java +++ b/src/main/java/io/split/android/client/utils/Json.java @@ -71,4 +71,5 @@ private static Gson getNonNullsGsonInstance() { return mNonNullJson; } + } diff --git a/src/main/java/io/split/android/client/utils/Zlib.java b/src/main/java/io/split/android/client/utils/Zlib.java index 049250c0d..efe50e914 100644 --- a/src/main/java/io/split/android/client/utils/Zlib.java +++ b/src/main/java/io/split/android/client/utils/Zlib.java @@ -4,6 +4,7 @@ import java.util.zip.Inflater; import io.split.android.client.service.ServiceConstants; +import io.split.android.client.utils.logger.Logger; public class Zlib implements CompressionUtil { @@ -20,9 +21,9 @@ public byte[] decompress(byte[] input) { inflater.end(); return Arrays.copyOfRange(result, 0, resultLength); } catch (java.util.zip.DataFormatException e) { - System.out.println("DataFormatException error: " + e.getLocalizedMessage()); + Logger.e("DataFormatException error: " + e.getLocalizedMessage()); } catch (Exception e) { - System.out.println("Error decompressing: " + e.getLocalizedMessage()); + Logger.e("Error decompressing: " + e.getLocalizedMessage()); } return null; } diff --git a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt index 335173c5d..04cc76c80 100644 --- a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt +++ b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt @@ -5,6 +5,7 @@ import io.split.android.client.SplitFactoryHelper.Initializer.Listener import io.split.android.client.events.EventsManagerCoordinator import io.split.android.client.events.SplitInternalEvent import io.split.android.client.lifecycle.SplitLifecycleManager +import io.split.android.client.service.CleanUpDatabaseTask import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor import io.split.android.client.service.executor.SplitTaskExecutionInfo import io.split.android.client.service.executor.SplitTaskExecutionListener diff --git a/src/test/java/io/split/android/client/UserConsentManagerTest.java b/src/test/java/io/split/android/client/UserConsentManagerTest.java index b3d1f689f..8dc3a2194 100644 --- a/src/test/java/io/split/android/client/UserConsentManagerTest.java +++ b/src/test/java/io/split/android/client/UserConsentManagerTest.java @@ -2,6 +2,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.junit.Assert; import org.junit.Before; @@ -31,6 +32,8 @@ public class UserConsentManagerTest { @Mock private EventsTracker mEventsTracker; @Mock + private SplitFactoryImpl.EventsTrackerProvider mEventsTrackerProvider; + @Mock private ImpressionManager mImpressionManager; private SplitTaskExecutor mTaskExecutor; @@ -41,6 +44,7 @@ public class UserConsentManagerTest { public void setup() { mTaskExecutor = new SplitTaskExecutorStub(); MockitoAnnotations.openMocks(this); + when(mEventsTrackerProvider.getEventsTracker()).thenReturn(mEventsTracker); } @Test @@ -137,7 +141,7 @@ private void createUserConsentManager(UserConsent status) { mManager = new UserConsentManagerImpl(mSplitConfig, mImpressionsStorage, mEventsStorage, mSyncManager, - mEventsTracker, + mEventsTrackerProvider, mImpressionManager, mTaskExecutor); } diff --git a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt index b89da1649..47faababc 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt +++ b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt @@ -134,15 +134,6 @@ class RolloutCacheManagerTest { verify(mGeneralInfoStorage, times(0)).setRolloutCacheLastClearTimestamp(anyLong()) } - @Test - fun `validateCache executes cleanUpDatabaseTask`() { - mRolloutCacheManager = getCacheManager(10, false) - - mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) - - verify(mCleanUpDatabaseTask).execute() - } - @Test fun `validateCache executes encryptionMigrationTask`() { mRolloutCacheManager = getCacheManager(10, false) @@ -177,7 +168,7 @@ class RolloutCacheManagerTest { } private fun getCacheManager(expiration: Int, clearOnInit: Boolean): RolloutCacheManager { - return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheConfiguration.builder().expirationDays(expiration).clearOnInit(clearOnInit).build(), mCleanUpDatabaseTask, mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) + return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheConfiguration.builder().expirationDays(expiration).clearOnInit(clearOnInit).build(), mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) } private fun createMockedTimestamp(period: Long): Long { diff --git a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java index 20801d90b..7ee312106 100644 --- a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java +++ b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java @@ -321,8 +321,7 @@ private SplitClientContainerImpl getSplitClientContainer(String mDefaultMatching mConfig, mSplitClientFactory, mClientComponentsRegister, - mWorkManagerWrapper, - mEventsTracker + mWorkManagerWrapper ); } diff --git a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt index 6d5f2b77c..af0b3609a 100644 --- a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt +++ b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt @@ -4,6 +4,8 @@ import io.split.android.client.service.executor.SplitTaskExecutionStatus import io.split.android.client.service.executor.SplitTaskType import io.split.android.client.storage.db.EventDao import io.split.android.client.storage.db.EventEntity +import io.split.android.client.storage.db.GeneralInfoDao +import io.split.android.client.storage.db.GeneralInfoEntity import io.split.android.client.storage.db.ImpressionDao import io.split.android.client.storage.db.ImpressionEntity import io.split.android.client.storage.db.ImpressionsCountDao @@ -12,8 +14,6 @@ import io.split.android.client.storage.db.MyLargeSegmentDao import io.split.android.client.storage.db.MyLargeSegmentEntity import io.split.android.client.storage.db.MySegmentDao import io.split.android.client.storage.db.MySegmentEntity -import io.split.android.client.storage.db.SegmentDao -import io.split.android.client.storage.db.SegmentEntity import io.split.android.client.storage.db.SplitDao import io.split.android.client.storage.db.SplitEntity import io.split.android.client.storage.db.SplitRoomDatabase @@ -69,6 +69,9 @@ class ApplyCipherTaskTest { @Mock private lateinit var attributesDao: AttributesDao + @Mock + private lateinit var generalInfoDao: GeneralInfoDao + private lateinit var applyCipherTask: ApplyCipherTask @Before @@ -82,6 +85,7 @@ class ApplyCipherTaskTest { `when`(splitDatabase.impressionsCountDao()).thenReturn(impressionsCountDao) `when`(splitDatabase.uniqueKeysDao()).thenReturn(uniqueKeysDao) `when`(splitDatabase.attributesDao()).thenReturn(attributesDao) + `when`(splitDatabase.generalInfoDao()).thenReturn(generalInfoDao) `when`(fromCipher.decrypt(anyString())).thenAnswer { invocation -> "decrypted_${invocation.arguments[0]}" } `when`(toCipher.encrypt(anyString())).thenAnswer { invocation -> "encrypted_${invocation.arguments[0]}" } @@ -123,6 +127,38 @@ class ApplyCipherTaskTest { verify(splitDao).update("name2", "encrypted_decrypted_name2", "encrypted_decrypted_body2") } + @Test + fun `traffic types are migrated`() { + `when`(generalInfoDao.getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP)).thenReturn( + GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, "trafficTypesMap") + ) + + applyCipherTask.execute() + + verify(generalInfoDao).getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP) + verify(fromCipher).decrypt("trafficTypesMap") + verify(toCipher).encrypt("decrypted_trafficTypesMap") + verify(generalInfoDao).update(argThat { + it.stringValue.equals("encrypted_decrypted_trafficTypesMap") + }) + } + + @Test + fun `flag sets are migrated`() { + `when`(generalInfoDao.getByName(GeneralInfoEntity.FLAG_SETS_MAP)).thenReturn( + GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, "flagSetsMap") + ) + + applyCipherTask.execute() + + verify(generalInfoDao).getByName(GeneralInfoEntity.FLAG_SETS_MAP) + verify(fromCipher).decrypt("flagSetsMap") + verify(toCipher).encrypt("decrypted_flagSetsMap") + verify(generalInfoDao).update(argThat { + it.stringValue.equals("encrypted_decrypted_flagSetsMap") + }) + } + @Test fun `segments are migrated`() { `when`(mySegmentDao.all).thenReturn( diff --git a/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java b/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java index 9fc67ff9d..0d0e3270a 100644 --- a/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java @@ -2,12 +2,10 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; @@ -15,10 +13,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import io.split.android.client.dtos.Split; -import io.split.android.client.service.executor.parallel.SplitDeferredTaskItem; import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutor; import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.db.SplitEntity; @@ -37,30 +36,19 @@ public void setUp() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(2); when(mSplitCipher.decrypt(any())).then((Answer) invocation -> (String) invocation.getArguments()[0]); - mConverter = new SplitEntityToSplitTransformer(mSplitTaskExecutor, mSplitCipher); - } - - @Test - public void tasksAreCreatedAccordingToTheAmountOfThreadsAvailable() { - ArgumentCaptor>>> argumentCaptor = ArgumentCaptor.forClass(List.class); - - when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); - List mockEntities = getMockEntities(65); - - int expectedNumberOfLists = 5; - - mConverter.transform(mockEntities); - - verify(mSplitTaskExecutor).execute(argumentCaptor.capture()); - assertEquals(expectedNumberOfLists, argumentCaptor.getValue().size()); + mConverter = new SplitEntityToSplitTransformer(mSplitCipher); } @Test public void amountOfSplitsEqualsAmountOfEntities() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); List mockEntities = getMockEntities(3); + Map map = new HashMap<>(); + for (SplitEntity entity : mockEntities) { + map.put(entity.getName(), entity); + } - List splits = mConverter.transform(mockEntities); + List splits = mConverter.transform(map); assertEquals(3, splits.size()); } @@ -69,12 +57,16 @@ public void amountOfSplitsEqualsAmountOfEntities() { public void amountOfSplitsEqualsAmountOfEntitiesWhenParallel() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(2); List mockEntities = getMockEntities(3); + Map map = new HashMap<>(); + for (SplitEntity entity : mockEntities) { + map.put(entity.getName(), entity); + } when(mSplitTaskExecutor.execute(any())).thenReturn( Arrays.asList(Collections.singletonList(new Split()), Collections.singletonList(new Split()), Collections.singletonList(new Split()))); - List splits = mConverter.transform(mockEntities); + List splits = mConverter.transform(map); assertEquals(3, splits.size()); } @@ -83,7 +75,7 @@ public void amountOfSplitsEqualsAmountOfEntitiesWhenParallel() { public void transformingNullReturnsEmptyList() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); - List splits = mConverter.transform(null); + List splits = mConverter.transform((Map) null); assertEquals(0, splits.size()); } diff --git a/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java b/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java index c2a1903aa..3df044115 100644 --- a/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import io.split.android.client.dtos.Split; import io.split.android.client.service.executor.parallel.SplitDeferredTaskItem; @@ -85,7 +86,7 @@ public void amountOfSplitsEqualsAmountOfEntitiesWhenParallel() { public void transformingNullReturnsEmptyList() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); - List splits = mConverter.transform(null); + List splits = mConverter.transform((Map) null); assertEquals(0, splits.size()); } diff --git a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java index ea28aff03..38c07f3c0 100644 --- a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java @@ -20,7 +20,10 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; import io.split.android.client.storage.cipher.SplitCipher; @@ -28,6 +31,7 @@ import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.SplitDao; import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitQueryDao; import io.split.android.client.storage.db.SplitRoomDatabase; public class SqLitePersistentSplitsStorageTest { @@ -44,6 +48,8 @@ public class SqLitePersistentSplitsStorageTest { private SplitCipher mCipher; private SqLitePersistentSplitsStorage mStorage; private AutoCloseable mAutoCloseable; + private final Map> mFlagSets = new HashMap<>(); + private final Map mTrafficTypes = new HashMap<>(); @Before public void setUp() { @@ -70,12 +76,20 @@ public void tearDown() throws Exception { @Test public void getAllUsesTransformer() { List mockEntities = getMockEntities(); + Map map = new HashMap<>(); + for (SplitEntity entity : mockEntities) { + map.put(entity.getName(), entity); + } + when(mSplitDao.getAll()).thenReturn(mockEntities); when(mDatabase.splitDao()).thenReturn(mSplitDao); + SplitQueryDao queryDao = mock(SplitQueryDao.class); + when(queryDao.getAllAsMap()).thenReturn(map); + when(mDatabase.getSplitQueryDao()).thenReturn(queryDao); mStorage.getAll(); - verify(mEntityToSplitTransformer).transform(mockEntities); + verify(mEntityToSplitTransformer).transform(map); } @Test @@ -129,7 +143,7 @@ public void updateRemovesEncryptedSplitNames() { ProcessedSplitChange change = new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, timestamp); when(mCipher.encrypt(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0) + "_encrypted"); - mStorage.update(change); + mStorage.update(change, mTrafficTypes, mFlagSets); verify(mSplitDao).delete(argThat(list -> list.contains("split-1_encrypted") && list.contains("split-2_encrypted") && list.contains("split-3_encrypted") && list.size() == 3)); } @@ -153,14 +167,14 @@ public void updateForSplitChangeUsesTransformer() { ProcessedSplitChange change = new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, timestamp); when(mCipher.encrypt(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0) + "_encrypted"); - mStorage.update(change); + mStorage.update(change, mTrafficTypes, mFlagSets); verify(mSplitToSplitEntityTransformer).transform(activeSplits); } @Test public void updatingNullSplitChangeDoesNotInteractWithDatabase() { - mStorage.update((ProcessedSplitChange) null); + mStorage.update((ProcessedSplitChange) null, mTrafficTypes, mFlagSets); verifyNoInteractions(mSplitToSplitEntityTransformer); verifyNoInteractions(mCipher);