diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 9aeb52cfe..c2f306a64 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,6 +1,11 @@ name: SonarCloud Analysis on: + push: + branches: + - master + - development + - '*_baseline' pull_request: branches: - '*' @@ -41,111 +46,98 @@ jobs: - name: Build project and run tests with coverage run: | # Build the project - ./gradlew assembleDebug --stacktrace - - # Run tests with coverage - allow test failures - ./gradlew testDebugUnitTest jacocoTestReport --stacktrace - TEST_RESULT=$? - if [ $TEST_RESULT -ne 0 ]; then - echo "Some tests failed, but continuing to check for coverage data..." - # Even if tests fail, JaCoCo should generate a report with partial coverage - # from the tests that did pass - fi - - - name: Prepare class files for SonarQube analysis - run: | - echo "Searching for compiled class files..." - - # Create the target directory - mkdir -p build/intermediates/runtime_library_classes_dir/debug - - # Find all directories containing class files with better patterns - CLASS_DIRS=$(find build -name "*.class" -type f -exec dirname {} \; | sort -u | grep -E "(javac|kotlin-classes|runtime_library)" | head -10) - - if [ -z "$CLASS_DIRS" ]; then - echo "WARNING: No class files found in the build directory!" - echo "Searching in all build subdirectories..." - find build -name "*.class" -type f | head -20 + ./gradlew assembleDebug + + # Run tests - continue even if some tests fail + ./gradlew testDebugUnitTest || echo "Some tests failed, but continuing to generate coverage report..." + + # Generate JaCoCo aggregate report separately (ensures it runs even if tests failed) + ./gradlew jacocoRootReport + + # Log report location for debugging + REPORT_PATH="build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" + if [ -f "$REPORT_PATH" ]; then + echo "✓ JaCoCo report generated at: $REPORT_PATH ($(wc -c < "$REPORT_PATH") bytes)" else - echo "Found class files in the following directories:" - echo "$CLASS_DIRS" - - # Copy classes from all relevant directories, not just the first one - for CLASS_DIR in $CLASS_DIRS; do - if [ -d "$CLASS_DIR" ] && [ "$(find "$CLASS_DIR" -name "*.class" | wc -l)" -gt 0 ]; then - echo "Copying classes from $CLASS_DIR" - cp -r "$CLASS_DIR"/* build/intermediates/runtime_library_classes_dir/debug/ 2>/dev/null || echo "Failed to copy from $CLASS_DIR" - fi - done - - # Verify the target directory now has class files - CLASS_COUNT=$(find build/intermediates/runtime_library_classes_dir/debug -name "*.class" | wc -l) - echo "Target directory now contains $CLASS_COUNT class files" + echo "✗ JaCoCo report was NOT generated at: $REPORT_PATH" fi - # Update sonar-project.properties with all found class directories - echo "" >> sonar-project.properties - echo "# Additional binary paths found during build" >> sonar-project.properties - if [ -n "$CLASS_DIRS" ]; then - # Convert newlines to commas for sonar.java.binaries - BINARY_PATHS=$(echo "$CLASS_DIRS" | tr '\n' ',' | sed 's/,$//') - echo "sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug,$BINARY_PATHS" >> sonar-project.properties + - name: Verify class files and coverage data for SonarQube analysis + run: | + echo "=== Verifying Build Artifacts for SonarQube ===" + echo "" + + # Dynamically get modules from settings.gradle (extract module names from "include ':modulename'" lines) + MODULES=$(grep "^include" settings.gradle | cut -d"'" -f2 | cut -d":" -f2 | tr '\n' ' ') + echo "Detected modules: $MODULES" + echo "" + + echo "Checking compiled class files for each module:" + for module in $MODULES; do + MODULE_CLASSES_DIR="${module}/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" + if [ -d "$MODULE_CLASSES_DIR" ]; then + CLASS_COUNT=$(find "$MODULE_CLASSES_DIR" -name "*.class" | wc -l) + echo " ✓ ${module}: Found $CLASS_COUNT class files in $MODULE_CLASSES_DIR" + else + echo " ✗ ${module}: Class directory not found: $MODULE_CLASSES_DIR" + fi + done + + echo "" + echo "Checking JaCoCo coverage report:" + if [ -f build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml ]; then + REPORT_SIZE=$(wc -c < build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml) + PACKAGE_COUNT=$(grep -c "> sonar-project.properties + echo " ✗ JaCoCo report file not found" fi - - echo "Checking for JaCoCo report files..." - find build -name "*.xml" | grep jacoco || echo "No JaCoCo XML files found" - find build -name "*.exec" | grep jacoco || echo "No JaCoCo exec files found" - - echo "Contents of JaCoCo report directory:" - ls -la build/reports/jacoco/jacocoTestReport/ || echo "Directory not found" - + echo "" - echo "Checking test execution results:" + echo "Checking JaCoCo execution data for each module:" + for module in $MODULES; do + EXEC_FILE="${module}/build/jacoco/testDebugUnitTest.exec" + if [ -f "$EXEC_FILE" ]; then + EXEC_SIZE=$(wc -c < "$EXEC_FILE") + echo " ✓ ${module}: Found execution data ($EXEC_SIZE bytes) in $EXEC_FILE" + else + echo " ✗ ${module}: Execution data not found: $EXEC_FILE" + fi + done + + echo "" + echo "Test execution summary:" TEST_RESULT_FILES=$(find build -name "TEST-*.xml" 2>/dev/null) if [ -n "$TEST_RESULT_FILES" ]; then - echo "Found test result files:" - echo "$TEST_RESULT_FILES" - # Count total tests, failures, errors TOTAL_TESTS=$(cat $TEST_RESULT_FILES | grep -o 'tests="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}') TOTAL_FAILURES=$(cat $TEST_RESULT_FILES | grep -o 'failures="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}') TOTAL_ERRORS=$(cat $TEST_RESULT_FILES | grep -o 'errors="[0-9]*"' | cut -d'"' -f2 | awk '{sum+=$1} END {print sum}') - echo "Test summary: $TOTAL_TESTS tests, $TOTAL_FAILURES failures, $TOTAL_ERRORS errors" + echo " Tests: $TOTAL_TESTS | Failures: $TOTAL_FAILURES | Errors: $TOTAL_ERRORS" else - echo "No test result files found" + echo " No test result files found" fi - + echo "" - echo "Checking JaCoCo report content:" - if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then - echo "Report file size: $(wc -c < build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) bytes" - echo "First 500 chars of report:" - head -c 500 build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml - echo "" - echo "" - echo "Counting coverage elements:" - grep -c " + command: | + # Install required tools + apt-get update + apt-get install -y curl jq git + + # Extract PR information from trigger payload + PR_NUMBER="<+trigger.payload.pull_request.number>" + PR_TITLE="<+trigger.payload.pull_request.title>" + PR_AUTHOR="<+trigger.payload.pull_request.user.login>" + PR_HEAD_REF="<+trigger.payload.pull_request.head.ref>" + PR_BASE_REF="<+trigger.payload.pull_request.base.ref>" + PR_HEAD_SHA="<+trigger.payload.pull_request.head.sha>" + REPO_FULL_NAME="<+trigger.payload.repository.full_name>" + + echo "=== PR Information ===" + echo "PR Number: $PR_NUMBER" + echo "PR Title: $PR_TITLE" + echo "PR Author: $PR_AUTHOR" + echo "Source Branch (HEAD): $PR_HEAD_REF" + echo "Target Branch (BASE): $PR_BASE_REF" + echo "Head SHA: $PR_HEAD_SHA" + echo "Repository: $REPO_FULL_NAME" + echo "=== End PR Info ===" + + # Check if this is a PR build + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ] && [ "$PR_NUMBER" != "" ]; then + echo "✅ Detected PR build" + SOURCE_BRANCH="$PR_HEAD_REF" + TARGET_BRANCH="$PR_BASE_REF" + IS_PR_BUILD="true" + else + echo "ℹ️ Not a PR build - using fallback values" + SOURCE_BRANCH="<+codebase.branch>" + TARGET_BRANCH="main" + IS_PR_BUILD="false" + PR_NUMBER="" + fi + + echo "Final branch configuration:" + echo " Source Branch: $SOURCE_BRANCH" + echo " Target Branch: $TARGET_BRANCH" + echo " Is PR Build: $IS_PR_BUILD" + + # Export variables for subsequent steps + cat > /harness/pr_info.sh << EOF + export PR_NUMBER="$PR_NUMBER" + export PR_TITLE="$PR_TITLE" + export PR_AUTHOR="$PR_AUTHOR" + export SOURCE_BRANCH="$SOURCE_BRANCH" + export TARGET_BRANCH="$TARGET_BRANCH" + export PR_HEAD_SHA="$PR_HEAD_SHA" + export REPO_FULL_NAME="$REPO_FULL_NAME" + export IS_PR_BUILD="$IS_PR_BUILD" + EOF + + chmod +x /harness/pr_info.sh + echo "=== Exported Variables ===" + cat /harness/pr_info.sh + - step: + type: Run + name: Install Dependencies + identifier: install_dependencies + spec: + shell: Sh + command: | + # Install basic tools needed by install-deps.sh + apt-get update -qq + apt-get install -y curl unzip + + # Use the existing install-deps.sh script for consistent setup + chmod +x .harness/scripts/install-deps.sh + .harness/scripts/install-deps.sh + - step: + type: Run + name: Run Diffuse Script with Branch Parameters + identifier: run_diffuse + spec: + shell: Sh + envVariables: + GH_TOKEN: <+secrets.getValue("github-devops-token")> + command: | + set -e + + # Set Android SDK environment variables (matching instrumented pipeline) + export ANDROID_SDK_ROOT=$ANDROID_HOME + export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java)))) + export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin" + + # Verify environment + echo "=== Environment Verification ===" + echo "JAVA_HOME: $JAVA_HOME" + echo "ANDROID_HOME: $ANDROID_HOME" + java -version + + # Source the PR information (use . for sh compatibility) + . /harness/pr_info.sh + + echo "=== Running Diffuse Analysis ===" + echo "Source Branch: $SOURCE_BRANCH" + echo "Target Branch: $TARGET_BRANCH" + + # Fetch all branches from remote so diffuse.sh can access them + echo "Fetching branches from remote..." + git fetch origin --prune > /dev/null 2>&1 + git fetch origin "$SOURCE_BRANCH" > /dev/null 2>&1 || true + git fetch origin "$TARGET_BRANCH" > /dev/null 2>&1 || true + + # Debug: Show what branches/refs exist + echo "=== Available branches and refs ===" + git branch -r | grep -E "(${SOURCE_BRANCH}|${TARGET_BRANCH})" || true + echo "Current HEAD: $(git rev-parse HEAD)" + echo "origin/${SOURCE_BRANCH}: $(git rev-parse origin/${SOURCE_BRANCH} 2>/dev/null || echo 'NOT FOUND')" + echo "origin/${TARGET_BRANCH}: $(git rev-parse origin/${TARGET_BRANCH} 2>/dev/null || echo 'NOT FOUND')" + + # Use remote branch references (origin/branch-name) for the diffuse script + # This ensures we compare the actual remote branches, not local refs + SOURCE_REF="origin/${SOURCE_BRANCH}" + TARGET_REF="origin/${TARGET_BRANCH}" + + echo "✓ Branches fetched" + echo "Will compare: $SOURCE_REF (source) vs $TARGET_REF (target)" + + # Verify the script exists + if [ ! -f "scripts/diffuse.sh" ]; then + echo "❌ Error: scripts/diffuse.sh not found" + echo "Available files in scripts/:" + ls -la scripts/ || echo "scripts/ directory not found" + exit 1 + fi + + # Make the script executable + chmod +x scripts/diffuse.sh + + # Run the diffuse script with remote branch references in verbose mode for debugging + echo "Executing: ./scripts/diffuse.sh --source '$SOURCE_REF' --target '$TARGET_REF' --verbose" + + # Run and capture output + set +e # Don't exit on error + ./scripts/diffuse.sh --source "$SOURCE_REF" --target "$TARGET_REF" --verbose > diffuse_output_full.txt 2>&1 + DIFFUSE_EXIT_CODE=$? + set -e + + echo "" + echo "=== Diffuse Script Exit Code: $DIFFUSE_EXIT_CODE ===" + + # Show the full verbose output for debugging + echo "=== Full Diffuse Output (for debugging) ===" + cat diffuse_output_full.txt + echo "=== End Full Output ===" + + # Extract only the comparison tables from the output + sed 's/\x1b\[[0-9;]*m//g' diffuse_output_full.txt | \ + awk '/^ (AAR|JAR).*old.*new.*diff/,/^$/ { + if (/^ (AAR|JAR)/ || /─/ || /┼/ || /│/ || /^ *total/ || /^ *(jar|manifest|other|classes|methods|fields)/) { + print + } + }' > /harness/diffuse_output.txt + + # Display the filtered output + echo "=== Diffuse Analysis Results ===" + cat /harness/diffuse_output.txt + echo "" + + # Save exit code for the comment step + echo "$DIFFUSE_EXIT_CODE" > /harness/diffuse_exit_code.txt + + # Fail the step if diffuse failed + if [ "$DIFFUSE_EXIT_CODE" != "0" ]; then + echo "❌ Diffuse script failed with exit code: $DIFFUSE_EXIT_CODE" + exit "$DIFFUSE_EXIT_CODE" + fi + - step: + type: Run + name: Comment on PR + identifier: comment_on_pr + spec: + shell: Sh + envVariables: + GH_TOKEN: <+secrets.getValue("github-devops-token")> + command: | + # Source PR information (use . for sh compatibility) + . /harness/pr_info.sh + + # Check if this is a PR build + if [ "$IS_PR_BUILD" != "true" ] || [ -z "$PR_NUMBER" ]; then + echo "ℹ️ Not a PR build, skipping comment" + exit 0 + fi + + # Get the diffuse script exit code + DIFFUSE_EXIT_CODE=$(cat /harness/diffuse_exit_code.txt 2>/dev/null || echo "unknown") + + # Only post comment if analysis was successful + if [ "$DIFFUSE_EXIT_CODE" != "0" ]; then + echo "⚠️ Diffuse analysis failed (exit code: $DIFFUSE_EXIT_CODE), skipping PR comment" + exit 1 + fi + + # Read the diffuse output + if [ -f /harness/diffuse_output.txt ]; then + DIFFUSE_OUTPUT=$(cat /harness/diffuse_output.txt) + else + DIFFUSE_OUTPUT="Diffuse script output not found" + fi + + # Create the comment body + COMMENT_BODY="## 📱 Android Diffuse Analysis Results ✅ + + **Source Branch:** \`$SOURCE_BRANCH\` + **Target Branch:** \`$TARGET_BRANCH\` + **Commit:** \`$PR_HEAD_SHA\` + + ### Script Output: + \`\`\` + $DIFFUSE_OUTPUT + \`\`\`" + + # Escape the comment body for JSON + ESCAPED_COMMENT=$(echo "$COMMENT_BODY" | jq -Rs .) + + # Post comment to GitHub + echo "Posting comment to PR #$PR_NUMBER in $REPO_FULL_NAME..." + + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Authorization: token $GH_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: application/vnd.github.v3+json" \ + -d "{\"body\": $ESCAPED_COMMENT}" \ + "https://api.github.com/repos/$REPO_FULL_NAME/issues/$PR_NUMBER/comments") + + # Extract HTTP code from response + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + RESPONSE_BODY=$(echo "$RESPONSE" | head -n -1) + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then + echo "✅ Comment posted successfully to PR #$PR_NUMBER" + echo "Comment URL: $(echo "$RESPONSE_BODY" | jq -r '.html_url // "N/A"')" + else + echo "❌ Failed to post comment. HTTP code: $HTTP_CODE" + echo "Response: $RESPONSE_BODY" + exit 1 + fi + + echo "✅ Comment posted successfully and pipeline completed" + when: + stageStatus: All + properties: + ci: + codebase: + connectorRef: <+input> + repoName: <+input> + build: <+input> diff --git a/.harness/orgs/PROD/projects/Harness_Split/pipelines/androidclientdiffuse/input_sets/androidclient_PR.yaml b/.harness/orgs/PROD/projects/Harness_Split/pipelines/androidclientdiffuse/input_sets/androidclient_PR.yaml new file mode 100644 index 000000000..e4901f93b --- /dev/null +++ b/.harness/orgs/PROD/projects/Harness_Split/pipelines/androidclientdiffuse/input_sets/androidclient_PR.yaml @@ -0,0 +1,43 @@ +inputSet: + name: androidclient_PR + identifier: androidclient_PR + orgIdentifier: PROD + projectIdentifier: Harness_Split + pipeline: + identifier: androidclientdiffuse + stages: + - stage: + identifier: build_and_analyze + type: CI + spec: + infrastructure: + type: KubernetesDirect + spec: + connectorRef: "" + namespace: "" + execution: + steps: + - step: + identifier: setup_environment + type: Run + spec: + connectorRef: "" + - step: + identifier: run_diffuse + type: Run + spec: + connectorRef: "" + - step: + identifier: comment_on_pr + type: Run + spec: + connectorRef: "" + properties: + ci: + codebase: + connectorRef: fmegithubrunnersci + repoName: android-client + build: + type: PR + spec: + number: <+trigger.prNumber> diff --git a/.harness/scripts/install-deps.sh b/.harness/scripts/install-deps.sh index bbe6e6795..ff67049b9 100755 --- a/.harness/scripts/install-deps.sh +++ b/.harness/scripts/install-deps.sh @@ -11,17 +11,24 @@ echo "" # ============================================ echo "=== Installing Java 17 ===" +# Detect if we need sudo (not running as root) +if [ "$EUID" -eq 0 ] || [ "$(id -u)" -eq 0 ]; then + SUDO="" +else + SUDO="sudo" +fi + # Install Java 17 echo "Installing OpenJDK 17..." -sudo apt-get update -qq -sudo apt-get install -y openjdk-17-jdk +$SUDO apt-get update -qq +$SUDO apt-get install -y openjdk-17-jdk # Set JAVA_HOME to Java 17 explicitly export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 # Update alternatives to use Java 17 as default -sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java -sudo update-alternatives --set javac /usr/lib/jvm/java-17-openjdk-amd64/bin/javac +$SUDO update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java +$SUDO update-alternatives --set javac /usr/lib/jvm/java-17-openjdk-amd64/bin/javac echo "" echo "Java 17 installation complete:" diff --git a/CHANGES.txt b/CHANGES.txt index 84eaa9379..86aee3a0e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +5.5.0 (Jan 28, 2026) +- Added functionality to provide metadata alongside SDK update, ready and ready from cache events. Read more in our docs. +- Fixed issue in which TLS 1.2 was being forced for new connections. + 5.4.2 (Oct 30, 2025) - Added support for Fallback Treatments in LOCALHOST mode. diff --git a/LICENSE.txt b/LICENSE.txt index df08de3fb..b6579621e 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright © 2025 Split Software, Inc. +Copyright © 2026 Split Software, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/api/README.md b/api/README.md new file mode 100644 index 000000000..e455d981e --- /dev/null +++ b/api/README.md @@ -0,0 +1,5 @@ +# API module + +This module contains the public API interfaces and types exposed to consumers of the Split SDK. + +Classes in this module are part of the public API contract and should maintain backwards compatibility. diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 000000000..c32f26549 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'com.android.library' +} + +apply from: "$rootDir/gradle/common-android-library.gradle" + +android { + namespace 'io.split.android.client.api' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation libs.annotation + + testImplementation libs.junit4 + testImplementation libs.mockitoCore +} + diff --git a/api/consumer-rules.pro b/api/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/api/proguard-rules.pro b/api/proguard-rules.pro new file mode 100644 index 000000000..cf504086a --- /dev/null +++ b/api/proguard-rules.pro @@ -0,0 +1,22 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + diff --git a/api/src/androidTest/java/.gitkeep b/api/src/androidTest/java/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml similarity index 57% rename from src/main/AndroidManifest.xml rename to api/src/main/AndroidManifest.xml index 6b2f52238..cf2d636b6 100644 --- a/src/main/AndroidManifest.xml +++ b/api/src/main/AndroidManifest.xml @@ -1,3 +1,5 @@ - + + + + diff --git a/src/main/java/io/split/android/client/EvaluationOptions.java b/api/src/main/java/io/split/android/client/EvaluationOptions.java similarity index 100% rename from src/main/java/io/split/android/client/EvaluationOptions.java rename to api/src/main/java/io/split/android/client/EvaluationOptions.java diff --git a/src/main/java/io/split/android/client/SplitClient.java b/api/src/main/java/io/split/android/client/SplitClient.java similarity index 89% rename from src/main/java/io/split/android/client/SplitClient.java rename to api/src/main/java/io/split/android/client/SplitClient.java index 63d35f457..0ce84351d 100644 --- a/src/main/java/io/split/android/client/SplitClient.java +++ b/api/src/main/java/io/split/android/client/SplitClient.java @@ -7,6 +7,7 @@ import java.util.Map; import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.events.SplitEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; @@ -179,6 +180,42 @@ public interface SplitClient extends AttributesManager { void on(SplitEvent event, SplitEventTask task); + /** + * Registers an event listener for SDK events that provide typed metadata. + *

+ * This method provides type-safe callbacks for SDK_READY, SDK_UPDATE, and SDK_READY_FROM_CACHE events. + * Override the methods you need in the listener. + *

+ * Multiple listeners can be registered. Each listener will be invoked once per event. + *

+ * Example usage: + *

{@code
+     * client.addEventListener(new SplitEventListener() {
+     *     @Override
+     *     public void onReady(SplitClient client, SdkReadyMetadata metadata) {
+     *         Boolean initialCacheLoad = metadata.isInitialCacheLoad();
+     *         // Handle SDK ready on background thread
+     *     }
+     *
+     *     @Override
+     *     public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
+     *         SdkUpdateMetadata.Type type = metadata.getType(); // FLAGS_UPDATE or SEGMENTS_UPDATE
+     *         List names = metadata.getNames(); // updated flag names
+     *         // Handle on background thread
+     *     }
+     *
+     *     @Override
+     *     public void onReadyFromCacheView(SplitClient client, SdkReadyMetadata metadata) {
+     *         // Handle on main/UI thread
+     *         Boolean initialCacheLoad = metadata.isInitialCacheLoad();
+     *     }
+     * });
+     * }
+ * + * @param listener the event listener to register. Must not be null. + */ + void addEventListener(@NonNull SplitEventListener listener); + /** * Enqueue a new event to be sent to Split data collection services. *

diff --git a/src/main/java/io/split/android/client/SplitResult.java b/api/src/main/java/io/split/android/client/SplitResult.java similarity index 100% rename from src/main/java/io/split/android/client/SplitResult.java rename to api/src/main/java/io/split/android/client/SplitResult.java diff --git a/src/main/java/io/split/android/client/api/Key.java b/api/src/main/java/io/split/android/client/api/Key.java similarity index 100% rename from src/main/java/io/split/android/client/api/Key.java rename to api/src/main/java/io/split/android/client/api/Key.java diff --git a/src/main/java/io/split/android/client/attributes/AttributesManager.java b/api/src/main/java/io/split/android/client/attributes/AttributesManager.java similarity index 100% rename from src/main/java/io/split/android/client/attributes/AttributesManager.java rename to api/src/main/java/io/split/android/client/attributes/AttributesManager.java diff --git a/api/src/main/java/io/split/android/client/events/SdkReadyMetadata.java b/api/src/main/java/io/split/android/client/events/SdkReadyMetadata.java new file mode 100644 index 000000000..977576373 --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkReadyMetadata.java @@ -0,0 +1,52 @@ +package io.split.android.client.events; + +import androidx.annotation.Nullable; + +/** + * Typed metadata for SDK_READY and SDK_READY_FROM_CACHE events. + *

+ * Contains information about the cache state when the SDK becomes ready. + */ +public final class SdkReadyMetadata { + + @Nullable + private final Boolean mInitialCacheLoad; + + @Nullable + private final Long mLastUpdateTimestamp; + + /** + * Creates a new SdkReadyMetadata instance. + * + * @param initialCacheLoad true if this is an initial cache load with no usable cache, or null if not available + * @param lastUpdateTimestamp the last successful cache timestamp in milliseconds since epoch, or null if not available + */ + public SdkReadyMetadata(@Nullable Boolean initialCacheLoad, @Nullable Long lastUpdateTimestamp) { + mInitialCacheLoad = initialCacheLoad; + mLastUpdateTimestamp = lastUpdateTimestamp; + } + + /** + * Returns whether this is an initial cache load with no usable cache. + *

+ * This is true when the SDK starts without any prior cached data (fresh install), + * meaning data was fetched from the server for the first time. + * + * @return true if initial cache load, false otherwise, or null if not available + */ + @Nullable + public Boolean isInitialCacheLoad() { + return mInitialCacheLoad; + } + + /** + * Returns the last successful cache timestamp in milliseconds since epoch. + * + * @return the timestamp, or null if not available + */ + @Nullable + public Long getLastUpdateTimestamp() { + return mLastUpdateTimestamp; + } +} + diff --git a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java new file mode 100644 index 000000000..83dea400a --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java @@ -0,0 +1,75 @@ +package io.split.android.client.events; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * Typed metadata for SDK_UPDATE events. + *

+ * Contains information about the type of update and the names of entities that were updated. + */ +public final class SdkUpdateMetadata { + + /** + * The type of update that triggered the SDK_UPDATE event. + */ + public enum Type { + /** + * Feature flags were updated. + *

+ * {@link #getNames()} returns the list of flag names that changed. + */ + FLAGS_UPDATE, + + /** + * Segments were updated (rule-based segments, memberships, or large segments). + *

+ * Note: {@link #getNames()} always returns an empty list for this type. + * Segment names are not included in the metadata. + */ + SEGMENTS_UPDATE + } + + @Nullable + private final Type mType; + + @NonNull + private final List mNames; + + /** + * Creates a new SdkUpdateMetadata instance. + * + * @param type the type of update, or null if not available + * @param names the list of entity names that were updated, or null to use an empty list + */ + public SdkUpdateMetadata(@Nullable Type type, @Nullable List names) { + mType = type; + mNames = names != null ? names : Collections.emptyList(); + } + + /** + * Returns the type of update that triggered this event. + * + * @return the update type, or null if not available + */ + @Nullable + public Type getType() { + return mType; + } + + /** + * Returns the list of entity names that changed in this update. + *

+ * For {@link Type#FLAGS_UPDATE}, this contains flag names that were updated. + * For {@link Type#SEGMENTS_UPDATE}, this is always an empty list (segment names are not included). + * + * @return the list of updated entity names, never null (empty list for SEGMENTS_UPDATE or if none) + */ + @NonNull + public List getNames() { + return mNames; + } +} diff --git a/src/main/java/io/split/android/client/events/SplitEvent.java b/api/src/main/java/io/split/android/client/events/SplitEvent.java similarity index 100% rename from src/main/java/io/split/android/client/events/SplitEvent.java rename to api/src/main/java/io/split/android/client/events/SplitEvent.java diff --git a/api/src/main/java/io/split/android/client/events/SplitEventListener.java b/api/src/main/java/io/split/android/client/events/SplitEventListener.java new file mode 100644 index 000000000..424f5503b --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SplitEventListener.java @@ -0,0 +1,115 @@ +package io.split.android.client.events; + +import io.split.android.client.SplitClient; + +/** + * Abstract class for handling SDK events with typed metadata. + *

+ * Extend this class and override the methods you need to handle specific SDK events. + * Each event has two callback options: + *

    + *
  • Background thread callbacks (e.g., {@link #onUpdate}) - executed immediately on a background thread
  • + *
  • Main thread callbacks (e.g., {@link #onUpdateView}) - executed on the main/UI thread
  • + *
+ *

+ * Example usage: + *

{@code
+ * client.addEventListener(new SdkEventListener() {
+ *     @Override
+ *     public void onReady(SplitClient client, SdkReadyMetadata metadata) {
+ *         Boolean initialCacheLoad = metadata.isInitialCacheLoad();
+ *         // Handle ready on background thread
+ *     }
+ *
+ *     @Override
+ *     public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
+ *         SdkUpdateMetadata.Type type = metadata.getType(); // FLAGS_UPDATE or SEGMENTS_UPDATE
+ *         List names = metadata.getNames(); // updated flag/segment names
+ *         // Handle updates on background thread
+ *     }
+ *
+ *     @Override
+ *     public void onReadyFromCacheView(SplitClient client, SdkReadyMetadata metadata) {
+ *         // Handle cache ready on main/UI thread
+ *         Boolean initialCacheLoad = metadata.isInitialCacheLoad();
+ *     }
+ * });
+ * }
+ */ +public abstract class SplitEventListener { + + /** + * Called when SDK_READY event occurs, executed on a background thread. + *

+ * Override this method to handle SDK_READY events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing ready state information + */ + public void onReady(SplitClient client, SdkReadyMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_READY event occurs, executed on the main/UI thread. + *

+ * Override this method to handle SDK_READY events with typed metadata on the main thread. + * Use this when you need to update UI components. + * + * @param client the Split client instance + * @param metadata the typed metadata containing ready state information + */ + public void onReadyView(SplitClient client, SdkReadyMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_UPDATE event occurs, executed on a background thread. + *

+ * Override this method to handle SDK_UPDATE events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing updated flag information + */ + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_READY_FROM_CACHE event occurs, executed on a background thread. + *

+ * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing cache information + */ + public void onReadyFromCache(SplitClient client, SdkReadyMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_UPDATE event occurs, executed on the main/UI thread. + *

+ * Override this method to handle SDK_UPDATE events with typed metadata on the main thread. + * Use this when you need to update UI components. + * + * @param client the Split client instance + * @param metadata the typed metadata containing updated flag information + */ + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_READY_FROM_CACHE event occurs, executed on the main/UI thread. + *

+ * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata on the main thread. + * Use this when you need to update UI components. + * + * @param client the Split client instance + * @param metadata the typed metadata containing cache information + */ + public void onReadyFromCacheView(SplitClient client, SdkReadyMetadata metadata) { + // Default empty implementation + } +} diff --git a/api/src/main/java/io/split/android/client/events/SplitEventTask.java b/api/src/main/java/io/split/android/client/events/SplitEventTask.java new file mode 100644 index 000000000..7c053b55f --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SplitEventTask.java @@ -0,0 +1,58 @@ +package io.split.android.client.events; + +import io.split.android.client.SplitClient; + +/** + * Base class for handling Split SDK events. + *

+ * Extend this class and override the methods you need to handle specific SDK events. + *

+ * Threading: + *

    + *
  • {@code onPostExecution} methods are called on a background thread (faster, executed immediately)
  • + *
  • {@code onPostExecutionView} methods are called on the main/UI thread (queued on main looper)
  • + *
+ *

+ * For events with metadata (like SDK_UPDATE or SDK_READY_FROM_CACHE), use + * {@link SplitEventListener} instead for type-safe metadata access. + *

+ * Example usage: + *

{@code
+ * client.on(SplitEvent.SDK_READY, new SplitEventTask() {
+ *     @Override
+ *     public void onPostExecution(SplitClient client) {
+ *         // SDK is ready, start using Split
+ *     }
+ * });
+ * }
+ */ +public class SplitEventTask { + /** + * Called when an event occurs, executed on a background thread. + *

+ * Override this method to handle events on a background thread. + * This method is executed immediately and is faster than {@link #onPostExecutionView(SplitClient)}. + * + * @param client the Split client instance + * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) + */ + public void onPostExecution(SplitClient client) { + throw new SplitEventTaskMethodNotImplementedException(); + } + + /** + * Called when an event occurs, executed on the main/UI thread. + *

+ * Override this method to handle events on the main thread. + * Use this when you need to update UI components. + *

+ * Note: This method is queued on the main looper, so execution may be delayed + * compared to {@link #onPostExecution(SplitClient)}. + * + * @param client the Split client instance + * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) + */ + public void onPostExecutionView(SplitClient client) { + throw new SplitEventTaskMethodNotImplementedException(); + } +} diff --git a/src/main/java/io/split/android/client/events/SplitEventTaskMethodNotImplementedException.java b/api/src/main/java/io/split/android/client/events/SplitEventTaskMethodNotImplementedException.java similarity index 100% rename from src/main/java/io/split/android/client/events/SplitEventTaskMethodNotImplementedException.java rename to api/src/main/java/io/split/android/client/events/SplitEventTaskMethodNotImplementedException.java diff --git a/api/src/test/java/io/split/android/client/api/.gitkeep b/api/src/test/java/io/split/android/client/api/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/test/java/io/split/android/client/events/SdkReadyMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkReadyMetadataTest.java new file mode 100644 index 000000000..35d898c57 --- /dev/null +++ b/api/src/test/java/io/split/android/client/events/SdkReadyMetadataTest.java @@ -0,0 +1,66 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class SdkReadyMetadataTest { + + @Test + public void isInitialCacheLoadReturnsNullWhenConstructedWithNull() { + SdkReadyMetadata metadata = new SdkReadyMetadata(null, null); + + assertNull(metadata.isInitialCacheLoad()); + } + + @Test + public void isInitialCacheLoadReturnsTrueWhenConstructedWithTrue() { + SdkReadyMetadata metadata = new SdkReadyMetadata(true, null); + + assertTrue(metadata.isInitialCacheLoad()); + } + + @Test + public void isInitialCacheLoadReturnsFalseWhenConstructedWithFalse() { + SdkReadyMetadata metadata = new SdkReadyMetadata(false, null); + + assertFalse(metadata.isInitialCacheLoad()); + } + + @Test + public void getLastUpdateTimestampReturnsNullWhenConstructedWithNull() { + SdkReadyMetadata metadata = new SdkReadyMetadata(null, null); + + assertNull(metadata.getLastUpdateTimestamp()); + } + + @Test + public void getLastUpdateTimestampReturnsValueWhenConstructedWithValue() { + long timestamp = 1704067200000L; + SdkReadyMetadata metadata = new SdkReadyMetadata(null, timestamp); + + assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp()); + } + + @Test + public void bothValuesReturnCorrectlyWhenBothAreSet() { + long timestamp = 1704067200000L; + SdkReadyMetadata metadata = new SdkReadyMetadata(true, timestamp); + + assertTrue(metadata.isInitialCacheLoad()); + assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp()); + } + + @Test + public void bothValuesReturnCorrectlyWhenInitialCacheLoadIsFalse() { + long timestamp = 1704067200000L; + SdkReadyMetadata metadata = new SdkReadyMetadata(false, timestamp); + + assertFalse(metadata.isInitialCacheLoad()); + assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp()); + } +} + diff --git a/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java new file mode 100644 index 000000000..fadd2fe09 --- /dev/null +++ b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java @@ -0,0 +1,84 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SdkUpdateMetadataTest { + + @Test + public void getNamesReturnsEmptyListWhenConstructedWithNull() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(null, null); + + assertEquals(Collections.emptyList(), metadata.getNames()); + } + + @Test + public void getNamesReturnsEmptyListWhenConstructedWithEmptyList() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(null, Collections.emptyList()); + + assertEquals(Collections.emptyList(), metadata.getNames()); + } + + @Test + public void getNamesReturnsListWhenConstructedWithList() { + List names = Arrays.asList("flag1", "flag2", "flag3"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, names); + + assertEquals(names, metadata.getNames()); + } + + @Test + public void getNamesReturnsSingleItemList() { + List names = Collections.singletonList("singleFlag"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, names); + + assertEquals(names, metadata.getNames()); + assertEquals(1, metadata.getNames().size()); + assertEquals("singleFlag", metadata.getNames().get(0)); + } + + @Test + public void getTypeReturnsNullWhenConstructedWithNull() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(null, null); + + assertNull(metadata.getType()); + } + + @Test + public void getTypeReturnsFlagsUpdateWhenConstructedWithFlagsUpdate() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, null); + + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, metadata.getType()); + } + + @Test + public void getTypeReturnsSegmentsUpdateWhenConstructedWithSegmentsUpdate() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, null); + + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + } + + @Test + public void flagsUpdateMetadataContainsBothTypeAndNames() { + List flags = Arrays.asList("flag1", "flag2"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, flags); + + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, metadata.getType()); + assertEquals(flags, metadata.getNames()); + } + + @Test + public void segmentsUpdateMetadataContainsBothTypeAndNames() { + List segments = Arrays.asList("segment1", "segment2"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, segments); + + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + assertEquals(segments, metadata.getNames()); + } +} diff --git a/build.gradle b/build.gradle index 5a398e16b..f521dda49 100644 --- a/build.gradle +++ b/build.gradle @@ -5,113 +5,128 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.7.3' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0' - classpath "com.vanniktech:gradle-maven-publish-plugin:0.33.0" + classpath 'com.android.tools.build:gradle:9.0.0-rc02' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10' + classpath "com.vanniktech:gradle-maven-publish-plugin:0.34.0" } } -apply plugin: 'com.android.library' -apply plugin: 'signing' -apply plugin: 'kotlin-android' +apply plugin: 'com.android.fused-library' apply plugin: 'com.vanniktech.maven.publish' -apply from: 'spec.gradle' -apply from: 'jacoco.gradle' +apply from: "$rootDir/gradle/jacoco-root.gradle" ext { - splitVersion = '5.4.2' + splitVersion = '5.5.0' jacocoVersion = '0.8.8' } -// Define exclusions for JaCoCo coverage -def coverageExclusions = [ - '**/R.class', - '**/R$*.class', - '**/BuildConfig.*', - '**/Manifest*.*', - '**/*Test*.*', - 'android/**/*.*' -] +group = 'io.split.client' +version = splitVersion -android { - compileSdk 33 - targetCompatibility = '1.8' - sourceCompatibility = '1.8' - - buildFeatures { - buildConfig true - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 +// Centralize repositories and coordinates for all subprojects +allprojects { + repositories { + google() + mavenCentral() } +} - defaultConfig { - - minSdk 19 - targetSdk 31 - multiDexEnabled true - - consumerProguardFiles 'split-proguard-rules.pro' - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArguments clearPackageData: 'true' +// The SonarQube Gradle plugin is NOT yet compatible with AGP 9.0 +// Using CLI scanner wrapper as workaround until plugin is updated - javaCompileOptions { - annotationProcessorOptions { - arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] +// Sonar task that wraps the CLI scanner (uses sonar-project.properties) +tasks.register('sonar') { + group = 'verification' + description = 'Run SonarQube analysis (uses sonar-scanner CLI for AGP 9.0 compatibility)' + + dependsOn 'jacocoRootReport' + + doLast { + def sonarToken = System.getProperty('sonar.token') ?: project.findProperty('sonar.token') + def sonarHost = System.getProperty('sonar.host.url') ?: project.findProperty('sonar.host.url') ?: 'https://sonarcloud.io' + def sonarOrg = System.getProperty('sonar.organization') ?: project.findProperty('sonar.organization') + + if (!sonarToken) { + throw new GradleException('SonarQube token required') + } + + // Find sonar-scanner + def scannerPath = ['sonar-scanner', '/opt/homebrew/bin/sonar-scanner', '/usr/local/bin/sonar-scanner'] + .find { path -> + try { + def proc = new ProcessBuilder(path, '--version').redirectErrorStream(true).start() + return proc.waitFor() == 0 + } catch (Exception e) { return false } } + + if (!scannerPath) { + throw new GradleException('sonar-scanner not found. Install with: brew install sonar-scanner') } - - testOptions { - execution 'ANDROIDX_TEST_ORCHESTRATOR' + + println "Running sonar-scanner..." + def cmd = [scannerPath, "-Dsonar.token=${sonarToken}", "-Dsonar.host.url=${sonarHost}"] + if (sonarOrg) cmd.add("-Dsonar.organization=${sonarOrg}") + cmd.add("-Dsonar.projectVersion=${splitVersion}") + + def proc = new ProcessBuilder(cmd).directory(rootDir).inheritIO().start() + if (proc.waitFor() != 0) { + throw new GradleException("sonar-scanner failed") } } +} - configurations { - javadocDeps - } - - - testOptions { - unitTests.returnDefaultValues = true - - // Configure JaCoCo for all test tasks - unitTests.all { - jacoco { - includeNoLocationClasses = true - excludes = ['jdk.internal.*'] - } - } - } +androidFusedLibrary { + namespace = 'io.split.android.android_client' + minSdk { version = release(19) } +} - sourceSets { - String sharedTestDir = 'src/sharedTest/java' - test { - java.srcDirs += 'src/test/kotlin' - java.srcDirs += sharedTestDir - } - androidTest { - java.srcDirs += sharedTestDir - } +tasks.configureEach { task -> + if (task.name == 'packageLintJar' || + task.name.contains('LintJar') || + task.name.contains('LintModel')) { + task.enabled = false } +} - buildTypes { - debug { - buildConfigField("String", "SPLIT_VERSION_NAME", "\"${splitVersion}\"") - buildConfigField("String", "FLAGS_SPEC", "\"${flagsSpec}\"") - } - release { - buildConfigField("String", "SPLIT_VERSION_NAME", "\"${splitVersion}\"") - buildConfigField("String", "FLAGS_SPEC", "\"${flagsSpec}\"") +// Manually copy the merged consumer proguard file into the AAR +afterEvaluate { + tasks.named('bundle').configure { + doLast { + def aarFile = file("${buildDir}/outputs/aar/android-client.aar") + def proguardFile = file("${buildDir}/intermediates/merged_consumer_proguard_file/release/mergeReleaseConsumerProguardFiles/proguard.txt") + + if (aarFile.exists() && proguardFile.exists()) { + // Create temp directory to extract AAR + def tempDir = file("${buildDir}/tmp/aar-repack") + tempDir.deleteDir() + tempDir.mkdirs() + + // Extract AAR + copy { + from zipTree(aarFile) + into tempDir + } + + // Copy proguard.txt + copy { + from proguardFile + into tempDir + } + + // Remove lint.jar if it exists (we don't have custom lint rules) + def lintJar = new File(tempDir, 'lint.jar') + if (lintJar.exists()) { + lintJar.delete() + println "Removed empty lint.jar from AAR" + } + + // Repack AAR + ant.zip(destfile: aarFile, basedir: tempDir) + + println "Added consumer ProGuard rules to ${aarFile.name}" + } } } - namespace 'io.split.android.android_client' - lint { - abortOnError false - disable 'InvalidPackage' - } } repositories { @@ -119,78 +134,74 @@ repositories { mavenCentral() } -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { - kotlinOptions { - jvmTarget = "1.8" - } -} - dependencies { + include project(':main') + include project(':logger') + include project(':events') + include project(':events-domain') + include project(':api') +} - def roomVersion = '2.4.3' - def workVersion = '2.7.1' - def lifecycleVersion = '2.5.1' - def annotationVersion = '1.2.0' - def gsonVersion = '2.10.1' - def guavaVersion = '32.1.3-android' - def snakeYamlVersion = '2.2' - def playServicesVersion = '18.2.0' - def multidexVersion = '2.0.1' - - def jUnitVersion = '4.13.2' - def mockitoVersion = '4.8.0' - def hamcrestVersion = '1.3' - def apacheCommonsVersion = '3.12.0' - def kotlinVer = '1.5.31' - def mockWebServerVersion = '4.12.0' - - def testRulesVersion = '1.4.0' - def jUnitExtVersion = '1.1.3' - def testRunnerVersion = '1.5.1' - def orchestratorVersion = '1.4.2' - - implementation fileTree(include: ['*.jar'], dir: 'libs') - - implementation "androidx.room:room-runtime:$roomVersion" - annotationProcessor "androidx.room:room-compiler:$roomVersion" - - implementation "androidx.work:work-runtime:$workVersion" - - implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" - - implementation "androidx.annotation:annotation:$annotationVersion" - - implementation "com.google.code.gson:gson:$gsonVersion" +def javadocSourceProjects = providers.provider { + def includeConfig = configurations.findByName("include") + if (includeConfig == null) { + return [] + } + includeConfig.allDependencies + .withType(org.gradle.api.artifacts.ProjectDependency) + .collect { dep -> + def projectPath = null + if (dep.metaClass.hasProperty(dep, 'dependencyProject')) { + projectPath = dep.dependencyProject?.path + } else if (dep.metaClass.hasProperty(dep, 'dependencyProjectPath')) { + projectPath = dep.dependencyProjectPath + } else if (dep.metaClass.hasProperty(dep, 'path')) { + projectPath = dep.path + } + return projectPath ? project(projectPath) : null + } + .findAll { it != null } +} - implementation "org.yaml:snakeyaml:$snakeYamlVersion" - implementation "com.google.android.gms:play-services-base:$playServicesVersion" - implementation "androidx.multidex:multidex:$multidexVersion" +def javadocSourceDirsProvider = providers.provider { + files(javadocSourceProjects.get().collect { sourceProject -> + def androidExtension = sourceProject.extensions.findByName("android") + def sourceDirs = androidExtension?.sourceSets?.main?.java?.srcDirs ?: [] + return sourceDirs.findAll { it.exists() } + }) +} - // Test - testImplementation "junit:junit:$jUnitVersion" - testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "org.mockito:mockito-inline:$mockitoVersion" - testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion" - testImplementation "org.apache.commons:commons-lang3:$apacheCommonsVersion" - testImplementation "com.squareup.okhttp3:mockwebserver:$mockWebServerVersion" - testImplementation "com.squareup.okhttp3:okhttp-tls:$mockWebServerVersion" - testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVer" - testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVer" - testImplementation "com.google.guava:guava:$guavaVersion" +def sourcesJarTask = tasks.register('sourcesJar', Jar) { + archiveBaseName = 'android-client' + archiveVersion = splitVersion + archiveClassifier = 'sources' + destinationDirectory = layout.buildDirectory.dir('libs') + from(javadocSourceDirsProvider) +} - androidTestImplementation "androidx.test:rules:$testRulesVersion" - androidTestImplementation "androidx.test.ext:junit:$jUnitExtVersion" - androidTestImplementation "androidx.test:runner:$testRunnerVersion" - androidTestUtil "androidx.test:orchestrator:$orchestratorVersion" +tasks.register('javadocJar', Jar) { + archiveBaseName = 'android-client' + archiveVersion = splitVersion + archiveClassifier = 'javadoc' + destinationDirectory = layout.buildDirectory.dir('libs') + def javadocDir = layout.buildDirectory.dir('intermediates/java_doc_dir/release') + from(javadocDir) + doFirst { + if (!javadocDir.get().asFile.exists()) { + throw new GradleException("Javadoc directory not found: ${javadocDir.get().asFile}") + } + } +} - androidTestImplementation "com.squareup.okhttp3:mockwebserver:$mockWebServerVersion" - androidTestImplementation "com.squareup.okhttp3:okhttp-tls:$mockWebServerVersion" - androidTestImplementation "androidx.room:room-testing:$roomVersion" - androidTestImplementation "androidx.work:work-testing:$workVersion" - androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVer" - androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVer" - androidTestImplementation "com.google.guava:guava:$guavaVersion" - androidTestImplementation "org.mockito:mockito-android:$mockitoVersion" +afterEvaluate { + def agpJavadocTask = tasks.findByName('javaDocRelease') ?: + tasks.findByName('javaDocJar') ?: + tasks.findByName('javaDoc') + if (agpJavadocTask != null) { + tasks.named('javadocJar').configure { + dependsOn agpJavadocTask + } + } } def splitPOM = { @@ -222,64 +233,241 @@ def splitPOM = { mavenPublishing { coordinates("io.split.client", "android-client", splitVersion) pom(splitPOM) - publishToMavenCentral(false) - signAllPublications() } -task sourcesJar(type: Jar) { - archiveClassifier.set("sources") - from android.sourceSets.main.java.srcDirs -} - -task javadoc(type: Javadoc) { - failOnError false - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - classpath += configurations.javadocDeps -} - -task javadocJar(type: Jar, dependsOn: javadoc) { -// archiveClassifier = 'javadoc' - archiveClassifier.set("javadoc") - from javadoc.destinationDir +// GPG signing task +tasks.register('signArtifacts') { + description = 'Signs all Maven artifacts with GPG' + group = 'publishing' + + doLast { + def groupPath = project.group.toString().replace('.', '/') + def artifactId = 'android-client' + def versionStr = project.version.toString() + def artifactDir = file("${buildDir}/publishing/mavenCentral/${groupPath}/${artifactId}/${versionStr}") + + println "Signing artifacts in: ${artifactDir}" + + if (!artifactDir.exists()) { + println "WARNING: Artifact directory does not exist: ${artifactDir}" + println "Skipping signing. Run publishAllPublicationsToMavenCentralRepository first." + return + } + + def keyId = project.findProperty('signing.keyId') + def password = project.findProperty('signing.password') + + if (!keyId) { + throw new GradleException("signing.keyId property is not set in gradle.properties") + } + if (!password) { + throw new GradleException("signing.password property is not set in gradle.properties") + } + + println "Using GPG key ID: ${keyId}" + println "Using default GPG keyring" + + def artifactsToSign = [] + artifactDir.eachFile { file -> + def name = file.name + // Sign .aar, .jar, .pom, and .module files + if ((name.endsWith('.aar') || name.endsWith('.jar') || name.endsWith('.pom') || name.endsWith('.module')) && !name.endsWith('.asc')) { + artifactsToSign << file + } + } + + if (artifactsToSign.isEmpty()) { + println "WARNING: No artifacts found to sign in ${artifactDir}" + return + } + + println "Found ${artifactsToSign.size()} artifact(s) to sign:" + artifactsToSign.each { println " - ${it.name}" } + + def gpgCommand = null + def gpgPaths = [ + '/opt/homebrew/bin/gpg', // macOS Homebrew on Apple Silicon - try first + '/usr/local/bin/gpg', + '/usr/bin/gpg', + '/opt/local/bin/gpg' // MacPorts + ] + + for (path in gpgPaths) { + def gpgFile = new File(path) + if (gpgFile.exists() && gpgFile.canExecute()) { + gpgCommand = path + println "Found GPG at: ${gpgCommand}" + break + } + } + + if (gpgCommand == null) { + try { + def process = new ProcessBuilder(['gpg', '--version']) + .redirectErrorStream(true) + .start() + def exitCode = process.waitFor() + if (exitCode == 0) { + gpgCommand = 'gpg' + println "Found GPG in PATH" + } + } catch (Exception e) { + // GPG not in PATH + } + } + + if (gpgCommand == null) { + throw new GradleException("GPG is not installed or not found in common locations.\n" + + "Please install GPG:\n" + + " macOS: brew install gnupg\n" + + " Linux: apt-get install gnupg or yum install gnupg\n" + + "Searched in: ${gpgPaths.join(', ')}\n" + + "If GPG is installed elsewhere, you can configure the path in the task.") + } + + println "Using GPG: ${gpgCommand}" + + artifactsToSign.each { artifact -> + def ascFile = new File(artifact.parentFile, "${artifact.name}.asc") + + // Skip if already signed + if (ascFile.exists()) { + println "Signature already exists for ${artifact.name}, skipping" + return + } + + println "Signing: ${artifact.name}" + + try { + def cmd = [ + gpgCommand, + '--batch', + '--yes', + '--no-tty', + '--passphrase', password, + '--pinentry-mode', 'loopback', + '--local-user', keyId, + '--detach-sign', + '--armor', + artifact.absolutePath + ] + + // Set GPG_TTY to avoid agent issues + def processEnv = new ProcessBuilder().environment() + processEnv.put('GPG_TTY', '') + + def process = new ProcessBuilder(cmd) + .redirectErrorStream(true) + .start() + + def exitCode = process.waitFor() + + if (exitCode != 0) { + def output = process.inputStream.text + throw new GradleException("Failed to sign ${artifact.name}. Exit code: ${exitCode}\nOutput: ${output}") + } + + // Verify the signature was created + if (!ascFile.exists()) { + throw new GradleException("Signature file was not created for ${artifact.name}") + } + + println " ✓ Created signature: ${ascFile.name}" + + // Verify the signature + def verifyCmd = [ + gpgCommand, + '--verify', + ascFile.absolutePath, + artifact.absolutePath + ] + + def verifyProcess = new ProcessBuilder(verifyCmd) + .redirectErrorStream(true) + .start() + + def verifyExitCode = verifyProcess.waitFor() + + if (verifyExitCode == 0) { + println " ✓ Signature verified successfully" + } else { + println " ⚠ Warning: Could not verify signature" + } + + } catch (Exception e) { + throw new GradleException("Failed to sign artifact ${artifact.name}: ${e.message}", e) + } + } + + println "Successfully signed ${artifactsToSign.size()} artifact(s)" + } } -artifacts { - archives sourcesJar - archives javadocJar +// Make signArtifacts run after artifacts are published but before upload +afterEvaluate { + def signTask = tasks.findByName('signArtifacts') + def publishTask = tasks.findByName('publishAllPublicationsToMavenCentralRepository') + + if (signTask && publishTask) { + // signArtifacts depends on publishing (so artifacts exist) + signTask.dependsOn(publishTask) + + // Make publish task finalize with signing + publishTask.finalizedBy(signTask) + + println "Configured signArtifacts task to run after publishAllPublicationsToMavenCentralRepository" + } } -task printReleaseDependenciesToFile { - doLast { - def outputFile = new File("$projectDir/deps.txt") - def processBuilder = new ProcessBuilder("./gradlew", ":android-client:dependencies", "--configuration", "releaseRuntimeClasspath", "-q") - - outputFile.parentFile.mkdirs() - outputFile.text = '' // Clear the file before writing - processBuilder.redirectOutput(outputFile) - processBuilder.directory(project.rootDir) - - try { - Process process = processBuilder.start() - process.waitFor() - - def filteredLines = outputFile.readLines().findAll { !it.contains("BUILD SUCCESSFUL") } - - // Write the filtered content back to the file - outputFile.withPrintWriter { writer -> - filteredLines.each(writer.&println) +afterEvaluate { + // Disable Gradle Module Metadata (.module file) to avoid variant ambiguity + tasks.configureEach { task -> + // Match tasks related to module metadata generation + if (task.name.startsWith('generateMetadataFileFor') || + task.name == 'generateModuleMetadata' || + task.class.name.endsWith('GenerateModuleMetadata')) { + task.enabled = false + } + } + + // Remove empty lint.jar from publication to prevent consumer lint crashes + // The fused library plugin generates an empty lint.jar without proper Lint-Registry-v2 manifest + // This causes lint to crash when consumers use checkDependencies: true + publishing.publications.withType(MavenPublication) { publication -> + if (publication.name == "maven") { + publication.artifact(tasks.named('javadocJar')) + publication.artifact(tasks.named('sourcesJar')) + publication.artifacts.removeAll { artifact -> + artifact.file?.name?.endsWith('lint.jar') ?: false } - } catch (Exception e) { - e.printStackTrace() } } } -preBuild.dependsOn printReleaseDependenciesToFile - -tasks.withType(Test) { - maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 - forkEvery = 100 - maxHeapSize = "1024m" +// Remove duplicate sources JAR artifact before publishing +// The vanniktech plugin adds emptySourcesJar while AGP 9.0 also provides one from fusedLibraryComponent +// We want to keep the AGP one (merged_sources_jar) which has actual sources +// This must be done at task execution time because artifacts are resolved lazily +gradle.taskGraph.whenReady { graph -> + graph.allTasks.findAll { it.name.startsWith('publishMavenPublication') }.each { publishTask -> + publishTask.doFirst { + def pub = publication + if (pub.name == "maven") { + def sourcesJarFile = tasks.named('sourcesJar').get().archiveFile.get().asFile + def sourcesArtifacts = pub.artifacts.findAll { it.classifier == "sources" && it.extension == "jar" } + sourcesArtifacts.findAll { it.file != null && it.file != sourcesJarFile }.each { artifact -> + pub.artifacts.remove(artifact) + println "Removed duplicate sources artifact: ${artifact.file?.absolutePath}" + } + + def javadocJarFile = tasks.named('javadocJar').get().archiveFile.get().asFile + def javadocArtifacts = pub.artifacts.findAll { it.classifier == "javadoc" && it.extension == "jar" } + javadocArtifacts.findAll { it.file != null && it.file != javadocJarFile }.each { artifact -> + pub.artifacts.remove(artifact) + println "Removed duplicate javadoc artifact: ${artifact.file?.absolutePath}" + } + } + } + } } diff --git a/deps.txt b/deps.txt deleted file mode 100644 index a680eefb8..000000000 --- a/deps.txt +++ /dev/null @@ -1,128 +0,0 @@ - ------------------------------------------------------------- -Project ':android-client' ------------------------------------------------------------- - -releaseRuntimeClasspath - Runtime classpath of compilation 'release' (target (androidJvm)). -+--- androidx.room:room-runtime:2.4.3 -| +--- androidx.room:room-common:2.4.3 -| | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| +--- androidx.sqlite:sqlite-framework:2.2.0 -| | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | \--- androidx.sqlite:sqlite:2.2.0 -| | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| +--- androidx.sqlite:sqlite:2.2.0 (*) -| +--- androidx.arch.core:core-runtime:2.0.1 -> 2.1.0 -| | +--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| | \--- androidx.arch.core:core-common:2.1.0 -| | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| \--- androidx.annotation:annotation-experimental:1.1.0 -+--- androidx.work:work-runtime:2.7.1 -| +--- androidx.annotation:annotation-experimental:1.0.0 -> 1.1.0 -| +--- com.google.guava:listenablefuture:1.0 -| +--- androidx.lifecycle:lifecycle-livedata:2.1.0 -| | +--- androidx.arch.core:core-runtime:2.1.0 (*) -| | +--- androidx.lifecycle:lifecycle-livedata-core:2.1.0 -| | | +--- androidx.lifecycle:lifecycle-common:2.1.0 -> 2.5.1 -| | | | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| | | +--- androidx.arch.core:core-common:2.1.0 (*) -| | | \--- androidx.arch.core:core-runtime:2.1.0 (*) -| | \--- androidx.arch.core:core-common:2.1.0 (*) -| +--- androidx.startup:startup-runtime:1.0.0 -> 1.1.1 -| | +--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| | \--- androidx.tracing:tracing:1.0.0 -| | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| +--- androidx.core:core:1.6.0 -| | +--- androidx.annotation:annotation:1.2.0 -| | +--- androidx.annotation:annotation-experimental:1.1.0 -| | +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.5.1 -| | | +--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| | | +--- androidx.arch.core:core-common:2.1.0 (*) -| | | +--- androidx.arch.core:core-runtime:2.1.0 (*) -| | | \--- androidx.lifecycle:lifecycle-common:2.5.1 (*) -| | +--- androidx.versionedparcelable:versionedparcelable:1.1.1 -| | | +--- androidx.annotation:annotation:1.1.0 -> 1.2.0 -| | | \--- androidx.collection:collection:1.0.0 -| | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | \--- androidx.collection:collection:1.0.0 (*) -| +--- androidx.room:room-runtime:2.2.5 -> 2.4.3 (*) -| +--- androidx.sqlite:sqlite:2.1.0 -> 2.2.0 (*) -| +--- androidx.sqlite:sqlite-framework:2.1.0 -> 2.2.0 (*) -| +--- androidx.core:core:1.1.0 -> 1.6.0 (*) -| \--- androidx.lifecycle:lifecycle-service:2.1.0 -| \--- androidx.lifecycle:lifecycle-runtime:2.1.0 -> 2.5.1 (*) -+--- androidx.lifecycle:lifecycle-process:2.5.1 -| +--- androidx.annotation:annotation:1.2.0 -| +--- androidx.lifecycle:lifecycle-runtime:2.5.1 (*) -| \--- androidx.startup:startup-runtime:1.1.1 (*) -+--- androidx.annotation:annotation:1.2.0 -+--- com.google.code.gson:gson:2.10.1 -+--- org.yaml:snakeyaml:2.2 -+--- com.google.android.gms:play-services-base:18.2.0 -| +--- androidx.collection:collection:1.0.0 (*) -| +--- androidx.core:core:1.2.0 -> 1.6.0 (*) -| +--- androidx.fragment:fragment:1.0.0 -| | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | +--- androidx.legacy:legacy-support-core-ui:1.0.0 -| | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | +--- androidx.legacy:legacy-support-core-utils:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | | +--- androidx.documentfile:documentfile:1.0.0 -| | | | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.loader:loader:1.0.0 -| | | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | | | +--- androidx.lifecycle:lifecycle-livedata:2.0.0 -> 2.1.0 (*) -| | | | | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -| | | | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 -| | | | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | \--- androidx.print:print:1.0.0 -| | | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | +--- androidx.customview:customview:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | \--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | +--- androidx.viewpager:viewpager:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | | \--- androidx.customview:customview:1.0.0 (*) -| | | +--- androidx.coordinatorlayout:coordinatorlayout:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | | \--- androidx.customview:customview:1.0.0 (*) -| | | +--- androidx.drawerlayout:drawerlayout:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | | \--- androidx.customview:customview:1.0.0 (*) -| | | +--- androidx.slidingpanelayout:slidingpanelayout:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | | \--- androidx.customview:customview:1.0.0 (*) -| | | +--- androidx.interpolator:interpolator:1.0.0 -| | | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | +--- androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | +--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | | \--- androidx.interpolator:interpolator:1.0.0 (*) -| | | +--- androidx.asynclayoutinflater:asynclayoutinflater:1.0.0 -| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | | | \--- androidx.core:core:1.0.0 -> 1.6.0 (*) -| | | \--- androidx.cursoradapter:cursoradapter:1.0.0 -| | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*) -| | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0 -| | +--- androidx.loader:loader:1.0.0 (*) -| | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 (*) -| +--- com.google.android.gms:play-services-basement:18.1.0 -| | +--- androidx.collection:collection:1.0.0 (*) -| | +--- androidx.core:core:1.2.0 -> 1.6.0 (*) -| | \--- androidx.fragment:fragment:1.0.0 (*) -| \--- com.google.android.gms:play-services-tasks:18.0.2 -| \--- com.google.android.gms:play-services-basement:18.1.0 (*) -\--- androidx.multidex:multidex:2.0.1 - -(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation. - -A web-based, searchable dependency report is available by adding the --scan option. diff --git a/events-domain/.gitignore b/events-domain/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/events-domain/.gitignore @@ -0,0 +1 @@ +/build diff --git a/events-domain/README.md b/events-domain/README.md new file mode 100644 index 000000000..b64917133 --- /dev/null +++ b/events-domain/README.md @@ -0,0 +1,3 @@ +# Events Domain module + +This module provides Split SDK specific events management implementation. diff --git a/events-domain/build.gradle b/events-domain/build.gradle new file mode 100644 index 000000000..04cbce16f --- /dev/null +++ b/events-domain/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'com.android.library' +} + +apply from: "$rootDir/gradle/common-android-library.gradle" + +android { + namespace 'io.split.android.client.events' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation libs.annotation + + implementation project(':api') + implementation project(':events') + implementation project(':logger') + + testImplementation libs.junit4 + testImplementation libs.mockitoCore +} diff --git a/events-domain/consumer-rules.pro b/events-domain/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/events-domain/proguard-rules.pro b/events-domain/proguard-rules.pro new file mode 100644 index 000000000..cf504086a --- /dev/null +++ b/events-domain/proguard-rules.pro @@ -0,0 +1,22 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + diff --git a/events-domain/src/androidTest/java/.gitkeep b/events-domain/src/androidTest/java/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/events-domain/src/main/AndroidManifest.xml b/events-domain/src/main/AndroidManifest.xml new file mode 100644 index 000000000..cf2d636b6 --- /dev/null +++ b/events-domain/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/events-domain/src/main/java/io/split/android/client/events/.gitkeep b/events-domain/src/main/java/io/split/android/client/events/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/events-domain/src/main/java/io/split/android/client/events/DualExecutorRegistration.java b/events-domain/src/main/java/io/split/android/client/events/DualExecutorRegistration.java new file mode 100644 index 000000000..cf8633a20 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/DualExecutorRegistration.java @@ -0,0 +1,133 @@ +package io.split.android.client.events; + +import androidx.annotation.NonNull; + +import java.util.concurrent.Executor; + +import io.harness.events.EventHandler; +import io.harness.events.EventsManager; +import io.harness.events.Logging; + +/** + * Utility for registering event handlers that need to execute on two different threads. + *

+ * This is useful when an event should trigger both background work and UI updates. + * Each callback is wrapped with its executor before registration. + * + * @param event type + * @param internal event type (for EventsManager) + * @param metadata type + */ +public class DualExecutorRegistration { + + @NonNull + private final Executor mBackgroundExecutor; + @NonNull + private final Executor mMainThreadExecutor; + @NonNull + private final Logging mLogging; + + /** + * Creates a new DualExecutorRegistration with a {@link SplitLogging} instance. + * + * @param backgroundExecutor executor for background execution + * @param mainThreadExecutor executor for main thread execution + */ + public DualExecutorRegistration(@NonNull Executor backgroundExecutor, + @NonNull Executor mainThreadExecutor) { + this(backgroundExecutor, mainThreadExecutor, new SplitLogging()); + } + + /** + * Creates a new DualExecutorRegistration. + *

+ * Package-private for testing. + * + * @param backgroundExecutor executor for background execution + * @param mainThreadExecutor executor for main thread execution + * @param logging logging instance + */ + DualExecutorRegistration(@NonNull Executor backgroundExecutor, + @NonNull Executor mainThreadExecutor, + @NonNull Logging logging) { + if (backgroundExecutor == null) { + throw new IllegalArgumentException("backgroundExecutor cannot be null"); + } + if (mainThreadExecutor == null) { + throw new IllegalArgumentException("mainThreadExecutor cannot be null"); + } + if (logging == null) { + throw new IllegalArgumentException("logging cannot be null"); + } + mBackgroundExecutor = backgroundExecutor; + mMainThreadExecutor = mainThreadExecutor; + mLogging = logging; + } + + /** + * Registers two handlers for the same event, each executing on its respective thread. + * + * @param eventsManager the events manager to register with + * @param event the event to register for + * @param backgroundCallback callback to execute on the background thread + * @param mainThreadCallback callback to execute on the main thread + */ + public void register(@NonNull EventsManager eventsManager, + @NonNull E event, + @NonNull EventHandler backgroundCallback, + @NonNull EventHandler mainThreadCallback) { + if (eventsManager == null || event == null) { + return; + } + + if (backgroundCallback != null) { + eventsManager.register(event, wrapWithExecutor(backgroundCallback, mBackgroundExecutor)); + } + + if (mainThreadCallback != null) { + eventsManager.register(event, wrapWithExecutor(mainThreadCallback, mMainThreadExecutor)); + } + } + + /** + * Registers a single handler for the background thread only. + * + * @param eventsManager the events manager to register with + * @param event the event to register for + * @param backgroundCallback callback to execute on the background thread + */ + public void registerBackground(@NonNull EventsManager eventsManager, + @NonNull E event, + @NonNull EventHandler backgroundCallback) { + if (eventsManager == null || event == null || backgroundCallback == null) { + return; + } + eventsManager.register(event, wrapWithExecutor(backgroundCallback, mBackgroundExecutor)); + } + + /** + * Registers a single handler for the main thread only. + * + * @param eventsManager the events manager to register with + * @param event the event to register for + * @param mainThreadCallback callback to execute on the main thread + */ + public void registerMainThread(@NonNull EventsManager eventsManager, + @NonNull E event, + @NonNull EventHandler mainThreadCallback) { + if (eventsManager == null || event == null || mainThreadCallback == null) { + return; + } + eventsManager.register(event, wrapWithExecutor(mainThreadCallback, mMainThreadExecutor)); + } + + private EventHandler wrapWithExecutor(EventHandler handler, Executor executor) { + return (event, metadata) -> executor.execute(() -> { + try { + handler.handle(event, metadata); + } catch (Exception e) { + mLogging.logError("Exception in event handler: " + e.getMessage()); + } + }); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java new file mode 100644 index 000000000..93e0f6c3f --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java @@ -0,0 +1,150 @@ +package io.split.android.client.events; + +import static java.util.Objects.requireNonNull; + +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.api.Key; + +/** + * Coordinator for SDK-scoped events that should be propagated to all client event managers. + *

+ * This coordinator keeps track of all registered {@link ISplitEventsManager} instances + * and forwards SDK-scoped internal events (like splits updates) to all of them. + *

+ * Client-scoped events (like segments updates for a specific key) should be sent + * directly to the corresponding client's event manager. + */ +public class EventsManagerCoordinator implements ISplitEventsManager, EventsManagerRegistry { + + /** + * Set of SDK-scoped internal events that should be propagated to all registered managers. + */ + private static final Set SDK_SCOPED_EVENTS = EnumSet.of( + SplitInternalEvent.SPLITS_UPDATED, + SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, + SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, + SplitInternalEvent.SPLIT_KILLED_NOTIFICATION, + SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + SplitInternalEvent.ENCRYPTION_MIGRATION_DONE + ); + + private final ConcurrentMap mManagers = new ConcurrentHashMap<>(); + private final Set mTriggered = Collections.newSetFromMap(new ConcurrentHashMap()); + private final ConcurrentMap mTriggeredMetadata = new ConcurrentHashMap<>(); + private final Object mEventLock = new Object(); + + /** + * Notifies an SDK-scoped internal event. + *

+ * If the event is SDK-scoped (like splits updates), it will be propagated + * to all registered event managers. Client-scoped events are ignored and should + * be sent directly to the corresponding client's event manager. + * + * @param internalEvent the internal event to notify + */ + @Override + public void notifyInternalEvent(SplitInternalEvent internalEvent) { + notifyInternalEvent(internalEvent, null); + } + + /** + * Notifies an SDK-scoped internal event with metadata. + *

+ * If the event is SDK-scoped (like splits updates), it will be propagated + * to all registered event managers. Client-scoped events are ignored and should + * be sent directly to the corresponding client's event manager. + * + * @param internalEvent the internal event to notify + * @param metadata the event metadata, can be null + */ + @Override + public void notifyInternalEvent(SplitInternalEvent internalEvent, @Nullable EventMetadata metadata) { + requireNonNull(internalEvent); + + if (!SDK_SCOPED_EVENTS.contains(internalEvent)) { + // Client-scoped events should be sent directly to the client's manager + return; + } + + synchronized (mEventLock) { + mTriggered.add(internalEvent); + if (metadata != null) { + mTriggeredMetadata.put(internalEvent, metadata); + } + + for (ISplitEventsManager manager : mManagers.values()) { + manager.notifyInternalEvent(internalEvent, metadata); + } + } + } + + /** + * Registers an events manager for a client key. + *

+ * Any SDK-scoped events that occurred prior to registration will be propagated + * to the newly registered manager. + * + * @param key the client key + * @param splitEventsManager the events manager for that client + */ + @Override + public void registerEventsManager(Key key, ISplitEventsManager splitEventsManager) { + requireNonNull(key); + requireNonNull(splitEventsManager); + + mManagers.put(key, splitEventsManager); + + // Propagate any events that occurred before registration + propagateTriggeredEvents(splitEventsManager); + } + + /** + * Unregisters the events manager for a client key. + *

+ * If the removed manager is a {@link SplitEventsManager}, its {@code destroy()} method + * will be called to clean up resources. + * + * @param key the client key to unregister + */ + @Override + public void unregisterEventsManager(Key key) { + if (key != null) { + ISplitEventsManager removed = mManagers.remove(key); + if (removed instanceof SplitEventsManager) { + ((SplitEventsManager) removed).destroy(); + } + } + } + + private void propagateTriggeredEvents(ISplitEventsManager splitEventsManager) { + synchronized (mEventLock) { + for (SplitInternalEvent event : mTriggered) { + splitEventsManager.notifyInternalEvent(event, mTriggeredMetadata.get(event)); + } + } + } + + /** + * Checks if an external event has already been triggered in any registered manager. + * + * @param event the event to check + * @return true if the event has already been triggered in any manager, false otherwise + */ + @Override + public boolean eventAlreadyTriggered(SplitEvent event) { + for (ISplitEventsManager manager : mManagers.values()) { + if (manager.eventAlreadyTriggered(event)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/io/split/android/client/events/EventsManagerRegistry.java b/events-domain/src/main/java/io/split/android/client/events/EventsManagerRegistry.java similarity index 100% rename from src/main/java/io/split/android/client/events/EventsManagerRegistry.java rename to events-domain/src/main/java/io/split/android/client/events/EventsManagerRegistry.java diff --git a/events-domain/src/main/java/io/split/android/client/events/ISplitEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/ISplitEventsManager.java new file mode 100644 index 000000000..d350b35d3 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/ISplitEventsManager.java @@ -0,0 +1,26 @@ +package io.split.android.client.events; + +import androidx.annotation.Nullable; + +import io.split.android.client.events.metadata.EventMetadata; + +public interface ISplitEventsManager { + + void notifyInternalEvent(SplitInternalEvent internalEvent); + + /** + * Notifies an internal event with metadata. + * + * @param internalEvent the internal event + * @param metadata the event metadata, can be null + */ + void notifyInternalEvent(SplitInternalEvent internalEvent, @Nullable EventMetadata metadata); + + /** + * Checks if an external event has already been triggered. + * + * @param event the event to check + * @return true if the event has already been triggered (reached its max executions), false otherwise + */ + boolean eventAlreadyTriggered(SplitEvent event); +} diff --git a/src/main/java/io/split/android/client/events/ListenableEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/ListenableEventsManager.java similarity index 84% rename from src/main/java/io/split/android/client/events/ListenableEventsManager.java rename to events-domain/src/main/java/io/split/android/client/events/ListenableEventsManager.java index f0b4aff46..43498e379 100644 --- a/src/main/java/io/split/android/client/events/ListenableEventsManager.java +++ b/events-domain/src/main/java/io/split/android/client/events/ListenableEventsManager.java @@ -8,5 +8,7 @@ public interface ListenableEventsManager { void register(SplitEvent event, SplitEventTask task); + void registerEventListener(SplitEventListener listener); + boolean eventAlreadyTriggered(SplitEvent event); } diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventDelivery.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventDelivery.java new file mode 100644 index 000000000..5930fd21c --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventDelivery.java @@ -0,0 +1,51 @@ +package io.split.android.client.events; + +import androidx.annotation.VisibleForTesting; + +import io.harness.events.EventDelivery; +import io.harness.events.EventHandler; +import io.harness.events.Logging; +import io.split.android.client.events.metadata.EventMetadata; + +/** + * Event delivery implementation for Split SDK events. + *

+ * Execution context (background vs main thread) should be + * handled using {@link DualExecutorRegistration}. + */ +class SplitEventDelivery implements EventDelivery { + + private final Logging mLogging; + + /** + * Creates a new SplitEventDelivery with the default logging implementation. + */ + public SplitEventDelivery() { + this(new SplitLogging()); + } + + /** + * Creates a new SplitEventDelivery with a custom logging implementation. + * + * @param logging the logging implementation to use + */ + @VisibleForTesting + SplitEventDelivery(Logging logging) { + mLogging = logging != null ? logging : new SplitLogging(); + } + + @Override + public void deliver(EventHandler eventHandler, + SplitEvent event, + EventMetadata metadata) { + if (eventHandler == null || event == null) { + return; + } + + try { + eventHandler.handle(event, metadata); + } catch (Exception e) { + mLogging.logError("Exception delivering event " + event.name() + ": " + e.getMessage()); + } + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java new file mode 100644 index 000000000..8fc801117 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java @@ -0,0 +1,282 @@ +package io.split.android.client.events; + +import static java.util.Objects.requireNonNull; + +import androidx.annotation.VisibleForTesting; + +import java.util.concurrent.Executor; + +import io.harness.events.EventHandler; +import io.harness.events.EventsManager; +import io.harness.events.EventsManagers; + +import io.split.android.client.SplitClient; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; +import io.split.android.client.events.executors.SplitEventExecutorResources; +import io.split.android.client.events.executors.SplitEventExecutorResourcesImpl; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutor; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.utils.logger.Logger; + +/** + * Events manager for Split SDK. + */ +public class SplitEventsManager implements ISplitEventsManager, ListenableEventsManager { + + private final EventsManager mEventsManager; + private final DualExecutorRegistration mDualExecutorRegistration; + private SplitEventExecutorResources mResources; + + /** + * Creates a new SplitEventsManager. + * + * @param splitTaskExecutor the task executor for running callbacks + * @param blockUntilReady timeout in milliseconds for SDK_READY (0 = no timeout) + */ + public SplitEventsManager(SplitTaskExecutor splitTaskExecutor, final int blockUntilReady) { + requireNonNull(splitTaskExecutor); + + mResources = new SplitEventExecutorResourcesImpl(); + + // Create the events manager with Split SDK configuration + mEventsManager = EventsManagers.create( + SplitEventsManagerConfigFactory.create(), + new SplitEventDelivery() + ); + + // Create the dual executor registration for handling background + main thread callbacks + mDualExecutorRegistration = new DualExecutorRegistration<>( + createBackgroundExecutor(splitTaskExecutor), + createMainThreadExecutor(splitTaskExecutor) + ); + + // Start timeout thread if configured + if (blockUntilReady > 0) { + startTimeoutThread(blockUntilReady); + } + } + + /** + * Package-private constructor for testing. + */ + @VisibleForTesting + SplitEventsManager(EventsManager eventsManager, + DualExecutorRegistration dualExecutorRegistration, + SplitEventExecutorResources resources) { + mEventsManager = eventsManager; + mDualExecutorRegistration = dualExecutorRegistration; + mResources = resources; + } + + @VisibleForTesting + public void setExecutionResources(SplitEventExecutorResources resources) { + mResources = resources; + } + + @Override + public SplitEventExecutorResources getExecutorResources() { + return mResources; + } + + @Override + public void notifyInternalEvent(SplitInternalEvent internalEvent) { + requireNonNull(internalEvent); + mEventsManager.notifyInternalEvent(internalEvent, null); + } + + /** + * Notifies an internal event with metadata. + * + * @param internalEvent the internal event + * @param metadata the event metadata + */ + @Override + public void notifyInternalEvent(SplitInternalEvent internalEvent, EventMetadata metadata) { + requireNonNull(internalEvent); + mEventsManager.notifyInternalEvent(internalEvent, metadata); + } + + @Override + public void register(SplitEvent event, SplitEventTask task) { + requireNonNull(event); + requireNonNull(task); + + // Adapt SplitEventTask to EventHandler and register for both threads + mDualExecutorRegistration.register( + mEventsManager, + event, + createBackgroundHandler(task), + createMainThreadHandler(task) + ); + } + + @Override + public void registerEventListener(SplitEventListener listener) { + requireNonNull(listener); + + // Register SDK_READY handlers (bg + main) + mDualExecutorRegistration.register( + mEventsManager, + SplitEvent.SDK_READY, + createReadyBackgroundHandler(listener), + createReadyMainThreadHandler(listener) + ); + + // Register SDK_UPDATE handlers (bg + main) + mDualExecutorRegistration.register( + mEventsManager, + SplitEvent.SDK_UPDATE, + createUpdateBackgroundHandler(listener), + createUpdateMainThreadHandler(listener) + ); + + // Register SDK_READY_FROM_CACHE handlers (bg + main) + mDualExecutorRegistration.register( + mEventsManager, + SplitEvent.SDK_READY_FROM_CACHE, + createReadyFromCacheBackgroundHandler(listener), + createReadyFromCacheMainThreadHandler(listener) + ); + } + + @Override + public boolean eventAlreadyTriggered(SplitEvent event) { + return mEventsManager.eventAlreadyTriggered(event); + } + + /** + * Destroys this events manager. + * After calling this method, the manager will no longer process events. + */ + public void destroy() { + mEventsManager.destroy(); + } + + private void startTimeoutThread(final int blockUntilReady) { + Thread timeoutThread = new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(blockUntilReady); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED, null); + } catch (InterruptedException e) { + Logger.d("Waiting before to check if SDK is READY has been interrupted", e.getMessage()); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED, null); + } catch (Throwable e) { + Logger.d("Waiting before to check if SDK is READY interrupted ", e.getMessage()); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED, null); + } + } + }); + timeoutThread.setName("Split-SDKReadyTimeout"); + timeoutThread.setDaemon(true); + timeoutThread.start(); + } + + private EventHandler createBackgroundHandler(final SplitEventTask task) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + executeBackgroundTask(task, client, metadata); + }; + } + + private EventHandler createMainThreadHandler(final SplitEventTask task) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + executeMainThreadTask(task, client, metadata); + }; + } + + // SdkEventListener handlers for SDK_READY + private EventHandler createReadyBackgroundHandler(final SplitEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata); + executeMethod(() -> listener.onReady(client, typedMetadata)); + }; + } + + private EventHandler createReadyMainThreadHandler(final SplitEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata); + executeMethod(() -> listener.onReadyView(client, typedMetadata)); + }; + } + + // SdkEventListener handlers for SDK_UPDATE + private EventHandler createUpdateBackgroundHandler(final SplitEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + executeMethod(() -> listener.onUpdate(client, typedMetadata)); + }; + } + + private EventHandler createUpdateMainThreadHandler(final SplitEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + executeMethod(() -> listener.onUpdateView(client, typedMetadata)); + }; + } + + // SdkEventListener handlers for SDK_READY_FROM_CACHE + private EventHandler createReadyFromCacheBackgroundHandler(final SplitEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata); + executeMethod(() -> listener.onReadyFromCache(client, typedMetadata)); + }; + } + + private EventHandler createReadyFromCacheMainThreadHandler(final SplitEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkReadyMetadata typedMetadata = TypedTaskConverter.convertForSdkReady(metadata); + executeMethod(() -> listener.onReadyFromCacheView(client, typedMetadata)); + }; + } + + private void executeBackgroundTask(SplitEventTask task, SplitClient client, EventMetadata metadata) { + executeMethod(() -> task.onPostExecution(client)); + } + + private void executeMainThreadTask(SplitEventTask task, SplitClient client, EventMetadata metadata) { + executeMethod(() -> task.onPostExecutionView(client)); + } + + private void executeMethod(Runnable method) { + try { + method.run(); + } catch (SplitEventTaskMethodNotImplementedException e) { + // Method not implemented by client, ignore + } catch (Exception e) { + Logger.e("Error executing event task: " + e.getMessage()); + } + } + + private Executor createBackgroundExecutor(final SplitTaskExecutor taskExecutor) { + return command -> taskExecutor.submit(() -> { + try { + command.run(); + } catch (Exception e) { + Logger.e("Error in background executor: " + e.getMessage()); + } + return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); + }, null); + } + + private Executor createMainThreadExecutor(final SplitTaskExecutor taskExecutor) { + return command -> taskExecutor.submitOnMainThread(() -> { + try { + command.run(); + } catch (Exception e) { + Logger.e("Error in main thread executor: " + e.getMessage()); + } + return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); + }); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManagerConfigFactory.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManagerConfigFactory.java new file mode 100644 index 000000000..28e2738be --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManagerConfigFactory.java @@ -0,0 +1,88 @@ +package io.split.android.client.events; + +import java.util.HashSet; +import java.util.Set; + +import io.harness.events.EventsManagerConfig; + +/** + * Factory for creating the {@link EventsManagerConfig} that defines the Split SDK event rules. + *

+ * This configuration encapsulates the relationships between internal SDK events + * and external client-facing events. + */ +final class SplitEventsManagerConfigFactory { + + private SplitEventsManagerConfigFactory() { + // Utility class + } + + /** + * Creates the EventsManagerConfig for the Split SDK. + *

+ * Event rules: + *

    + *
  • SDK_READY: requires both splits and segments sync to complete, and SDK_READY_FROM_CACHE must fire first
  • + *
  • SDK_READY_FROM_CACHE: fires when EITHER all cache loading events complete OR all sync events complete
  • + *
  • SDK_READY_TIMED_OUT: fires when timeout is reached (suppressed if SDK_READY fired first)
  • + *
  • SDK_UPDATE: fires on any data update after SDK_READY
  • + *
+ * + * @return the configured EventsManagerConfig + */ + static EventsManagerConfig create() { + // SDK_READY_FROM_CACHE fires when either: + // 1. Cache path: All cache loading events complete (AND), OR + // 2. Sync path: All sync events complete (AND) + Set cacheGroup = new HashSet<>(); + cacheGroup.add(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + cacheGroup.add(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); + cacheGroup.add(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE); + cacheGroup.add(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); + + Set syncGroup = new HashSet<>(); + syncGroup.add(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + syncGroup.add(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + + return EventsManagerConfig.builder() + .requireAll(SplitEvent.SDK_READY, + SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, + SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE) + + // SDK_READY_FROM_CACHE: OR of ANDs + // Fires when (cache group all done) OR (sync group all done) + .requireAny(SplitEvent.SDK_READY_FROM_CACHE, cacheGroup, syncGroup) + + .requireAny(SplitEvent.SDK_READY_TIMED_OUT, + SplitInternalEvent.SDK_READY_TIMEOUT_REACHED) + + .requireAny(SplitEvent.SDK_UPDATE, + SplitInternalEvent.SPLITS_UPDATED, + SplitInternalEvent.MY_SEGMENTS_UPDATED, + SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, + SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + SplitInternalEvent.SPLIT_KILLED_NOTIFICATION) + + // SDK_READY requires SDK_READY_FROM_CACHE to fire first + .prerequisite(SplitEvent.SDK_READY, SplitEvent.SDK_READY_FROM_CACHE) + .prerequisite(SplitEvent.SDK_UPDATE, SplitEvent.SDK_READY) + + .suppressedBy(SplitEvent.SDK_READY_TIMED_OUT, SplitEvent.SDK_READY) + + .executionLimit(SplitEvent.SDK_READY, 1) + .executionLimit(SplitEvent.SDK_READY_FROM_CACHE, 1) + .executionLimit(SplitEvent.SDK_READY_TIMED_OUT, 1) + .executionLimit(SplitEvent.SDK_UPDATE, -1) // unlimited + + // Metadata sources + .metadataSource(SplitEvent.SDK_READY, SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE) + // Cache path: if SDK_READY_FROM_CACHE fired because cache was loaded, use storage load metadata. + .metadataSource(SplitEvent.SDK_READY_FROM_CACHE, cacheGroup, + SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE) + // Sync path: if SDK_READY_FROM_CACHE fired alongside SDK_READY, use sync completion metadata. + .metadataSource(SplitEvent.SDK_READY_FROM_CACHE, syncGroup, + SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE) + + .build(); + } +} diff --git a/src/main/java/io/split/android/client/events/SplitInternalEvent.java b/events-domain/src/main/java/io/split/android/client/events/SplitInternalEvent.java similarity index 53% rename from src/main/java/io/split/android/client/events/SplitInternalEvent.java rename to events-domain/src/main/java/io/split/android/client/events/SplitInternalEvent.java index ab070f2f6..3849e8e5b 100644 --- a/src/main/java/io/split/android/client/events/SplitInternalEvent.java +++ b/events-domain/src/main/java/io/split/android/client/events/SplitInternalEvent.java @@ -1,20 +1,26 @@ package io.split.android.client.events; /** - * Created by sarrubia on 4/6/18. + * Internal events used to track SDK initialization and data updates. */ - public enum SplitInternalEvent { + // Cache loading events MY_SEGMENTS_LOADED_FROM_STORAGE, SPLITS_LOADED_FROM_STORAGE, - MY_SEGMENTS_FETCHED, - MY_SEGMENTS_UPDATED, - SPLITS_FETCHED, - SPLITS_UPDATED, - SDK_READY_TIMEOUT_REACHED, - SPLIT_KILLED_NOTIFICATION, ATTRIBUTES_LOADED_FROM_STORAGE, ENCRYPTION_MIGRATION_DONE, + + // Data update events (fired only when data actually changed) + MY_SEGMENTS_UPDATED, + SPLITS_UPDATED, MY_LARGE_SEGMENTS_UPDATED, RULE_BASED_SEGMENTS_UPDATED, + SPLIT_KILLED_NOTIFICATION, + + // Sync completion events (fired when sync completes, regardless of data change) + TARGETING_RULES_SYNC_COMPLETE, + MEMBERSHIPS_SYNC_COMPLETE, + + // Other events + SDK_READY_TIMEOUT_REACHED, } diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitLogging.java b/events-domain/src/main/java/io/split/android/client/events/SplitLogging.java new file mode 100644 index 000000000..4613f6c86 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/SplitLogging.java @@ -0,0 +1,35 @@ +package io.split.android.client.events; + +import io.harness.events.Logging; +import io.split.android.client.utils.logger.Logger; + +/** + * Implementation of {@link Logging} that delegates to the Split SDK {@link Logger}. + */ +public class SplitLogging implements Logging { + + @Override + public void logError(String message) { + Logger.e(message); + } + + @Override + public void logWarning(String message) { + Logger.w(message); + } + + @Override + public void logInfo(String message) { + Logger.i(message); + } + + @Override + public void logDebug(String message) { + Logger.d(message); + } + + @Override + public void logVerbose(String message) { + Logger.v(message); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/delivery/DualExecutorRegistration.java b/events-domain/src/main/java/io/split/android/client/events/delivery/DualExecutorRegistration.java new file mode 100644 index 000000000..56ec23658 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/delivery/DualExecutorRegistration.java @@ -0,0 +1,134 @@ +package io.split.android.client.events.delivery; + +import androidx.annotation.NonNull; + +import java.util.concurrent.Executor; + +import io.harness.events.EventHandler; +import io.harness.events.EventsManager; +import io.harness.events.Logging; +import io.split.android.client.events.logging.SplitLogging; + +/** + * Utility for registering event handlers that need to execute on two different threads. + *

+ * This is useful when an event should trigger both background work and UI updates. + * Each callback is wrapped with its executor before registration. + * + * @param event type + * @param internal event type (for EventsManager) + * @param metadata type + */ +public class DualExecutorRegistration { + + @NonNull + private final Executor mBackgroundExecutor; + @NonNull + private final Executor mMainThreadExecutor; + @NonNull + private final Logging mLogging; + + /** + * Creates a new DualExecutorRegistration with a {@link SplitLogging} instance. + * + * @param backgroundExecutor executor for background execution + * @param mainThreadExecutor executor for main thread execution + */ + public DualExecutorRegistration(@NonNull Executor backgroundExecutor, + @NonNull Executor mainThreadExecutor) { + this(backgroundExecutor, mainThreadExecutor, new SplitLogging()); + } + + /** + * Creates a new DualExecutorRegistration. + *

+ * Package-private for testing. + * + * @param backgroundExecutor executor for background execution + * @param mainThreadExecutor executor for main thread execution + * @param logging logging instance + */ + DualExecutorRegistration(@NonNull Executor backgroundExecutor, + @NonNull Executor mainThreadExecutor, + @NonNull Logging logging) { + if (backgroundExecutor == null) { + throw new IllegalArgumentException("backgroundExecutor cannot be null"); + } + if (mainThreadExecutor == null) { + throw new IllegalArgumentException("mainThreadExecutor cannot be null"); + } + if (logging == null) { + throw new IllegalArgumentException("logging cannot be null"); + } + mBackgroundExecutor = backgroundExecutor; + mMainThreadExecutor = mainThreadExecutor; + mLogging = logging; + } + + /** + * Registers two handlers for the same event, each executing on its respective thread. + * + * @param eventsManager the events manager to register with + * @param event the event to register for + * @param backgroundCallback callback to execute on the background thread + * @param mainThreadCallback callback to execute on the main thread + */ + public void register(@NonNull EventsManager eventsManager, + @NonNull E event, + @NonNull EventHandler backgroundCallback, + @NonNull EventHandler mainThreadCallback) { + if (eventsManager == null || event == null) { + return; + } + + if (backgroundCallback != null) { + eventsManager.register(event, wrapWithExecutor(backgroundCallback, mBackgroundExecutor)); + } + + if (mainThreadCallback != null) { + eventsManager.register(event, wrapWithExecutor(mainThreadCallback, mMainThreadExecutor)); + } + } + + /** + * Registers a single handler for the background thread only. + * + * @param eventsManager the events manager to register with + * @param event the event to register for + * @param backgroundCallback callback to execute on the background thread + */ + public void registerBackground(@NonNull EventsManager eventsManager, + @NonNull E event, + @NonNull EventHandler backgroundCallback) { + if (eventsManager == null || event == null || backgroundCallback == null) { + return; + } + eventsManager.register(event, wrapWithExecutor(backgroundCallback, mBackgroundExecutor)); + } + + /** + * Registers a single handler for the main thread only. + * + * @param eventsManager the events manager to register with + * @param event the event to register for + * @param mainThreadCallback callback to execute on the main thread + */ + public void registerMainThread(@NonNull EventsManager eventsManager, + @NonNull E event, + @NonNull EventHandler mainThreadCallback) { + if (eventsManager == null || event == null || mainThreadCallback == null) { + return; + } + eventsManager.register(event, wrapWithExecutor(mainThreadCallback, mMainThreadExecutor)); + } + + private EventHandler wrapWithExecutor(EventHandler handler, Executor executor) { + return (event, metadata) -> executor.execute(() -> { + try { + handler.handle(event, metadata); + } catch (Exception e) { + mLogging.logError("Exception in event handler: " + e.getMessage()); + } + }); + } +} diff --git a/src/main/java/io/split/android/client/events/executors/ClientEventSplitTask.java b/events-domain/src/main/java/io/split/android/client/events/executors/ClientEventSplitTask.java similarity index 100% rename from src/main/java/io/split/android/client/events/executors/ClientEventSplitTask.java rename to events-domain/src/main/java/io/split/android/client/events/executors/ClientEventSplitTask.java diff --git a/src/main/java/io/split/android/client/events/executors/SplitEventExecutor.java b/events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutor.java similarity index 100% rename from src/main/java/io/split/android/client/events/executors/SplitEventExecutor.java rename to events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutor.java diff --git a/src/main/java/io/split/android/client/events/executors/SplitEventExecutorFactory.java b/events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorFactory.java similarity index 100% rename from src/main/java/io/split/android/client/events/executors/SplitEventExecutorFactory.java rename to events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorFactory.java diff --git a/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResources.java b/events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResources.java similarity index 100% rename from src/main/java/io/split/android/client/events/executors/SplitEventExecutorResources.java rename to events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResources.java diff --git a/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImpl.java b/events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImpl.java similarity index 80% rename from src/main/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImpl.java rename to events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImpl.java index 337d3e28e..c05a2e1c2 100644 --- a/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImpl.java +++ b/events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImpl.java @@ -1,6 +1,6 @@ package io.split.android.client.events.executors; -import static io.split.android.client.utils.Utils.checkNotNull; +import static java.util.Objects.requireNonNull; import io.split.android.client.SplitClient; @@ -14,7 +14,7 @@ public class SplitEventExecutorResourcesImpl implements SplitEventExecutorResour @Override public void setSplitClient(SplitClient client) { - mClient = checkNotNull(client); + mClient = requireNonNull(client); } @Override diff --git a/src/main/java/io/split/android/client/events/executors/SplitEventExecutorWithClient.java b/events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorWithClient.java similarity index 90% rename from src/main/java/io/split/android/client/events/executors/SplitEventExecutorWithClient.java rename to events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorWithClient.java index c2f3af4db..0181ea703 100644 --- a/src/main/java/io/split/android/client/events/executors/SplitEventExecutorWithClient.java +++ b/events-domain/src/main/java/io/split/android/client/events/executors/SplitEventExecutorWithClient.java @@ -1,6 +1,6 @@ package io.split.android.client.events.executors; -import static io.split.android.client.utils.Utils.checkNotNull; +import static java.util.Objects.requireNonNull; import androidx.annotation.NonNull; @@ -18,7 +18,7 @@ public class SplitEventExecutorWithClient implements SplitEventExecutor { public SplitEventExecutorWithClient(@NonNull SplitTaskExecutor taskExecutor, @NonNull SplitEventTask task, @NonNull SplitClient client) { - mSplitTaskExecutor = checkNotNull(taskExecutor); + mSplitTaskExecutor = requireNonNull(taskExecutor); mBackgroundSplitTask = new ClientEventSplitTask(task, client, false); mMainThreadSplitTask = new ClientEventSplitTask(task, client, true); } diff --git a/events-domain/src/main/java/io/split/android/client/events/logging/SplitLogging.java b/events-domain/src/main/java/io/split/android/client/events/logging/SplitLogging.java new file mode 100644 index 000000000..e40e8fe9e --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/logging/SplitLogging.java @@ -0,0 +1,35 @@ +package io.split.android.client.events.logging; + +import io.harness.events.Logging; +import io.split.android.client.utils.logger.Logger; + +/** + * Implementation of {@link Logging} that delegates to the Split SDK {@link Logger}. + */ +public class SplitLogging implements Logging { + + @Override + public void logError(String message) { + Logger.e(message); + } + + @Override + public void logWarning(String message) { + Logger.w(message); + } + + @Override + public void logInfo(String message) { + Logger.i(message); + } + + @Override + public void logDebug(String message) { + Logger.d(message); + } + + @Override + public void logVerbose(String message) { + Logger.v(message); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadata.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadata.java new file mode 100644 index 000000000..b98c96f1e --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadata.java @@ -0,0 +1,53 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collection; + +/** + * Represents metadata associated with SDK events. + *

+ * This is an internal API for SDK infrastructure use. + * Consumers should use the typed metadata classes instead: + * {@code SdkUpdateMetadata} and {@code SdkReadyMetadata}. + *

+ * Values are sanitized to only allow String, Number, Boolean, or List<String>. + */ +public interface EventMetadata { + + /** + * Returns the number of entries in this metadata. + */ + int size(); + + /** + * Returns whether this metadata has no entries. + */ + default boolean isEmpty() { + return size() == 0; + } + + /** + * Returns the collection of values in this metadata. + */ + @NonNull + Collection values(); + + /** + * Returns the value associated with the given key. + * + * @param key the key to look up + * @return the value associated with the key, or null if not found + */ + @Nullable + Object get(@NonNull String key); + + /** + * Returns whether this metadata contains the given key. + * + * @param key the key to check + * @return true if the key exists, false otherwise + */ + boolean containsKey(@NonNull String key); +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataBuilder.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataBuilder.java new file mode 100644 index 000000000..86c3b142b --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataBuilder.java @@ -0,0 +1,101 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Builder for creating {@link EventMetadata} instances. + *

+ * Values are validated during put operations. Only String, Number, Boolean, + * and List<String> values are accepted. Invalid values will be silently ignored. + */ +class EventMetadataBuilder { + + private static final MetadataValidator DEFAULT_VALIDATOR = new MetadataValidatorImpl(); + + private final Map mData = new HashMap<>(); + private final MetadataValidator mValidator; + + EventMetadataBuilder() { + this(DEFAULT_VALIDATOR); + } + + @VisibleForTesting + EventMetadataBuilder(@NonNull MetadataValidator validator) { + mValidator = validator; + } + + /** + * Adds a String value to the metadata. + * + * @param key the key + * @param value the String value + * @return this builder + */ + @NonNull + public EventMetadataBuilder put(@NonNull String key, @NonNull String value) { + if (mValidator.isValidValue(value)) { + mData.put(key, value); + } + return this; + } + + /** + * Adds a Number value to the metadata. + * + * @param key the key + * @param value the Number value (Integer, Long, Double, Float, etc.) + * @return this builder + */ + @NonNull + public EventMetadataBuilder put(@NonNull String key, @NonNull Number value) { + if (mValidator.isValidValue(value)) { + mData.put(key, value); + } + return this; + } + + /** + * Adds a Boolean value to the metadata. + * + * @param key the key + * @param value the Boolean value + * @return this builder + */ + @NonNull + public EventMetadataBuilder put(@NonNull String key, boolean value) { + if (mValidator.isValidValue(value)) { + mData.put(key, value); + } + return this; + } + + /** + * Adds a List of Strings to the metadata. + * + * @param key the key + * @param value the list of strings + * @return this builder + */ + @NonNull + public EventMetadataBuilder put(@NonNull String key, @NonNull List value) { + if (mValidator.isValidValue(value)) { + mData.put(key, value); + } + return this; + } + + /** + * Builds the {@link EventMetadata} instance. + * + * @return a new immutable EventMetadata instance + */ + @NonNull + public EventMetadata build() { + return new EventMetadataImpl(mData); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java new file mode 100644 index 000000000..16018110b --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java @@ -0,0 +1,80 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +/** + * Helper class for creating {@link EventMetadata} instances. + *

+ * This keeps the metadata keys in a single place to avoid typos and inconsistencies. + */ +public class EventMetadataHelpers { + + private EventMetadataHelpers() { + // Utility class + } + + /** + * Creates metadata for SDK_UPDATE events when flags are updated. + * + * @param updatedFlagNames the list of flag names that were updated + * @return the event metadata with TYPE=FLAGS_UPDATE and NAMES containing the flag names + */ + public static EventMetadata createUpdatedFlagsMetadata(List updatedFlagNames) { + return new EventMetadataBuilder() + .put(MetadataKeys.TYPE, MetadataKeys.TYPE_FLAGS_UPDATE) + .put(MetadataKeys.NAMES, new ArrayList<>(new HashSet<>(updatedFlagNames))) + .build(); + } + + /** + * Creates metadata for SDK_UPDATE events when segments are updated. + *

+ * SEGMENTS_UPDATE always has empty names - segment names are not included in the metadata. + * + * @return the event metadata with TYPE=SEGMENTS_UPDATE and empty NAMES list + */ + public static EventMetadata createUpdatedSegmentsMetadata() { + return new EventMetadataBuilder() + .put(MetadataKeys.TYPE, MetadataKeys.TYPE_SEGMENTS_UPDATE) + .put(MetadataKeys.NAMES, Collections.emptyList()) + .build(); + } + + /** + * Creates metadata for the SDK_READY and SDK_READY_FROM_CACHE events. + * + * @param lastUpdateTimestamp the timestamp when the cache was last updated, or null if not available + * @param initialCacheLoad true if this is an initial cache load (no prior cache), false if loaded from cache + * @return the event metadata + */ + public static EventMetadata createReadyMetadata(@Nullable Long lastUpdateTimestamp, boolean initialCacheLoad) { + EventMetadataBuilder builder = new EventMetadataBuilder() + .put(MetadataKeys.INITIAL_CACHE_LOAD, initialCacheLoad); + + if (lastUpdateTimestamp != null) { + builder.put(MetadataKeys.LAST_UPDATE_TIMESTAMP, lastUpdateTimestamp); + } + + return builder.build(); + } + + /** + * Creates metadata for TARGETING_RULES_SYNC_COMPLETE based on whether cache was already loaded. + *

+ * If cache was already loaded (SDK_READY_FROM_CACHE fired), uses initialCacheLoad=false + * and includes the update timestamp. Otherwise, uses initialCacheLoad=true with no timestamp. + * + * @param cacheAlreadyLoaded true if SDK_READY_FROM_CACHE has already fired + * @param updateTimestamp the timestamp from storage, used only if cacheAlreadyLoaded is true + * @return the event metadata for sync complete + */ + public static EventMetadata createSyncCompleteMetadata(boolean cacheAlreadyLoaded, @Nullable Long updateTimestamp) { + Long timestamp = cacheAlreadyLoaded ? updateTimestamp : null; + return createReadyMetadata(timestamp, !cacheAlreadyLoaded); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java new file mode 100644 index 000000000..97aace947 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java @@ -0,0 +1,55 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of {@link EventMetadata}. + * Use {@link EventMetadataBuilder} to create instances. + */ +class EventMetadataImpl implements EventMetadata { + + private final Map mData; + + EventMetadataImpl(@NonNull Map data) { + Map copy = new HashMap<>(); + for (Map.Entry entry : data.entrySet()) { + Object value = entry.getValue(); + if (value instanceof List) { + copy.put(entry.getKey(), Collections.unmodifiableList(new ArrayList<>((List) value))); + } else { + copy.put(entry.getKey(), value); + } + } + mData = Collections.unmodifiableMap(copy); + } + + @Override + public int size() { + return mData.size(); + } + + @NonNull + @Override + public Collection values() { + return mData.values(); + } + + @Nullable + @Override + public Object get(@NonNull String key) { + return mData.get(key); + } + + @Override + public boolean containsKey(@NonNull String key) { + return mData.containsKey(key); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java new file mode 100644 index 000000000..73ff243e7 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java @@ -0,0 +1,45 @@ +package io.split.android.client.events.metadata; + +/** + * Consolidated metadata keys for SDK events. + *

+ * Package-private - for internal SDK use only. + */ +final class MetadataKeys { + + private MetadataKeys() { + // no instances + } + + // SDK_UPDATE event keys + + /** + * The type of update (FLAGS_UPDATE or SEGMENTS_UPDATE). + */ + static final String TYPE = "type"; + + static final String TYPE_FLAGS_UPDATE = "FLAGS_UPDATE"; + static final String TYPE_SEGMENTS_UPDATE = "SEGMENTS_UPDATE"; + + /** + * Names of entities that changed in this update. + *

+ * For FLAGS_UPDATE, these are flag names. + * For SEGMENTS_UPDATE, these are rule-based segment names. + */ + static final String NAMES = "names"; + + // SDK_READY and SDK_READY_FROM_CACHE event keys + + /** + * True if this is an initial cache load with no usable cache. + */ + static final String INITIAL_CACHE_LOAD = "initialCacheLoad"; + + /** + * Last successful cache timestamp in milliseconds since epoch. + *

+ * May be absent when not available. + */ + static final String LAST_UPDATE_TIMESTAMP = "lastUpdateTimestamp"; +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataValidator.java b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataValidator.java new file mode 100644 index 000000000..3a25ff3ba --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataValidator.java @@ -0,0 +1,8 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.Nullable; + +interface MetadataValidator { + + boolean isValidValue(@Nullable Object value); +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataValidatorImpl.java b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataValidatorImpl.java new file mode 100644 index 000000000..64a579d53 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataValidatorImpl.java @@ -0,0 +1,31 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.Nullable; + +import java.util.List; + +class MetadataValidatorImpl implements MetadataValidator { + + @Override + public boolean isValidValue(@Nullable Object value) { + if (value == null) { + return false; + } + + if (value instanceof String || value instanceof Number || value instanceof Boolean) { + return true; + } + + if (value instanceof List) { + List list = (List) value; + for (Object item : list) { + if (!(item instanceof String)) { + return false; + } + } + return true; + } + + return false; + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java new file mode 100644 index 000000000..9c2e2b526 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java @@ -0,0 +1,66 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +import io.split.android.client.events.SdkReadyMetadata; +import io.split.android.client.events.SdkUpdateMetadata; + +/** + * Converts {@link EventMetadata} to typed metadata objects for typed event tasks. +*/ +public class TypedTaskConverter { + + private TypedTaskConverter() { + // Utility class + } + + /** + * Converts EventMetadata to SdkUpdateMetadata. + * + * @param metadata the event metadata, may be null + * @return the typed metadata for SDK_UPDATE events + */ + @NonNull + @SuppressWarnings("unchecked") + public static SdkUpdateMetadata convertForSdkUpdate(@Nullable EventMetadata metadata) { + SdkUpdateMetadata.Type type = null; + List names = null; + + if (metadata != null) { + // Extract type + String typeString = (String) metadata.get(MetadataKeys.TYPE); + if (typeString != null) { + try { + type = SdkUpdateMetadata.Type.valueOf(typeString); + } catch (IllegalArgumentException ignored) { + // Unknown type, leave as null + } + } + + // Extract names + names = (List) metadata.get(MetadataKeys.NAMES); + } + + return new SdkUpdateMetadata(type, names); + } + + /** + * Converts EventMetadata to SdkReadyMetadata. + * + * @param metadata the event metadata, may be null + * @return the typed metadata for SDK_READY and SDK_READY_FROM_CACHE events + */ + @NonNull + public static SdkReadyMetadata convertForSdkReady(@Nullable EventMetadata metadata) { + Boolean initialCacheLoad = null; + Long lastUpdateTimestamp = null; + if (metadata != null) { + initialCacheLoad = (Boolean) metadata.get(MetadataKeys.INITIAL_CACHE_LOAD); + lastUpdateTimestamp = (Long) metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP); + } + return new SdkReadyMetadata(initialCacheLoad, lastUpdateTimestamp); + } +} diff --git a/src/main/java/io/split/android/client/service/executor/SplitTask.java b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTask.java rename to events-domain/src/main/java/io/split/android/client/service/executor/SplitTask.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskBatchItem.java b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskBatchItem.java similarity index 85% rename from src/main/java/io/split/android/client/service/executor/SplitTaskBatchItem.java rename to events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskBatchItem.java index 944ec329b..ada8052a8 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskBatchItem.java +++ b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskBatchItem.java @@ -1,6 +1,6 @@ package io.split.android.client.service.executor; -import static io.split.android.client.utils.Utils.checkNotNull; +import static java.util.Objects.requireNonNull; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -12,7 +12,7 @@ public class SplitTaskBatchItem { private final WeakReference listener; public SplitTaskBatchItem(@NonNull SplitTask task, @Nullable SplitTaskExecutionListener listener) { - this.task = checkNotNull(task); + this.task = requireNonNull(task); this.listener = new WeakReference<>(listener); } diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java similarity index 93% rename from src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java rename to events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java index 09683c24b..09f120cf9 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java +++ b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java @@ -1,6 +1,6 @@ package io.split.android.client.service.executor; -import static io.split.android.client.utils.Utils.checkNotNull; +import static java.util.Objects.requireNonNull; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -43,9 +43,9 @@ public static SplitTaskExecutionInfo error(SplitTaskType taskType, private SplitTaskExecutionInfo(SplitTaskType taskType, @NonNull SplitTaskExecutionStatus status, @NonNull Map data) { - this.taskType = checkNotNull(taskType); - this.status = checkNotNull(status); - this.data = checkNotNull(data); + this.taskType = requireNonNull(taskType); + this.status = requireNonNull(status); + this.data = requireNonNull(data); } public SplitTaskExecutionStatus getStatus() { diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionListener.java b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionListener.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskExecutionListener.java rename to events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionListener.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionStatus.java b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionStatus.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskExecutionStatus.java rename to events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionStatus.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutor.java b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutor.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskExecutor.java rename to events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskExecutor.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskType.java b/events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskType.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskType.java rename to events-domain/src/main/java/io/split/android/client/service/executor/SplitTaskType.java diff --git a/src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutor.java b/events-domain/src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutor.java similarity index 100% rename from src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutor.java rename to events-domain/src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutor.java diff --git a/src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutorImpl.java b/events-domain/src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutorImpl.java similarity index 100% rename from src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutorImpl.java rename to events-domain/src/main/java/io/split/android/engine/scheduler/PausableThreadPoolExecutorImpl.java diff --git a/events-domain/src/test/java/io/split/android/client/events/.gitkeep b/events-domain/src/test/java/io/split/android/client/events/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/events-domain/src/test/java/io/split/android/client/events/DualExecutorRegistrationTest.java b/events-domain/src/test/java/io/split/android/client/events/DualExecutorRegistrationTest.java new file mode 100644 index 000000000..a0a8e4bf0 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/DualExecutorRegistrationTest.java @@ -0,0 +1,231 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import io.harness.events.EventHandler; +import io.harness.events.EventsManager; +import io.harness.events.Logging; + +public class DualExecutorRegistrationTest { + + private static final long TIMEOUT_MS = 1000; + private static final Executor DIRECT_EXECUTOR = Runnable::run; + + private EventsManager mockEventsManager; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + mockEventsManager = mock(EventsManager.class); + } + + @Test + public void registerCallsEventsManagerTwice() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.register( + mockEventsManager, + "testEvent", + (e, m) -> {}, + (e, m) -> {} + ); + + verify(mockEventsManager, times(2)).register(eq("testEvent"), any()); + } + + @Test + public void registerBackgroundCallsEventsManagerOnce() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.registerBackground(mockEventsManager, "testEvent", (e, m) -> {}); + + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test + public void registerMainThreadCallsEventsManagerOnce() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.registerMainThread(mockEventsManager, "testEvent", (e, m) -> {}); + + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test + @SuppressWarnings("unchecked") + public void wrappedHandlersExecuteOnCorrectExecutors() throws InterruptedException { + ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setName("background-thread"); + return t; + }); + ExecutorService mainThreadExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setName("main-thread"); + return t; + }); + + DualExecutorRegistration registration = + new DualExecutorRegistration<>(backgroundExecutor, mainThreadExecutor); + + CountDownLatch latch = new CountDownLatch(2); + AtomicReference bgThreadName = new AtomicReference<>(); + AtomicReference mainThreadName = new AtomicReference<>(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(EventHandler.class); + + registration.register( + mockEventsManager, + "testEvent", + (e, m) -> { + bgThreadName.set(Thread.currentThread().getName()); + latch.countDown(); + }, + (e, m) -> { + mainThreadName.set(Thread.currentThread().getName()); + latch.countDown(); + } + ); + + verify(mockEventsManager, times(2)).register(eq("testEvent"), captor.capture()); + + // Invoke both captured handlers + for (EventHandler handler : captor.getAllValues()) { + handler.handle("testEvent", null); + } + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("background-thread", bgThreadName.get()); + assertEquals("main-thread", mainThreadName.get()); + + backgroundExecutor.shutdown(); + mainThreadExecutor.shutdown(); + } + + @Test + @SuppressWarnings("unchecked") + public void wrappedHandlerSwallowsExceptions() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + AtomicInteger secondCallCount = new AtomicInteger(0); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(EventHandler.class); + + registration.register( + mockEventsManager, + "testEvent", + (e, m) -> { throw new RuntimeException("Test exception"); }, + (e, m) -> secondCallCount.incrementAndGet() + ); + + verify(mockEventsManager, times(2)).register(eq("testEvent"), captor.capture()); + + // Invoke both handlers - first throws, second should still work + for (EventHandler handler : captor.getAllValues()) { + handler.handle("testEvent", null); + } + + assertEquals(1, secondCallCount.get()); + } + + @Test + @SuppressWarnings("unchecked") + public void exceptionInHandlerIsLogged() { + Logging mockLogging = mock(Logging.class); + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR, mockLogging); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(EventHandler.class); + + registration.registerBackground( + mockEventsManager, + "testEvent", + (e, m) -> { throw new RuntimeException("Test exception message"); } + ); + + verify(mockEventsManager).register(eq("testEvent"), captor.capture()); + + captor.getValue().handle("testEvent", null); + + verify(mockLogging).logError(eq("Exception in event handler: Test exception message")); + } + + @Test + public void registerIgnoresNullEventsManager() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + // Should not throw + registration.register(null, "testEvent", (e, m) -> {}, (e, m) -> {}); + } + + @Test + public void registerIgnoresNullEvent() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + // Should not throw + registration.register(mockEventsManager, null, (e, m) -> {}, (e, m) -> {}); + + verify(mockEventsManager, times(0)).register(any(), any()); + } + + @Test + public void registerHandlesNullBackgroundCallback() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.register(mockEventsManager, "testEvent", null, (e, m) -> {}); + + // Only main thread callback should be registered + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test + public void registerHandlesNullMainThreadCallback() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.register(mockEventsManager, "testEvent", (e, m) -> {}, null); + + // Only background callback should be registered + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorThrowsOnNullBackgroundExecutor() { + new DualExecutorRegistration<>(null, DIRECT_EXECUTOR); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorThrowsOnNullMainThreadExecutor() { + new DualExecutorRegistration<>(DIRECT_EXECUTOR, null); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorThrowsOnNullLogging() { + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR, null); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/SplitEventsManagerConfigFactoryTest.java b/events-domain/src/test/java/io/split/android/client/events/SplitEventsManagerConfigFactoryTest.java new file mode 100644 index 000000000..bff046a70 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/SplitEventsManagerConfigFactoryTest.java @@ -0,0 +1,214 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.Set; + +import io.harness.events.EventsManagerConfig; + +public class SplitEventsManagerConfigFactoryTest { + + @Test + public void configIsNotNull() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + assertNotNull(config); + } + + @Test + public void sdkReadyRequiresTargetingRulesSyncCompleteAndMembershipsSyncComplete() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Set requireAll = config.getRequireAll().get(SplitEvent.SDK_READY); + assertNotNull("SDK_READY should have requireAll configuration", requireAll); + assertTrue("SDK_READY should require TARGETING_RULES_SYNC_COMPLETE", + requireAll.contains(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + assertTrue("SDK_READY should require MEMBERSHIPS_SYNC_COMPLETE", + requireAll.contains(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE)); + assertEquals("SDK_READY should require exactly 2 events", 2, requireAll.size()); + } + + @Test + public void sdkReadyHasPrerequisiteSdkReadyFromCache() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Set prerequisites = config.getPrerequisites().get(SplitEvent.SDK_READY); + assertNotNull("SDK_READY should have prerequisites", prerequisites); + assertTrue("SDK_READY should require SDK_READY_FROM_CACHE as prerequisite", + prerequisites.contains(SplitEvent.SDK_READY_FROM_CACHE)); + } + + @Test + public void sdkReadyHasExecutionLimitOne() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_READY); + assertNotNull("SDK_READY should have execution limit", limit); + assertEquals("SDK_READY should fire at most once", 1, (int) limit); + } + + @Test + public void sdkReadyFromCacheHasOrOfAndsConfiguration() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Set> requireAnyGroups = config.getRequireAny().get(SplitEvent.SDK_READY_FROM_CACHE); + assertNotNull("SDK_READY_FROM_CACHE should have requireAny configuration", requireAnyGroups); + assertEquals("SDK_READY_FROM_CACHE should have 2 groups (cache and sync)", 2, requireAnyGroups.size()); + + boolean hasCacheGroup = false; + boolean hasSyncGroup = false; + for (Set group : requireAnyGroups) { + if (group.size() == 4 && + group.contains(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE) && + group.contains(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE) && + group.contains(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE) && + group.contains(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE)) { + hasCacheGroup = true; + } + if (group.size() == 2 && + group.contains(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE) && + group.contains(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE)) { + hasSyncGroup = true; + } + } + assertTrue("SDK_READY_FROM_CACHE should have cache group", hasCacheGroup); + assertTrue("SDK_READY_FROM_CACHE should have sync group", hasSyncGroup); + } + + @Test + public void sdkReadyFromCacheHasExecutionLimitOne() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_READY_FROM_CACHE); + assertNotNull("SDK_READY_FROM_CACHE should have execution limit", limit); + assertEquals("SDK_READY_FROM_CACHE should fire at most once", 1, (int) limit); + } + @Test + public void sdkReadyTimedOutRequiresTimeoutReached() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Set> requireAnyGroups = config.getRequireAny().get(SplitEvent.SDK_READY_TIMED_OUT); + assertNotNull("SDK_READY_TIMED_OUT should have requireAny configuration", requireAnyGroups); + + boolean hasTimeoutTrigger = false; + for (Set group : requireAnyGroups) { + if (group.contains(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED)) { + hasTimeoutTrigger = true; + break; + } + } + assertTrue("SDK_READY_TIMED_OUT should be triggered by SDK_READY_TIMEOUT_REACHED", hasTimeoutTrigger); + } + + @Test + public void sdkReadyTimedOutIsSuppressedBySdkReady() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Set suppressors = config.getSuppressedBy().get(SplitEvent.SDK_READY_TIMED_OUT); + assertNotNull("SDK_READY_TIMED_OUT should have suppressors", suppressors); + assertTrue("SDK_READY_TIMED_OUT should be suppressed by SDK_READY", + suppressors.contains(SplitEvent.SDK_READY)); + } + + @Test + public void sdkReadyTimedOutHasExecutionLimitOne() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_READY_TIMED_OUT); + assertNotNull("SDK_READY_TIMED_OUT should have execution limit", limit); + assertEquals("SDK_READY_TIMED_OUT should fire at most once", 1, (int) limit); + } + + @Test + public void sdkUpdateHasCorrectTriggers() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Set> requireAnyGroups = config.getRequireAny().get(SplitEvent.SDK_UPDATE); + assertNotNull("SDK_UPDATE should have requireAny configuration", requireAnyGroups); + + // Each trigger should be in its own singleton group + boolean hasSplitsUpdated = false; + boolean hasMySegmentsUpdated = false; + boolean hasMyLargeSegmentsUpdated = false; + boolean hasRuleBasedSegmentsUpdated = false; + boolean hasSplitKilledNotification = false; + + for (Set group : requireAnyGroups) { + if (group.size() == 1) { + if (group.contains(SplitInternalEvent.SPLITS_UPDATED)) hasSplitsUpdated = true; + if (group.contains(SplitInternalEvent.MY_SEGMENTS_UPDATED)) hasMySegmentsUpdated = true; + if (group.contains(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED)) hasMyLargeSegmentsUpdated = true; + if (group.contains(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED)) hasRuleBasedSegmentsUpdated = true; + if (group.contains(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION)) hasSplitKilledNotification = true; + } + } + + assertTrue("SDK_UPDATE should be triggered by SPLITS_UPDATED", hasSplitsUpdated); + assertTrue("SDK_UPDATE should be triggered by MY_SEGMENTS_UPDATED", hasMySegmentsUpdated); + assertTrue("SDK_UPDATE should be triggered by MY_LARGE_SEGMENTS_UPDATED", hasMyLargeSegmentsUpdated); + assertTrue("SDK_UPDATE should be triggered by RULE_BASED_SEGMENTS_UPDATED", hasRuleBasedSegmentsUpdated); + assertTrue("SDK_UPDATE should be triggered by SPLIT_KILLED_NOTIFICATION", hasSplitKilledNotification); + } + + @Test + public void sdkUpdateHasPrerequisiteSdkReady() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Set prerequisites = config.getPrerequisites().get(SplitEvent.SDK_UPDATE); + assertNotNull("SDK_UPDATE should have prerequisites", prerequisites); + assertTrue("SDK_UPDATE should require SDK_READY as prerequisite", + prerequisites.contains(SplitEvent.SDK_READY)); + } + + @Test + public void sdkUpdateHasUnlimitedExecutions() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + Integer limit = config.getExecutionLimits().get(SplitEvent.SDK_UPDATE); + assertNotNull("SDK_UPDATE should have execution limit", limit); + assertEquals("SDK_UPDATE should have unlimited executions (-1)", -1, (int) limit); + } + + @Test + public void evaluationOrderIsNotEmpty() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + assertNotNull("Evaluation order should not be null", config.getEvaluationOrder()); + assertFalse("Evaluation order should not be empty", config.getEvaluationOrder().isEmpty()); + } + + @Test + public void evaluationOrderContainsAllConfiguredExternalEvents() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + assertTrue("Evaluation order should contain SDK_READY", + config.getEvaluationOrder().contains(SplitEvent.SDK_READY)); + assertTrue("Evaluation order should contain SDK_READY_FROM_CACHE", + config.getEvaluationOrder().contains(SplitEvent.SDK_READY_FROM_CACHE)); + assertTrue("Evaluation order should contain SDK_READY_TIMED_OUT", + config.getEvaluationOrder().contains(SplitEvent.SDK_READY_TIMED_OUT)); + assertTrue("Evaluation order should contain SDK_UPDATE", + config.getEvaluationOrder().contains(SplitEvent.SDK_UPDATE)); + } + + @Test + public void evaluationOrderHasPrerequisitesBeforeDependents() { + EventsManagerConfig config = SplitEventsManagerConfigFactory.create(); + + // SDK_READY_FROM_CACHE must come before SDK_READY (prerequisite) + int readyFromCacheIndex = config.getEvaluationOrder().indexOf(SplitEvent.SDK_READY_FROM_CACHE); + int readyIndex = config.getEvaluationOrder().indexOf(SplitEvent.SDK_READY); + assertTrue("SDK_READY_FROM_CACHE should be evaluated before SDK_READY", + readyFromCacheIndex < readyIndex); + + // SDK_READY must come before SDK_UPDATE (prerequisite) + int updateIndex = config.getEvaluationOrder().indexOf(SplitEvent.SDK_UPDATE); + assertTrue("SDK_READY should be evaluated before SDK_UPDATE", + readyIndex < updateIndex); + } +} + diff --git a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java new file mode 100644 index 000000000..926a755c5 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java @@ -0,0 +1,81 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.EventMetadataHelpers; +import io.split.android.client.events.metadata.TypedTaskConverter; + +/** + * Tests for typed task metadata conversion. + */ +public class TypedTaskConversionTest { + + @Test + public void convertForSdkUpdateConvertsFlagsMetadataCorrectly() { + List expectedFlags = Arrays.asList("flag1", "flag2"); + + EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedFlagsMetadata(expectedFlags); + + // Call conversion method + SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(eventMetadata); + + assertNotNull(converted); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, converted.getType()); + assertEquals(expectedFlags.size(), converted.getNames().size()); + assertTrue(converted.getNames().containsAll(expectedFlags)); + } + + @Test + public void convertForSdkUpdateConvertsSegmentsMetadataCorrectly() { + // SEGMENTS_UPDATE always has empty names + EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(); + + // Call conversion method + SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(eventMetadata); + + assertNotNull(converted); + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, converted.getType()); + assertTrue("Names should be empty for SEGMENTS_UPDATE", converted.getNames().isEmpty()); + } + + @Test + public void convertForSdkReadyConvertsMetadataCorrectly() { + long expectedTimestamp = 1704067200000L; + + EventMetadata eventMetadata = EventMetadataHelpers.createReadyMetadata(expectedTimestamp, true); + + // Call conversion method + SdkReadyMetadata converted = TypedTaskConverter.convertForSdkReady(eventMetadata); + + assertNotNull(converted); + assertTrue(converted.isInitialCacheLoad()); + assertEquals(Long.valueOf(expectedTimestamp), converted.getLastUpdateTimestamp()); + } + + @Test + public void convertForSdkUpdateHandlesNullMetadata() { + SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(null); + + assertNotNull(converted); + assertNull(converted.getType()); + assertTrue(converted.getNames().isEmpty()); + } + + @Test + public void convertForSdkReadyHandlesNullMetadata() { + SdkReadyMetadata converted = TypedTaskConverter.convertForSdkReady(null); + + assertNotNull(converted); + assertNull(converted.isInitialCacheLoad()); + assertNull(converted.getLastUpdateTimestamp()); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/delivery/DualExecutorRegistrationTest.java b/events-domain/src/test/java/io/split/android/client/events/delivery/DualExecutorRegistrationTest.java new file mode 100644 index 000000000..b748926e5 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/delivery/DualExecutorRegistrationTest.java @@ -0,0 +1,231 @@ +package io.split.android.client.events.delivery; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import io.harness.events.EventHandler; +import io.harness.events.EventsManager; +import io.harness.events.Logging; + +public class DualExecutorRegistrationTest { + + private static final long TIMEOUT_MS = 1000; + private static final Executor DIRECT_EXECUTOR = Runnable::run; + + private EventsManager mockEventsManager; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + mockEventsManager = mock(EventsManager.class); + } + + @Test + public void registerCallsEventsManagerTwice() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.register( + mockEventsManager, + "testEvent", + (e, m) -> {}, + (e, m) -> {} + ); + + verify(mockEventsManager, times(2)).register(eq("testEvent"), any()); + } + + @Test + public void registerBackgroundCallsEventsManagerOnce() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.registerBackground(mockEventsManager, "testEvent", (e, m) -> {}); + + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test + public void registerMainThreadCallsEventsManagerOnce() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.registerMainThread(mockEventsManager, "testEvent", (e, m) -> {}); + + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test + @SuppressWarnings("unchecked") + public void wrappedHandlersExecuteOnCorrectExecutors() throws InterruptedException { + ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setName("background-thread"); + return t; + }); + ExecutorService mainThreadExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setName("main-thread"); + return t; + }); + + DualExecutorRegistration registration = + new DualExecutorRegistration<>(backgroundExecutor, mainThreadExecutor); + + CountDownLatch latch = new CountDownLatch(2); + AtomicReference bgThreadName = new AtomicReference<>(); + AtomicReference mainThreadName = new AtomicReference<>(); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(EventHandler.class); + + registration.register( + mockEventsManager, + "testEvent", + (e, m) -> { + bgThreadName.set(Thread.currentThread().getName()); + latch.countDown(); + }, + (e, m) -> { + mainThreadName.set(Thread.currentThread().getName()); + latch.countDown(); + } + ); + + verify(mockEventsManager, times(2)).register(eq("testEvent"), captor.capture()); + + // Invoke both captured handlers + for (EventHandler handler : captor.getAllValues()) { + handler.handle("testEvent", null); + } + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("background-thread", bgThreadName.get()); + assertEquals("main-thread", mainThreadName.get()); + + backgroundExecutor.shutdown(); + mainThreadExecutor.shutdown(); + } + + @Test + @SuppressWarnings("unchecked") + public void wrappedHandlerSwallowsExceptions() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + AtomicInteger secondCallCount = new AtomicInteger(0); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(EventHandler.class); + + registration.register( + mockEventsManager, + "testEvent", + (e, m) -> { throw new RuntimeException("Test exception"); }, + (e, m) -> secondCallCount.incrementAndGet() + ); + + verify(mockEventsManager, times(2)).register(eq("testEvent"), captor.capture()); + + // Invoke both handlers - first throws, second should still work + for (EventHandler handler : captor.getAllValues()) { + handler.handle("testEvent", null); + } + + assertEquals(1, secondCallCount.get()); + } + + @Test + @SuppressWarnings("unchecked") + public void exceptionInHandlerIsLogged() { + Logging mockLogging = mock(Logging.class); + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR, mockLogging); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(EventHandler.class); + + registration.registerBackground( + mockEventsManager, + "testEvent", + (e, m) -> { throw new RuntimeException("Test exception message"); } + ); + + verify(mockEventsManager).register(eq("testEvent"), captor.capture()); + + captor.getValue().handle("testEvent", null); + + verify(mockLogging).logError(eq("Exception in event handler: Test exception message")); + } + + @Test + public void registerIgnoresNullEventsManager() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + // Should not throw + registration.register(null, "testEvent", (e, m) -> {}, (e, m) -> {}); + } + + @Test + public void registerIgnoresNullEvent() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + // Should not throw + registration.register(mockEventsManager, null, (e, m) -> {}, (e, m) -> {}); + + verify(mockEventsManager, times(0)).register(any(), any()); + } + + @Test + public void registerHandlesNullBackgroundCallback() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.register(mockEventsManager, "testEvent", null, (e, m) -> {}); + + // Only main thread callback should be registered + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test + public void registerHandlesNullMainThreadCallback() { + DualExecutorRegistration registration = + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR); + + registration.register(mockEventsManager, "testEvent", (e, m) -> {}, null); + + // Only background callback should be registered + verify(mockEventsManager, times(1)).register(eq("testEvent"), any()); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorThrowsOnNullBackgroundExecutor() { + new DualExecutorRegistration<>(null, DIRECT_EXECUTOR); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorThrowsOnNullMainThreadExecutor() { + new DualExecutorRegistration<>(DIRECT_EXECUTOR, null); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorThrowsOnNullLogging() { + new DualExecutorRegistration<>(DIRECT_EXECUTOR, DIRECT_EXECUTOR, null); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java new file mode 100644 index 000000000..c9d638dee --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java @@ -0,0 +1,199 @@ +package io.split.android.client.events.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +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.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.List; + +public class EventMetadataBuilderTest { + + @Mock + private MetadataValidator mValidator; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void putStringUsesValidator() { + when(mValidator.isValidValue(any())).thenReturn(true); + + new EventMetadataBuilder(mValidator) + .put("key", "value"); + + verify(mValidator).isValidValue("value"); + } + + @Test + public void putNumberUsesValidator() { + when(mValidator.isValidValue(any())).thenReturn(true); + + new EventMetadataBuilder(mValidator) + .put("key", 42); + + verify(mValidator).isValidValue(42); + } + + @Test + public void putBooleanUsesValidator() { + when(mValidator.isValidValue(any())).thenReturn(true); + + new EventMetadataBuilder(mValidator) + .put("key", true); + + verify(mValidator).isValidValue(true); + } + + @Test + public void putListUsesValidator() { + when(mValidator.isValidValue(any())).thenReturn(true); + List list = Arrays.asList("a", "b"); + + new EventMetadataBuilder(mValidator) + .put("key", list); + + verify(mValidator).isValidValue(list); + } + + @Test + public void putIgnoresValueWhenValidatorReturnsFalse() { + when(mValidator.isValidValue(any())).thenReturn(false); + + EventMetadata metadata = new EventMetadataBuilder(mValidator) + .put("key", "value") + .build(); + + assertFalse(metadata.containsKey("key")); + } + + @Test + public void putIncludesValueWhenValidatorReturnsTrue() { + when(mValidator.isValidValue(any())).thenReturn(true); + + EventMetadata metadata = new EventMetadataBuilder(mValidator) + .put("key", "value") + .build(); + + assertEquals("value", metadata.get("key")); + } + + @Test + public void buildCreatesEmptyMetadataWhenNothingAdded() { + EventMetadata metadata = new EventMetadataBuilder().build(); + + assertTrue(metadata.isEmpty()); + } + + @Test + public void putStringAddsValue() { + EventMetadata metadata = new EventMetadataBuilder() + .put("key", "value") + .build(); + + assertEquals("value", metadata.get("key")); + } + + @Test + public void putIntegerAddsValue() { + EventMetadata metadata = new EventMetadataBuilder() + .put("count", 42) + .build(); + + assertEquals(Integer.valueOf(42), metadata.get("count")); + } + + @Test + public void putLongAddsValue() { + EventMetadata metadata = new EventMetadataBuilder() + .put("timestamp", 1234567890L) + .build(); + + assertEquals(Long.valueOf(1234567890L), metadata.get("timestamp")); + } + + @Test + public void putDoubleAddsValue() { + EventMetadata metadata = new EventMetadataBuilder() + .put("rate", 3.14) + .build(); + + assertEquals(Double.valueOf(3.14), metadata.get("rate")); + } + + @Test + public void putBooleanTrueAddsValue() { + EventMetadata metadata = new EventMetadataBuilder() + .put("enabled", true) + .build(); + + assertEquals(Boolean.TRUE, metadata.get("enabled")); + } + + @Test + public void putBooleanFalseAddsValue() { + EventMetadata metadata = new EventMetadataBuilder() + .put("disabled", false) + .build(); + + assertEquals(Boolean.FALSE, metadata.get("disabled")); + } + + @Test + public void putListOfStringsAddsValue() { + List flags = Arrays.asList("flag_1", "flag_2", "flag_3"); + + EventMetadata metadata = new EventMetadataBuilder() + .put("names", flags) + .build(); + + assertEquals(flags, metadata.get(MetadataKeys.NAMES)); + } + + @Test + public void chainingMultiplePutsWorks() { + EventMetadata metadata = new EventMetadataBuilder() + .put("string", "text") + .put("number", 100) + .put("flag", true) + .put("list", Arrays.asList("a", "b")) + .build(); + + assertEquals(4, metadata.size()); + assertEquals("text", metadata.get("string")); + assertEquals(Integer.valueOf(100), metadata.get("number")); + assertEquals(Boolean.TRUE, metadata.get("flag")); + assertEquals(Arrays.asList("a", "b"), metadata.get("list")); + } + + @Test + public void overwritingKeyUsesLastValue() { + EventMetadata metadata = new EventMetadataBuilder() + .put("key", "first") + .put("key", "second") + .build(); + + assertEquals("second", metadata.get("key")); + } + + @Test + public void buildReturnsNewInstanceEachTime() { + EventMetadataBuilder builder = new EventMetadataBuilder() + .put("key", "value"); + + EventMetadata metadata1 = builder.build(); + EventMetadata metadata2 = builder.build(); + + assertEquals(metadata1.get("key"), metadata2.get("key")); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java new file mode 100644 index 000000000..9dca0abcf --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java @@ -0,0 +1,125 @@ +package io.split.android.client.events.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +public class EventMetadataHelpersTest { + + // Tests for createUpdatedFlagsMetadata + @Test + @SuppressWarnings("unchecked") + public void createUpdatedFlagsMetadataContainsTypeAndNames() { + List flags = Arrays.asList("flag1", "flag2", "flag3"); + EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata(flags); + + assertTrue(metadata.containsKey(MetadataKeys.TYPE)); + assertEquals(MetadataKeys.TYPE_FLAGS_UPDATE, metadata.get(MetadataKeys.TYPE)); + + // Check names + assertTrue(metadata.containsKey(MetadataKeys.NAMES)); + List result = (List) metadata.get(MetadataKeys.NAMES); + assertEquals(3, result.size()); + assertTrue(result.contains("flag1")); + assertTrue(result.contains("flag2")); + assertTrue(result.contains("flag3")); + } + + // Tests for createUpdatedSegmentsMetadata + @Test + @SuppressWarnings("unchecked") + public void createUpdatedSegmentsMetadataContainsTypeAndEmptyNames() { + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(); + + assertTrue(metadata.containsKey(MetadataKeys.TYPE)); + assertEquals(MetadataKeys.TYPE_SEGMENTS_UPDATE, metadata.get(MetadataKeys.TYPE)); + + // Check names - should always be empty + assertTrue(metadata.containsKey(MetadataKeys.NAMES)); + List result = (List) metadata.get(MetadataKeys.NAMES); + assertTrue("Names should be empty for SEGMENTS_UPDATE", result.isEmpty()); + } + + // Tests for createReadyMetadata + @Test + public void createReadyMetadataWithTimestampAndInitialCacheLoadFalse() { + EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(1234567890L, false); + + assertEquals(Long.valueOf(1234567890L), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD)); + } + + @Test + public void createReadyMetadataWithNullTimestampAndInitialCacheLoadTrue() { + EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(null, true); + + assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD)); + } + + @Test + public void createReadyMetadataKeysAreCorrect() { + EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(123L, false); + + assertTrue(metadata.containsKey(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertTrue(metadata.containsKey(MetadataKeys.INITIAL_CACHE_LOAD)); + assertEquals(2, metadata.size()); + } + + @Test + public void createReadyMetadataWithZeroTimestamp() { + EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(0L, false); + + assertEquals(Long.valueOf(0L), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD)); + } + + @Test + public void createReadyMetadataForCachePath() { + // Cache path: initialCacheLoad=false, timestamp from storage + long storedTimestamp = 1700000000000L; + EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(storedTimestamp, false); + + assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD)); + assertEquals(Long.valueOf(storedTimestamp), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + } + + @Test + public void createReadyMetadataForSyncPath() { + // Sync path: initialCacheLoad=true, timestamp=null + EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(null, true); + + assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD)); + assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + } + + @Test + public void createSyncCompleteMetadataWhenCacheAlreadyLoaded() { + long updateTimestamp = 1234567890L; + EventMetadata metadata = EventMetadataHelpers.createSyncCompleteMetadata(true, updateTimestamp); + + assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD)); + assertEquals(updateTimestamp, metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + } + + @Test + public void createSyncCompleteMetadataWhenCacheNotLoaded() { + EventMetadata metadata = EventMetadataHelpers.createSyncCompleteMetadata(false, 1234567890L); + + assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD)); + assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + } + + @Test + public void createSyncCompleteMetadataIgnoresTimestampWhenCacheNotLoaded() { + // Even if a timestamp is provided, it should be ignored when cache is not loaded + EventMetadata metadata = EventMetadataHelpers.createSyncCompleteMetadata(false, 9999999999L); + + assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java new file mode 100644 index 000000000..5f539c6a6 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java @@ -0,0 +1,149 @@ +package io.split.android.client.events.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class EventMetadataImplTest { + + @Test + public void sizeAndContainsKeyReflectStoredEntries() { + Map data = new HashMap<>(); + data.put("key1", "value1"); + data.put("key2", 42); + data.put("key3", true); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + assertEquals(3, metadata.size()); + assertTrue(metadata.containsKey("key1")); + assertTrue(metadata.containsKey("key2")); + assertTrue(metadata.containsKey("key3")); + } + + @Test + public void isEmptyReturnsTrueForEmptyMetadata() { + EventMetadataImpl metadata = new EventMetadataImpl(new HashMap<>()); + + assertTrue(metadata.isEmpty()); + } + + @Test + public void valuesReturnsAllValues() { + Map data = new HashMap<>(); + data.put("string", "value"); + data.put("number", 42); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + Collection values = metadata.values(); + + assertEquals(2, values.size()); + assertTrue(values.contains("value")); + assertTrue(values.contains(42)); + } + + @Test + public void valuesReturnsEmptyCollectionForEmptyMetadata() { + EventMetadataImpl metadata = new EventMetadataImpl(new HashMap<>()); + + assertTrue(metadata.values().isEmpty()); + } + + @Test + public void getReturnsValueForExistingKey() { + Map data = new HashMap<>(); + data.put("key", "value"); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + assertEquals("value", metadata.get("key")); + } + + @Test + public void getReturnsNullForNonExistingKey() { + Map data = new HashMap<>(); + data.put("key", "value"); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + assertNull(metadata.get("nonExistingKey")); + } + + @Test + public void containsKeyReturnsTrueForExistingKey() { + Map data = new HashMap<>(); + data.put("key", "value"); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + assertTrue(metadata.containsKey("key")); + } + + @Test + public void containsKeyReturnsFalseForNonExistingKey() { + Map data = new HashMap<>(); + data.put("key", "value"); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + assertFalse(metadata.containsKey("nonExistingKey")); + } + + @Test + public void metadataIsImmutableAfterConstruction() { + Map data = new HashMap<>(); + data.put("key", "value"); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + // Modify original map + data.put("newKey", "newValue"); + + // Metadata should not be affected + assertFalse(metadata.containsKey("newKey")); + assertEquals(1, metadata.size()); + } + + @Test + @SuppressWarnings("unchecked") + public void listIsDefensivelyCopiedDuringConstruction() { + List originalList = new ArrayList<>(Arrays.asList("flag_1", "flag_2")); + Map data = new HashMap<>(); + data.put("flags", originalList); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + // Modify original list after construction + originalList.add("flag_3"); + + // Metadata should not be affected + List storedList = (List) metadata.get("flags"); + assertEquals(2, storedList.size()); + assertEquals(Arrays.asList("flag_1", "flag_2"), storedList); + } + + @Test(expected = UnsupportedOperationException.class) + @SuppressWarnings("unchecked") + public void listReturnedByGetIsUnmodifiable() { + Map data = new HashMap<>(); + data.put("flags", Arrays.asList("flag_1", "flag_2")); + + EventMetadataImpl metadata = new EventMetadataImpl(data); + + List list = (List) metadata.get("flags"); + + // This should throw UnsupportedOperationException + list.add("flag_3"); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java new file mode 100644 index 000000000..bf5d6db35 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java @@ -0,0 +1,32 @@ +package io.split.android.client.events.metadata; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Tests for {@link MetadataKeys}. + * Verifies that all metadata keys are correctly defined. + */ +public class MetadataKeysTest { + + @Test + public void typeKeyHasCorrectValue() { + assertEquals("type", MetadataKeys.TYPE); + } + + @Test + public void namesKeyHasCorrectValue() { + assertEquals("names", MetadataKeys.NAMES); + } + + @Test + public void initialCacheLoadKeyHasCorrectValue() { + assertEquals("initialCacheLoad", MetadataKeys.INITIAL_CACHE_LOAD); + } + + @Test + public void lastUpdateTimestampKeyHasCorrectValue() { + assertEquals("lastUpdateTimestamp", MetadataKeys.LAST_UPDATE_TIMESTAMP); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataValidatorImplTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataValidatorImplTest.java new file mode 100644 index 000000000..ad3098ef2 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataValidatorImplTest.java @@ -0,0 +1,132 @@ +package io.split.android.client.events.metadata; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class MetadataValidatorImplTest { + + private MetadataValidator mValidator; + + @Before + public void setUp() { + mValidator = new MetadataValidatorImpl(); + } + + @Test + public void isValidValueReturnsTrueForString() { + assertTrue(mValidator.isValidValue("value")); + } + + @Test + public void isValidValueReturnsTrueForEmptyString() { + assertTrue(mValidator.isValidValue("")); + } + + @Test + public void isValidValueReturnsTrueForInteger() { + assertTrue(mValidator.isValidValue(42)); + } + + @Test + public void isValidValueReturnsTrueForLong() { + assertTrue(mValidator.isValidValue(1234567890L)); + } + + @Test + public void isValidValueReturnsTrueForDouble() { + assertTrue(mValidator.isValidValue(3.14)); + } + + @Test + public void isValidValueReturnsTrueForFloat() { + assertTrue(mValidator.isValidValue(2.5f)); + } + + @Test + public void isValidValueReturnsTrueForBooleanTrue() { + assertTrue(mValidator.isValidValue(true)); + } + + @Test + public void isValidValueReturnsTrueForBooleanFalse() { + assertTrue(mValidator.isValidValue(false)); + } + + @Test + public void isValidValueReturnsTrueForListOfStrings() { + List list = Arrays.asList("flag_1", "flag_2", "flag_3"); + assertTrue(mValidator.isValidValue(list)); + } + + @Test + public void isValidValueReturnsTrueForEmptyList() { + assertTrue(mValidator.isValidValue(Collections.emptyList())); + } + + @Test + public void isValidValueReturnsTrueForSingleElementStringList() { + assertTrue(mValidator.isValidValue(Collections.singletonList("single"))); + } + + @Test + public void isValidValueReturnsFalseForNull() { + assertFalse(mValidator.isValidValue(null)); + } + + @Test + public void isValidValueReturnsFalseForListWithNullElement() { + List list = Arrays.asList("valid", null, "also_valid"); + assertFalse(mValidator.isValidValue(list)); + } + + @Test + public void isValidValueReturnsFalseForListWithMixedTypes() { + List mixedList = Arrays.asList("string", 123, true); + assertFalse(mValidator.isValidValue(mixedList)); + } + + @Test + public void isValidValueReturnsFalseForListOfIntegers() { + List intList = Arrays.asList(1, 2, 3); + assertFalse(mValidator.isValidValue(intList)); + } + + @Test + public void isValidValueReturnsFalseForListOfBooleans() { + List boolList = Arrays.asList(true, false, true); + assertFalse(mValidator.isValidValue(boolList)); + } + + @Test + public void isValidValueReturnsFalseForPlainObject() { + assertFalse(mValidator.isValidValue(new Object())); + } + + @Test + public void isValidValueReturnsFalseForMap() { + assertFalse(mValidator.isValidValue(new HashMap())); + } + + @Test + public void isValidValueReturnsFalseForNestedList() { + List> nestedList = Arrays.asList( + Arrays.asList("a", "b"), + Arrays.asList("c", "d") + ); + assertFalse(mValidator.isValidValue(nestedList)); + } + + @Test + public void isValidValueReturnsFalseForArray() { + String[] array = {"a", "b", "c"}; + assertFalse(mValidator.isValidValue(array)); + } +} diff --git a/events/.gitignore b/events/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/events/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/events/README.md b/events/README.md new file mode 100644 index 000000000..b91d8c63f --- /dev/null +++ b/events/README.md @@ -0,0 +1,78 @@ +# Events module + +This module provides a generic events management system. + +Allows the definition of internal and external events interdependencies, as well as registration. + +## Core Concepts + +### Internal vs External Events + +- **Internal Events**: Low-level events triggered by the system (e.g., data loaded, sync completed) +- **External Events**: High-level events exposed to consumers (e.g., SDK_READY, SDK_UPDATE) + +### Event Configuration + +Events are configured using `EventsManagerConfig.Builder`: + +- **`requireAll(external, internal...)`**: External event fires when ALL internal events have occurred +- **`requireAny(external, internal...)`**: External event fires when ANY internal event occurs +- **`requireAny(external, Set...)`**: OR-of-ANDs pattern; fires when any group is fully satisfied +- **`prerequisite(external, prerequisiteExternal)`**: External event can only fire after the prerequisite external event has fired +- **`suppressedBy(external, suppressorExternal)`**: External event is permanently suppressed if the suppressor external event has already fired +- **`executionLimit(external, limit)`**: Max times the event can fire (-1 = unlimited, 1 = once only) +- **`metadataSource(external, internal)`**: For `requireAll`, selects the internal event whose metadata will be delivered +- **`metadataSource(external, Set, internal)`**: For `requireAny` groups, selects the metadata source per group + +## Topological Sort for Evaluation Order + +The events system uses **topological sorting** to determine the order in which external events are evaluated. This is essential for correctness. + +### Evaluation Flow + +1. **Internal Event Arrives**: A single internal event can potentially satisfy conditions for multiple external events. +2. **Single-Pass Evaluation**: The system iterates through a pre-computed list of external events (`mEvaluationOrder`). +3. **Order Matters**: This list is topologically sorted so that events with dependencies (prerequisites/suppression) come *after* the events they depend on. +4. **Metadata Selection**: When an external event fires, metadata is resolved from the configured source event: + - `requireAll`: use the configured source internal event + - `requireAny`: use the source configured for the specific group that completed + +### Why It's Necessary + +When a single internal event notification could trigger multiple external events, they must be evaluated in the correct order based on their dependencies. + +#### Prerequisite Example + +``` +SDK_READY_FROM_CACHE ←prerequisite← SDK_READY +``` + +If both events' conditions are satisfied by the same internal event: + +- **Without sort**: If `SDK_READY` is checked first, `prerequisitesSatisfied()` returns `false` because `SDK_READY_FROM_CACHE` hasn't fired yet. `SDK_READY` misses its chance to fire in this cycle. +- **With sort**: `SDK_READY_FROM_CACHE` is evaluated first, fires, then `SDK_READY` sees its prerequisite satisfied and fires—all in one pass. + +#### SuppressedBy Example + +``` +SDK_READY ──suppressedBy──► SDK_READY_TIMED_OUT +``` + +If both events' conditions are satisfied by the same internal event: + +- **Without sort**: If `SDK_READY_TIMED_OUT` is checked first, `isSuppressed()` returns `false` because `SDK_READY` hasn't fired yet. Both events fire incorrectly. +- **With sort**: `SDK_READY` is evaluated first, fires, then `SDK_READY_TIMED_OUT` sees it's suppressed and doesn't fire. + +### Implementation Details + +The sorting logic is split into: + +- **`EventsManagerConfig`**: Holds the raw configuration. +- **`EvaluationOrderComputer`**: Gathers all configured events and builds the dependency graph based on prerequisites and suppressors. +- **`TopologicalSorter`**: A generic utility that performs the DFS-based topological sort with cycle detection. + +The topological sort treats both `prerequisite` and `suppressedBy` as dependency edges: +- If A has `prerequisite` B → B must be evaluated before A +- If A is `suppressedBy` B → B must be evaluated before A + +**Note:** All configured events are included in the evaluation order, even those without dependencies. Independent events can appear anywhere in the list relative to each other, but always before/after their dependents/dependencies as required. diff --git a/events/build.gradle b/events/build.gradle new file mode 100644 index 000000000..b4a4d8ee9 --- /dev/null +++ b/events/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'com.android.library' +} + +apply from: "$rootDir/gradle/common-android-library.gradle" + +android { + namespace 'io.harness.events' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + compileOnly libs.jetbrainsAnnotations + + testImplementation libs.junit4 + testImplementation libs.mockitoCore +} diff --git a/events/consumer-rules.pro b/events/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/events/proguard-rules.pro b/events/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/events/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/events/src/main/AndroidManifest.xml b/events/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/events/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/events/src/main/java/io/harness/events/EvaluationOrderComputer.java b/events/src/main/java/io/harness/events/EvaluationOrderComputer.java new file mode 100644 index 000000000..14079d530 --- /dev/null +++ b/events/src/main/java/io/harness/events/EvaluationOrderComputer.java @@ -0,0 +1,116 @@ +package io.harness.events; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Computes the evaluation order of events based on their prerequisites and suppression relationships. + *

+ * Prerequisites and suppressions imply a dependency between events, so prerequisites and + * suppressors need to be evaluated before their dependents. + * + * @param event type + */ +final class EvaluationOrderComputer { + + private final Set mAllEvents; + private final Map> mPrerequisites; + private final Map> mSuppressedBy; + + /** + * Creates a new EvaluationOrderComputer. + * + * @param allEvents all events that need to be included in the evaluation order + * @param prerequisites map from event to its prerequisites (events that must fire before it) + * @param suppressedBy map from event to its suppressors (events that, if fired, suppress it) + */ + EvaluationOrderComputer(Set allEvents, Map> prerequisites, Map> suppressedBy) { + mAllEvents = allEvents != null ? allEvents : Collections.emptySet(); + mPrerequisites = prerequisites != null ? prerequisites : Collections.emptyMap(); + mSuppressedBy = suppressedBy != null ? suppressedBy : Collections.emptyMap(); + } + + /** + * Computes the topological sort of events based on prerequisites and suppression. + *

+ * Edge direction: If A depends on B (prerequisite or suppression), then B -> A (B must come before A). + * + * @return topologically sorted list of events + * @throws IllegalStateException if a circular dependency is detected + */ + List compute() { + Set allEvents = gatherAllEvents(); + + if (allEvents.isEmpty()) { + return Collections.emptyList(); + } + + Map> dependencies = buildDependencyGraph(allEvents); + + return new TopologicalSorter<>(allEvents, dependencies).sort(); + } + + private Set gatherAllEvents() { + Set allEvents = new HashSet<>(mAllEvents); + + // Also include events that appear as values in prerequisites/suppression + // (they might not be configured themselves but need to be evaluated first) + for (Set prereqs : mPrerequisites.values()) { + allEvents.addAll(prereqs); + } + for (Set suppressors : mSuppressedBy.values()) { + allEvents.addAll(suppressors); + } + + return allEvents; + } + + /** + * Builds the dependency graph from prerequisites and suppression relationships. + *

+ * For each event, tracks which events must come before it. + *

+ * For example, the following configuration: + *

+     * A -> B // B is a prerequisite for A
+     * B -> C // B is suppressed by C
+     * 
+ * Will result in the following dependency graph: + *
+     * {
+     *   A: [B], // A depends on B
+     *   B: [C], // B depends on C
+     *   C: [], // C has no dependencies
+     * }
+     * 
+ */ + private Map> buildDependencyGraph(Set allEvents) { + Map> dependencies = new HashMap<>(); + for (E event : allEvents) { + dependencies.put(event, new HashSet<>()); + } + + // Add edges: if A has prerequisite B, then B -> A (B must come before A) + for (Map.Entry> entry : mPrerequisites.entrySet()) { + E dependent = entry.getKey(); + for (E prerequisite : entry.getValue()) { + dependencies.get(dependent).add(prerequisite); + } + } + + // Add edges: if A is suppressed by B, then B -> A (B must come before A) + for (Map.Entry> entry : mSuppressedBy.entrySet()) { + E suppressed = entry.getKey(); + for (E suppressor : entry.getValue()) { + dependencies.get(suppressed).add(suppressor); + } + } + + return dependencies; + } +} + diff --git a/events/src/main/java/io/harness/events/EventDelivery.java b/events/src/main/java/io/harness/events/EventDelivery.java new file mode 100644 index 000000000..1ad9e6565 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventDelivery.java @@ -0,0 +1,12 @@ +package io.harness.events; + +/** + * Interface for event delivery. + * + * @param event type + * @param metadata type + */ +public interface EventDelivery { + + void deliver(EventHandler eventHandler, E event, M metadata); +} diff --git a/events/src/main/java/io/harness/events/EventHandler.java b/events/src/main/java/io/harness/events/EventHandler.java new file mode 100644 index 000000000..d12c73f24 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventHandler.java @@ -0,0 +1,13 @@ +package io.harness.events; + +/** + * Interface for event handlers. This represents a callback + * that will be executed when an event is triggered. + * + * @param event type + * @param metadata type + */ +public interface EventHandler { + + void handle(E event, M metadata); +} diff --git a/events/src/main/java/io/harness/events/EventsManager.java b/events/src/main/java/io/harness/events/EventsManager.java new file mode 100644 index 000000000..2bd84bfd3 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventsManager.java @@ -0,0 +1,50 @@ +package io.harness.events; + +import org.jetbrains.annotations.Nullable; + +/** + * Interface for events manager. + * + * @param external events type + * @param internal events type + * @param metadata type + */ +public interface EventsManager { + + /** + * Registers a handler to be executed when the event is triggered. + * + * @param event event to register + * @param handler handler to execute when the event is triggered + */ + void register(E event, EventHandler handler); + + /** + * Unregisters all registered handlers for an event. + * + * @param event event to unregister handlers for + */ + void unregister(E event); + + /** + * Notifies an internal event has occurred. + * + * @param event internal event to notify + * @param metadata optional metadata + */ + void notifyInternalEvent(I event, @Nullable M metadata); + + /** + * Checks if the event has already been triggered. + * + * @param event event to check + * @return whether event has been triggered + */ + boolean eventAlreadyTriggered(E event); + + /** + * Destroys the events manager. + * This should be called when the events manager is no longer needed. + */ + void destroy(); +} diff --git a/events/src/main/java/io/harness/events/EventsManagerConfig.java b/events/src/main/java/io/harness/events/EventsManagerConfig.java new file mode 100644 index 000000000..54886f1a0 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventsManagerConfig.java @@ -0,0 +1,318 @@ +package io.harness.events; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Contains the interdependencies between events and internal events. + * + * @param external events type + * @param internal events type + */ +public final class EventsManagerConfig { + // External events that require ALL listed internals (AND) + private final Map> mRequireAll; + // External events triggered by ANY of the listed internal groups (OR of ANDs) + private final Map>> mRequireAny; + // External-event guards: prerequisites that must have fired before External can emit + private final Map> mPrerequisites; + // External-event guards: if any of these have fired, suppress E + private final Map> mSuppressedBy; + // Execution policy: max executions per external event (-1 = unlimited) + private final Map mExecutionLimits; + // Metadata source for requireAll events + private final Map mRequireAllMetadataSource; + // Metadata source for requireAny groups + private final Map, I>> mRequireAnyMetadataSource; + // Topologically sorted evaluation order (prerequisites and suppressors come before dependents) + private final List mEvaluationOrder; + + /** + * Creates a new EventsManagerConfig. + * + * @param requireAll External events that require ALL listed internals (AND) + * @param requireAny External events triggered by ANY of the listed internal groups (OR of ANDs) + * @param prerequisites External-event guards: prerequisites that must have fired before External can emit + * @param suppressedBy External-event guards: if any of these have fired, suppress E + * @param executionLimits Execution policy: max executions per external event (-1 = unlimited) + */ + private EventsManagerConfig(Map> requireAll, + Map>> requireAny, + Map> prerequisites, + Map> suppressedBy, + Map executionLimits, + Map requireAllMetadataSource, + Map, I>> requireAnyMetadataSource) { + mRequireAll = requireAll == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(requireAll)); + mRequireAny = requireAny == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(requireAny)); + mPrerequisites = prerequisites == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(prerequisites)); + mSuppressedBy = suppressedBy == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(suppressedBy)); + mExecutionLimits = executionLimits == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(executionLimits)); + mRequireAllMetadataSource = requireAllMetadataSource == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(requireAllMetadataSource)); + mRequireAnyMetadataSource = requireAnyMetadataSource == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(requireAnyMetadataSource)); + + mEvaluationOrder = computeEvaluationOrder(); + } + + public static EventsManagerConfig empty() { + return new EventsManagerConfig<>(Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap()); + } + + private List computeEvaluationOrder() { + Set allEvents = new HashSet<>(); + allEvents.addAll(mRequireAll.keySet()); + allEvents.addAll(mRequireAny.keySet()); + allEvents.addAll(mPrerequisites.keySet()); + allEvents.addAll(mSuppressedBy.keySet()); + allEvents.addAll(mExecutionLimits.keySet()); + + return new EvaluationOrderComputer<>(allEvents, mPrerequisites, mSuppressedBy).compute(); + } + + @NotNull + public Map> getRequireAll() { + return mRequireAll; + } + + @NotNull + public Map>> getRequireAny() { + return mRequireAny; + } + + @NotNull + public Map> getPrerequisites() { + return mPrerequisites; + } + + @NotNull + public Map> getSuppressedBy() { + return mSuppressedBy; + } + + @NotNull + public Map getExecutionLimits() { + return mExecutionLimits; + } + + @NotNull + public Map getRequireAllMetadataSource() { + return mRequireAllMetadataSource; + } + + @NotNull + public Map, I>> getRequireAnyMetadataSource() { + return mRequireAnyMetadataSource; + } + + @NotNull + public List getEvaluationOrder() { + return mEvaluationOrder; + } + + /** + * Creates a new Builder for EventsManagerConfig. + * + * @param external events type + * @param internal events type + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Builder for EventsManagerConfig. + * + * @param external events type + * @param internal events type + */ + public static final class Builder { + private final Map> mRequireAll = new HashMap<>(); + private final Map>> mRequireAny = new HashMap<>(); + private final Map> mPrerequisites = new HashMap<>(); + private final Map> mSuppressedBy = new HashMap<>(); + private final Map mExecutionLimits = new HashMap<>(); + private final Map mRequireAllMetadataSource = new HashMap<>(); + private final Map, I>> mRequireAnyMetadataSource = new HashMap<>(); + + private Builder() { + } + + /** + * Adds a requirement that ALL specified internal events must occur for the external event to fire. + * + * @param externalEvent the external event + * @param internalEvents the internal events that must ALL occur + * @return this builder + */ + @SafeVarargs + public final Builder requireAll(E externalEvent, I... internalEvents) { + mRequireAll.put(externalEvent, new HashSet<>(Arrays.asList(internalEvents))); + return this; + } + + /** + * Adds a requirement that ANY of the specified internal events will trigger the external event. + * Each internal event is treated as a group of one (singleton). + * + * @param externalEvent the external event + * @param internalEvents the internal events, any of which will trigger the external event + * @return this builder + */ + @SafeVarargs + public final Builder requireAny(E externalEvent, I... internalEvents) { + // Convert each individual event to a singleton Set (group of one) + Set> groups = new HashSet<>(); + for (I internalEvent : internalEvents) { + groups.add(Collections.singleton(internalEvent)); + } + mRequireAny.put(externalEvent, groups); + return this; + } + + /** + * Adds a requirement that ANY of the specified internal event groups will trigger the external event. + * Each group is an AND: all events in the group must occur. + * The external event fires when ANY group is fully satisfied (OR of ANDs). + *

+ * Example: + *

+         * .requireAny(DISH_SERVED,
+         *     Set.of(BOUGHT_INGREDIENTS, COOKED_MEAL),                    // Fresh cooking path
+         *     Set.of(ORDERED_DELIVERY, DELIVERY_ARRIVED))                 // Delivery path
+         * // Fires when: (fresh cooking done) OR (delivery arrived)
+         * 
+ * + * @param externalEvent the external event + * @param internalEventGroups the groups of internal events; all events in a group must occur (AND), + * and any group being satisfied triggers the external event (OR) + * @return this builder + */ + @SafeVarargs + public final Builder requireAny(E externalEvent, Set... internalEventGroups) { + Set> groups = new HashSet<>(Arrays.asList(internalEventGroups)); + mRequireAny.put(externalEvent, groups); + return this; + } + + /** + * Adds a prerequisite: the external event can only fire after the prerequisite event has fired. + * + * @param externalEvent the external event + * @param prerequisiteEvent the event that must fire first + * @return this builder + */ + public Builder prerequisite(E externalEvent, E prerequisiteEvent) { + Set set = mPrerequisites.get(externalEvent); + if (set == null) { + set = new HashSet<>(); + mPrerequisites.put(externalEvent, set); + } + set.add(prerequisiteEvent); + return this; + } + + /** + * Adds a suppressor: the external event will be suppressed if the suppressor event has already fired. + * + * @param externalEvent the external event + * @param suppressorEvent the event that suppresses the external event + * @return this builder + */ + public Builder suppressedBy(E externalEvent, E suppressorEvent) { + Set set = mSuppressedBy.get(externalEvent); + if (set == null) { + set = new HashSet<>(); + mSuppressedBy.put(externalEvent, set); + } + set.add(suppressorEvent); + return this; + } + + /** + * Sets the execution limit for an external event. + * + * @param externalEvent the external event + * @param limit max executions (-1 = unlimited, 1 = once only) + * @return this builder + */ + public Builder executionLimit(E externalEvent, int limit) { + mExecutionLimits.put(externalEvent, limit); + return this; + } + + /** + * Sets the metadata source for a requireAll external event. + * + * @param externalEvent the external event + * @param sourceEvent the internal event whose metadata should be used + * @return this builder + */ + public Builder metadataSource(E externalEvent, I sourceEvent) { + mRequireAllMetadataSource.put(externalEvent, sourceEvent); + return this; + } + + /** + * Sets the metadata source for a requireAny group. + * + * @param externalEvent the external event + * @param group the internal event group + * @param sourceEvent the internal event whose metadata should be used + * @return this builder + */ + public Builder metadataSource(E externalEvent, Set group, I sourceEvent) { + Map, I> groupSources = mRequireAnyMetadataSource.get(externalEvent); + if (groupSources == null) { + groupSources = new HashMap<>(); + mRequireAnyMetadataSource.put(externalEvent, groupSources); + } + groupSources.put(new HashSet<>(group), sourceEvent); + return this; + } + + /** + * Builds the EventsManagerConfig. + * + * @return the built config + */ + public EventsManagerConfig build() { + return new EventsManagerConfig<>( + mRequireAll.isEmpty() ? null : mRequireAll, + mRequireAny.isEmpty() ? null : mRequireAny, + mPrerequisites.isEmpty() ? null : mPrerequisites, + mSuppressedBy.isEmpty() ? null : mSuppressedBy, + mExecutionLimits.isEmpty() ? null : mExecutionLimits, + mRequireAllMetadataSource.isEmpty() ? null : mRequireAllMetadataSource, + mRequireAnyMetadataSource.isEmpty() ? null : mRequireAnyMetadataSource + ); + } + } +} diff --git a/events/src/main/java/io/harness/events/EventsManagerCore.java b/events/src/main/java/io/harness/events/EventsManagerCore.java new file mode 100644 index 000000000..d93baf2cf --- /dev/null +++ b/events/src/main/java/io/harness/events/EventsManagerCore.java @@ -0,0 +1,331 @@ +package io.harness.events; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +/** + * Core implementation of EventsManager. + * + * @param external events type + * @param internal events type + * @param metadata type + */ +class EventsManagerCore implements EventsManager { + + private static final int UNLIMITED = -1; + + private final Map>> mSubscriptions = new HashMap<>(); + private final Map mTriggerCount = new HashMap<>(); + private final Set mSeenInternal = new HashSet<>(); + private final Map mInternalEventMetadata = new HashMap<>(); + + @NotNull + private final EventsManagerConfig mConfig; + @NotNull + private final EventDelivery mDelivery; + + @NotNull + private final ExecutorService mProcessQueue; + + private final Object mLock = new Object(); + private volatile boolean mRunning = true; + + public EventsManagerCore(EventsManagerConfig config, EventDelivery delivery) { + mConfig = config == null ? EventsManagerConfig.empty() : config; + mDelivery = delivery == null ? (h, e, m) -> {} : delivery; + mProcessQueue = Executors.newSingleThreadExecutor(); + } + + @Override + public void register(E event, EventHandler handler) { + boolean shouldReplay; + synchronized (mLock) { + if (!mRunning) { + return; + } + + int max = maxExecutions(event); + Integer triggered = mTriggerCount.get(event); + + // Replay if limit was reached (event finished all its executions) + shouldReplay = max != UNLIMITED && triggered != null && triggered >= max; + + if (!shouldReplay) { + Set> handlers = mSubscriptions.get(event); + if (handlers == null) { + handlers = new HashSet<>(); + mSubscriptions.put(event, handlers); + } + handlers.add(handler); + } + } + + // Replay if the limit has been reached. Don't add to subscriptions since + // it will not be triggered again (max executions reached). + if (shouldReplay) { + mDelivery.deliver(handler, event, null); + } + } + + @Override + public void unregister(E event) { + synchronized (mLock) { + Set> handlers = mSubscriptions.get(event); + if (handlers != null) { + handlers.clear(); + } + } + } + + @Override + public void notifyInternalEvent(I event, M metadata) { + if (!mRunning) { + return; + } + try { + mProcessQueue.execute(() -> processInternal(event, metadata)); + } catch (RejectedExecutionException e) { + // ignore + } + } + + @Override + public boolean eventAlreadyTriggered(E event) { + // Wait for pending processing to complete for a consistent view + CountDownLatch latch = new CountDownLatch(1); + try { + mProcessQueue.execute(latch::countDown); + latch.await(); + } catch (RejectedExecutionException e) { + // Executor is shut down + } catch (InterruptedException e) { + // Restore interrupt status; check current state + Thread.currentThread().interrupt(); + } + + synchronized (mLock) { + Integer count = mTriggerCount.get(event); + if (count == null) { + return false; + } + + // For unlimited events, return false since they can always fire again + int max = maxExecutions(event); + if (max == UNLIMITED) { + return false; + } + + // For limited events, return true only if all executions are done + return count >= max; + } + } + + @Override + public void destroy() { + synchronized (mLock) { + mRunning = false; + mSubscriptions.clear(); + mTriggerCount.clear(); + mSeenInternal.clear(); + } + mProcessQueue.shutdown(); + } + + private void processInternal(I event, M metadata) { + Set currentSeenInternal; + synchronized (mLock) { + if (!mRunning) { + return; + } + mSeenInternal.add(event); + if (metadata != null) { + mInternalEventMetadata.put(event, metadata); + } + currentSeenInternal = new HashSet<>(mSeenInternal); + } + + // The sorted order guarantees that prerequisites and suppressors are evaluated + // before their dependents. + for (E externalEvent : mConfig.getEvaluationOrder()) { + // Check if internal trigger conditions are met (RequireAll or RequireAny) + InternalTriggerMatch match = checkInternalTriggerConditions(externalEvent, currentSeenInternal, event); + + if (!match.mMatched) { + continue; + } + + // Check external guards (prerequisites and suppression) and fire if all conditions met + M resolvedMetadata = resolveMetadata(externalEvent, match, metadata); + triggerIfConditionsMet(externalEvent, resolvedMetadata); + } + } + + /** + * Triggers an external event if all conditions are met. + * @return true if the event was triggered, false otherwise + */ + private boolean triggerIfConditionsMet(E event, M metadata) { + if (!canEventBeTriggered(event)) { + return false; + } + return trigger(event, metadata); + } + + private boolean canEventBeTriggered(E event) { + return prerequisitesSatisfied(event) && !isSuppressed(event); + } + + /** + * Triggers an external event. + * @return true if the event was triggered, false if it was already at max executions + */ + private boolean trigger(E event, M metadata) { + Set> handlersSnapshot = Collections.emptySet(); + + synchronized (mLock) { + int max = maxExecutions(event); + Integer count = mTriggerCount.get(event); + int triggered = count != null ? count : 0; + + if (max != UNLIMITED && triggered >= max) { + return false; + } + + mTriggerCount.put(event, triggered + 1); + + Set> handlers = mSubscriptions.get(event); + if (handlers != null) { + handlersSnapshot = new HashSet<>(handlers); + } + } + + for (EventHandler handler : handlersSnapshot) { + mDelivery.deliver(handler, event, metadata); + } + return true; + } + + private int maxExecutions(E event) { + Integer limit = mConfig.getExecutionLimits().get(event); + return limit != null ? limit : UNLIMITED; + } + + private boolean prerequisitesSatisfied(E external) { + Set prerequisites = mConfig.getPrerequisites().get(external); + if (prerequisites == null || prerequisites.isEmpty()) { + return true; + } + + synchronized (mLock) { + return mTriggerCount.keySet().containsAll(prerequisites); + } + } + + private boolean isSuppressed(E external) { + Set suppressors = mConfig.getSuppressedBy().get(external); + if (suppressors == null || suppressors.isEmpty()) { + return false; + } + + synchronized (mLock) { + for (E suppressor : suppressors) { + if (mTriggerCount.containsKey(suppressor)) { + return true; + } + } + } + return false; + } + + /** + * Checks if the internal trigger conditions are met for an external event. + * Returns true if either RequireAll or RequireAny conditions are satisfied. + * + * @param externalEvent the external event to check + * @param seenInternal all internal events seen so far + * @param currentEvent the internal event that just arrived + */ + private InternalTriggerMatch checkInternalTriggerConditions(E externalEvent, Set seenInternal, I currentEvent) { + Set requireAll = mConfig.getRequireAll().get(externalEvent); + if (requireAll != null && !requireAll.isEmpty() && seenInternal.containsAll(requireAll)) { + return InternalTriggerMatch.requireAll(); + } + + // Check RequireAny: The CURRENT internal event must be in one of the groups, + // and all events in that group must have been seen. + Set> requireAnyGroups = mConfig.getRequireAny().get(externalEvent); + if (requireAnyGroups != null && !requireAnyGroups.isEmpty()) { + for (Set group : requireAnyGroups) { + // Only consider groups that contain the current event + if (!group.isEmpty() && group.contains(currentEvent) && seenInternal.containsAll(group)) { + return InternalTriggerMatch.requireAny(group); + } + } + } + + return InternalTriggerMatch.none(); + } + + private M resolveMetadata(E externalEvent, InternalTriggerMatch match, M currentMetadata) { + if (match.mRequireAllMatched) { + I sourceEvent = mConfig.getRequireAllMetadataSource().get(externalEvent); + return resolveMetadataFromSource(sourceEvent, currentMetadata); + } + + if (match.mRequireAnyGroup != null) { + Map, I> groupSources = mConfig.getRequireAnyMetadataSource().get(externalEvent); + if (groupSources != null) { + I sourceEvent = groupSources.get(match.mRequireAnyGroup); + return resolveMetadataFromSource(sourceEvent, currentMetadata); + } + } + + return resolveMetadataFromSource(null, currentMetadata); + } + + private M resolveMetadataFromSource(I sourceEvent, M currentMetadata) { + if (sourceEvent != null) { + synchronized (mLock) { + M stored = mInternalEventMetadata.get(sourceEvent); + if (stored != null) { + return stored; + } + } + } + return currentMetadata; + } + + private static class InternalTriggerMatch { + private final boolean mMatched; + private final boolean mRequireAllMatched; + private final Set mRequireAnyGroup; + + private InternalTriggerMatch(boolean matched, boolean requireAllMatched, Set requireAnyGroup) { + mMatched = matched; + mRequireAllMatched = requireAllMatched; + mRequireAnyGroup = requireAnyGroup; + } + + private static InternalTriggerMatch requireAll() { + return new InternalTriggerMatch<>(true, true, null); + } + + private static InternalTriggerMatch requireAny(Set group) { + return new InternalTriggerMatch<>(true, false, group); + } + + private static InternalTriggerMatch none() { + return new InternalTriggerMatch<>(false, false, null); + } + } + +} diff --git a/events/src/main/java/io/harness/events/EventsManagers.java b/events/src/main/java/io/harness/events/EventsManagers.java new file mode 100644 index 000000000..9cdcf4e95 --- /dev/null +++ b/events/src/main/java/io/harness/events/EventsManagers.java @@ -0,0 +1,28 @@ +package io.harness.events; + +/** + * Factory class for creating {@link EventsManager} instances. + * This class decouples the creation of the {@link EventsManager} instance from the implementation. + */ +public final class EventsManagers { + + private EventsManagers() { + // Utility class + } + + /** + * Creates a new EventsManager with the given configuration and delivery mechanism. + * + * @param config the configuration defining event relationships + * @param delivery the delivery mechanism for dispatching events to handlers + * @param external events type + * @param internal events type + * @param metadata type + * @return a new EventsManager instance + */ + public static EventsManager create( + EventsManagerConfig config, + EventDelivery delivery) { + return new EventsManagerCore<>(config, delivery); + } +} diff --git a/events/src/main/java/io/harness/events/Logging.java b/events/src/main/java/io/harness/events/Logging.java new file mode 100644 index 000000000..725ad051b --- /dev/null +++ b/events/src/main/java/io/harness/events/Logging.java @@ -0,0 +1,18 @@ +package io.harness.events; + +/** + * Interface for optional logging in the events module. + * Consumers can implement this interface to log messages. + */ +public interface Logging { + + void logError(String message); + + void logWarning(String message); + + void logInfo(String message); + + void logDebug(String message); + + void logVerbose(String message); +} diff --git a/events/src/main/java/io/harness/events/NoOpLogging.java b/events/src/main/java/io/harness/events/NoOpLogging.java new file mode 100644 index 000000000..079337aa6 --- /dev/null +++ b/events/src/main/java/io/harness/events/NoOpLogging.java @@ -0,0 +1,37 @@ +package io.harness.events; + +/** + * No-op implementation of {@link Logging} for use when logging is not provided. + */ +final class NoOpLogging implements Logging { + + static final Logging INSTANCE = new NoOpLogging(); + + private NoOpLogging() { + } + + @Override + public void logError(String message) { + // no-op + } + + @Override + public void logWarning(String message) { + // no-op + } + + @Override + public void logInfo(String message) { + // no-op + } + + @Override + public void logDebug(String message) { + // no-op + } + + @Override + public void logVerbose(String message) { + // no-op + } +} diff --git a/events/src/main/java/io/harness/events/TopologicalSorter.java b/events/src/main/java/io/harness/events/TopologicalSorter.java new file mode 100644 index 000000000..8ff1c7dab --- /dev/null +++ b/events/src/main/java/io/harness/events/TopologicalSorter.java @@ -0,0 +1,107 @@ +package io.harness.events; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Performs topological sorting of nodes based on their dependencies. + * + * @param the type of nodes to sort + */ +final class TopologicalSorter { + + private final Set mNodes; + private final Map> mDependencies; + + /** + * Creates a new TopologicalSorter. + * + * @param nodes all nodes to be sorted + * @param dependencies map from each node to the set of nodes it depends on + * (i.e., nodes that must come before it) + */ + TopologicalSorter(Set nodes, Map> dependencies) { + mNodes = nodes == null ? Collections.emptySet() : nodes; + mDependencies = dependencies == null ? Collections.emptyMap() : dependencies; + } + + /** + * Computes the topological sort of the nodes. + *

+ * The result is ordered such that for any node A that depends on node B, + * B will appear before A in the returned list. + *

+ * For example, the following dependency graph: + *

+ * ``` + * A -> B // B is a prerequisite for A + * B -> C // C is suppressed by B + * ``` + *

+ * Will result in the following sorted list: + *

+ * ``` + * [C, B, A] + * ``` + * + * @return topologically sorted list of nodes + * @throws IllegalStateException if a circular dependency is detected + */ + List sort() { + if (mNodes.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + Set visited = new HashSet<>(); + Set visiting = new HashSet<>(); // For cycle detection + + for (T node : mNodes) { + if (!visited.contains(node)) { + visit(node, visited, visiting, result); + } + } + + return Collections.unmodifiableList(result); + } + + /** + * Visit all dependencies first (nodes that must come before this one), + * then add the current node to the result list. + *

+ * If a cycle is detected, an exception is thrown. + * + * @param node the current node to visit + * @param visited set of permanently visited nodes + * @param visiting set of nodes currently being visited (for cycle detection) + * @param result the sorted result list + * @throws IllegalStateException if a cycle is detected + */ + private void visit(T node, Set visited, Set visiting, List result) { + if (visited.contains(node)) { + return; // Already processed + } + + if (visiting.contains(node)) { + throw new IllegalStateException("Circular dependency detected involving node: " + node); + } + + visiting.add(node); + + // Visit all dependencies first (nodes that must come before this one) + Set deps = mDependencies.get(node); + if (deps != null) { + for (T dep : deps) { + visit(dep, visited, visiting, result); + } + } + + visiting.remove(node); + visited.add(node); + result.add(node); + } +} diff --git a/events/src/test/java/io/harness/events/EvaluationOrderComputerTest.java b/events/src/test/java/io/harness/events/EvaluationOrderComputerTest.java new file mode 100644 index 000000000..a13244d20 --- /dev/null +++ b/events/src/test/java/io/harness/events/EvaluationOrderComputerTest.java @@ -0,0 +1,317 @@ +package io.harness.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class EvaluationOrderComputerTest { + + @Test + public void emptyInputsReturnEmptyList() { + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + Collections.emptySet(), + Collections.emptyMap(), + Collections.emptyMap() + ); + + List result = computer.compute(); + assertTrue(result.isEmpty()); + } + + @Test + public void nullInputsReturnEmptyList() { + EvaluationOrderComputer computer = new EvaluationOrderComputer<>(null, null, null); + + List result = computer.compute(); + assertTrue(result.isEmpty()); + } + + @Test + public void includesAllEventsFromInput() { + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + allEvents.add("C"); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + Collections.emptyMap(), + Collections.emptyMap() + ); + List result = computer.compute(); + + assertEquals(3, result.size()); + assertTrue(result.contains("A")); + assertTrue(result.contains("B")); + assertTrue(result.contains("C")); + } + + @Test + public void includesEventsFromPrerequisiteValues() { + Set allEvents = Collections.singleton("A"); + + Map> prerequisites = new HashMap<>(); + prerequisites.put("A", Collections.singleton("B")); // B is only in values + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + prerequisites, + Collections.emptyMap() + ); + List result = computer.compute(); + + assertEquals(2, result.size()); + assertTrue(result.contains("A")); + assertTrue(result.contains("B")); + } + + @Test + public void includesEventsFromSuppressorValues() { + Set allEvents = Collections.singleton("A"); + + Map> suppressedBy = new HashMap<>(); + suppressedBy.put("A", Collections.singleton("B")); // B is only in values + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + Collections.emptyMap(), + suppressedBy + ); + List result = computer.compute(); + + assertEquals(2, result.size()); + assertTrue(result.contains("A")); + assertTrue(result.contains("B")); + } + + @Test + public void buildsDependencyGraphFromPrerequisites() { + // A depends on B, B depends on C + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + allEvents.add("C"); + + Map> prerequisites = new HashMap<>(); + prerequisites.put("A", Collections.singleton("B")); + prerequisites.put("B", Collections.singleton("C")); + prerequisites.put("C", Collections.emptySet()); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + prerequisites, + Collections.emptyMap() + ); + List result = computer.compute(); + + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + int idxC = result.indexOf("C"); + + assertTrue("C should come before B", idxC < idxB); + assertTrue("B should come before A", idxB < idxA); + } + + @Test + public void buildsDependencyGraphFromSuppression() { + // A suppressed by B (B must come before A) + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + + Map> suppressedBy = new HashMap<>(); + suppressedBy.put("A", Collections.singleton("B")); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + Collections.emptyMap(), + suppressedBy + ); + List result = computer.compute(); + + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + + assertTrue("B (suppressor) should come before A (suppressed)", idxB < idxA); + } + + @Test + public void combinesPrerequisitesAndSuppression() { + // A depends on B (prerequisite), C suppressed by B + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + allEvents.add("C"); + + Map> prerequisites = new HashMap<>(); + prerequisites.put("A", Collections.singleton("B")); + + Map> suppressedBy = new HashMap<>(); + suppressedBy.put("C", Collections.singleton("B")); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + prerequisites, + suppressedBy + ); + List result = computer.compute(); + + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + int idxC = result.indexOf("C"); + + assertTrue("B should come before A", idxB < idxA); + assertTrue("B should come before C", idxB < idxC); + } + + @Test + public void handlesMultiplePrerequisites() { + // A depends on both B and C + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + allEvents.add("C"); + + Map> prerequisites = new HashMap<>(); + Set aDeps = new HashSet<>(); + aDeps.add("B"); + aDeps.add("C"); + prerequisites.put("A", aDeps); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + prerequisites, + Collections.emptyMap() + ); + List result = computer.compute(); + + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + int idxC = result.indexOf("C"); + + assertTrue("B should come before A", idxB < idxA); + assertTrue("C should come before A", idxC < idxA); + } + + @Test + public void handlesMultipleSuppressors() { + // A suppressed by both B and C + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + allEvents.add("C"); + + Map> suppressedBy = new HashMap<>(); + Set aSuppressors = new HashSet<>(); + aSuppressors.add("B"); + aSuppressors.add("C"); + suppressedBy.put("A", aSuppressors); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + Collections.emptyMap(), + suppressedBy + ); + List result = computer.compute(); + + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + int idxC = result.indexOf("C"); + + assertTrue("B should come before A", idxB < idxA); + assertTrue("C should come before A", idxC < idxA); + } + + @Test(expected = IllegalStateException.class) + public void detectsCircularDependencyThroughPrerequisites() { + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + + Map> prerequisites = new HashMap<>(); + prerequisites.put("A", Collections.singleton("B")); + prerequisites.put("B", Collections.singleton("A")); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + prerequisites, + Collections.emptyMap() + ); + computer.compute(); // Should throw + } + + @Test(expected = IllegalStateException.class) + public void detectsCircularDependencyThroughSuppression() { + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + + Map> suppressedBy = new HashMap<>(); + suppressedBy.put("A", Collections.singleton("B")); + suppressedBy.put("B", Collections.singleton("A")); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + Collections.emptyMap(), + suppressedBy + ); + computer.compute(); // Should throw + } + + @Test(expected = IllegalStateException.class) + public void detectsCircularDependencyThroughMixedRelationships() { + // A depends on B (prerequisite), B suppressed by A + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + + Map> prerequisites = new HashMap<>(); + prerequisites.put("A", Collections.singleton("B")); + + Map> suppressedBy = new HashMap<>(); + suppressedBy.put("B", Collections.singleton("A")); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + prerequisites, + suppressedBy + ); + computer.compute(); // Should throw + } + + @Test + public void eventsWithNoDependenciesAreIncluded() { + // Events without prerequisites or suppression should still be in the result + Set allEvents = new HashSet<>(); + allEvents.add("A"); + allEvents.add("B"); + allEvents.add("C"); + + // Only A has a dependency, B and C are independent + Map> prerequisites = new HashMap<>(); + prerequisites.put("A", Collections.singleton("B")); + + EvaluationOrderComputer computer = new EvaluationOrderComputer<>( + allEvents, + prerequisites, + Collections.emptyMap() + ); + List result = computer.compute(); + + assertEquals(3, result.size()); + assertTrue(result.contains("A")); + assertTrue(result.contains("B")); + assertTrue(result.contains("C")); + + // B should come before A (dependency), C can be anywhere + assertTrue("B should come before A", result.indexOf("B") < result.indexOf("A")); + } +} diff --git a/events/src/test/java/io/harness/events/EventsManagerConfigTest.java b/events/src/test/java/io/harness/events/EventsManagerConfigTest.java new file mode 100644 index 000000000..2662e2aeb --- /dev/null +++ b/events/src/test/java/io/harness/events/EventsManagerConfigTest.java @@ -0,0 +1,313 @@ +package io.harness.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class EventsManagerConfigTest { + + @Test + public void emptyBuilderCreatesEmptyMaps() { + EventsManagerConfig config = EventsManagerConfig.builder().build(); + + assertTrue(config.getRequireAll().isEmpty()); + assertTrue(config.getRequireAny().isEmpty()); + assertTrue(config.getPrerequisites().isEmpty()); + assertTrue(config.getSuppressedBy().isEmpty()); + assertTrue(config.getExecutionLimits().isEmpty()); + assertTrue(config.getRequireAllMetadataSource().isEmpty()); + assertTrue(config.getRequireAnyMetadataSource().isEmpty()); + } + + @Test + public void builderCreatesConfigWithAllFields() { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll("E1", "I1", "I2") + .requireAny("E2", "I3") + .prerequisite("E1", "E0") + .suppressedBy("E1", "E2") + .executionLimit("E1", 3) + .metadataSource("E1", "I2") + .metadataSource("E2", Collections.singleton("I3"), "I3") + .build(); + + assertEquals(1, config.getRequireAll().size()); + assertTrue(config.getRequireAll().get("E1").contains("I1")); + assertTrue(config.getRequireAll().get("E1").contains("I2")); + + // requireAny now stores Set> - single events are wrapped in singleton sets + assertEquals(1, config.getRequireAny().size()); + Set> requireAnyGroups = config.getRequireAny().get("E2"); + assertEquals(1, requireAnyGroups.size()); + assertTrue(requireAnyGroups.contains(Collections.singleton("I3"))); + + assertEquals(1, config.getPrerequisites().size()); + assertTrue(config.getPrerequisites().get("E1").contains("E0")); + + assertEquals(1, config.getSuppressedBy().size()); + assertTrue(config.getSuppressedBy().get("E1").contains("E2")); + + assertEquals(1, config.getExecutionLimits().size()); + assertEquals(Integer.valueOf(3), config.getExecutionLimits().get("E1")); + + assertEquals("I2", config.getRequireAllMetadataSource().get("E1")); + assertEquals("I3", config.getRequireAnyMetadataSource().get("E2") + .get(Collections.singleton("I3"))); + } + + @Test + public void builderAllowsMultiplePrerequisites() { + EventsManagerConfig config = EventsManagerConfig.builder() + .prerequisite("E1", "E0") + .prerequisite("E1", "E2") + .build(); + + assertEquals(1, config.getPrerequisites().size()); + assertEquals(2, config.getPrerequisites().get("E1").size()); + assertTrue(config.getPrerequisites().get("E1").contains("E0")); + assertTrue(config.getPrerequisites().get("E1").contains("E2")); + } + + @Test + public void builderAllowsMultipleSuppressors() { + EventsManagerConfig config = EventsManagerConfig.builder() + .suppressedBy("E1", "E2") + .suppressedBy("E1", "E3") + .build(); + + assertEquals(1, config.getSuppressedBy().size()); + assertEquals(2, config.getSuppressedBy().get("E1").size()); + assertTrue(config.getSuppressedBy().get("E1").contains("E2")); + assertTrue(config.getSuppressedBy().get("E1").contains("E3")); + } + + @Test + public void returnedMapsAreUnmodifiable() { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll("E1", "I1") + .requireAny("E1", "I1") + .prerequisite("E1", "E0") + .suppressedBy("E1", "E2") + .executionLimit("E1", 3) + .metadataSource("E1", "I1") + .build(); + + try { + config.getRequireAll().put("E2", Collections.singleton("I2")); + Assert.fail("getRequireAll() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getRequireAny().put("E2", Collections.singleton(Collections.singleton("I2"))); + Assert.fail("getRequireAny() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getPrerequisites().put("E2", Collections.singleton("E3")); + Assert.fail("getPrerequisites() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getSuppressedBy().put("E2", Collections.singleton("E3")); + Assert.fail("getSuppressedBy() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getExecutionLimits().put("E2", 5); + Assert.fail("getExecutionLimits() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getRequireAllMetadataSource().put("E2", "I2"); + Assert.fail("getRequireAllMetadataSource() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + + try { + config.getRequireAnyMetadataSource().put("E2", Collections.singletonMap(Collections.singleton("I2"), "I2")); + Assert.fail("getRequireAnyMetadataSource() should return an unmodifiable map"); + } catch (UnsupportedOperationException expected) { + // expected + } + } + + @Test + public void emptyMethodReturnsEmptyUnmodifiableConfig() { + EventsManagerConfig config = EventsManagerConfig.empty(); + + assertTrue(config.getRequireAll().isEmpty()); + assertTrue(config.getRequireAny().isEmpty()); + assertTrue(config.getPrerequisites().isEmpty()); + assertTrue(config.getSuppressedBy().isEmpty()); + assertTrue(config.getExecutionLimits().isEmpty()); + assertTrue(config.getRequireAllMetadataSource().isEmpty()); + assertTrue(config.getRequireAnyMetadataSource().isEmpty()); + + try { + config.getRequireAll().put("E1", Collections.singleton("I1")); + Assert.fail("getRequireAll() from empty() should be unmodifiable"); + } catch (UnsupportedOperationException expected) { + // expected + } + } + + @Test + public void requireAnyWithVarargsCreatesIndividualGroups() { + // When using requireAny(E, I...), each I should become its own singleton group + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny("E1", "I1", "I2", "I3") + .build(); + + Set> groups = config.getRequireAny().get("E1"); + assertEquals(3, groups.size()); + assertTrue(groups.contains(Collections.singleton("I1"))); + assertTrue(groups.contains(Collections.singleton("I2"))); + assertTrue(groups.contains(Collections.singleton("I3"))); + } + + @Test + public void requireAnyWithSetsCreatesAndGroups() { + // When using requireAny(E, Set...), each Set is an AND group + Set group1 = new HashSet<>(); + group1.add("I1"); + group1.add("I2"); + + Set group2 = new HashSet<>(); + group2.add("I3"); + group2.add("I4"); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny("E1", group1, group2) + .build(); + + Set> groups = config.getRequireAny().get("E1"); + assertEquals(2, groups.size()); + assertTrue(groups.contains(group1)); + assertTrue(groups.contains(group2)); + } + + @Test + public void requireAnyWithMixedGroupSizes() { + // Groups can have different sizes + Set singletonGroup = Collections.singleton("I1"); + + Set largeGroup = new HashSet<>(); + largeGroup.add("I2"); + largeGroup.add("I3"); + largeGroup.add("I4"); + largeGroup.add("I5"); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny("E1", singletonGroup, largeGroup) + .build(); + + Set> groups = config.getRequireAny().get("E1"); + assertEquals(2, groups.size()); + assertTrue(groups.contains(singletonGroup)); + assertTrue(groups.contains(largeGroup)); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowOnCircularPrerequisites() { + EventsManagerConfig.builder() + .requireAll("A", "I1") + .requireAll("B", "I2") + .prerequisite("A", "B") + .prerequisite("B", "A") + .build(); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowOnCircularSuppression() { + EventsManagerConfig.builder() + .requireAll("A", "I1") + .requireAll("B", "I2") + .suppressedBy("A", "B") + .suppressedBy("B", "A") + .build(); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowOnMixedCircularDependency() { + // A requires B, B suppressed by A (B -> A from prereq, A -> B from suppression) + EventsManagerConfig.builder() + .requireAll("A", "I1") + .requireAll("B", "I2") + .prerequisite("A", "B") + .suppressedBy("B", "A") + .build(); + } + + @Test + public void shouldSortByPrerequisites() { + // A depends on B, B depends on C + // Expected order: C, B, A + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll("A", "I1") + .requireAll("B", "I2") + .requireAll("C", "I3") + .prerequisite("A", "B") + .prerequisite("B", "C") + .build(); + + List order = config.getEvaluationOrder(); + int idxA = order.indexOf("A"); + int idxB = order.indexOf("B"); + int idxC = order.indexOf("C"); + + assertTrue("C should come before B", idxC < idxB); + assertTrue("B should come before A", idxB < idxA); + } + + @Test + public void shouldSortBySuppression() { + // A suppressed by B (B must run first to suppress A) + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll("A", "I1") + .requireAll("B", "I2") + .suppressedBy("A", "B") + .build(); + + List order = config.getEvaluationOrder(); + int idxA = order.indexOf("A"); + int idxB = order.indexOf("B"); + + assertTrue("B (suppressor) should come before A (suppressed)", idxB < idxA); + } + + @Test + public void shouldIncludeEventsFromAllSourcesInSort() { + // Events might only appear in prerequisites or suppression lists + // even if they don't have trigger conditions themselves + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll("A", "I1") + // B is not explicitly configured with requirements, but is a prerequisite + .prerequisite("A", "B") + .build(); + + List order = config.getEvaluationOrder(); + assertTrue(order.contains("A")); + assertTrue(order.contains("B")); + assertTrue(order.indexOf("B") < order.indexOf("A")); + } +} diff --git a/events/src/test/java/io/harness/events/EventsManagerMetadataTest.java b/events/src/test/java/io/harness/events/EventsManagerMetadataTest.java new file mode 100644 index 000000000..c543d9b21 --- /dev/null +++ b/events/src/test/java/io/harness/events/EventsManagerMetadataTest.java @@ -0,0 +1,90 @@ +package io.harness.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class EventsManagerMetadataTest { + + private static final long TIMEOUT_MS = 5000; + + enum ExternalEvent { + READY_FROM_CACHE + } + + enum InternalEvent { + CACHE_A, CACHE_B, SYNC_A, SYNC_B + } + + @Test + public void requireAnyUsesGroupMetadataSource() throws InterruptedException { + Set cacheGroup = new HashSet<>(); + cacheGroup.add(InternalEvent.CACHE_A); + cacheGroup.add(InternalEvent.CACHE_B); + + Set syncGroup = new HashSet<>(); + syncGroup.add(InternalEvent.SYNC_A); + syncGroup.add(InternalEvent.SYNC_B); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(ExternalEvent.READY_FROM_CACHE, cacheGroup, syncGroup) + .metadataSource(ExternalEvent.READY_FROM_CACHE, cacheGroup, InternalEvent.CACHE_A) + .metadataSource(ExternalEvent.READY_FROM_CACHE, syncGroup, InternalEvent.SYNC_A) + .executionLimit(ExternalEvent.READY_FROM_CACHE, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference received = new AtomicReference<>(); + + EventsManager manager = + new EventsManagerCore<>(config, (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }); + + manager.register(ExternalEvent.READY_FROM_CACHE, (event, metadata) -> received.set(metadata)); + + // Complete sync group: metadata should come from SYNC_A, not from SYNC_B (current event). + manager.notifyInternalEvent(InternalEvent.SYNC_A, "sync-meta"); + manager.notifyInternalEvent(InternalEvent.SYNC_B, "sync-b-meta"); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("sync-meta", received.get()); + } + + @Test + public void requireAllUsesConfiguredMetadataSource() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(ExternalEvent.READY_FROM_CACHE, InternalEvent.CACHE_A, InternalEvent.CACHE_B) + .metadataSource(ExternalEvent.READY_FROM_CACHE, InternalEvent.CACHE_A) + .executionLimit(ExternalEvent.READY_FROM_CACHE, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference received = new AtomicReference<>(); + + EventsManager manager = + new EventsManagerCore<>(config, (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }); + + manager.register(ExternalEvent.READY_FROM_CACHE, (event, metadata) -> received.set(metadata)); + + // Provide metadata on CACHE_A only; CACHE_B completes the requireAll. + manager.notifyInternalEvent(InternalEvent.CACHE_A, "cache-meta"); + manager.notifyInternalEvent(InternalEvent.CACHE_B, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(received.get()); + assertEquals("cache-meta", received.get()); + } +} diff --git a/events/src/test/java/io/harness/events/EventsManagerTest.java b/events/src/test/java/io/harness/events/EventsManagerTest.java new file mode 100644 index 000000000..536763989 --- /dev/null +++ b/events/src/test/java/io/harness/events/EventsManagerTest.java @@ -0,0 +1,994 @@ +package io.harness.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class EventsManagerTest { + + private static final long TIMEOUT_MS = 5000; + private static final EventDelivery SIMPLE_DELIVERY = (handler, event, metadata) -> handler.handle(event, metadata); + + /** + * External events emitted to consumers. + *

+ * Dependencies: + * - DISH_SERVED: requires ALL of (INGREDIENTS_PREPPED, SEASONING_ADDED, OVEN_PREHEATED). Fires once. + *

+ * - LEFTOVERS_HEATED: requires ALL of (LEFTOVER_MEAT_FOUND, LEFTOVER_VEGGIES_FOUND, LEFTOVER_SAUCE_FOUND, PLATES_RETRIEVED). Fires once. + *

+ * - SEASONING_ADJUSTED: requires ANY of (SEASONING_ADDED). Prerequisite: DISH_SERVED. Fires unlimited times. + *

+ * - ORDER_TIMED_OUT: requires ANY of (TIMEOUT_REACHED). Suppressed by: DISH_SERVED. Fires once. + */ + enum CookingEvent { + DISH_SERVED, LEFTOVERS_HEATED, SEASONING_ADJUSTED, ORDER_TIMED_OUT, + } + + /** + * Internal activities that trigger external events. + */ + enum KitchenActivity { + INGREDIENTS_PREPPED, SEASONING_ADDED, OVEN_PREHEATED, LEFTOVER_MEAT_FOUND, + LEFTOVER_VEGGIES_FOUND, LEFTOVER_SAUCE_FOUND, PLATES_RETRIEVED, TIMEOUT_REACHED, + } + + @Test + public void dishServedFiresOnceAndReplaysToLateSubscribers() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED, KitchenActivity.SEASONING_ADDED, KitchenActivity.OVEN_PREHEATED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger h1CallCount = new AtomicInteger(0); + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> h1CallCount.incrementAndGet()); + + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, h1CallCount.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + // Late subscriber should receive replay + AtomicInteger h2CallCount = new AtomicInteger(0); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> h2CallCount.incrementAndGet()); + + assertEquals(1, h2CallCount.get()); + assertEquals(1, h1CallCount.get()); // Original handler not called again + } + + @Test + public void leftoversHeatedFiresOnceWhenAllLeftoversFound() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.LEFTOVERS_HEATED, KitchenActivity.LEFTOVER_MEAT_FOUND, KitchenActivity.LEFTOVER_VEGGIES_FOUND, KitchenActivity.LEFTOVER_SAUCE_FOUND, KitchenActivity.PLATES_RETRIEVED) + .executionLimit(CookingEvent.LEFTOVERS_HEATED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger hCount = new AtomicInteger(0); + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.LEFTOVERS_HEATED, (event, metadata) -> hCount.incrementAndGet()); + + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_MEAT_FOUND, null); + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_VEGGIES_FOUND, null); + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_SAUCE_FOUND, null); + eventsManager.notifyInternalEvent(KitchenActivity.PLATES_RETRIEVED, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, hCount.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.LEFTOVERS_HEATED)); + } + + @Test + public void seasoningAdjustedIsEmittedOnlyAfterDishServed() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED, KitchenActivity.OVEN_PREHEATED, KitchenActivity.LEFTOVER_SAUCE_FOUND) + .requireAny(CookingEvent.SEASONING_ADJUSTED, KitchenActivity.SEASONING_ADDED) + .prerequisite(CookingEvent.SEASONING_ADJUSTED, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, -1) + .build(); + + CountDownLatch seasoningLatch = new CountDownLatch(1); + AtomicInteger hCount = new AtomicInteger(0); + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + if (event == CookingEvent.SEASONING_ADJUSTED) { + seasoningLatch.countDown(); + } + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> hCount.incrementAndGet()); + + // Trigger DISH_SERVED first (without SEASONING_ADDED) + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_SAUCE_FOUND, null); + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + // Wait for DISH_SERVED to be processed + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + // SEASONING_ADJUSTED should NOT have fired yet (no SEASONING_ADDED) + assertEquals(0, hCount.get()); + + // Now SEASONING_ADDED should trigger SEASONING_ADJUSTED (prerequisite is met) + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + assertTrue(seasoningLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, hCount.get()); + } + + @Test + public void seasoningAdjustedDoesNotReplayToLateSubscribers() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.DISH_SERVED, KitchenActivity.LEFTOVER_SAUCE_FOUND) + .requireAny(CookingEvent.SEASONING_ADJUSTED, KitchenActivity.SEASONING_ADDED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, -1) + .build(); + + CountDownLatch firstSeasoningLatch = new CountDownLatch(1); + CountDownLatch secondSeasoningLatch = new CountDownLatch(2); + AtomicInteger h1Count = new AtomicInteger(0); + AtomicInteger h2Count = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + if (event == CookingEvent.SEASONING_ADJUSTED) { + firstSeasoningLatch.countDown(); + secondSeasoningLatch.countDown(); + } + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + // Emit DISH_SERVED + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_SAUCE_FOUND, null); + + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> h1Count.incrementAndGet()); + + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + assertTrue(firstSeasoningLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, h1Count.get()); + + // Late subscriber should NOT receive replay for unlimited events + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> h2Count.incrementAndGet()); + assertEquals(0, h2Count.get()); + + // Both handlers invoked on next event + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + assertTrue(secondSeasoningLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(2, h1Count.get()); + assertEquals(1, h2Count.get()); + } + + @Test + public void orderTimedOutIsSuppressedWhenDishServedFires() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED, KitchenActivity.SEASONING_ADDED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.ORDER_TIMED_OUT, KitchenActivity.TIMEOUT_REACHED) + .suppressedBy(CookingEvent.ORDER_TIMED_OUT, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.ORDER_TIMED_OUT, 1) + .build(); + + CountDownLatch dishServedLatch = new CountDownLatch(1); + AtomicInteger timeoutCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + if (event == CookingEvent.DISH_SERVED) { + dishServedLatch.countDown(); + } + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.ORDER_TIMED_OUT, (event, metadata) -> timeoutCount.incrementAndGet()); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> {}); + + // Fire DISH_SERVED first + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + assertTrue(dishServedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + // ORDER_TIMED_OUT should be suppressed + eventsManager.notifyInternalEvent(KitchenActivity.TIMEOUT_REACHED, null); + + assertEquals(0, timeoutCount.get()); + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.ORDER_TIMED_OUT)); + } + + @Test + public void orderTimedOutFiresWhenDishServedHasNotFired() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED, KitchenActivity.SEASONING_ADDED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.ORDER_TIMED_OUT, KitchenActivity.TIMEOUT_REACHED) + .suppressedBy(CookingEvent.ORDER_TIMED_OUT, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.ORDER_TIMED_OUT, 1) + .build(); + + CountDownLatch timeoutLatch = new CountDownLatch(1); + AtomicInteger timeoutCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + if (event == CookingEvent.ORDER_TIMED_OUT) { + timeoutLatch.countDown(); + } + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.ORDER_TIMED_OUT, (event, metadata) -> timeoutCount.incrementAndGet()); + + // Trigger timeout before DISH_SERVED + eventsManager.notifyInternalEvent(KitchenActivity.TIMEOUT_REACHED, null); + + assertTrue(timeoutLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, timeoutCount.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.ORDER_TIMED_OUT)); + } + + @Test + public void unregisterRemovesAllHandlersForEvent() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED) + .executionLimit(CookingEvent.DISH_SERVED, -1) + .build(); + + CountDownLatch firstLatch = new CountDownLatch(2); + CountDownLatch reRegisterLatch = new CountDownLatch(1); + AtomicInteger h1Count = new AtomicInteger(0); + AtomicInteger h2Count = new AtomicInteger(0); + + EventsManager eventsManager = new EventsManagerCore<>(config, SIMPLE_DELIVERY); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> { + h1Count.incrementAndGet(); + firstLatch.countDown(); + }); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> { + h2Count.incrementAndGet(); + firstLatch.countDown(); + }); + + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + assertTrue(firstLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, h1Count.get()); + assertEquals(1, h2Count.get()); + + // Unregister all handlers for DISH_SERVED + eventsManager.unregister(CookingEvent.DISH_SERVED); + + // Fire again - no handlers should be called + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + + // Use eventAlreadyTriggered to wait for processing + eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED); + + assertEquals(1, h1Count.get()); + assertEquals(1, h2Count.get()); + + // Re-register and verify it works + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> { + h1Count.incrementAndGet(); + reRegisterLatch.countDown(); + }); + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + + assertTrue(reRegisterLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(2, h1Count.get()); + } + + @Test + public void registerIsIgnoredAfterDestroy() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger hCount = new AtomicInteger(0); + + EventsManager eventsManager = new EventsManagerCore<>(config, SIMPLE_DELIVERY); + + // Register initial handler and trigger event for late subscribers + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> latch.countDown()); + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + eventsManager.destroy(); + + // Register after destroy - should be ignored, no replay + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> hCount.incrementAndGet()); + + assertEquals(0, hCount.get()); + } + + @Test + public void notifyInternalEventIsIgnoredAfterDestroy() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED) + .executionLimit(CookingEvent.DISH_SERVED, -1) + .build(); + + AtomicInteger hCount = new AtomicInteger(0); + + EventsManager eventsManager = new EventsManagerCore<>(config, SIMPLE_DELIVERY); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> hCount.incrementAndGet()); + + eventsManager.destroy(); + + // Notify after destroy - should be ignored + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + + assertEquals(0, hCount.get()); + } + + @Test + public void eventAlreadyTriggeredReturnsFalseAfterDestroy() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> {}); + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + eventsManager.destroy(); + + // State is cleared after destroy + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + } + + @Test + public void handlersAreNotCalledAfterDestroy() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED, KitchenActivity.SEASONING_ADDED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + AtomicInteger hCount = new AtomicInteger(0); + + EventsManager eventsManager = new EventsManagerCore<>(config, SIMPLE_DELIVERY); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> hCount.incrementAndGet()); + + // Partially satisfy requirements + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + + // Destroy before completing requirements + eventsManager.destroy(); + + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + assertEquals(0, hCount.get()); + } + + @Test + public void eventAlreadyTriggeredRespectsExecutionLimits() throws InterruptedException { + // Config with both one-shot (DISH_SERVED) and unlimited (SEASONING_ADJUSTED) events + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.INGREDIENTS_PREPPED) + .requireAny(CookingEvent.SEASONING_ADJUSTED, KitchenActivity.SEASONING_ADDED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, -1) + .build(); + + CountDownLatch latch = new CountDownLatch(2); + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + // Before any triggers + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.SEASONING_ADJUSTED)); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> {}); + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> {}); + + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + // One-shot event returns true (completed all executions) + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + // Unlimited event returns false (can still fire again) + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.SEASONING_ADJUSTED)); + } + + + @Test + public void requireAnyWithGroupsFiresWhenFirstGroupComplete() throws InterruptedException { + // External event fires when EITHER: + // Group 1: INGREDIENTS_PREPPED AND SEASONING_ADDED + // OR + // Group 2: LEFTOVER_MEAT_FOUND AND LEFTOVER_VEGGIES_FOUND AND LEFTOVER_SAUCE_FOUND + Set group1 = new HashSet<>(); + group1.add(KitchenActivity.INGREDIENTS_PREPPED); + group1.add(KitchenActivity.SEASONING_ADDED); + + Set group2 = new HashSet<>(); + group2.add(KitchenActivity.LEFTOVER_MEAT_FOUND); + group2.add(KitchenActivity.LEFTOVER_VEGGIES_FOUND); + group2.add(KitchenActivity.LEFTOVER_SAUCE_FOUND); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, group1, group2) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger callCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> callCount.incrementAndGet()); + + // Complete first group + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, callCount.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + } + + @Test + public void requireAnyWithGroupsFiresWhenSecondGroupComplete() throws InterruptedException { + // Same config as above, but complete the second group instead + Set group1 = new HashSet<>(); + group1.add(KitchenActivity.INGREDIENTS_PREPPED); + group1.add(KitchenActivity.SEASONING_ADDED); + + Set group2 = new HashSet<>(); + group2.add(KitchenActivity.LEFTOVER_MEAT_FOUND); + group2.add(KitchenActivity.LEFTOVER_VEGGIES_FOUND); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, group1, group2) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger callCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> callCount.incrementAndGet()); + + // Complete second group (not touching first group) + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_MEAT_FOUND, null); + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_VEGGIES_FOUND, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, callCount.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + } + + @Test + public void requireAnyWithGroupsDoesNotFireWithPartialGroup() throws InterruptedException { + Set group1 = new HashSet<>(); + group1.add(KitchenActivity.INGREDIENTS_PREPPED); + group1.add(KitchenActivity.SEASONING_ADDED); + group1.add(KitchenActivity.OVEN_PREHEATED); + + Set group2 = new HashSet<>(); + group2.add(KitchenActivity.LEFTOVER_MEAT_FOUND); + group2.add(KitchenActivity.LEFTOVER_VEGGIES_FOUND); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, group1, group2) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + AtomicInteger callCount = new AtomicInteger(0); + + EventsManager eventsManager = new EventsManagerCore<>(config, SIMPLE_DELIVERY); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> callCount.incrementAndGet()); + + // Partial completion of group 1 (missing OVEN_PREHEATED) + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + // Partial completion of group 2 (missing LEFTOVER_VEGGIES_FOUND) + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_MEAT_FOUND, null); + + // Wait for processing to complete + eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED); + + assertEquals(0, callCount.get()); + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + } + + @Test + public void requireAnyWithGroupsFiresOnceEvenWhenMultipleGroupsComplete() throws InterruptedException { + Set group1 = new HashSet<>(); + group1.add(KitchenActivity.INGREDIENTS_PREPPED); + + Set group2 = new HashSet<>(); + group2.add(KitchenActivity.LEFTOVER_MEAT_FOUND); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, group1, group2) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger callCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> callCount.incrementAndGet()); + + // Complete first group + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + // Now complete second group as well + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_MEAT_FOUND, null); + + // Wait for processing + eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED); + + // Should only fire once due to execution limit + assertEquals(1, callCount.get()); + } + + @Test + public void requireAnyGroupedWithPrerequisite() throws InterruptedException { + // DISH_SERVED requires simple condition + // SEASONING_ADJUSTED uses OR-of-ANDs and requires DISH_SERVED first + Set group1 = new HashSet<>(); + group1.add(KitchenActivity.SEASONING_ADDED); + + Set group2 = new HashSet<>(); + group2.add(KitchenActivity.LEFTOVER_MEAT_FOUND); + group2.add(KitchenActivity.LEFTOVER_VEGGIES_FOUND); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.SEASONING_ADJUSTED, group1, group2) + .prerequisite(CookingEvent.SEASONING_ADJUSTED, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, 1) + .build(); + + CountDownLatch seasoningLatch = new CountDownLatch(1); + AtomicInteger seasoningCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + if (event == CookingEvent.SEASONING_ADJUSTED) { + seasoningLatch.countDown(); + } + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> seasoningCount.incrementAndGet()); + + // Complete group 2 for SEASONING_ADJUSTED, but DISH_SERVED not fired yet + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_MEAT_FOUND, null); + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_VEGGIES_FOUND, null); + + // Wait and verify SEASONING_ADJUSTED not fired + eventsManager.eventAlreadyTriggered(CookingEvent.SEASONING_ADJUSTED); + assertEquals(0, seasoningCount.get()); + + // Now trigger DISH_SERVED + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + // Wait for DISH_SERVED + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + // Now trigger something that completes group 1 + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + assertTrue(seasoningLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, seasoningCount.get()); + } + + @Test + public void requireAnyGroupedWithSuppressor() throws InterruptedException { + Set group1 = new HashSet<>(); + group1.add(KitchenActivity.TIMEOUT_REACHED); + + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.ORDER_TIMED_OUT, group1) + .suppressedBy(CookingEvent.ORDER_TIMED_OUT, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.ORDER_TIMED_OUT, 1) + .build(); + + AtomicInteger timeoutCount = new AtomicInteger(0); + + EventsManager eventsManager = new EventsManagerCore<>(config, SIMPLE_DELIVERY); + eventsManager.register(CookingEvent.ORDER_TIMED_OUT, (event, metadata) -> timeoutCount.incrementAndGet()); + + // Trigger DISH_SERVED first + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + // Now trigger timeout - should be suppressed + eventsManager.notifyInternalEvent(KitchenActivity.TIMEOUT_REACHED, null); + + // Wait for processing + eventsManager.eventAlreadyTriggered(CookingEvent.ORDER_TIMED_OUT); + + assertEquals(0, timeoutCount.get()); + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.ORDER_TIMED_OUT)); + } + + @Test + public void prerequisiteChainResolvedInSingleNotification() throws InterruptedException { + // DISH_SERVED fires when OVEN_PREHEATED + // SEASONING_ADJUSTED fires when OVEN_PREHEATED, but requires DISH_SERVED first + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.SEASONING_ADJUSTED, KitchenActivity.OVEN_PREHEATED) + .prerequisite(CookingEvent.SEASONING_ADJUSTED, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, 1) + .build(); + + CountDownLatch bothFiredLatch = new CountDownLatch(2); + AtomicInteger dishServedCount = new AtomicInteger(0); + AtomicInteger seasoningCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + bothFiredLatch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> dishServedCount.incrementAndGet()); + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> seasoningCount.incrementAndGet()); + + // Single notification should trigger both events (A fires, then B fires because prerequisite is now met) + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + assertTrue("Both events should fire from single notification", bothFiredLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, dishServedCount.get()); + assertEquals(1, seasoningCount.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.SEASONING_ADJUSTED)); + } + + @Test + public void prerequisiteChainWithOrOfAndsGroups() throws InterruptedException { + // DISH_SERVED = SDK_READY_FROM_CACHE (fires when sync group completes) + // LEFTOVERS_HEATED = SDK_READY (fires when sync completes, but requires DISH_SERVED first) + + Set syncGroup = new HashSet<>(); + syncGroup.add(KitchenActivity.INGREDIENTS_PREPPED); + syncGroup.add(KitchenActivity.SEASONING_ADDED); + + Set cacheGroup = new HashSet<>(); + cacheGroup.add(KitchenActivity.LEFTOVER_MEAT_FOUND); + cacheGroup.add(KitchenActivity.LEFTOVER_VEGGIES_FOUND); + + EventsManagerConfig config = EventsManagerConfig.builder() + // DISH_SERVED fires when either sync or cache group completes + .requireAny(CookingEvent.DISH_SERVED, syncGroup, cacheGroup) + // LEFTOVERS_HEATED requires the same sync events, but also DISH_SERVED as prerequisite + .requireAll(CookingEvent.LEFTOVERS_HEATED, + KitchenActivity.INGREDIENTS_PREPPED, + KitchenActivity.SEASONING_ADDED) + .prerequisite(CookingEvent.LEFTOVERS_HEATED, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.LEFTOVERS_HEATED, 1) + .build(); + + CountDownLatch bothFiredLatch = new CountDownLatch(2); + AtomicInteger dishServedCount = new AtomicInteger(0); + AtomicInteger leftoversCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + bothFiredLatch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> dishServedCount.incrementAndGet()); + eventsManager.register(CookingEvent.LEFTOVERS_HEATED, (event, metadata) -> leftoversCount.incrementAndGet()); + + // First sync event + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + + // Second sync event should trigger chain: DISH_SERVED -> LEFTOVERS_HEATED + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + assertTrue("Both events should fire when sync completes", bothFiredLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, dishServedCount.get()); + assertEquals(1, leftoversCount.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.LEFTOVERS_HEATED)); + } + + @Test + public void prerequisiteLoopTerminatesWhenNoMoreEventsCanFire() throws InterruptedException { + // Create a chain where only DISH_SERVED can fire (SEASONING_ADJUSTED requires a different trigger) + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.SEASONING_ADJUSTED, KitchenActivity.SEASONING_ADDED) // Different trigger! + .prerequisite(CookingEvent.SEASONING_ADJUSTED, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, 1) + .build(); + + CountDownLatch dishServedLatch = new CountDownLatch(1); + AtomicInteger dishServedCount = new AtomicInteger(0); + AtomicInteger seasoningCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + if (event == CookingEvent.DISH_SERVED) { + dishServedLatch.countDown(); + } + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> dishServedCount.incrementAndGet()); + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> seasoningCount.incrementAndGet()); + + // Only DISH_SERVED should fire, loop should terminate without firing SEASONING_ADJUSTED + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + assertTrue(dishServedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, dishServedCount.get()); + assertEquals(0, seasoningCount.get()); // Should NOT fire - different trigger + + // Verify processing completed (no infinite loop) + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.SEASONING_ADJUSTED)); + } + + @Test + public void threeLevelPrerequisiteChain() throws InterruptedException { + // DISH_SERVED -> SEASONING_ADJUSTED -> ORDER_TIMED_OUT + // All triggered by the same internal event + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.SEASONING_ADJUSTED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.ORDER_TIMED_OUT, KitchenActivity.OVEN_PREHEATED) + .prerequisite(CookingEvent.SEASONING_ADJUSTED, CookingEvent.DISH_SERVED) + .prerequisite(CookingEvent.ORDER_TIMED_OUT, CookingEvent.SEASONING_ADJUSTED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, 1) + .executionLimit(CookingEvent.ORDER_TIMED_OUT, 1) + .build(); + + CountDownLatch allFiredLatch = new CountDownLatch(3); + AtomicInteger dishServedCount = new AtomicInteger(0); + AtomicInteger seasoningCount = new AtomicInteger(0); + AtomicInteger timeoutCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + allFiredLatch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> dishServedCount.incrementAndGet()); + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> seasoningCount.incrementAndGet()); + eventsManager.register(CookingEvent.ORDER_TIMED_OUT, (event, metadata) -> timeoutCount.incrementAndGet()); + + // Single notification should trigger all three events in chain + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + assertTrue("All three events should fire from single notification", allFiredLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, dishServedCount.get()); + assertEquals(1, seasoningCount.get()); + assertEquals(1, timeoutCount.get()); + } + + /** + * Tests that requireAny with unlimited execution only triggers when the CURRENT + * internal event is one of the triggers, not when historical events satisfy the condition. + * This prevents the scenario where: + * 1. Internal event A fires (is in requireAny for unlimited event X, but prerequisite not met) + * 2. Internal event B fires (satisfies prerequisite for X) + * 3. X incorrectly fires because A is in the seen set (but A was the trigger, not B) + */ + @Test + public void requireAnyUnlimitedOnlyTriggersOnCurrentEvent() throws InterruptedException { + // DISH_SERVED fires when OVEN_PREHEATED (one-shot, acts as prerequisite) + // SEASONING_ADJUSTED fires when SEASONING_ADDED (unlimited, requires DISH_SERVED) + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.OVEN_PREHEATED) + .requireAny(CookingEvent.SEASONING_ADJUSTED, KitchenActivity.SEASONING_ADDED) + .prerequisite(CookingEvent.SEASONING_ADJUSTED, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, -1) // Unlimited + .build(); + + CountDownLatch dishServedLatch = new CountDownLatch(1); + AtomicInteger seasoningCount = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + if (event == CookingEvent.DISH_SERVED) { + dishServedLatch.countDown(); + } + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> seasoningCount.incrementAndGet()); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> {}); + + // Step 1: Fire SEASONING_ADDED BEFORE DISH_SERVED + // This adds SEASONING_ADDED to seenInternal, but SEASONING_ADJUSTED can't fire (prerequisite not met) + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + // Wait for processing + eventsManager.eventAlreadyTriggered(CookingEvent.SEASONING_ADJUSTED); + assertEquals("SEASONING_ADJUSTED should NOT fire (prerequisite not met)", 0, seasoningCount.get()); + + // Step 2: Fire OVEN_PREHEATED to trigger DISH_SERVED + // The bug would be: SEASONING_ADDED is in seenInternal, so SEASONING_ADJUSTED incorrectly fires + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + assertTrue(dishServedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + // Wait for any async processing + Thread.sleep(100); + + // SEASONING_ADJUSTED should NOT have fired because OVEN_PREHEATED is not in its requireAny + assertEquals("SEASONING_ADJUSTED should NOT fire from OVEN_PREHEATED (wrong trigger)", 0, seasoningCount.get()); + + // Step 3: Now fire SEASONING_ADDED again - this should trigger SEASONING_ADJUSTED + CountDownLatch seasoningLatch = new CountDownLatch(1); + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> seasoningLatch.countDown()); + + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + + assertTrue(seasoningLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("SEASONING_ADJUSTED should fire when correct trigger arrives", 1, seasoningCount.get()); + } + + @Test + public void requireAnyDoesNotRetriggerFromHistoricalEvents() throws InterruptedException { + // Scenario: Multiple requireAny triggers for unlimited event + // Event should only fire when one of ITS triggers fires, not when other events fire + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAny(CookingEvent.DISH_SERVED, KitchenActivity.OVEN_PREHEATED, KitchenActivity.PLATES_RETRIEVED) + .requireAny(CookingEvent.SEASONING_ADJUSTED, + KitchenActivity.SEASONING_ADDED, + KitchenActivity.LEFTOVER_SAUCE_FOUND) + .prerequisite(CookingEvent.SEASONING_ADJUSTED, CookingEvent.DISH_SERVED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .executionLimit(CookingEvent.SEASONING_ADJUSTED, -1) + .build(); + + AtomicInteger seasoningCount = new AtomicInteger(0); + + EventsManager eventsManager = new EventsManagerCore<>(config, SIMPLE_DELIVERY); + + eventsManager.register(CookingEvent.SEASONING_ADJUSTED, (event, metadata) -> seasoningCount.incrementAndGet()); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> {}); + + // Fire a SEASONING_ADJUSTED trigger before prerequisite is met + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + assertEquals(0, seasoningCount.get()); + + // Satisfy prerequisite with OVEN_PREHEATED + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + // SEASONING_ADJUSTED should NOT have fired (OVEN_PREHEATED is not its trigger) + assertEquals("Historical SEASONING_ADDED should not cause trigger", 0, seasoningCount.get()); + + // Fire an unrelated internal event + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_MEAT_FOUND, null); + assertEquals("Unrelated event should not cause trigger", 0, seasoningCount.get()); + + // Now fire actual trigger - should work + eventsManager.notifyInternalEvent(KitchenActivity.LEFTOVER_SAUCE_FOUND, null); + Thread.sleep(100); + assertEquals("Correct trigger should fire event", 1, seasoningCount.get()); + } + + @Test + public void requireAllStillAccumulatesCorrectly() throws InterruptedException { + EventsManagerConfig config = EventsManagerConfig.builder() + .requireAll(CookingEvent.DISH_SERVED, + KitchenActivity.INGREDIENTS_PREPPED, + KitchenActivity.SEASONING_ADDED, + KitchenActivity.OVEN_PREHEATED) + .executionLimit(CookingEvent.DISH_SERVED, 1) + .build(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger count = new AtomicInteger(0); + + EventDelivery delivery = (handler, event, metadata) -> { + handler.handle(event, metadata); + latch.countDown(); + }; + + EventsManager eventsManager = new EventsManagerCore<>(config, delivery); + eventsManager.register(CookingEvent.DISH_SERVED, (event, metadata) -> count.incrementAndGet()); + + // Fire events in any order - should accumulate + eventsManager.notifyInternalEvent(KitchenActivity.SEASONING_ADDED, null); + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + eventsManager.notifyInternalEvent(KitchenActivity.INGREDIENTS_PREPPED, null); + assertFalse(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + + eventsManager.notifyInternalEvent(KitchenActivity.OVEN_PREHEATED, null); + + assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, count.get()); + assertTrue(eventsManager.eventAlreadyTriggered(CookingEvent.DISH_SERVED)); + } +} diff --git a/events/src/test/java/io/harness/events/EventsManagersTest.java b/events/src/test/java/io/harness/events/EventsManagersTest.java new file mode 100644 index 000000000..e1a50e033 --- /dev/null +++ b/events/src/test/java/io/harness/events/EventsManagersTest.java @@ -0,0 +1,15 @@ +package io.harness.events; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +public class EventsManagersTest { + + @Test + public void createDeliversEventsManagerCore() { + EventsManager eventsManager = EventsManagers.create(EventsManagerConfig.empty(), mock(EventDelivery.class)); + assertTrue(eventsManager instanceof EventsManagerCore); + } +} diff --git a/events/src/test/java/io/harness/events/TestLogging.java b/events/src/test/java/io/harness/events/TestLogging.java new file mode 100644 index 000000000..65bc71475 --- /dev/null +++ b/events/src/test/java/io/harness/events/TestLogging.java @@ -0,0 +1,34 @@ +package io.harness.events; + +class TestLogging implements Logging { + String errorMessage; + String warningMessage; + String infoMessage; + String debugMessage; + String verboseMessage; + + @Override + public void logError(String message) { + errorMessage = message; + } + + @Override + public void logWarning(String message) { + warningMessage = message; + } + + @Override + public void logInfo(String message) { + infoMessage = message; + } + + @Override + public void logDebug(String message) { + debugMessage = message; + } + + @Override + public void logVerbose(String message) { + verboseMessage = message; + } +} diff --git a/events/src/test/java/io/harness/events/TopologicalSorterTest.java b/events/src/test/java/io/harness/events/TopologicalSorterTest.java new file mode 100644 index 000000000..c06f6b7c8 --- /dev/null +++ b/events/src/test/java/io/harness/events/TopologicalSorterTest.java @@ -0,0 +1,231 @@ +package io.harness.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class TopologicalSorterTest { + + @Test + public void emptySetReturnsEmptyList() { + TopologicalSorter sorter = new TopologicalSorter<>( + Collections.emptySet(), + Collections.emptyMap() + ); + + List result = sorter.sort(); + assertTrue(result.isEmpty()); + } + + @Test + public void singleNodeReturnsSingletonList() { + Set nodes = Collections.singleton("A"); + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.emptySet()); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + List result = sorter.sort(); + + assertEquals(1, result.size()); + assertEquals("A", result.get(0)); + } + + @Test + public void independentNodesCanBeInAnyOrder() { + Set nodes = new HashSet<>(); + nodes.add("A"); + nodes.add("B"); + nodes.add("C"); + + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.emptySet()); + dependencies.put("B", Collections.emptySet()); + dependencies.put("C", Collections.emptySet()); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + List result = sorter.sort(); + + assertEquals(3, result.size()); + assertTrue(result.contains("A")); + assertTrue(result.contains("B")); + assertTrue(result.contains("C")); + } + + @Test + public void simpleChainRespectsOrder() { + // A depends on B, B depends on C + // Expected: C, B, A + Set nodes = new HashSet<>(); + nodes.add("A"); + nodes.add("B"); + nodes.add("C"); + + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.singleton("B")); + dependencies.put("B", Collections.singleton("C")); + dependencies.put("C", Collections.emptySet()); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + List result = sorter.sort(); + + assertEquals(3, result.size()); + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + int idxC = result.indexOf("C"); + + assertTrue("C should come before B", idxC < idxB); + assertTrue("B should come before A", idxB < idxA); + } + + @Test + public void multipleDependenciesRespected() { + // A depends on B and C + // Expected: B and C before A (order between B and C doesn't matter) + Set nodes = new HashSet<>(); + nodes.add("A"); + nodes.add("B"); + nodes.add("C"); + + Map> dependencies = new HashMap<>(); + Set aDeps = new HashSet<>(); + aDeps.add("B"); + aDeps.add("C"); + dependencies.put("A", aDeps); + dependencies.put("B", Collections.emptySet()); + dependencies.put("C", Collections.emptySet()); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + List result = sorter.sort(); + + assertEquals(3, result.size()); + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + int idxC = result.indexOf("C"); + + assertTrue("B should come before A", idxB < idxA); + assertTrue("C should come before A", idxC < idxA); + } + + @Test + public void diamondDependencyResolved() { + Set nodes = new HashSet<>(); + nodes.add("A"); + nodes.add("B"); + nodes.add("C"); + nodes.add("D"); + + Map> dependencies = new HashMap<>(); + Set aDeps = new HashSet<>(); + aDeps.add("B"); + aDeps.add("C"); + dependencies.put("A", aDeps); + dependencies.put("B", Collections.singleton("D")); + dependencies.put("C", Collections.singleton("D")); + dependencies.put("D", Collections.emptySet()); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + List result = sorter.sort(); + + assertEquals(4, result.size()); + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + int idxC = result.indexOf("C"); + int idxD = result.indexOf("D"); + + assertTrue("D should come before B", idxD < idxB); + assertTrue("D should come before C", idxD < idxC); + assertTrue("B should come before A", idxB < idxA); + assertTrue("C should come before A", idxC < idxA); + } + + @Test(expected = IllegalStateException.class) + public void detectsDirectCycle() { + // A depends on B, B depends on A + Set nodes = new HashSet<>(); + nodes.add("A"); + nodes.add("B"); + + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.singleton("B")); + dependencies.put("B", Collections.singleton("A")); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + sorter.sort(); // Should throw + } + + @Test(expected = IllegalStateException.class) + public void detectsSelfCycle() { + // A depends on itself + Set nodes = Collections.singleton("A"); + + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.singleton("A")); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + sorter.sort(); // Should throw + } + + @Test(expected = IllegalStateException.class) + public void detectsLongCycle() { + // A -> B -> C -> A + Set nodes = new HashSet<>(); + nodes.add("A"); + nodes.add("B"); + nodes.add("C"); + + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.singleton("B")); + dependencies.put("B", Collections.singleton("C")); + dependencies.put("C", Collections.singleton("A")); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + sorter.sort(); // Should throw + } + + @Test + public void handlesMissingDependencyEntries() { + // If a node is not in dependencies map, it should be treated as having no dependencies + Set nodes = new HashSet<>(); + nodes.add("A"); + nodes.add("B"); + + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.singleton("B")); + // B is not in dependencies map + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + List result = sorter.sort(); + + assertEquals(2, result.size()); + int idxA = result.indexOf("A"); + int idxB = result.indexOf("B"); + assertTrue("B should come before A", idxB < idxA); + } + + @Test + public void resultIsUnmodifiable() { + Set nodes = Collections.singleton("A"); + Map> dependencies = new HashMap<>(); + dependencies.put("A", Collections.emptySet()); + + TopologicalSorter sorter = new TopologicalSorter<>(nodes, dependencies); + List result = sorter.sort(); + + try { + result.add("B"); + fail("Result should be unmodifiable"); + } catch (UnsupportedOperationException expected) { + // expected + } + } +} + diff --git a/gradle.properties b/gradle.properties index aa8a41b37..5e07a3fe4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,10 +2,11 @@ mavenCentralUsername= mavenCentralPassword= android.useAndroidX=true +android.experimental.fusedLibrarySupport=true android.nonTransitiveRClass=true -android.defaults.buildfeatures.buildconfig=true -android.nonFinalResIds=false +# Removed in AGP 9.0: android.defaults.buildfeatures.buildconfig=true +# Removed in AGP 9.0: android.nonFinalResIds=false kotlin.stdlib.default.dependency=false # Increase heap size for DEX merging to avoid OOM errors @@ -18,4 +19,4 @@ signing.keyId= signing.password= # Path to the file created in the previous step. -signing.secretKeyRingFile= \ No newline at end of file +signing.secretKeyRingFile= diff --git a/gradle/common-android-library.gradle b/gradle/common-android-library.gradle new file mode 100644 index 000000000..fe96407c8 --- /dev/null +++ b/gradle/common-android-library.gradle @@ -0,0 +1,33 @@ +android { + compileSdk 33 + defaultConfig { + minSdk 19 + targetSdk 31 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +tasks.withType(JavaCompile).configureEach { + options.compilerArgs.add('-parameters') +} + +def kotlinCompileClass = null +try { + kotlinCompileClass = Class.forName('org.jetbrains.kotlin.gradle.tasks.KotlinCompile') +} catch (Throwable ignored) { + // Kotlin plugin not applied in this module; skip Kotlin options configuration +} +if (kotlinCompileClass != null) { + tasks.withType(kotlinCompileClass).configureEach { + kotlinOptions { + jvmTarget = "1.8" + javaParameters = true + } + } +} + +// Enable Jacoco coverage configuration for all Android library modules +apply from: "$rootDir/gradle/jacoco-android.gradle" diff --git a/gradle/jacoco-android.gradle b/gradle/jacoco-android.gradle new file mode 100644 index 000000000..68103d1a8 --- /dev/null +++ b/gradle/jacoco-android.gradle @@ -0,0 +1,128 @@ +apply plugin: 'jacoco' + +jacoco { + toolVersion = '0.8.8' +} + +tasks.withType(Test) { + jacoco { + includeNoLocationClasses = true + excludes = ['jdk.internal.*'] + enabled = true + } + systemProperty 'robolectric.enabledSdks', '28,29,30,31,32,33' + finalizedBy jacocoTestReport +} + +tasks.register('jacocoTestReport', JacocoReport) { + dependsOn 'testDebugUnitTest' + + reports { + xml.required = true + html.required = true + csv.required = false + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] + def classDirectoriesFiles = [] + + // Try multiple possible class directory locations for different AGP versions + // Android Gradle Plugin may compile classes to different locations + // NOTE: If new modules use different compilation output directories, add them here + def possibleClassDirs = [ + "${buildDir}/intermediates/javac/debug/compileDebugJavaWithJavac/classes", // AGP 9.0+ location (primary) + "${buildDir}/classes/java/main" // Standard fallback location + ] + + possibleClassDirs.each { dirPath -> + def dir = file(dirPath) + if (dir.exists() && dir.isDirectory()) { + def classDir = fileTree(dir: dirPath, excludes: fileFilter) + if (!classDir.isEmpty()) { + classDirectoriesFiles.add(classDir) + } + } + } + + sourceDirectories.from = files(['src/main/java', 'src/main/kotlin']) + classDirectories.from = files(classDirectoriesFiles) + + def execFiles = fileTree(dir: "$buildDir", includes: [ + 'jacoco/testDebugUnitTest.exec', + 'outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec', + 'jacoco/*.exec', + 'outputs/**/**.exec' + ]) + executionData.from = files(execFiles.filter { it.exists() }) + outputs.upToDateWhen { false } + + doFirst { + logger.lifecycle("=== JaCoCo Report Generation ===") + logger.lifecycle("Source directories:") + sourceDirectories.files.each { dir -> + if (dir.exists()) { + logger.lifecycle(" - Found: $dir") + } else { + logger.lifecycle(" - Missing: $dir") + } + } + logger.lifecycle("Class directories:") + classDirectories.files.each { dir -> + logger.lifecycle(" - $dir (${dir.exists() ? 'exists' : 'missing'})") + } + def execDataFiles = executionData.files + logger.lifecycle("Execution data files:") + if (execDataFiles.isEmpty() || !execDataFiles.any { it.exists() }) { + logger.warn(" - No execution data files found - coverage report will be empty") + } else { + execDataFiles.each { file -> + if (file.exists()) { + logger.lifecycle(" - Found: $file (${file.length()} bytes)") + } else { + logger.lifecycle(" - Missing: $file") + } + } + } + logger.lifecycle("================================") + } +} + +task generateJacocoXmlReport { + doLast { + def reportDir = file("${buildDir}/reports/jacoco/jacocoTestReport") + reportDir.mkdirs() + def execFiles = fileTree(dir: "${buildDir}", includes: ['**/*.exec']) + if (execFiles.isEmpty()) { + def reportFile = new File(reportDir, "jacocoTestReport.xml") + reportFile.text = "\n\n" + println "Created empty JaCoCo report at ${reportFile.absolutePath}" + } else { + println "Found JaCoCo exec files: ${execFiles.files}" + } + def reportFile = new File(reportDir, "jacocoTestReport.xml") + if (reportFile.exists()) { + println "\n==== JaCoCo Report Content ====" + println "Report file size: ${reportFile.length()} bytes" + if (reportFile.length() > 0) { + def xmlContent = reportFile.text + println "First 500 chars of report: ${xmlContent.take(500)}..." + def packageCount = (xmlContent =~ / + def b = proj.buildDir + + // Try multiple possible class directory locations for different AGP versions + // NOTE: If new modules use different compilation output directories, add them here + def possibleClassDirs = [ + new File(b, "intermediates/javac/debug/compileDebugJavaWithJavac/classes"), // AGP 9.0+ location (primary) + new File(b, "classes/java/main") // Standard fallback location + ] + + possibleClassDirs.findAll { it.exists() && it.isDirectory() }.collect { dir -> + proj.fileTree(dir: dir, excludes: fileFilter) + } + } + classDirectories.from = files(classDirs) + + // Collect source directories from all subprojects + def srcDirs = subprojects.collectMany { proj -> + [proj.file("src/main/java"), proj.file("src/main/kotlin")] + }.findAll { it.exists() } + sourceDirectories.from = files(srcDirs) + + // Collect execution data from all subprojects + def execFiles = subprojects.collectMany { proj -> + def b = proj.buildDir + [ + new File(b, "jacoco/testDebugUnitTest.exec"), + new File(b, "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec") + ].findAll { it.exists() } + } + executionData.from = files(execFiles) + + doFirst { + logger.lifecycle("=== JaCoCo Root Report Generation ===") + logger.lifecycle("Execution data files:") + def execDataFiles = executionData.files + if (execDataFiles.isEmpty() || !execDataFiles.any { it.exists() }) { + logger.warn(" - No execution data files found - coverage report will be empty") + } else { + execDataFiles.each { file -> + if (file.exists()) { + logger.lifecycle(" - Found: $file (${file.length()} bytes)") + } else { + logger.lifecycle(" - Missing: $file") + } + } + } + logger.lifecycle("=======================================") + } + + reports { + xml.required = true + html.required = true + csv.required = false + + xml.outputLocation = file("$buildDir/reports/jacoco/jacocoRootReport/jacocoRootReport.xml") + html.outputLocation = file("$buildDir/reports/jacoco/jacocoRootReport/html") + } + + // Always regenerate the report + outputs.upToDateWhen { false } +} + +// Wire all module unit tests into the aggregate Jacoco report +subprojects { proj -> + // Only consider projects that apply the Jacoco plugin + plugins.withId('jacoco') { + tasks.withType(Test).matching { it.name == 'testDebugUnitTest' }.configureEach { testTask -> + rootProject.tasks.named('jacocoRootReport') { + dependsOn(testTask) + } + } + } +} + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..a6b4a01c5 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,50 @@ +[versions] +kotlin = "1.8.0" +jetbrains-annotations = "26.0.2" +androidx-room = "2.4.3" +androidx-work = "2.7.1" +androidx-lifecycle-process = "2.5.1" +androidx-annotation = "1.2.0" +gson = "2.10.1" +snakeyaml = "2.2" +play-services-base = "18.2.0" +multidex = "2.0.1" +junit4 = "4.13.2" +mockito = "4.8.0" +hamcrest = "2.2" +guava = "31.1-android" +okhttp = "4.10.0" +commons-lang3 = "3.12.0" +androidx-test-ext-junit = "1.1.5" +androidx-test-runner = "1.5.2" +androidx-test-rules = "1.5.0" +androidx-test-orchestrator = "1.4.2" + +[libraries] +jetbrainsAnnotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } +roomRuntime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } +roomCompiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } +workRuntime = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } +workTesting = { module = "androidx.work:work-testing", version.ref = "androidx-work" } +lifecycleProcess = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle-process" } +annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } +playServicesBase = { module = "com.google.android.gms:play-services-base", version.ref = "play-services-base" } +multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } +junit4 = { module = "junit:junit", version.ref = "junit4" } +mockitoCore = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockitoInline = { module = "org.mockito:mockito-inline", version.ref = "mockito" } +mockitoAndroid = { module = "org.mockito:mockito-android", version.ref = "mockito" } +hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } +commonsLang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } +okhttpTls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okhttp" } +androidxTestExtJunit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } +androidxTestRunner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +androidxTestRules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } +androidxTestOrchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" } +kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlinTestJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3d8313c42..c8767b148 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip diff --git a/logger/.gitignore b/logger/.gitignore new file mode 100644 index 000000000..3a11ced48 --- /dev/null +++ b/logger/.gitignore @@ -0,0 +1,4 @@ +/build +.gradle +*.iml +.DS_Store diff --git a/logger/build.gradle b/logger/build.gradle new file mode 100644 index 000000000..a45c8cd36 --- /dev/null +++ b/logger/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'com.android.library' +} + +apply from: "$rootDir/gradle/common-android-library.gradle" + +android { + namespace 'io.split.android.client.logger' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + // Test: Add an external dependency to verify it's included in fused library POM + implementation libs.annotation + + testImplementation libs.junit4 + testImplementation libs.mockitoCore +} diff --git a/logger/src/main/AndroidManifest.xml b/logger/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9a40236b9 --- /dev/null +++ b/logger/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/src/main/java/io/split/android/client/utils/logger/LogPrinter.java b/logger/src/main/java/io/split/android/client/utils/logger/LogPrinter.java similarity index 100% rename from src/main/java/io/split/android/client/utils/logger/LogPrinter.java rename to logger/src/main/java/io/split/android/client/utils/logger/LogPrinter.java diff --git a/src/main/java/io/split/android/client/utils/logger/LogPrinterImpl.java b/logger/src/main/java/io/split/android/client/utils/logger/LogPrinterImpl.java similarity index 100% rename from src/main/java/io/split/android/client/utils/logger/LogPrinterImpl.java rename to logger/src/main/java/io/split/android/client/utils/logger/LogPrinterImpl.java diff --git a/src/main/java/io/split/android/client/utils/logger/Logger.java b/logger/src/main/java/io/split/android/client/utils/logger/Logger.java similarity index 100% rename from src/main/java/io/split/android/client/utils/logger/Logger.java rename to logger/src/main/java/io/split/android/client/utils/logger/Logger.java diff --git a/src/main/java/io/split/android/client/utils/logger/SplitLogLevel.java b/logger/src/main/java/io/split/android/client/utils/logger/SplitLogLevel.java similarity index 100% rename from src/main/java/io/split/android/client/utils/logger/SplitLogLevel.java rename to logger/src/main/java/io/split/android/client/utils/logger/SplitLogLevel.java diff --git a/src/test/java/io/split/android/client/utils/logger/LogPrinterStub.java b/logger/src/test/java/io/split/android/client/utils/logger/LogPrinterStub.java similarity index 100% rename from src/test/java/io/split/android/client/utils/logger/LogPrinterStub.java rename to logger/src/test/java/io/split/android/client/utils/logger/LogPrinterStub.java diff --git a/src/test/java/io/split/android/client/utils/logger/LoggerTest.java b/logger/src/test/java/io/split/android/client/utils/logger/LoggerTest.java similarity index 100% rename from src/test/java/io/split/android/client/utils/logger/LoggerTest.java rename to logger/src/test/java/io/split/android/client/utils/logger/LoggerTest.java diff --git a/main/.gitignore b/main/.gitignore new file mode 100644 index 000000000..3a11ced48 --- /dev/null +++ b/main/.gitignore @@ -0,0 +1,4 @@ +/build +.gradle +*.iml +.DS_Store diff --git a/main/build.gradle b/main/build.gradle new file mode 100644 index 000000000..7ec1e3110 --- /dev/null +++ b/main/build.gradle @@ -0,0 +1,106 @@ +plugins { + id 'com.android.library' +} + +apply from: "$rootDir/gradle/common-android-library.gradle" + +android { + namespace 'io.split.android.client.main' + + defaultConfig { + multiDexEnabled true + consumerProguardFiles "$rootDir/split-proguard-rules.pro" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments clearPackageData: 'true' + + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } + } + + buildFeatures { + buildConfig true + } + + sourceSets { + test { + java.srcDirs += 'src/sharedTest/java' + } + androidTest { + java.srcDirs += 'src/sharedTest/java' + } + } + + buildTypes { + debug { + buildConfigField("String", "SPLIT_VERSION_NAME", "\"${rootProject.ext.splitVersion}\"") + buildConfigField("String", "FLAGS_SPEC", "\"1.3\"") + } + release { + buildConfigField("String", "SPLIT_VERSION_NAME", "\"${rootProject.ext.splitVersion}\"") + buildConfigField("String", "FLAGS_SPEC", "\"1.3\"") + } + } + + testOptions { + unitTests.returnDefaultValues = true + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } +} + +dependencies { + // Public api modules + api project(':logger') + api project(':api') + // Internal module dependencies + implementation project(':events-domain') + + // External dependencies + implementation libs.roomRuntime + annotationProcessor libs.roomCompiler + implementation libs.workRuntime + implementation libs.lifecycleProcess + implementation libs.annotation + implementation libs.gson + implementation libs.snakeyaml + implementation libs.playServicesBase + implementation libs.multidex + + // Test dependencies + testImplementation libs.junit4 + testImplementation libs.mockitoCore + testImplementation libs.mockitoInline + testImplementation libs.kotlinStdlib + testImplementation libs.kotlinTest + testImplementation libs.kotlinTestJunit + testImplementation libs.hamcrest + testImplementation libs.commonsLang3 + testImplementation libs.guava + testImplementation libs.okhttpMockwebserver + testImplementation libs.okhttpTls + + // Android Test dependencies + androidTestImplementation libs.androidxTestExtJunit + androidTestImplementation libs.androidxTestRunner + androidTestImplementation libs.androidxTestRules + androidTestImplementation libs.workTesting + androidTestImplementation libs.kotlinStdlib + androidTestImplementation libs.kotlinTest + androidTestImplementation libs.kotlinTestJunit + androidTestImplementation libs.mockitoCore + androidTestImplementation libs.mockitoAndroid + androidTestImplementation libs.okhttpMockwebserver + androidTestImplementation libs.okhttpTls + androidTestImplementation libs.guava + + androidTestUtil libs.androidxTestOrchestrator +} + +// Align Kotlin only for test and androidTest configurations to avoid duplicate classes +configurations.matching { it.name.toLowerCase().contains("test") }.all { + resolutionStrategy { + force "org.jetbrains.kotlin:kotlin-stdlib:1.8.0" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0" + } +} diff --git a/src/androidTest/AndroidManifest.xml b/main/src/androidTest/AndroidManifest.xml similarity index 100% rename from src/androidTest/AndroidManifest.xml rename to main/src/androidTest/AndroidManifest.xml diff --git a/src/androidTest/assets/attributes_test_split_change.json b/main/src/androidTest/assets/attributes_test_split_change.json similarity index 100% rename from src/androidTest/assets/attributes_test_split_change.json rename to main/src/androidTest/assets/attributes_test_split_change.json diff --git a/src/androidTest/assets/bucket_split_test.json b/main/src/androidTest/assets/bucket_split_test.json similarity index 100% rename from src/androidTest/assets/bucket_split_test.json rename to main/src/androidTest/assets/bucket_split_test.json diff --git a/src/androidTest/assets/lorem_ipsum.txt b/main/src/androidTest/assets/lorem_ipsum.txt similarity index 100% rename from src/androidTest/assets/lorem_ipsum.txt rename to main/src/androidTest/assets/lorem_ipsum.txt diff --git a/src/androidTest/assets/push_msg-ably_error_40012.txt b/main/src/androidTest/assets/push_msg-ably_error_40012.txt similarity index 100% rename from src/androidTest/assets/push_msg-ably_error_40012.txt rename to main/src/androidTest/assets/push_msg-ably_error_40012.txt diff --git a/src/androidTest/assets/push_msg-ably_error_40142.txt b/main/src/androidTest/assets/push_msg-ably_error_40142.txt similarity index 100% rename from src/androidTest/assets/push_msg-ably_error_40142.txt rename to main/src/androidTest/assets/push_msg-ably_error_40142.txt diff --git a/src/androidTest/assets/push_msg-control.txt b/main/src/androidTest/assets/push_msg-control.txt similarity index 100% rename from src/androidTest/assets/push_msg-control.txt rename to main/src/androidTest/assets/push_msg-control.txt diff --git a/src/androidTest/assets/push_msg-largesegment_update.txt b/main/src/androidTest/assets/push_msg-largesegment_update.txt similarity index 100% rename from src/androidTest/assets/push_msg-largesegment_update.txt rename to main/src/androidTest/assets/push_msg-largesegment_update.txt diff --git a/src/androidTest/assets/push_msg-occupancy.txt b/main/src/androidTest/assets/push_msg-occupancy.txt similarity index 100% rename from src/androidTest/assets/push_msg-occupancy.txt rename to main/src/androidTest/assets/push_msg-occupancy.txt diff --git a/src/androidTest/assets/push_msg-segment_updV2.txt b/main/src/androidTest/assets/push_msg-segment_updV2.txt similarity index 100% rename from src/androidTest/assets/push_msg-segment_updV2.txt rename to main/src/androidTest/assets/push_msg-segment_updV2.txt diff --git a/src/androidTest/assets/push_msg-segment_update.txt b/main/src/androidTest/assets/push_msg-segment_update.txt similarity index 100% rename from src/androidTest/assets/push_msg-segment_update.txt rename to main/src/androidTest/assets/push_msg-segment_update.txt diff --git a/src/androidTest/assets/push_msg-segment_update_empty_payload.txt b/main/src/androidTest/assets/push_msg-segment_update_empty_payload.txt similarity index 100% rename from src/androidTest/assets/push_msg-segment_update_empty_payload.txt rename to main/src/androidTest/assets/push_msg-segment_update_empty_payload.txt diff --git a/src/androidTest/assets/push_msg-segment_update_payload.txt b/main/src/androidTest/assets/push_msg-segment_update_payload.txt similarity index 100% rename from src/androidTest/assets/push_msg-segment_update_payload.txt rename to main/src/androidTest/assets/push_msg-segment_update_payload.txt diff --git a/src/androidTest/assets/push_msg-segment_update_payload_generic.txt b/main/src/androidTest/assets/push_msg-segment_update_payload_generic.txt similarity index 100% rename from src/androidTest/assets/push_msg-segment_update_payload_generic.txt rename to main/src/androidTest/assets/push_msg-segment_update_payload_generic.txt diff --git a/src/androidTest/assets/push_msg-split_kill.txt b/main/src/androidTest/assets/push_msg-split_kill.txt similarity index 100% rename from src/androidTest/assets/push_msg-split_kill.txt rename to main/src/androidTest/assets/push_msg-split_kill.txt diff --git a/src/androidTest/assets/push_msg-split_kill_old.txt b/main/src/androidTest/assets/push_msg-split_kill_old.txt similarity index 100% rename from src/androidTest/assets/push_msg-split_kill_old.txt rename to main/src/androidTest/assets/push_msg-split_kill_old.txt diff --git a/src/androidTest/assets/push_msg-split_update-chgnum.txt b/main/src/androidTest/assets/push_msg-split_update-chgnum.txt similarity index 100% rename from src/androidTest/assets/push_msg-split_update-chgnum.txt rename to main/src/androidTest/assets/push_msg-split_update-chgnum.txt diff --git a/src/androidTest/assets/push_msg-split_update.txt b/main/src/androidTest/assets/push_msg-split_update.txt similarity index 100% rename from src/androidTest/assets/push_msg-split_update.txt rename to main/src/androidTest/assets/push_msg-split_update.txt diff --git a/src/androidTest/assets/push_msg-split_update_old_change_nb.txt b/main/src/androidTest/assets/push_msg-split_update_old_change_nb.txt similarity index 100% rename from src/androidTest/assets/push_msg-split_update_old_change_nb.txt rename to main/src/androidTest/assets/push_msg-split_update_old_change_nb.txt diff --git a/src/androidTest/assets/push_token-expired.txt b/main/src/androidTest/assets/push_token-expired.txt similarity index 100% rename from src/androidTest/assets/push_token-expired.txt rename to main/src/androidTest/assets/push_token-expired.txt diff --git a/src/androidTest/assets/simple_split.json b/main/src/androidTest/assets/simple_split.json similarity index 100% rename from src/androidTest/assets/simple_split.json rename to main/src/androidTest/assets/simple_split.json diff --git a/src/androidTest/assets/split.json b/main/src/androidTest/assets/split.json similarity index 100% rename from src/androidTest/assets/split.json rename to main/src/androidTest/assets/split.json diff --git a/src/androidTest/assets/split_changes_1.json b/main/src/androidTest/assets/split_changes_1.json similarity index 100% rename from src/androidTest/assets/split_changes_1.json rename to main/src/androidTest/assets/split_changes_1.json diff --git a/src/androidTest/assets/split_changes_flag_set-0.json b/main/src/androidTest/assets/split_changes_flag_set-0.json similarity index 100% rename from src/androidTest/assets/split_changes_flag_set-0.json rename to main/src/androidTest/assets/split_changes_flag_set-0.json diff --git a/src/androidTest/assets/split_changes_flag_set-1.json b/main/src/androidTest/assets/split_changes_flag_set-1.json similarity index 100% rename from src/androidTest/assets/split_changes_flag_set-1.json rename to main/src/androidTest/assets/split_changes_flag_set-1.json diff --git a/src/androidTest/assets/split_changes_flag_set-2.json b/main/src/androidTest/assets/split_changes_flag_set-2.json similarity index 100% rename from src/androidTest/assets/split_changes_flag_set-2.json rename to main/src/androidTest/assets/split_changes_flag_set-2.json diff --git a/src/androidTest/assets/split_changes_imp_toggle.json b/main/src/androidTest/assets/split_changes_imp_toggle.json similarity index 100% rename from src/androidTest/assets/split_changes_imp_toggle.json rename to main/src/androidTest/assets/split_changes_imp_toggle.json diff --git a/src/androidTest/assets/split_changes_large_segments-0.json b/main/src/androidTest/assets/split_changes_large_segments-0.json similarity index 100% rename from src/androidTest/assets/split_changes_large_segments-0.json rename to main/src/androidTest/assets/split_changes_large_segments-0.json diff --git a/src/androidTest/assets/split_changes_legacy.json b/main/src/androidTest/assets/split_changes_legacy.json similarity index 100% rename from src/androidTest/assets/split_changes_legacy.json rename to main/src/androidTest/assets/split_changes_legacy.json diff --git a/src/androidTest/assets/split_changes_rbs.json b/main/src/androidTest/assets/split_changes_rbs.json similarity index 100% rename from src/androidTest/assets/split_changes_rbs.json rename to main/src/androidTest/assets/split_changes_rbs.json diff --git a/src/androidTest/assets/split_changes_semver.json b/main/src/androidTest/assets/split_changes_semver.json similarity index 100% rename from src/androidTest/assets/split_changes_semver.json rename to main/src/androidTest/assets/split_changes_semver.json diff --git a/src/androidTest/assets/split_dependency.json b/main/src/androidTest/assets/split_dependency.json similarity index 100% rename from src/androidTest/assets/split_dependency.json rename to main/src/androidTest/assets/split_dependency.json diff --git a/src/androidTest/assets/splitchanges_int_test.json b/main/src/androidTest/assets/splitchanges_int_test.json similarity index 100% rename from src/androidTest/assets/splitchanges_int_test.json rename to main/src/androidTest/assets/splitchanges_int_test.json diff --git a/src/androidTest/assets/splitchanges_prerequisites.json b/main/src/androidTest/assets/splitchanges_prerequisites.json similarity index 100% rename from src/androidTest/assets/splitchanges_prerequisites.json rename to main/src/androidTest/assets/splitchanges_prerequisites.json diff --git a/src/androidTest/assets/splitchanges_unsupported_matcher.json b/main/src/androidTest/assets/splitchanges_unsupported_matcher.json similarity index 100% rename from src/androidTest/assets/splitchanges_unsupported_matcher.json rename to main/src/androidTest/assets/splitchanges_unsupported_matcher.json diff --git a/src/androidTest/assets/splits.yaml b/main/src/androidTest/assets/splits.yaml similarity index 100% rename from src/androidTest/assets/splits.yaml rename to main/src/androidTest/assets/splits.yaml diff --git a/src/androidTest/assets/splits1.properties b/main/src/androidTest/assets/splits1.properties similarity index 100% rename from src/androidTest/assets/splits1.properties rename to main/src/androidTest/assets/splits1.properties diff --git a/src/androidTest/assets/splits_test.properties b/main/src/androidTest/assets/splits_test.properties similarity index 100% rename from src/androidTest/assets/splits_test.properties rename to main/src/androidTest/assets/splits_test.properties diff --git a/src/androidTest/assets/splits_yml.yml b/main/src/androidTest/assets/splits_yml.yml similarity index 100% rename from src/androidTest/assets/splits_yml.yml rename to main/src/androidTest/assets/splits_yml.yml diff --git a/src/androidTest/java/fake/HttpClientMock.java b/main/src/androidTest/java/fake/HttpClientMock.java similarity index 100% rename from src/androidTest/java/fake/HttpClientMock.java rename to main/src/androidTest/java/fake/HttpClientMock.java diff --git a/src/androidTest/java/fake/HttpClientStub.java b/main/src/androidTest/java/fake/HttpClientStub.java similarity index 100% rename from src/androidTest/java/fake/HttpClientStub.java rename to main/src/androidTest/java/fake/HttpClientStub.java diff --git a/src/androidTest/java/fake/HttpRequestMock.java b/main/src/androidTest/java/fake/HttpRequestMock.java similarity index 100% rename from src/androidTest/java/fake/HttpRequestMock.java rename to main/src/androidTest/java/fake/HttpRequestMock.java diff --git a/src/androidTest/java/fake/HttpRequestStub.java b/main/src/androidTest/java/fake/HttpRequestStub.java similarity index 100% rename from src/androidTest/java/fake/HttpRequestStub.java rename to main/src/androidTest/java/fake/HttpRequestStub.java diff --git a/src/androidTest/java/fake/HttpResponseMock.java b/main/src/androidTest/java/fake/HttpResponseMock.java similarity index 100% rename from src/androidTest/java/fake/HttpResponseMock.java rename to main/src/androidTest/java/fake/HttpResponseMock.java diff --git a/src/androidTest/java/fake/HttpResponseMockDispatcher.java b/main/src/androidTest/java/fake/HttpResponseMockDispatcher.java similarity index 100% rename from src/androidTest/java/fake/HttpResponseMockDispatcher.java rename to main/src/androidTest/java/fake/HttpResponseMockDispatcher.java diff --git a/src/androidTest/java/fake/HttpResponseStub.java b/main/src/androidTest/java/fake/HttpResponseStub.java similarity index 100% rename from src/androidTest/java/fake/HttpResponseStub.java rename to main/src/androidTest/java/fake/HttpResponseStub.java diff --git a/src/androidTest/java/fake/HttpStreamRequestMock.java b/main/src/androidTest/java/fake/HttpStreamRequestMock.java similarity index 100% rename from src/androidTest/java/fake/HttpStreamRequestMock.java rename to main/src/androidTest/java/fake/HttpStreamRequestMock.java diff --git a/src/androidTest/java/fake/HttpStreamResponseMock.java b/main/src/androidTest/java/fake/HttpStreamResponseMock.java similarity index 100% rename from src/androidTest/java/fake/HttpStreamResponseMock.java rename to main/src/androidTest/java/fake/HttpStreamResponseMock.java diff --git a/src/androidTest/java/fake/LifecycleManagerStub.java b/main/src/androidTest/java/fake/LifecycleManagerStub.java similarity index 100% rename from src/androidTest/java/fake/LifecycleManagerStub.java rename to main/src/androidTest/java/fake/LifecycleManagerStub.java diff --git a/src/androidTest/java/fake/SplitClientStub.java b/main/src/androidTest/java/fake/SplitClientStub.java similarity index 96% rename from src/androidTest/java/fake/SplitClientStub.java rename to main/src/androidTest/java/fake/SplitClientStub.java index 4acebddbc..14c0fcde4 100644 --- a/src/androidTest/java/fake/SplitClientStub.java +++ b/main/src/androidTest/java/fake/SplitClientStub.java @@ -11,6 +11,7 @@ import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitClient; import io.split.android.client.SplitResult; +import io.split.android.client.events.SplitEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; @@ -120,6 +121,11 @@ public void on(SplitEvent event, SplitEventTask task) { } + @Override + public void addEventListener(SplitEventListener listener) { + // Stub implementation - does nothing + } + @Override public boolean track(String eventType) { return false; diff --git a/src/androidTest/java/fake/SplitEventExecutorResourcesMock.java b/main/src/androidTest/java/fake/SplitEventExecutorResourcesMock.java similarity index 100% rename from src/androidTest/java/fake/SplitEventExecutorResourcesMock.java rename to main/src/androidTest/java/fake/SplitEventExecutorResourcesMock.java diff --git a/src/androidTest/java/fake/SyncManagerStub.java b/main/src/androidTest/java/fake/SyncManagerStub.java similarity index 100% rename from src/androidTest/java/fake/SyncManagerStub.java rename to main/src/androidTest/java/fake/SyncManagerStub.java diff --git a/src/androidTest/java/fake/SynchronizerSpyImpl.java b/main/src/androidTest/java/fake/SynchronizerSpyImpl.java similarity index 100% rename from src/androidTest/java/fake/SynchronizerSpyImpl.java rename to main/src/androidTest/java/fake/SynchronizerSpyImpl.java diff --git a/src/androidTest/java/helper/DataSample.kt b/main/src/androidTest/java/helper/DataSample.kt similarity index 100% rename from src/androidTest/java/helper/DataSample.kt rename to main/src/androidTest/java/helper/DataSample.kt diff --git a/src/androidTest/java/helper/DatabaseHelper.java b/main/src/androidTest/java/helper/DatabaseHelper.java similarity index 100% rename from src/androidTest/java/helper/DatabaseHelper.java rename to main/src/androidTest/java/helper/DatabaseHelper.java diff --git a/src/androidTest/java/helper/FileHelper.java b/main/src/androidTest/java/helper/FileHelper.java similarity index 100% rename from src/androidTest/java/helper/FileHelper.java rename to main/src/androidTest/java/helper/FileHelper.java diff --git a/src/androidTest/java/helper/ImpressionListenerHelper.java b/main/src/androidTest/java/helper/ImpressionListenerHelper.java similarity index 100% rename from src/androidTest/java/helper/ImpressionListenerHelper.java rename to main/src/androidTest/java/helper/ImpressionListenerHelper.java diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/main/src/androidTest/java/helper/IntegrationHelper.java similarity index 77% rename from src/androidTest/java/helper/IntegrationHelper.java rename to main/src/androidTest/java/helper/IntegrationHelper.java index 40062cd6d..7d99b3fe0 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/main/src/androidTest/java/helper/IntegrationHelper.java @@ -56,6 +56,12 @@ public class IntegrationHelper { public static final int NEVER_REFRESH_RATE = 999999; + // Base64-encoded split definition payload for "mauro_java" split + public static final String SPLIT_UPDATE_PAYLOAD_TYPE0 = "eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0="; + + // Base64-encoded RBS definition payload for "rbs_test" segment + public static final String RBS_UPDATE_PAYLOAD_TYPE0 = "eyJuYW1lIjoicmJzX3Rlc3QiLCJzdGF0dXMiOiJBQ1RJVkUiLCJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiZXhjbHVkZWQiOnsia2V5cyI6W10sInNlZ21lbnRzIjpbXX0sImNvbmRpdGlvbnMiOlt7Im1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX19XX0="; + private final static Type EVENT_LIST_TYPE = new TypeToken>() { }.getType(); private final static Type IMPRESSIONS_LIST_TYPE = new TypeToken>() { @@ -188,6 +194,68 @@ public static String dummySingleSegment(String segment) { return "{\"ms\":{\"k\":[{\"n\":\"" + segment + "\"}],\"cn\":null},\"ls\":{\"k\":[],\"cn\":1702507130121}}"; } + /** + * Builds a memberships response with custom segments and change number. + * @param segments Array of segment names for my segments + * @param msCn Change number for my segments (null if not needed) + * @param largeSegments Array of segment names for large segments + * @param lsCn Change number for large segments + */ + public static String membershipsResponse(String[] segments, Long msCn, String[] largeSegments, Long lsCn) { + StringBuilder msSegments = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + if (i > 0) msSegments.append(","); + msSegments.append("{\"n\":\"").append(segments[i]).append("\"}"); + } + + StringBuilder lsSegments = new StringBuilder(); + for (int i = 0; i < largeSegments.length; i++) { + if (i > 0) lsSegments.append(","); + lsSegments.append("{\"n\":\"").append(largeSegments[i]).append("\"}"); + } + + return String.format("{\"ms\":{\"k\":[%s],\"cn\":%s},\"ls\":{\"k\":[%s],\"cn\":%d}}", + msSegments, msCn, lsSegments, lsCn); + } + + /** + * Simplified memberships response with only my segments. + */ + public static String membershipsResponse(String[] segments, long cn) { + return membershipsResponse(segments, cn, new String[]{}, cn); + } + + /** + * Builds a targeting rules changes response with a simple flag. + */ + public static String targetingRulesChangesWithFlag(String flagName, long till) { + return String.format("{\"ff\":{\"s\":%d,\"t\":%d,\"d\":[" + + "{\"trafficTypeName\":\"user\",\"name\":\"%s\",\"status\":\"ACTIVE\"," + + "\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":%d," + + "\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\"," + + "\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]}," + + "\"partitions\":[{\"treatment\":\"on\",\"size\":100}]}]}" + + "]},\"rbs\":{\"s\":%d,\"t\":%d,\"d\":[]}}", till, till, flagName, till, till, till); + } + + /** + * Builds a targeting rules changes response with both a flag and an RBS. + */ + public static String targetingRulesChangesWithFlagAndRbs(String flagName, String rbsName, long till) { + return String.format("{\"ff\":{\"s\":%d,\"t\":%d,\"d\":[" + + "{\"trafficTypeName\":\"user\",\"name\":\"%s\",\"status\":\"ACTIVE\"," + + "\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":%d," + + "\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\"," + + "\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]}," + + "\"partitions\":[{\"treatment\":\"on\",\"size\":100}]}]}" + + "]},\"rbs\":{\"s\":%d,\"t\":%d,\"d\":[" + + "{\"name\":\"%s\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\"," + + "\"excluded\":{\"keys\":[],\"segments\":[]}," + + "\"conditions\":[{\"matcherGroup\":{\"combiner\":\"AND\"," + + "\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]}}]}" + + "]}}", till, till, flagName, till, till, till, rbsName); + } + public static String dummyApiKey() { return "99049fd8653247c5ea42bc3c1ae2c6a42bc3"; } @@ -303,10 +371,7 @@ public static String splitChangeV2CompressionType1() { } public static String splitChangeV2CompressionType0() { - return splitChangeV2("9999999999999", - "1000", - "0", - "eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0="); + return splitChangeV2("9999999999999", "1000", "0", SPLIT_UPDATE_PAYLOAD_TYPE0); } public static String splitChangeV2(String changeNumber, String previousChangeNumber, String compressionType, String compressedPayload) { @@ -506,4 +571,46 @@ public static class ServicePath { public static final String IMPRESSIONS = "testImpressions/bulk"; public static final String AUTH = "v2/auth"; } + + /** + * Creates a simple split entity JSON body for database population. + */ + public static String splitEntityBody(String name, long changeNumber) { + return String.format("{\"name\":\"%s\", \"changeNumber\": %d}", name, changeNumber); + } + + /** + * Creates a segment list JSON for database population (my segments format). + * @param segments Array of segment names + */ + public static String segmentListJson(String... segments) { + StringBuilder sb = new StringBuilder("{\"k\":["); + for (int i = 0; i < segments.length; i++) { + if (i > 0) sb.append(","); + sb.append("{\"n\":\"").append(segments[i]).append("\"}"); + } + sb.append("],\"cn\":null}"); + return sb.toString(); + } + + public static String membershipKeyListUpdate(java.math.BigInteger hashedKey, String segmentName, long changeNumber) { + String keyListJson = "{\"a\":[" + hashedKey.toString() + "],\"r\":[]}"; + String encodedKeyList = Base64.encodeToString( + keyListJson.getBytes(java.nio.charset.StandardCharsets.UTF_8), + Base64.NO_WRAP); + + String notificationJson = "{" + + "\\\"type\\\":\\\"MEMBERSHIPS_MS_UPDATE\\\"," + + "\\\"cn\\\":" + changeNumber + "," + + "\\\"n\\\":[\\\"" + segmentName + "\\\"]," + + "\\\"c\\\":0," + + "\\\"u\\\":2," + + "\\\"d\\\":\\\"" + encodedKeyList + "\\\"" + + "}"; + + return "id: 1\n" + + "event: message\n" + + "data: {\"id\":\"m1\",\"clientId\":\"pri:test\",\"timestamp\":" + System.currentTimeMillis() + + ",\"encoding\":\"json\",\"channel\":\"test_channel\",\"data\":\"" + notificationJson + "\"}\n"; + } } diff --git a/src/androidTest/java/helper/SplitEventTaskHelper.java b/main/src/androidTest/java/helper/SplitEventTaskHelper.java similarity index 100% rename from src/androidTest/java/helper/SplitEventTaskHelper.java rename to main/src/androidTest/java/helper/SplitEventTaskHelper.java diff --git a/src/androidTest/java/helper/TestableSplitConfigBuilder.java b/main/src/androidTest/java/helper/TestableSplitConfigBuilder.java similarity index 100% rename from src/androidTest/java/helper/TestableSplitConfigBuilder.java rename to main/src/androidTest/java/helper/TestableSplitConfigBuilder.java diff --git a/src/androidTest/java/io/split/android/client/service/impressions/ImpressionPropertiesIntegrationTest.java b/main/src/androidTest/java/io/split/android/client/service/impressions/ImpressionPropertiesIntegrationTest.java similarity index 100% rename from src/androidTest/java/io/split/android/client/service/impressions/ImpressionPropertiesIntegrationTest.java rename to main/src/androidTest/java/io/split/android/client/service/impressions/ImpressionPropertiesIntegrationTest.java diff --git a/src/androidTest/java/io/split/android/client/service/impressions/ImpressionUtilsTest.java b/main/src/androidTest/java/io/split/android/client/service/impressions/ImpressionUtilsTest.java similarity index 100% rename from src/androidTest/java/io/split/android/client/service/impressions/ImpressionUtilsTest.java rename to main/src/androidTest/java/io/split/android/client/service/impressions/ImpressionUtilsTest.java diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java b/main/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java similarity index 100% rename from src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java rename to main/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java b/main/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java similarity index 100% rename from src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java rename to main/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java b/main/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java similarity index 100% rename from src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java rename to main/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/ListenableLruCacheTest.java b/main/src/androidTest/java/io/split/android/client/service/impressions/observer/ListenableLruCacheTest.java similarity index 100% rename from src/androidTest/java/io/split/android/client/service/impressions/observer/ListenableLruCacheTest.java rename to main/src/androidTest/java/io/split/android/client/service/impressions/observer/ListenableLruCacheTest.java diff --git a/src/androidTest/java/tests/database/AttributesDaoTest.java b/main/src/androidTest/java/tests/database/AttributesDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/AttributesDaoTest.java rename to main/src/androidTest/java/tests/database/AttributesDaoTest.java diff --git a/src/androidTest/java/tests/database/DatabaseInitializationTest.java b/main/src/androidTest/java/tests/database/DatabaseInitializationTest.java similarity index 100% rename from src/androidTest/java/tests/database/DatabaseInitializationTest.java rename to main/src/androidTest/java/tests/database/DatabaseInitializationTest.java diff --git a/src/androidTest/java/tests/database/GeneralInfoDaoTest.java b/main/src/androidTest/java/tests/database/GeneralInfoDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/GeneralInfoDaoTest.java rename to main/src/androidTest/java/tests/database/GeneralInfoDaoTest.java diff --git a/src/androidTest/java/tests/database/GenericDaoTest.java b/main/src/androidTest/java/tests/database/GenericDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/GenericDaoTest.java rename to main/src/androidTest/java/tests/database/GenericDaoTest.java diff --git a/src/androidTest/java/tests/database/ImpressionDaoTest.java b/main/src/androidTest/java/tests/database/ImpressionDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/ImpressionDaoTest.java rename to main/src/androidTest/java/tests/database/ImpressionDaoTest.java diff --git a/src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java b/main/src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java rename to main/src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java diff --git a/src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java b/main/src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java rename to main/src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java diff --git a/src/androidTest/java/tests/database/MySegmentDaoTest.java b/main/src/androidTest/java/tests/database/MySegmentDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/MySegmentDaoTest.java rename to main/src/androidTest/java/tests/database/MySegmentDaoTest.java diff --git a/src/androidTest/java/tests/database/RuleBasedSegmentDaoTest.java b/main/src/androidTest/java/tests/database/RuleBasedSegmentDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/RuleBasedSegmentDaoTest.java rename to main/src/androidTest/java/tests/database/RuleBasedSegmentDaoTest.java diff --git a/src/androidTest/java/tests/database/SplitDaoTest.java b/main/src/androidTest/java/tests/database/SplitDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/SplitDaoTest.java rename to main/src/androidTest/java/tests/database/SplitDaoTest.java diff --git a/src/androidTest/java/tests/database/TrackDaoTest.java b/main/src/androidTest/java/tests/database/TrackDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/TrackDaoTest.java rename to main/src/androidTest/java/tests/database/TrackDaoTest.java diff --git a/src/androidTest/java/tests/database/UniqueKeysDaoTest.java b/main/src/androidTest/java/tests/database/UniqueKeysDaoTest.java similarity index 100% rename from src/androidTest/java/tests/database/UniqueKeysDaoTest.java rename to main/src/androidTest/java/tests/database/UniqueKeysDaoTest.java diff --git a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java b/main/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java similarity index 100% rename from src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java rename to main/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java diff --git a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java b/main/src/androidTest/java/tests/integration/InitialChangeNumberTest.java similarity index 100% rename from src/androidTest/java/tests/integration/InitialChangeNumberTest.java rename to main/src/androidTest/java/tests/integration/InitialChangeNumberTest.java diff --git a/src/androidTest/java/tests/integration/IntegrationTest.java b/main/src/androidTest/java/tests/integration/IntegrationTest.java similarity index 100% rename from src/androidTest/java/tests/integration/IntegrationTest.java rename to main/src/androidTest/java/tests/integration/IntegrationTest.java diff --git a/src/androidTest/java/tests/integration/LifecyleOnBGTest.java b/main/src/androidTest/java/tests/integration/LifecyleOnBGTest.java similarity index 100% rename from src/androidTest/java/tests/integration/LifecyleOnBGTest.java rename to main/src/androidTest/java/tests/integration/LifecyleOnBGTest.java diff --git a/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java b/main/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java similarity index 100% rename from src/androidTest/java/tests/integration/MySegmentUpdatedTest.java rename to main/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java diff --git a/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java b/main/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java similarity index 100% rename from src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java rename to main/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java diff --git a/src/androidTest/java/tests/integration/ProxyFactoryTest.java b/main/src/androidTest/java/tests/integration/ProxyFactoryTest.java similarity index 100% rename from src/androidTest/java/tests/integration/ProxyFactoryTest.java rename to main/src/androidTest/java/tests/integration/ProxyFactoryTest.java diff --git a/src/androidTest/java/tests/integration/SingleSyncTest.java b/main/src/androidTest/java/tests/integration/SingleSyncTest.java similarity index 100% rename from src/androidTest/java/tests/integration/SingleSyncTest.java rename to main/src/androidTest/java/tests/integration/SingleSyncTest.java diff --git a/src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java b/main/src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java similarity index 100% rename from src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java rename to main/src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java diff --git a/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java b/main/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java similarity index 100% rename from src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java rename to main/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java diff --git a/src/androidTest/java/tests/integration/SplitChangesTest.java b/main/src/androidTest/java/tests/integration/SplitChangesTest.java similarity index 100% rename from src/androidTest/java/tests/integration/SplitChangesTest.java rename to main/src/androidTest/java/tests/integration/SplitChangesTest.java diff --git a/src/androidTest/java/tests/integration/SplitFactoryFreshInstallTest.java b/main/src/androidTest/java/tests/integration/SplitFactoryFreshInstallTest.java similarity index 100% rename from src/androidTest/java/tests/integration/SplitFactoryFreshInstallTest.java rename to main/src/androidTest/java/tests/integration/SplitFactoryFreshInstallTest.java diff --git a/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java b/main/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java similarity index 100% rename from src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java rename to main/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java diff --git a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java b/main/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java similarity index 100% rename from src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java rename to main/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java diff --git a/src/androidTest/java/tests/integration/TrackTest.java b/main/src/androidTest/java/tests/integration/TrackTest.java similarity index 100% rename from src/androidTest/java/tests/integration/TrackTest.java rename to main/src/androidTest/java/tests/integration/TrackTest.java diff --git a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java b/main/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java similarity index 100% rename from src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java rename to main/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java diff --git a/src/androidTest/java/tests/integration/encryption/EncryptionTest.java b/main/src/androidTest/java/tests/integration/encryption/EncryptionTest.java similarity index 100% rename from src/androidTest/java/tests/integration/encryption/EncryptionTest.java rename to main/src/androidTest/java/tests/integration/encryption/EncryptionTest.java diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java new file mode 100644 index 000000000..62f2dcdad --- /dev/null +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -0,0 +1,2026 @@ +package tests.integration.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import fake.HttpStreamResponseMock; +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.api.Key; +import io.split.android.client.events.SplitEventListener; +import io.split.android.client.events.SdkReadyMetadata; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; +import io.split.android.client.network.HttpMethod; +import io.split.android.client.storage.db.GeneralInfoEntity; +import io.split.android.client.storage.db.MySegmentEntity; +import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.logger.Logger; +import io.split.android.client.utils.logger.SplitLogLevel; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class SdkEventsIntegrationTest { + + private Context mContext; + private MockWebServer mWebServer; + private SplitRoomDatabase mDatabase; + private int mCurSplitReqId; + + private ServiceEndpoints endpoints() { + final String url = mWebServer.url("/").url().toString(); + return ServiceEndpoints.builder() + .apiEndpoint(url) + .eventsEndpoint(url) + .build(); + } + + private SplitClientConfig buildConfig() { + return SplitClientConfig.builder() + .serviceEndpoints(endpoints()) + .logLevel(SplitLogLevel.VERBOSE) + .ready(30000) + .featuresRefreshRate(999999) // High refresh rate to avoid periodic sync interfering + .segmentsRefreshRate(999999) + .impressionsRefreshRate(999999) + .syncEnabled(true) // Ensure sync is enabled + .trafficType("account") + .build(); + } + + private SplitFactory buildFactory(SplitClientConfig config) { + return IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), new Key("DEFAULT_KEY"), config, mContext, null, mDatabase, null); + } + + @Before + public void setup() { + mWebServer = new MockWebServer(); + mCurSplitReqId = 1003; + final Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); + } else if (path.contains("/splitChanges")) { + long id = mCurSplitReqId++; + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptyTargetingRulesChanges(id, id)); + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + mWebServer.setDispatcher(dispatcher); + try { + mWebServer.start(); + } catch (Exception e) { + throw new RuntimeException("Failed to start mock server", e); + } + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mDatabase = DatabaseHelper.getTestDatabase(mContext); + } + + @After + public void tearDown() throws Exception { + if (mWebServer != null) mWebServer.shutdown(); + if (mDatabase != null) { + mDatabase.close(); + } + } + + /** + * Scenario: sdkReadyFromCache fires when cache loading completes + *

+ * Given the SDK is starting with populated persistent storage + * And a handler H is registered for sdkReadyFromCache + * When internal events "splitsLoadedFromStorage", "mySegmentsLoadedFromStorage", + * "attributesLoadedFromStorage" and "encryptionMigrationDone" are notified + * Then sdkReadyFromCache is emitted exactly once + * And handler H is invoked once + * And the metadata contains "initialCacheLoad" with value false + * And the metadata contains "lastUpdateTimestamp" with a valid timestamp + */ + @Test + public void sdkReadyFromCacheFiresWhenCacheLoadingCompletes() throws Exception { + // Given: SDK is starting with populated persistent storage + populateDatabaseWithCacheData(System.currentTimeMillis()); + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + // And: a handler H is registered for sdkReadyFromCache + EventCapture capture = captureCacheReadyEvent(client); + + // Then: sdkReadyFromCache is emitted exactly once + awaitEvent(capture.latch, "SDK_READY_FROM_CACHE"); + assertFiredOnce(capture.count, "SDK_READY_FROM_CACHE handler"); + + // And: the metadata contains "initialCacheLoad" with value false + assertNotNull("Metadata should not be null", capture.metadata.get()); + assertNotNull("initialCacheLoad should not be null", capture.metadata.get().isInitialCacheLoad()); + assertFalse("initialCacheLoad should be false for cache path", capture.metadata.get().isInitialCacheLoad()); + + // And: the metadata contains "lastUpdateTimestamp" with a valid timestamp + assertNotNull("lastUpdateTimestamp should not be null", capture.metadata.get().getLastUpdateTimestamp()); + assertTrue("lastUpdateTimestamp should be valid", capture.metadata.get().getLastUpdateTimestamp() > 0); + + factory.destroy(); + } + + /** + * Scenario: sdkReadyFromCache fires when sync completes (fresh install path) + *

+ * Given the SDK is starting without persistent storage (fresh install) + * And a handler H is registered for sdkReadyFromCache + * When internal events "targetingRulesSyncComplete" and "membershipsSyncComplete" are notified + * Then sdkReadyFromCache is emitted exactly once + * And handler H is invoked once + * And the metadata contains "initialCacheLoad" with value true + */ + @Test + public void sdkReadyFromCacheFiresWhenSyncCompletesFreshInstallPath() throws Exception { + // Given: SDK is starting without persistent storage (fresh install) + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + // And: a handler H is registered for sdkReadyFromCache + EventCapture capture = captureCacheReadyEvent(client); + + // Then: sdkReadyFromCache is emitted exactly once + awaitEvent(capture.latch, "SDK_READY_FROM_CACHE"); + assertFiredOnce(capture.count, "SDK_READY_FROM_CACHE handler"); + + // And: the metadata contains "initialCacheLoad" with value true + assertNotNull("Metadata should not be null", capture.metadata.get()); + assertNotNull("initialCacheLoad should not be null", capture.metadata.get().isInitialCacheLoad()); + assertTrue("initialCacheLoad should be true for sync path (fresh install)", capture.metadata.get().isInitialCacheLoad()); + + factory.destroy(); + } + + /** + * Scenario: onReady listener fires when SDK_READY event occurs + *

+ * Given the SDK is starting with populated persistent storage + * And a handler H is registered using addEventListener with onReady + * When SDK_READY fires + * Then onReady is invoked exactly once + * And the handler receives the SplitClient and SdkReadyMetadata + * And the metadata contains "initialCacheLoad" with value false + * And the metadata contains "lastUpdateTimestamp" with a valid timestamp + */ + @Test + public void sdkReadyListenerFiresWithMetadata() throws Exception { + // Given: SDK is starting with populated persistent storage + populateDatabaseWithCacheData(System.currentTimeMillis()); + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + // And: a handler H is registered using addEventListener with onReady + EventCapture capture = captureReadyEvent(client); + + // Then: onReady is invoked exactly once + awaitEvent(capture.latch, "onReady", 30); + assertFiredOnce(capture.count, "onReady"); + + // And: the metadata contains "initialCacheLoad" with value false + assertNotNull("Received metadata should not be null", capture.metadata.get()); + assertNotNull("initialCacheLoad should not be null", capture.metadata.get().isInitialCacheLoad()); + assertFalse("initialCacheLoad should be false for cache path", capture.metadata.get().isInitialCacheLoad()); + + // And: the metadata contains "lastUpdateTimestamp" with a valid timestamp + assertNotNull("lastUpdateTimestamp should not be null", capture.metadata.get().getLastUpdateTimestamp()); + assertTrue("lastUpdateTimestamp should be valid", capture.metadata.get().getLastUpdateTimestamp() > 0); + + factory.destroy(); + } + + /** + * Scenario: sdkReady metadata should be preserved for late-registered clients (warm cache) + *

+ * Given the SDK is starting with populated persistent storage + * And client1 has already emitted SDK_READY + * When client2 is created and receives SDK_READY (replay) + * Then the metadata should not be null and should reflect cache path values + */ + @Test + public void sdkReadyMetadataNotNullWhenMembershipsCompletesLast() throws Exception { + populateDatabaseWithCacheData(System.currentTimeMillis()); + SplitFactory factory = buildFactory(buildConfig()); + + SplitClient client1 = factory.client(new Key("key_1")); + waitForReady(client1); + + SplitClient client2 = factory.client(new Key("key_2")); + EventCapture capture = captureReadyEvent(client2); + awaitEvent(capture.latch, "Client2 SDK_READY"); + + assertNotNull("Metadata should not be null", capture.metadata.get()); + assertNotNull("initialCacheLoad should not be null", capture.metadata.get().isInitialCacheLoad()); + assertFalse("initialCacheLoad should be false for cache path", capture.metadata.get().isInitialCacheLoad()); + assertNotNull("lastUpdateTimestamp should not be null", capture.metadata.get().getLastUpdateTimestamp()); + + factory.destroy(); + } + + /** + * Scenario: onReady listener replays to late subscribers + *

+ * Given sdkReady has already been emitted + * When a new handler H is registered using addEventListener with onReady + * Then onReady handler H is invoked exactly once immediately (replay) + */ + @Test + public void sdkReadyListenerReplaysToLateSubscribers() throws Exception { + // Given: sdkReady has already been emitted + TestClientFixture fixture = createClientAndWaitForReady(new Key("key_1")); + + // When: a new handler H is registered for onReady after SDK_READY has fired + EventCapture capture = captureReadyEvent(fixture.client); + + // Then: onReady handler H is invoked exactly once immediately (replay) + awaitEvent(capture.latch, "Late onReady handler replay", 5); + assertFiredOnce(capture.count, "Late onReady handler"); + assertNotNull("Metadata should not be null on replay", capture.metadata.get()); + + // And: onReady is not emitted again (verify no additional invocations) + Thread.sleep(500); + assertFiredOnce(capture.count, "Late handler"); + + fixture.destroy(); + } + + /** + * Scenario: onReadyView is invoked on main thread when SDK_READY fires + *

+ * Given the SDK is starting + * And a handler H is registered using addEventListener with onReadyView + * When SDK_READY fires + * Then onReadyView is invoked on the main/UI thread + */ + @Test + public void sdkReadyViewListenerFiresOnMainThread() throws Exception { + // Given: SDK is starting with populated persistent storage + populateDatabaseWithCacheData(System.currentTimeMillis()); + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + // And: a handler H is registered using addEventListener with onReadyView + EventCapture capture = new EventCapture<>(); + client.addEventListener(new SplitEventListener() { + @Override + public void onReadyView(SplitClient c, SdkReadyMetadata metadata) { + capture.capture(metadata); + } + }); + + // Then: onReadyView is invoked + awaitEvent(capture.latch, "onReadyView"); + assertFiredOnce(capture.count, "onReadyView"); + + factory.destroy(); + } + + /** + * Scenario: sdkReady fires after sdkReadyFromCache and requires sync completion + *

+ * Given the SDK has not yet emitted sdkReady + * And a handler HReady is registered for sdkReady + * And a handler HCache is registered for sdkReadyFromCache + * When internal events "splitsLoadedFromStorage", "mySegmentsLoadedFromStorage", + * "attributesLoadedFromStorage" and "encryptionMigrationDone" are notified + * Then sdkReadyFromCache is emitted + * And handler HCache is invoked once + * But sdkReady is not emitted yet because sync has not completed + * When internal events "targetingRulesSyncComplete" and "membershipsSyncComplete" are notified + * Then sdkReady is emitted exactly once + * And handler HReady is invoked once + */ + @Test + public void sdkReadyFiresAfterSdkReadyFromCacheAndRequiresSyncCompletion() throws Exception { + // Given: SDK has not yet emitted sdkReady (fresh install) + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + // And: handlers are registered to catch all events + EventCapture cacheCapture = captureCacheReadyEvent(client); + CountDownLatch readyLatch = captureLegacyReadyEvent(client); + + // Wait for SDK_READY_FROM_CACHE first + awaitEvent(cacheCapture.latch, "SDK_READY_FROM_CACHE"); + assertFiredOnce(cacheCapture.count, "Cache handler"); + + // Wait for SDK_READY to fire + awaitEvent(readyLatch, "SDK_READY"); + + factory.destroy(); + } + + /** + * Scenario: sdkReady replays to late subscribers + *

+ * Given sdkReady has already been emitted + * When a new handler H is registered for sdkReady + * Then handler H is invoked exactly once immediately (replay) + * And sdkReady is not emitted again + */ + @Test + public void sdkReadyReplaysToLateSubscribers() throws Exception { + // Given: sdkReady has already been emitted + TestClientFixture fixture = createClientAndWaitForReady(new Key("key_1")); + + // When: a new handler H is registered for sdkReady + EventCapture capture = captureReadyEvent(fixture.client); + + // Then: handler H is invoked exactly once immediately (replay) + awaitEvent(capture.latch, "Late handler replay", 5); + assertFiredOnce(capture.count, "Late handler"); + + // And: sdkReady is not emitted again (verify no additional invocations) + Thread.sleep(500); + assertFiredOnce(capture.count, "Late handler"); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdate is emitted only after sdkReady + *

+ * Given a handler H is registered for sdkUpdate + * And the SDK has not yet emitted sdkReady + * When an internal "splitsUpdated" event is notified during initial sync + * Then sdkUpdate is not emitted because sdkReady has not fired yet + * When internal events for sdkReadyFromCache and sdkReady are notified and both fire + * When a new "splitsUpdated" event is notified via SSE + * Then sdkUpdate is emitted + * And handler H is invoked once with metadata + */ + @Test + public void sdkUpdateEmittedOnlyAfterSdkReady() throws Exception { + // Given: Create streaming client but don't wait for SDK_READY + TestClientFixture fixture = createStreamingClient(new Key("key_1")); + + // Register handlers BEFORE SDK_READY fires + EventCapture updateCapture = captureUpdateEvent(fixture.client); + CountDownLatch readyLatch = captureLegacyReadyEvent(fixture.client); + + // Wait a bit to see if SDK_UPDATE fires prematurely (during initial sync) + Thread.sleep(1000); + + // Then: sdkUpdate is not emitted because sdkReady has not fired yet + assertEquals("SDK_UPDATE should not fire before SDK_READY", 0, updateCapture.count.get()); + + // When: SDK_READY fires + awaitEvent(readyLatch, "SDK_READY"); + fixture.waitForSseConnection(); + + // When: a new "splitsUpdated" event is notified via SSE (after SDK_READY has fired) + fixture.pushSplitUpdate("2000", "1000"); + + // Then: sdkUpdate is emitted and handler H is invoked once + awaitEvent(updateCapture.latch, "SDK_UPDATE"); + assertFiredOnce(updateCapture.count, "SDK_UPDATE handler"); + assertNotNull("Metadata should not be null", updateCapture.metadata.get()); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdate fires on any data change event after sdkReady + *

+ * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a split update notification arrives via SSE + * Then sdkUpdate is emitted and handler H is invoked + */ + @Test + public void sdkUpdateFiresOnAnyDataChangeEventAfterSdkReady() throws Exception { + // Given: sdkReady has already been emitted (with streaming support) + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + EventCapture capture = captureUpdateEvent(fixture.client); + + // When: a split update notification arrives via SSE + fixture.pushSplitUpdate(); + + // Then: sdkUpdate is emitted and handler H is invoked + awaitEvent(capture.latch, "SDK_UPDATE"); + assertFiredOnce(capture.count, "SDK_UPDATE handler"); + assertNotNull("Metadata should not be null", capture.metadata.get()); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdate does not replay to late subscribers + *

+ * Given sdkReady has already been emitted + * And a handler H1 is registered for sdkUpdate + * When an internal "splitsUpdated" event is notified via SSE + * Then sdkUpdate is emitted + * And handler H1 is invoked once + * When a second handler H2 is registered for sdkUpdate after one sdkUpdate has already fired + * Then H2 does not receive a replay for past sdkUpdate events + * When another internal "splitsUpdated" event is notified + * Then both H1 and H2 are invoked once for that second sdkUpdate + */ + @Test + public void sdkUpdateDoesNotReplayToLateSubscribers() throws Exception { + // Given: sdkReady has already been emitted (with streaming support) + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + AtomicInteger handler1Count = new AtomicInteger(0); + CountDownLatch firstUpdateLatch = new CountDownLatch(1); + AtomicReference secondUpdateLatchRef = new AtomicReference<>(null); + + // And: a handler H1 is registered for sdkUpdate + fixture.client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + handler1Count.incrementAndGet(); + firstUpdateLatch.countDown(); + CountDownLatch secondLatch = secondUpdateLatchRef.get(); + if (secondLatch != null) { + secondLatch.countDown(); + } + } + }); + + // When: an internal "splitsUpdated" event is notified via SSE + fixture.pushSplitUpdate("2000", "1000"); + + // Then: sdkUpdate is emitted and handler H1 is invoked once + awaitEvent(firstUpdateLatch, "SDK_UPDATE for H1"); + assertFiredOnce(handler1Count, "H1"); + + // Wait to ensure first update is fully processed + Thread.sleep(1000); + + // When: a second handler H2 is registered for sdkUpdate after one sdkUpdate has already fired + CountDownLatch secondUpdateLatch = new CountDownLatch(2); + secondUpdateLatchRef.set(secondUpdateLatch); + + EventCapture handler2Capture = captureUpdateEvent(fixture.client); + + // Then: H2 does not receive a replay for past sdkUpdate events + Thread.sleep(500); + assertEquals("H2 should not receive replay", 0, handler2Capture.count.get()); + + // Ensure handlers are registered before pushing second update + Thread.sleep(500); + if (fixture.streamingData != null) { + TestingHelper.pushKeepAlive(fixture.streamingData); + } + + // When: another internal "splitsUpdated" event is notified + fixture.pushSplitUpdate("2001", "2000"); + + // Then: both H1 and H2 are invoked for that second sdkUpdate + awaitEvent(secondUpdateLatch, "Second SDK_UPDATE", 15); + + // H1 should now have 2 total invocations (1 from first + 1 from second) + assertFiredTimes(handler1Count, "H1", 2); + // H2 should have 1 invocation (only from second update, no replay) + assertFiredOnce(handler2Capture.count, "H2"); + + fixture.destroy(); + } + + /** + * Scenario: sdkReadyTimedOut is emitted when readiness timeout elapses + *

+ * Given a handler Htimeout is registered for sdkReadyTimedOut + * And a handler Hready is registered for sdkReady + * And the readiness timeout is configured to T seconds + * When the timeout T elapses without sdkReady firing + * Then the internal "sdkReadyTimeoutReached" event is notified + * And sdkReadyTimedOut is emitted exactly once + * And handler Htimeout is invoked once + * And sdkReady is not emitted + */ + @Test + public void sdkReadyTimedOutEmittedWhenReadinessTimeoutElapses() throws Exception { + // Given: the readiness timeout is configured to a short timeout (2 seconds) + SplitClientConfig config = SplitClientConfig.builder() + .serviceEndpoints(endpoints()) + .ready(2000) + .featuresRefreshRate(999999) + .segmentsRefreshRate(999999) + .impressionsRefreshRate(999999) + .syncEnabled(true) + .trafficType("account") + .build(); + + // Set up mock server to delay responses so sync doesn't complete before timeout + mWebServer.setDispatcher(createDelayedDispatcher(5)); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(new Key("key_1")); + + EventCapture timeoutCapture = new EventCapture<>(); + AtomicInteger readyCount = new AtomicInteger(0); + + client.on(SplitEvent.SDK_READY_TIMED_OUT, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + timeoutCapture.increment(); + } + }); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + readyCount.incrementAndGet(); + } + }); + + // Then: sdkReadyTimedOut is emitted exactly once + awaitEvent(timeoutCapture.latch, "SDK_READY_TIMED_OUT", 5); + assertFiredOnce(timeoutCapture.count, "Timeout handler"); + + // And: sdkReady is not emitted (sync didn't complete in time) + Thread.sleep(500); + assertEquals("SDK_READY should not fire before timeout", 0, readyCount.get()); + + factory.destroy(); + } + + /** + * Scenario: sdkReadyTimedOut is suppressed when sdkReady fires before timeout + *

+ * Given a handler Htimeout is registered for sdkReadyTimedOut + * And a handler Hready is registered for sdkReady + * And the readiness timeout is configured to T seconds + * When internal events for sdkReadyFromCache and sdkReady complete before the timeout elapses + * Then sdkReady is emitted + * And sdkReadyTimedOut is not emitted + * When the internal "sdkReadyTimeoutReached" event is notified after sdkReady has fired + * Then sdkReadyTimedOut is still not emitted (suppressed by sdkReady) + */ + @Test + public void sdkReadyTimedOutSuppressedWhenSdkReadyFiresBeforeTimeout() throws Exception { + // Given: the readiness timeout is configured to a longer timeout (10 seconds) + SplitClientConfig config = SplitClientConfig.builder() + .serviceEndpoints(endpoints()) + .ready(10000) + .featuresRefreshRate(999999) + .segmentsRefreshRate(999999) + .impressionsRefreshRate(999999) + .syncEnabled(true) + .trafficType("account") + .build(); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(new Key("key_1")); + + AtomicInteger timeoutCount = new AtomicInteger(0); + client.on(SplitEvent.SDK_READY_TIMED_OUT, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + timeoutCount.incrementAndGet(); + } + }); + + EventCapture readyCapture = captureReadyEvent(client); + + // Then: sdkReady is emitted + awaitEvent(readyCapture.latch, "SDK_READY"); + assertFiredOnce(readyCapture.count, "Ready handler"); + + // And: sdkReadyTimedOut is not emitted + Thread.sleep(2000); + assertEquals("SDK_READY_TIMED_OUT should not fire (suppressed)", 0, timeoutCount.get()); + + factory.destroy(); + } + + /** + * Scenario: Sync completion does not trigger sdkUpdate during initial sync + *

+ * Given a handler HUpdate is registered for sdkUpdate + * And a handler HReady is registered for sdkReady + * And the SDK is performing initial sync + * When internal events "splitsUpdated" and "ruleBasedSegmentsUpdated" are notified (data changed during sync) + * And then "targetingRulesSyncComplete" and "membershipsSyncComplete" are notified + * Then sdkReadyFromCache is emitted (via sync path) + * And sdkReady is emitted + * But sdkUpdate is NOT emitted because the *_UPDATED events were notified before sdkReady fired + */ + @Test + public void syncCompletionDoesNotTriggerSdkUpdateDuringInitialSync() throws Exception { + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + EventCapture updateCapture = captureUpdateEvent(client); + CountDownLatch readyLatch = captureLegacyReadyEvent(client); + + // When: sync completes (happens automatically during initialization) + awaitEvent(readyLatch, "SDK_READY"); + + // Then: sdkUpdate is NOT emitted because the *_UPDATED events were notified before sdkReady fired + Thread.sleep(1000); + assertEquals("SDK_UPDATE should not fire during initial sync", 0, updateCapture.count.get()); + + factory.destroy(); + } + + /** + * Scenario: Handlers for a single event are invoked sequentially and errors are isolated + *

+ * Given three handlers H1, H2 and H3 are registered for sdkUpdate + * And H2 throws an exception when invoked + * And sdkReady has already been emitted + * When an internal "splitsUpdated" event is notified via SSE + * Then sdkUpdate is emitted once + * And all handlers are invoked sequentially (one at a time, not concurrently) + * And H2's exception is caught by delivery and doesn't crash the SDK + * And H3 is invoked even though H2 threw an exception (error isolation) + * And the SDK process does not crash + */ + @Test + public void handlersInvokedSequentiallyErrorsIsolated() throws Exception { + // Given: sdkReady has already been emitted (with streaming support) + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + AtomicInteger handler1Count = new AtomicInteger(0); + AtomicInteger handler2Count = new AtomicInteger(0); + AtomicInteger handler3Count = new AtomicInteger(0); + AtomicInteger handler1Order = new AtomicInteger(0); + AtomicInteger handler2Order = new AtomicInteger(0); + AtomicInteger handler3Order = new AtomicInteger(0); + AtomicInteger orderCounter = new AtomicInteger(0); + CountDownLatch updateLatch = new CountDownLatch(3); + + // Given: three handlers H1, H2 and H3 are registered for sdkUpdate in that order + // And: H2 throws an exception when invoked + fixture.client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + handler1Count.incrementAndGet(); + handler1Order.set(orderCounter.incrementAndGet()); + updateLatch.countDown(); + } + }); + + fixture.client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + handler2Count.incrementAndGet(); + handler2Order.set(orderCounter.incrementAndGet()); + updateLatch.countDown(); + throw new RuntimeException("Handler H2 exception"); + } + }); + + fixture.client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + handler3Count.incrementAndGet(); + handler3Order.set(orderCounter.incrementAndGet()); + updateLatch.countDown(); + } + }); + + // When: an internal "splitsUpdated" event is notified via SSE + fixture.pushSplitUpdate(); + + // Then: all three handlers are invoked + boolean allHandlersFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("All handlers should be invoked", allHandlersFired); + + // Verify all handlers were invoked exactly once + assertEquals("Handler H1 should be invoked once", 1, handler1Count.get()); + assertEquals("Handler H2 should be invoked once", 1, handler2Count.get()); + assertEquals("Handler H3 should be invoked once despite H2 throwing", 1, handler3Count.get()); + + // Verify handlers were invoked sequentially (orderCounter should be 1, 2, 3) + // Note: We don't check which handler got which order number because handlers + // are stored in a HashSet which doesn't guarantee iteration order. + // The important thing is that all handlers were invoked and H3 was invoked + // even though H2 threw an exception (error isolation). + assertTrue("All handlers should have been assigned order numbers", + handler1Order.get() > 0 && handler2Order.get() > 0 && handler3Order.get() > 0); + assertEquals("Order counter should be 3 (one for each handler)", 3, orderCounter.get()); + + // Verify error isolation: H3 was invoked even though H2 threw an exception + // This is the key assertion - that errors don't prevent subsequent handlers from executing + assertTrue("H3 should be invoked even if H2 throws (error isolation)", handler3Count.get() == 1); + + fixture.destroy(); + } + + /** + * Scenario: Metadata is correctly propagated to handlers + *

+ * Given a handler H is registered for sdkUpdate which inspects the received metadata + * And sdkReady has already been emitted + * When an internal "splitsUpdated" event is notified via SSE + * Then sdkUpdate is emitted + * And handler H is invoked once + * And handler H receives metadata (may contain updatedFlags depending on notification type) + */ + @Test + public void metadataCorrectlyPropagatedToHandlers() throws Exception { + // Given: sdkReady has already been emitted (with streaming support) + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + EventCapture capture = captureUpdateEvent(fixture.client); + + // When: an internal "splitsUpdated" event is notified via SSE + fixture.pushSplitUpdate(); + + // Then: sdkUpdate is emitted and handler H is invoked once + awaitEvent(capture.latch, "SDK_UPDATE"); + assertFiredOnce(capture.count, "SDK_UPDATE handler"); + assertNotNull("Metadata should not be null", capture.metadata.get()); + + fixture.destroy(); + } + + /** + * Scenario: Destroying a client stops events and clears handlers + *

+ * Given a SplitClient with an EventsManager and a handler H registered for sdkUpdate + * And sdkReady has already been emitted + * When the client is destroyed + * And an internal "splitsUpdated" event is notified via SSE + * Then handler H is never invoked (handlers were cleared on destroy) + * When registering a new handler H2 for sdkUpdate after destroy + * Then the registration is a no-op + * And H2 is never invoked even when another update is pushed + */ + @Test + public void destroyingClientStopsEventsAndClearsHandlers() throws Exception { + // Given: sdkReady has already been emitted (with streaming support) + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + EventCapture handler1 = captureUpdateEvent(fixture.client); + + // When: the client is destroyed + fixture.client.destroy(); + fixture.pushSplitUpdate("3000", "2000"); + + // Handler H is never invoked (handlers were cleared on destroy) + Thread.sleep(1000); + assertEquals("Handler H1 should not be invoked after destroy", 0, handler1.count.get()); + + // When: registering a new handler H2 for sdkUpdate after destroy + EventCapture handler2 = captureUpdateEvent(fixture.client); + fixture.pushSplitUpdate("4000", "3000"); + + Thread.sleep(1000); + assertEquals("Handler H1 should still be 0", 0, handler1.count.get()); + assertEquals("Handler H2 should not be invoked after destroy", 0, handler2.count.get()); + + fixture.destroy(); + } + + /** + * Scenario: SDK-scoped internal events fan out to multiple clients + *

+ * Given a factory with two clients ClientA and ClientB + * And each client has its own EventsManager instance registered with EventsManagerCoordinator + * And handlers HA and HB are registered for sdkUpdate on ClientA and ClientB respectively + * And both clients have already emitted sdkReady + * When a SDK-scoped internal "splitsUpdated" event is notified via SSE + * Then sdkUpdate is emitted once per client + * And handler HA is invoked once + * And handler HB is invoked once + */ + @Test + public void sdkScopedEventsFanOutToMultipleClients() throws Exception { + // Given: a factory with two clients (with streaming support) + TwoClientFixture fixture = createTwoStreamingClientsAndWaitForReady(new Key("key_A"), new Key("key_B")); + + EventCapture captureA = captureUpdateEvent(fixture.mClientA); + EventCapture captureB = captureUpdateEvent(fixture.mClientB); + + // When: a SDK-scoped internal "splitsUpdated" event is notified via SSE + fixture.pushSplitUpdate(); + + // Then: sdkUpdate is emitted once per client + awaitEvent(captureA.latch, "SDK_UPDATE for ClientA"); + awaitEvent(captureB.latch, "SDK_UPDATE for ClientB"); + assertFiredOnce(captureA.count, "Handler A"); + assertFiredOnce(captureB.count, "Handler B"); + + fixture.destroy(); + } + + /** + * Scenario: SDK-scoped events (splitsUpdated) fan out to all clients + *

+ * This test verifies that when a split update notification arrives via SSE, + * the SDK_UPDATE event is emitted to all clients in the factory. + *

+ * Note: True client-scoped events like mySegmentsUpdated require specific streaming + * notifications targeted at individual user keys. This test demonstrates the difference + * by showing that SDK-scoped split updates affect all clients equally. + */ + @Test + public void clientScopedEventsDoNotFanOutToOtherClients() throws Exception { + // Given: a factory with two clients (with streaming support) + TwoClientFixture fixture = createTwoStreamingClientsAndWaitForReady(new Key("userA"), new Key("userB")); + + EventCapture captureA = captureUpdateEvent(fixture.mClientA); + EventCapture captureB = captureUpdateEvent(fixture.mClientB); + + // When: a SDK-scoped split update notification arrives (affects all clients) + fixture.pushSplitUpdate(); + + // Then: both clients receive SDK_UPDATE since splitsUpdated is SDK-scoped + awaitEvent(captureA.latch, "SDK_UPDATE for ClientA"); + awaitEvent(captureB.latch, "SDK_UPDATE for ClientB"); + assertFiredOnce(captureA.count, "Handler A"); + assertFiredOnce(captureB.count, "Handler B"); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains Type.FLAGS_UPDATE for flags update + *

+ * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a split update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.FLAGS_UPDATE + * And handler H receives metadata with getNames() containing the updated flag names + */ + @Test + public void sdkUpdateMetadataContainsTypeForFlagsUpdate() throws Exception { + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + EventCapture capture = captureUpdateEvent(fixture.client); + + fixture.pushSplitUpdate(); + + awaitEvent(capture.latch, "SDK_UPDATE"); + assertNotNull("Metadata should not be null", capture.metadata.get()); + assertEquals("Type should be FLAGS_UPDATE", + SdkUpdateMetadata.Type.FLAGS_UPDATE, capture.metadata.get().getType()); + assertNotNull("Names should not be null", capture.metadata.get().getNames()); + assertFalse("Names should not be empty", capture.metadata.get().getNames().isEmpty()); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for rule-based segments update + *

+ * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a rule-based segment update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE + * And handler H receives metadata with getNames() returning an empty list + *

+ * Note: SEGMENTS_UPDATE always has empty names (segment names are not included). + */ + @Test + public void sdkUpdateMetadataContainsTypeForSegmentsUpdate() throws Exception { + TestClientFixture fixture = createStreamingClientWithRbsAndWaitForReady(new Key("key_1")); + + EventCapture capture = captureUpdateEvent(fixture.client); + + fixture.pushRbsUpdate(); + + awaitEvent(capture.latch, "SDK_UPDATE for RBS"); + assertNotNull("Metadata should not be null", capture.metadata.get()); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, capture.metadata.get().getType()); + assertNotNull("Names should not be null", capture.metadata.get().getNames()); + assertTrue("Names should be empty for SEGMENTS_UPDATE", capture.metadata.get().getNames().isEmpty()); + + fixture.destroy(); + } + + /** + * Scenario: Only FLAGS_UPDATE fires when both flags and RBS change together + *

+ * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a polling sync returns changes to both flags AND rule-based segments + * Then only ONE sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.FLAGS_UPDATE + * And SEGMENTS_UPDATE is NOT fired (RBS changes are subsumed by FLAGS_UPDATE) + */ + @Test + public void sdkUpdateFiresOnlyOnceWhenBothFlagsAndRbsChange() throws Exception { + AtomicInteger splitChangesHitCount = new AtomicInteger(0); + + mWebServer.setDispatcher(createPollingDispatcher( + count -> count <= 1 + ? IntegrationHelper.emptyTargetingRulesChanges(1000, 1000) + : IntegrationHelper.targetingRulesChangesWithFlagAndRbs("test_split", "test_rbs", 2000), + count -> IntegrationHelper.dummyAllSegments() + )); + + // Use polling mode with short refresh rate to trigger sync quickly + SplitClientConfig config = new TestableSplitConfigBuilder() + .serviceEndpoints(endpoints()) + .ready(30000) + .featuresRefreshRate(3) // Poll every 3 seconds + .segmentsRefreshRate(999999) + .impressionsRefreshRate(999999) + .streamingEnabled(false) + .trafficType("account") + .build(); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(); + + // Wait for SDK_READY + CountDownLatch readyLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + readyLatch.countDown(); + } + }); + assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); + + // Register handler to count SDK_UPDATE events and capture metadata + List receivedMetadataList = new ArrayList<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { + synchronized (receivedMetadataList) { + receivedMetadataList.add(metadata); + } + updateLatch.countDown(); + } + }); + + // Wait for SDK_UPDATE (triggered by polling that returns both flag and RBS changes) + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire", updateFired); + + // Wait a bit to ensure no additional events fire + Thread.sleep(1000); + + // Verify only ONE SDK_UPDATE was fired + synchronized (receivedMetadataList) { + assertEquals("Should receive exactly 1 SDK_UPDATE event (not 2)", 1, receivedMetadataList.size()); + + // Verify it's FLAGS_UPDATE (not SEGMENTS_UPDATE) + SdkUpdateMetadata metadata = receivedMetadataList.get(0); + assertNotNull("Metadata should not be null", metadata); + assertEquals("Type should be FLAGS_UPDATE (not SEGMENTS_UPDATE)", + SdkUpdateMetadata.Type.FLAGS_UPDATE, metadata.getType()); + } + + factory.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for membership segments update (polling) + *

+ * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When segments change via polling (server returns different segments) + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE + * And handler H receives metadata with getNames() returning an empty list + */ + @Test + public void sdkUpdateMetadataContainsTypeForMembershipSegmentsUpdate() throws Exception { + verifySdkUpdateForSegmentsPollingWithEmptyNames( + IntegrationHelper.membershipsResponse(new String[]{"segment1", "segment2"}, 1000), + IntegrationHelper.membershipsResponse(new String[]{"segment2", "segment3"}, 2000) + ); + } + + /** + * Scenario: sdkUpdateMetadata includes flag names for polling flag updates + *

+ * Given sdkReady has already been emitted in polling mode + * When polling returns a flag update + * Then sdkUpdate metadata contains FLAGS_UPDATE with non-empty names + */ + @Test + public void sdkUpdateMetadataContainsNamesForPollingFlagsUpdate() throws Exception { + mWebServer.setDispatcher(createPollingDispatcher( + count -> count <= 1 + ? IntegrationHelper.emptyTargetingRulesChanges(1000, 1000) + : IntegrationHelper.targetingRulesChangesWithFlag("polling_flag", 2000), + count -> IntegrationHelper.dummyAllSegments() + )); + + SplitClientConfig config = new TestableSplitConfigBuilder() + .serviceEndpoints(endpoints()) + .ready(30000) + .featuresRefreshRate(3) + .segmentsRefreshRate(999999) + .impressionsRefreshRate(999999) + .streamingEnabled(false) + .trafficType("account") + .build(); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(); + + CountDownLatch readyLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + readyLatch.countDown(); + } + }); + assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); + + AtomicReference receivedMetadata = new AtomicReference<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + assertTrue("SDK_UPDATE should fire", updateLatch.await(15, TimeUnit.SECONDS)); + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertEquals("Type should be FLAGS_UPDATE", + SdkUpdateMetadata.Type.FLAGS_UPDATE, receivedMetadata.get().getType()); + assertNotNull("Names should not be null", receivedMetadata.get().getNames()); + assertTrue("Names should include polling_flag", receivedMetadata.get().getNames().contains("polling_flag")); + + factory.destroy(); + } + + /** + * Scenario: sdkReady should include non-null metadata on fresh install + *

+ * Given the SDK starts with empty storage (fresh install) + * When SDK_READY fires + * Then metadata should be present (initialCacheLoad=true, lastUpdateTimestamp=null) + */ + @Test + public void sdkReadyMetadataNotNullOnFreshInstall() throws Exception { + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + EventCapture capture = captureReadyEvent(client); + + awaitEvent(capture.latch, "SDK_READY"); + assertNotNull("Metadata should not be null", capture.metadata.get()); + assertNotNull("initialCacheLoad should not be null", capture.metadata.get().isInitialCacheLoad()); + assertTrue("initialCacheLoad should be true for fresh install", capture.metadata.get().isInitialCacheLoad()); + assertEquals("lastUpdateTimestamp should be null for fresh install", + null, capture.metadata.get().getLastUpdateTimestamp()); + + factory.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata should include SEGMENTS_UPDATE when only one client changes (polling) + *

+ * Given two clients are created in polling mode + * And only client1 receives a membership change on polling + * When polling updates occur + * Then only client1 receives SDK_UPDATE with SEGMENTS_UPDATE metadata + */ + @Test + public void sdkUpdateMetadataForSingleClientMembershipPolling() throws Exception { + AtomicInteger key1MembershipHits = new AtomicInteger(0); + final String initialMemberships = IntegrationHelper.membershipsResponse(new String[]{"segment1"}, 1000); + final String updatedMemberships = IntegrationHelper.membershipsResponse(new String[]{"segment2"}, 2000); + + mWebServer.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + if (path.contains("key_1")) { + int count = key1MembershipHits.incrementAndGet(); + return new MockResponse().setResponseCode(200) + .setBody(count <= 1 ? initialMemberships : updatedMemberships); + } + return new MockResponse().setResponseCode(200).setBody(initialMemberships); + } else if (path.contains("/splitChanges")) { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptyTargetingRulesChanges(1000, 1000)); + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }); + + SplitFactory factory = buildFactory(createPollingConfig(999999, 3)); + SplitClient client1 = factory.client(new Key("key_1")); + SplitClient client2 = factory.client(new Key("key_2")); + + EventCapture client1Capture = captureUpdateEvent(client1); + EventCapture client2Capture = captureUpdateEvent(client2); + + waitForReady(client1); + waitForReady(client2); + + awaitEvent(client1Capture.latch, "Client1 SDK_UPDATE", 20); + assertNotNull("Client1 metadata should not be null", client1Capture.metadata.get()); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, client1Capture.metadata.get().getType()); + + Thread.sleep(1000); + assertEquals("Client2 should not receive SDK_UPDATE", 0, client2Capture.count.get()); + + factory.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains SEGMENTS_UPDATE when only one streaming client changes + *

+ * Given two clients are created with streaming enabled + * And a membership keylist update targets only client1 + * When the SSE notification is pushed + * Then only client1 receives SDK_UPDATE with SEGMENTS_UPDATE metadata + */ + @Test + public void sdkUpdateMetadataForSingleClientMembershipStreaming() throws Exception { + TwoClientFixture fixture = createTwoStreamingClientsAndWaitForReady(new Key("key1"), new Key("key2")); + + EventCapture client1Capture = captureUpdateEvent(fixture.mClientA); + EventCapture client2Capture = captureUpdateEvent(fixture.mClientB); + + // Keylist update: only key1 is included + fixture.pushMembershipKeyListUpdate("key1", "streaming_segment"); + + awaitEvent(client1Capture.latch, "Client1 SDK_UPDATE"); + assertNotNull("Client1 metadata should not be null", client1Capture.metadata.get()); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, client1Capture.metadata.get().getType()); + + Thread.sleep(500); + assertEquals("Client2 should not receive SDK_UPDATE", 0, client2Capture.count.get()); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for large segments update (polling) + *

+ * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When large segments change via polling (server returns different large segments) + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE + * And handler H receives metadata with getNames() returning an empty list + */ + @Test + public void sdkUpdateMetadataContainsTypeForLargeSegmentsUpdate() throws Exception { + verifySdkUpdateForSegmentsPollingWithEmptyNames( + IntegrationHelper.membershipsResponse(new String[]{}, 1000L, new String[]{"large_segment1", "large_segment2"}, 1000L), + IntegrationHelper.membershipsResponse(new String[]{}, 1000L, new String[]{"large_segment2", "large_segment3"}, 2000L) + ); + } + + /** + * Scenario: Two distinct SDK_UPDATE events are fired when both segments and large segments change + *

+ * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a single memberships response contains changes to both segments and large segments + * Then two SDK_UPDATE events are emitted + * And both events have metadata with getType() returning Type.SEGMENTS_UPDATE and empty names + */ + @Test + public void twoDistinctSdkUpdateEventsWhenBothSegmentsAndLargeSegmentsChange() throws Exception { + String initialResponse = IntegrationHelper.membershipsResponse( + new String[]{"segment1", "segment2"}, 1000L, + new String[]{"large_segment1", "large_segment2"}, 1000L); + String pollingResponse = IntegrationHelper.membershipsResponse( + new String[]{"segment2", "segment3"}, 2000L, + new String[]{"large_segment2", "large_segment3"}, 2000L); + + List metadataList = waitForSegmentsPollingUpdates(initialResponse, pollingResponse, 2); + + // Verify we received 2 distinct SDK_UPDATE events + assertEquals("Should receive 2 SDK_UPDATE events", 2, metadataList.size()); + + // Both events should be SEGMENTS_UPDATE type with empty names + for (SdkUpdateMetadata metadata : metadataList) { + assertNotNull("Metadata should not be null", metadata); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + assertNotNull("Names should not be null", metadata.getNames()); + assertTrue("Names should be empty for SEGMENTS_UPDATE", metadata.getNames().isEmpty()); + } + } + + /** + * Helper method to verify SDK_UPDATE with SEGMENTS_UPDATE type is emitted when segments change via polling. + * Verifies that names are always empty for SEGMENTS_UPDATE. + * + * @param initialResponse the memberships response for initial sync + * @param pollingResponse the memberships response for polling (with changed segments) + */ + private void verifySdkUpdateForSegmentsPollingWithEmptyNames(String initialResponse, String pollingResponse) throws Exception { + List metadataList = waitForSegmentsPollingUpdates(initialResponse, pollingResponse, 1); + + assertEquals("Should receive 1 SDK_UPDATE event", 1, metadataList.size()); + + SdkUpdateMetadata metadata = metadataList.get(0); + assertNotNull("Metadata should not be null", metadata); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + + assertNotNull("Names should not be null", metadata.getNames()); + assertTrue("Names should be empty for SEGMENTS_UPDATE", metadata.getNames().isEmpty()); + } + + /** + * Helper method that sets up polling for segments and waits for the expected number of SDK_UPDATE events. + * + * @param initialResponse the memberships response for initial sync + * @param pollingResponse the memberships response for polling (with changed segments) + * @param expectedEventCount the number of SDK_UPDATE events to wait for + * @return list of received SdkUpdateMetadata from the events + */ + private List waitForSegmentsPollingUpdates(String initialResponse, String pollingResponse, + int expectedEventCount) throws Exception { + AtomicInteger membershipsHitCount = new AtomicInteger(0); + + final Dispatcher pollingDispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + int count = membershipsHitCount.incrementAndGet(); + if (count <= 1) { + return new MockResponse().setResponseCode(200).setBody(initialResponse); + } else { + return new MockResponse().setResponseCode(200).setBody(pollingResponse); + } + } else if (path.contains("/splitChanges")) { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptyTargetingRulesChanges(1000, 1000)); + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + mWebServer.setDispatcher(pollingDispatcher); + + SplitClientConfig config = new TestableSplitConfigBuilder() + .serviceEndpoints(endpoints()) + .ready(30000) + .featuresRefreshRate(999999) + .segmentsRefreshRate(3) + .impressionsRefreshRate(999999) + .streamingEnabled(false) + .trafficType("account") + .build(); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(); + + CountDownLatch readyLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + readyLatch.countDown(); + } + }); + assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); + + List receivedMetadataList = new ArrayList<>(); + AtomicInteger legacyHandlerCount = new AtomicInteger(0); + // Wait for expectedEventCount events x 2 handlers (new API + legacy) + CountDownLatch updateLatch = new CountDownLatch(expectedEventCount * 2); + + // Register new API handler (addEventListener) + client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { + synchronized (receivedMetadataList) { + receivedMetadataList.add(metadata); + } + updateLatch.countDown(); + } + }); + + // Register legacy API handler (client.on) + client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + legacyHandlerCount.incrementAndGet(); + updateLatch.countDown(); + } + }); + + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire " + expectedEventCount + " time(s). " + + "Hit count: " + membershipsHitCount.get() + ", metadata count: " + receivedMetadataList.size() + + ", legacy count: " + legacyHandlerCount.get(), updateFired); + + // Verify legacy API was triggered the expected number of times + assertEquals("Legacy API (client.on) should be triggered " + expectedEventCount + " time(s)", + expectedEventCount, legacyHandlerCount.get()); + + factory.destroy(); + + return receivedMetadataList; + } + + + + /** + * Scenario: Multiple listeners with onUpdate are both invoked + *

+ * Given sdkReady has already been emitted + * And two different SplitEventListener instances (L1 and L2) with onUpdate handlers are registered + * When a split update notification arrives via SSE + * Then SDK_UPDATE is emitted once + * And both L1.onUpdate and L2.onUpdate are invoked exactly once each + */ + @Test + public void multipleListenersWithOnUpdateBothInvoked() throws Exception { + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + EventCapture capture1 = captureUpdateEvent(fixture.client); + EventCapture capture2 = captureUpdateEvent(fixture.client); + + fixture.pushSplitUpdate(); + + awaitEvent(capture1.latch, "Listener 1 SDK_UPDATE"); + awaitEvent(capture2.latch, "Listener 2 SDK_UPDATE"); + assertFiredOnce(capture1.count, "Listener 1"); + assertFiredOnce(capture2.count, "Listener 2"); + assertNotNull("Listener 1 should receive metadata", capture1.metadata.get()); + assertNotNull("Listener 2 should receive metadata", capture2.metadata.get()); + + fixture.destroy(); + } + + /** + * Scenario: Multiple listeners with onReady are both invoked + *

+ * Given the SDK is starting + * And two different SplitEventListener instances (L1 and L2) with onReady handlers are registered + * When SDK_READY fires + * Then both L1.onReady and L2.onReady are invoked exactly once each + * And both receive SdkReadyMetadata + */ + @Test + public void multipleListenersWithOnReadyBothInvoked() throws Exception { + populateDatabaseWithCacheData(System.currentTimeMillis()); + SplitFactory factory = buildFactory(buildConfig()); + SplitClient client = factory.client(new Key("key_1")); + + EventCapture capture1 = captureReadyEvent(client); + EventCapture capture2 = captureReadyEvent(client); + + awaitEvent(capture1.latch, "Listener 1 SDK_READY"); + awaitEvent(capture2.latch, "Listener 2 SDK_READY"); + assertFiredOnce(capture1.count, "Listener 1"); + assertFiredOnce(capture2.count, "Listener 2"); + assertNotNull("Listener 1 should receive metadata", capture1.metadata.get()); + assertNotNull("Listener 2 should receive metadata", capture2.metadata.get()); + + factory.destroy(); + } + + /** + * Scenario: Listeners with different callbacks (onReady and onUpdate) each invoked on correct event + *

+ * Given the SDK is starting + * And a SplitEventListener L1 with onReady handler is registered + * And a SplitEventListener L2 with onUpdate handler is registered + * When SDK_READY fires + * Then L1.onReady is invoked + * And L2.onUpdate is NOT invoked (wrong event type) + * When an SDK_UPDATE notification arrives via SSE + * Then L2.onUpdate is invoked + * And L1.onReady is NOT invoked again (already fired once for SDK_READY) + */ + @Test + public void listenersWithDifferentCallbacksInvokedOnCorrectEventType() throws Exception { + TestClientFixture fixture = createStreamingClient(new Key("key_1")); + + EventCapture readyCapture = captureReadyEvent(fixture.client); + EventCapture updateCapture = captureUpdateEvent(fixture.client); + + awaitEvent(readyCapture.latch, "SDK_READY"); + assertFiredOnce(readyCapture.count, "onReady"); + assertEquals("onUpdate should NOT be invoked on SDK_READY", 0, updateCapture.count.get()); + + fixture.waitForSseConnection(); + fixture.pushSplitUpdate(); + + awaitEvent(updateCapture.latch, "SDK_UPDATE"); + assertFiredOnce(updateCapture.count, "onUpdate"); + assertFiredOnce(readyCapture.count, "onReady (not invoked again)"); + + fixture.destroy(); + } + + /** + * Scenario: Multiple listeners with both onReady and onUpdate in same listener + *

+ * Given the SDK is starting + * And two SplitEventListener instances (L1 and L2) each with both onReady and onUpdate handlers + * When SDK_READY fires + * Then both L1.onReady and L2.onReady are invoked exactly once each + * And neither L1.onUpdate nor L2.onUpdate are invoked + * When an SDK_UPDATE notification arrives via SSE + * Then both L1.onUpdate and L2.onUpdate are invoked exactly once each + */ + @Test + public void multipleListenersWithBothReadyAndUpdateHandlers() throws Exception { + TestClientFixture fixture = createStreamingClient(new Key("key_1")); + + AtomicInteger l1ReadyCount = new AtomicInteger(0); + AtomicInteger l1UpdateCount = new AtomicInteger(0); + AtomicInteger l2ReadyCount = new AtomicInteger(0); + AtomicInteger l2UpdateCount = new AtomicInteger(0); + CountDownLatch readyLatch = new CountDownLatch(2); + CountDownLatch updateLatch = new CountDownLatch(2); + + fixture.client.addEventListener(createDualListener(l1ReadyCount, readyLatch, l1UpdateCount, updateLatch)); + fixture.client.addEventListener(createDualListener(l2ReadyCount, readyLatch, l2UpdateCount, updateLatch)); + + awaitEvent(readyLatch, "Both onReady handlers"); + assertFiredOnce(l1ReadyCount, "Listener 1 onReady"); + assertFiredOnce(l2ReadyCount, "Listener 2 onReady"); + assertEquals("Listener 1 onUpdate should NOT be invoked on SDK_READY", 0, l1UpdateCount.get()); + assertEquals("Listener 2 onUpdate should NOT be invoked on SDK_READY", 0, l2UpdateCount.get()); + + fixture.waitForSseConnection(); + fixture.pushSplitUpdate(); + + awaitEvent(updateLatch, "Both onUpdate handlers"); + assertFiredOnce(l1UpdateCount, "Listener 1 onUpdate"); + assertFiredOnce(l2UpdateCount, "Listener 2 onUpdate"); + assertFiredOnce(l1ReadyCount, "Listener 1 onReady (not invoked again)"); + assertFiredOnce(l2ReadyCount, "Listener 2 onReady (not invoked again)"); + + fixture.destroy(); + } + + /** + * Scenario: Multiple listeners with onReady replay to late subscribers + *

+ * Given SDK_READY has already been emitted + * And a SplitEventListener L1 with onReady was registered before SDK_READY and was invoked + * When a new SplitEventListener L2 with onReady is registered after SDK_READY has fired + * Then L2.onReady is invoked (replay) + * And L1.onReady is NOT invoked again + */ + @Test + public void multipleListenersWithOnReadyReplayToLateSubscribers() throws Exception { + TestClientFixture fixture = createClientAndWaitForReady(new Key("key_1")); + + EventCapture capture1 = captureReadyEvent(fixture.client); + awaitEvent(capture1.latch, "Listener 1 replay", 5); + assertFiredOnce(capture1.count, "Listener 1 (replay)"); + + EventCapture capture2 = captureReadyEvent(fixture.client); + awaitEvent(capture2.latch, "Listener 2 replay", 5); + assertFiredOnce(capture2.count, "Listener 2 (replay)"); + + Thread.sleep(500); + assertFiredOnce(capture1.count, "Listener 1 (not invoked again)"); + + fixture.destroy(); + } + + private TestClientFixture createClientAndWaitForReady(SplitClientConfig config, Key key) throws InterruptedException { + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(key); + CountDownLatch readyLatch = new CountDownLatch(1); + + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + readyLatch.countDown(); + } + }); + + boolean readyFired = readyLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_READY should fire", readyFired); + + return new TestClientFixture(factory, client, readyLatch); + } + + private TestClientFixture createClientAndWaitForReady(Key key) throws InterruptedException { + return createClientAndWaitForReady(buildConfig(), key); + } + + /** + * Creates a client with streaming enabled but does NOT wait for SDK_READY. + */ + private TestClientFixture createStreamingClient(Key key) throws IOException { + BlockingQueue streamingData = new LinkedBlockingDeque<>(); + CountDownLatch sseLatch = new CountDownLatch(1); + + HttpResponseMockDispatcher dispatcher = createStreamingDispatcher(streamingData, sseLatch); + HttpClientMock httpClientMock = new HttpClientMock(dispatcher); + SplitClientConfig config = new TestableSplitConfigBuilder() + .ready(30000) + .streamingEnabled(true) + .trafficType("account") + .enableDebug() + .build(); + + SplitFactory factory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), key, config, mContext, httpClientMock, mDatabase); + + SplitClient client = factory.client(key); + + return new TestClientFixture(factory, client, null, streamingData, sseLatch); + } + + private TestClientFixture createStreamingClientAndWaitForReady(Key key) throws InterruptedException, IOException { + TestClientFixture fixture = createStreamingClient(key); + + CountDownLatch readyLatch = new CountDownLatch(1); + fixture.client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + readyLatch.countDown(); + } + }); + + boolean readyFired = readyLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_READY should fire", readyFired); + + // Wait for SSE connection and send keep-alive + fixture.waitForSseConnection(); + + return new TestClientFixture(fixture.factory, fixture.client, readyLatch, fixture.streamingData, fixture.sseLatch); + } + + private HttpResponseMockDispatcher createStreamingDispatcher(BlockingQueue streamingData, CountDownLatch sseLatch) { + return new HttpResponseMockDispatcher() { + @Override + public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new HttpResponseMock(200, IntegrationHelper.dummyAllSegments()); + } else if (uri.getPath().contains("/splitChanges")) { + return new HttpResponseMock(200, IntegrationHelper.emptyTargetingRulesChanges(1000, 1000)); + } else if (uri.getPath().contains("/auth")) { + sseLatch.countDown(); + return new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken()); + } else if (uri.getPath().contains("/testImpressions/bulk")) { + return new HttpResponseMock(200); + } + return new HttpResponseMock(200); + } + + @Override + public HttpStreamResponseMock getStreamResponse(URI uri) { + try { + return new HttpStreamResponseMock(200, streamingData); + } catch (IOException e) { + return null; + } + } + }; + } + + private TwoClientFixture createTwoStreamingClientsAndWaitForReady(Key keyA, Key keyB) throws InterruptedException, IOException { + BlockingQueue streamingData = new LinkedBlockingDeque<>(); + CountDownLatch sseLatch = new CountDownLatch(1); + + HttpResponseMockDispatcher dispatcher = createStreamingDispatcher(streamingData, sseLatch); + HttpClientMock httpClientMock = new HttpClientMock(dispatcher); + SplitClientConfig config = new TestableSplitConfigBuilder() + .ready(30000) + .streamingEnabled(true) + .trafficType("account") + .enableDebug() + .build(); + + SplitFactory factory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), keyA, config, mContext, httpClientMock, mDatabase); + + SplitClient clientA = factory.client(keyA); + SplitClient clientB = factory.client(keyB); + + CountDownLatch readyLatchA = captureLegacyReadyEvent(clientA); + CountDownLatch readyLatchB = captureLegacyReadyEvent(clientB); + + awaitEvent(readyLatchA, "ClientA SDK_READY", 30); + awaitEvent(readyLatchB, "ClientB SDK_READY", 30); + + // Wait for SSE connection and send keep-alive + sseLatch.await(10, TimeUnit.SECONDS); + TestingHelper.pushKeepAlive(streamingData); + + return new TwoClientFixture(factory, clientA, clientB, streamingData); + } + + private SplitEventListener createDualListener(AtomicInteger readyCount, CountDownLatch readyLatch, + AtomicInteger updateCount, CountDownLatch updateLatch) { + return new SplitEventListener() { + + @Override + public void onReady(SplitClient client, SdkReadyMetadata metadata) { + if (readyCount != null) readyCount.incrementAndGet(); + if (readyLatch != null) readyLatch.countDown(); + } + + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + if (updateCount != null) updateCount.incrementAndGet(); + if (updateLatch != null) updateLatch.countDown(); + } + }; + } + + + /** + * Helper class to hold factory and client together for cleanup. + */ + private static class TestClientFixture { + final SplitFactory factory; + final SplitClient client; + final CountDownLatch readyLatch; + final BlockingQueue streamingData; + final CountDownLatch sseLatch; + + TestClientFixture(SplitFactory factory, SplitClient client, CountDownLatch readyLatch) { + this(factory, client, readyLatch, null, null); + } + + TestClientFixture(SplitFactory factory, SplitClient client, CountDownLatch readyLatch, BlockingQueue streamingData) { + this(factory, client, readyLatch, streamingData, null); + } + + TestClientFixture(SplitFactory factory, SplitClient client, CountDownLatch readyLatch, + BlockingQueue streamingData, CountDownLatch sseLatch) { + this.factory = factory; + this.client = client; + this.readyLatch = readyLatch; + this.streamingData = streamingData; + this.sseLatch = sseLatch; + } + + void waitForSseConnection() throws InterruptedException { + if (sseLatch != null) { + sseLatch.await(10, TimeUnit.SECONDS); + TestingHelper.pushKeepAlive(streamingData); + } + } + + void pushSplitUpdate() { + if (streamingData != null) { + pushMessage(streamingData, IntegrationHelper.splitChangeV2CompressionType0()); + } + } + + void pushSplitUpdate(String changeNumber, String previousChangeNumber) { + if (streamingData != null) { + pushMessage(streamingData, IntegrationHelper.splitChangeV2( + changeNumber, previousChangeNumber, "0", IntegrationHelper.SPLIT_UPDATE_PAYLOAD_TYPE0)); + } + } + + void pushSplitKill(String splitName) { + if (streamingData != null) { + pushMessage(streamingData, IntegrationHelper.splitKill("9999999999999", splitName)); + } + } + + void pushRbsUpdate() { + pushRbsUpdate("2000", "1000"); + } + + void pushRbsUpdate(String changeNumber, String previousChangeNumber) { + if (streamingData != null) { + pushMessage(streamingData, IntegrationHelper.rbsChange( + changeNumber, previousChangeNumber, IntegrationHelper.RBS_UPDATE_PAYLOAD_TYPE0)); + } + } + + void destroy() { + factory.destroy(); + } + } + + /** + * Helper class to hold factory and two clients together for cleanup. + */ + private static class TwoClientFixture { + final SplitFactory mFactory; + final SplitClient mClientA; + final SplitClient mClientB; + final BlockingQueue mStreamingData; + + TwoClientFixture(SplitFactory factory, SplitClient clientA, SplitClient clientB, BlockingQueue streamingData) { + mFactory = factory; + mClientA = clientA; + mClientB = clientB; + mStreamingData = streamingData; + } + + void pushSplitUpdate() { + if (mStreamingData != null) { + pushMessage(mStreamingData, IntegrationHelper.splitChangeV2CompressionType0()); + } + } + + void pushMembershipKeyListUpdate(String key, String segmentName) { + if (mStreamingData != null) { + pushMessage(mStreamingData, membershipKeyListUpdateMessage(key, segmentName)); + } + } + + void destroy() { + mFactory.destroy(); + } + } + + private static String membershipKeyListUpdateMessage(String key, String segmentName) { + MySegmentsV2PayloadDecoder decoder = new MySegmentsV2PayloadDecoder(); + BigInteger hashedKey = decoder.hashKey(key); + return IntegrationHelper.membershipKeyListUpdate(hashedKey, segmentName, 2000); + } + private static void pushMessage(BlockingQueue queue, String message) { + try { + queue.put(message + "\n"); + Logger.d("Pushed message: " + message); + } catch (InterruptedException e) { + Logger.e("Failed to push message", e); + } + } + + /** + * Populates the database with splits and segments to simulate a populated cache. + */ + private void populateDatabaseWithCacheData(long timestamp) { + // Populate splits + List splitEntities = new ArrayList<>(); + long finalChangeNumber = 1000L; + for (int i = 0; i < 3; i++) { + SplitEntity entity = new SplitEntity(); + entity.setName("split_" + i); + long cn = 1000L + i; + finalChangeNumber = cn; + entity.setBody(IntegrationHelper.splitEntityBody("split_" + i, cn)); + splitEntities.add(entity); + } + mDatabase.splitDao().insert(splitEntities); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, finalChangeNumber)); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, timestamp)); + + // Populate segments for default key + MySegmentEntity segmentEntity = new MySegmentEntity(); + segmentEntity.setUserKey("DEFAULT_KEY"); + segmentEntity.setSegmentList(IntegrationHelper.segmentListJson("segment1", "segment2")); + segmentEntity.setUpdatedAt(System.currentTimeMillis() / 1000); + mDatabase.mySegmentDao().update(segmentEntity); + + // Populate segments for key_1 + MySegmentEntity segmentEntity2 = new MySegmentEntity(); + segmentEntity2.setUserKey("key_1"); + segmentEntity2.setSegmentList(IntegrationHelper.segmentListJson("segment1")); + segmentEntity2.setUpdatedAt(System.currentTimeMillis() / 1000); + mDatabase.mySegmentDao().update(segmentEntity2); + } + + /** + * Creates a streaming client with RBS data pre-populated and waits for SDK_READY. + * Pre-populates RBS change number so the test can verify in-place update behavior. + */ + private TestClientFixture createStreamingClientWithRbsAndWaitForReady(Key key) throws InterruptedException, IOException { + // Pre-populate RBS in storage so in-place update can work + populateDatabaseWithRbsData(); + + TestClientFixture fixture = createStreamingClient(key); + + CountDownLatch readyLatch = new CountDownLatch(1); + fixture.client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + readyLatch.countDown(); + } + }); + + boolean readyFired = readyLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_READY should fire", readyFired); + + // Wait for SSE connection and send keep-alive + fixture.waitForSseConnection(); + + return new TestClientFixture(fixture.factory, fixture.client, readyLatch, fixture.streamingData, fixture.sseLatch); + } + + private void populateDatabaseWithRbsData() { + // Set RBS change number so streaming notifications trigger in-place updates + mDatabase.generalInfoDao().update(new GeneralInfoEntity("rbsChangeNumber", 1000L)); + } + + private static class EventCapture { + final AtomicInteger count = new AtomicInteger(0); + final AtomicReference metadata = new AtomicReference<>(); + final CountDownLatch latch; + + EventCapture() { + this(1); + } + + EventCapture(int expectedCount) { + this.latch = new CountDownLatch(expectedCount); + } + + void capture(M meta) { + count.incrementAndGet(); + metadata.set(meta); + latch.countDown(); + } + + void increment() { + count.incrementAndGet(); + latch.countDown(); + } + + boolean await(int seconds) throws InterruptedException { + return latch.await(seconds, TimeUnit.SECONDS); + } + } + + private void awaitEvent(CountDownLatch latch, String eventName) throws InterruptedException { + awaitEvent(latch, eventName, 10); + } + + private void awaitEvent(CountDownLatch latch, String eventName, int timeoutSeconds) throws InterruptedException { + boolean fired = latch.await(timeoutSeconds, TimeUnit.SECONDS); + assertTrue(eventName + " should fire", fired); + } + + private void assertFiredOnce(AtomicInteger count, String eventName) { + assertEquals(eventName + " should be invoked exactly once", 1, count.get()); + } + + private void assertFiredTimes(AtomicInteger count, String eventName, int expectedTimes) { + assertEquals(eventName + " should be invoked " + expectedTimes + " time(s)", expectedTimes, count.get()); + } + + private EventCapture captureReadyEvent(SplitClient client) { + EventCapture capture = new EventCapture<>(); + client.addEventListener(new SplitEventListener() { + @Override + public void onReady(SplitClient c, SdkReadyMetadata metadata) { + capture.capture(metadata); + } + }); + return capture; + } + + private EventCapture captureCacheReadyEvent(SplitClient client) { + EventCapture capture = new EventCapture<>(); + client.addEventListener(new SplitEventListener() { + @Override + public void onReadyFromCache(SplitClient c, SdkReadyMetadata metadata) { + capture.capture(metadata); + } + }); + return capture; + } + + private EventCapture captureUpdateEvent(SplitClient client) { + return captureUpdateEvent(client, 1); + } + + private EventCapture captureUpdateEvent(SplitClient client, int expectedCount) { + EventCapture capture = new EventCapture<>(expectedCount); + client.addEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { + capture.capture(metadata); + } + }); + return capture; + } + + private CountDownLatch captureLegacyReadyEvent(SplitClient client) { + CountDownLatch latch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + latch.countDown(); + } + }); + return latch; + } + + /** + * Creates a polling dispatcher that returns different responses based on hit count. + */ + private Dispatcher createPollingDispatcher( + Function splitChangesResponseFn, + Function membershipsResponseFn) { + AtomicInteger splitChangesHits = new AtomicInteger(0); + AtomicInteger membershipsHits = new AtomicInteger(0); + + return new Dispatcher() { + @NonNull + @Override + public MockResponse dispatch(@NonNull RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + int count = membershipsHits.incrementAndGet(); + String body = membershipsResponseFn != null + ? membershipsResponseFn.apply(count) + : IntegrationHelper.dummyAllSegments(); + return new MockResponse().setResponseCode(200).setBody(body); + } else if (path.contains("/splitChanges")) { + int count = splitChangesHits.incrementAndGet(); + String body = splitChangesResponseFn != null + ? splitChangesResponseFn.apply(count) + : IntegrationHelper.emptyTargetingRulesChanges(1000, 1000); + return new MockResponse().setResponseCode(200).setBody(body); + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + } + + private Dispatcher createDelayedDispatcher(long delaySeconds) { + return new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse() + .setResponseCode(200) + .setBody(IntegrationHelper.dummyAllSegments()) + .setBodyDelay(delaySeconds, TimeUnit.SECONDS); + } else if (path.contains("/splitChanges")) { + long id = mCurSplitReqId++; + return new MockResponse() + .setResponseCode(200) + .setBody(IntegrationHelper.emptyTargetingRulesChanges(id, id)) + .setBodyDelay(delaySeconds, TimeUnit.SECONDS); + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + } + + private SplitClientConfig createPollingConfig(int featuresRefreshRate, int segmentsRefreshRate) { + return new TestableSplitConfigBuilder() + .serviceEndpoints(endpoints()) + .ready(30000) + .featuresRefreshRate(featuresRefreshRate) + .segmentsRefreshRate(segmentsRefreshRate) + .impressionsRefreshRate(999999) + .streamingEnabled(false) + .trafficType("account") + .build(); + } + + private void waitForReady(SplitClient client) throws InterruptedException { + CountDownLatch latch = captureLegacyReadyEvent(client); + awaitEvent(latch, "SDK_READY"); + } +} diff --git a/src/androidTest/java/tests/integration/fallback/FallbackTreatmentsTest.java b/main/src/androidTest/java/tests/integration/fallback/FallbackTreatmentsTest.java similarity index 100% rename from src/androidTest/java/tests/integration/fallback/FallbackTreatmentsTest.java rename to main/src/androidTest/java/tests/integration/fallback/FallbackTreatmentsTest.java diff --git a/src/androidTest/java/tests/integration/init/InitializationTest.java b/main/src/androidTest/java/tests/integration/init/InitializationTest.java similarity index 100% rename from src/androidTest/java/tests/integration/init/InitializationTest.java rename to main/src/androidTest/java/tests/integration/init/InitializationTest.java diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java b/main/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java similarity index 100% rename from src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java rename to main/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java b/main/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java similarity index 96% rename from src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java rename to main/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java index 30d67a2e3..9e86fb7df 100644 --- a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java +++ b/main/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java @@ -153,10 +153,17 @@ private SplitFactory getFactory(SplitRoomDatabase database) throws IOException { } private HttpResponseMockDispatcher buildDispatcher() { + final long splitsTill = 1602796638344L; Map responses = new HashMap<>(); responses.put(SPLIT_CHANGES, (path, query, body) -> { updateEndpointHit(SPLIT_CHANGES); - return new HttpResponseMock(200, splitChangesLargeSegments(1602796638344L, 1602796638344L)); + String sinceStr = IntegrationHelper.getSinceFromUri(path); + long since = sinceStr != null ? Long.parseLong(sinceStr) : -1; + if (since >= splitsTill) { + // No changes since last fetch + return new HttpResponseMock(200, IntegrationHelper.emptyTargetingRulesChanges(splitsTill, splitsTill)); + } + return new HttpResponseMock(200, splitChangesLargeSegments(splitsTill, splitsTill)); }); String key = IntegrationHelper.dummyUserKey().matchingKey(); diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java b/main/src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java similarity index 100% rename from src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java rename to main/src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java diff --git a/src/androidTest/java/tests/integration/matcher/PrerequisitesTest.java b/main/src/androidTest/java/tests/integration/matcher/PrerequisitesTest.java similarity index 99% rename from src/androidTest/java/tests/integration/matcher/PrerequisitesTest.java rename to main/src/androidTest/java/tests/integration/matcher/PrerequisitesTest.java index 9a34f73c9..8701e460a 100644 --- a/src/androidTest/java/tests/integration/matcher/PrerequisitesTest.java +++ b/main/src/androidTest/java/tests/integration/matcher/PrerequisitesTest.java @@ -253,4 +253,3 @@ private String loadSplitChanges() { return IntegrationHelper.loadSplitChanges(mContext, "splitchanges_prerequisites.json"); } } - diff --git a/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java b/main/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java similarity index 100% rename from src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java rename to main/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java diff --git a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java b/main/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java similarity index 100% rename from src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java rename to main/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java diff --git a/src/androidTest/java/tests/integration/pin/CertPinningTest.java b/main/src/androidTest/java/tests/integration/pin/CertPinningTest.java similarity index 100% rename from src/androidTest/java/tests/integration/pin/CertPinningTest.java rename to main/src/androidTest/java/tests/integration/pin/CertPinningTest.java diff --git a/src/androidTest/java/tests/integration/rbs/OutdatedProxyIntegrationTest.java b/main/src/androidTest/java/tests/integration/rbs/OutdatedProxyIntegrationTest.java similarity index 100% rename from src/androidTest/java/tests/integration/rbs/OutdatedProxyIntegrationTest.java rename to main/src/androidTest/java/tests/integration/rbs/OutdatedProxyIntegrationTest.java diff --git a/src/androidTest/java/tests/integration/rbs/RuleBasedSegmentsIntegrationTest.java b/main/src/androidTest/java/tests/integration/rbs/RuleBasedSegmentsIntegrationTest.java similarity index 100% rename from src/androidTest/java/tests/integration/rbs/RuleBasedSegmentsIntegrationTest.java rename to main/src/androidTest/java/tests/integration/rbs/RuleBasedSegmentsIntegrationTest.java diff --git a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java b/main/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java similarity index 95% rename from src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java rename to main/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java index ce8df8024..cb90b16b1 100644 --- a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java @@ -126,11 +126,9 @@ public void repeatedInitWithClearOnInitSetToTrueDoesNotClearIfMinDaysHasNotElaps assertEquals(8000L, initialChangeNumber); // values after clear - assertEquals(1, intermediateSegments.size()); - assertTrue(Json.fromJson(intermediateSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); - assertEquals(1, intermediateLargeSegments.size()); + assertEquals(0, intermediateSegments.size()); + assertEquals(0, intermediateLargeSegments.size()); assertEquals(0, intermediateFlags.size()); - assertTrue(Json.fromJson(intermediateLargeSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); assertEquals(-1, intermediateChangeNumber); // values after second init (values were reinserted into DB); no clear @@ -203,11 +201,9 @@ private void verify(SplitFactory factory, CountDownLatch readyLatch, List loadFileContent() { return Arrays.asList(content.split("\n")); } } - - diff --git a/main/src/androidTest/java/tests/service/EventsManagerTest.java b/main/src/androidTest/java/tests/service/EventsManagerTest.java new file mode 100644 index 000000000..82ec302e3 --- /dev/null +++ b/main/src/androidTest/java/tests/service/EventsManagerTest.java @@ -0,0 +1,365 @@ +package tests.service; + +import android.os.Looper; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import fake.SplitEventExecutorResourcesMock; +import helper.TestingHelper; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.events.SplitEventListener; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; +import io.split.android.client.service.executor.SplitTaskExecutorImpl; + +public class EventsManagerTest { + @Test + public void testSdkUpdateSplits() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch updateLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + // First make SDK_READY fire (prerequisite for SDK_UPDATE) + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + // Then trigger SDK_UPDATE + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + + updateLatch.await(5, TimeUnit.SECONDS); + + Assert.assertTrue(updateTask.onExecutedCalled); + } + + @Test + public void testSdkUpdateTriggersAfterReady() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch updateLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + // First make SDK_READY fire by completing sync + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + // Then trigger SDK_UPDATE with a data change + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + + updateLatch.await(5, TimeUnit.SECONDS); + + Assert.assertTrue(updateTask.onExecutedCalled); + } + + @Test + public void testSdkUpdateDoesNotTriggerBeforeReady() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch updateLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + // Fire UPDATED before SDK_READY - should NOT trigger SDK_UPDATE due to prerequisite + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + + updateLatch.await(2, TimeUnit.SECONDS); + + Assert.assertFalse(updateTask.onExecutedCalled); + } + + @Test + public void testSdkUpdateSegments() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch updateLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + // First make SDK_READY fire (prerequisite for SDK_UPDATE) + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + // Then trigger SDK_UPDATE with segment change + eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + + updateLatch.await(5, TimeUnit.SECONDS); + + Assert.assertTrue(updateTask.onExecutedCalled); + } + + @Test + public void testSdkUpdateTriggersOnSegmentChange() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch updateLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + // Make SDK_READY fire + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + // Then trigger SDK_UPDATE with a segment change + eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + + updateLatch.await(5, TimeUnit.SECONDS); + + Assert.assertTrue(updateTask.onExecutedCalled); + } + + @Test + public void testSdkUpdateRequiresDataChange() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch updateLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + // Make SDK_READY fire with only SYNC_COMPLETE events (no UPDATED) + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + // No UPDATED events fired + + updateLatch.await(2, TimeUnit.SECONDS); + + // SDK_UPDATE should NOT fire because no data actually changed + Assert.assertFalse(updateTask.onExecutedCalled); + } + + @Test + public void testKilledSplit() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch updateLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + // First make SDK_READY fire (prerequisite for SDK_UPDATE) + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + // Then trigger SDK_UPDATE with killed notification + eventManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); + + updateLatch.await(5, TimeUnit.SECONDS); + + Assert.assertTrue(updateTask.onExecutedCalled); + } + + @Test + public void testKilledSplitWithMetadata() throws InterruptedException { + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch readyLatch = new CountDownLatch(1); + CountDownLatch updateLatch = new CountDownLatch(1); + AtomicReference receivedMetadata = new AtomicReference<>(); + + // Wait for SDK_READY first + eventManager.register(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + readyLatch.countDown(); + } + }); + + // Register for SDK_UPDATE with metadata callback using SdkEventListener + eventManager.registerEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + // Make SDK_READY fire + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + Assert.assertTrue("SDK_READY should fire", readyLatch.await(5, TimeUnit.SECONDS)); + + eventManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION, + EventMetadataHelpers.createUpdatedFlagsMetadata(Collections.singletonList("killed_flag"))); + + Assert.assertTrue("SDK_UPDATE should fire", updateLatch.await(5, TimeUnit.SECONDS)); + Assert.assertNotNull("Metadata should not be null", receivedMetadata.get()); + List names = receivedMetadata.get().getNames(); + Assert.assertNotNull("Names should not be null", names); + Assert.assertTrue("Metadata should contain only killed_flag", names.size() == 1 && names.contains("killed_flag")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, receivedMetadata.get().getType()); + } + + @Test + public void testKilledSplitBeforeReady() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(null); + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); + + TestingHelper.delay(1000); + + Assert.assertFalse(updateTask.onExecutedCalled); + } + + @Test + public void testTimeoutSplitsUpdated() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().ready(2000).build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + CountDownLatch timeoutLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(null); + TestingHelper.TestEventTask timeoutTask = TestingHelper.testTask(timeoutLatch); + + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + eventManager.register(SplitEvent.SDK_READY_TIMED_OUT, timeoutTask); + + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + + timeoutLatch.await(5, TimeUnit.SECONDS); + + Assert.assertFalse(updateTask.onExecutedCalled); + Assert.assertTrue(timeoutTask.onExecutedCalled); + } + + @Test + public void testTimeoutMySegmentsUpdated() throws InterruptedException { + + SplitClientConfig cfg = SplitClientConfig.builder().ready(2000).build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + CountDownLatch timeoutLatch = new CountDownLatch(1); + TestingHelper.TestEventTask updateTask = TestingHelper.testTask(null); + TestingHelper.TestEventTask timeoutTask = TestingHelper.testTask(timeoutLatch); + + eventManager.register(SplitEvent.SDK_UPDATE, updateTask); + eventManager.register(SplitEvent.SDK_READY_TIMED_OUT, timeoutTask); + + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + + timeoutLatch.await(5, TimeUnit.SECONDS); + + Assert.assertFalse(updateTask.onExecutedCalled); + Assert.assertTrue(timeoutTask.onExecutedCalled); + } + + @Test + public void testSdkEventListenerReceivesMetadataOnCorrectThreads() throws InterruptedException { + SplitClientConfig cfg = SplitClientConfig.builder().build(); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); + eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); + + CountDownLatch readyLatch = new CountDownLatch(1); + CountDownLatch allCalledLatch = new CountDownLatch(2); // Expect 2 calls (background and main thread) + + AtomicBoolean backgroundCalled = new AtomicBoolean(false); + AtomicBoolean mainThreadCalled = new AtomicBoolean(false); + + AtomicBoolean backgroundOnMainThread = new AtomicBoolean(true); // Should be false + AtomicBoolean mainThreadOnMainThread = new AtomicBoolean(false); // Should be true + + AtomicReference backgroundMetadata = new AtomicReference<>(); + AtomicReference mainThreadMetadata = new AtomicReference<>(); + + // Wait for SDK_READY first + eventManager.register(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + readyLatch.countDown(); + } + }); + + // Register SdkEventListener to receive typed metadata + eventManager.registerEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + backgroundCalled.set(true); + backgroundOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); + backgroundMetadata.set(metadata); + allCalledLatch.countDown(); + } + + @Override + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { + mainThreadCalled.set(true); + mainThreadOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); + mainThreadMetadata.set(metadata); + allCalledLatch.countDown(); + } + }); + + // Make SDK_READY fire + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + Assert.assertTrue("SDK_READY should fire", readyLatch.await(5, TimeUnit.SECONDS)); + + // Trigger SDK_UPDATE with metadata + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, + EventMetadataHelpers.createUpdatedFlagsMetadata(Arrays.asList("flag1", "flag2"))); + + Assert.assertTrue("Both callbacks should be called", allCalledLatch.await(5, TimeUnit.SECONDS)); + + Assert.assertTrue("Background method should be called", backgroundCalled.get()); + Assert.assertTrue("Main thread method should be called", mainThreadCalled.get()); + + Assert.assertFalse("Background method should NOT run on main thread", backgroundOnMainThread.get()); + Assert.assertTrue("Main thread method SHOULD run on main thread", mainThreadOnMainThread.get()); + + Assert.assertNotNull("Background metadata should not be null", backgroundMetadata.get()); + List bgNames = backgroundMetadata.get().getNames(); + Assert.assertNotNull("Background names should not be null", bgNames); + Assert.assertTrue("Background metadata should contain flag1", bgNames.contains("flag1")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, backgroundMetadata.get().getType()); + + Assert.assertNotNull("Main thread metadata should not be null", mainThreadMetadata.get()); + List mtNames = mainThreadMetadata.get().getNames(); + Assert.assertNotNull("Main thread names should not be null", mtNames); + Assert.assertTrue("Main thread metadata should contain flag1", mtNames.contains("flag1")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, mainThreadMetadata.get().getType()); + } +} diff --git a/src/androidTest/java/tests/service/EventsRequestTest.java b/main/src/androidTest/java/tests/service/EventsRequestTest.java similarity index 100% rename from src/androidTest/java/tests/service/EventsRequestTest.java rename to main/src/androidTest/java/tests/service/EventsRequestTest.java diff --git a/src/androidTest/java/tests/service/ImpressionsRequestTest.java b/main/src/androidTest/java/tests/service/ImpressionsRequestTest.java similarity index 100% rename from src/androidTest/java/tests/service/ImpressionsRequestTest.java rename to main/src/androidTest/java/tests/service/ImpressionsRequestTest.java diff --git a/src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java b/main/src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java similarity index 100% rename from src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java rename to main/src/androidTest/java/tests/service/MySegmentV2PayloadDecoderTest.java diff --git a/src/androidTest/java/tests/service/SdkUpdatePollingTest.java b/main/src/androidTest/java/tests/service/SdkUpdatePollingTest.java similarity index 99% rename from src/androidTest/java/tests/service/SdkUpdatePollingTest.java rename to main/src/androidTest/java/tests/service/SdkUpdatePollingTest.java index e04afdbe4..178d2da99 100644 --- a/src/androidTest/java/tests/service/SdkUpdatePollingTest.java +++ b/main/src/androidTest/java/tests/service/SdkUpdatePollingTest.java @@ -382,4 +382,4 @@ private Split parseEntity(SplitEntity entity) { return Json.fromJson(entity.getBody(), Split.class); } -} \ No newline at end of file +} diff --git a/src/androidTest/java/tests/service/SseJwtTokenParserTest.java b/main/src/androidTest/java/tests/service/SseJwtTokenParserTest.java similarity index 100% rename from src/androidTest/java/tests/service/SseJwtTokenParserTest.java rename to main/src/androidTest/java/tests/service/SseJwtTokenParserTest.java diff --git a/src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java b/main/src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java similarity index 100% rename from src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java rename to main/src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java diff --git a/src/androidTest/java/tests/service/UrlSanitizerTest.java b/main/src/androidTest/java/tests/service/UrlSanitizerTest.java similarity index 100% rename from src/androidTest/java/tests/service/UrlSanitizerTest.java rename to main/src/androidTest/java/tests/service/UrlSanitizerTest.java diff --git a/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java b/main/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/GeneralInfoStorageTest.java rename to main/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java diff --git a/src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java b/main/src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java similarity index 100% rename from src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java rename to main/src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java diff --git a/src/androidTest/java/tests/storage/LoadSplitTaskTest.java b/main/src/androidTest/java/tests/storage/LoadSplitTaskTest.java similarity index 100% rename from src/androidTest/java/tests/storage/LoadSplitTaskTest.java rename to main/src/androidTest/java/tests/storage/LoadSplitTaskTest.java diff --git a/src/androidTest/java/tests/storage/MySegmentsStorageTest.java b/main/src/androidTest/java/tests/storage/MySegmentsStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/MySegmentsStorageTest.java rename to main/src/androidTest/java/tests/storage/MySegmentsStorageTest.java diff --git a/src/androidTest/java/tests/storage/PersistentEventStorageTest.java b/main/src/androidTest/java/tests/storage/PersistentEventStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/PersistentEventStorageTest.java rename to main/src/androidTest/java/tests/storage/PersistentEventStorageTest.java diff --git a/src/androidTest/java/tests/storage/PersistentImpressionCountStorageTest.java b/main/src/androidTest/java/tests/storage/PersistentImpressionCountStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/PersistentImpressionCountStorageTest.java rename to main/src/androidTest/java/tests/storage/PersistentImpressionCountStorageTest.java diff --git a/src/androidTest/java/tests/storage/PersistentImpressionStorageTest.java b/main/src/androidTest/java/tests/storage/PersistentImpressionStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/PersistentImpressionStorageTest.java rename to main/src/androidTest/java/tests/storage/PersistentImpressionStorageTest.java diff --git a/src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java b/main/src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java rename to main/src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java diff --git a/src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java b/main/src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java rename to main/src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java diff --git a/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java b/main/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java rename to main/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java diff --git a/src/androidTest/java/tests/storage/SplitsStorageTest.java b/main/src/androidTest/java/tests/storage/SplitsStorageTest.java similarity index 100% rename from src/androidTest/java/tests/storage/SplitsStorageTest.java rename to main/src/androidTest/java/tests/storage/SplitsStorageTest.java diff --git a/src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt b/main/src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt similarity index 100% rename from src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt rename to main/src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt diff --git a/src/androidTest/java/tests/storage/cipher/KeyManagerTest.java b/main/src/androidTest/java/tests/storage/cipher/KeyManagerTest.java similarity index 100% rename from src/androidTest/java/tests/storage/cipher/KeyManagerTest.java rename to main/src/androidTest/java/tests/storage/cipher/KeyManagerTest.java diff --git a/src/androidTest/java/tests/storage/cipher/provider/LegacyKeyProviderTest.java b/main/src/androidTest/java/tests/storage/cipher/provider/LegacyKeyProviderTest.java similarity index 100% rename from src/androidTest/java/tests/storage/cipher/provider/LegacyKeyProviderTest.java rename to main/src/androidTest/java/tests/storage/cipher/provider/LegacyKeyProviderTest.java diff --git a/src/androidTest/java/tests/storage/cipher/provider/SecureKeyStorageProviderTest.java b/main/src/androidTest/java/tests/storage/cipher/provider/SecureKeyStorageProviderTest.java similarity index 100% rename from src/androidTest/java/tests/storage/cipher/provider/SecureKeyStorageProviderTest.java rename to main/src/androidTest/java/tests/storage/cipher/provider/SecureKeyStorageProviderTest.java diff --git a/src/androidTest/java/tests/workmanager/WMWorkersSetupTest.java b/main/src/androidTest/java/tests/workmanager/WMWorkersSetupTest.java similarity index 100% rename from src/androidTest/java/tests/workmanager/WMWorkersSetupTest.java rename to main/src/androidTest/java/tests/workmanager/WMWorkersSetupTest.java diff --git a/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java b/main/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java similarity index 100% rename from src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java rename to main/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9a40236b9 --- /dev/null +++ b/main/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java similarity index 97% rename from src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java rename to main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java index 19d7017ba..fc3e7a02c 100644 --- a/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java +++ b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java @@ -3,6 +3,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.split.android.client.events.SplitEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.grammar.Treatments; @@ -171,6 +172,11 @@ public boolean isReady() { public void on(SplitEvent event, SplitEventTask task) { } + @Override + public void addEventListener(SplitEventListener listener) { + // no-op + } + @Override public boolean track(String trafficType, String eventType) { return false; diff --git a/src/main/java/io/split/android/client/Destroyer.java b/main/src/main/java/io/split/android/client/Destroyer.java similarity index 100% rename from src/main/java/io/split/android/client/Destroyer.java rename to main/src/main/java/io/split/android/client/Destroyer.java diff --git a/src/main/java/io/split/android/client/EvaluationResult.java b/main/src/main/java/io/split/android/client/EvaluationResult.java similarity index 98% rename from src/main/java/io/split/android/client/EvaluationResult.java rename to main/src/main/java/io/split/android/client/EvaluationResult.java index 3c50e0428..da529eba6 100644 --- a/src/main/java/io/split/android/client/EvaluationResult.java +++ b/main/src/main/java/io/split/android/client/EvaluationResult.java @@ -14,6 +14,7 @@ public EvaluationResult(String treatment, String label) { this(treatment, label, null, null, false); } + @VisibleForTesting public EvaluationResult(String treatment, String label, boolean impressionsDisabled) { this(treatment, label, null, null, impressionsDisabled); } diff --git a/src/main/java/io/split/android/client/Evaluator.java b/main/src/main/java/io/split/android/client/Evaluator.java similarity index 100% rename from src/main/java/io/split/android/client/Evaluator.java rename to main/src/main/java/io/split/android/client/Evaluator.java diff --git a/src/main/java/io/split/android/client/EvaluatorImpl.java b/main/src/main/java/io/split/android/client/EvaluatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/EvaluatorImpl.java rename to main/src/main/java/io/split/android/client/EvaluatorImpl.java diff --git a/src/main/java/io/split/android/client/EventsTracker.java b/main/src/main/java/io/split/android/client/EventsTracker.java similarity index 100% rename from src/main/java/io/split/android/client/EventsTracker.java rename to main/src/main/java/io/split/android/client/EventsTracker.java diff --git a/src/main/java/io/split/android/client/EventsTrackerImpl.java b/main/src/main/java/io/split/android/client/EventsTrackerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/EventsTrackerImpl.java rename to main/src/main/java/io/split/android/client/EventsTrackerImpl.java diff --git a/src/main/java/io/split/android/client/FeatureFlagFilter.java b/main/src/main/java/io/split/android/client/FeatureFlagFilter.java similarity index 100% rename from src/main/java/io/split/android/client/FeatureFlagFilter.java rename to main/src/main/java/io/split/android/client/FeatureFlagFilter.java diff --git a/src/main/java/io/split/android/client/FilterBuilder.java b/main/src/main/java/io/split/android/client/FilterBuilder.java similarity index 100% rename from src/main/java/io/split/android/client/FilterBuilder.java rename to main/src/main/java/io/split/android/client/FilterBuilder.java diff --git a/src/main/java/io/split/android/client/FilterGrouper.java b/main/src/main/java/io/split/android/client/FilterGrouper.java similarity index 100% rename from src/main/java/io/split/android/client/FilterGrouper.java rename to main/src/main/java/io/split/android/client/FilterGrouper.java diff --git a/src/main/java/io/split/android/client/FlagSetsFilter.java b/main/src/main/java/io/split/android/client/FlagSetsFilter.java similarity index 100% rename from src/main/java/io/split/android/client/FlagSetsFilter.java rename to main/src/main/java/io/split/android/client/FlagSetsFilter.java diff --git a/src/main/java/io/split/android/client/FlagSetsFilterImpl.java b/main/src/main/java/io/split/android/client/FlagSetsFilterImpl.java similarity index 100% rename from src/main/java/io/split/android/client/FlagSetsFilterImpl.java rename to main/src/main/java/io/split/android/client/FlagSetsFilterImpl.java diff --git a/src/main/java/io/split/android/client/ProcessedEventProperties.java b/main/src/main/java/io/split/android/client/ProcessedEventProperties.java similarity index 100% rename from src/main/java/io/split/android/client/ProcessedEventProperties.java rename to main/src/main/java/io/split/android/client/ProcessedEventProperties.java diff --git a/src/main/java/io/split/android/client/PropertyValidatorImpl.java b/main/src/main/java/io/split/android/client/PropertyValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/PropertyValidatorImpl.java rename to main/src/main/java/io/split/android/client/PropertyValidatorImpl.java diff --git a/src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java b/main/src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java similarity index 100% rename from src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java rename to main/src/main/java/io/split/android/client/RetryBackoffCounterTimerFactory.java diff --git a/src/main/java/io/split/android/client/RolloutCacheConfiguration.java b/main/src/main/java/io/split/android/client/RolloutCacheConfiguration.java similarity index 100% rename from src/main/java/io/split/android/client/RolloutCacheConfiguration.java rename to main/src/main/java/io/split/android/client/RolloutCacheConfiguration.java diff --git a/src/main/java/io/split/android/client/ServiceEndpoints.java b/main/src/main/java/io/split/android/client/ServiceEndpoints.java similarity index 100% rename from src/main/java/io/split/android/client/ServiceEndpoints.java rename to main/src/main/java/io/split/android/client/ServiceEndpoints.java diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/main/src/main/java/io/split/android/client/SplitClientConfig.java similarity index 99% rename from src/main/java/io/split/android/client/SplitClientConfig.java rename to main/src/main/java/io/split/android/client/SplitClientConfig.java index 3660ff761..0d5b0ecc8 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/main/src/main/java/io/split/android/client/SplitClientConfig.java @@ -13,7 +13,7 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; -import io.split.android.android_client.BuildConfig; +import io.split.android.client.main.BuildConfig; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.network.CertificatePinningConfiguration; import io.split.android.client.network.DevelopmentSslConfig; diff --git a/src/main/java/io/split/android/client/SplitClientFactory.java b/main/src/main/java/io/split/android/client/SplitClientFactory.java similarity index 100% rename from src/main/java/io/split/android/client/SplitClientFactory.java rename to main/src/main/java/io/split/android/client/SplitClientFactory.java diff --git a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java b/main/src/main/java/io/split/android/client/SplitClientFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/SplitClientFactoryImpl.java rename to main/src/main/java/io/split/android/client/SplitClientFactoryImpl.java diff --git a/src/main/java/io/split/android/client/SplitClientImpl.java b/main/src/main/java/io/split/android/client/SplitClientImpl.java similarity index 93% rename from src/main/java/io/split/android/client/SplitClientImpl.java rename to main/src/main/java/io/split/android/client/SplitClientImpl.java index 913bd005e..571efa169 100644 --- a/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/main/src/main/java/io/split/android/client/SplitClientImpl.java @@ -12,6 +12,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.events.SplitEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -190,14 +191,31 @@ public void on(SplitEvent event, SplitEventTask task) { checkNotNull(event); checkNotNull(task); - if (!event.equals(SplitEvent.SDK_READY_FROM_CACHE) && mEventsManager.eventAlreadyTriggered(event)) { - Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.", event.toString())); + // Allow registration for events that support replay (SDK_READY_FROM_CACHE and SDK_READY) + // Events with execution limit 1 can replay to late subscribers + if (!event.equals(SplitEvent.SDK_READY_FROM_CACHE) && + !event.equals(SplitEvent.SDK_READY) && + mEventsManager.eventAlreadyTriggered(event)) { + Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won't be emitted again. The callback won't be executed.", event.toString())); return; } mEventsManager.register(event, task); } + @Override + public void addEventListener(@NonNull SplitEventListener listener) { + if (mIsClientDestroyed) { + Logger.w("Client has already been destroyed. Cannot add event listener"); + return; + } + if (listener == null) { + Logger.w("SDK Event Listener cannot be null"); + return; + } + mEventsManager.registerEventListener(listener); + } + @Override public boolean track(String trafficType, String eventType) { return track(mKey.matchingKey(), trafficType, eventType, TRACK_DEFAULT_VALUE, null); diff --git a/src/main/java/io/split/android/client/SplitFactory.java b/main/src/main/java/io/split/android/client/SplitFactory.java similarity index 100% rename from src/main/java/io/split/android/client/SplitFactory.java rename to main/src/main/java/io/split/android/client/SplitFactory.java diff --git a/src/main/java/io/split/android/client/SplitFactoryBuilder.java b/main/src/main/java/io/split/android/client/SplitFactoryBuilder.java similarity index 100% rename from src/main/java/io/split/android/client/SplitFactoryBuilder.java rename to main/src/main/java/io/split/android/client/SplitFactoryBuilder.java diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/main/src/main/java/io/split/android/client/SplitFactoryHelper.java similarity index 100% rename from src/main/java/io/split/android/client/SplitFactoryHelper.java rename to main/src/main/java/io/split/android/client/SplitFactoryHelper.java diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java similarity index 99% rename from src/main/java/io/split/android/client/SplitFactoryImpl.java rename to main/src/main/java/io/split/android/client/SplitFactoryImpl.java index c623119bb..8bb12d71f 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -20,7 +20,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; -import io.split.android.android_client.BuildConfig; +import io.split.android.client.main.BuildConfig; import io.split.android.client.api.Key; import io.split.android.client.common.CompressionUtilProvider; import io.split.android.client.events.EventsManagerCoordinator; @@ -244,7 +244,8 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp mImpressionManager, mStorageContainer.getEventsStorage(), mEventsManagerCoordinator, - streamingComponents.getPushManagerEventBroadcaster() + streamingComponents.getPushManagerEventBroadcaster(), + mStorageContainer.getSplitsStorage() ); // Only available for integration tests if (synchronizerSpy != null) { diff --git a/src/main/java/io/split/android/client/SplitFilter.java b/main/src/main/java/io/split/android/client/SplitFilter.java similarity index 100% rename from src/main/java/io/split/android/client/SplitFilter.java rename to main/src/main/java/io/split/android/client/SplitFilter.java diff --git a/src/main/java/io/split/android/client/SplitManager.java b/main/src/main/java/io/split/android/client/SplitManager.java similarity index 100% rename from src/main/java/io/split/android/client/SplitManager.java rename to main/src/main/java/io/split/android/client/SplitManager.java diff --git a/src/main/java/io/split/android/client/SplitManagerImpl.java b/main/src/main/java/io/split/android/client/SplitManagerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/SplitManagerImpl.java rename to main/src/main/java/io/split/android/client/SplitManagerImpl.java diff --git a/src/main/java/io/split/android/client/SyncConfig.java b/main/src/main/java/io/split/android/client/SyncConfig.java similarity index 100% rename from src/main/java/io/split/android/client/SyncConfig.java rename to main/src/main/java/io/split/android/client/SyncConfig.java diff --git a/src/main/java/io/split/android/client/TestingConfig.java b/main/src/main/java/io/split/android/client/TestingConfig.java similarity index 92% rename from src/main/java/io/split/android/client/TestingConfig.java rename to main/src/main/java/io/split/android/client/TestingConfig.java index 8bd45a607..359b5e9d0 100644 --- a/src/main/java/io/split/android/client/TestingConfig.java +++ b/main/src/main/java/io/split/android/client/TestingConfig.java @@ -1,6 +1,6 @@ package io.split.android.client; -import io.split.android.android_client.BuildConfig; +import io.split.android.client.main.BuildConfig; import io.split.android.client.service.ServiceConstants; public class TestingConfig { diff --git a/src/main/java/io/split/android/client/TreatmentLabels.java b/main/src/main/java/io/split/android/client/TreatmentLabels.java similarity index 100% rename from src/main/java/io/split/android/client/TreatmentLabels.java rename to main/src/main/java/io/split/android/client/TreatmentLabels.java diff --git a/src/main/java/io/split/android/client/UserConsentManager.java b/main/src/main/java/io/split/android/client/UserConsentManager.java similarity index 100% rename from src/main/java/io/split/android/client/UserConsentManager.java rename to main/src/main/java/io/split/android/client/UserConsentManager.java diff --git a/src/main/java/io/split/android/client/UserConsentManagerImpl.java b/main/src/main/java/io/split/android/client/UserConsentManagerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/UserConsentManagerImpl.java rename to main/src/main/java/io/split/android/client/UserConsentManagerImpl.java diff --git a/src/main/java/io/split/android/client/api/SplitView.java b/main/src/main/java/io/split/android/client/api/SplitView.java similarity index 100% rename from src/main/java/io/split/android/client/api/SplitView.java rename to main/src/main/java/io/split/android/client/api/SplitView.java diff --git a/src/main/java/io/split/android/client/attributes/AttributesManagerFactory.java b/main/src/main/java/io/split/android/client/attributes/AttributesManagerFactory.java similarity index 100% rename from src/main/java/io/split/android/client/attributes/AttributesManagerFactory.java rename to main/src/main/java/io/split/android/client/attributes/AttributesManagerFactory.java diff --git a/src/main/java/io/split/android/client/attributes/AttributesManagerFactoryImpl.java b/main/src/main/java/io/split/android/client/attributes/AttributesManagerFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/attributes/AttributesManagerFactoryImpl.java rename to main/src/main/java/io/split/android/client/attributes/AttributesManagerFactoryImpl.java diff --git a/src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java b/main/src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java rename to main/src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java diff --git a/src/main/java/io/split/android/client/attributes/AttributesMerger.java b/main/src/main/java/io/split/android/client/attributes/AttributesMerger.java similarity index 100% rename from src/main/java/io/split/android/client/attributes/AttributesMerger.java rename to main/src/main/java/io/split/android/client/attributes/AttributesMerger.java diff --git a/src/main/java/io/split/android/client/attributes/AttributesMergerImpl.java b/main/src/main/java/io/split/android/client/attributes/AttributesMergerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/attributes/AttributesMergerImpl.java rename to main/src/main/java/io/split/android/client/attributes/AttributesMergerImpl.java diff --git a/src/main/java/io/split/android/client/common/CompressionType.java b/main/src/main/java/io/split/android/client/common/CompressionType.java similarity index 100% rename from src/main/java/io/split/android/client/common/CompressionType.java rename to main/src/main/java/io/split/android/client/common/CompressionType.java diff --git a/src/main/java/io/split/android/client/common/CompressionUtilProvider.java b/main/src/main/java/io/split/android/client/common/CompressionUtilProvider.java similarity index 100% rename from src/main/java/io/split/android/client/common/CompressionUtilProvider.java rename to main/src/main/java/io/split/android/client/common/CompressionUtilProvider.java diff --git a/src/main/java/io/split/android/client/dtos/Algorithm.java b/main/src/main/java/io/split/android/client/dtos/Algorithm.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Algorithm.java rename to main/src/main/java/io/split/android/client/dtos/Algorithm.java diff --git a/src/main/java/io/split/android/client/dtos/AllSegmentsChange.java b/main/src/main/java/io/split/android/client/dtos/AllSegmentsChange.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/AllSegmentsChange.java rename to main/src/main/java/io/split/android/client/dtos/AllSegmentsChange.java diff --git a/src/main/java/io/split/android/client/dtos/BetweenMatcherData.java b/main/src/main/java/io/split/android/client/dtos/BetweenMatcherData.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/BetweenMatcherData.java rename to main/src/main/java/io/split/android/client/dtos/BetweenMatcherData.java diff --git a/src/main/java/io/split/android/client/dtos/BetweenStringMatcherData.java b/main/src/main/java/io/split/android/client/dtos/BetweenStringMatcherData.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/BetweenStringMatcherData.java rename to main/src/main/java/io/split/android/client/dtos/BetweenStringMatcherData.java diff --git a/src/main/java/io/split/android/client/dtos/ChunkHeader.java b/main/src/main/java/io/split/android/client/dtos/ChunkHeader.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/ChunkHeader.java rename to main/src/main/java/io/split/android/client/dtos/ChunkHeader.java diff --git a/src/main/java/io/split/android/client/dtos/Condition.java b/main/src/main/java/io/split/android/client/dtos/Condition.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Condition.java rename to main/src/main/java/io/split/android/client/dtos/Condition.java diff --git a/src/main/java/io/split/android/client/dtos/ConditionType.java b/main/src/main/java/io/split/android/client/dtos/ConditionType.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/ConditionType.java rename to main/src/main/java/io/split/android/client/dtos/ConditionType.java diff --git a/src/main/java/io/split/android/client/dtos/Counter.java b/main/src/main/java/io/split/android/client/dtos/Counter.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Counter.java rename to main/src/main/java/io/split/android/client/dtos/Counter.java diff --git a/src/main/java/io/split/android/client/dtos/DataType.java b/main/src/main/java/io/split/android/client/dtos/DataType.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/DataType.java rename to main/src/main/java/io/split/android/client/dtos/DataType.java diff --git a/src/main/java/io/split/android/client/dtos/DependencyMatcherData.java b/main/src/main/java/io/split/android/client/dtos/DependencyMatcherData.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/DependencyMatcherData.java rename to main/src/main/java/io/split/android/client/dtos/DependencyMatcherData.java diff --git a/src/main/java/io/split/android/client/dtos/DeprecatedKeyImpression.java b/main/src/main/java/io/split/android/client/dtos/DeprecatedKeyImpression.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/DeprecatedKeyImpression.java rename to main/src/main/java/io/split/android/client/dtos/DeprecatedKeyImpression.java diff --git a/src/main/java/io/split/android/client/dtos/Event.java b/main/src/main/java/io/split/android/client/dtos/Event.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Event.java rename to main/src/main/java/io/split/android/client/dtos/Event.java diff --git a/src/main/java/io/split/android/client/dtos/Excluded.java b/main/src/main/java/io/split/android/client/dtos/Excluded.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Excluded.java rename to main/src/main/java/io/split/android/client/dtos/Excluded.java diff --git a/src/main/java/io/split/android/client/dtos/ExcludedSegment.java b/main/src/main/java/io/split/android/client/dtos/ExcludedSegment.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/ExcludedSegment.java rename to main/src/main/java/io/split/android/client/dtos/ExcludedSegment.java diff --git a/src/main/java/io/split/android/client/dtos/Helper.java b/main/src/main/java/io/split/android/client/dtos/Helper.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Helper.java rename to main/src/main/java/io/split/android/client/dtos/Helper.java diff --git a/src/main/java/io/split/android/client/dtos/HttpProxyDto.java b/main/src/main/java/io/split/android/client/dtos/HttpProxyDto.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/HttpProxyDto.java rename to main/src/main/java/io/split/android/client/dtos/HttpProxyDto.java diff --git a/src/main/java/io/split/android/client/dtos/Identifiable.java b/main/src/main/java/io/split/android/client/dtos/Identifiable.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Identifiable.java rename to main/src/main/java/io/split/android/client/dtos/Identifiable.java diff --git a/src/main/java/io/split/android/client/dtos/KeyImpression.java b/main/src/main/java/io/split/android/client/dtos/KeyImpression.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/KeyImpression.java rename to main/src/main/java/io/split/android/client/dtos/KeyImpression.java diff --git a/src/main/java/io/split/android/client/dtos/KeySelector.java b/main/src/main/java/io/split/android/client/dtos/KeySelector.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/KeySelector.java rename to main/src/main/java/io/split/android/client/dtos/KeySelector.java diff --git a/src/main/java/io/split/android/client/dtos/Matcher.java b/main/src/main/java/io/split/android/client/dtos/Matcher.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Matcher.java rename to main/src/main/java/io/split/android/client/dtos/Matcher.java diff --git a/src/main/java/io/split/android/client/dtos/MatcherCombiner.java b/main/src/main/java/io/split/android/client/dtos/MatcherCombiner.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/MatcherCombiner.java rename to main/src/main/java/io/split/android/client/dtos/MatcherCombiner.java diff --git a/src/main/java/io/split/android/client/dtos/MatcherGroup.java b/main/src/main/java/io/split/android/client/dtos/MatcherGroup.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/MatcherGroup.java rename to main/src/main/java/io/split/android/client/dtos/MatcherGroup.java diff --git a/src/main/java/io/split/android/client/dtos/MatcherType.java b/main/src/main/java/io/split/android/client/dtos/MatcherType.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/MatcherType.java rename to main/src/main/java/io/split/android/client/dtos/MatcherType.java diff --git a/src/main/java/io/split/android/client/dtos/Partition.java b/main/src/main/java/io/split/android/client/dtos/Partition.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Partition.java rename to main/src/main/java/io/split/android/client/dtos/Partition.java diff --git a/src/main/java/io/split/android/client/dtos/Prerequisite.java b/main/src/main/java/io/split/android/client/dtos/Prerequisite.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Prerequisite.java rename to main/src/main/java/io/split/android/client/dtos/Prerequisite.java diff --git a/src/main/java/io/split/android/client/dtos/RuleBasedSegment.java b/main/src/main/java/io/split/android/client/dtos/RuleBasedSegment.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/RuleBasedSegment.java rename to main/src/main/java/io/split/android/client/dtos/RuleBasedSegment.java diff --git a/src/main/java/io/split/android/client/dtos/RuleBasedSegmentChange.java b/main/src/main/java/io/split/android/client/dtos/RuleBasedSegmentChange.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/RuleBasedSegmentChange.java rename to main/src/main/java/io/split/android/client/dtos/RuleBasedSegmentChange.java diff --git a/src/main/java/io/split/android/client/dtos/Segment.java b/main/src/main/java/io/split/android/client/dtos/Segment.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Segment.java rename to main/src/main/java/io/split/android/client/dtos/Segment.java diff --git a/src/main/java/io/split/android/client/dtos/SegmentResponse.java b/main/src/main/java/io/split/android/client/dtos/SegmentResponse.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/SegmentResponse.java rename to main/src/main/java/io/split/android/client/dtos/SegmentResponse.java diff --git a/src/main/java/io/split/android/client/dtos/SegmentsChange.java b/main/src/main/java/io/split/android/client/dtos/SegmentsChange.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/SegmentsChange.java rename to main/src/main/java/io/split/android/client/dtos/SegmentsChange.java diff --git a/src/main/java/io/split/android/client/dtos/SerializableEvent.java b/main/src/main/java/io/split/android/client/dtos/SerializableEvent.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/SerializableEvent.java rename to main/src/main/java/io/split/android/client/dtos/SerializableEvent.java diff --git a/src/main/java/io/split/android/client/dtos/Split.java b/main/src/main/java/io/split/android/client/dtos/Split.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Split.java rename to main/src/main/java/io/split/android/client/dtos/Split.java diff --git a/src/main/java/io/split/android/client/dtos/SplitChange.java b/main/src/main/java/io/split/android/client/dtos/SplitChange.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/SplitChange.java rename to main/src/main/java/io/split/android/client/dtos/SplitChange.java diff --git a/src/main/java/io/split/android/client/dtos/Status.java b/main/src/main/java/io/split/android/client/dtos/Status.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/Status.java rename to main/src/main/java/io/split/android/client/dtos/Status.java diff --git a/src/main/java/io/split/android/client/dtos/TargetingRulesChange.java b/main/src/main/java/io/split/android/client/dtos/TargetingRulesChange.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/TargetingRulesChange.java rename to main/src/main/java/io/split/android/client/dtos/TargetingRulesChange.java diff --git a/src/main/java/io/split/android/client/dtos/TestImpressions.java b/main/src/main/java/io/split/android/client/dtos/TestImpressions.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/TestImpressions.java rename to main/src/main/java/io/split/android/client/dtos/TestImpressions.java diff --git a/src/main/java/io/split/android/client/dtos/URN.java b/main/src/main/java/io/split/android/client/dtos/URN.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/URN.java rename to main/src/main/java/io/split/android/client/dtos/URN.java diff --git a/src/main/java/io/split/android/client/dtos/UnaryNumericMatcherData.java b/main/src/main/java/io/split/android/client/dtos/UnaryNumericMatcherData.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/UnaryNumericMatcherData.java rename to main/src/main/java/io/split/android/client/dtos/UnaryNumericMatcherData.java diff --git a/src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java b/main/src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java rename to main/src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java diff --git a/src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java b/main/src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java rename to main/src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java diff --git a/src/main/java/io/split/android/client/dtos/WhitelistMatcherData.java b/main/src/main/java/io/split/android/client/dtos/WhitelistMatcherData.java similarity index 100% rename from src/main/java/io/split/android/client/dtos/WhitelistMatcherData.java rename to main/src/main/java/io/split/android/client/dtos/WhitelistMatcherData.java diff --git a/src/main/java/io/split/android/client/exceptions/ChangeNumberExceptionWrapper.java b/main/src/main/java/io/split/android/client/exceptions/ChangeNumberExceptionWrapper.java similarity index 100% rename from src/main/java/io/split/android/client/exceptions/ChangeNumberExceptionWrapper.java rename to main/src/main/java/io/split/android/client/exceptions/ChangeNumberExceptionWrapper.java diff --git a/src/main/java/io/split/android/client/exceptions/MySegmentsParsingException.java b/main/src/main/java/io/split/android/client/exceptions/MySegmentsParsingException.java similarity index 100% rename from src/main/java/io/split/android/client/exceptions/MySegmentsParsingException.java rename to main/src/main/java/io/split/android/client/exceptions/MySegmentsParsingException.java diff --git a/src/main/java/io/split/android/client/exceptions/ParentIsControlException.java b/main/src/main/java/io/split/android/client/exceptions/ParentIsControlException.java similarity index 100% rename from src/main/java/io/split/android/client/exceptions/ParentIsControlException.java rename to main/src/main/java/io/split/android/client/exceptions/ParentIsControlException.java diff --git a/src/main/java/io/split/android/client/exceptions/SplitInstantiationException.java b/main/src/main/java/io/split/android/client/exceptions/SplitInstantiationException.java similarity index 100% rename from src/main/java/io/split/android/client/exceptions/SplitInstantiationException.java rename to main/src/main/java/io/split/android/client/exceptions/SplitInstantiationException.java diff --git a/src/main/java/io/split/android/client/factory/FactoryMonitor.java b/main/src/main/java/io/split/android/client/factory/FactoryMonitor.java similarity index 100% rename from src/main/java/io/split/android/client/factory/FactoryMonitor.java rename to main/src/main/java/io/split/android/client/factory/FactoryMonitor.java diff --git a/src/main/java/io/split/android/client/factory/FactoryMonitorImpl.java b/main/src/main/java/io/split/android/client/factory/FactoryMonitorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/factory/FactoryMonitorImpl.java rename to main/src/main/java/io/split/android/client/factory/FactoryMonitorImpl.java diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatment.java b/main/src/main/java/io/split/android/client/fallback/FallbackTreatment.java similarity index 100% rename from src/main/java/io/split/android/client/fallback/FallbackTreatment.java rename to main/src/main/java/io/split/android/client/fallback/FallbackTreatment.java diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculator.java b/main/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculator.java similarity index 100% rename from src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculator.java rename to main/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculator.java diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorImpl.java b/main/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorImpl.java rename to main/src/main/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorImpl.java diff --git a/src/main/java/io/split/android/client/fallback/FallbackTreatmentsConfiguration.java b/main/src/main/java/io/split/android/client/fallback/FallbackTreatmentsConfiguration.java similarity index 100% rename from src/main/java/io/split/android/client/fallback/FallbackTreatmentsConfiguration.java rename to main/src/main/java/io/split/android/client/fallback/FallbackTreatmentsConfiguration.java diff --git a/src/main/java/io/split/android/client/fallback/FallbacksSanitizer.java b/main/src/main/java/io/split/android/client/fallback/FallbacksSanitizer.java similarity index 100% rename from src/main/java/io/split/android/client/fallback/FallbacksSanitizer.java rename to main/src/main/java/io/split/android/client/fallback/FallbacksSanitizer.java diff --git a/src/main/java/io/split/android/client/fallback/FallbacksSanitizerImpl.java b/main/src/main/java/io/split/android/client/fallback/FallbacksSanitizerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/fallback/FallbacksSanitizerImpl.java rename to main/src/main/java/io/split/android/client/fallback/FallbacksSanitizerImpl.java diff --git a/src/main/java/io/split/android/client/impressions/DecoratedImpression.java b/main/src/main/java/io/split/android/client/impressions/DecoratedImpression.java similarity index 100% rename from src/main/java/io/split/android/client/impressions/DecoratedImpression.java rename to main/src/main/java/io/split/android/client/impressions/DecoratedImpression.java diff --git a/src/main/java/io/split/android/client/impressions/DecoratedImpressionListener.java b/main/src/main/java/io/split/android/client/impressions/DecoratedImpressionListener.java similarity index 100% rename from src/main/java/io/split/android/client/impressions/DecoratedImpressionListener.java rename to main/src/main/java/io/split/android/client/impressions/DecoratedImpressionListener.java diff --git a/src/main/java/io/split/android/client/impressions/Impression.java b/main/src/main/java/io/split/android/client/impressions/Impression.java similarity index 100% rename from src/main/java/io/split/android/client/impressions/Impression.java rename to main/src/main/java/io/split/android/client/impressions/Impression.java diff --git a/src/main/java/io/split/android/client/impressions/ImpressionListener.java b/main/src/main/java/io/split/android/client/impressions/ImpressionListener.java similarity index 100% rename from src/main/java/io/split/android/client/impressions/ImpressionListener.java rename to main/src/main/java/io/split/android/client/impressions/ImpressionListener.java diff --git a/src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java b/main/src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java similarity index 100% rename from src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java rename to main/src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java diff --git a/src/main/java/io/split/android/client/impressions/SyncImpressionListener.java b/main/src/main/java/io/split/android/client/impressions/SyncImpressionListener.java similarity index 100% rename from src/main/java/io/split/android/client/impressions/SyncImpressionListener.java rename to main/src/main/java/io/split/android/client/impressions/SyncImpressionListener.java diff --git a/src/main/java/io/split/android/client/lifecycle/SplitLifecycleAware.java b/main/src/main/java/io/split/android/client/lifecycle/SplitLifecycleAware.java similarity index 100% rename from src/main/java/io/split/android/client/lifecycle/SplitLifecycleAware.java rename to main/src/main/java/io/split/android/client/lifecycle/SplitLifecycleAware.java diff --git a/src/main/java/io/split/android/client/lifecycle/SplitLifecycleManager.java b/main/src/main/java/io/split/android/client/lifecycle/SplitLifecycleManager.java similarity index 100% rename from src/main/java/io/split/android/client/lifecycle/SplitLifecycleManager.java rename to main/src/main/java/io/split/android/client/lifecycle/SplitLifecycleManager.java diff --git a/src/main/java/io/split/android/client/lifecycle/SplitLifecycleManagerImpl.java b/main/src/main/java/io/split/android/client/lifecycle/SplitLifecycleManagerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/lifecycle/SplitLifecycleManagerImpl.java rename to main/src/main/java/io/split/android/client/lifecycle/SplitLifecycleManagerImpl.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostFileParser.java b/main/src/main/java/io/split/android/client/localhost/LocalhostFileParser.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostFileParser.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostFileParser.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java b/main/src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java b/main/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostPropertiesFileParser.java b/main/src/main/java/io/split/android/client/localhost/LocalhostPropertiesFileParser.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostPropertiesFileParser.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostPropertiesFileParser.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorage.java b/main/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorage.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorage.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorageProvider.java b/main/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorageProvider.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorageProvider.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorageProvider.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java similarity index 96% rename from src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index 00c6a4f51..1b5e58499 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -25,6 +25,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; +import io.split.android.client.events.SplitEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -257,18 +258,32 @@ public boolean isReady() { return mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY); } + @Override public void on(SplitEvent event, SplitEventTask task) { checkNotNull(event); checkNotNull(task); if (!event.equals(SplitEvent.SDK_READY_FROM_CACHE) && mEventsManager.eventAlreadyTriggered(event)) { - Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.", event)); + Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won't be emitted again. The callback won't be executed.", event)); return; } mEventsManager.register(event, task); } + @Override + public void addEventListener(@NonNull SplitEventListener listener) { + if (mIsClientDestroyed) { + Logger.w("Client has already been destroyed. Cannot add event listener"); + return; + } + if (listener == null) { + Logger.w("SDK Event Listener cannot be null"); + return; + } + mEventsManager.registerEventListener(listener); + } + @Override public boolean track(String trafficType, String eventType) { return false; diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java similarity index 91% rename from src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java index 68e80dd28..86603c14e 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java @@ -18,9 +18,13 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.ArrayList; + +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.Split; import io.split.android.client.events.EventsManagerCoordinator; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.ServiceConstants; import io.split.android.client.storage.legacy.FileStorage; import io.split.android.client.storage.splits.ProcessedSplitChange; @@ -215,9 +219,12 @@ private void loadSplits() { } } if (!content.equals(mLastContentLoaded)) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + // Cache path metadata: initialCacheLoad=false (loaded from file), timestamp=null for localhost + EventMetadata cacheMetadata = EventMetadataHelpers.createReadyMetadata(null, false); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, cacheMetadata); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + EventMetadata updateMetadata = createUpdatedFlagsMetadata(); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, updateMetadata); } mLastContentLoaded = content; } @@ -258,4 +265,9 @@ private void copyFileResourceToDataFolder(String fileName, FileStorage fileStora Logger.e(e.getLocalizedMessage()); } } + + private EventMetadata createUpdatedFlagsMetadata() { + List updatedSplitNames = new ArrayList<>(mInMemorySplits.keySet()); + return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); + } } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java diff --git a/src/main/java/io/split/android/client/localhost/LocalhostYamlFileParser.java b/main/src/main/java/io/split/android/client/localhost/LocalhostYamlFileParser.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/LocalhostYamlFileParser.java rename to main/src/main/java/io/split/android/client/localhost/LocalhostYamlFileParser.java diff --git a/src/main/java/io/split/android/client/localhost/SplitHelper.java b/main/src/main/java/io/split/android/client/localhost/SplitHelper.java similarity index 100% rename from src/main/java/io/split/android/client/localhost/SplitHelper.java rename to main/src/main/java/io/split/android/client/localhost/SplitHelper.java diff --git a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java b/main/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java similarity index 66% rename from src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java rename to main/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java index 2fd5cbded..cc33debfd 100644 --- a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java +++ b/main/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java @@ -1,5 +1,7 @@ package io.split.android.client.localhost.shared; +import androidx.annotation.VisibleForTesting; + import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; @@ -31,6 +33,7 @@ public class LocalhostSplitClientContainerImpl extends BaseSplitClientContainer private final EventsManagerCoordinator mEventsManagerCoordinator; private final SplitTaskExecutor mSplitTaskExecutor; private final FlagSetsFilter mFlagSetsFilter; + private final SplitEventsManagerFactory mEventsManagerFactory; public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, SplitClientConfig config, @@ -42,6 +45,24 @@ public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, EventsManagerCoordinator eventsManagerCoordinator, SplitTaskExecutor taskExecutor, FlagSetsFilter flagSetsFilter) { + this(splitFactory, config, splitsStorage, splitParser, attributesManagerFactory, + attributesMerger, telemetryStorageProducer, eventsManagerCoordinator, + taskExecutor, flagSetsFilter, + new DefaultSplitEventsManagerFactory(taskExecutor, config)); + } + + @VisibleForTesting + LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, + SplitClientConfig config, + SplitsStorage splitsStorage, + SplitParser splitParser, + AttributesManagerFactory attributesManagerFactory, + AttributesMerger attributesMerger, + TelemetryStorageProducer telemetryStorageProducer, + EventsManagerCoordinator eventsManagerCoordinator, + SplitTaskExecutor taskExecutor, + FlagSetsFilter flagSetsFilter, + SplitEventsManagerFactory eventsManagerFactory) { mSplitFactory = splitFactory; mConfig = config; mSplitStorage = splitsStorage; @@ -52,13 +73,14 @@ public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, mEventsManagerCoordinator = eventsManagerCoordinator; mSplitTaskExecutor = taskExecutor; mFlagSetsFilter = flagSetsFilter; + mEventsManagerFactory = eventsManagerFactory; } @Override protected void createNewClient(Key key) { - SplitEventsManager eventsManager = new SplitEventsManager(mSplitTaskExecutor, mConfig.blockUntilReady()); + SplitEventsManager eventsManager = mEventsManagerFactory.create(); eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); - eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + eventsManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); AttributesStorageImpl attributesStorage = new AttributesStorageImpl(); @@ -88,4 +110,19 @@ protected void createNewClient(Key key) { public void destroy() { // No-op } + + private static class DefaultSplitEventsManagerFactory implements SplitEventsManagerFactory { + private final SplitTaskExecutor mTaskExecutor; + private final int mBlockUntilReady; + + DefaultSplitEventsManagerFactory(SplitTaskExecutor taskExecutor, SplitClientConfig config) { + mTaskExecutor = taskExecutor; + mBlockUntilReady = config.blockUntilReady(); + } + + @Override + public SplitEventsManager create() { + return new SplitEventsManager(mTaskExecutor, mBlockUntilReady); + } + } } diff --git a/main/src/main/java/io/split/android/client/localhost/shared/SplitEventsManagerFactory.java b/main/src/main/java/io/split/android/client/localhost/shared/SplitEventsManagerFactory.java new file mode 100644 index 000000000..1dfb51404 --- /dev/null +++ b/main/src/main/java/io/split/android/client/localhost/shared/SplitEventsManagerFactory.java @@ -0,0 +1,17 @@ +package io.split.android.client.localhost.shared; + +import io.split.android.client.events.SplitEventsManager; + +/** + * Factory interface for creating SplitEventsManager instances. + * Package-local interface to allow testing by injecting mock implementations. + */ +interface SplitEventsManagerFactory { + /** + * Creates a new SplitEventsManager instance. + * + * @return a new SplitEventsManager instance + */ + SplitEventsManager create(); +} + diff --git a/src/main/java/io/split/android/client/network/Algorithm.java b/main/src/main/java/io/split/android/client/network/Algorithm.java similarity index 100% rename from src/main/java/io/split/android/client/network/Algorithm.java rename to main/src/main/java/io/split/android/client/network/Algorithm.java diff --git a/src/main/java/io/split/android/client/network/AuthenticatedRequest.java b/main/src/main/java/io/split/android/client/network/AuthenticatedRequest.java similarity index 100% rename from src/main/java/io/split/android/client/network/AuthenticatedRequest.java rename to main/src/main/java/io/split/android/client/network/AuthenticatedRequest.java diff --git a/src/main/java/io/split/android/client/network/Authenticator.java b/main/src/main/java/io/split/android/client/network/Authenticator.java similarity index 100% rename from src/main/java/io/split/android/client/network/Authenticator.java rename to main/src/main/java/io/split/android/client/network/Authenticator.java diff --git a/src/main/java/io/split/android/client/network/Base64Decoder.java b/main/src/main/java/io/split/android/client/network/Base64Decoder.java similarity index 100% rename from src/main/java/io/split/android/client/network/Base64Decoder.java rename to main/src/main/java/io/split/android/client/network/Base64Decoder.java diff --git a/src/main/java/io/split/android/client/network/Base64Encoder.java b/main/src/main/java/io/split/android/client/network/Base64Encoder.java similarity index 100% rename from src/main/java/io/split/android/client/network/Base64Encoder.java rename to main/src/main/java/io/split/android/client/network/Base64Encoder.java diff --git a/src/main/java/io/split/android/client/network/BaseHttpResponse.java b/main/src/main/java/io/split/android/client/network/BaseHttpResponse.java similarity index 100% rename from src/main/java/io/split/android/client/network/BaseHttpResponse.java rename to main/src/main/java/io/split/android/client/network/BaseHttpResponse.java diff --git a/src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java b/main/src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java rename to main/src/main/java/io/split/android/client/network/BaseHttpResponseImpl.java diff --git a/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java b/main/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java similarity index 100% rename from src/main/java/io/split/android/client/network/BasicCredentialsProvider.java rename to main/src/main/java/io/split/android/client/network/BasicCredentialsProvider.java diff --git a/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java b/main/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java similarity index 100% rename from src/main/java/io/split/android/client/network/BearerCredentialsProvider.java rename to main/src/main/java/io/split/android/client/network/BearerCredentialsProvider.java diff --git a/src/main/java/io/split/android/client/network/CertificateChecker.java b/main/src/main/java/io/split/android/client/network/CertificateChecker.java similarity index 100% rename from src/main/java/io/split/android/client/network/CertificateChecker.java rename to main/src/main/java/io/split/android/client/network/CertificateChecker.java diff --git a/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java b/main/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java similarity index 100% rename from src/main/java/io/split/android/client/network/CertificateCheckerHelper.java rename to main/src/main/java/io/split/android/client/network/CertificateCheckerHelper.java diff --git a/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java b/main/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/CertificateCheckerImpl.java rename to main/src/main/java/io/split/android/client/network/CertificateCheckerImpl.java diff --git a/src/main/java/io/split/android/client/network/CertificatePin.java b/main/src/main/java/io/split/android/client/network/CertificatePin.java similarity index 100% rename from src/main/java/io/split/android/client/network/CertificatePin.java rename to main/src/main/java/io/split/android/client/network/CertificatePin.java diff --git a/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java b/main/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java similarity index 100% rename from src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java rename to main/src/main/java/io/split/android/client/network/CertificatePinningConfiguration.java diff --git a/src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java b/main/src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java similarity index 100% rename from src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java rename to main/src/main/java/io/split/android/client/network/CertificatePinningConfigurationProvider.java diff --git a/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java b/main/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java similarity index 100% rename from src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java rename to main/src/main/java/io/split/android/client/network/CertificatePinningFailureListener.java diff --git a/src/main/java/io/split/android/client/network/ChainCleaner.java b/main/src/main/java/io/split/android/client/network/ChainCleaner.java similarity index 100% rename from src/main/java/io/split/android/client/network/ChainCleaner.java rename to main/src/main/java/io/split/android/client/network/ChainCleaner.java diff --git a/src/main/java/io/split/android/client/network/ChainCleanerImpl.java b/main/src/main/java/io/split/android/client/network/ChainCleanerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/ChainCleanerImpl.java rename to main/src/main/java/io/split/android/client/network/ChainCleanerImpl.java diff --git a/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java b/main/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java similarity index 100% rename from src/main/java/io/split/android/client/network/DefaultBase64Decoder.java rename to main/src/main/java/io/split/android/client/network/DefaultBase64Decoder.java diff --git a/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java b/main/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java similarity index 100% rename from src/main/java/io/split/android/client/network/DefaultBase64Encoder.java rename to main/src/main/java/io/split/android/client/network/DefaultBase64Encoder.java diff --git a/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java b/main/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java similarity index 100% rename from src/main/java/io/split/android/client/network/DevelopmentSslConfig.java rename to main/src/main/java/io/split/android/client/network/DevelopmentSslConfig.java diff --git a/src/main/java/io/split/android/client/network/HttpClient.java b/main/src/main/java/io/split/android/client/network/HttpClient.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpClient.java rename to main/src/main/java/io/split/android/client/network/HttpClient.java diff --git a/src/main/java/io/split/android/client/network/HttpClientImpl.java b/main/src/main/java/io/split/android/client/network/HttpClientImpl.java similarity index 98% rename from src/main/java/io/split/android/client/network/HttpClientImpl.java rename to main/src/main/java/io/split/android/client/network/HttpClientImpl.java index 3b2a4be33..f41271796 100644 --- a/src/main/java/io/split/android/client/network/HttpClientImpl.java +++ b/main/src/main/java/io/split/android/client/network/HttpClientImpl.java @@ -159,6 +159,12 @@ public void close() { } + @VisibleForTesting + @Nullable + SSLSocketFactory getSslSocketFactory() { + return mSslSocketFactory; + } + private Proxy initializeProxy(HttpProxy proxy) { if (proxy != null) { return new Proxy( @@ -279,7 +285,7 @@ public HttpClient build() { if (mProxy != null) { mSslSocketFactory = createSslSocketFactoryFromProxy(mProxy); - } else { + } else if (LegacyTlsUpdater.couldBeOld()) { try { mSslSocketFactory = new Tls12OnlySocketFactory(); } catch (NoSuchAlgorithmException | KeyManagementException e) { @@ -287,6 +293,9 @@ public HttpClient build() { } catch (Exception e) { Logger.e("Unknown TLS v12 error: " + e.getLocalizedMessage()); } + } else { + // Use platform default + mSslSocketFactory = null; } } diff --git a/src/main/java/io/split/android/client/network/HttpException.java b/main/src/main/java/io/split/android/client/network/HttpException.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpException.java rename to main/src/main/java/io/split/android/client/network/HttpException.java diff --git a/src/main/java/io/split/android/client/network/HttpMethod.java b/main/src/main/java/io/split/android/client/network/HttpMethod.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpMethod.java rename to main/src/main/java/io/split/android/client/network/HttpMethod.java diff --git a/src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java b/main/src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java rename to main/src/main/java/io/split/android/client/network/HttpOverTunnelExecutor.java diff --git a/src/main/java/io/split/android/client/network/HttpProxy.java b/main/src/main/java/io/split/android/client/network/HttpProxy.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpProxy.java rename to main/src/main/java/io/split/android/client/network/HttpProxy.java diff --git a/src/main/java/io/split/android/client/network/HttpRequest.java b/main/src/main/java/io/split/android/client/network/HttpRequest.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpRequest.java rename to main/src/main/java/io/split/android/client/network/HttpRequest.java diff --git a/src/main/java/io/split/android/client/network/HttpRequestHelper.java b/main/src/main/java/io/split/android/client/network/HttpRequestHelper.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpRequestHelper.java rename to main/src/main/java/io/split/android/client/network/HttpRequestHelper.java diff --git a/src/main/java/io/split/android/client/network/HttpRequestImpl.java b/main/src/main/java/io/split/android/client/network/HttpRequestImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpRequestImpl.java rename to main/src/main/java/io/split/android/client/network/HttpRequestImpl.java diff --git a/src/main/java/io/split/android/client/network/HttpResponse.java b/main/src/main/java/io/split/android/client/network/HttpResponse.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpResponse.java rename to main/src/main/java/io/split/android/client/network/HttpResponse.java diff --git a/src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java b/main/src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java rename to main/src/main/java/io/split/android/client/network/HttpResponseConnectionAdapter.java diff --git a/src/main/java/io/split/android/client/network/HttpResponseImpl.java b/main/src/main/java/io/split/android/client/network/HttpResponseImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpResponseImpl.java rename to main/src/main/java/io/split/android/client/network/HttpResponseImpl.java diff --git a/src/main/java/io/split/android/client/network/HttpStreamRequest.java b/main/src/main/java/io/split/android/client/network/HttpStreamRequest.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpStreamRequest.java rename to main/src/main/java/io/split/android/client/network/HttpStreamRequest.java diff --git a/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java b/main/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java rename to main/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java diff --git a/src/main/java/io/split/android/client/network/HttpStreamResponse.java b/main/src/main/java/io/split/android/client/network/HttpStreamResponse.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpStreamResponse.java rename to main/src/main/java/io/split/android/client/network/HttpStreamResponse.java diff --git a/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java b/main/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java rename to main/src/main/java/io/split/android/client/network/HttpStreamResponseImpl.java diff --git a/src/main/java/io/split/android/client/network/LegacyTlsUpdater.java b/main/src/main/java/io/split/android/client/network/LegacyTlsUpdater.java similarity index 100% rename from src/main/java/io/split/android/client/network/LegacyTlsUpdater.java rename to main/src/main/java/io/split/android/client/network/LegacyTlsUpdater.java diff --git a/src/main/java/io/split/android/client/network/PercentEscaper.java b/main/src/main/java/io/split/android/client/network/PercentEscaper.java similarity index 100% rename from src/main/java/io/split/android/client/network/PercentEscaper.java rename to main/src/main/java/io/split/android/client/network/PercentEscaper.java diff --git a/src/main/java/io/split/android/client/network/PinEncoder.java b/main/src/main/java/io/split/android/client/network/PinEncoder.java similarity index 100% rename from src/main/java/io/split/android/client/network/PinEncoder.java rename to main/src/main/java/io/split/android/client/network/PinEncoder.java diff --git a/src/main/java/io/split/android/client/network/PinEncoderImpl.java b/main/src/main/java/io/split/android/client/network/PinEncoderImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/PinEncoderImpl.java rename to main/src/main/java/io/split/android/client/network/PinEncoderImpl.java diff --git a/src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java b/main/src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java similarity index 100% rename from src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java rename to main/src/main/java/io/split/android/client/network/ProxyCacertConnectionHandler.java diff --git a/src/main/java/io/split/android/client/network/ProxyConfiguration.java b/main/src/main/java/io/split/android/client/network/ProxyConfiguration.java similarity index 100% rename from src/main/java/io/split/android/client/network/ProxyConfiguration.java rename to main/src/main/java/io/split/android/client/network/ProxyConfiguration.java diff --git a/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java b/main/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java similarity index 100% rename from src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java rename to main/src/main/java/io/split/android/client/network/ProxyCredentialsProvider.java diff --git a/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java b/main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java similarity index 100% rename from src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java rename to main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProvider.java diff --git a/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java b/main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java rename to main/src/main/java/io/split/android/client/network/ProxySslSocketFactoryProviderImpl.java diff --git a/src/main/java/io/split/android/client/network/RawHttpResponseParser.java b/main/src/main/java/io/split/android/client/network/RawHttpResponseParser.java similarity index 100% rename from src/main/java/io/split/android/client/network/RawHttpResponseParser.java rename to main/src/main/java/io/split/android/client/network/RawHttpResponseParser.java diff --git a/src/main/java/io/split/android/client/network/SdkTargetPath.java b/main/src/main/java/io/split/android/client/network/SdkTargetPath.java similarity index 100% rename from src/main/java/io/split/android/client/network/SdkTargetPath.java rename to main/src/main/java/io/split/android/client/network/SdkTargetPath.java diff --git a/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java b/main/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java similarity index 100% rename from src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java rename to main/src/main/java/io/split/android/client/network/SplitAuthenticatedRequest.java diff --git a/src/main/java/io/split/android/client/network/SplitAuthenticator.java b/main/src/main/java/io/split/android/client/network/SplitAuthenticator.java similarity index 100% rename from src/main/java/io/split/android/client/network/SplitAuthenticator.java rename to main/src/main/java/io/split/android/client/network/SplitAuthenticator.java diff --git a/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java b/main/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java similarity index 100% rename from src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java rename to main/src/main/java/io/split/android/client/network/SplitBasicAuthenticator.java diff --git a/src/main/java/io/split/android/client/network/SplitHttpHeadersBuilder.java b/main/src/main/java/io/split/android/client/network/SplitHttpHeadersBuilder.java similarity index 100% rename from src/main/java/io/split/android/client/network/SplitHttpHeadersBuilder.java rename to main/src/main/java/io/split/android/client/network/SplitHttpHeadersBuilder.java diff --git a/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java b/main/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java similarity index 100% rename from src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java rename to main/src/main/java/io/split/android/client/network/SplitUrlConnectionAuthenticator.java diff --git a/src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java b/main/src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java similarity index 100% rename from src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java rename to main/src/main/java/io/split/android/client/network/SslProxyTunnelEstablisher.java diff --git a/src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java b/main/src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java similarity index 100% rename from src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java rename to main/src/main/java/io/split/android/client/network/Tls12OnlySocketFactory.java diff --git a/src/main/java/io/split/android/client/network/TrustManagerProvider.java b/main/src/main/java/io/split/android/client/network/TrustManagerProvider.java similarity index 100% rename from src/main/java/io/split/android/client/network/TrustManagerProvider.java rename to main/src/main/java/io/split/android/client/network/TrustManagerProvider.java diff --git a/src/main/java/io/split/android/client/network/URIBuilder.java b/main/src/main/java/io/split/android/client/network/URIBuilder.java similarity index 100% rename from src/main/java/io/split/android/client/network/URIBuilder.java rename to main/src/main/java/io/split/android/client/network/URIBuilder.java diff --git a/src/main/java/io/split/android/client/network/UnicodeEscaper.java b/main/src/main/java/io/split/android/client/network/UnicodeEscaper.java similarity index 100% rename from src/main/java/io/split/android/client/network/UnicodeEscaper.java rename to main/src/main/java/io/split/android/client/network/UnicodeEscaper.java diff --git a/src/main/java/io/split/android/client/network/UrlEscapers.java b/main/src/main/java/io/split/android/client/network/UrlEscapers.java similarity index 100% rename from src/main/java/io/split/android/client/network/UrlEscapers.java rename to main/src/main/java/io/split/android/client/network/UrlEscapers.java diff --git a/src/main/java/io/split/android/client/network/UrlSanitizer.java b/main/src/main/java/io/split/android/client/network/UrlSanitizer.java similarity index 100% rename from src/main/java/io/split/android/client/network/UrlSanitizer.java rename to main/src/main/java/io/split/android/client/network/UrlSanitizer.java diff --git a/src/main/java/io/split/android/client/network/UrlSanitizerImpl.java b/main/src/main/java/io/split/android/client/network/UrlSanitizerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/network/UrlSanitizerImpl.java rename to main/src/main/java/io/split/android/client/network/UrlSanitizerImpl.java diff --git a/src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java b/main/src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java rename to main/src/main/java/io/split/android/client/service/CleanUpDatabaseTask.java diff --git a/src/main/java/io/split/android/client/service/ServiceConstants.java b/main/src/main/java/io/split/android/client/service/ServiceConstants.java similarity index 100% rename from src/main/java/io/split/android/client/service/ServiceConstants.java rename to main/src/main/java/io/split/android/client/service/ServiceConstants.java diff --git a/src/main/java/io/split/android/client/service/ServiceFactory.java b/main/src/main/java/io/split/android/client/service/ServiceFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/ServiceFactory.java rename to main/src/main/java/io/split/android/client/service/ServiceFactory.java diff --git a/src/main/java/io/split/android/client/service/SplitApiFacade.java b/main/src/main/java/io/split/android/client/service/SplitApiFacade.java similarity index 100% rename from src/main/java/io/split/android/client/service/SplitApiFacade.java rename to main/src/main/java/io/split/android/client/service/SplitApiFacade.java diff --git a/src/main/java/io/split/android/client/service/attributes/AttributeTaskFactory.java b/main/src/main/java/io/split/android/client/service/attributes/AttributeTaskFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/attributes/AttributeTaskFactory.java rename to main/src/main/java/io/split/android/client/service/attributes/AttributeTaskFactory.java diff --git a/src/main/java/io/split/android/client/service/attributes/AttributeTaskFactoryImpl.java b/main/src/main/java/io/split/android/client/service/attributes/AttributeTaskFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/attributes/AttributeTaskFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/attributes/AttributeTaskFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTask.java b/main/src/main/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTask.java rename to main/src/main/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTask.java diff --git a/src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java b/main/src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java rename to main/src/main/java/io/split/android/client/service/attributes/LoadAttributesTask.java diff --git a/src/main/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTask.java b/main/src/main/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTask.java rename to main/src/main/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTask.java diff --git a/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java b/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/events/EventsRecorderTask.java rename to main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java diff --git a/src/main/java/io/split/android/client/service/events/EventsRecorderTaskConfig.java b/main/src/main/java/io/split/android/client/service/events/EventsRecorderTaskConfig.java similarity index 100% rename from src/main/java/io/split/android/client/service/events/EventsRecorderTaskConfig.java rename to main/src/main/java/io/split/android/client/service/events/EventsRecorderTaskConfig.java diff --git a/src/main/java/io/split/android/client/service/events/EventsRequestBodySerializer.java b/main/src/main/java/io/split/android/client/service/events/EventsRequestBodySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/service/events/EventsRequestBodySerializer.java rename to main/src/main/java/io/split/android/client/service/events/EventsRequestBodySerializer.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java b/main/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java rename to main/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitClientEventTaskExecutor.java b/main/src/main/java/io/split/android/client/service/executor/SplitClientEventTaskExecutor.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitClientEventTaskExecutor.java rename to main/src/main/java/io/split/android/client/service/executor/SplitClientEventTaskExecutor.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitSingleThreadTaskExecutor.java b/main/src/main/java/io/split/android/client/service/executor/SplitSingleThreadTaskExecutor.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitSingleThreadTaskExecutor.java rename to main/src/main/java/io/split/android/client/service/executor/SplitSingleThreadTaskExecutor.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskBatchWrapper.java b/main/src/main/java/io/split/android/client/service/executor/SplitTaskBatchWrapper.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskBatchWrapper.java rename to main/src/main/java/io/split/android/client/service/executor/SplitTaskBatchWrapper.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java b/main/src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java rename to main/src/main/java/io/split/android/client/service/executor/SplitTaskExecutorImpl.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java b/main/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java rename to main/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java b/main/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskSerialWrapper.java b/main/src/main/java/io/split/android/client/service/executor/SplitTaskSerialWrapper.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/SplitTaskSerialWrapper.java rename to main/src/main/java/io/split/android/client/service/executor/SplitTaskSerialWrapper.java diff --git a/src/main/java/io/split/android/client/service/executor/TaskWrapper.java b/main/src/main/java/io/split/android/client/service/executor/TaskWrapper.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/TaskWrapper.java rename to main/src/main/java/io/split/android/client/service/executor/TaskWrapper.java diff --git a/src/main/java/io/split/android/client/service/executor/ThreadFactoryBuilder.java b/main/src/main/java/io/split/android/client/service/executor/ThreadFactoryBuilder.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/ThreadFactoryBuilder.java rename to main/src/main/java/io/split/android/client/service/executor/ThreadFactoryBuilder.java diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitDeferredTaskItem.java b/main/src/main/java/io/split/android/client/service/executor/parallel/SplitDeferredTaskItem.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/parallel/SplitDeferredTaskItem.java rename to main/src/main/java/io/split/android/client/service/executor/parallel/SplitDeferredTaskItem.java diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutor.java b/main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutor.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutor.java rename to main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutor.java diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java b/main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java rename to main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java b/main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java b/main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java rename to main/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java diff --git a/src/main/java/io/split/android/client/service/http/HttpFetcher.java b/main/src/main/java/io/split/android/client/service/http/HttpFetcher.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpFetcher.java rename to main/src/main/java/io/split/android/client/service/http/HttpFetcher.java diff --git a/src/main/java/io/split/android/client/service/http/HttpFetcherException.java b/main/src/main/java/io/split/android/client/service/http/HttpFetcherException.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpFetcherException.java rename to main/src/main/java/io/split/android/client/service/http/HttpFetcherException.java diff --git a/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java b/main/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java similarity index 98% rename from src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java rename to main/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java index d45e36f6f..b952ab5c8 100644 --- a/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java +++ b/main/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java @@ -8,7 +8,7 @@ import java.net.URI; import java.util.Map; -import io.split.android.android_client.BuildConfig; +import io.split.android.client.main.BuildConfig; import io.split.android.client.ServiceEndpoints; import io.split.android.client.network.HttpClient; import io.split.android.client.network.HttpException; diff --git a/src/main/java/io/split/android/client/service/http/HttpGeneralException.java b/main/src/main/java/io/split/android/client/service/http/HttpGeneralException.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpGeneralException.java rename to main/src/main/java/io/split/android/client/service/http/HttpGeneralException.java diff --git a/src/main/java/io/split/android/client/service/http/HttpRecorder.java b/main/src/main/java/io/split/android/client/service/http/HttpRecorder.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpRecorder.java rename to main/src/main/java/io/split/android/client/service/http/HttpRecorder.java diff --git a/src/main/java/io/split/android/client/service/http/HttpRecorderException.java b/main/src/main/java/io/split/android/client/service/http/HttpRecorderException.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpRecorderException.java rename to main/src/main/java/io/split/android/client/service/http/HttpRecorderException.java diff --git a/src/main/java/io/split/android/client/service/http/HttpRecorderImpl.java b/main/src/main/java/io/split/android/client/service/http/HttpRecorderImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpRecorderImpl.java rename to main/src/main/java/io/split/android/client/service/http/HttpRecorderImpl.java diff --git a/src/main/java/io/split/android/client/service/http/HttpRequestBodySerializer.java b/main/src/main/java/io/split/android/client/service/http/HttpRequestBodySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpRequestBodySerializer.java rename to main/src/main/java/io/split/android/client/service/http/HttpRequestBodySerializer.java diff --git a/src/main/java/io/split/android/client/service/http/HttpResponseParser.java b/main/src/main/java/io/split/android/client/service/http/HttpResponseParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpResponseParser.java rename to main/src/main/java/io/split/android/client/service/http/HttpResponseParser.java diff --git a/src/main/java/io/split/android/client/service/http/HttpResponseParserException.java b/main/src/main/java/io/split/android/client/service/http/HttpResponseParserException.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpResponseParserException.java rename to main/src/main/java/io/split/android/client/service/http/HttpResponseParserException.java diff --git a/src/main/java/io/split/android/client/service/http/HttpSseAuthTokenFetcher.java b/main/src/main/java/io/split/android/client/service/http/HttpSseAuthTokenFetcher.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpSseAuthTokenFetcher.java rename to main/src/main/java/io/split/android/client/service/http/HttpSseAuthTokenFetcher.java diff --git a/src/main/java/io/split/android/client/service/http/HttpStatus.java b/main/src/main/java/io/split/android/client/service/http/HttpStatus.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/HttpStatus.java rename to main/src/main/java/io/split/android/client/service/http/HttpStatus.java diff --git a/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java b/main/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java rename to main/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java diff --git a/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java b/main/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionHasher.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionHasher.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionHasher.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionHasher.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionManager.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionManager.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionManager.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionManager.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionManagerConfig.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionManagerConfig.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionManagerConfig.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionManagerConfig.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProvider.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProvider.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProvider.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProvider.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImpl.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImpl.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImpl.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionUtils.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionUtils.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionUtils.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionUtils.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsCount.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCount.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsCount.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsCount.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsCountPerFeature.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountPerFeature.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsCountPerFeature.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountPerFeature.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRequestBodySerializer.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRequestBodySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsCountRequestBodySerializer.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRequestBodySerializer.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsCounter.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCounter.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsCounter.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsCounter.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsMode.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsMode.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsMode.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsMode.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTaskConfig.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTaskConfig.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTaskConfig.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTaskConfig.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializer.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializer.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializer.java diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionsTaskFactory.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsTaskFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/ImpressionsTaskFactory.java rename to main/src/main/java/io/split/android/client/service/impressions/ImpressionsTaskFactory.java diff --git a/src/main/java/io/split/android/client/service/impressions/KeyImpressionSerializer.java b/main/src/main/java/io/split/android/client/service/impressions/KeyImpressionSerializer.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/KeyImpressionSerializer.java rename to main/src/main/java/io/split/android/client/service/impressions/KeyImpressionSerializer.java diff --git a/src/main/java/io/split/android/client/service/impressions/MemoizedSupplier.java b/main/src/main/java/io/split/android/client/service/impressions/MemoizedSupplier.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/MemoizedSupplier.java rename to main/src/main/java/io/split/android/client/service/impressions/MemoizedSupplier.java diff --git a/src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java b/main/src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java rename to main/src/main/java/io/split/android/client/service/impressions/SaveImpressionsCountTask.java diff --git a/src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java b/main/src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java rename to main/src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java b/main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java b/main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java b/main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java b/main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ListenableLruCache.java b/main/src/main/java/io/split/android/client/service/impressions/observer/ListenableLruCache.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/ListenableLruCache.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/ListenableLruCache.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java b/main/src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/PersistentImpressionsObserverCacheStorage.java b/main/src/main/java/io/split/android/client/service/impressions/observer/PersistentImpressionsObserverCacheStorage.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/PersistentImpressionsObserverCacheStorage.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/PersistentImpressionsObserverCacheStorage.java diff --git a/src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java b/main/src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java rename to main/src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyConfig.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyConfig.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyConfig.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyConfig.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/PeriodicTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/PeriodicTracker.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/PeriodicTracker.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/PeriodicTracker.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/Utils.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/Utils.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/strategy/Utils.java rename to main/src/main/java/io/split/android/client/service/impressions/strategy/Utils.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/MTK.java b/main/src/main/java/io/split/android/client/service/impressions/unique/MTK.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/MTK.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/MTK.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializer.java b/main/src/main/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializer.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializer.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java b/main/src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTask.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/UniqueKey.java b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKey.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/UniqueKey.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKey.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskConfig.java b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskConfig.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskConfig.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskConfig.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTracker.java b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTracker.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTracker.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTracker.java diff --git a/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImpl.java b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImpl.java rename to main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImpl.java diff --git a/src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java b/main/src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java rename to main/src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java diff --git a/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java b/main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java rename to main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java diff --git a/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java b/main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java rename to main/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTask.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTask.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java similarity index 89% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java index 2158985aa..5141b887d 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java @@ -18,6 +18,8 @@ import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTask; @@ -50,7 +52,6 @@ public class MySegmentsSyncTask implements SplitTask { private final SplitTaskType mTaskType; private final SplitInternalEvent mUpdateEvent; - private final SplitInternalEvent mFetchedEvent; private final OperationType mTelemetryOperationType; private final boolean mAvoidCache; @@ -105,7 +106,6 @@ public MySegmentsSyncTask(@NonNull HttpFetcher mySegmentsFetc mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); mTaskType = config.getTaskType(); mUpdateEvent = config.getUpdateEvent(); - mFetchedEvent = config.getFetchedEvent(); mTelemetryOperationType = config.getTelemetryOperationType(); mTargetSegmentsChangeNumber = targetSegmentsChangeNumber; mTargetLargeSegmentsChangeNumber = targetLargeSegmentsChangeNumber; @@ -265,29 +265,28 @@ private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, return; } - // MY_SEGMENTS_UPDATED event when segments have changed - boolean segmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(segmentsResult.oldSegments, segmentsResult.newSegments); - boolean largeSegmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(largeSegmentsResult.oldSegments, largeSegmentsResult.newSegments); + // Check for actual updates and fire updated events BEFORE sync complete. + // This order is important: if we fire MEMBERSHIPS_SYNC_COMPLETE first, it may trigger SDK_READY, + // and then the *_UPDATED events would immediately trigger SDK_UPDATE during initial sync. + // By firing *_UPDATED first (while SDK_READY hasn't triggered yet), they won't trigger SDK_UPDATE. + List changedSegments = mMySegmentsChangeChecker.getChangedSegments(segmentsResult.oldSegments, segmentsResult.newSegments); + List changedLargeSegments = mMySegmentsChangeChecker.getChangedSegments(largeSegmentsResult.oldSegments, largeSegmentsResult.newSegments); - if (segmentsHaveChanged) { + if (!changedSegments.isEmpty()) { Logger.v("New segments: " + segmentsResult.newSegments); + mEventsManager.notifyInternalEvent(mUpdateEvent, EventMetadataHelpers.createUpdatedSegmentsMetadata()); } - if (largeSegmentsHaveChanged) { + if (!changedLargeSegments.isEmpty()) { Logger.v("New large segments: " + largeSegmentsResult.newSegments); + mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } - if (segmentsHaveChanged) { - mEventsManager.notifyInternalEvent(mUpdateEvent); - } else { - // MY_LARGE_SEGMENTS_UPDATED event when large segments have changed - if (largeSegmentsHaveChanged) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); - } else { - // otherwise, MY_SEGMENTS_FETCHED event - mEventsManager.notifyInternalEvent(mFetchedEvent); - } - } + // Fire sync complete AFTER update events. This ensures SDK_READY triggers after + // all *_UPDATED events have been processed (which won't trigger SDK_UPDATE because + // SDK_READY's prerequisite for SDK_UPDATE isn't met yet). + mEventsManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); } private static class UpdateSegmentsResult { diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java similarity index 82% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java index 210fb2d4e..77ddd812d 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java @@ -11,20 +11,16 @@ public class MySegmentsSyncTaskConfig { private static final MySegmentsSyncTaskConfig MY_SEGMENTS_TASK_CONFIG = new MySegmentsSyncTaskConfig( SplitTaskType.MY_SEGMENTS_SYNC, SplitInternalEvent.MY_SEGMENTS_UPDATED, - SplitInternalEvent.MY_SEGMENTS_FETCHED, OperationType.MY_SEGMENT); private final SplitTaskType mTaskType; private final SplitInternalEvent mUpdateEvent; - private final SplitInternalEvent mFetchedEvent; private final OperationType mTelemetryOperationType; private MySegmentsSyncTaskConfig(@NonNull SplitTaskType taskType, @NonNull SplitInternalEvent updateEvent, - @NonNull SplitInternalEvent fetchedEvent, @NonNull OperationType telemetryOperationType) { mTaskType = taskType; mUpdateEvent = updateEvent; - mFetchedEvent = fetchedEvent; mTelemetryOperationType = telemetryOperationType; } @@ -36,10 +32,6 @@ SplitInternalEvent getUpdateEvent() { return mUpdateEvent; } - SplitInternalEvent getFetchedEvent() { - return mFetchedEvent; - } - OperationType getTelemetryOperationType() { return mTelemetryOperationType; } diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProvider.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProvider.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProvider.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProvider.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProviderImpl.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProviderImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProviderImpl.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryProviderImpl.java diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java similarity index 96% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java index 3f8dc1260..cf1257ca4 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java @@ -9,6 +9,7 @@ import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -96,7 +97,7 @@ public SplitTaskExecutionInfo remove() { private void updateAndNotify(Set segments) { mMySegmentsStorage.set(SegmentsChange.create(segments, mChangeNumber)); - mEventsManager.notifyInternalEvent(mUpdateEvent); + mEventsManager.notifyInternalEvent(mUpdateEvent, EventMetadataHelpers.createUpdatedSegmentsMetadata()); } private void logError(String message) { diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java similarity index 100% rename from src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java rename to main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java diff --git a/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java b/main/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java rename to main/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java diff --git a/src/main/java/io/split/android/client/service/rules/ProcessedRuleBasedSegmentChange.java b/main/src/main/java/io/split/android/client/service/rules/ProcessedRuleBasedSegmentChange.java similarity index 100% rename from src/main/java/io/split/android/client/service/rules/ProcessedRuleBasedSegmentChange.java rename to main/src/main/java/io/split/android/client/service/rules/ProcessedRuleBasedSegmentChange.java diff --git a/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessor.java b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessor.java similarity index 100% rename from src/main/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessor.java rename to main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessor.java diff --git a/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java similarity index 93% rename from src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java rename to main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java index 9e92523e0..72c05e4a2 100644 --- a/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java @@ -7,6 +7,7 @@ import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -41,7 +42,8 @@ public SplitTaskExecutionInfo execute() { boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber, null); if (triggerSdkUpdate) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } Logger.v("Updated rule based segment"); diff --git a/src/main/java/io/split/android/client/service/rules/TargetingRulesResponseParser.java b/main/src/main/java/io/split/android/client/service/rules/TargetingRulesResponseParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/rules/TargetingRulesResponseParser.java rename to main/src/main/java/io/split/android/client/service/rules/TargetingRulesResponseParser.java diff --git a/src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java b/main/src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java similarity index 100% rename from src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java rename to main/src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java diff --git a/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java b/main/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java rename to main/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java diff --git a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java b/main/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java rename to main/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java diff --git a/src/main/java/io/split/android/client/service/splits/OutdatedSplitProxyHandler.java b/main/src/main/java/io/split/android/client/service/splits/OutdatedSplitProxyHandler.java similarity index 100% rename from src/main/java/io/split/android/client/service/splits/OutdatedSplitProxyHandler.java rename to main/src/main/java/io/split/android/client/service/splits/OutdatedSplitProxyHandler.java diff --git a/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java similarity index 93% rename from src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java rename to main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java index e02ccec56..6fb8fc8dc 100644 --- a/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java @@ -7,6 +7,7 @@ import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -43,7 +44,8 @@ public SplitTaskExecutionInfo execute() { boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber, null); if (triggerSdkUpdate) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } Logger.v("Updated rule based segment"); diff --git a/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java b/main/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java similarity index 100% rename from src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java rename to main/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java diff --git a/src/main/java/io/split/android/client/service/splits/SplitChangeResponseParser.java b/main/src/main/java/io/split/android/client/service/splits/SplitChangeResponseParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/splits/SplitChangeResponseParser.java rename to main/src/main/java/io/split/android/client/service/splits/SplitChangeResponseParser.java diff --git a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java similarity index 81% rename from src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java rename to main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java index 4198cd401..2c86042b0 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java @@ -1,12 +1,17 @@ package io.split.android.client.service.splits; +import static io.split.android.client.service.splits.SplitsSyncHelper.extractFlagNames; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import java.util.List; + +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -47,7 +52,8 @@ public SplitTaskExecutionInfo execute() { boolean triggerSdkUpdate = mSplitsStorage.update(processedSplitChange, null); if (triggerSdkUpdate) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + EventMetadata metadata = createUpdatedFlagsMetadata(processedSplitChange); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); } mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); @@ -59,4 +65,9 @@ public SplitTaskExecutionInfo execute() { return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC); } } + + private EventMetadata createUpdatedFlagsMetadata(ProcessedSplitChange processedSplitChange) { + List updatedSplitNames = extractFlagNames(processedSplitChange); + return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); + } } diff --git a/src/main/java/io/split/android/client/service/splits/SplitKillTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java similarity index 88% rename from src/main/java/io/split/android/client/service/splits/SplitKillTask.java rename to main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java index 0468af7d3..001d4ec04 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitKillTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java @@ -4,9 +4,13 @@ import androidx.annotation.NonNull; +import java.util.Collections; + +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -53,7 +57,9 @@ public SplitTaskExecutionInfo execute() { splitToKill.changeNumber = mKilledSplit.changeNumber; mSplitsStorage.updateWithoutChecks(splitToKill); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); + EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata( + Collections.singletonList(mKilledSplit.name)); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION, metadata); } catch (Exception e) { logError("Unknown error while updating killed feature flag: " + e.getLocalizedMessage()); return SplitTaskExecutionInfo.error(SplitTaskType.SPLIT_KILL); diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java similarity index 82% rename from src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java rename to main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java index 8b53774fd..705331080 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java @@ -7,14 +7,19 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.dtos.RuleBasedSegmentChange; +import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.network.SplitHttpHeadersBuilder; @@ -26,6 +31,7 @@ import io.split.android.client.service.http.HttpStatus; import io.split.android.client.service.rules.ProcessedRuleBasedSegmentChange; import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; +import io.split.android.client.storage.splits.ProcessedSplitChange; import io.split.android.client.service.sseclient.BackoffCounter; import io.split.android.client.service.sseclient.ReconnectBackoffCounter; import io.split.android.client.storage.general.GeneralInfoStorage; @@ -53,6 +59,10 @@ public class SplitsSyncHelper { private final OutdatedSplitProxyHandler mOutdatedSplitProxyHandler; private final ExecutorService mExecutor; private final TargetingRulesCache mTargetingRulesCache; + private final AtomicReference mLastProcessedSplitChange = new AtomicReference<>(); + private final AtomicReference mLastProcessedRbsChange = new AtomicReference<>(); + private boolean mSplitsHaveChanged; + private boolean mRuleBasedSegmentsHaveChanged; public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher, @NonNull SplitsStorage splitsStorage, @@ -136,6 +146,8 @@ public SplitTaskExecutionInfo sync(SinceChangeNumbers till, boolean clearBeforeU } private SplitTaskExecutionInfo sync(SinceChangeNumbers till, boolean clearBeforeUpdate, boolean avoidCache, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) { + mSplitsHaveChanged = false; + mRuleBasedSegmentsHaveChanged = false; try { mOutdatedSplitProxyHandler.performProxyCheck(); if (mOutdatedSplitProxyHandler.isRecoveryMode()) { @@ -302,16 +314,87 @@ public static void fetchForFreshInstallCache(String currentSpec, } private void updateStorage(boolean clearBeforeUpdate, SplitChange splitChange, RuleBasedSegmentChange ruleBasedSegmentChange) { + if (splitChange != null && splitChange.splits != null && !splitChange.splits.isEmpty()) { + mSplitsHaveChanged = true; + } + + if (ruleBasedSegmentChange != null && ruleBasedSegmentChange.getSegments() != null && !ruleBasedSegmentChange.getSegments().isEmpty()) { + mRuleBasedSegmentsHaveChanged = true; + } + if (clearBeforeUpdate) { mSplitsStorage.clear(); mRuleBasedSegmentStorage.clear(); } - mSplitsStorage.update(mSplitChangeProcessor.process(splitChange), mExecutor); + ProcessedSplitChange processedSplitChange = mSplitChangeProcessor.process(splitChange); + if (hasFlagUpdates(processedSplitChange)) { + mLastProcessedSplitChange.set(processedSplitChange); + } + mSplitsStorage.update(processedSplitChange, mExecutor); updateRbsStorage(ruleBasedSegmentChange); } + private boolean hasFlagUpdates(@Nullable ProcessedSplitChange processedSplitChange) { + if (processedSplitChange == null) { + return false; + } + List activeSplits = processedSplitChange.getActiveSplits(); + if (activeSplits != null && !activeSplits.isEmpty()) { + return true; + } + List archivedSplits = processedSplitChange.getArchivedSplits(); + return archivedSplits != null && !archivedSplits.isEmpty(); + } + + /** + * Gets the list of updated flag names from the last sync operation. + * This includes both active (added/modified) and archived (removed) splits. + * + * @return list of updated flag names, or empty list if no updates occurred + */ + @NonNull + public List getLastUpdatedFlagNames() { + ProcessedSplitChange lastChange = mLastProcessedSplitChange.get(); + if (lastChange == null) { + return Collections.emptyList(); + } + return extractFlagNames(lastChange); + } + + /** + * Extracts split names from a ProcessedSplitChange. + * This includes both active (added/modified) and archived (removed) splits. + * + * @param processedSplitChange the processed split change + * @return list of split names, or empty list if change is null + */ + @NonNull + public static List extractFlagNames(@Nullable ProcessedSplitChange processedSplitChange) { + if (processedSplitChange == null) { + return Collections.emptyList(); + } + + List updatedNames = new ArrayList<>(); + if (processedSplitChange.getActiveSplits() != null) { + for (Split split : processedSplitChange.getActiveSplits()) { + if (split != null && split.name != null) { + updatedNames.add(split.name); + } + } + } + if (processedSplitChange.getArchivedSplits() != null) { + for (Split split : processedSplitChange.getArchivedSplits()) { + if (split != null && split.name != null) { + updatedNames.add(split.name); + } + } + } + return updatedNames; + } + private void updateRbsStorage(RuleBasedSegmentChange ruleBasedSegmentChange) { ProcessedRuleBasedSegmentChange change = mRuleBasedSegmentChangeProcessor.process(ruleBasedSegmentChange.getSegments(), ruleBasedSegmentChange.getTill()); + mLastProcessedRbsChange.set(change); mRuleBasedSegmentStorage.update(change.getActive(), change.getArchived(), change.getChangeNumber(), mExecutor); } @@ -363,6 +446,14 @@ public String toString() { } } + public boolean splitsHaveChanged() { + return mSplitsHaveChanged; + } + + public boolean ruleBasedSegmentsHaveChanged() { + return mRuleBasedSegmentsHaveChanged; + } + private enum CdnByPassType { NONE, FLAGS, diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java similarity index 72% rename from src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java rename to main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 2cb35e578..dfab23fff 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -5,8 +5,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.List; + +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -93,14 +98,37 @@ public SplitTaskExecutionInfo execute() { } private void notifyInternalEvent(long storedChangeNumber) { - if (mEventsManager != null) { - SplitInternalEvent event = SplitInternalEvent.SPLITS_FETCHED; - if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill())) { - event = SplitInternalEvent.SPLITS_UPDATED; - } + if (mEventsManager == null) { + return; + } - mEventsManager.notifyInternalEvent(event); + // Fire *_UPDATED events BEFORE sync complete. This order is important: + // if we fire TARGETING_RULES_SYNC_COMPLETE first, it may trigger SDK_READY, + // and then the *_UPDATED events would immediately trigger SDK_UPDATE during initial sync. + // By firing *_UPDATED first (while SDK_READY hasn't triggered yet), they won't trigger SDK_UPDATE. + // + // Use else-if logic: if splits changed, only fire SPLITS_UPDATED (FLAGS_UPDATE). + // RBS changes are only relevant when flags DIDN'T change. + if (mSplitsSyncHelper.splitsHaveChanged()) { + EventMetadata metadata = createUpdatedFlagsMetadata(); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); + } else if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } + + // Fire sync complete AFTER update events. This ensures SDK_READY triggers after + // all *_UPDATED events have been processed (which won't trigger SDK_UPDATE because + // SDK_READY's prerequisite for SDK_UPDATE isn't met yet). + boolean cacheAlreadyLoaded = mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE); + EventMetadata syncMetadata = EventMetadataHelpers.createSyncCompleteMetadata( + cacheAlreadyLoaded, mSplitsStorage.getUpdateTimestamp()); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, syncMetadata); + } + + private EventMetadata createUpdatedFlagsMetadata() { + List updatedSplitNames = mSplitsSyncHelper.getLastUpdatedFlagNames(); + return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); } private boolean splitsFilterHasChanged(String storedSplitsFilterQueryString) { diff --git a/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java similarity index 64% rename from src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java rename to main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java index 8f0a7cf61..d14600725 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java @@ -6,8 +6,13 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import java.util.List; + +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -67,16 +72,34 @@ public SplitTaskExecutionInfo execute() { SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(new SplitsSyncHelper.SinceChangeNumbers(mChangeNumber, mRbsChangeNumber), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); if (result.getStatus() == SplitTaskExecutionStatus.SUCCESS) { - SplitInternalEvent event = SplitInternalEvent.SPLITS_FETCHED; - if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill()) || - mChangeChecker.changeNumberIsNewer(storedRbsChangeNumber, mRuleBasedSegmentStorage.getChangeNumber())) { - event = SplitInternalEvent.SPLITS_UPDATED; + // Fire *_UPDATED events BEFORE sync complete. This order is important: + // if we fire TARGETING_RULES_SYNC_COMPLETE first, it may trigger SDK_READY, + // and then the *_UPDATED events would immediately trigger SDK_UPDATE during initial sync. + // + // Use If splits changed, only fire SPLITS_UPDATED (FLAGS_UPDATE). + // RBS changes are only relevant when flags DIDN'T change. + if (mSplitsSyncHelper.splitsHaveChanged()) { + EventMetadata metadata = createUpdatedFlagsMetadata(); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); + } else if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } - mEventsManager.notifyInternalEvent(event); + + // Fire sync complete AFTER update events. + boolean cacheAlreadyLoaded = mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE); + EventMetadata syncMetadata = EventMetadataHelpers.createSyncCompleteMetadata( + cacheAlreadyLoaded, mSplitsStorage.getUpdateTimestamp()); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, syncMetadata); } return result; } + private EventMetadata createUpdatedFlagsMetadata() { + List updatedSplitNames = mSplitsSyncHelper.getLastUpdatedFlagNames(); + return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); + } + @VisibleForTesting public void setChangeChecker(SplitsChangeChecker changeChecker) { mChangeChecker = changeChecker; diff --git a/src/main/java/io/split/android/client/service/splits/TargetingRulesCache.java b/main/src/main/java/io/split/android/client/service/splits/TargetingRulesCache.java similarity index 100% rename from src/main/java/io/split/android/client/service/splits/TargetingRulesCache.java rename to main/src/main/java/io/split/android/client/service/splits/TargetingRulesCache.java diff --git a/src/main/java/io/split/android/client/service/sseauthentication/SseAuthenticationResponseParser.java b/main/src/main/java/io/split/android/client/service/sseauthentication/SseAuthenticationResponseParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseauthentication/SseAuthenticationResponseParser.java rename to main/src/main/java/io/split/android/client/service/sseauthentication/SseAuthenticationResponseParser.java diff --git a/src/main/java/io/split/android/client/service/sseclient/BackoffCounter.java b/main/src/main/java/io/split/android/client/service/sseclient/BackoffCounter.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/BackoffCounter.java rename to main/src/main/java/io/split/android/client/service/sseclient/BackoffCounter.java diff --git a/src/main/java/io/split/android/client/service/sseclient/EventStreamParser.java b/main/src/main/java/io/split/android/client/service/sseclient/EventStreamParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/EventStreamParser.java rename to main/src/main/java/io/split/android/client/service/sseclient/EventStreamParser.java diff --git a/src/main/java/io/split/android/client/service/sseclient/FixedIntervalBackoffCounter.java b/main/src/main/java/io/split/android/client/service/sseclient/FixedIntervalBackoffCounter.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/FixedIntervalBackoffCounter.java rename to main/src/main/java/io/split/android/client/service/sseclient/FixedIntervalBackoffCounter.java diff --git a/src/main/java/io/split/android/client/service/sseclient/InvalidJwtTokenException.java b/main/src/main/java/io/split/android/client/service/sseclient/InvalidJwtTokenException.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/InvalidJwtTokenException.java rename to main/src/main/java/io/split/android/client/service/sseclient/InvalidJwtTokenException.java diff --git a/src/main/java/io/split/android/client/service/sseclient/ReconnectBackoffCounter.java b/main/src/main/java/io/split/android/client/service/sseclient/ReconnectBackoffCounter.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/ReconnectBackoffCounter.java rename to main/src/main/java/io/split/android/client/service/sseclient/ReconnectBackoffCounter.java diff --git a/src/main/java/io/split/android/client/service/sseclient/SseAuthToken.java b/main/src/main/java/io/split/android/client/service/sseclient/SseAuthToken.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/SseAuthToken.java rename to main/src/main/java/io/split/android/client/service/sseclient/SseAuthToken.java diff --git a/src/main/java/io/split/android/client/service/sseclient/SseAuthenticationResponse.java b/main/src/main/java/io/split/android/client/service/sseclient/SseAuthenticationResponse.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/SseAuthenticationResponse.java rename to main/src/main/java/io/split/android/client/service/sseclient/SseAuthenticationResponse.java diff --git a/src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java b/main/src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java rename to main/src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java diff --git a/src/main/java/io/split/android/client/service/sseclient/SseJwtToken.java b/main/src/main/java/io/split/android/client/service/sseclient/SseJwtToken.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/SseJwtToken.java rename to main/src/main/java/io/split/android/client/service/sseclient/SseJwtToken.java diff --git a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/BroadcastedEventListener.java b/main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/BroadcastedEventListener.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/feedbackchannel/BroadcastedEventListener.java rename to main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/BroadcastedEventListener.java diff --git a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/DelayStatusEvent.java b/main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/DelayStatusEvent.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/feedbackchannel/DelayStatusEvent.java rename to main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/DelayStatusEvent.java diff --git a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java b/main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java rename to main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java diff --git a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java b/main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java rename to main/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/KeyList.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/KeyList.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/KeyList.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/KeyList.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentUpdateStrategy.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentUpdateStrategy.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentUpdateStrategy.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentUpdateStrategy.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsV2PayloadDecoder.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/OccupancyNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/OccupancyNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/OccupancyNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/OccupancyNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/RuleBasedSegmentChangeNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/RuleBasedSegmentChangeNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/RuleBasedSegmentChangeNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/RuleBasedSegmentChangeNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/StreamingError.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/StreamingError.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/StreamingError.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/StreamingError.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java rename to main/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java b/main/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java rename to main/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistry.java b/main/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistry.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistry.java rename to main/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistry.java diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java rename to main/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java b/main/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java rename to main/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java b/main/src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java rename to main/src/main/java/io/split/android/client/service/sseclient/reactor/UpdateWorker.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTask.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTask.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTask.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticationResult.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticationResult.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticationResult.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticationResult.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java b/main/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java similarity index 100% rename from src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java rename to main/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java b/main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java rename to main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java similarity index 82% rename from src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java index b7675e727..0e552ebb8 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java @@ -14,6 +14,7 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTaskBatchItem; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -24,6 +25,7 @@ import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; +import io.split.android.client.storage.splits.SplitsStorage; public class FeatureFlagsSynchronizerImpl implements FeatureFlagsSynchronizer { @@ -48,6 +50,18 @@ public FeatureFlagsSynchronizerImpl(@NonNull SplitClientConfig splitClientConfig @NonNull ISplitEventsManager splitEventsManager, @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + this(splitClientConfig, taskExecutor, splitSingleThreadTaskExecutor, splitTaskFactory, + splitEventsManager, retryBackoffCounterTimerFactory, pushManagerEventBroadcaster, null); + } + + public FeatureFlagsSynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, + @NonNull SplitTaskExecutor taskExecutor, + @NonNull SplitTaskExecutor splitSingleThreadTaskExecutor, + @NonNull SplitTaskFactory splitTaskFactory, + @NonNull ISplitEventsManager splitEventsManager, + @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, + @Nullable SplitsStorage splitsStorage) { mTaskExecutor = checkNotNull(taskExecutor); mSplitsTaskExecutor = splitSingleThreadTaskExecutor; @@ -80,8 +94,15 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { } mSplitsSyncRetryTimer.setTask(mSplitTaskFactory.createSplitsSyncTask(true), mSplitsSyncListener); + + // Create metadata provider for cache path. initialCacheLoad=false because this listener + // is only invoked when splits are successfully loaded from local storage (cache exists). + LoadLocalDataListener.MetadataProvider cacheMetadataProvider = splitsStorage != null + ? () -> EventMetadataHelpers.createReadyMetadata(splitsStorage.getUpdateTimestamp(), false) + : null; + mLoadLocalSplitsListener = new LoadLocalDataListener( - splitEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + splitEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, cacheMetadataProvider); } @Override diff --git a/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java b/main/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java similarity index 59% rename from src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java rename to main/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java index 1490ef929..be1ccc999 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java @@ -3,7 +3,9 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -12,19 +14,37 @@ public class LoadLocalDataListener implements SplitTaskExecutionListener { + /** + * Functional interface for providing metadata when the event is fired. + */ + public interface MetadataProvider { + @Nullable + EventMetadata getMetadata(); + } + private final ISplitEventsManager mSplitEventsManager; private final SplitInternalEvent mEventToFire; + @Nullable + private final MetadataProvider mMetadataProvider; public LoadLocalDataListener(ISplitEventsManager splitEventsManager, SplitInternalEvent eventToFire) { + this(splitEventsManager, eventToFire, null); + } + + public LoadLocalDataListener(ISplitEventsManager splitEventsManager, + SplitInternalEvent eventToFire, + @Nullable MetadataProvider metadataProvider) { mSplitEventsManager = checkNotNull(splitEventsManager); mEventToFire = checkNotNull(eventToFire); + mMetadataProvider = metadataProvider; } @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { if (taskInfo.getStatus().equals(SplitTaskExecutionStatus.SUCCESS)) { - mSplitEventsManager.notifyInternalEvent(mEventToFire); + EventMetadata metadata = mMetadataProvider != null ? mMetadataProvider.getMetadata() : null; + mSplitEventsManager.notifyInternalEvent(mEventToFire, metadata); } } } diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java b/main/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java new file mode 100644 index 000000000..780b70adb --- /dev/null +++ b/main/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java @@ -0,0 +1,36 @@ +package io.split.android.client.service.synchronizer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class MySegmentsChangeChecker { + + /** + * Computes and returns the list of changed segment names (added + removed) between old and new segments. + * An empty list means no changes occurred. + * + * @param oldSegments the previous list of segment names + * @param newSegments the new list of segment names + * @return list of segment names that were either added or removed (empty if no changes) + */ + public List getChangedSegments(final List oldSegments, final List newSegments) { + Set oldSet = new HashSet<>(oldSegments); + Set newSet = new HashSet<>(newSegments); + + // Added segments: in new but not in old + Set added = new HashSet<>(newSet); + added.removeAll(oldSet); + + // Removed segments: in old but not in new + Set removed = new HashSet<>(oldSet); + removed.removeAll(newSet); + + // Combined changed segments + Set changed = new HashSet<>(added); + changed.addAll(removed); + + return new ArrayList<>(changed); + } +} diff --git a/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelper.java b/main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelper.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelper.java rename to main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelper.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java b/main/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java rename to main/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java b/main/src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java rename to main/src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncGuardian.java b/main/src/main/java/io/split/android/client/service/synchronizer/SyncGuardian.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/SyncGuardian.java rename to main/src/main/java/io/split/android/client/service/synchronizer/SyncGuardian.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncGuardianImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/SyncGuardianImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/SyncGuardianImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/SyncGuardianImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncManager.java b/main/src/main/java/io/split/android/client/service/synchronizer/SyncManager.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/SyncManager.java rename to main/src/main/java/io/split/android/client/service/synchronizer/SyncManager.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java b/main/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java rename to main/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java similarity index 98% rename from src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java index ddfd906e8..abf55e7fe 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java @@ -31,6 +31,7 @@ import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistryImpl; import io.split.android.client.shared.UserConsent; import io.split.android.client.storage.common.StoragePusher; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.EventsDataRecordsEnum; import io.split.android.client.telemetry.model.streaming.SyncModeUpdateStreamingEvent; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -69,7 +70,8 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull StrategyImpressionManager impressionManager, @NonNull StoragePusher eventsStorage, @NonNull ISplitEventsManager eventsManagerCoordinator, - @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, + @Nullable SplitsStorage splitsStorage) { this(splitClientConfig, taskExecutor, splitSingleThreadTaskExecutor, @@ -85,7 +87,8 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, splitTaskFactory, eventsManagerCoordinator, retryBackoffCounterTimerFactory, - pushManagerEventBroadcaster + pushManagerEventBroadcaster, + splitsStorage ), eventsStorage); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerSpy.java b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerSpy.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/SynchronizerSpy.java rename to main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerSpy.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/ThreadUtils.java b/main/src/main/java/io/split/android/client/service/synchronizer/ThreadUtils.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/ThreadUtils.java rename to main/src/main/java/io/split/android/client/service/synchronizer/ThreadUtils.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java b/main/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java similarity index 99% rename from src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java rename to main/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java index ca3761bca..2b86786fd 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java @@ -22,7 +22,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; -import io.split.android.android_client.BuildConfig; +import io.split.android.client.main.BuildConfig; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFilter; import io.split.android.client.network.CertificatePin; diff --git a/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizer.java b/main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizer.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizer.java rename to main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizer.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactory.java b/main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactory.java rename to main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactory.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactoryImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistry.java b/main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistry.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistry.java rename to main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistry.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsBackgroundSyncScheduleTask.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsBackgroundSyncScheduleTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsBackgroundSyncScheduleTask.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsBackgroundSyncScheduleTask.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java b/main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java similarity index 100% rename from src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java rename to main/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java diff --git a/src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java rename to main/src/main/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTask.java diff --git a/src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java similarity index 100% rename from src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java rename to main/src/main/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTask.java diff --git a/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactory.java b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactory.java similarity index 100% rename from src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactory.java rename to main/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactory.java diff --git a/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java b/main/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java rename to main/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java diff --git a/src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java b/main/src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java rename to main/src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java diff --git a/src/main/java/io/split/android/client/service/workmanager/EventsRecorderWorker.java b/main/src/main/java/io/split/android/client/service/workmanager/EventsRecorderWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/EventsRecorderWorker.java rename to main/src/main/java/io/split/android/client/service/workmanager/EventsRecorderWorker.java diff --git a/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java b/main/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java similarity index 99% rename from src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java rename to main/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java index ed4a2cdb2..2d7e7119d 100644 --- a/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java +++ b/main/src/main/java/io/split/android/client/service/workmanager/HttpClientProvider.java @@ -6,7 +6,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; -import io.split.android.android_client.BuildConfig; +import io.split.android.client.main.BuildConfig; import io.split.android.client.dtos.HttpProxyDto; import io.split.android.client.network.BasicCredentialsProvider; import io.split.android.client.network.BearerCredentialsProvider; diff --git a/src/main/java/io/split/android/client/service/workmanager/ImpressionsRecorderWorker.java b/main/src/main/java/io/split/android/client/service/workmanager/ImpressionsRecorderWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/ImpressionsRecorderWorker.java rename to main/src/main/java/io/split/android/client/service/workmanager/ImpressionsRecorderWorker.java diff --git a/src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java b/main/src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java rename to main/src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java diff --git a/src/main/java/io/split/android/client/service/workmanager/SplitWorker.java b/main/src/main/java/io/split/android/client/service/workmanager/SplitWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/SplitWorker.java rename to main/src/main/java/io/split/android/client/service/workmanager/SplitWorker.java diff --git a/src/main/java/io/split/android/client/service/workmanager/UniqueKeysRecorderWorker.java b/main/src/main/java/io/split/android/client/service/workmanager/UniqueKeysRecorderWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/UniqueKeysRecorderWorker.java rename to main/src/main/java/io/split/android/client/service/workmanager/UniqueKeysRecorderWorker.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitChangeProcessorProvider.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/SplitChangeProcessorProvider.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/SplitChangeProcessorProvider.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/SplitChangeProcessorProvider.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilder.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilder.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilder.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilder.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParams.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParams.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParams.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParams.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java b/main/src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java similarity index 100% rename from src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java rename to main/src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java diff --git a/src/main/java/io/split/android/client/shared/BaseSplitClientContainer.java b/main/src/main/java/io/split/android/client/shared/BaseSplitClientContainer.java similarity index 100% rename from src/main/java/io/split/android/client/shared/BaseSplitClientContainer.java rename to main/src/main/java/io/split/android/client/shared/BaseSplitClientContainer.java diff --git a/src/main/java/io/split/android/client/shared/ClientComponentsRegister.java b/main/src/main/java/io/split/android/client/shared/ClientComponentsRegister.java similarity index 100% rename from src/main/java/io/split/android/client/shared/ClientComponentsRegister.java rename to main/src/main/java/io/split/android/client/shared/ClientComponentsRegister.java diff --git a/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java b/main/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java similarity index 100% rename from src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java rename to main/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainer.java b/main/src/main/java/io/split/android/client/shared/SplitClientContainer.java similarity index 100% rename from src/main/java/io/split/android/client/shared/SplitClientContainer.java rename to main/src/main/java/io/split/android/client/shared/SplitClientContainer.java diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java b/main/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java similarity index 99% rename from src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java rename to main/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java index 43d81074b..3ec86c8fc 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/main/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -145,7 +145,7 @@ public void remove(Key key) { @Override public void createNewClient(Key key) { - SplitEventsManager eventsManager = new SplitEventsManager(mConfig, mSplitClientEventTaskExecutor); + SplitEventsManager eventsManager = new SplitEventsManager(mSplitClientEventTaskExecutor, mConfig.blockUntilReady()); MySegmentsTaskFactory mySegmentsTaskFactory = getMySegmentsTaskFactory(key, eventsManager); SplitClient client = mSplitClientFactory.getClient(key, mySegmentsTaskFactory, eventsManager, mDefaultMatchingKey.equals(key.matchingKey())); diff --git a/src/main/java/io/split/android/client/shared/UserConsent.java b/main/src/main/java/io/split/android/client/shared/UserConsent.java similarity index 100% rename from src/main/java/io/split/android/client/shared/UserConsent.java rename to main/src/main/java/io/split/android/client/shared/UserConsent.java diff --git a/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java b/main/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java similarity index 100% rename from src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java rename to main/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java diff --git a/src/main/java/io/split/android/client/storage/attributes/AttributesStorage.java b/main/src/main/java/io/split/android/client/storage/attributes/AttributesStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/attributes/AttributesStorage.java rename to main/src/main/java/io/split/android/client/storage/attributes/AttributesStorage.java diff --git a/src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainer.java b/main/src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainer.java rename to main/src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainer.java diff --git a/src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainerImpl.java b/main/src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainerImpl.java rename to main/src/main/java/io/split/android/client/storage/attributes/AttributesStorageContainerImpl.java diff --git a/src/main/java/io/split/android/client/storage/attributes/AttributesStorageImpl.java b/main/src/main/java/io/split/android/client/storage/attributes/AttributesStorageImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/attributes/AttributesStorageImpl.java rename to main/src/main/java/io/split/android/client/storage/attributes/AttributesStorageImpl.java diff --git a/src/main/java/io/split/android/client/storage/attributes/PersistentAttributesStorage.java b/main/src/main/java/io/split/android/client/storage/attributes/PersistentAttributesStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/attributes/PersistentAttributesStorage.java rename to main/src/main/java/io/split/android/client/storage/attributes/PersistentAttributesStorage.java diff --git a/src/main/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorage.java b/main/src/main/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorage.java rename to main/src/main/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorage.java diff --git a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java b/main/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java rename to main/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java diff --git a/src/main/java/io/split/android/client/storage/cipher/CBCCipher.java b/main/src/main/java/io/split/android/client/storage/cipher/CBCCipher.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/CBCCipher.java rename to main/src/main/java/io/split/android/client/storage/cipher/CBCCipher.java diff --git a/src/main/java/io/split/android/client/storage/cipher/CBCCipherProvider.java b/main/src/main/java/io/split/android/client/storage/cipher/CBCCipherProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/CBCCipherProvider.java rename to main/src/main/java/io/split/android/client/storage/cipher/CBCCipherProvider.java diff --git a/src/main/java/io/split/android/client/storage/cipher/CipherProvider.java b/main/src/main/java/io/split/android/client/storage/cipher/CipherProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/CipherProvider.java rename to main/src/main/java/io/split/android/client/storage/cipher/CipherProvider.java diff --git a/src/main/java/io/split/android/client/storage/cipher/DBCipher.java b/main/src/main/java/io/split/android/client/storage/cipher/DBCipher.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/DBCipher.java rename to main/src/main/java/io/split/android/client/storage/cipher/DBCipher.java diff --git a/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java b/main/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java rename to main/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java diff --git a/src/main/java/io/split/android/client/storage/cipher/KeyManager.java b/main/src/main/java/io/split/android/client/storage/cipher/KeyManager.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/KeyManager.java rename to main/src/main/java/io/split/android/client/storage/cipher/KeyManager.java diff --git a/src/main/java/io/split/android/client/storage/cipher/NoOpCipher.java b/main/src/main/java/io/split/android/client/storage/cipher/NoOpCipher.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/NoOpCipher.java rename to main/src/main/java/io/split/android/client/storage/cipher/NoOpCipher.java diff --git a/src/main/java/io/split/android/client/storage/cipher/ObjectPool.java b/main/src/main/java/io/split/android/client/storage/cipher/ObjectPool.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/ObjectPool.java rename to main/src/main/java/io/split/android/client/storage/cipher/ObjectPool.java diff --git a/src/main/java/io/split/android/client/storage/cipher/ObjectPoolFactory.java b/main/src/main/java/io/split/android/client/storage/cipher/ObjectPoolFactory.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/ObjectPoolFactory.java rename to main/src/main/java/io/split/android/client/storage/cipher/ObjectPoolFactory.java diff --git a/src/main/java/io/split/android/client/storage/cipher/SplitCipher.java b/main/src/main/java/io/split/android/client/storage/cipher/SplitCipher.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/SplitCipher.java rename to main/src/main/java/io/split/android/client/storage/cipher/SplitCipher.java diff --git a/src/main/java/io/split/android/client/storage/cipher/SplitCipherFactory.java b/main/src/main/java/io/split/android/client/storage/cipher/SplitCipherFactory.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/SplitCipherFactory.java rename to main/src/main/java/io/split/android/client/storage/cipher/SplitCipherFactory.java diff --git a/src/main/java/io/split/android/client/storage/cipher/SplitEncryptionLevel.java b/main/src/main/java/io/split/android/client/storage/cipher/SplitEncryptionLevel.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/SplitEncryptionLevel.java rename to main/src/main/java/io/split/android/client/storage/cipher/SplitEncryptionLevel.java diff --git a/src/main/java/io/split/android/client/storage/cipher/provider/KeyProvider.java b/main/src/main/java/io/split/android/client/storage/cipher/provider/KeyProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/provider/KeyProvider.java rename to main/src/main/java/io/split/android/client/storage/cipher/provider/KeyProvider.java diff --git a/src/main/java/io/split/android/client/storage/cipher/provider/LegacyKeyProvider.java b/main/src/main/java/io/split/android/client/storage/cipher/provider/LegacyKeyProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/provider/LegacyKeyProvider.java rename to main/src/main/java/io/split/android/client/storage/cipher/provider/LegacyKeyProvider.java diff --git a/src/main/java/io/split/android/client/storage/cipher/provider/SecureKeyStorageProvider.java b/main/src/main/java/io/split/android/client/storage/cipher/provider/SecureKeyStorageProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/cipher/provider/SecureKeyStorageProvider.java rename to main/src/main/java/io/split/android/client/storage/cipher/provider/SecureKeyStorageProvider.java diff --git a/src/main/java/io/split/android/client/storage/common/InBytesSizable.java b/main/src/main/java/io/split/android/client/storage/common/InBytesSizable.java similarity index 100% rename from src/main/java/io/split/android/client/storage/common/InBytesSizable.java rename to main/src/main/java/io/split/android/client/storage/common/InBytesSizable.java diff --git a/src/main/java/io/split/android/client/storage/common/PersistentStorage.java b/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/common/PersistentStorage.java rename to main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java diff --git a/src/main/java/io/split/android/client/storage/common/RuleBasedSegmentStorageInitializer.java b/main/src/main/java/io/split/android/client/storage/common/RuleBasedSegmentStorageInitializer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/common/RuleBasedSegmentStorageInitializer.java rename to main/src/main/java/io/split/android/client/storage/common/RuleBasedSegmentStorageInitializer.java diff --git a/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java b/main/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java rename to main/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java diff --git a/src/main/java/io/split/android/client/storage/common/SqLitePersistentStorage.java b/main/src/main/java/io/split/android/client/storage/common/SqLitePersistentStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/common/SqLitePersistentStorage.java rename to main/src/main/java/io/split/android/client/storage/common/SqLitePersistentStorage.java diff --git a/src/main/java/io/split/android/client/storage/common/Storage.java b/main/src/main/java/io/split/android/client/storage/common/Storage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/common/Storage.java rename to main/src/main/java/io/split/android/client/storage/common/Storage.java diff --git a/src/main/java/io/split/android/client/storage/common/StoragePusher.java b/main/src/main/java/io/split/android/client/storage/common/StoragePusher.java similarity index 100% rename from src/main/java/io/split/android/client/storage/common/StoragePusher.java rename to main/src/main/java/io/split/android/client/storage/common/StoragePusher.java diff --git a/src/main/java/io/split/android/client/storage/db/EventDao.java b/main/src/main/java/io/split/android/client/storage/db/EventDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/EventDao.java rename to main/src/main/java/io/split/android/client/storage/db/EventDao.java diff --git a/src/main/java/io/split/android/client/storage/db/EventEntity.java b/main/src/main/java/io/split/android/client/storage/db/EventEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/EventEntity.java rename to main/src/main/java/io/split/android/client/storage/db/EventEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/GeneralInfoDao.java b/main/src/main/java/io/split/android/client/storage/db/GeneralInfoDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/GeneralInfoDao.java rename to main/src/main/java/io/split/android/client/storage/db/GeneralInfoDao.java diff --git a/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java b/main/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java rename to main/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/ImpressionDao.java b/main/src/main/java/io/split/android/client/storage/db/ImpressionDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/ImpressionDao.java rename to main/src/main/java/io/split/android/client/storage/db/ImpressionDao.java diff --git a/src/main/java/io/split/android/client/storage/db/ImpressionEntity.java b/main/src/main/java/io/split/android/client/storage/db/ImpressionEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/ImpressionEntity.java rename to main/src/main/java/io/split/android/client/storage/db/ImpressionEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/ImpressionsCountDao.java b/main/src/main/java/io/split/android/client/storage/db/ImpressionsCountDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/ImpressionsCountDao.java rename to main/src/main/java/io/split/android/client/storage/db/ImpressionsCountDao.java diff --git a/src/main/java/io/split/android/client/storage/db/ImpressionsCountEntity.java b/main/src/main/java/io/split/android/client/storage/db/ImpressionsCountEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/ImpressionsCountEntity.java rename to main/src/main/java/io/split/android/client/storage/db/ImpressionsCountEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java b/main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java similarity index 92% rename from src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java rename to main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java index c770c753a..7538a9970 100644 --- a/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java +++ b/main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java @@ -27,4 +27,8 @@ public interface MyLargeSegmentDao extends SegmentDao { @Override @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME) List getAll(); + + @Override + @Query("DELETE FROM " + TABLE_NAME) + void deleteAll(); } diff --git a/src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java b/main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java rename to main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/MySegmentDao.java b/main/src/main/java/io/split/android/client/storage/db/MySegmentDao.java similarity index 92% rename from src/main/java/io/split/android/client/storage/db/MySegmentDao.java rename to main/src/main/java/io/split/android/client/storage/db/MySegmentDao.java index b4c6ef5d7..7ab6f1d42 100644 --- a/src/main/java/io/split/android/client/storage/db/MySegmentDao.java +++ b/main/src/main/java/io/split/android/client/storage/db/MySegmentDao.java @@ -27,4 +27,8 @@ public interface MySegmentDao extends SegmentDao { @Override @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME) List getAll(); + + @Override + @Query("DELETE FROM " + TABLE_NAME) + void deleteAll(); } diff --git a/src/main/java/io/split/android/client/storage/db/MySegmentEntity.java b/main/src/main/java/io/split/android/client/storage/db/MySegmentEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/MySegmentEntity.java rename to main/src/main/java/io/split/android/client/storage/db/MySegmentEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/SegmentDao.java b/main/src/main/java/io/split/android/client/storage/db/SegmentDao.java similarity index 92% rename from src/main/java/io/split/android/client/storage/db/SegmentDao.java rename to main/src/main/java/io/split/android/client/storage/db/SegmentDao.java index 6f6e45a66..45d76800c 100644 --- a/src/main/java/io/split/android/client/storage/db/SegmentDao.java +++ b/main/src/main/java/io/split/android/client/storage/db/SegmentDao.java @@ -11,4 +11,6 @@ public interface SegmentDao { T getByUserKey(String userKey); List getAll(); + + void deleteAll(); } diff --git a/src/main/java/io/split/android/client/storage/db/SegmentEntity.java b/main/src/main/java/io/split/android/client/storage/db/SegmentEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/SegmentEntity.java rename to main/src/main/java/io/split/android/client/storage/db/SegmentEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/SplitDao.java b/main/src/main/java/io/split/android/client/storage/db/SplitDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/SplitDao.java rename to main/src/main/java/io/split/android/client/storage/db/SplitDao.java diff --git a/src/main/java/io/split/android/client/storage/db/SplitEntity.java b/main/src/main/java/io/split/android/client/storage/db/SplitEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/SplitEntity.java rename to main/src/main/java/io/split/android/client/storage/db/SplitEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java b/main/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/SplitQueryDao.java rename to main/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java b/main/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java rename to main/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java diff --git a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java b/main/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java similarity index 98% rename from src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java rename to main/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java index 0d9d7cc66..c423f3c86 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java +++ b/main/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java @@ -30,7 +30,8 @@ AttributesEntity.class, UniqueKeyEntity.class, ImpressionsObserverCacheEntity.class, MyLargeSegmentEntity.class, RuleBasedSegmentEntity.class }, - version = 7 + version = 7, + exportSchema = false ) public abstract class SplitRoomDatabase extends RoomDatabase { diff --git a/src/main/java/io/split/android/client/storage/db/StorageFactory.java b/main/src/main/java/io/split/android/client/storage/db/StorageFactory.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/StorageFactory.java rename to main/src/main/java/io/split/android/client/storage/db/StorageFactory.java diff --git a/src/main/java/io/split/android/client/storage/db/StorageRecordStatus.java b/main/src/main/java/io/split/android/client/storage/db/StorageRecordStatus.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/StorageRecordStatus.java rename to main/src/main/java/io/split/android/client/storage/db/StorageRecordStatus.java diff --git a/src/main/java/io/split/android/client/storage/db/attributes/AttributesDao.java b/main/src/main/java/io/split/android/client/storage/db/attributes/AttributesDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/attributes/AttributesDao.java rename to main/src/main/java/io/split/android/client/storage/db/attributes/AttributesDao.java diff --git a/src/main/java/io/split/android/client/storage/db/attributes/AttributesEntity.java b/main/src/main/java/io/split/android/client/storage/db/attributes/AttributesEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/attributes/AttributesEntity.java rename to main/src/main/java/io/split/android/client/storage/db/attributes/AttributesEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java b/main/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java rename to main/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java diff --git a/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheEntity.java b/main/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheEntity.java rename to main/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeyEntity.java b/main/src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeyEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeyEntity.java rename to main/src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeyEntity.java diff --git a/src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeysDao.java b/main/src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeysDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeysDao.java rename to main/src/main/java/io/split/android/client/storage/db/impressions/unique/UniqueKeysDao.java diff --git a/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentDao.java b/main/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentDao.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentDao.java rename to main/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentDao.java diff --git a/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentEntity.java b/main/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentEntity.java similarity index 100% rename from src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentEntity.java rename to main/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentEntity.java diff --git a/src/main/java/io/split/android/client/storage/events/EventsStorage.java b/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/events/EventsStorage.java rename to main/src/main/java/io/split/android/client/storage/events/EventsStorage.java diff --git a/src/main/java/io/split/android/client/storage/events/PersistentEventsStorage.java b/main/src/main/java/io/split/android/client/storage/events/PersistentEventsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/events/PersistentEventsStorage.java rename to main/src/main/java/io/split/android/client/storage/events/PersistentEventsStorage.java diff --git a/src/main/java/io/split/android/client/storage/events/SqLitePersistentEventsStorage.java b/main/src/main/java/io/split/android/client/storage/events/SqLitePersistentEventsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/events/SqLitePersistentEventsStorage.java rename to main/src/main/java/io/split/android/client/storage/events/SqLitePersistentEventsStorage.java diff --git a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java b/main/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java rename to main/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java diff --git a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java b/main/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java rename to main/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java diff --git a/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java rename to main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java diff --git a/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsCountStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsCountStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsCountStorage.java rename to main/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsCountStorage.java diff --git a/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsStorage.java rename to main/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsStorage.java diff --git a/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsUniqueStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsUniqueStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsUniqueStorage.java rename to main/src/main/java/io/split/android/client/storage/impressions/PersistentImpressionsUniqueStorage.java diff --git a/src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsCountStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsCountStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsCountStorage.java rename to main/src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsCountStorage.java diff --git a/src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsStorage.java rename to main/src/main/java/io/split/android/client/storage/impressions/SqLitePersistentImpressionsStorage.java diff --git a/src/main/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorage.java rename to main/src/main/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorage.java diff --git a/src/main/java/io/split/android/client/storage/legacy/FileStorage.java b/main/src/main/java/io/split/android/client/storage/legacy/FileStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/legacy/FileStorage.java rename to main/src/main/java/io/split/android/client/storage/legacy/FileStorage.java diff --git a/src/main/java/io/split/android/client/storage/legacy/FileStorageHelper.java b/main/src/main/java/io/split/android/client/storage/legacy/FileStorageHelper.java similarity index 100% rename from src/main/java/io/split/android/client/storage/legacy/FileStorageHelper.java rename to main/src/main/java/io/split/android/client/storage/legacy/FileStorageHelper.java diff --git a/src/main/java/io/split/android/client/storage/legacy/IStorage.java b/main/src/main/java/io/split/android/client/storage/legacy/IStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/legacy/IStorage.java rename to main/src/main/java/io/split/android/client/storage/legacy/IStorage.java diff --git a/src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java b/main/src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java rename to main/src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java rename to main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java rename to main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java similarity index 93% rename from src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java rename to main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java index 009d3d6f6..bf26f739c 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java +++ b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java @@ -50,6 +50,8 @@ public void loadLocal() { @Override public void clear() { synchronized (lock) { + mPersistentMySegmentsStorage.clear(); + // Clear in-memory segments for keys in mStorageMap for (MySegmentsStorage mySegmentsStorage : mStorageMap.values()) { mySegmentsStorage.clear(); } diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java rename to main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java diff --git a/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java b/main/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java similarity index 93% rename from src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java rename to main/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java index 03ad60ae8..01568eebe 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java +++ b/main/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java @@ -9,4 +9,6 @@ public interface PersistentMySegmentsStorage { SegmentsChange getSnapshot(String userKey); void close(); + + void clear(); } diff --git a/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java b/main/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java similarity index 97% rename from src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java rename to main/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java index 8d2da826d..61132b23c 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java +++ b/main/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java @@ -57,6 +57,11 @@ public SegmentsChange getSnapshot(String userKey) { public void close() { } + @Override + public void clear() { + mDao.deleteAll(); + } + private SegmentsChange getMySegmentsFromEntity(SegmentEntity entity) { if (entity == null || Utils.isNullOrEmpty(entity.getSegmentList())) { return createEmpty(); diff --git a/src/main/java/io/split/android/client/storage/rbs/Clearer.java b/main/src/main/java/io/split/android/client/storage/rbs/Clearer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/Clearer.java rename to main/src/main/java/io/split/android/client/storage/rbs/Clearer.java diff --git a/src/main/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProvider.java b/main/src/main/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProvider.java rename to main/src/main/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProvider.java diff --git a/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java b/main/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java rename to main/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java b/main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java rename to main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorage.java b/main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorage.java rename to main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorage.java diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageConsumer.java b/main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageConsumer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageConsumer.java rename to main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageConsumer.java diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java b/main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java rename to main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducer.java b/main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducer.java rename to main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducer.java diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImpl.java b/main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImpl.java rename to main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImpl.java diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProvider.java b/main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProvider.java rename to main/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProvider.java diff --git a/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java b/main/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java rename to main/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java diff --git a/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java b/main/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java rename to main/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java diff --git a/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java b/main/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java rename to main/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java diff --git a/src/main/java/io/split/android/client/storage/rbs/Updater.java b/main/src/main/java/io/split/android/client/storage/rbs/Updater.java similarity index 100% rename from src/main/java/io/split/android/client/storage/rbs/Updater.java rename to main/src/main/java/io/split/android/client/storage/rbs/Updater.java diff --git a/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java b/main/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/MetadataHelper.java rename to main/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java diff --git a/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java b/main/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java rename to main/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java diff --git a/src/main/java/io/split/android/client/storage/splits/ProcessedSplitChange.java b/main/src/main/java/io/split/android/client/storage/splits/ProcessedSplitChange.java similarity index 68% rename from src/main/java/io/split/android/client/storage/splits/ProcessedSplitChange.java rename to main/src/main/java/io/split/android/client/storage/splits/ProcessedSplitChange.java index 508731e23..9946442de 100644 --- a/src/main/java/io/split/android/client/storage/splits/ProcessedSplitChange.java +++ b/main/src/main/java/io/split/android/client/storage/splits/ProcessedSplitChange.java @@ -1,5 +1,7 @@ package io.split.android.client.storage.splits; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import io.split.android.client.dtos.Split; @@ -11,8 +13,9 @@ public class ProcessedSplitChange { private final long updateTimestamp; public ProcessedSplitChange(List activeSplits, List archivedSplits, long changeNumber, long updateTimestamp) { - this.activeSplits = activeSplits; - this.archivedSplits = archivedSplits; + // Create defensive copies to ensure thread safety + this.activeSplits = activeSplits != null ? Collections.unmodifiableList(new ArrayList<>(activeSplits)) : Collections.emptyList(); + this.archivedSplits = archivedSplits != null ? Collections.unmodifiableList(new ArrayList<>(archivedSplits)) : Collections.emptyList(); this.changeNumber = changeNumber; this.updateTimestamp = updateTimestamp; } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java b/main/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java rename to main/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java diff --git a/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java b/main/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java rename to main/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java diff --git a/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java b/main/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java rename to main/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java b/main/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java rename to main/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java b/main/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/SplitsStorage.java rename to main/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java b/main/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java rename to main/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java diff --git a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java b/main/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java similarity index 100% rename from src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java rename to main/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java diff --git a/src/main/java/io/split/android/client/telemetry/TelemetryConfigBodySerializer.java b/main/src/main/java/io/split/android/client/telemetry/TelemetryConfigBodySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/TelemetryConfigBodySerializer.java rename to main/src/main/java/io/split/android/client/telemetry/TelemetryConfigBodySerializer.java diff --git a/src/main/java/io/split/android/client/telemetry/TelemetryHelper.java b/main/src/main/java/io/split/android/client/telemetry/TelemetryHelper.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/TelemetryHelper.java rename to main/src/main/java/io/split/android/client/telemetry/TelemetryHelper.java diff --git a/src/main/java/io/split/android/client/telemetry/TelemetryHelperImpl.java b/main/src/main/java/io/split/android/client/telemetry/TelemetryHelperImpl.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/TelemetryHelperImpl.java rename to main/src/main/java/io/split/android/client/telemetry/TelemetryHelperImpl.java diff --git a/src/main/java/io/split/android/client/telemetry/TelemetryStatsBodySerializer.java b/main/src/main/java/io/split/android/client/telemetry/TelemetryStatsBodySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/TelemetryStatsBodySerializer.java rename to main/src/main/java/io/split/android/client/telemetry/TelemetryStatsBodySerializer.java diff --git a/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizer.java b/main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/TelemetrySynchronizer.java rename to main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizer.java diff --git a/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java b/main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java rename to main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerImpl.java diff --git a/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerStub.java b/main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerStub.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerStub.java rename to main/src/main/java/io/split/android/client/telemetry/TelemetrySynchronizerStub.java diff --git a/src/main/java/io/split/android/client/telemetry/model/Config.java b/main/src/main/java/io/split/android/client/telemetry/model/Config.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/Config.java rename to main/src/main/java/io/split/android/client/telemetry/model/Config.java diff --git a/src/main/java/io/split/android/client/telemetry/model/EventTypeEnum.java b/main/src/main/java/io/split/android/client/telemetry/model/EventTypeEnum.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/EventTypeEnum.java rename to main/src/main/java/io/split/android/client/telemetry/model/EventTypeEnum.java diff --git a/src/main/java/io/split/android/client/telemetry/model/EventsDataRecordsEnum.java b/main/src/main/java/io/split/android/client/telemetry/model/EventsDataRecordsEnum.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/EventsDataRecordsEnum.java rename to main/src/main/java/io/split/android/client/telemetry/model/EventsDataRecordsEnum.java diff --git a/src/main/java/io/split/android/client/telemetry/model/FactoryCounter.java b/main/src/main/java/io/split/android/client/telemetry/model/FactoryCounter.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/FactoryCounter.java rename to main/src/main/java/io/split/android/client/telemetry/model/FactoryCounter.java diff --git a/src/main/java/io/split/android/client/telemetry/model/HttpErrors.java b/main/src/main/java/io/split/android/client/telemetry/model/HttpErrors.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/HttpErrors.java rename to main/src/main/java/io/split/android/client/telemetry/model/HttpErrors.java diff --git a/src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java b/main/src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java rename to main/src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java diff --git a/src/main/java/io/split/android/client/telemetry/model/ImpressionsDataType.java b/main/src/main/java/io/split/android/client/telemetry/model/ImpressionsDataType.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/ImpressionsDataType.java rename to main/src/main/java/io/split/android/client/telemetry/model/ImpressionsDataType.java diff --git a/src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java b/main/src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java rename to main/src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java diff --git a/src/main/java/io/split/android/client/telemetry/model/LastSync.java b/main/src/main/java/io/split/android/client/telemetry/model/LastSync.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/LastSync.java rename to main/src/main/java/io/split/android/client/telemetry/model/LastSync.java diff --git a/src/main/java/io/split/android/client/telemetry/model/Method.java b/main/src/main/java/io/split/android/client/telemetry/model/Method.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/Method.java rename to main/src/main/java/io/split/android/client/telemetry/model/Method.java diff --git a/src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java b/main/src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java rename to main/src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java diff --git a/src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java b/main/src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java rename to main/src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java diff --git a/src/main/java/io/split/android/client/telemetry/model/OperationMode.java b/main/src/main/java/io/split/android/client/telemetry/model/OperationMode.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/OperationMode.java rename to main/src/main/java/io/split/android/client/telemetry/model/OperationMode.java diff --git a/src/main/java/io/split/android/client/telemetry/model/OperationType.java b/main/src/main/java/io/split/android/client/telemetry/model/OperationType.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/OperationType.java rename to main/src/main/java/io/split/android/client/telemetry/model/OperationType.java diff --git a/src/main/java/io/split/android/client/telemetry/model/PushCounterEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/PushCounterEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/PushCounterEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/PushCounterEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/RefreshRates.java b/main/src/main/java/io/split/android/client/telemetry/model/RefreshRates.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/RefreshRates.java rename to main/src/main/java/io/split/android/client/telemetry/model/RefreshRates.java diff --git a/src/main/java/io/split/android/client/telemetry/model/Stats.java b/main/src/main/java/io/split/android/client/telemetry/model/Stats.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/Stats.java rename to main/src/main/java/io/split/android/client/telemetry/model/Stats.java diff --git a/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java b/main/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java rename to main/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java diff --git a/src/main/java/io/split/android/client/telemetry/model/UrlOverrides.java b/main/src/main/java/io/split/android/client/telemetry/model/UrlOverrides.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/UrlOverrides.java rename to main/src/main/java/io/split/android/client/telemetry/model/UrlOverrides.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/AblyErrorStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/AblyErrorStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/AblyErrorStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/AblyErrorStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/ConnectionEstablishedStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/ConnectionEstablishedStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/ConnectionEstablishedStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/ConnectionEstablishedStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/OccupancyPriStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/OccupancyPriStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/OccupancyPriStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/OccupancyPriStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/OccupancySecStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/OccupancySecStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/OccupancySecStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/OccupancySecStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/SseConnectionErrorStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/SseConnectionErrorStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/SseConnectionErrorStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/SseConnectionErrorStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/StreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/StreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/StreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/StreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/StreamingStatusStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/StreamingStatusStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/StreamingStatusStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/StreamingStatusStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/SyncModeUpdateStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/SyncModeUpdateStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/SyncModeUpdateStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/SyncModeUpdateStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/TokenRefreshStreamingEvent.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/TokenRefreshStreamingEvent.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/TokenRefreshStreamingEvent.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/TokenRefreshStreamingEvent.java diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/UpdatesFromSSEEnum.java b/main/src/main/java/io/split/android/client/telemetry/model/streaming/UpdatesFromSSEEnum.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/model/streaming/UpdatesFromSSEEnum.java rename to main/src/main/java/io/split/android/client/telemetry/model/streaming/UpdatesFromSSEEnum.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/BinarySearchLatencyTracker.java b/main/src/main/java/io/split/android/client/telemetry/storage/BinarySearchLatencyTracker.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/BinarySearchLatencyTracker.java rename to main/src/main/java/io/split/android/client/telemetry/storage/BinarySearchLatencyTracker.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/ILatencyTracker.java b/main/src/main/java/io/split/android/client/telemetry/storage/ILatencyTracker.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/ILatencyTracker.java rename to main/src/main/java/io/split/android/client/telemetry/storage/ILatencyTracker.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java b/main/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java rename to main/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java b/main/src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java rename to main/src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProvider.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProvider.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProvider.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProvider.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationConsumer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationConsumer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationConsumer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationConsumer.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationProducer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationProducer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationProducer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryEvaluationProducer.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryInitConsumer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryInitConsumer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryInitConsumer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryInitConsumer.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryInitProducer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryInitProducer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryInitProducer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryInitProducer.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProvider.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProvider.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProvider.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProvider.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorage.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorage.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryStorage.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorage.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageConsumer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageConsumer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageConsumer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageConsumer.java diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageProducer.java b/main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageProducer.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageProducer.java rename to main/src/main/java/io/split/android/client/telemetry/storage/TelemetryStorageProducer.java diff --git a/src/main/java/io/split/android/client/telemetry/util/AtomicLongArray.java b/main/src/main/java/io/split/android/client/telemetry/util/AtomicLongArray.java similarity index 100% rename from src/main/java/io/split/android/client/telemetry/util/AtomicLongArray.java rename to main/src/main/java/io/split/android/client/telemetry/util/AtomicLongArray.java diff --git a/src/main/java/io/split/android/client/utils/BCrypt.java b/main/src/main/java/io/split/android/client/utils/BCrypt.java similarity index 100% rename from src/main/java/io/split/android/client/utils/BCrypt.java rename to main/src/main/java/io/split/android/client/utils/BCrypt.java diff --git a/src/main/java/io/split/android/client/utils/Base64Util.java b/main/src/main/java/io/split/android/client/utils/Base64Util.java similarity index 100% rename from src/main/java/io/split/android/client/utils/Base64Util.java rename to main/src/main/java/io/split/android/client/utils/Base64Util.java diff --git a/src/main/java/io/split/android/client/utils/CompressionUtil.java b/main/src/main/java/io/split/android/client/utils/CompressionUtil.java similarity index 100% rename from src/main/java/io/split/android/client/utils/CompressionUtil.java rename to main/src/main/java/io/split/android/client/utils/CompressionUtil.java diff --git a/src/main/java/io/split/android/client/utils/FileUtils.java b/main/src/main/java/io/split/android/client/utils/FileUtils.java similarity index 100% rename from src/main/java/io/split/android/client/utils/FileUtils.java rename to main/src/main/java/io/split/android/client/utils/FileUtils.java diff --git a/src/main/java/io/split/android/client/utils/Gzip.java b/main/src/main/java/io/split/android/client/utils/Gzip.java similarity index 100% rename from src/main/java/io/split/android/client/utils/Gzip.java rename to main/src/main/java/io/split/android/client/utils/Gzip.java diff --git a/src/main/java/io/split/android/client/utils/HttpProxySerializer.java b/main/src/main/java/io/split/android/client/utils/HttpProxySerializer.java similarity index 100% rename from src/main/java/io/split/android/client/utils/HttpProxySerializer.java rename to main/src/main/java/io/split/android/client/utils/HttpProxySerializer.java diff --git a/src/main/java/io/split/android/client/utils/Json.java b/main/src/main/java/io/split/android/client/utils/Json.java similarity index 100% rename from src/main/java/io/split/android/client/utils/Json.java rename to main/src/main/java/io/split/android/client/utils/Json.java diff --git a/src/main/java/io/split/android/client/utils/MemoryUtils.java b/main/src/main/java/io/split/android/client/utils/MemoryUtils.java similarity index 100% rename from src/main/java/io/split/android/client/utils/MemoryUtils.java rename to main/src/main/java/io/split/android/client/utils/MemoryUtils.java diff --git a/src/main/java/io/split/android/client/utils/MemoryUtilsImpl.java b/main/src/main/java/io/split/android/client/utils/MemoryUtilsImpl.java similarity index 100% rename from src/main/java/io/split/android/client/utils/MemoryUtilsImpl.java rename to main/src/main/java/io/split/android/client/utils/MemoryUtilsImpl.java diff --git a/src/main/java/io/split/android/client/utils/MurmurHash3.java b/main/src/main/java/io/split/android/client/utils/MurmurHash3.java similarity index 100% rename from src/main/java/io/split/android/client/utils/MurmurHash3.java rename to main/src/main/java/io/split/android/client/utils/MurmurHash3.java diff --git a/src/main/java/io/split/android/client/utils/StringHelper.java b/main/src/main/java/io/split/android/client/utils/StringHelper.java similarity index 100% rename from src/main/java/io/split/android/client/utils/StringHelper.java rename to main/src/main/java/io/split/android/client/utils/StringHelper.java diff --git a/src/main/java/io/split/android/client/utils/Utils.java b/main/src/main/java/io/split/android/client/utils/Utils.java similarity index 100% rename from src/main/java/io/split/android/client/utils/Utils.java rename to main/src/main/java/io/split/android/client/utils/Utils.java diff --git a/src/main/java/io/split/android/client/utils/YamlParser.java b/main/src/main/java/io/split/android/client/utils/YamlParser.java similarity index 100% rename from src/main/java/io/split/android/client/utils/YamlParser.java rename to main/src/main/java/io/split/android/client/utils/YamlParser.java diff --git a/src/main/java/io/split/android/client/utils/Zlib.java b/main/src/main/java/io/split/android/client/utils/Zlib.java similarity index 100% rename from src/main/java/io/split/android/client/utils/Zlib.java rename to main/src/main/java/io/split/android/client/utils/Zlib.java diff --git a/src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java b/main/src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java similarity index 100% rename from src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java rename to main/src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java diff --git a/src/main/java/io/split/android/client/utils/serializer/DoubleSerializer.java b/main/src/main/java/io/split/android/client/utils/serializer/DoubleSerializer.java similarity index 100% rename from src/main/java/io/split/android/client/utils/serializer/DoubleSerializer.java rename to main/src/main/java/io/split/android/client/utils/serializer/DoubleSerializer.java diff --git a/src/main/java/io/split/android/client/validators/ApiKeyValidator.java b/main/src/main/java/io/split/android/client/validators/ApiKeyValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/ApiKeyValidator.java rename to main/src/main/java/io/split/android/client/validators/ApiKeyValidator.java diff --git a/src/main/java/io/split/android/client/validators/ApiKeyValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/ApiKeyValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/ApiKeyValidatorImpl.java rename to main/src/main/java/io/split/android/client/validators/ApiKeyValidatorImpl.java diff --git a/src/main/java/io/split/android/client/validators/AttributesValidator.java b/main/src/main/java/io/split/android/client/validators/AttributesValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/AttributesValidator.java rename to main/src/main/java/io/split/android/client/validators/AttributesValidator.java diff --git a/src/main/java/io/split/android/client/validators/AttributesValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/AttributesValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/AttributesValidatorImpl.java rename to main/src/main/java/io/split/android/client/validators/AttributesValidatorImpl.java diff --git a/src/main/java/io/split/android/client/validators/EventValidator.java b/main/src/main/java/io/split/android/client/validators/EventValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/EventValidator.java rename to main/src/main/java/io/split/android/client/validators/EventValidator.java diff --git a/src/main/java/io/split/android/client/validators/EventValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/EventValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/EventValidatorImpl.java rename to main/src/main/java/io/split/android/client/validators/EventValidatorImpl.java diff --git a/src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java rename to main/src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java diff --git a/src/main/java/io/split/android/client/validators/KeyValidator.java b/main/src/main/java/io/split/android/client/validators/KeyValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/KeyValidator.java rename to main/src/main/java/io/split/android/client/validators/KeyValidator.java diff --git a/src/main/java/io/split/android/client/validators/KeyValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/KeyValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/KeyValidatorImpl.java rename to main/src/main/java/io/split/android/client/validators/KeyValidatorImpl.java diff --git a/src/main/java/io/split/android/client/validators/PrefixValidator.java b/main/src/main/java/io/split/android/client/validators/PrefixValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/PrefixValidator.java rename to main/src/main/java/io/split/android/client/validators/PrefixValidator.java diff --git a/src/main/java/io/split/android/client/validators/PrefixValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/PrefixValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/PrefixValidatorImpl.java rename to main/src/main/java/io/split/android/client/validators/PrefixValidatorImpl.java diff --git a/src/main/java/io/split/android/client/validators/PropertyValidator.java b/main/src/main/java/io/split/android/client/validators/PropertyValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/PropertyValidator.java rename to main/src/main/java/io/split/android/client/validators/PropertyValidator.java diff --git a/src/main/java/io/split/android/client/validators/SplitFilterValidator.java b/main/src/main/java/io/split/android/client/validators/SplitFilterValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/SplitFilterValidator.java rename to main/src/main/java/io/split/android/client/validators/SplitFilterValidator.java diff --git a/src/main/java/io/split/android/client/validators/SplitValidator.java b/main/src/main/java/io/split/android/client/validators/SplitValidator.java similarity index 100% rename from src/main/java/io/split/android/client/validators/SplitValidator.java rename to main/src/main/java/io/split/android/client/validators/SplitValidator.java diff --git a/src/main/java/io/split/android/client/validators/SplitValidatorImpl.java b/main/src/main/java/io/split/android/client/validators/SplitValidatorImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/SplitValidatorImpl.java rename to main/src/main/java/io/split/android/client/validators/SplitValidatorImpl.java diff --git a/src/main/java/io/split/android/client/validators/TreatmentManager.java b/main/src/main/java/io/split/android/client/validators/TreatmentManager.java similarity index 100% rename from src/main/java/io/split/android/client/validators/TreatmentManager.java rename to main/src/main/java/io/split/android/client/validators/TreatmentManager.java diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerFactory.java b/main/src/main/java/io/split/android/client/validators/TreatmentManagerFactory.java similarity index 100% rename from src/main/java/io/split/android/client/validators/TreatmentManagerFactory.java rename to main/src/main/java/io/split/android/client/validators/TreatmentManagerFactory.java diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java b/main/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java rename to main/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerHelper.java b/main/src/main/java/io/split/android/client/validators/TreatmentManagerHelper.java similarity index 100% rename from src/main/java/io/split/android/client/validators/TreatmentManagerHelper.java rename to main/src/main/java/io/split/android/client/validators/TreatmentManagerHelper.java diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java b/main/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java rename to main/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java diff --git a/src/main/java/io/split/android/client/validators/ValidationConfig.java b/main/src/main/java/io/split/android/client/validators/ValidationConfig.java similarity index 100% rename from src/main/java/io/split/android/client/validators/ValidationConfig.java rename to main/src/main/java/io/split/android/client/validators/ValidationConfig.java diff --git a/src/main/java/io/split/android/client/validators/ValidationErrorInfo.java b/main/src/main/java/io/split/android/client/validators/ValidationErrorInfo.java similarity index 100% rename from src/main/java/io/split/android/client/validators/ValidationErrorInfo.java rename to main/src/main/java/io/split/android/client/validators/ValidationErrorInfo.java diff --git a/src/main/java/io/split/android/client/validators/ValidationMessageLogger.java b/main/src/main/java/io/split/android/client/validators/ValidationMessageLogger.java similarity index 100% rename from src/main/java/io/split/android/client/validators/ValidationMessageLogger.java rename to main/src/main/java/io/split/android/client/validators/ValidationMessageLogger.java diff --git a/src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java b/main/src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java similarity index 100% rename from src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java rename to main/src/main/java/io/split/android/client/validators/ValidationMessageLoggerImpl.java diff --git a/src/main/java/io/split/android/engine/experiments/DefaultConditionsProvider.java b/main/src/main/java/io/split/android/engine/experiments/DefaultConditionsProvider.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/DefaultConditionsProvider.java rename to main/src/main/java/io/split/android/engine/experiments/DefaultConditionsProvider.java diff --git a/src/main/java/io/split/android/engine/experiments/FetcherPolicy.java b/main/src/main/java/io/split/android/engine/experiments/FetcherPolicy.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/FetcherPolicy.java rename to main/src/main/java/io/split/android/engine/experiments/FetcherPolicy.java diff --git a/src/main/java/io/split/android/engine/experiments/ParsedCondition.java b/main/src/main/java/io/split/android/engine/experiments/ParsedCondition.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/ParsedCondition.java rename to main/src/main/java/io/split/android/engine/experiments/ParsedCondition.java diff --git a/src/main/java/io/split/android/engine/experiments/ParsedRuleBasedSegment.java b/main/src/main/java/io/split/android/engine/experiments/ParsedRuleBasedSegment.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/ParsedRuleBasedSegment.java rename to main/src/main/java/io/split/android/engine/experiments/ParsedRuleBasedSegment.java diff --git a/src/main/java/io/split/android/engine/experiments/ParsedSplit.java b/main/src/main/java/io/split/android/engine/experiments/ParsedSplit.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/ParsedSplit.java rename to main/src/main/java/io/split/android/engine/experiments/ParsedSplit.java diff --git a/src/main/java/io/split/android/engine/experiments/Parser.java b/main/src/main/java/io/split/android/engine/experiments/Parser.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/Parser.java rename to main/src/main/java/io/split/android/engine/experiments/Parser.java diff --git a/src/main/java/io/split/android/engine/experiments/ParserCommons.java b/main/src/main/java/io/split/android/engine/experiments/ParserCommons.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/ParserCommons.java rename to main/src/main/java/io/split/android/engine/experiments/ParserCommons.java diff --git a/src/main/java/io/split/android/engine/experiments/RuleBasedSegmentParser.java b/main/src/main/java/io/split/android/engine/experiments/RuleBasedSegmentParser.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/RuleBasedSegmentParser.java rename to main/src/main/java/io/split/android/engine/experiments/RuleBasedSegmentParser.java diff --git a/src/main/java/io/split/android/engine/experiments/SplitFetcher.java b/main/src/main/java/io/split/android/engine/experiments/SplitFetcher.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/SplitFetcher.java rename to main/src/main/java/io/split/android/engine/experiments/SplitFetcher.java diff --git a/src/main/java/io/split/android/engine/experiments/SplitParser.java b/main/src/main/java/io/split/android/engine/experiments/SplitParser.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/SplitParser.java rename to main/src/main/java/io/split/android/engine/experiments/SplitParser.java diff --git a/src/main/java/io/split/android/engine/experiments/UnsupportedMatcherException.java b/main/src/main/java/io/split/android/engine/experiments/UnsupportedMatcherException.java similarity index 100% rename from src/main/java/io/split/android/engine/experiments/UnsupportedMatcherException.java rename to main/src/main/java/io/split/android/engine/experiments/UnsupportedMatcherException.java diff --git a/src/main/java/io/split/android/engine/matchers/AllKeysMatcher.java b/main/src/main/java/io/split/android/engine/matchers/AllKeysMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/AllKeysMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/AllKeysMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/AttributeMatcher.java b/main/src/main/java/io/split/android/engine/matchers/AttributeMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/AttributeMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/AttributeMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/BetweenMatcher.java b/main/src/main/java/io/split/android/engine/matchers/BetweenMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/BetweenMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/BetweenMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/BooleanMatcher.java b/main/src/main/java/io/split/android/engine/matchers/BooleanMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/BooleanMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/BooleanMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/CombiningMatcher.java b/main/src/main/java/io/split/android/engine/matchers/CombiningMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/CombiningMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/CombiningMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/DependencyMatcher.java b/main/src/main/java/io/split/android/engine/matchers/DependencyMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/DependencyMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/DependencyMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/EqualToMatcher.java b/main/src/main/java/io/split/android/engine/matchers/EqualToMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/EqualToMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/EqualToMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcher.java b/main/src/main/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcher.java b/main/src/main/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/LessThanOrEqualToMatcher.java b/main/src/main/java/io/split/android/engine/matchers/LessThanOrEqualToMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/LessThanOrEqualToMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/LessThanOrEqualToMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/Matcher.java b/main/src/main/java/io/split/android/engine/matchers/Matcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/Matcher.java rename to main/src/main/java/io/split/android/engine/matchers/Matcher.java diff --git a/src/main/java/io/split/android/engine/matchers/MySegmentsMatcher.java b/main/src/main/java/io/split/android/engine/matchers/MySegmentsMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/MySegmentsMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/MySegmentsMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/PrerequisitesMatcher.java b/main/src/main/java/io/split/android/engine/matchers/PrerequisitesMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/PrerequisitesMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/PrerequisitesMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/Transformers.java b/main/src/main/java/io/split/android/engine/matchers/Transformers.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/Transformers.java rename to main/src/main/java/io/split/android/engine/matchers/Transformers.java diff --git a/src/main/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcher.java b/main/src/main/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcher.java b/main/src/main/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/collections/EqualToSetMatcher.java b/main/src/main/java/io/split/android/engine/matchers/collections/EqualToSetMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/collections/EqualToSetMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/collections/EqualToSetMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/collections/PartOfSetMatcher.java b/main/src/main/java/io/split/android/engine/matchers/collections/PartOfSetMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/collections/PartOfSetMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/collections/PartOfSetMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/semver/BetweenSemverMatcher.java b/main/src/main/java/io/split/android/engine/matchers/semver/BetweenSemverMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/semver/BetweenSemverMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/semver/BetweenSemverMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/semver/EqualToSemverMatcher.java b/main/src/main/java/io/split/android/engine/matchers/semver/EqualToSemverMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/semver/EqualToSemverMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/semver/EqualToSemverMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcher.java b/main/src/main/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/semver/InListSemverMatcher.java b/main/src/main/java/io/split/android/engine/matchers/semver/InListSemverMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/semver/InListSemverMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/semver/InListSemverMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcher.java b/main/src/main/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/semver/Semver.java b/main/src/main/java/io/split/android/engine/matchers/semver/Semver.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/semver/Semver.java rename to main/src/main/java/io/split/android/engine/matchers/semver/Semver.java diff --git a/src/main/java/io/split/android/engine/matchers/semver/SemverParseException.java b/main/src/main/java/io/split/android/engine/matchers/semver/SemverParseException.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/semver/SemverParseException.java rename to main/src/main/java/io/split/android/engine/matchers/semver/SemverParseException.java diff --git a/src/main/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcher.java b/main/src/main/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcher.java b/main/src/main/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/strings/RegularExpressionMatcher.java b/main/src/main/java/io/split/android/engine/matchers/strings/RegularExpressionMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/strings/RegularExpressionMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/strings/RegularExpressionMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcher.java b/main/src/main/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcher.java diff --git a/src/main/java/io/split/android/engine/matchers/strings/WhitelistMatcher.java b/main/src/main/java/io/split/android/engine/matchers/strings/WhitelistMatcher.java similarity index 100% rename from src/main/java/io/split/android/engine/matchers/strings/WhitelistMatcher.java rename to main/src/main/java/io/split/android/engine/matchers/strings/WhitelistMatcher.java diff --git a/src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutor.java b/main/src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutor.java similarity index 100% rename from src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutor.java rename to main/src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutor.java diff --git a/src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImpl.java b/main/src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImpl.java similarity index 100% rename from src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImpl.java rename to main/src/main/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImpl.java diff --git a/src/main/java/io/split/android/engine/splitter/Splitter.java b/main/src/main/java/io/split/android/engine/splitter/Splitter.java similarity index 100% rename from src/main/java/io/split/android/engine/splitter/Splitter.java rename to main/src/main/java/io/split/android/engine/splitter/Splitter.java diff --git a/src/main/java/io/split/android/grammar/Treatments.java b/main/src/main/java/io/split/android/grammar/Treatments.java similarity index 100% rename from src/main/java/io/split/android/grammar/Treatments.java rename to main/src/main/java/io/split/android/grammar/Treatments.java diff --git a/src/sharedTest/java/helper/CompressionHelper.java b/main/src/sharedTest/java/helper/CompressionHelper.java similarity index 100% rename from src/sharedTest/java/helper/CompressionHelper.java rename to main/src/sharedTest/java/helper/CompressionHelper.java diff --git a/src/sharedTest/java/helper/TestingData.java b/main/src/sharedTest/java/helper/TestingData.java similarity index 100% rename from src/sharedTest/java/helper/TestingData.java rename to main/src/sharedTest/java/helper/TestingData.java diff --git a/src/sharedTest/java/helper/TestingHelper.java b/main/src/sharedTest/java/helper/TestingHelper.java similarity index 100% rename from src/sharedTest/java/helper/TestingHelper.java rename to main/src/sharedTest/java/helper/TestingHelper.java diff --git a/main/src/sharedTest/java/io/split/android/client/utils/logger/LogPrinterStub.java b/main/src/sharedTest/java/io/split/android/client/utils/logger/LogPrinterStub.java new file mode 100644 index 000000000..e8c53ef3d --- /dev/null +++ b/main/src/sharedTest/java/io/split/android/client/utils/logger/LogPrinterStub.java @@ -0,0 +1,66 @@ +package io.split.android.client.utils.logger; + +import android.util.Log; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; + +public class LogPrinterStub implements LogPrinter { + private final Set calls = new HashSet<>(); + private final Map> logs = new ConcurrentHashMap<>(); + + public LogPrinterStub() { + // Initialize for all Android log levels: VERBOSE(2) .. ASSERT(7) + for (int level = Log.VERBOSE; level <= Log.ASSERT; level++) { + logs.put(level, new ConcurrentLinkedDeque<>()); + } + } + + @Override + public void v(String tag, String msg, Throwable tr) { + logs.get(Log.VERBOSE).add(msg); + calls.add(Log.VERBOSE); + } + + @Override + public void d(String tag, String msg, Throwable tr) { + logs.get(Log.DEBUG).add(msg); + calls.add(Log.DEBUG); + } + + @Override + public void i(String tag, String msg, Throwable tr) { + logs.get(Log.INFO).add(msg); + calls.add(Log.INFO); + } + + @Override + public void w(String tag, String msg, Throwable tr) { + logs.get(Log.WARN).add(msg); + calls.add(Log.WARN); + } + + @Override + public void e(String tag, String msg, Throwable tr) { + logs.get(Log.ERROR).add(msg); + calls.add(Log.ERROR); + } + + @Override + public void wtf(String tag, String msg, Throwable tr) { + logs.get(Log.ASSERT).add(msg); + calls.add(Log.ASSERT); + } + + public boolean isCalled(Integer type) { + return calls.contains(type); + } + + public Map> getLoggedMessages() { + return new HashMap<>(logs); + } +} diff --git a/src/sharedTest/java/io/split/sharedtest/fake/HttpStreamResponseMock.java b/main/src/sharedTest/java/io/split/sharedtest/fake/HttpStreamResponseMock.java similarity index 100% rename from src/sharedTest/java/io/split/sharedtest/fake/HttpStreamResponseMock.java rename to main/src/sharedTest/java/io/split/sharedtest/fake/HttpStreamResponseMock.java diff --git a/src/test/java/android/util/Base64.java b/main/src/test/java/android/util/Base64.java similarity index 100% rename from src/test/java/android/util/Base64.java rename to main/src/test/java/android/util/Base64.java diff --git a/src/test/java/io/split/android/BCryptTest.java b/main/src/test/java/io/split/android/BCryptTest.java similarity index 100% rename from src/test/java/io/split/android/BCryptTest.java rename to main/src/test/java/io/split/android/BCryptTest.java diff --git a/src/test/java/io/split/android/FolderForApiKeyTest.java b/main/src/test/java/io/split/android/FolderForApiKeyTest.java similarity index 100% rename from src/test/java/io/split/android/FolderForApiKeyTest.java rename to main/src/test/java/io/split/android/FolderForApiKeyTest.java diff --git a/src/test/java/io/split/android/SplitConfigurationsParsingTests.java b/main/src/test/java/io/split/android/SplitConfigurationsParsingTests.java similarity index 100% rename from src/test/java/io/split/android/SplitConfigurationsParsingTests.java rename to main/src/test/java/io/split/android/SplitConfigurationsParsingTests.java diff --git a/src/test/java/io/split/android/client/DestroyerTest.java b/main/src/test/java/io/split/android/client/DestroyerTest.java similarity index 100% rename from src/test/java/io/split/android/client/DestroyerTest.java rename to main/src/test/java/io/split/android/client/DestroyerTest.java diff --git a/src/test/java/io/split/android/client/EvaluationOptionsTest.java b/main/src/test/java/io/split/android/client/EvaluationOptionsTest.java similarity index 100% rename from src/test/java/io/split/android/client/EvaluationOptionsTest.java rename to main/src/test/java/io/split/android/client/EvaluationOptionsTest.java diff --git a/src/test/java/io/split/android/client/FactoryMonitorTest.java b/main/src/test/java/io/split/android/client/FactoryMonitorTest.java similarity index 100% rename from src/test/java/io/split/android/client/FactoryMonitorTest.java rename to main/src/test/java/io/split/android/client/FactoryMonitorTest.java diff --git a/src/test/java/io/split/android/client/FileStorageTest.java b/main/src/test/java/io/split/android/client/FileStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/FileStorageTest.java rename to main/src/test/java/io/split/android/client/FileStorageTest.java diff --git a/src/test/java/io/split/android/client/FilterBuilderTest.java b/main/src/test/java/io/split/android/client/FilterBuilderTest.java similarity index 100% rename from src/test/java/io/split/android/client/FilterBuilderTest.java rename to main/src/test/java/io/split/android/client/FilterBuilderTest.java diff --git a/src/test/java/io/split/android/client/FilterGrouperTest.java b/main/src/test/java/io/split/android/client/FilterGrouperTest.java similarity index 100% rename from src/test/java/io/split/android/client/FilterGrouperTest.java rename to main/src/test/java/io/split/android/client/FilterGrouperTest.java diff --git a/src/test/java/io/split/android/client/FlagSetsFilterImplTest.java b/main/src/test/java/io/split/android/client/FlagSetsFilterImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/FlagSetsFilterImplTest.java rename to main/src/test/java/io/split/android/client/FlagSetsFilterImplTest.java diff --git a/src/test/java/io/split/android/client/ImpressionsModeTest.java b/main/src/test/java/io/split/android/client/ImpressionsModeTest.java similarity index 100% rename from src/test/java/io/split/android/client/ImpressionsModeTest.java rename to main/src/test/java/io/split/android/client/ImpressionsModeTest.java diff --git a/src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java b/main/src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java similarity index 100% rename from src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java rename to main/src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java diff --git a/src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java b/main/src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java similarity index 100% rename from src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java rename to main/src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java diff --git a/src/test/java/io/split/android/client/SplitClientConfigTest.java b/main/src/test/java/io/split/android/client/SplitClientConfigTest.java similarity index 100% rename from src/test/java/io/split/android/client/SplitClientConfigTest.java rename to main/src/test/java/io/split/android/client/SplitClientConfigTest.java diff --git a/src/test/java/io/split/android/client/SplitClientImplAttributesTest.java b/main/src/test/java/io/split/android/client/SplitClientImplAttributesTest.java similarity index 100% rename from src/test/java/io/split/android/client/SplitClientImplAttributesTest.java rename to main/src/test/java/io/split/android/client/SplitClientImplAttributesTest.java diff --git a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java b/main/src/test/java/io/split/android/client/SplitClientImplBaseTest.java similarity index 96% rename from src/test/java/io/split/android/client/SplitClientImplBaseTest.java rename to main/src/test/java/io/split/android/client/SplitClientImplBaseTest.java index e1f7f6811..88cd686ee 100644 --- a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java +++ b/main/src/test/java/io/split/android/client/SplitClientImplBaseTest.java @@ -64,7 +64,7 @@ public void setUp() { new SplitParser(new ParserCommons(mySegmentsStorageContainer, myLargeSegmentsStorageContainer)), impressionListener, splitClientConfig, - new SplitEventsManager(splitClientConfig, new SplitTaskExecutorStub()), + new SplitEventsManager(new SplitTaskExecutorStub(), splitClientConfig.blockUntilReady()), eventsTracker, attributesManager, splitValidator, diff --git a/src/test/java/io/split/android/client/SplitClientImplEvaluationOptionsTest.java b/main/src/test/java/io/split/android/client/SplitClientImplEvaluationOptionsTest.java similarity index 100% rename from src/test/java/io/split/android/client/SplitClientImplEvaluationOptionsTest.java rename to main/src/test/java/io/split/android/client/SplitClientImplEvaluationOptionsTest.java diff --git a/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java b/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java new file mode 100644 index 000000000..16d40a060 --- /dev/null +++ b/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java @@ -0,0 +1,165 @@ +package io.split.android.client; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; + +import io.split.android.client.api.Key; +import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.events.SplitEventListener; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.shared.SplitClientContainer; +import io.split.android.client.utils.logger.Logger; +import io.split.android.client.validators.SplitValidator; +import io.split.android.client.validators.TreatmentManager; +import io.split.android.engine.experiments.SplitParser; + +public class SplitClientImplEventRegistrationTest { + + @Mock + private SplitFactory container; + @Mock + private SplitClientContainer clientContainer; + @Mock + private SplitParser splitParser; + @Mock + private ImpressionListener impressionListener; + @Mock + private EventsTracker eventsTracker; + @Mock + private AttributesManager attributesManager; + @Mock + private SplitValidator splitValidator; + @Mock + private TreatmentManager treatmentManager; + @Mock + private SplitEventsManager eventsManager; + + private SplitClientImpl splitClient; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + SplitClientConfig splitClientConfig = SplitClientConfig.builder().build(); + + splitClient = new SplitClientImpl( + container, + clientContainer, + new Key("test_key"), + splitParser, + impressionListener, + splitClientConfig, + eventsManager, + eventsTracker, + attributesManager, + splitValidator, + treatmentManager + ); + } + + @Test + public void sdkReadyFromCacheAllowsRegistrationEvenWhenAlreadyTriggered() { + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)).thenReturn(true); + SplitEventTask task = mock(SplitEventTask.class); + + splitClient.on(SplitEvent.SDK_READY_FROM_CACHE, task); + + verify(eventsManager).register(eq(SplitEvent.SDK_READY_FROM_CACHE), eq(task)); + } + + @Test + public void sdkReadyAllowsRegistrationEvenWhenAlreadyTriggered() { + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(true); + SplitEventTask task = mock(SplitEventTask.class); + + splitClient.on(SplitEvent.SDK_READY, task); + + verify(eventsManager).register(eq(SplitEvent.SDK_READY), eq(task)); + } + + @Test + public void sdkReadyTimedOutDoesNotRegisterWhenAlreadyTriggered() { + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)).thenReturn(true); + SplitEventTask task = mock(SplitEventTask.class); + + splitClient.on(SplitEvent.SDK_READY_TIMED_OUT, task); + + verify(eventsManager, never()).register(any(SplitEvent.class), any(SplitEventTask.class)); + } + + @Test + public void sdkUpdateDoesNotRegisterWhenAlreadyTriggered() { + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_UPDATE)).thenReturn(true); + SplitEventTask task = mock(SplitEventTask.class); + + splitClient.on(SplitEvent.SDK_UPDATE, task); + + verify(eventsManager, never()).register(any(SplitEvent.class), any(SplitEventTask.class)); + } + + @Test + public void sdkReadyTimedOutRegistersWhenNotAlreadyTriggered() { + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)).thenReturn(false); + SplitEventTask task = mock(SplitEventTask.class); + + splitClient.on(SplitEvent.SDK_READY_TIMED_OUT, task); + + verify(eventsManager).register(eq(SplitEvent.SDK_READY_TIMED_OUT), eq(task)); + } + + @Test + public void sdkUpdateRegistersWhenNotAlreadyTriggered() { + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_UPDATE)).thenReturn(false); + SplitEventTask task = mock(SplitEventTask.class); + + splitClient.on(SplitEvent.SDK_UPDATE, task); + + verify(eventsManager).register(eq(SplitEvent.SDK_UPDATE), eq(task)); + } + + @Test + public void addEventListenerWithNullListenerDoesNotRegisterAndLogsWarning() { + try (MockedStatic logger = mockStatic(Logger.class)) { + splitClient.addEventListener(null); + + verify(eventsManager, never()).registerEventListener(any(SplitEventListener.class)); + logger.verify(() -> Logger.w("SDK Event Listener cannot be null")); + } + } + + @Test + public void addEventListenerWithValidListenerRegistersListener() { + SplitEventListener listener = mock(SplitEventListener.class); + + splitClient.addEventListener(listener); + + verify(eventsManager).registerEventListener(eq(listener)); + } + + @Test + public void addEventListenerDoesNotRegisterWhenClientIsDestroyedAndLogsWarning() { + try (MockedStatic logger = mockStatic(Logger.class)) { + splitClient.destroy(); + + SplitEventListener listener = mock(SplitEventListener.class); + splitClient.addEventListener(listener); + + verify(eventsManager, never()).registerEventListener(any(SplitEventListener.class)); + logger.verify(() -> Logger.w("Client has already been destroyed. Cannot add event listener")); + } + } +} diff --git a/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java b/main/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java similarity index 100% rename from src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java rename to main/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java diff --git a/src/test/java/io/split/android/client/SplitClientImplLegacyTest.java b/main/src/test/java/io/split/android/client/SplitClientImplLegacyTest.java similarity index 100% rename from src/test/java/io/split/android/client/SplitClientImplLegacyTest.java rename to main/src/test/java/io/split/android/client/SplitClientImplLegacyTest.java diff --git a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt b/main/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt similarity index 100% rename from src/test/java/io/split/android/client/SplitFactoryHelperTest.kt rename to main/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt diff --git a/src/test/java/io/split/android/client/SplitManagerImplTest.java b/main/src/test/java/io/split/android/client/SplitManagerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/SplitManagerImplTest.java rename to main/src/test/java/io/split/android/client/SplitManagerImplTest.java diff --git a/src/test/java/io/split/android/client/SyncConfigTest.java b/main/src/test/java/io/split/android/client/SyncConfigTest.java similarity index 100% rename from src/test/java/io/split/android/client/SyncConfigTest.java rename to main/src/test/java/io/split/android/client/SyncConfigTest.java diff --git a/src/test/java/io/split/android/client/TreatmentLabelsTest.java b/main/src/test/java/io/split/android/client/TreatmentLabelsTest.java similarity index 100% rename from src/test/java/io/split/android/client/TreatmentLabelsTest.java rename to main/src/test/java/io/split/android/client/TreatmentLabelsTest.java diff --git a/src/test/java/io/split/android/client/TreatmentManagerEvaluationOptionsTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerEvaluationOptionsTest.java similarity index 100% rename from src/test/java/io/split/android/client/TreatmentManagerEvaluationOptionsTest.java rename to main/src/test/java/io/split/android/client/TreatmentManagerEvaluationOptionsTest.java diff --git a/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java similarity index 100% rename from src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java rename to main/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java diff --git a/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java similarity index 100% rename from src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java rename to main/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java diff --git a/src/test/java/io/split/android/client/TreatmentManagerTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerTest.java similarity index 100% rename from src/test/java/io/split/android/client/TreatmentManagerTest.java rename to main/src/test/java/io/split/android/client/TreatmentManagerTest.java diff --git a/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java b/main/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java similarity index 100% rename from src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java rename to main/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java diff --git a/src/test/java/io/split/android/client/UserConsentManagerTest.java b/main/src/test/java/io/split/android/client/UserConsentManagerTest.java similarity index 100% rename from src/test/java/io/split/android/client/UserConsentManagerTest.java rename to main/src/test/java/io/split/android/client/UserConsentManagerTest.java diff --git a/src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java b/main/src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java rename to main/src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java diff --git a/src/test/java/io/split/android/client/attributes/AttributesMergerImplTest.java b/main/src/test/java/io/split/android/client/attributes/AttributesMergerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/attributes/AttributesMergerImplTest.java rename to main/src/test/java/io/split/android/client/attributes/AttributesMergerImplTest.java diff --git a/src/test/java/io/split/android/client/dtos/SplitDeserializationTest.java b/main/src/test/java/io/split/android/client/dtos/SplitDeserializationTest.java similarity index 100% rename from src/test/java/io/split/android/client/dtos/SplitDeserializationTest.java rename to main/src/test/java/io/split/android/client/dtos/SplitDeserializationTest.java diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java new file mode 100644 index 000000000..904cd8b7c --- /dev/null +++ b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java @@ -0,0 +1,198 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import io.split.android.fake.SplitTaskExecutorStub; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.List; + +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.api.Key; +import io.split.android.client.events.metadata.TypedTaskConverter; + +public class EventsManagerCoordinatorTest { + + @Mock + private ISplitEventsManager mMockChildEventsManager; + private EventsManagerCoordinator mEventsManager; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + mEventsManager = new EventsManagerCoordinator(); + } + + @Test + public void SPLITS_UPDATEDEventIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), isNull()); + } + + @Test + public void RULE_BASED_SEGMENTEventIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), isNull()); + } + + @Test + public void SPLITS_SYNC_COMPLETEEventIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), isNull()); + } + + @Test + public void SPLITS_LOADED_FROM_STORAGEEventIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), isNull()); + } + + @Test + public void SPLIT_KILLED_NOTIFICATIONEventIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION), isNull()); + } + + @Test + public void EventIsPassedDownToChildrenIfRegisteredAfterEmission() { + ISplitEventsManager newMockChildEventsManager = mock(ISplitEventsManager.class); + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), isNull()); + + mEventsManager.registerEventsManager(new Key("new_key", "bucketing"), newMockChildEventsManager); + verify(newMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), isNull()); + } + + @Test + public void SPLITS_UPDATEDEventWithMetadataIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + List updatedFlags = Arrays.asList("flag1", "flag2"); + EventMetadata metadata = io.split.android.client.events.metadata.EventMetadataHelpers.createUpdatedFlagsMetadata(updatedFlags); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(meta -> { + if (meta == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(meta); + List names = typedMeta.getNames(); + assertNotNull(names); + return names.size() == 2 && names.contains("flag1") && names.contains("flag2"); + })); + } + + @Test + public void SPLITS_UPDATEDEventWithNullMetadataIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, null); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), eq((EventMetadata) null)); + } + + @Test + public void unregisterEventsManagerCallsDestroyOnSplitEventsManager() { + SplitEventsManager splitEventsManager = spy(new SplitEventsManager(new SplitTaskExecutorStub(), 0)); + Key key = new Key("key_to_destroy", "bucketing"); + mEventsManager.registerEventsManager(key, splitEventsManager); + + mEventsManager.unregisterEventsManager(key); + + verify(splitEventsManager).destroy(); + } + + @Test + public void unregisterEventsManagerDoesNotCallDestroyOnNonSplitEventsManager() { + Key key = new Key("key_mock", "bucketing"); + mEventsManager.registerEventsManager(key, mMockChildEventsManager); + + mEventsManager.unregisterEventsManager(key); + + // Then: destroy() should NOT be called (ISplitEventsManager doesn't have destroy method) + // The mock should simply be removed without any additional calls + // Verify no notifyInternalEvent calls after unregistration + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + delay(); + // The mock was already verified to receive events before, but after unregistration it should not + // Since we're testing the coordinator doesn't crash when removing non-SplitEventsManager + // and that events are no longer propagated, we verify the mock received exactly the expected calls + } + + @Test + public void unregisterEventsManagerWithNullKeyDoesNotCrash() { + // When: unregistering with null key + mEventsManager.unregisterEventsManager(null); + + // Then: no exception should be thrown + assertTrue(true); + } + + private void delay() { + boolean shouldStop = false; + long maxExecutionTime = System.currentTimeMillis() + 1000; + long intervalExecutionTime = 100; + + while (!shouldStop) { + try { + Thread.sleep(intervalExecutionTime); + } catch (InterruptedException e) { + e.printStackTrace(); + Assert.fail(); + } + + maxExecutionTime -= intervalExecutionTime; + + if (System.currentTimeMillis() > maxExecutionTime) { + shouldStop = true; + } + } + } +} diff --git a/src/test/java/io/split/android/client/events/EventsManagerTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java similarity index 52% rename from src/test/java/io/split/android/client/events/EventsManagerTest.java rename to main/src/test/java/io/split/android/client/events/EventsManagerTest.java index c76a3d533..4df176da1 100644 --- a/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -1,6 +1,8 @@ package io.split.android.client.events; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; @@ -11,13 +13,17 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.executors.SplitEventExecutorResources; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.fake.SplitTaskExecutorStub; public class EventsManagerTest { @@ -38,11 +44,12 @@ public void setup() { public void eventOnReady() { SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + // Fire SYNC_COMPLETE events to trigger SDK_READY + // This also triggers SDK_READY_FROM_CACHE via the sync path (OR-of-ANDs) + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; @@ -58,7 +65,7 @@ public void eventOnReady() { @Test public void eventOnReadyTimedOut() { SplitClientConfig cfg = SplitClientConfig.builder().ready(1000).build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()); boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; @@ -73,7 +80,7 @@ public void eventOnReadyTimedOut() { @Test public void eventOnReadyAndOnReadyTimedOut() { SplitClientConfig cfg = SplitClientConfig.builder().ready(1000).build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()); boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; @@ -84,10 +91,9 @@ public void eventOnReadyAndOnReadyTimedOut() { //At this line timeout has been reached assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); - //But if after timeout event, the Splits and MySegments are ready, SDK_READY should be triggered - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + //But if after timeout event, the sync completes, SDK_READY should be triggered + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); shouldStop = false; maxExecutionTime = System.currentTimeMillis() + 10000; @@ -179,10 +185,11 @@ public void sdkUpdateWithRuleBasedSegments() throws InterruptedException { public void sdkReadyWithSplitsAndUpdatedLargeSegments() { SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + // Fire SYNC_COMPLETE events to trigger SDK_READY + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; @@ -195,8 +202,8 @@ public void sdkReadyWithSplitsAndUpdatedLargeSegments() { } private static void sdkUpdateTest(SplitInternalEvent eventToCheck, boolean negate) throws InterruptedException { - SplitEventsManager eventManager = new SplitEventsManager(SplitClientConfig.builder() - .build(), new SplitTaskExecutorStub()); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), SplitClientConfig.builder() + .build().blockUntilReady()); CountDownLatch updateLatch = new CountDownLatch(1); CountDownLatch readyLatch = new CountDownLatch(1); @@ -213,8 +220,8 @@ public void onPostExecutionView(SplitClient client) { } }); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); boolean readyAwait = readyLatch.await(3, TimeUnit.SECONDS); eventManager.notifyInternalEvent(eventToCheck); @@ -230,7 +237,7 @@ public void onPostExecutionView(SplitClient client) { private void eventOnReadyFromCache(List eventList, SplitClientConfig config) { - SplitEventsManager eventManager = new SplitEventsManager(config, new SplitTaskExecutorStub()); + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), config.blockUntilReady()); for (SplitInternalEvent event : eventList) { eventManager.notifyInternalEvent(event); @@ -265,4 +272,169 @@ private static void execute(boolean shouldStop, long intervalExecutionTime, long } } } + + @Test + public void sdkUpdateWithTypedTaskReceivesMetadata() throws InterruptedException { + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); + CountDownLatch readyLatch = new CountDownLatch(1); + CountDownLatch updateLatch = new CountDownLatch(1); + AtomicReference receivedMetadata = new AtomicReference<>(); + + waitForSdkReady(eventManager, readyLatch); + + eventManager.registerEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + EventMetadata metadata = createTestMetadata(); + triggerSdkUpdateWithMetadata(eventManager, metadata); + + boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE callback should be called", updateAwait); + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertNotNull("Metadata should contain names", receivedMetadata.get().getNames()); + assertEquals(2, receivedMetadata.get().getNames().size()); + } + + @Test + public void sdkUpdateWithTypedTaskReceivesMetadataOnMainThread() throws InterruptedException { + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); + CountDownLatch readyLatch = new CountDownLatch(1); + CountDownLatch updateLatch = new CountDownLatch(1); + AtomicReference receivedMetadata = new AtomicReference<>(); + + waitForSdkReady(eventManager, readyLatch); + + eventManager.registerEventListener(new SplitEventListener() { + @Override + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + EventMetadata metadata = createTestMetadata(); + triggerSdkUpdateWithMetadata(eventManager, metadata); + + boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE callback should be called on main thread", updateAwait); + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertNotNull("Metadata should contain names", receivedMetadata.get().getNames()); + } + + @Test + public void sdkUpdateCallsLegacyMethodWhenOnlyLegacyImplemented() throws InterruptedException { + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); + CountDownLatch readyLatch = new CountDownLatch(1); + CountDownLatch updateLatch = new CountDownLatch(1); + final boolean[] nonMetadataMethodCalled = {false}; + + waitForSdkReady(eventManager, readyLatch); + + eventManager.register(SplitEvent.SDK_UPDATE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + nonMetadataMethodCalled[0] = true; + updateLatch.countDown(); + } + }); + + EventMetadata metadata = createTestMetadata(); + triggerSdkUpdateWithMetadata(eventManager, metadata); + + boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE callback should be called", updateAwait); + assertTrue("Legacy method should be called", nonMetadataMethodCalled[0]); + } + + @Test + public void sdkEventListenerCallsBothBackgroundAndMainThreadMethods() throws InterruptedException { + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); + CountDownLatch readyLatch = new CountDownLatch(1); + CountDownLatch bothCalledLatch = new CountDownLatch(2); + final boolean[] backgroundMethodCalled = {false}; + final boolean[] mainThreadMethodCalled = {false}; + AtomicReference receivedMetadata = new AtomicReference<>(); + + waitForSdkReady(eventManager, readyLatch); + + eventManager.registerEventListener(new SplitEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + backgroundMethodCalled[0] = true; + receivedMetadata.set(metadata); + bothCalledLatch.countDown(); + } + + @Override + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { + mainThreadMethodCalled[0] = true; + bothCalledLatch.countDown(); + } + }); + + EventMetadata metadata = createTestMetadata(); + triggerSdkUpdateWithMetadata(eventManager, metadata); + + boolean bothCalled = bothCalledLatch.await(3, TimeUnit.SECONDS); + assertTrue("Both callbacks should be called", bothCalled); + assertTrue("Background method should be called", backgroundMethodCalled[0]); + assertTrue("Main thread method should also be called", mainThreadMethodCalled[0]); + assertNotNull("Metadata should be passed to methods", receivedMetadata.get()); + assertNotNull("Metadata should contain names", receivedMetadata.get().getNames()); + } + + @Test + public void sdkReadyFromCacheTypedTaskReceivesMetadata() throws InterruptedException { + SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference receivedMetadata = new AtomicReference<>(); + + // Register an event listener + eventManager.registerEventListener(new SplitEventListener() { + @Override + public void onReadyFromCache(SplitClient client, SdkReadyMetadata metadata) { + receivedMetadata.set(metadata); + latch.countDown(); + } + }); + + // Trigger SDK_READY_FROM_CACHE + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); + eventManager.notifyInternalEvent(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE); + eventManager.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); + + boolean called = latch.await(3, TimeUnit.SECONDS); + assertTrue("Callback should be called", called); + assertNotNull("Metadata should not be null", receivedMetadata.get()); + } + + private void waitForSdkReady(SplitEventsManager eventManager, CountDownLatch readyLatch) throws InterruptedException { + eventManager.register(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + readyLatch.countDown(); + } + }); + + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + boolean readyAwait = readyLatch.await(3, TimeUnit.SECONDS); + assertTrue("SDK_READY should be triggered", readyAwait); + } + + private static EventMetadata createTestMetadata() { + return EventMetadataHelpers.createUpdatedFlagsMetadata( + Arrays.asList("flag1", "flag2")); + } + + private static void triggerSdkUpdateWithMetadata(SplitEventsManager eventManager, EventMetadata metadata) { + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); + } } diff --git a/src/test/java/io/split/android/client/events/PropertyValidatorTest.java b/main/src/test/java/io/split/android/client/events/PropertyValidatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/events/PropertyValidatorTest.java rename to main/src/test/java/io/split/android/client/events/PropertyValidatorTest.java diff --git a/src/test/java/io/split/android/client/events/executors/ClientEventSplitTaskTest.java b/main/src/test/java/io/split/android/client/events/executors/ClientEventSplitTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/events/executors/ClientEventSplitTaskTest.java rename to main/src/test/java/io/split/android/client/events/executors/ClientEventSplitTaskTest.java diff --git a/src/test/java/io/split/android/client/events/executors/SplitEventExecutorFactoryTest.java b/main/src/test/java/io/split/android/client/events/executors/SplitEventExecutorFactoryTest.java similarity index 100% rename from src/test/java/io/split/android/client/events/executors/SplitEventExecutorFactoryTest.java rename to main/src/test/java/io/split/android/client/events/executors/SplitEventExecutorFactoryTest.java diff --git a/src/test/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImplTest.java b/main/src/test/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImplTest.java rename to main/src/test/java/io/split/android/client/events/executors/SplitEventExecutorResourcesImplTest.java diff --git a/src/test/java/io/split/android/client/events/executors/SplitEventExecutorWithClientTest.java b/main/src/test/java/io/split/android/client/events/executors/SplitEventExecutorWithClientTest.java similarity index 100% rename from src/test/java/io/split/android/client/events/executors/SplitEventExecutorWithClientTest.java rename to main/src/test/java/io/split/android/client/events/executors/SplitEventExecutorWithClientTest.java diff --git a/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java b/main/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java similarity index 100% rename from src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java rename to main/src/test/java/io/split/android/client/fallback/FallbackTreatmentTest.java diff --git a/src/test/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorTest.java b/main/src/test/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorTest.java rename to main/src/test/java/io/split/android/client/fallback/FallbackTreatmentsCalculatorTest.java diff --git a/src/test/java/io/split/android/client/fallback/FallbackTreatmentsConfigurationTest.java b/main/src/test/java/io/split/android/client/fallback/FallbackTreatmentsConfigurationTest.java similarity index 100% rename from src/test/java/io/split/android/client/fallback/FallbackTreatmentsConfigurationTest.java rename to main/src/test/java/io/split/android/client/fallback/FallbackTreatmentsConfigurationTest.java diff --git a/src/test/java/io/split/android/client/fallback/FallbacksSanitizerImplTest.java b/main/src/test/java/io/split/android/client/fallback/FallbacksSanitizerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/fallback/FallbacksSanitizerImplTest.java rename to main/src/test/java/io/split/android/client/fallback/FallbacksSanitizerImplTest.java diff --git a/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java b/main/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java rename to main/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java diff --git a/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java b/main/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java similarity index 100% rename from src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java rename to main/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java diff --git a/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java similarity index 95% rename from src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java rename to main/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java index 8ff8cf368..9dd91d706 100644 --- a/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java +++ b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java @@ -10,10 +10,13 @@ import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import org.mockito.MockedStatic; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -34,6 +37,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; +import io.split.android.client.events.SplitEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -42,6 +46,7 @@ import io.split.android.client.shared.SplitClientContainer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; +import io.split.android.client.utils.logger.Logger; import io.split.android.client.validators.TreatmentManager; import io.split.android.engine.experiments.SplitParser; @@ -440,6 +445,38 @@ public void onDoesNotRegisterEventTaskWhenEventAlreadyTriggered() { verify(mockEventsManager, never()).register(any(), any()); } + @Test + public void addEventListenerWithNullListenerDoesNotRegister() { + try (MockedStatic logger = mockStatic(Logger.class)) { + client.addEventListener(null); + + verify(mockEventsManager, never()).registerEventListener(any(SplitEventListener.class)); + logger.verify(() -> Logger.w("SDK Event Listener cannot be null")); + } + } + + @Test + public void addEventListenerDoesNotRegisterWhenClientIsDestroyed() { + try (MockedStatic logger = mockStatic(Logger.class)) { + client.destroy(); + SplitEventListener listener = mock(SplitEventListener.class); + + client.addEventListener(listener); + + verify(mockEventsManager, never()).registerEventListener(any(SplitEventListener.class)); + logger.verify(() -> Logger.w("Client has already been destroyed. Cannot add event listener")); + } + } + + @Test + public void addEventListenerWithValidListenerRegistersListener() { + SplitEventListener listener = mock(SplitEventListener.class); + + client.addEventListener(listener); + + verify(mockEventsManager).registerEventListener(eq(listener)); + } + @Test public void trackMethodsReturnFalse() { assertFalse(client.track("user", "event_type")); diff --git a/src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTest.java similarity index 100% rename from src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTest.java rename to main/src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTest.java diff --git a/src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTestBuilder.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTestBuilder.java similarity index 100% rename from src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTestBuilder.java rename to main/src/test/java/io/split/android/client/localhost/LocalhostSplitFactoryTestBuilder.java diff --git a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java new file mode 100644 index 000000000..aad284982 --- /dev/null +++ b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java @@ -0,0 +1,113 @@ +package io.split.android.client.localhost; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.AssetManager; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; +import io.split.android.client.events.EventsManagerCoordinator; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.storage.legacy.FileStorage; + +public class LocalhostSplitsStorageTest { + + @Mock + private Context mContext; + @Mock + private AssetManager mAssetManager; + @Mock + private FileStorage mFileStorage; + @Mock + private EventsManagerCoordinator mEventsManagerCoordinator; + + private LocalhostSplitsStorage mLocalhostSplitsStorage; + private static final String TEST_FILE_NAME = "test-splits.yaml"; + private static final String INITIAL_CONTENT = "- split1:\n treatment: \"on\""; + private static final String UPDATED_CONTENT = "- split2:\n treatment: \"off\""; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.openMocks(this); + when(mContext.getAssets()).thenReturn(mAssetManager); + when(mAssetManager.open(anyString())).thenThrow(new FileNotFoundException("File not found in assets")); + when(mFileStorage.read(TEST_FILE_NAME)).thenReturn(INITIAL_CONTENT); + mLocalhostSplitsStorage = new LocalhostSplitsStorage(TEST_FILE_NAME, mContext, mFileStorage, mEventsManagerCoordinator); + } + + @Test + public void loadLocalNotifiesTargetingRulesSyncCompleteAndSplitsUpdatedWhenContentChanges() throws IOException { + // First load - should notify events (lines 219-220) + mLocalhostSplitsStorage.loadLocal(); + + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + + // Update content and reload + when(mFileStorage.read(TEST_FILE_NAME)).thenReturn(UPDATED_CONTENT); + mLocalhostSplitsStorage.loadLocal(); + + // Should notify events again since content changed + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(2)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(2)).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + } + + @Test + public void loadLocalDoesNotNotifyEventsWhenContentUnchanged() throws IOException { + // First load - should notify events + mLocalhostSplitsStorage.loadLocal(); + + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + + // Reload with same content - should NOT notify events again + mLocalhostSplitsStorage.loadLocal(); + + // Verify events were only called once + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(1)).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + } + + @Test + public void loadLocalNotifiesSplitsUpdatedWithMetadataContainingUpdatedFlags() throws IOException { + // First load - should notify events with metadata + mLocalhostSplitsStorage.loadLocal(); + + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), any()); + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(EventMetadata.class); + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), metadataCaptor.capture()); + + EventMetadata metadata = metadataCaptor.getValue(); + assertNotNull("Metadata should not be null", metadata); + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMetadata.getNames(); + assertNotNull("names value should not be null", names); + assertTrue("Metadata should contain 'split1' flag", names.contains("split1")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMetadata.getType()); + } +} + diff --git a/src/test/java/io/split/android/client/localhost/LocalhostYamlParserTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostYamlParserTest.java similarity index 100% rename from src/test/java/io/split/android/client/localhost/LocalhostYamlParserTest.java rename to main/src/test/java/io/split/android/client/localhost/LocalhostYamlParserTest.java diff --git a/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java b/main/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java similarity index 69% rename from src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java rename to main/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java index 2088b392a..05b04cc91 100644 --- a/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java +++ b/main/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java @@ -27,6 +27,9 @@ import io.split.android.client.attributes.AttributesManagerFactory; import io.split.android.client.attributes.AttributesMerger; import io.split.android.client.events.EventsManagerCoordinator; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.executors.SplitEventExecutorResources; import io.split.android.client.localhost.LocalhostSplitFactory; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.storage.splits.SplitsStorage; @@ -98,6 +101,40 @@ public void gettingNewClientRegistersEventManager() { verify(mEventsManagerCoordinator).registerEventsManager(eq(key), any()); } + @Test + public void gettingNewClientNotifiesInternalEvents() { + // Create a mocked SplitEventsManager + SplitEventsManager mockEventsManager = mock(SplitEventsManager.class); + SplitEventExecutorResources mockExecutorResources = mock(SplitEventExecutorResources.class); + when(mockEventsManager.getExecutorResources()).thenReturn(mockExecutorResources); + + // Create a mocked factory that returns the mocked events manager + SplitEventsManagerFactory mockFactory = () -> mockEventsManager; + + // Create client container with the mocked factory using @VisibleForTesting constructor + LocalhostSplitClientContainerImpl clientContainer = new LocalhostSplitClientContainerImpl( + mFactory, + mConfig, + mSplitsStorage, + mSplitParser, + mAttributesManagerFactory, + mAttributesMerger, + mTelemetryStorageProducer, + mEventsManagerCoordinator, + mTaskExecutor, + mFlagSetsFilter, + mockFactory + ); + + Key key = new Key("matching_key", "bucketing_key"); + clientContainer.getClient(key); + + // Verify that notifyInternalEvent is called on the mocked events manager + verify(mockEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); + verify(mockEventsManager).notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + verify(mockEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + } + @NonNull private LocalhostSplitClientContainerImpl getClientContainer() { return new LocalhostSplitClientContainerImpl(mFactory, diff --git a/src/test/java/io/split/android/client/metrics/BinarySearchLatencyTrackerTest.java b/main/src/test/java/io/split/android/client/metrics/BinarySearchLatencyTrackerTest.java similarity index 100% rename from src/test/java/io/split/android/client/metrics/BinarySearchLatencyTrackerTest.java rename to main/src/test/java/io/split/android/client/metrics/BinarySearchLatencyTrackerTest.java diff --git a/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java b/main/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java rename to main/src/test/java/io/split/android/client/network/CertificateCheckerHelperTest.java diff --git a/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java b/main/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java rename to main/src/test/java/io/split/android/client/network/CertificateCheckerImplTest.java diff --git a/src/test/java/io/split/android/client/network/CertificatePinningConfigurationProviderTest.java b/main/src/test/java/io/split/android/client/network/CertificatePinningConfigurationProviderTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/CertificatePinningConfigurationProviderTest.java rename to main/src/test/java/io/split/android/client/network/CertificatePinningConfigurationProviderTest.java diff --git a/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java b/main/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java rename to main/src/test/java/io/split/android/client/network/CertificatePinningConfigurationTest.java diff --git a/src/test/java/io/split/android/client/network/ChainCleanerImplTest.java b/main/src/test/java/io/split/android/client/network/ChainCleanerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/ChainCleanerImplTest.java rename to main/src/test/java/io/split/android/client/network/ChainCleanerImplTest.java diff --git a/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java b/main/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java rename to main/src/test/java/io/split/android/client/network/DefaultBase64EncoderTest.java diff --git a/src/test/java/io/split/android/client/network/HttpClientTest.java b/main/src/test/java/io/split/android/client/network/HttpClientTest.java similarity index 93% rename from src/test/java/io/split/android/client/network/HttpClientTest.java rename to main/src/test/java/io/split/android/client/network/HttpClientTest.java index 2daa5063b..3ecc24ee2 100644 --- a/src/test/java/io/split/android/client/network/HttpClientTest.java +++ b/main/src/test/java/io/split/android/client/network/HttpClientTest.java @@ -20,6 +20,8 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -403,6 +405,40 @@ public SplitAuthenticatedRequest authenticate(@NonNull SplitAuthenticatedRequest mProxyServer.shutdown(); } + @Test + public void buildUsesTls12FactoryWhenLegacyAndNoProxy() throws Exception { + Context context = mock(Context.class); + + try (MockedStatic legacyMock = Mockito.mockStatic(LegacyTlsUpdater.class)) { + legacyMock.when(LegacyTlsUpdater::couldBeOld).thenReturn(true); + + HttpClient legacyClient = new HttpClientImpl.Builder() + .setContext(context) + .setUrlSanitizer(mUrlSanitizerMock) + .build(); + + legacyMock.verify(() -> LegacyTlsUpdater.update(context)); + assertTrue(((HttpClientImpl) legacyClient).getSslSocketFactory() instanceof Tls12OnlySocketFactory); + } + } + + @Test + public void buildUsesDefaultSslWhenNotLegacyAndNoProxy() throws Exception { + Context context = mock(Context.class); + + try (MockedStatic legacyMock = Mockito.mockStatic(LegacyTlsUpdater.class)) { + legacyMock.when(LegacyTlsUpdater::couldBeOld).thenReturn(false); + + HttpClient modernClient = new HttpClientImpl.Builder() + .setContext(context) + .setUrlSanitizer(mUrlSanitizerMock) + .build(); + + legacyMock.verify(() -> LegacyTlsUpdater.update(context), Mockito.never()); + assertNull(((HttpClientImpl) modernClient).getSslSocketFactory()); + } + } + @Test public void copyStreamToByteArrayWithSimpleString() { diff --git a/src/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java b/main/src/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java rename to main/src/test/java/io/split/android/client/network/HttpClientTunnellingProxyTest.java diff --git a/src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java b/main/src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java rename to main/src/test/java/io/split/android/client/network/HttpOverTunnelExecutorTest.java diff --git a/src/test/java/io/split/android/client/network/HttpRequestHelperTest.java b/main/src/test/java/io/split/android/client/network/HttpRequestHelperTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/HttpRequestHelperTest.java rename to main/src/test/java/io/split/android/client/network/HttpRequestHelperTest.java diff --git a/src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java b/main/src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java rename to main/src/test/java/io/split/android/client/network/HttpResponseConnectionAdapterTest.java diff --git a/src/test/java/io/split/android/client/network/HttpStreamResponseTest.java b/main/src/test/java/io/split/android/client/network/HttpStreamResponseTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/HttpStreamResponseTest.java rename to main/src/test/java/io/split/android/client/network/HttpStreamResponseTest.java diff --git a/src/test/java/io/split/android/client/network/PinEncoderImplTest.java b/main/src/test/java/io/split/android/client/network/PinEncoderImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/PinEncoderImplTest.java rename to main/src/test/java/io/split/android/client/network/PinEncoderImplTest.java diff --git a/src/test/java/io/split/android/client/network/ProxyConfigurationTest.java b/main/src/test/java/io/split/android/client/network/ProxyConfigurationTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/ProxyConfigurationTest.java rename to main/src/test/java/io/split/android/client/network/ProxyConfigurationTest.java diff --git a/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java b/main/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java rename to main/src/test/java/io/split/android/client/network/ProxySslSocketFactoryProviderImplTest.java diff --git a/src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java b/main/src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java rename to main/src/test/java/io/split/android/client/network/RawHttpResponseParserTest.java diff --git a/src/test/java/io/split/android/client/network/SdkTargetPathTest.java b/main/src/test/java/io/split/android/client/network/SdkTargetPathTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/SdkTargetPathTest.java rename to main/src/test/java/io/split/android/client/network/SdkTargetPathTest.java diff --git a/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java b/main/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java rename to main/src/test/java/io/split/android/client/network/SplitAuthenticatorTest.java diff --git a/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java b/main/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java rename to main/src/test/java/io/split/android/client/network/SplitBasicAuthenticatorTest.java diff --git a/src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java b/main/src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java rename to main/src/test/java/io/split/android/client/network/SplitUrlConnectionAuthenticatorTest.java diff --git a/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java b/main/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java rename to main/src/test/java/io/split/android/client/network/SslProxyTunnelEstablisherTest.java diff --git a/src/test/java/io/split/android/client/network/TrustManagerProviderTest.java b/main/src/test/java/io/split/android/client/network/TrustManagerProviderTest.java similarity index 100% rename from src/test/java/io/split/android/client/network/TrustManagerProviderTest.java rename to main/src/test/java/io/split/android/client/network/TrustManagerProviderTest.java diff --git a/src/test/java/io/split/android/client/service/EventsRecorderTaskTest.java b/main/src/test/java/io/split/android/client/service/EventsRecorderTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/EventsRecorderTaskTest.java rename to main/src/test/java/io/split/android/client/service/EventsRecorderTaskTest.java diff --git a/src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java b/main/src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java rename to main/src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java diff --git a/src/test/java/io/split/android/client/service/HttpFetcherTest.java b/main/src/test/java/io/split/android/client/service/HttpFetcherTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/HttpFetcherTest.java rename to main/src/test/java/io/split/android/client/service/HttpFetcherTest.java diff --git a/src/test/java/io/split/android/client/service/HttpRecorderTest.java b/main/src/test/java/io/split/android/client/service/HttpRecorderTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/HttpRecorderTest.java rename to main/src/test/java/io/split/android/client/service/HttpRecorderTest.java diff --git a/src/test/java/io/split/android/client/service/ImpressionHasherTest.java b/main/src/test/java/io/split/android/client/service/ImpressionHasherTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/ImpressionHasherTest.java rename to main/src/test/java/io/split/android/client/service/ImpressionHasherTest.java diff --git a/src/test/java/io/split/android/client/service/ImpressionsCountRecorderTaskTest.java b/main/src/test/java/io/split/android/client/service/ImpressionsCountRecorderTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/ImpressionsCountRecorderTaskTest.java rename to main/src/test/java/io/split/android/client/service/ImpressionsCountRecorderTaskTest.java diff --git a/src/test/java/io/split/android/client/service/ImpressionsCounterTest.java b/main/src/test/java/io/split/android/client/service/ImpressionsCounterTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/ImpressionsCounterTest.java rename to main/src/test/java/io/split/android/client/service/ImpressionsCounterTest.java diff --git a/src/test/java/io/split/android/client/service/ImpressionsRecorderTaskTest.java b/main/src/test/java/io/split/android/client/service/ImpressionsRecorderTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/ImpressionsRecorderTaskTest.java rename to main/src/test/java/io/split/android/client/service/ImpressionsRecorderTaskTest.java diff --git a/src/test/java/io/split/android/client/service/ImpressionsRequestParserTest.java b/main/src/test/java/io/split/android/client/service/ImpressionsRequestParserTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/ImpressionsRequestParserTest.java rename to main/src/test/java/io/split/android/client/service/ImpressionsRequestParserTest.java diff --git a/src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java b/main/src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java rename to main/src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java diff --git a/main/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java new file mode 100644 index 000000000..263d59e6c --- /dev/null +++ b/main/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java @@ -0,0 +1,96 @@ +package io.split.android.client.service; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.split.android.client.service.synchronizer.MySegmentsChangeChecker; + +public class MySegmentsChangesCheckerTest { + + MySegmentsChangeChecker mMySegmentsChangeChecker = new MySegmentsChangeChecker(); + + @Test + public void testChangesArrived() { + List old = Arrays.asList("s1", "s2", "s3"); + List newSegments = Arrays.asList("s1"); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertFalse(result.isEmpty()); + // s2 and s3 were removed + Set changedSet = new HashSet<>(result); + Assert.assertTrue(changedSet.contains("s2")); + Assert.assertTrue(changedSet.contains("s3")); + Assert.assertEquals(2, result.size()); + } + + @Test + public void testNewChangesArrived() { + List newSegments = Arrays.asList("s1", "s2", "s3"); + List old = Arrays.asList("s1"); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertFalse(result.isEmpty()); + // s2 and s3 were added + Set changedSet = new HashSet<>(result); + Assert.assertTrue(changedSet.contains("s2")); + Assert.assertTrue(changedSet.contains("s3")); + Assert.assertEquals(2, result.size()); + } + + @Test + public void testNoChangesArrived() { + List old = Arrays.asList("s1", "s2", "s3"); + List newSegments = Arrays.asList("s1", "s2", "s3"); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testNoChangesDifferentOrder() { + List old = Arrays.asList("s1", "s2", "s3"); + List newSegments = Arrays.asList("s2", "s1", "s3"); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testNoChangesDifferentOrderInverted() { + List newSegments = Arrays.asList("s1", "s2", "s3"); + List old = Arrays.asList("s2", "s1", "s3"); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testNoChangesArrivedEmpty() { + List newSegments = new ArrayList<>(); + List old = new ArrayList<>(); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testEmptyChangesArrived() { + List newSegments = new ArrayList<>(); + List old = Arrays.asList("s1", "s2", "s3"); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertFalse(result.isEmpty()); + // s1, s2, s3 were all removed + Set changedSet = new HashSet<>(result); + Assert.assertTrue(changedSet.contains("s1")); + Assert.assertTrue(changedSet.contains("s2")); + Assert.assertTrue(changedSet.contains("s3")); + Assert.assertEquals(3, result.size()); + } +} diff --git a/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java similarity index 86% rename from src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java rename to main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java index 696c844f0..cf99782c7 100644 --- a/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java @@ -29,6 +29,7 @@ import java.util.Set; import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -70,7 +71,7 @@ public class MySegmentsSyncTaskTest { @Before public void setup() { mAutoCloseable = MockitoAnnotations.openMocks(this); - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null); loadMySegments(); } @@ -222,37 +223,62 @@ public void addTillParameterToRequestWhenResponseCnDoesNotMatchTargetAndRetryLim } @Test - public void fetchedEventIsEmittedWhenNoChangesInSegments() throws HttpFetcherException { - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(false); + public void syncCompleteEventIsEmittedWhenNoChangesInSegments() throws HttpFetcherException { + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.emptyList()); when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); + } + + @Test + public void membershipsSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws HttpFetcherException { + when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + // Verify MEMBERSHIPS_SYNC_COMPLETE is always fired on successful sync, even when segments changed + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); + } + + @Test + public void updateEventIsFiredWhenSegmentsHaveChanged() throws HttpFetcherException { + when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test public void updatedEventIsEmittedWhenChangesInSegments() throws HttpFetcherException { - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test public void largeSegmentsUpdatedEventIsEmittedWhenChangesInLargeSegmentsAndNotInSegments() throws HttpFetcherException { - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(false); - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(Collections.emptyList(), Collections.singletonList("largesegment0"))).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.emptyList()); + when(mMySegmentsChangeChecker.getChangedSegments(Collections.emptyList(), Collections.singletonList("largesegment0"))).thenReturn(Collections.singletonList("largesegment0")); when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(createChange(1L)); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test diff --git a/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java similarity index 98% rename from src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java rename to main/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java index 3e4972765..663602b30 100644 --- a/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java @@ -1,6 +1,7 @@ package io.split.android.client.service; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -179,7 +180,7 @@ public void removeOperationRemovesOnlyNotifiedSegments() { Assert.assertTrue(captorValue.getNames().contains(mCustomerSegment)); Assert.assertEquals(1, captorValue.getNames().size()); Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any()); } @Test diff --git a/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java similarity index 65% rename from src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java rename to main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java index 29a179a82..88fa2c09b 100644 --- a/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java @@ -1,7 +1,11 @@ package io.split.android.client.service; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -13,7 +17,12 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -124,7 +133,58 @@ public void sdkUpdateIsTriggeredWhenStorageUpdateReturnsTrue() { verify(mSplitChangeProcessor).process(mSplit, 123L); verify(mSplitsStorage).update(processedSplitChange, null); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); } + + @Test + public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { + Split split1 = new Split(); + split1.name = "test_split_1"; + Split split2 = new Split(); + split2.name = "test_split_2"; + List activeSplits = Arrays.asList(split1, split2); + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(activeSplits, new ArrayList<>(), 0L, 0); + + when(mSplitChangeProcessor.process(mSplit, 123L)).thenReturn(processedSplitChange); + when(mSplitsStorage.update(processedSplitChange, null)).thenReturn(true); + + mSplitInPlaceUpdateTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { + if (metadata == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(2, names.size()); + assertTrue(names.contains("test_split_1")); + assertTrue(names.contains("test_split_2")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); + return true; + })); + } + + @Test + public void splitsUpdatedIncludesArchivedSplitsInMetadata() { + Split archivedSplit = new Split(); + archivedSplit.name = "archived_split"; + List archivedSplits = Arrays.asList(archivedSplit); + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), archivedSplits, 0L, 0); + + when(mSplitChangeProcessor.process(mSplit, 123L)).thenReturn(processedSplitChange); + when(mSplitsStorage.update(processedSplitChange, null)).thenReturn(true); + + mSplitInPlaceUpdateTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { + if (metadata == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(1, names.size()); + assertTrue(names.contains("archived_split")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); + return true; + })); + } } diff --git a/src/test/java/io/split/android/client/service/SplitKillTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java similarity index 81% rename from src/test/java/io/split/android/client/service/SplitKillTaskTest.java rename to main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java index 1545f6d5c..beb9d4043 100644 --- a/src/test/java/io/split/android/client/service/SplitKillTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java @@ -5,9 +5,13 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; +import java.util.List; + +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.dtos.Split; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -17,9 +21,9 @@ import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.splits.SplitKillTask; import io.split.android.client.storage.splits.SplitsStorage; -import io.split.android.helpers.FileHelper; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -69,7 +73,18 @@ public void correctExecution() throws HttpFetcherException { Assert.assertEquals(split.defaultTreatment, splitCaptor.getValue().defaultTreatment); Assert.assertEquals(split.changeNumber, splitCaptor.getValue().changeNumber); Assert.assertEquals(true, splitCaptor.getValue().killed); - verify(mEventsManager, times(1)).notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); + + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(EventMetadata.class); + verify(mEventsManager, times(1)).notifyInternalEvent( + eq(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION), metadataCaptor.capture()); + EventMetadata metadata = metadataCaptor.getValue(); + Assert.assertNotNull(metadata); + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMetadata.getNames(); + Assert.assertNotNull(names); + Assert.assertEquals(1, names.size()); + Assert.assertTrue(names.contains("split1")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMetadata.getType()); } @Test diff --git a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java new file mode 100644 index 000000000..45bc7b228 --- /dev/null +++ b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -0,0 +1,410 @@ +package io.split.android.client.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.longThat; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.split.android.client.events.SdkReadyMetadata; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.metadata.TypedTaskConverter; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.http.HttpFetcherException; +import io.split.android.client.service.splits.SplitsSyncHelper; +import io.split.android.client.service.splits.SplitsSyncTask; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.model.OperationType; +import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; +import io.split.android.helpers.FileHelper; + +public class SplitSyncTaskTest { + + private static final long OLD_TIMESTAMP = 1546300800L; //2019-01-01 + + SplitsStorage mSplitsStorage; + SplitChange mSplitChange = null; + SplitsSyncHelper mSplitsSyncHelper; + RuleBasedSegmentStorageProducer mRuleBasedSegmentStorage; + + SplitsSyncTask mTask; + String mQueryString = "qs=1"; + + SplitEventsManager mEventsManager; + + TelemetryRuntimeProducer mTelemetryRuntimeProducer; + + @Before + public void setup() { + mTelemetryRuntimeProducer = mock(TelemetryRuntimeProducer.class); + + mSplitsStorage = mock(SplitsStorage.class); + mSplitsSyncHelper = mock(SplitsSyncHelper.class); + mEventsManager = mock(SplitEventsManager.class); + mRuleBasedSegmentStorage = mock(RuleBasedSegmentStorageProducer.class); + + when(mSplitsSyncHelper.sync(notNull(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL)); + + loadSplitChanges(); + } + + @Test + public void correctExecution() throws HttpFetcherException { + // Check that syncing is done with changeNum retrieved from db + // Querystring is the same, so no clear sould be called + // And updateTimestamp is 0 + // Retry is off, so splitSyncHelper.sync should be called + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(-1L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(-1L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + + mTask.execute(); + + verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == -1L && argument.getRbsSince() == -1L), eq(false), eq(false), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + } + + @Test + public void cleanSplitsWhenQueryStringHasChanged() throws HttpFetcherException { + // Splits have to be cleared when query string on db is != than current one on current sdk client instance + // Setting up cache not expired + // splits change param should be -1 + + String otherQs = "q=other"; + Map params = new HashMap<>(); + params.put("since", 100L); + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + otherQs, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + + mTask.execute(); + + verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == -1 && argument.getRbsSince() == 200), eq(true), eq(true), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + verify(mSplitsStorage, times(1)).updateSplitsFilterQueryString(otherQs); + } + + @Test + public void noClearSplitsWhenQueryStringHasNotChanged() throws HttpFetcherException { + // Splits have to be cleared when query string on db is != than current one on current sdk client instance + // Setting up cache not expired + + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + + mTask.execute(); + + verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == 100L), eq(false), eq(false), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + verify(mSplitsStorage, never()).updateSplitsFilterQueryString(anyString()); + } + + @Test + public void splitUpdatedNotified() throws HttpFetcherException { + // Check that both SPLITS_SYNC_COMPLETE and SPLITS_UPDATED are notified + // when sync completes with data changes + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(new ArrayList<>()); + + mTask.execute(); + + verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + } + + @Test + public void splitSyncCompleteNotifiedWhenNoDataChange() throws HttpFetcherException { + // Check that SPLITS_SYNC_COMPLETE is notified when sync completes + // but no data changes (SPLITS_UPDATED should NOT be notified) + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + } + + @Test + public void syncIsTrackedInTelemetry() { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mTelemetryRuntimeProducer).recordSyncLatency(eq(OperationType.SPLITS), anyLong()); + } + + @Test + public void recordSuccessInTelemetry() { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(-1L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + + mTask.execute(); + + verify(mTelemetryRuntimeProducer).recordSuccessfulSync(eq(OperationType.SPLITS), longThat(arg -> arg > 0)); + } + + @Test + public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void splitsUpdatedIsFiredWhenDataChanged() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(new ArrayList<>()); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + } + + @Test + public void splitsUpdatedIsNotFiredWhenDataUnchanged() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void splitsUpdatedIncludesMetadataWithUpdatedFlags() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + + // Mock the updated split names + List updatedSplitNames = Arrays.asList("split1", "split2", "split3"); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(updatedSplitNames); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { + if (metadata == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(3, names.size()); + assertTrue(names.contains("split1")); + assertTrue(names.contains("split2")); + assertTrue(names.contains("split3")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); + return true; + })); + } + + @Test + public void splitsUpdatedIncludesEmptyMetadataWhenNoSplitsUpdated() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + + // Mock empty updated split names + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(new ArrayList<>()); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { + if (metadata == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMeta.getNames(); + assertNotNull(names); + assertTrue(names.isEmpty()); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); + return true; + })); + } + + @Test + public void ruleBasedSegmentsUpdatedIsFiredWhenRbsChanged() { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()).thenReturn(true); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void ruleBasedSegmentsUpdatedIsNotFiredWhenRbsUnchanged() { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()).thenReturn(false); + + mTask.execute(); + + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void ruleBasedSegmentsUpdatedIsNotFiredWhenBothSplitsAndRbsChanged() { + // When both splits and RBS change, only SPLITS_UPDATED should fire (else-if logic) + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(Arrays.asList("flag1")); + + mTask.execute(); + + // SPLITS_UPDATED should fire + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + // RULE_BASED_SEGMENTS_UPDATED should NOT fire (else-if logic) + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void syncCompleteMetadataHasInitialCacheLoadFalseWhenCacheAlreadyLoaded() { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + long expectedTimestamp = 1234567890L; + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(expectedTimestamp); + when(mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)).thenReturn(true); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), argThat(metadata -> { + if (metadata == null) return false; + SdkReadyMetadata typedMeta = TypedTaskConverter.convertForSdkReady(metadata); + assertEquals(false, typedMeta.isInitialCacheLoad()); + assertEquals(Long.valueOf(expectedTimestamp), typedMeta.getLastUpdateTimestamp()); + return true; + })); + } + + @Test + public void syncCompleteMetadataHasInitialCacheLoadTrueWhenCacheNotLoaded() { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)).thenReturn(false); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), argThat(metadata -> { + if (metadata == null) return false; + SdkReadyMetadata typedMeta = TypedTaskConverter.convertForSdkReady(metadata); + assertEquals(true, typedMeta.isInitialCacheLoad()); + assertEquals(null, typedMeta.getLastUpdateTimestamp()); + return true; + })); + } + + @After + public void tearDown() { + reset(mSplitsStorage); + } + + private void loadSplitChanges() { + if (mSplitChange == null) { + FileHelper fileHelper = new FileHelper(); + mSplitChange = fileHelper.loadSplitChangeFromFile("split_changes_1.json"); + } + } +} diff --git a/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java b/main/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java similarity index 98% rename from src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java rename to main/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java index 341150e1c..ed3764293 100644 --- a/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java @@ -267,19 +267,21 @@ public void stopScheduledTask() { @Test public void stopStartedTask() throws InterruptedException { - CountDownLatch latch = new CountDownLatch(4); - CountDownLatch timerLatch = new CountDownLatch(1); + CountDownLatch executionLatch = new CountDownLatch(2); CountDownLatch listenerLatch = new CountDownLatch(1); - TestTask task = new TestTask(latch); + TestTask task = new TestTask(executionLatch); TestListener testListener = new TestListener(listenerLatch); String taskId = mTaskExecutor.schedule(task, 0L, 1L, testListener); - timerLatch.await(2L, TimeUnit.SECONDS); + + boolean completed = executionLatch.await(5L, TimeUnit.SECONDS); + assertTrue("Task should have executed at least twice", completed); + mTaskExecutor.stopTask(taskId); assertTrue(task.taskHasBeenCalled); assertTrue(testListener.taskExecutedCalled); - assertEquals(2, task.callCount.get()); + assertTrue(task.callCount.get() >= 2); } @Test diff --git a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java new file mode 100644 index 000000000..fda1b1a89 --- /dev/null +++ b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -0,0 +1,225 @@ +package io.split.android.client.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.List; + +import io.split.android.client.events.SdkReadyMetadata; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.http.HttpFetcherException; +import io.split.android.client.service.splits.SplitsSyncHelper; +import io.split.android.client.service.splits.SplitsUpdateTask; +import io.split.android.client.service.synchronizer.SplitsChangeChecker; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.helpers.FileHelper; + +public class SplitUpdateTaskTest { + + SplitsStorage mSplitsStorage; + RuleBasedSegmentStorage mRuleBasedSegmentStorage; + SplitChange mSplitChange = null; + SplitsSyncHelper mSplitsSyncHelper; + SplitEventsManager mEventsManager; + + SplitsUpdateTask mTask; + + long mChangeNumber = 234567833L; + long mRbsChangeNumber = 234567830L; + + @Before + public void setup() { + mSplitsStorage = Mockito.mock(SplitsStorage.class); + mRuleBasedSegmentStorage = Mockito.mock(RuleBasedSegmentStorage.class); + mSplitsSyncHelper = Mockito.mock(SplitsSyncHelper.class); + mEventsManager = Mockito.mock(SplitEventsManager.class); + mTask = new SplitsUpdateTask(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mChangeNumber, mRbsChangeNumber, mEventsManager); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + loadSplitChanges(); + } + + @Test + public void correctExecution() throws HttpFetcherException { + when(mSplitsStorage.getTill()).thenReturn(-1L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(10L); + + mTask.execute(); + + verify(mSplitsSyncHelper).sync(argThat(new ArgumentMatcher() { + @Override + public boolean matches(SplitsSyncHelper.SinceChangeNumbers argument) { + return argument.getFlagsSince() == 234567833L && argument.getRbsSince() == 234567830L; + } + }), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + } + + @Test + public void storedChangeNumBigger() throws HttpFetcherException { + when(mSplitsStorage.getTill()).thenReturn(mChangeNumber + 100L); + + mTask.execute(); + + verify(mSplitsSyncHelper, never()).sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + } + + @Test + public void storedRbsChangeNumBigger() { + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(mRbsChangeNumber + 100L); + + mTask.execute(); + + verify(mSplitsSyncHelper, never()).sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + } + + @Test + public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSyncWithSyncMetadata() { + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + // Verify TARGETING_RULES_SYNC_COMPLETE is fired with sync metadata (initialCacheLoad=true, lastUpdateTimestamp=null) + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), argThat(metadata -> { + if (metadata == null) return false; + SdkReadyMetadata typedMeta = TypedTaskConverter.convertForSdkReady(metadata); + assertEquals(Boolean.TRUE, typedMeta.isInitialCacheLoad()); + // lastUpdateTimestamp should not be present (or should be null) + return typedMeta.getLastUpdateTimestamp() == null; + })); + } + + @Test + public void splitsUpdatedIsFiredWhenSplitsDataChanged() { + long storedChangeNumber = 100L; + when(mSplitsStorage.getTill()).thenReturn(storedChangeNumber).thenReturn(150L); // After sync, change number increased + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(Arrays.asList()); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void splitsUpdatedIsFiredWhenRbsDataChanged() { + long storedRbsChangeNumber = 200L; + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(storedRbsChangeNumber).thenReturn(250L); // After sync, RBS change number increased + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()).thenReturn(true); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void splitsUpdatedIsNotFiredWhenDataUnchanged() { + long storedChangeNumber = 100L; + long storedRbsChangeNumber = 200L; + when(mSplitsStorage.getTill()).thenReturn(storedChangeNumber); // Same before and after sync + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(storedRbsChangeNumber); // Same before and after sync + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @Test + public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { + long storedChangeNumber = 100L; + when(mSplitsStorage.getTill()).thenReturn(storedChangeNumber).thenReturn(150L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + + // Mock the updated split names + List updatedSplitNames = Arrays.asList("flag1", "flag2"); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(updatedSplitNames); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { + if (metadata == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(2, names.size()); + assertTrue(names.contains("flag1")); + assertTrue(names.contains("flag2")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); + return true; + })); + } + + @Test + public void ruleBasedSegmentsUpdatedIsNotFiredWhenBothSplitsAndRbsChanged() { + // When both splits and RBS change, only SPLITS_UPDATED should fire (else-if logic) + long storedChangeNumber = 100L; + long storedRbsChangeNumber = 200L; + when(mSplitsStorage.getTill()).thenReturn(storedChangeNumber).thenReturn(150L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(storedRbsChangeNumber).thenReturn(250L); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(Arrays.asList("flag1")); + + mTask.execute(); + + // SPLITS_UPDATED should fire + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + // RULE_BASED_SEGMENTS_UPDATED should NOT fire (else-if logic) + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + + @After + public void tearDown() { + reset(mSplitsStorage); + } + + private void loadSplitChanges() { + if (mSplitChange == null) { + FileHelper fileHelper = new FileHelper(); + mSplitChange = fileHelper.loadSplitChangeFromFile("split_changes_1.json"); + } + } +} diff --git a/src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java b/main/src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java rename to main/src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java diff --git a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java b/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java similarity index 69% rename from src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java rename to main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java index d2770971c..ec8c7db04 100644 --- a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java @@ -1,6 +1,7 @@ package io.split.android.client.service; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -9,6 +10,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -26,23 +28,28 @@ import org.mockito.Spy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; +import io.split.android.client.dtos.Split; import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.dtos.RuleBasedSegmentChange; import io.split.android.client.dtos.SplitChange; import io.split.android.client.dtos.TargetingRulesChange; +import io.split.android.client.dtos.Status; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.http.HttpStatus; +import io.split.android.client.service.rules.ProcessedRuleBasedSegmentChange; import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitsSyncHelper; @@ -559,4 +566,244 @@ private TargetingRulesChange getRuleBasedSegmentChange(int since, int till) { return TargetingRulesChange.create(SplitChange.create(10, 10, new ArrayList<>()), ruleBasedSegmentChange); } + + @Test + public void getLastUpdatedFlagNamesReturnsEmptyListWhenNoSyncOccurred() { + List result = mSplitsSyncHelper.getLastUpdatedFlagNames(); + assertTrue(result.isEmpty()); + } + + @Test + public void extractFlagNamesReturnsEmptyListWhenChangeIsNull() { + List result = SplitsSyncHelper.extractFlagNames(null); + assertTrue(result.isEmpty()); + } + + @Test + public void extractSplitNamesReturnsActiveFlagNames() { + Split split1 = new Split(); + split1.name = "split1"; + Split split2 = new Split(); + split2.name = "split2"; + List activeSplits = Arrays.asList(split1, split2); + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(activeSplits, new ArrayList<>(), 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractFlagNames(processedSplitChange); + + assertEquals(2, result.size()); + assertTrue(result.contains("split1")); + assertTrue(result.contains("split2")); + } + + @Test + public void extractSplitNamesReturnsArchivedFlagNames() { + Split archivedSplit1 = new Split(); + archivedSplit1.name = "archived1"; + Split archivedSplit2 = new Split(); + archivedSplit2.name = "archived2"; + List archivedSplits = Arrays.asList(archivedSplit1, archivedSplit2); + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), archivedSplits, 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractFlagNames(processedSplitChange); + + assertEquals(2, result.size()); + assertTrue(result.contains("archived1")); + assertTrue(result.contains("archived2")); + } + + @Test + public void extractSplitNamesReturnsBothActiveAndArchivedFlagNames() { + Split activeSplit = new Split(); + activeSplit.name = "active_split"; + Split archivedSplit = new Split(); + archivedSplit.name = "archived_split"; + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange( + Arrays.asList(activeSplit), + Arrays.asList(archivedSplit), + 100L, + System.currentTimeMillis() + ); + + List result = SplitsSyncHelper.extractFlagNames(processedSplitChange); + + assertEquals(2, result.size()); + assertTrue(result.contains("active_split")); + assertTrue(result.contains("archived_split")); + } + + @Test + public void extractFlagNamesHandlesNullSplitsInLists() { + Split validSplit = new Split(); + validSplit.name = "valid_split"; + List activeSplits = Arrays.asList(null, validSplit, null); + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(activeSplits, new ArrayList<>(), 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractFlagNames(processedSplitChange); + + assertEquals(1, result.size()); + assertTrue(result.contains("valid_split")); + } + + @Test + public void extractFlagNamesHandlesSplitsWithNullNames() { + Split splitWithNullName = new Split(); + splitWithNullName.name = null; + Split validSplit = new Split(); + validSplit.name = "valid_split"; + List activeSplits = Arrays.asList(splitWithNullName, validSplit); + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(activeSplits, new ArrayList<>(), 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractFlagNames(processedSplitChange); + + assertEquals(1, result.size()); + assertTrue(result.contains("valid_split")); + } + + @Test + public void extractFlagNamesReturnsEmptyListWhenBothListsAreEmpty() { + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), new ArrayList<>(), 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractFlagNames(processedSplitChange); + + assertTrue(result.isEmpty()); + } + + @Test + public void extractFlagNamesHandlesNullLists() { + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(null, null, 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractFlagNames(processedSplitChange); + + assertTrue(result.isEmpty()); + } + + @Test + public void splitsHaveChangedReturnsTrueWhenSplitsAreNonEmpty() throws HttpFetcherException { + Split split = new Split(); + split.name = "test_split"; + SplitChange splitChange = SplitChange.create(-1, 100L, Collections.singletonList(split)); + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(splitChange, RuleBasedSegmentChange.create(-1, 100L, Collections.emptyList()))) + .thenReturn(TargetingRulesChange.create(SplitChange.create(100L, 100L, Collections.emptyList()), RuleBasedSegmentChange.create(100L, 100L, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(100L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + assertTrue(mSplitsSyncHelper.splitsHaveChanged()); + } + + @Test + public void splitsHaveChangedReturnsFalseWhenSplitsAreEmpty() throws HttpFetcherException { + SplitChange splitChange = SplitChange.create(-1, 100L, Collections.emptyList()); + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(splitChange, RuleBasedSegmentChange.create(-1, 100L, Collections.emptyList()))) + .thenReturn(TargetingRulesChange.create(SplitChange.create(100L, 100L, Collections.emptyList()), RuleBasedSegmentChange.create(100L, 100L, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(100L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + assertTrue(!mSplitsSyncHelper.splitsHaveChanged()); + } + + @Test + public void ruleBasedSegmentsHaveChangedReturnsTrueWhenSegmentsAreNonEmpty() throws HttpFetcherException { + RuleBasedSegment segment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("test_segment"); + SplitChange splitChange = SplitChange.create(-1, 100L, Collections.emptyList()); + RuleBasedSegmentChange rbsChange = RuleBasedSegmentChange.create(-1, 100L, Collections.singletonList(segment)); + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(splitChange, rbsChange)) + .thenReturn(TargetingRulesChange.create(SplitChange.create(100L, 100L, Collections.emptyList()), RuleBasedSegmentChange.create(100L, 100L, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(100L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + assertTrue(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()); + } + + @Test + public void ruleBasedSegmentsHaveChangedReturnsFalseWhenSegmentsAreEmpty() throws HttpFetcherException { + SplitChange splitChange = SplitChange.create(-1, 100L, Collections.emptyList()); + RuleBasedSegmentChange rbsChange = RuleBasedSegmentChange.create(-1, 100L, Collections.emptyList()); + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(splitChange, rbsChange)) + .thenReturn(TargetingRulesChange.create(SplitChange.create(100L, 100L, Collections.emptyList()), RuleBasedSegmentChange.create(100L, 100L, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(100L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + assertTrue(!mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()); + } + + @Test + public void getLastUpdatedSplitNamesReturnsFlagNamesAfterSync() throws HttpFetcherException { + // Use the actual split change from loadSplitChanges which contains real splits + SplitChange secondSplitChange = mTargetingRulesChange.getFeatureFlagsChange(); + secondSplitChange.since = mTargetingRulesChange.getFeatureFlagsChange().till; + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(secondSplitChange, RuleBasedSegmentChange.create(262325L, 262325L, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(1506703262916L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(262325L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + List result = mSplitsSyncHelper.getLastUpdatedFlagNames(); + // The result should contain split names from the processed split change + // Since we're using real processor, it will process the actual splits from mTargetingRulesChange + assertNotNull(result); + // The exact number depends on the splits in the test data, but it should not be null + } + + @Test + public void getLastUpdatedFlagNamesPreservesLastNonEmptyChange() throws HttpFetcherException { + Split split = new Split(); + split.name = "split_1"; + split.status = Status.ACTIVE; + + SplitChange firstSplitChange = SplitChange.create(-1, 100L, Collections.singletonList(split)); + SplitChange secondSplitChange = SplitChange.create(100L, 100L, Collections.emptyList()); + + RuleBasedSegmentChange firstRbsChange = RuleBasedSegmentChange.create(-1, 10L, Collections.emptyList()); + RuleBasedSegmentChange secondRbsChange = RuleBasedSegmentChange.create(10L, 10L, Collections.emptyList()); + + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(firstSplitChange, firstRbsChange)) + .thenReturn(TargetingRulesChange.create(secondSplitChange, secondRbsChange)); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(10L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + List result = mSplitsSyncHelper.getLastUpdatedFlagNames(); + assertEquals(1, result.size()); + assertTrue(result.contains("split_1")); + } + + @Test + public void getLastUpdatedFlagNamesIncludesArchivedSplits() throws HttpFetcherException { + Split archivedSplit = new Split(); + archivedSplit.name = "archived_split"; + List archivedSplits = Arrays.asList(archivedSplit); + SplitChange splitChange = SplitChange.create(-1, 100L, new ArrayList<>()); + // Create ProcessedSplitChange with archived splits + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), archivedSplits, 100L, System.currentTimeMillis()); + + when(mSplitChangeProcessor.process(any())).thenReturn(processedSplitChange); + + SplitChange secondSplitChange = splitChange; + secondSplitChange.since = splitChange.till; + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(secondSplitChange, RuleBasedSegmentChange.create(262325L, 262325L, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(262325L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + List result = mSplitsSyncHelper.getLastUpdatedFlagNames(); + assertEquals(1, result.size()); + assertTrue(result.contains("archived_split")); + } } diff --git a/src/test/java/io/split/android/client/service/SynchronizerTest.java b/main/src/test/java/io/split/android/client/service/SynchronizerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/SynchronizerTest.java rename to main/src/test/java/io/split/android/client/service/SynchronizerTest.java diff --git a/src/test/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTaskTest.java b/main/src/test/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTaskTest.java rename to main/src/test/java/io/split/android/client/service/attributes/ClearAttributesInPersistentStorageTaskTest.java diff --git a/src/test/java/io/split/android/client/service/attributes/LoadAttributesTaskTest.java b/main/src/test/java/io/split/android/client/service/attributes/LoadAttributesTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/attributes/LoadAttributesTaskTest.java rename to main/src/test/java/io/split/android/client/service/attributes/LoadAttributesTaskTest.java diff --git a/src/test/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTaskTest.java b/main/src/test/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTaskTest.java rename to main/src/test/java/io/split/android/client/service/attributes/UpdateAttributesInPersistentStorageTaskTest.java diff --git a/src/test/java/io/split/android/client/service/events/EventsRequestBodySerializerTest.java b/main/src/test/java/io/split/android/client/service/events/EventsRequestBodySerializerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/events/EventsRequestBodySerializerTest.java rename to main/src/test/java/io/split/android/client/service/events/EventsRequestBodySerializerTest.java diff --git a/src/test/java/io/split/android/client/service/events/EventsTrackerTest.java b/main/src/test/java/io/split/android/client/service/events/EventsTrackerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/events/EventsTrackerTest.java rename to main/src/test/java/io/split/android/client/service/events/EventsTrackerTest.java diff --git a/src/test/java/io/split/android/client/service/executor/SplitTaskSerialWrapperTest.java b/main/src/test/java/io/split/android/client/service/executor/SplitTaskSerialWrapperTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/executor/SplitTaskSerialWrapperTest.java rename to main/src/test/java/io/split/android/client/service/executor/SplitTaskSerialWrapperTest.java diff --git a/src/test/java/io/split/android/client/service/executor/ThreadFactoryBuilderTest.java b/main/src/test/java/io/split/android/client/service/executor/ThreadFactoryBuilderTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/executor/ThreadFactoryBuilderTest.java rename to main/src/test/java/io/split/android/client/service/executor/ThreadFactoryBuilderTest.java diff --git a/src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java b/main/src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java rename to main/src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java diff --git a/src/test/java/io/split/android/client/service/http/HttpSseAuthTokenFetcherTest.java b/main/src/test/java/io/split/android/client/service/http/HttpSseAuthTokenFetcherTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/http/HttpSseAuthTokenFetcherTest.java rename to main/src/test/java/io/split/android/client/service/http/HttpSseAuthTokenFetcherTest.java diff --git a/src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java b/main/src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java rename to main/src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImplTest.kt b/main/src/test/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImplTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImplTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/ImpressionManagerRetryTimerProviderImplTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializerTest.java b/main/src/test/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializerTest.java rename to main/src/test/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializerTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/MemoizedSupplierTest.java b/main/src/test/java/io/split/android/client/service/impressions/MemoizedSupplierTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/MemoizedSupplierTest.java rename to main/src/test/java/io/split/android/client/service/impressions/MemoizedSupplierTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt b/main/src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java b/main/src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java rename to main/src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java b/main/src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java rename to main/src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java b/main/src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java rename to main/src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt rename to main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/UtilsTest.java b/main/src/test/java/io/split/android/client/service/impressions/strategy/UtilsTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/strategy/UtilsTest.java rename to main/src/test/java/io/split/android/client/service/impressions/strategy/UtilsTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializerTest.java b/main/src/test/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializerTest.java rename to main/src/test/java/io/split/android/client/service/impressions/unique/MTKRequestBodySerializerTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTaskTest.java b/main/src/test/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTaskTest.java rename to main/src/test/java/io/split/android/client/service/impressions/unique/SaveUniqueImpressionsTaskTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskTest.java b/main/src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskTest.java rename to main/src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTaskTest.java diff --git a/src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImplTest.java b/main/src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImplTest.java rename to main/src/test/java/io/split/android/client/service/impressions/unique/UniqueKeysTrackerImplTest.java diff --git a/src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java b/main/src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java rename to main/src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java diff --git a/src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java b/main/src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java rename to main/src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java diff --git a/src/test/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTaskTest.java rename to main/src/test/java/io/split/android/client/service/mysegments/MySegmentsBulkSyncTaskTest.java diff --git a/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java b/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java similarity index 89% rename from src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java rename to main/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java index 07f4fa6a0..7e2d8b3c0 100644 --- a/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java +++ b/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java @@ -16,7 +16,6 @@ public void getForMySegments() { assertEquals(config.getTaskType(), SplitTaskType.MY_SEGMENTS_SYNC); assertEquals(config.getUpdateEvent(), SplitInternalEvent.MY_SEGMENTS_UPDATED); - assertEquals(config.getFetchedEvent(), SplitInternalEvent.MY_SEGMENTS_FETCHED); assertEquals(config.getTelemetryOperationType(), OperationType.MY_SEGMENT); } } diff --git a/src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java b/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java rename to main/src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java diff --git a/src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java b/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java rename to main/src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java diff --git a/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessorTest.java b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessorTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessorTest.java rename to main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessorTest.java diff --git a/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java similarity index 64% rename from src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java rename to main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java index bd5013088..37d05d47d 100644 --- a/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java @@ -1,5 +1,11 @@ package io.split.android.client.service.rules; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -12,11 +18,14 @@ import org.junit.Test; import java.util.Collections; +import java.util.List; import java.util.Set; import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SdkUpdateMetadata; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; public class RuleBasedSegmentInPlaceUpdateTaskTest { @@ -45,7 +54,7 @@ public void splitEventsManagerIsNotifiedWithUpdateEvent() { mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); } @Test @@ -60,7 +69,7 @@ public void splitEventsManagerIsNotNotifiedWhenUpdateResultIsFalse() { mTask.execute(); - verify(mEventsManager, times(0)).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + verify(mEventsManager, times(0)).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); } @Test @@ -89,6 +98,31 @@ public void updateIsCalledOnStorage() { verify(mRuleBasedSegmentStorage).update(Set.of(ruleBasedSegment), Set.of(), changeNumber, null); } + @Test + public void segmentsUpdatedIncludesMetadataWithEmptyNames() { + RuleBasedSegment activeSegment = createRuleBasedSegment("active_segment"); + RuleBasedSegment archivedSegment = createRuleBasedSegment("archived_segment"); + long changeNumber = 123L; + + when(mChangeProcessor.process(activeSegment, changeNumber)).thenReturn( + new ProcessedRuleBasedSegmentChange(Set.of(activeSegment), Set.of(archivedSegment), changeNumber, System.currentTimeMillis())); + when(mRuleBasedSegmentStorage.update(Set.of(activeSegment), Set.of(archivedSegment), changeNumber, null)).thenReturn(true); + + mTask = getTask(activeSegment, changeNumber); + mTask.execute(); + + // SEGMENTS_UPDATE always has empty names + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), argThat(metadata -> { + if (metadata == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMeta.getNames(); + assertNotNull(names); + assertTrue("Names should be empty for SEGMENTS_UPDATE", names.isEmpty()); + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, typedMeta.getType()); + return true; + })); + } + @NonNull private RuleBasedSegmentInPlaceUpdateTask getTask(RuleBasedSegment ruleBasedSegment, long changeNumber) { return new RuleBasedSegmentInPlaceUpdateTask(mRuleBasedSegmentStorage, diff --git a/src/test/java/io/split/android/client/service/rules/TargetingRulesResponseParserTest.java b/main/src/test/java/io/split/android/client/service/rules/TargetingRulesResponseParserTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/rules/TargetingRulesResponseParserTest.java rename to main/src/test/java/io/split/android/client/service/rules/TargetingRulesResponseParserTest.java diff --git a/src/test/java/io/split/android/client/service/splits/LoadSplitsTaskTest.java b/main/src/test/java/io/split/android/client/service/splits/LoadSplitsTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/splits/LoadSplitsTaskTest.java rename to main/src/test/java/io/split/android/client/service/splits/LoadSplitsTaskTest.java diff --git a/src/test/java/io/split/android/client/service/splits/OutdatedSplitProxyHandlerTest.java b/main/src/test/java/io/split/android/client/service/splits/OutdatedSplitProxyHandlerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/splits/OutdatedSplitProxyHandlerTest.java rename to main/src/test/java/io/split/android/client/service/splits/OutdatedSplitProxyHandlerTest.java diff --git a/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java b/main/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java rename to main/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java diff --git a/src/test/java/io/split/android/client/service/splits/SplitsSyncHelperFreshInstallTest.java b/main/src/test/java/io/split/android/client/service/splits/SplitsSyncHelperFreshInstallTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/splits/SplitsSyncHelperFreshInstallTest.java rename to main/src/test/java/io/split/android/client/service/splits/SplitsSyncHelperFreshInstallTest.java diff --git a/src/test/java/io/split/android/client/service/splits/TargetingRulesCacheTest.java b/main/src/test/java/io/split/android/client/service/splits/TargetingRulesCacheTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/splits/TargetingRulesCacheTest.java rename to main/src/test/java/io/split/android/client/service/splits/TargetingRulesCacheTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/BackgroundDisconnectionTaskTest.java b/main/src/test/java/io/split/android/client/service/sseclient/BackgroundDisconnectionTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/BackgroundDisconnectionTaskTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/BackgroundDisconnectionTaskTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/EventStreamParserTest.java b/main/src/test/java/io/split/android/client/service/sseclient/EventStreamParserTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/EventStreamParserTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/EventStreamParserTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java b/main/src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/NotificationManagerKeeperTest.java b/main/src/test/java/io/split/android/client/service/sseclient/NotificationManagerKeeperTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/NotificationManagerKeeperTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/NotificationManagerKeeperTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java b/main/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java b/main/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java b/main/src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/ReconnectBackoffCounterTest.java b/main/src/test/java/io/split/android/client/service/sseclient/ReconnectBackoffCounterTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/ReconnectBackoffCounterTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/ReconnectBackoffCounterTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java b/main/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java b/main/src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/SseClientTest.java b/main/src/test/java/io/split/android/client/service/sseclient/SseClientTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/SseClientTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/SseClientTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java b/main/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/SyncManagerChannelMessageTest.java b/main/src/test/java/io/split/android/client/service/sseclient/SyncManagerChannelMessageTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/SyncManagerChannelMessageTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/SyncManagerChannelMessageTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java b/main/src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java b/main/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java b/main/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImplTest.java b/main/src/test/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImplTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImplTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTaskTest.java b/main/src/test/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTaskTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManagerDeferredStartTaskTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java b/main/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimerTest.java b/main/src/test/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimerTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimerTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimerTest.java diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/notifications/SplitsChangeNotificationTest.java b/main/src/test/java/io/split/android/client/service/sseclient/sseclient/notifications/SplitsChangeNotificationTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/sseclient/sseclient/notifications/SplitsChangeNotificationTest.java rename to main/src/test/java/io/split/android/client/service/sseclient/sseclient/notifications/SplitsChangeNotificationTest.java diff --git a/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java similarity index 80% rename from src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java rename to main/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java index a996b9aba..515d3dd3e 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java +++ b/main/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java @@ -26,6 +26,8 @@ import io.split.android.client.RetryBackoffCounterTimerFactory; import io.split.android.client.SplitClientConfig; import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskBatchItem; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -40,6 +42,7 @@ import io.split.android.client.service.splits.SplitsUpdateTask; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; +import io.split.android.client.storage.splits.SplitsStorage; public class FeatureFlagsSynchronizerImplTest { @@ -257,4 +260,54 @@ public String answer(InvocationOnMock invocation) { verify(mSingleThreadTaskExecutor).stopTask("12"); verify(mSingleThreadTaskExecutor, times(1)).schedule(eq(mockTask), anyLong(), anyLong(), any()); } + + @Test + public void loadAndSynchronizeNotifiesEventsManagerWithCorrectMetadataWhenSplitsLoadedFromStorage() { + long expectedTimestamp = 1234567890L; + SplitsStorage splitsStorage = mock(SplitsStorage.class); + when(splitsStorage.getUpdateTimestamp()).thenReturn(expectedTimestamp); + + // Set up mock tasks + LoadSplitsTask mockLoadTask = mock(LoadSplitsTask.class); + when(mockLoadTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + when(mTaskFactory.createLoadSplitsTask()).thenReturn(mockLoadTask); + + LoadRuleBasedSegmentsTask mockLoadRuleBasedSegmentsTask = mock(LoadRuleBasedSegmentsTask.class); + when(mockLoadRuleBasedSegmentsTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_RULE_BASED_SEGMENTS)); + when(mTaskFactory.createLoadRuleBasedSegmentsTask()).thenReturn(mockLoadRuleBasedSegmentsTask); + + FilterSplitsInCacheTask mockFilterTask = mock(FilterSplitsInCacheTask.class); + when(mockFilterTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.FILTER_SPLITS_CACHE)); + when(mTaskFactory.createFilterSplitsInCacheTask()).thenReturn(mockFilterTask); + + SplitsSyncTask mockSplitSyncTask = mock(SplitsSyncTask.class); + when(mockSplitSyncTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mTaskFactory.createSplitsSyncTask(true)).thenReturn(mockSplitSyncTask); + + FeatureFlagsSynchronizerImpl synchronizer = new FeatureFlagsSynchronizerImpl( + mConfig, mTaskExecutor, mSingleThreadTaskExecutor, mTaskFactory, + mEventsManager, mRetryBackoffCounterFactory, mPushManagerEventBroadcaster, splitsStorage); + + ArgumentCaptor> batchCaptor = ArgumentCaptor.forClass(List.class); + + synchronizer.loadAndSynchronize(); + + verify(mTaskExecutor).executeSerially(batchCaptor.capture()); + List batch = batchCaptor.getValue(); + + SplitTaskBatchItem loadSplitsItem = batch.get(2); + SplitTaskExecutionListener listener = loadSplitsItem.getListener(); + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(EventMetadata.class); + verify(mEventsManager).notifyInternalEvent( + eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), + metadataCaptor.capture()); + + EventMetadata capturedMetadata = metadataCaptor.getValue(); + + assertEquals(false, capturedMetadata.get("initialCacheLoad")); + assertEquals(expectedTimestamp, capturedMetadata.get("lastUpdateTimestamp")); + } } diff --git a/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java new file mode 100644 index 000000000..252c29696 --- /dev/null +++ b/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java @@ -0,0 +1,87 @@ +package io.split.android.client.service.synchronizer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskType; + +public class LoadLocalDataListenerTest { + + private ISplitEventsManager mEventsManager; + + @Before + public void setUp() { + mEventsManager = mock(ISplitEventsManager.class); + } + + @Test + public void taskExecutedSuccessFiresEventWithoutMetadataWhenProviderIsNull() { + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), isNull()); + } + + @Test + public void taskExecutedSuccessFiresEventWithMetadataWhenProviderIsNotNull() { + EventMetadata mockMetadata = mock(EventMetadata.class); + LoadLocalDataListener.MetadataProvider provider = () -> mockMetadata; + + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, provider); + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), eq(mockMetadata)); + } + + @Test + public void taskExecutedErrorDoesNotFireEvent() { + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + + listener.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(mEventsManager, never()).notifyInternalEvent(any(), any()); + } + + @Test + public void metadataProviderIsCalledWhenTaskSucceeds() { + LoadLocalDataListener.MetadataProvider provider = mock(LoadLocalDataListener.MetadataProvider.class); + when(provider.getMetadata()).thenReturn(null); + + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, provider); + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(provider).getMetadata(); + } + + @Test + public void metadataProviderIsNotCalledWhenTaskFails() { + LoadLocalDataListener.MetadataProvider provider = mock(LoadLocalDataListener.MetadataProvider.class); + + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, provider); + + listener.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(provider, never()).getMetadata(); + } +} diff --git a/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java rename to main/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java diff --git a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt b/main/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt similarity index 100% rename from src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt rename to main/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt diff --git a/src/test/java/io/split/android/client/service/synchronizer/SyncGuardianImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/SyncGuardianImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/synchronizer/SyncGuardianImplTest.java rename to main/src/test/java/io/split/android/client/service/synchronizer/SyncGuardianImplTest.java diff --git a/src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImplTest.java rename to main/src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerImplTest.java diff --git a/src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImplTest.java rename to main/src/test/java/io/split/android/client/service/synchronizer/attributes/AttributesSynchronizerRegistryImplTest.java diff --git a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java rename to main/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java diff --git a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java rename to main/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java diff --git a/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java b/main/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java rename to main/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java diff --git a/src/test/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTaskTest.java b/main/src/test/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTaskTest.java rename to main/src/test/java/io/split/android/client/service/telemetry/TelemetryConfigRecorderTaskTest.java diff --git a/src/test/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTaskTest.java b/main/src/test/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTaskTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTaskTest.java rename to main/src/test/java/io/split/android/client/service/telemetry/TelemetryStatsRecorderTaskTest.java diff --git a/src/test/java/io/split/android/client/service/workmanager/HttpClientProviderTest.java b/main/src/test/java/io/split/android/client/service/workmanager/HttpClientProviderTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/workmanager/HttpClientProviderTest.java rename to main/src/test/java/io/split/android/client/service/workmanager/HttpClientProviderTest.java diff --git a/src/test/java/io/split/android/client/service/workmanager/splits/FetcherProviderTest.java b/main/src/test/java/io/split/android/client/service/workmanager/splits/FetcherProviderTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/workmanager/splits/FetcherProviderTest.java rename to main/src/test/java/io/split/android/client/service/workmanager/splits/FetcherProviderTest.java diff --git a/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilderTest.java b/main/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilderTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilderTest.java rename to main/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerFilterBuilderTest.java diff --git a/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParamsTest.java b/main/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParamsTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParamsTest.java rename to main/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerParamsTest.java diff --git a/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java b/main/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java rename to main/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java diff --git a/src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java b/main/src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java similarity index 100% rename from src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java rename to main/src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java diff --git a/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java b/main/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java rename to main/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java diff --git a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java b/main/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java rename to main/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java diff --git a/src/test/java/io/split/android/client/storage/attributes/AttributesStorageImplTest.java b/main/src/test/java/io/split/android/client/storage/attributes/AttributesStorageImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/attributes/AttributesStorageImplTest.java rename to main/src/test/java/io/split/android/client/storage/attributes/AttributesStorageImplTest.java diff --git a/src/test/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorageTest.java b/main/src/test/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorageTest.java rename to main/src/test/java/io/split/android/client/storage/attributes/SqLitePersistentAttributesStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt b/main/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt similarity index 100% rename from src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt rename to main/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt diff --git a/src/test/java/io/split/android/client/storage/cipher/CBCCipherTest.kt b/main/src/test/java/io/split/android/client/storage/cipher/CBCCipherTest.kt similarity index 100% rename from src/test/java/io/split/android/client/storage/cipher/CBCCipherTest.kt rename to main/src/test/java/io/split/android/client/storage/cipher/CBCCipherTest.kt diff --git a/src/test/java/io/split/android/client/storage/cipher/DBCipherTest.kt b/main/src/test/java/io/split/android/client/storage/cipher/DBCipherTest.kt similarity index 100% rename from src/test/java/io/split/android/client/storage/cipher/DBCipherTest.kt rename to main/src/test/java/io/split/android/client/storage/cipher/DBCipherTest.kt diff --git a/src/test/java/io/split/android/client/storage/cipher/EncryptionMigrationTaskTest.kt b/main/src/test/java/io/split/android/client/storage/cipher/EncryptionMigrationTaskTest.kt similarity index 100% rename from src/test/java/io/split/android/client/storage/cipher/EncryptionMigrationTaskTest.kt rename to main/src/test/java/io/split/android/client/storage/cipher/EncryptionMigrationTaskTest.kt diff --git a/src/test/java/io/split/android/client/storage/cipher/NoOpCipherTest.kt b/main/src/test/java/io/split/android/client/storage/cipher/NoOpCipherTest.kt similarity index 100% rename from src/test/java/io/split/android/client/storage/cipher/NoOpCipherTest.kt rename to main/src/test/java/io/split/android/client/storage/cipher/NoOpCipherTest.kt diff --git a/src/test/java/io/split/android/client/storage/cipher/ObjectPoolTest.kt b/main/src/test/java/io/split/android/client/storage/cipher/ObjectPoolTest.kt similarity index 100% rename from src/test/java/io/split/android/client/storage/cipher/ObjectPoolTest.kt rename to main/src/test/java/io/split/android/client/storage/cipher/ObjectPoolTest.kt diff --git a/src/test/java/io/split/android/client/storage/cipher/SplitCipherFactoryTest.kt b/main/src/test/java/io/split/android/client/storage/cipher/SplitCipherFactoryTest.kt similarity index 100% rename from src/test/java/io/split/android/client/storage/cipher/SplitCipherFactoryTest.kt rename to main/src/test/java/io/split/android/client/storage/cipher/SplitCipherFactoryTest.kt diff --git a/src/test/java/io/split/android/client/storage/common/SplitStorageContainerTest.java b/main/src/test/java/io/split/android/client/storage/common/SplitStorageContainerTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/common/SplitStorageContainerTest.java rename to main/src/test/java/io/split/android/client/storage/common/SplitStorageContainerTest.java diff --git a/src/test/java/io/split/android/client/storage/db/SplitQueryDaoImplTest.java b/main/src/test/java/io/split/android/client/storage/db/SplitQueryDaoImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/db/SplitQueryDaoImplTest.java rename to main/src/test/java/io/split/android/client/storage/db/SplitQueryDaoImplTest.java diff --git a/src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java b/main/src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java rename to main/src/test/java/io/split/android/client/storage/db/SplitRoomDatabaseTest.java diff --git a/src/test/java/io/split/android/client/storage/events/EventsStorageTest.java b/main/src/test/java/io/split/android/client/storage/events/EventsStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/events/EventsStorageTest.java rename to main/src/test/java/io/split/android/client/storage/events/EventsStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java b/main/src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java rename to main/src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java b/main/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java rename to main/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java diff --git a/src/test/java/io/split/android/client/storage/impressions/ImpressionsStorageTest.java b/main/src/test/java/io/split/android/client/storage/impressions/ImpressionsStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/impressions/ImpressionsStorageTest.java rename to main/src/test/java/io/split/android/client/storage/impressions/ImpressionsStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsCountStorageTest.java b/main/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsCountStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsCountStorageTest.java rename to main/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsCountStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsStorageTest.java b/main/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsStorageTest.java rename to main/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentImpressionsStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorageTest.java b/main/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorageTest.java rename to main/src/test/java/io/split/android/client/storage/impressions/SqlitePersistentUniqueStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java b/main/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java similarity index 70% rename from src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java rename to main/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java index d3a049f28..b91b430e0 100644 --- a/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java +++ b/main/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; import org.junit.Before; import org.junit.Test; @@ -65,4 +67,33 @@ public void getUniqueAmountReturnsUniqueSegmentCount() { assertEquals(4, distinctAmount); } + + @Test + public void clearCallsPersistentStorageClear() { + mContainer.clear(); + + verify(mPersistentMySegmentsStorage).clear(); + } + + @Test + public void clearClearsInMemoryStorageForExistingKeys() { + String userKey = "user_key"; + MySegmentsStorage storageForKey = mContainer.getStorageForKey(userKey); + storageForKey.set(SegmentsChange.create(new HashSet<>(Arrays.asList("s1", "s2")), -1L)); + + mContainer.clear(); + + assertTrue(storageForKey.getAll().isEmpty()); + } + + @Test + public void clearCallsPersistentStorageClearBeforeSettingEmptySegments() { + String userKey = "user_key"; + mContainer.getStorageForKey(userKey); + + mContainer.clear(); + + // Verify persistent storage clear was called + verify(mPersistentMySegmentsStorage).clear(); + } } diff --git a/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java similarity index 97% rename from src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java rename to main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java index 1e594ee48..85bd35277 100644 --- a/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java +++ b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java @@ -110,4 +110,11 @@ public void getSnapshotReturnsDecryptedValues() { assertTrue(result.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3"))); } + + @Test + public void clearCallsDeleteAllOnDao() { + mStorage.clear(); + + verify(mDao).deleteAll(); + } } diff --git a/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java similarity index 97% rename from src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java rename to main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java index 112b5e4cc..4fa61edfc 100644 --- a/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java +++ b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java @@ -110,4 +110,11 @@ public void getSnapshotReturnsDecryptedValues() { assertTrue(result.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3"))); } + + @Test + public void clearCallsDeleteAllOnDao() { + mStorage.clear(); + + verify(mDao).deleteAll(); + } } diff --git a/src/test/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProviderTest.java b/main/src/test/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProviderTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProviderTest.java rename to main/src/test/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProviderTest.java diff --git a/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java b/main/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java rename to main/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java diff --git a/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImplTest.java b/main/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImplTest.java rename to main/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImplTest.java diff --git a/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java b/main/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java rename to main/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java diff --git a/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java b/main/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java rename to main/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java diff --git a/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java b/main/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java rename to main/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java diff --git a/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java b/main/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java rename to main/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java diff --git a/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java b/main/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java rename to main/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java diff --git a/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java b/main/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java rename to main/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java diff --git a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java b/main/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java rename to main/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java diff --git a/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java b/main/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java similarity index 100% rename from src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java rename to main/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java diff --git a/src/test/java/io/split/android/client/telemetry/TelemetryHelperImplTest.java b/main/src/test/java/io/split/android/client/telemetry/TelemetryHelperImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/telemetry/TelemetryHelperImplTest.java rename to main/src/test/java/io/split/android/client/telemetry/TelemetryHelperImplTest.java diff --git a/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java b/main/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java similarity index 100% rename from src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java rename to main/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java diff --git a/src/test/java/io/split/android/client/telemetry/TelemetrySynchronizerImplTest.java b/main/src/test/java/io/split/android/client/telemetry/TelemetrySynchronizerImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/telemetry/TelemetrySynchronizerImplTest.java rename to main/src/test/java/io/split/android/client/telemetry/TelemetrySynchronizerImplTest.java diff --git a/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java b/main/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java similarity index 100% rename from src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java rename to main/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java diff --git a/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java b/main/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java rename to main/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java diff --git a/src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java b/main/src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java rename to main/src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java diff --git a/src/test/java/io/split/android/client/utils/HttpProxySerializerTest.java b/main/src/test/java/io/split/android/client/utils/HttpProxySerializerTest.java similarity index 100% rename from src/test/java/io/split/android/client/utils/HttpProxySerializerTest.java rename to main/src/test/java/io/split/android/client/utils/HttpProxySerializerTest.java diff --git a/src/test/java/io/split/android/client/utils/JsonTest.java b/main/src/test/java/io/split/android/client/utils/JsonTest.java similarity index 100% rename from src/test/java/io/split/android/client/utils/JsonTest.java rename to main/src/test/java/io/split/android/client/utils/JsonTest.java diff --git a/src/test/java/io/split/android/client/utils/PartitionTest.java b/main/src/test/java/io/split/android/client/utils/PartitionTest.java similarity index 100% rename from src/test/java/io/split/android/client/utils/PartitionTest.java rename to main/src/test/java/io/split/android/client/utils/PartitionTest.java diff --git a/src/test/java/io/split/android/client/utils/RepeatTest.java b/main/src/test/java/io/split/android/client/utils/RepeatTest.java similarity index 100% rename from src/test/java/io/split/android/client/utils/RepeatTest.java rename to main/src/test/java/io/split/android/client/utils/RepeatTest.java diff --git a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java b/main/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java similarity index 96% rename from src/test/java/io/split/android/client/utils/SplitClientImplFactory.java rename to main/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java index bc8ab7410..50fec3e8f 100644 --- a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java +++ b/main/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java @@ -36,7 +36,7 @@ public class SplitClientImplFactory { public static SplitClientImpl get(Key key, SplitsStorage splitsStorage) { SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventsManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); + SplitEventsManager eventsManager = new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()); SplitParser splitParser = getSplitParser(); TelemetryStorage telemetryStorage = mock(TelemetryStorage.class); TreatmentManagerFactory treatmentManagerFactory = new TreatmentManagerFactoryImpl( @@ -73,7 +73,7 @@ public static SplitClientImpl get(Key key, ImpressionListener impressionListener splitParser, impressionListener, cfg, - new SplitEventsManager(cfg, new SplitTaskExecutorStub()), + new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()), mock(EventsTracker.class), mock(AttributesManager.class), mock(SplitValidator.class), diff --git a/src/test/java/io/split/android/client/utils/deserializer/EventDeserializerTest.java b/main/src/test/java/io/split/android/client/utils/deserializer/EventDeserializerTest.java similarity index 100% rename from src/test/java/io/split/android/client/utils/deserializer/EventDeserializerTest.java rename to main/src/test/java/io/split/android/client/utils/deserializer/EventDeserializerTest.java diff --git a/src/test/java/io/split/android/client/validators/ApiKeyValidatorTest.java b/main/src/test/java/io/split/android/client/validators/ApiKeyValidatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/ApiKeyValidatorTest.java rename to main/src/test/java/io/split/android/client/validators/ApiKeyValidatorTest.java diff --git a/src/test/java/io/split/android/client/validators/AttributesValidatorImplTest.java b/main/src/test/java/io/split/android/client/validators/AttributesValidatorImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/AttributesValidatorImplTest.java rename to main/src/test/java/io/split/android/client/validators/AttributesValidatorImplTest.java diff --git a/src/test/java/io/split/android/client/validators/EventTypeNameHelper.java b/main/src/test/java/io/split/android/client/validators/EventTypeNameHelper.java similarity index 100% rename from src/test/java/io/split/android/client/validators/EventTypeNameHelper.java rename to main/src/test/java/io/split/android/client/validators/EventTypeNameHelper.java diff --git a/src/test/java/io/split/android/client/validators/EventValidatorTest.java b/main/src/test/java/io/split/android/client/validators/EventValidatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/EventValidatorTest.java rename to main/src/test/java/io/split/android/client/validators/EventValidatorTest.java diff --git a/src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java b/main/src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java rename to main/src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java diff --git a/src/test/java/io/split/android/client/validators/KeyValidatorTest.java b/main/src/test/java/io/split/android/client/validators/KeyValidatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/KeyValidatorTest.java rename to main/src/test/java/io/split/android/client/validators/KeyValidatorTest.java diff --git a/src/test/java/io/split/android/client/validators/PrefixValidatorImplTest.java b/main/src/test/java/io/split/android/client/validators/PrefixValidatorImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/PrefixValidatorImplTest.java rename to main/src/test/java/io/split/android/client/validators/PrefixValidatorImplTest.java diff --git a/src/test/java/io/split/android/client/validators/SplitValidatorTest.java b/main/src/test/java/io/split/android/client/validators/SplitValidatorTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/SplitValidatorTest.java rename to main/src/test/java/io/split/android/client/validators/SplitValidatorTest.java diff --git a/src/test/java/io/split/android/client/validators/TreatmentManagerFactoryImplTest.java b/main/src/test/java/io/split/android/client/validators/TreatmentManagerFactoryImplTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/TreatmentManagerFactoryImplTest.java rename to main/src/test/java/io/split/android/client/validators/TreatmentManagerFactoryImplTest.java diff --git a/src/test/java/io/split/android/client/validators/TreatmentManagerFallbackTreatmentsTest.java b/main/src/test/java/io/split/android/client/validators/TreatmentManagerFallbackTreatmentsTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/TreatmentManagerFallbackTreatmentsTest.java rename to main/src/test/java/io/split/android/client/validators/TreatmentManagerFallbackTreatmentsTest.java diff --git a/src/test/java/io/split/android/client/validators/TreatmentManagerHelperTest.java b/main/src/test/java/io/split/android/client/validators/TreatmentManagerHelperTest.java similarity index 100% rename from src/test/java/io/split/android/client/validators/TreatmentManagerHelperTest.java rename to main/src/test/java/io/split/android/client/validators/TreatmentManagerHelperTest.java diff --git a/src/test/java/io/split/android/engine/ConditionsTestUtil.java b/main/src/test/java/io/split/android/engine/ConditionsTestUtil.java similarity index 100% rename from src/test/java/io/split/android/engine/ConditionsTestUtil.java rename to main/src/test/java/io/split/android/engine/ConditionsTestUtil.java diff --git a/src/test/java/io/split/android/engine/experiments/DefaultConditionsProviderTest.java b/main/src/test/java/io/split/android/engine/experiments/DefaultConditionsProviderTest.java similarity index 100% rename from src/test/java/io/split/android/engine/experiments/DefaultConditionsProviderTest.java rename to main/src/test/java/io/split/android/engine/experiments/DefaultConditionsProviderTest.java diff --git a/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java b/main/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java similarity index 100% rename from src/test/java/io/split/android/engine/experiments/EvaluatorTest.java rename to main/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java diff --git a/src/test/java/io/split/android/engine/experiments/PrerequisitesEvaluatorTest.java b/main/src/test/java/io/split/android/engine/experiments/PrerequisitesEvaluatorTest.java similarity index 100% rename from src/test/java/io/split/android/engine/experiments/PrerequisitesEvaluatorTest.java rename to main/src/test/java/io/split/android/engine/experiments/PrerequisitesEvaluatorTest.java diff --git a/src/test/java/io/split/android/engine/experiments/RuleBasedSegmentParserTest.java b/main/src/test/java/io/split/android/engine/experiments/RuleBasedSegmentParserTest.java similarity index 100% rename from src/test/java/io/split/android/engine/experiments/RuleBasedSegmentParserTest.java rename to main/src/test/java/io/split/android/engine/experiments/RuleBasedSegmentParserTest.java diff --git a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java b/main/src/test/java/io/split/android/engine/experiments/SplitParserTest.java similarity index 100% rename from src/test/java/io/split/android/engine/experiments/SplitParserTest.java rename to main/src/test/java/io/split/android/engine/experiments/SplitParserTest.java diff --git a/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java b/main/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java similarity index 100% rename from src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java rename to main/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java diff --git a/src/test/java/io/split/android/engine/matchers/AllKeysMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/AllKeysMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/AllKeysMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/AllKeysMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/AttributeMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/AttributeMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/AttributeMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/AttributeMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/BetweenMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/BetweenMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/BetweenMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/BetweenMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/BooleanMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/BooleanMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/BooleanMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/BooleanMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/CombiningMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/CombiningMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/CombiningMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/CombiningMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/EqualToMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/EqualToMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/EqualToMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/EqualToMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/GreaterThanOrEqualToMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/LessThanOrEqualToMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/LessThanOrEqualToMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/LessThanOrEqualToMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/LessThanOrEqualToMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/NegatableMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/NegatableMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/NegatableMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/NegatableMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/PrerequisitesMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/PrerequisitesMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/PrerequisitesMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/PrerequisitesMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/TransformersTest.java b/main/src/test/java/io/split/android/engine/matchers/TransformersTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/TransformersTest.java rename to main/src/test/java/io/split/android/engine/matchers/TransformersTest.java diff --git a/src/test/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/collections/ContainsAllOfSetMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/collections/EqualToSetMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/collections/EqualToSetMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/collections/EqualToSetMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/collections/EqualToSetMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/collections/PartOfSetMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/collections/PartOfSetMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/collections/PartOfSetMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/collections/PartOfSetMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/semver/BetweenSemverMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/semver/BetweenSemverMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/semver/BetweenSemverMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/semver/BetweenSemverMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/semver/EqualToSemverMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/semver/EqualToSemverMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/semver/EqualToSemverMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/semver/EqualToSemverMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/semver/GreaterThanOrEqualToSemverMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/semver/InListSemverMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/semver/InListSemverMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/semver/InListSemverMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/semver/InListSemverMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/semver/LessThanOrEqualToSemverMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/semver/SemverTest.java b/main/src/test/java/io/split/android/engine/matchers/semver/SemverTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/semver/SemverTest.java rename to main/src/test/java/io/split/android/engine/matchers/semver/SemverTest.java diff --git a/src/test/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/strings/ContainsAnyOfMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/strings/EndsWithAnyOfMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/strings/RegularExpressionMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/strings/RegularExpressionMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/strings/RegularExpressionMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/strings/RegularExpressionMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/strings/StartsWithAnyOfMatcherTest.java diff --git a/src/test/java/io/split/android/engine/matchers/strings/WhitelistMatcherTest.java b/main/src/test/java/io/split/android/engine/matchers/strings/WhitelistMatcherTest.java similarity index 100% rename from src/test/java/io/split/android/engine/matchers/strings/WhitelistMatcherTest.java rename to main/src/test/java/io/split/android/engine/matchers/strings/WhitelistMatcherTest.java diff --git a/src/test/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImplTest.java b/main/src/test/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImplTest.java similarity index 100% rename from src/test/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImplTest.java rename to main/src/test/java/io/split/android/engine/scheduler/PausableScheduledThreadPoolExecutorImplTest.java diff --git a/src/test/java/io/split/android/engine/splitter/HashConsistencyTest.java b/main/src/test/java/io/split/android/engine/splitter/HashConsistencyTest.java similarity index 100% rename from src/test/java/io/split/android/engine/splitter/HashConsistencyTest.java rename to main/src/test/java/io/split/android/engine/splitter/HashConsistencyTest.java diff --git a/src/test/java/io/split/android/engine/splitter/HashingTest.java b/main/src/test/java/io/split/android/engine/splitter/HashingTest.java similarity index 100% rename from src/test/java/io/split/android/engine/splitter/HashingTest.java rename to main/src/test/java/io/split/android/engine/splitter/HashingTest.java diff --git a/src/test/java/io/split/android/engine/splitter/MyHash.java b/main/src/test/java/io/split/android/engine/splitter/MyHash.java similarity index 100% rename from src/test/java/io/split/android/engine/splitter/MyHash.java rename to main/src/test/java/io/split/android/engine/splitter/MyHash.java diff --git a/src/test/java/io/split/android/engine/splitter/SplitterTest.java b/main/src/test/java/io/split/android/engine/splitter/SplitterTest.java similarity index 100% rename from src/test/java/io/split/android/engine/splitter/SplitterTest.java rename to main/src/test/java/io/split/android/engine/splitter/SplitterTest.java diff --git a/src/test/java/io/split/android/fake/ExecutorServiceMock.java b/main/src/test/java/io/split/android/fake/ExecutorServiceMock.java similarity index 100% rename from src/test/java/io/split/android/fake/ExecutorServiceMock.java rename to main/src/test/java/io/split/android/fake/ExecutorServiceMock.java diff --git a/src/test/java/io/split/android/fake/ImpressionListenerMock.java b/main/src/test/java/io/split/android/fake/ImpressionListenerMock.java similarity index 100% rename from src/test/java/io/split/android/fake/ImpressionListenerMock.java rename to main/src/test/java/io/split/android/fake/ImpressionListenerMock.java diff --git a/src/test/java/io/split/android/fake/MemoryUtilsNoMemoryStub.java b/main/src/test/java/io/split/android/fake/MemoryUtilsNoMemoryStub.java similarity index 100% rename from src/test/java/io/split/android/fake/MemoryUtilsNoMemoryStub.java rename to main/src/test/java/io/split/android/fake/MemoryUtilsNoMemoryStub.java diff --git a/src/test/java/io/split/android/fake/SplitEventsManagerStub.java b/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java similarity index 66% rename from src/test/java/io/split/android/fake/SplitEventsManagerStub.java rename to main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java index 49b372e7e..b8eb66b9f 100644 --- a/src/test/java/io/split/android/fake/SplitEventsManagerStub.java +++ b/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java @@ -1,7 +1,11 @@ package io.split.android.fake; +import androidx.annotation.Nullable; + +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.ListenableEventsManager; +import io.split.android.client.events.SplitEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitInternalEvent; @@ -18,7 +22,12 @@ public SplitEventExecutorResources getExecutorResources() { @Override public void notifyInternalEvent(SplitInternalEvent internalEvent) { + notifyInternalEvent(internalEvent, null); + } + @Override + public void notifyInternalEvent(SplitInternalEvent internalEvent, @Nullable EventMetadata metadata) { + // Stub implementation - does nothing } @Override @@ -33,4 +42,9 @@ public boolean eventAlreadyTriggered(SplitEvent event) { } return false; } + + @Override + public void registerEventListener(SplitEventListener listener) { + // Stub implementation - does nothing + } } diff --git a/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java b/main/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java similarity index 100% rename from src/test/java/io/split/android/fake/SplitTaskExecutorStub.java rename to main/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java diff --git a/src/test/java/io/split/android/fake/SseClientMock.java b/main/src/test/java/io/split/android/fake/SseClientMock.java similarity index 100% rename from src/test/java/io/split/android/fake/SseClientMock.java rename to main/src/test/java/io/split/android/fake/SseClientMock.java diff --git a/src/test/java/io/split/android/helpers/FileHelper.java b/main/src/test/java/io/split/android/helpers/FileHelper.java similarity index 100% rename from src/test/java/io/split/android/helpers/FileHelper.java rename to main/src/test/java/io/split/android/helpers/FileHelper.java diff --git a/src/test/java/io/split/android/helpers/ResourcesFileStorage.java b/main/src/test/java/io/split/android/helpers/ResourcesFileStorage.java similarity index 100% rename from src/test/java/io/split/android/helpers/ResourcesFileStorage.java rename to main/src/test/java/io/split/android/helpers/ResourcesFileStorage.java diff --git a/src/test/java/io/split/android/helpers/SplitHelper.java b/main/src/test/java/io/split/android/helpers/SplitHelper.java similarity index 100% rename from src/test/java/io/split/android/helpers/SplitHelper.java rename to main/src/test/java/io/split/android/helpers/SplitHelper.java diff --git a/src/test/java/io/split/android/http/SplitHttpHeadersBuilderTest.java b/main/src/test/java/io/split/android/http/SplitHttpHeadersBuilderTest.java similarity index 100% rename from src/test/java/io/split/android/http/SplitHttpHeadersBuilderTest.java rename to main/src/test/java/io/split/android/http/SplitHttpHeadersBuilderTest.java diff --git a/src/test/java/io/split/android/http/URIBuilderTest.java b/main/src/test/java/io/split/android/http/URIBuilderTest.java similarity index 100% rename from src/test/java/io/split/android/http/URIBuilderTest.java rename to main/src/test/java/io/split/android/http/URIBuilderTest.java diff --git a/src/test/resources/README.txt b/main/src/test/resources/README.txt similarity index 100% rename from src/test/resources/README.txt rename to main/src/test/resources/README.txt diff --git a/src/test/resources/between_semver.csv b/main/src/test/resources/between_semver.csv similarity index 100% rename from src/test/resources/between_semver.csv rename to main/src/test/resources/between_semver.csv diff --git a/src/test/resources/equal_to_semver.csv b/main/src/test/resources/equal_to_semver.csv similarity index 100% rename from src/test/resources/equal_to_semver.csv rename to main/src/test/resources/equal_to_semver.csv diff --git a/src/test/resources/impressions_1.json b/main/src/test/resources/impressions_1.json similarity index 100% rename from src/test/resources/impressions_1.json rename to main/src/test/resources/impressions_1.json diff --git a/src/test/resources/invalid_semantic_versions.csv b/main/src/test/resources/invalid_semantic_versions.csv similarity index 100% rename from src/test/resources/invalid_semantic_versions.csv rename to main/src/test/resources/invalid_semantic_versions.csv diff --git a/src/test/resources/legacy-hash-sample-data-non-alpha-numeric.csv b/main/src/test/resources/legacy-hash-sample-data-non-alpha-numeric.csv similarity index 100% rename from src/test/resources/legacy-hash-sample-data-non-alpha-numeric.csv rename to main/src/test/resources/legacy-hash-sample-data-non-alpha-numeric.csv diff --git a/src/test/resources/legacy-hash-sample-data.csv b/main/src/test/resources/legacy-hash-sample-data.csv similarity index 100% rename from src/test/resources/legacy-hash-sample-data.csv rename to main/src/test/resources/legacy-hash-sample-data.csv diff --git a/src/test/resources/murmur3-sample-data-non-alpha-numeric-v2.csv b/main/src/test/resources/murmur3-sample-data-non-alpha-numeric-v2.csv similarity index 100% rename from src/test/resources/murmur3-sample-data-non-alpha-numeric-v2.csv rename to main/src/test/resources/murmur3-sample-data-non-alpha-numeric-v2.csv diff --git a/src/test/resources/murmur3-sample-data-non-alpha-numeric.csv b/main/src/test/resources/murmur3-sample-data-non-alpha-numeric.csv similarity index 100% rename from src/test/resources/murmur3-sample-data-non-alpha-numeric.csv rename to main/src/test/resources/murmur3-sample-data-non-alpha-numeric.csv diff --git a/src/test/resources/murmur3-sample-data-v2.csv b/main/src/test/resources/murmur3-sample-data-v2.csv similarity index 100% rename from src/test/resources/murmur3-sample-data-v2.csv rename to main/src/test/resources/murmur3-sample-data-v2.csv diff --git a/src/test/resources/murmur3-sample-data.csv b/main/src/test/resources/murmur3-sample-data.csv similarity index 100% rename from src/test/resources/murmur3-sample-data.csv rename to main/src/test/resources/murmur3-sample-data.csv diff --git a/src/test/resources/murmur3-sample-double-treatment-users.csv b/main/src/test/resources/murmur3-sample-double-treatment-users.csv similarity index 100% rename from src/test/resources/murmur3-sample-double-treatment-users.csv rename to main/src/test/resources/murmur3-sample-double-treatment-users.csv diff --git a/src/test/resources/murmur3_64_uuids.csv b/main/src/test/resources/murmur3_64_uuids.csv similarity index 100% rename from src/test/resources/murmur3_64_uuids.csv rename to main/src/test/resources/murmur3_64_uuids.csv diff --git a/src/test/resources/split_changes_1.json b/main/src/test/resources/split_changes_1.json similarity index 100% rename from src/test/resources/split_changes_1.json rename to main/src/test/resources/split_changes_1.json diff --git a/src/test/resources/split_changes_1_updated.json b/main/src/test/resources/split_changes_1_updated.json similarity index 100% rename from src/test/resources/split_changes_1_updated.json rename to main/src/test/resources/split_changes_1_updated.json diff --git a/src/test/resources/split_changes_legacy.json b/main/src/test/resources/split_changes_legacy.json similarity index 100% rename from src/test/resources/split_changes_legacy.json rename to main/src/test/resources/split_changes_legacy.json diff --git a/src/test/resources/split_changes_prerequisites.json b/main/src/test/resources/split_changes_prerequisites.json similarity index 100% rename from src/test/resources/split_changes_prerequisites.json rename to main/src/test/resources/split_changes_prerequisites.json diff --git a/src/test/resources/split_changes_small.json b/main/src/test/resources/split_changes_small.json similarity index 100% rename from src/test/resources/split_changes_small.json rename to main/src/test/resources/split_changes_small.json diff --git a/src/test/resources/split_changes_with_prerequisites.json b/main/src/test/resources/split_changes_with_prerequisites.json similarity index 100% rename from src/test/resources/split_changes_with_prerequisites.json rename to main/src/test/resources/split_changes_with_prerequisites.json diff --git a/src/test/resources/splits.yaml b/main/src/test/resources/splits.yaml similarity index 100% rename from src/test/resources/splits.yaml rename to main/src/test/resources/splits.yaml diff --git a/src/test/resources/splits_incorrect_value.yaml b/main/src/test/resources/splits_incorrect_value.yaml similarity index 100% rename from src/test/resources/splits_incorrect_value.yaml rename to main/src/test/resources/splits_incorrect_value.yaml diff --git a/src/test/resources/splits_missing_name.yaml b/main/src/test/resources/splits_missing_name.yaml similarity index 100% rename from src/test/resources/splits_missing_name.yaml rename to main/src/test/resources/splits_missing_name.yaml diff --git a/src/test/resources/splits_missing_treatment.yaml b/main/src/test/resources/splits_missing_treatment.yaml similarity index 100% rename from src/test/resources/splits_missing_treatment.yaml rename to main/src/test/resources/splits_missing_treatment.yaml diff --git a/src/test/resources/splits_no_yaml.yaml b/main/src/test/resources/splits_no_yaml.yaml similarity index 100% rename from src/test/resources/splits_no_yaml.yaml rename to main/src/test/resources/splits_no_yaml.yaml diff --git a/src/test/resources/tracks_1.json b/main/src/test/resources/tracks_1.json similarity index 100% rename from src/test/resources/tracks_1.json rename to main/src/test/resources/tracks_1.json diff --git a/src/test/resources/valid_semantic_versions.csv b/main/src/test/resources/valid_semantic_versions.csv similarity index 100% rename from src/test/resources/valid_semantic_versions.csv rename to main/src/test/resources/valid_semantic_versions.csv diff --git a/project/build.gradle b/project/build.gradle deleted file mode 100644 index fd3ad14bc..000000000 --- a/project/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - google() - mavenCentral() - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} -plugins { - id 'org.sonarqube' version '3.2.0' -} - -sonarqube { - properties { - property 'sonar.projectKey', 'splitio_android-client' - property 'sonar.projectName', 'android-client' - } -} -allprojects { - repositories { - google() - mavenCentral() - jcenter() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/project/gradle/wrapper/gradle-wrapper.properties b/project/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 69ecd4abb..000000000 --- a/project/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sat Apr 22 10:53:48 ART 2023 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/project/gradlew b/project/gradlew deleted file mode 100755 index cccdd3d51..000000000 --- a/project/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/project/run_all_tests.sh b/project/run_all_tests.sh deleted file mode 100755 index 2c5e2c931..000000000 --- a/project/run_all_tests.sh +++ /dev/null @@ -1,5 +0,0 @@ -!#/bin/zsh - -./gradlew test -./gradlew connectedAndroidTest -open ./build/reports/androidTests/connected/index.html \ No newline at end of file diff --git a/project/settings.gradle b/project/settings.gradle deleted file mode 100644 index 643baca5a..000000000 --- a/project/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -rootProject.name = 'project' -include('android-client') -project(':android-client').projectDir=file('..') \ No newline at end of file diff --git a/scripts/compare-api-metalava.sh b/scripts/compare-api-metalava.sh new file mode 100755 index 000000000..fb66f243a --- /dev/null +++ b/scripts/compare-api-metalava.sh @@ -0,0 +1,726 @@ +#!/bin/bash + +# API Diff Tool using Metalava +# Compares public API surface between two Git branches using Metalava +# Usage: ./scripts/compare-api-metalava.sh --target --source + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +API_SIGNATURE_FILE="api.txt" +GRADLE_TASK_GENERATE_API="generateApi" +METALAVA_VERSION="1.0.0-alpha13" # Latest version from Maven +METALAVA_CACHE_DIR_RELATIVE=".metalava" +METALAVA_GROUP="com.android.tools.metalava" +METALAVA_ARTIFACT="metalava" +METALAVA_JAR_NAME="${METALAVA_ARTIFACT}-${METALAVA_VERSION}.jar" + +# These will be set after repo path is determined +METALAVA_CACHE_DIR="" +METALAVA_JAR="" + +# Temporary files +TEMP_DIR=$(mktemp -d) +CLONE_DIR="${TEMP_DIR}/repo" +TARGET_API="${TEMP_DIR}/target-branch-api.txt" +SOURCE_API="${TEMP_DIR}/source-branch-api.txt" + +# Optional: Save final API signature to a persistent location +SAVE_API_FILE=false +OUTPUT_API_FILE="" + +# Script arguments +TARGET_BRANCH="" +SOURCE_BRANCH="" +MODULE_PATH="" +REPO_PATH="" + +# Cleanup function - runs on exit, error, or interrupt +cleanup() { + local exit_code=$? + + # Always cleanup, even on success + if [ -d "$TEMP_DIR" ]; then + echo -e "\n${BLUE}Cleaning up temporary files...${NC}" + rm -rf "$TEMP_DIR" + echo "Removed temporary repository clone and API signature files" + fi + + # Only show error message if there was an error + if [ $exit_code -ne 0 ]; then + echo -e "${RED}Script failed with exit code $exit_code${NC}" + fi + + exit $exit_code +} + +# Set trap for cleanup on exit +trap cleanup EXIT INT TERM + +# Print error and exit +error_exit() { + echo -e "${RED}Error: $1${NC}" >&2 + exit 1 +} + +# Print usage +usage() { + cat << EOF +Usage: $0 --target --source [--module ] + +Options: + --target Target branch/tag/commit to compare against (e.g., main, origin/main, v1.0.0) + --source Source branch/tag/commit to compare (e.g., feature/my-feature, v1.1.0) + --module Optional: Module path (defaults to root, future-proof for multi-module) + --save-api Optional: Save the final API signature to a file (default: not saved) + +Examples: + # Compare branches + $0 --target origin/main --source feature/my-feature + + # Compare tags + $0 --target v1.0.0 --source v1.1.0 + + # Compare tag to branch + $0 --target v1.0.0 --source main + +Note: This script uses Metalava to generate API signature files that contain ONLY public API elements. +Private fields, methods, and internal implementation details are excluded. + +EOF + exit 1 +} + +# Parse command line arguments +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --target) + TARGET_BRANCH="$2" + shift 2 + ;; + --source) + SOURCE_BRANCH="$2" + shift 2 + ;; + --module) + MODULE_PATH="$2" + shift 2 + ;; + --save-api) + SAVE_API_FILE=true + OUTPUT_API_FILE="$2" + shift 2 + ;; + -h|--help) + usage + ;; + *) + error_exit "Unknown option: $1" + ;; + esac + done + + if [ -z "$TARGET_BRANCH" ] || [ -z "$SOURCE_BRANCH" ]; then + error_exit "Both --target and --source branches must be specified" + fi +} + +# Find the Gradle project root (where settings.gradle or build.gradle is located) +find_gradle_root() { + local current_dir="$PWD" + + # Check current directory first + if [ -f "settings.gradle" ] || [ -f "build.gradle" ]; then + echo "$PWD" + return 0 + fi + + # Check if we're in a subdirectory and need to go up + local check_dir="$current_dir" + while [ "$check_dir" != "/" ]; do + if [ -f "$check_dir/settings.gradle" ] || [ -f "$check_dir/build.gradle" ]; then + echo "$check_dir" + return 0 + fi + check_dir=$(dirname "$check_dir") + done + + # If no settings.gradle found, assume current directory + echo "$PWD" +} + +# Get repository path/URL +get_repo_path() { + echo -e "${BLUE}Detecting repository...${NC}" + + # Check if we're in a Git repository + if ! git rev-parse --git-dir > /dev/null 2>&1; then + error_exit "Not in a Git repository. Please run this script from within a Git repository." + fi + + # Get the repository root directory + REPO_PATH=$(git rev-parse --show-toplevel) + echo "Repository path: $REPO_PATH" + + echo -e "${GREEN}✓${NC} Repository detected" +} + +# Download and setup Metalava +setup_metalava() { + echo -e "${BLUE}Setting up Metalava...${NC}" + + # Use absolute path for cache directory (since we'll be working in temp dir) + if [ -z "$METALAVA_CACHE_DIR" ]; then + if [ -n "$REPO_PATH" ]; then + METALAVA_CACHE_DIR="$REPO_PATH/$METALAVA_CACHE_DIR_RELATIVE" + else + METALAVA_CACHE_DIR="$PWD/$METALAVA_CACHE_DIR_RELATIVE" + fi + METALAVA_JAR="$METALAVA_CACHE_DIR/$METALAVA_JAR_NAME" + fi + + # Create cache directory if it doesn't exist + mkdir -p "$METALAVA_CACHE_DIR" + + # Check if Metalava is already cached + if [ -f "$METALAVA_JAR" ]; then + # Verify it's a valid JAR file (ZIP format) + if unzip -t "$METALAVA_JAR" > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC} Metalava ${METALAVA_VERSION} found in cache" + return 0 + else + echo "Cached JAR appears invalid, re-downloading..." + rm -f "$METALAVA_JAR" + fi + fi + + # Check if Java is available (required for Metalava JAR) + if ! command -v java > /dev/null 2>&1; then + error_exit "Java is required to run Metalava. Please install Java." + fi + + # First, try to find Metalava in Gradle cache (might already be downloaded) + local gradle_user_home="${GRADLE_USER_HOME:-$HOME/.gradle}" + echo "Searching for Metalava in Gradle cache..." + if [ -d "$gradle_user_home/caches" ]; then + local cached_jar=$(find "$gradle_user_home/caches" -name "metalava*.jar" -type f 2>/dev/null | grep -v "sources" | grep -v "javadoc" | head -1) + if [ -n "$cached_jar" ] && [ -f "$cached_jar" ]; then + # Verify it's a valid JAR file (ZIP format) + if unzip -t "$cached_jar" > /dev/null 2>&1; then + echo "Found Metalava in Gradle cache, copying to script cache..." + cp "$cached_jar" "$METALAVA_JAR" + echo -e "${GREEN}✓${NC} Metalava ${METALAVA_VERSION} found in Gradle cache" + return 0 + fi + fi + fi + + # Download from Google Maven repository (maven.google.com) + echo "Downloading Metalava ${METALAVA_VERSION} from Google Maven repository..." + local group_path=$(echo "$METALAVA_GROUP" | tr '.' '/') + local maven_url="https://dl.google.com/dl/android/maven2/${group_path}/${METALAVA_ARTIFACT}/${METALAVA_VERSION}/${METALAVA_JAR_NAME}" + + echo "Downloading from: $maven_url" + if command -v curl > /dev/null 2>&1; then + if ! curl -L -f -o "$METALAVA_JAR" "$maven_url"; then + error_exit "Failed to download Metalava from $maven_url" + fi + elif command -v wget > /dev/null 2>&1; then + if ! wget -O "$METALAVA_JAR" "$maven_url"; then + error_exit "Failed to download Metalava from $maven_url" + fi + else + error_exit "Neither curl nor wget found. Please install one to download Metalava." + fi + + # Verify JAR download was successful (JAR should be > 100KB) + local jar_size + jar_size=$(stat -f%z "$METALAVA_JAR" 2>/dev/null || stat -c%s "$METALAVA_JAR" 2>/dev/null || echo "0") + if [ "$jar_size" -lt 100000 ]; then + error_exit "Downloaded JAR appears corrupted (${jar_size} bytes). Expected > 100KB." + fi + echo "Downloaded ${jar_size} bytes" + + # Verify the JAR is valid by checking if it's a valid ZIP file (JARs are ZIP files) + if ! unzip -t "$METALAVA_JAR" > /dev/null 2>&1; then + error_exit "Downloaded Metalava JAR appears to be invalid or corrupted (not a valid ZIP/JAR file)" + fi + + echo -e "${GREEN}✓${NC} Metalava ${METALAVA_VERSION} downloaded and ready" +} + +# Clone repository to temporary directory +clone_repo() { + echo -e "${BLUE}Cloning repository to temporary directory...${NC}" + + # Clone the repository (using file:// protocol for local repos) + # This creates a clean copy without affecting the original + if [ -d "$CLONE_DIR" ]; then + rm -rf "$CLONE_DIR" + fi + + echo "Cloning to: $CLONE_DIR" + # Use file:// protocol for local repository cloning + local repo_url + if [[ "$REPO_PATH" == /* ]]; then + # Absolute path - use file:// protocol + repo_url="file://$REPO_PATH" + else + # Relative path + repo_url="file://$(cd "$REPO_PATH" && pwd)" + fi + + # Clone with tags to ensure all tags are available + git clone --tags "$repo_url" "$CLONE_DIR" > /dev/null 2>&1 || error_exit "Failed to clone repository" + + # Fetch all remotes and tags from the original repo (in case of remote refs) + (cd "$CLONE_DIR" && git remote set-url origin "$repo_url" > /dev/null 2>&1 || true) + (cd "$CLONE_DIR" && git fetch --all --tags --prune > /dev/null 2>&1 || true) + + # Also fetch tags directly from the original repo to ensure we have all local tags + (cd "$CLONE_DIR" && git fetch "$repo_url" "+refs/tags/*:refs/tags/*" > /dev/null 2>&1 || true) + + echo -e "${GREEN}✓${NC} Repository cloned" +} + +# Add Metalava Gradle task to generate API signature +add_metalava_task() { + local build_gradle="$1" + local metalava_jar_path="$2" + + # Check if task already exists + if grep -q "task generateApiSignature" "$build_gradle" 2>/dev/null; then + return 0 + fi + + echo "Adding Metalava task to build.gradle..." + + # Escape the path for use in Groovy string + local escaped_jar_path=$(echo "$metalava_jar_path" | sed "s/'/\\\\'/g") + + # Append the task at the end of the file + # Also add repositories if not present + if ! grep -q "repositories" "$build_gradle" 2>/dev/null; then + cat >> "$build_gradle" << 'REPOS_EOF' + +repositories { + google() + mavenCentral() +} +REPOS_EOF + fi + + # Append the task at the end of the file + cat >> "$build_gradle" << METALAVA_TASK_EOF + +// Metalava API signature generation task (added by compare-api-metalava.sh) +configurations { + metalavaRuntime +} + +dependencies { + metalavaRuntime 'com.android.tools.metalava:metalava:1.0.0-alpha13' +} + +task generateApiSignature(type: JavaExec) { + dependsOn configurations.metalavaRuntime + + // Use Driver as the main class (this is the correct entry point for Metalava) + main = 'com.android.tools.metalava.Driver' + classpath = configurations.metalavaRuntime + + // Ensure compile classpath is resolved before running + doFirst { + try { + def compileClasspath = configurations.findByName("compileClasspath") + if (compileClasspath != null) { + compileClasspath.resolve() + } + } catch (Exception e) { + // Ignore if compileClasspath is not available + } + } + + // Get Android extension to find source directories and SDK information + doFirst { + def androidExtension = project.extensions.findByName('android') + if (androidExtension == null) { + throw new GradleException("Android extension not found") + } + + def apiFile = file("\${project.rootDir}/api.txt") + apiFile.parentFile.mkdirs() + + def sourceDirs = androidExtension.sourceSets.main.java.srcDirs + + // Build arguments for Metalava + def argsList = [ + "--api", apiFile.absolutePath, + "--source-path", sourceDirs.join(File.pathSeparator), + "--format=v2" + ] + + // Add compile SDK version if available (Metalava uses --compile-sdk-version, not --bootclasspath) + try { + def compileSdk = androidExtension.compileSdkVersion + if (compileSdk != null) { + // compileSdkVersion might be a string like "33" or an integer + def sdkVersion = compileSdk.toString() + argsList.add("--compile-sdk-version") + argsList.add(sdkVersion) + } + } catch (Exception e) { + // compileSdkVersion might not be accessible, try compileSdk + try { + def compileSdk = androidExtension.compileSdk + if (compileSdk != null) { + def sdkVersion = compileSdk.toString() + argsList.add("--compile-sdk-version") + argsList.add(sdkVersion) + } + } catch (Exception e2) { + // If we can't get compile SDK, Metalava might be able to infer it + // or we can try to get it from the project's build.gradle + } + } + + // Try to get SDK home from environment or Android extension + def sdkHome = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") + if (sdkHome != null && new File(sdkHome).exists()) { + argsList.add("--sdk-home") + argsList.add(sdkHome) + } + + // Build classpath from project dependencies to help resolve imports + def classpathEntries = [] + try { + // Add boot classpath if available + def bootClasspath = androidExtension.bootClasspath + if (bootClasspath != null && !bootClasspath.isEmpty()) { + classpathEntries.addAll(bootClasspath) + } + } catch (Exception e) { + // bootClasspath might not be accessible in newer Android Gradle Plugin versions + } + + // Add compile classpath from configurations to help resolve dependencies + try { + def compileClasspath = configurations.findByName("compileClasspath") + if (compileClasspath != null) { + compileClasspath.resolve().each { file -> + if (file.exists() && file.name.endsWith('.jar')) { + classpathEntries.add(file.absolutePath) + } + } + } + } catch (Exception e) { + // compileClasspath might not be available + } + + // Add the classpath if we have entries + if (!classpathEntries.isEmpty()) { + argsList.add("--classpath") + argsList.add(classpathEntries.join(File.pathSeparator)) + } + + // Suppress API lint errors that are causing failures + // These are informational and don't prevent API signature generation + argsList.add("--hide") + argsList.add("DeprecationMismatch") + argsList.add("--hide") + argsList.add("ReferencesHidden") + argsList.add("--hide") + argsList.add("HiddenTypeParameter") + argsList.add("--hide") + argsList.add("UnresolvedImport") + + args = argsList + } +} +METALAVA_TASK_EOF +} + +# Generate API signature file for a specific branch/tag/commit +generate_api_signature() { + local ref="$1" + local output_api="$2" + + echo -e "\n${BLUE}Generating API signature for ref: ${ref}${NC}" + + # Change to cloned repository directory + cd "$CLONE_DIR" || error_exit "Failed to change to cloned repository directory" + + # Verify ref exists (handles branches, tags, and commits) + if ! git rev-parse --verify "$ref" > /dev/null 2>&1; then + # If that fails, try fetching the ref from origin + echo "Ref not found locally, attempting to fetch..." + git fetch origin "$ref:$ref" > /dev/null 2>&1 || true + # Also try fetching as a tag + if [[ "$ref" =~ ^[0-9] ]]; then + git fetch origin "refs/tags/$ref:refs/tags/$ref" > /dev/null 2>&1 || true + fi + # Try again + if ! git rev-parse --verify "$ref" > /dev/null 2>&1; then + # List available tags for debugging + echo -e "${YELLOW}Available tags:${NC}" + git tag | grep -E "^5\.[34]" | head -10 || git tag | tail -10 + error_exit "Ref does not exist: $ref (must be a branch, tag, or commit)" + fi + fi + + # Checkout the ref (works for branches, tags, and commits) + echo "Checking out ref: $ref" + if ! git checkout "$ref" 2>/dev/null; then + # Try with -f flag for tags that might have conflicts + if ! git checkout -f "$ref" 2>/dev/null; then + error_exit "Failed to checkout ref: $ref" + fi + fi + + # Find the Gradle project root after checkout + local gradle_root + gradle_root=$(find_gradle_root) + + # Check for Gradle wrapper in the project root + local gradlew_path + if [ -f "$gradle_root/gradlew" ]; then + gradlew_path="$gradle_root/gradlew" + elif [ -f "./gradlew" ]; then + gradlew_path="./gradlew" + else + error_exit "Gradle wrapper (gradlew) not found. Expected in: $gradle_root or current directory" + fi + + # Make gradlew executable + chmod +x "$gradlew_path" 2>/dev/null || true + + # Determine the working directory for gradle + local gradle_cmd + if [ "$gradle_root" != "$PWD" ]; then + gradle_cmd="$gradlew_path" + echo "Using Gradle wrapper from: $gradle_root" + else + gradle_cmd="./gradlew" + fi + + # Handle parent settings.gradle interference + local temp_settings_created=false + if [ -f "build.gradle" ] && [ ! -f "settings.gradle" ]; then + local parent_settings=$(dirname "$PWD")/settings.gradle + if [ -f "$parent_settings" ]; then + echo "rootProject.name = 'android-client'" > "settings.gradle" + temp_settings_created=true + echo "Created temporary settings.gradle to isolate build from parent" + fi + fi + + # Verify Metalava JAR is available + if [ ! -f "$METALAVA_JAR" ]; then + error_exit "Metalava JAR not found at: $METALAVA_JAR. Please run setup_metalava first." + fi + + # Add Metalava task to build.gradle if needed + local build_gradle_file + if [ -f "$gradle_root/build.gradle" ]; then + build_gradle_file="$gradle_root/build.gradle" + elif [ -f "build.gradle" ]; then + build_gradle_file="build.gradle" + else + error_exit "build.gradle not found" + fi + + # Add the Metalava task to build.gradle + add_metalava_task "$build_gradle_file" "$METALAVA_JAR" + + # Generate API signature using the Gradle task + echo "Generating API signature using Metalava..." + + # First, try to compile the project to ensure source is valid + echo "Compiling project..." + if ! "$gradle_cmd" compileReleaseJavaWithJavac --quiet --no-daemon > /dev/null 2>&1; then + echo -e "${YELLOW}Warning: Compilation had issues, but continuing with API generation...${NC}" + fi + + # Generate API signature using our custom task + echo "Running generateApiSignature task..." + if ! "$gradle_cmd" generateApiSignature --quiet --no-daemon; then + error_exit "Failed to generate API signature using Metalava" + fi + + # Find the generated API signature file + local api_path="" + local possible_paths=( + "$gradle_root/$API_SIGNATURE_FILE" + "./$API_SIGNATURE_FILE" + "api.txt" + ) + + for path in "${possible_paths[@]}"; do + if [ -f "$path" ]; then + api_path="$path" + break + fi + done + + if [ -z "$api_path" ] || [ ! -f "$api_path" ]; then + echo -e "${YELLOW}Warning: API signature file not found. Attempting to create a basic one...${NC}" + echo -e "${YELLOW}Note: You may need to configure Metalava in your build.gradle manually.${NC}" + echo -e "${YELLOW}Add this to your android block:${NC}" + echo "" + echo " metalava {" + echo " generateSignature = true" + echo " signatureOutputDirectory = project.rootDir" + echo " filename = \"api.txt\"" + echo " }" + echo "" + + # Create an empty API file as fallback (so script doesn't fail) + touch "$gradle_root/$API_SIGNATURE_FILE" + api_path="$gradle_root/$API_SIGNATURE_FILE" + fi + + echo "Found API signature at: $api_path" + + # Copy API signature to temp location + cp "$api_path" "$output_api" + echo -e "${GREEN}✓${NC} API signature generated and copied: $(basename "$output_api")" + + # Clean up temporary settings.gradle if we created one + if [ "$temp_settings_created" = true ] && [ -f "settings.gradle" ]; then + rm -f "settings.gradle" + fi +} + +# Colorize diff output +colorize_diff() { + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ---*|+++*) + # File headers - blue + echo -e "${BLUE}${line}${NC}" + ;; + -" "*) + # Removed lines - red + echo -e "${RED}${line}${NC}" + ;; + +" "*) + # Added lines - green + echo -e "${GREEN}${line}${NC}" + ;; + @@*) + # Hunk headers - yellow + echo -e "${YELLOW}${line}${NC}" + ;; + *) + # Context lines - no color + echo "$line" + ;; + esac + done +} + +# Compare API signature files +compare_api_signatures() { + local old_api="$1" + local new_api="$2" + + if [ ! -f "$old_api" ]; then + error_exit "Target API signature not found: $old_api" + fi + + if [ ! -f "$new_api" ]; then + error_exit "Source API signature not found: $new_api" + fi + + echo -e "\n${BLUE}Comparing API signatures...${NC}" + echo "OLD: $(basename "$old_api") (target: $TARGET_BRANCH)" + echo "NEW: $(basename "$new_api") (source: $SOURCE_BRANCH)" + echo "" + echo -e "${GREEN}Note: Only public API changes are shown. Private fields and methods are excluded.${NC}" + echo "" + + # Use diff to show the differences + # Use unified diff format for better readability + if command -v diff > /dev/null 2>&1; then + # Run diff directly and pipe through colorize_diff + # This ensures we always see the output + if diff -u "$old_api" "$new_api" 2>&1 | colorize_diff; then + # If diff returns 0 (no differences), colorize_diff will have shown nothing + # Check if files are actually identical + if cmp -s "$old_api" "$new_api" 2>/dev/null; then + echo -e "${GREEN}No public API changes detected.${NC}" + fi + else + # diff returned non-zero, meaning there are differences + # The diff output was already shown by colorize_diff above + echo -e "\n${YELLOW}Public API changes detected (see diff above).${NC}" + fi + else + error_exit "diff command not found. Please install diff utility." + fi +} + +# Main execution +main() { + echo -e "${GREEN}API Diff Tool (Metalava)${NC}" + echo "==============================" + echo "" + echo -e "${BLUE}This tool compares ONLY public API changes using Metalava.${NC}" + echo -e "${BLUE}Private fields, methods, and internal implementation details are excluded.${NC}" + echo "" + + # Parse arguments + parse_args "$@" + + # Get repository path + get_repo_path + + # Setup Metalava (use absolute path for cache since we'll be in temp dir) + setup_metalava + + # Clone repository to temporary directory + clone_repo + + # Generate source branch API signature first (newer version) + generate_api_signature "$SOURCE_BRANCH" "$SOURCE_API" + + # Generate target branch API signature second (older version) + generate_api_signature "$TARGET_BRANCH" "$TARGET_API" + + # Compare API signatures + # Note: We show OLD -> NEW, so we pass target (old) first, then source (new) + compare_api_signatures "$TARGET_API" "$SOURCE_API" + + # Show file locations + echo -e "\n${BLUE}API Signature File Locations:${NC}" + echo "Target (old) API: $TARGET_API" + echo "Source (new) API: $SOURCE_API" + + # Save final API signature if requested + if [ "$SAVE_API_FILE" = true ]; then + if [ -n "$OUTPUT_API_FILE" ]; then + cp "$SOURCE_API" "$OUTPUT_API_FILE" + echo -e "\n${GREEN}✓${NC} Final API signature saved to: $OUTPUT_API_FILE" + else + # Default: save to current directory with timestamp + local timestamp=$(date +%Y%m%d_%H%M%S) + OUTPUT_API_FILE="api-${SOURCE_BRANCH//\//_}-${timestamp}.txt" + cp "$SOURCE_API" "$OUTPUT_API_FILE" + echo -e "\n${GREEN}✓${NC} Final API signature saved to: $OUTPUT_API_FILE" + fi + fi + + echo -e "\n${GREEN}✓${NC} API comparison completed successfully" +} + +# Run main function +main "$@" + diff --git a/scripts/diffuse.sh b/scripts/diffuse.sh new file mode 100755 index 000000000..c8842919f --- /dev/null +++ b/scripts/diffuse.sh @@ -0,0 +1,762 @@ +#!/bin/bash + +# API Diff Tool +# Compares public API surface between two Git branches using Diffuse +# Usage: ./scripts/diffuse.sh --target --source [--verbose] + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +DIFFUSE_VERSION="0.3.0" +DIFFUSE_CACHE_DIR_RELATIVE=".diffuse" +DIFFUSE_URL="https://github.com/JakeWharton/diffuse/releases/download/${DIFFUSE_VERSION}/diffuse-${DIFFUSE_VERSION}.zip" + +# These will be set after repo path is determined +DIFFUSE_CACHE_DIR="" +DIFFUSE_BINARY="" + +# AAR configuration (fused library output at root) +AAR_OUTPUT_DIR="build/outputs/aar" +# Prefer exact fused name; also support variant-suffixed names +AAR_CANDIDATE_NAMES=("android-client.aar" "android-client-release.aar" "android-client-debug.aar") +# Candidate Gradle tasks in order of preference; pick the first that exists +# Prefer 'bundle' for fused plugin roots, then legacy variant-specific bundles +GRADLE_TASK_CANDIDATES=("bundle" "bundleReleaseAar" "assembleRelease") + +# Temporary files +TEMP_DIR=$(mktemp -d) +SOURCE_CLONE_DIR="${TEMP_DIR}/source-repo" +TARGET_CLONE_DIR="${TEMP_DIR}/target-repo" +TARGET_AAR="${TEMP_DIR}/target-branch.aar" +SOURCE_AAR="${TEMP_DIR}/source-branch.aar" + +# Script arguments +TARGET_BRANCH="" +SOURCE_BRANCH="" +MODULE_PATH="" +REPO_PATH="" +VERBOSE=false +FULL=false + +# Cleanup function - runs on exit, error, or interrupt +cleanup() { + local exit_code=$? + + # Always cleanup, even on success + if [ -d "$TEMP_DIR" ]; then + [ "$VERBOSE" = true ] && echo -e "\n${BLUE}Cleaning up temporary files...${NC}" + rm -rf "$TEMP_DIR" 2>/dev/null || true + [ "$VERBOSE" = true ] && echo "Removed temporary repository clone and AAR files" + fi + + # Only show error message if there was an error + if [ $exit_code -ne 0 ]; then + echo -e "${RED}Script failed with exit code $exit_code${NC}" >&2 + fi + + exit $exit_code +} + +# Set trap for cleanup on exit +trap cleanup EXIT INT TERM + +# Print error and exit +error_exit() { + echo -e "${RED}Error: $1${NC}" >&2 + exit 1 +} + +# Verbose logging +log_verbose() { + if [ "$VERBOSE" = true ]; then + echo -e "${BLUE}$1${NC}" + fi +} + +# Print usage +usage() { + cat << EOF +Usage: $0 --target --source [--module ] [--verbose|--full] + +Options: + --target Target branch/tag/commit to compare against (e.g., main, origin/main, v1.0.0) + --source Source branch/tag/commit to compare (e.g., feature/my-feature, v1.1.0) + --module Optional: Module path (defaults to root, future-proof for multi-module) + --verbose Show verbose output including build logs and progress messages + --full Show complete Diffuse output including detailed class/method/field changes + +Examples: + # Compare branches + $0 --target origin/main --source feature/my-feature + + # Compare tags + $0 --target v1.0.0 --source v1.1.0 + + # Compare with verbose output (shows build logs) + $0 --target v1.0.0 --source main --verbose + + # Compare with full Diffuse output (shows all details) + $0 --target v1.0.0 --source main --full + +EOF + exit 1 +} + +# Parse command line arguments +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --target) + TARGET_BRANCH="$2" + shift 2 + ;; + --source) + SOURCE_BRANCH="$2" + shift 2 + ;; + --module) + MODULE_PATH="$2" + shift 2 + ;; + --verbose) + VERBOSE=true + shift + ;; + --full) + FULL=true + shift + ;; + -h|--help) + usage + ;; + *) + error_exit "Unknown option: $1" + ;; + esac + done + + if [ -z "$TARGET_BRANCH" ] || [ -z "$SOURCE_BRANCH" ]; then + error_exit "Both --target and --source branches must be specified" + fi +} + +# Find the Gradle project root (where settings.gradle or build.gradle is located) +find_gradle_root() { + local current_dir="$PWD" + + # Check current directory first + if [ -f "settings.gradle" ] || [ -f "build.gradle" ]; then + echo "$PWD" + return 0 + fi + + # Check if we're in a subdirectory and need to go up + local check_dir="$current_dir" + while [ "$check_dir" != "/" ]; do + if [ -f "$check_dir/settings.gradle" ] || [ -f "$check_dir/build.gradle" ]; then + echo "$check_dir" + return 0 + fi + check_dir=$(dirname "$check_dir") + done + + # If no settings.gradle found, assume current directory + echo "$PWD" +} + +# Get repository path/URL +get_repo_path() { + log_verbose "Detecting repository..." + + # Check if we're in a Git repository + if ! git rev-parse --git-dir > /dev/null 2>&1; then + error_exit "Not in a Git repository. Please run this script from within a Git repository." + fi + + # Get the repository root directory + set +eu # Temporarily disable exit on error and unset variable check + set +o pipefail + REPO_PATH=$(git rev-parse --show-toplevel 2>/dev/null) + local git_exit=$? + set -eu # Re-enable exit on error and unset variable check + set -o pipefail + + # Check if git command failed or result is empty + if [ $git_exit -ne 0 ] || [ -z "${REPO_PATH:-}" ]; then + error_exit "Failed to determine repository root directory" + fi + + log_verbose "Repository path: $REPO_PATH" + + log_verbose "✓ Repository detected" +} + +# Clone repository to temporary directory +clone_repo() { + local clone_dir="$1" + local label="$2" + + log_verbose "Cloning repository to temporary directory ($label)..." + + # Clone the repository (using file:// protocol for local repos) + # This creates a clean copy without affecting the original + if [ -d "$clone_dir" ]; then + rm -rf "$clone_dir" + fi + + log_verbose "Cloning to: $clone_dir" + # Use file:// protocol for local repository cloning + local repo_url + if [[ "$REPO_PATH" == /* ]]; then + # Absolute path - use file:// protocol + repo_url="file://$REPO_PATH" + else + # Relative path + repo_url="file://$(cd "$REPO_PATH" && pwd)" + fi + + git clone --tags "$repo_url" "$clone_dir" > /dev/null 2>&1 || error_exit "Failed to clone repository for $label" + + # After cloning from local repo, we need to fetch from the actual remote origin + # to ensure all remote branches are properly available + ( + cd "$clone_dir" || error_exit "Failed to enter clone dir for $label" + + # Get the actual remote origin URL from the source repo + ACTUAL_ORIGIN=$(cd "$REPO_PATH" && git remote get-url origin 2>/dev/null || echo "") + + if [ -n "$ACTUAL_ORIGIN" ]; then + log_verbose "Fetching from actual remote origin: $ACTUAL_ORIGIN" + # Update origin to point to the actual remote + git remote set-url origin "$ACTUAL_ORIGIN" > /dev/null 2>&1 || true + else + log_verbose "Using local repository as origin: $repo_url" + git remote set-url origin "$repo_url" > /dev/null 2>&1 || true + fi + + # Fetch branches and tags for origin using default refspec + git fetch origin --prune --tags > /dev/null 2>&1 || true + ) + + log_verbose "✓ Repository cloned for $label" +} + +# Download and setup Diffuse +setup_diffuse() { + log_verbose "Setting up Diffuse..." + + # Use absolute path for cache directory (since we'll be working in temp dir) + if [ -z "$DIFFUSE_CACHE_DIR" ]; then + if [ -n "$REPO_PATH" ]; then + DIFFUSE_CACHE_DIR="$REPO_PATH/$DIFFUSE_CACHE_DIR_RELATIVE" + else + DIFFUSE_CACHE_DIR="$PWD/$DIFFUSE_CACHE_DIR_RELATIVE" + fi + fi + + # Set the expected binary path + DIFFUSE_BINARY="${DIFFUSE_CACHE_DIR}/diffuse-${DIFFUSE_VERSION}/bin/diffuse" + + # Create cache directory if it doesn't exist + mkdir -p "$DIFFUSE_CACHE_DIR" + + # Check if Diffuse is already cached and valid (binary should exist and be executable) + if [ -f "$DIFFUSE_BINARY" ] && [ -x "$DIFFUSE_BINARY" ]; then + log_verbose "✓ Diffuse ${DIFFUSE_VERSION} found in cache" + return 0 + fi + + # Check if Java is available (required for Diffuse JAR) + if ! command -v java > /dev/null 2>&1; then + error_exit "Java is required to run Diffuse. Please install Java." + fi + + log_verbose "Downloading Diffuse ${DIFFUSE_VERSION} from GitHub..." + + # Download Diffuse ZIP (releases are distributed as ZIP files) + local diffuse_zip="${DIFFUSE_CACHE_DIR}/diffuse-${DIFFUSE_VERSION}.zip" + log_verbose "Downloading from: $DIFFUSE_URL" + if command -v curl > /dev/null 2>&1; then + if ! curl -L -f -o "$diffuse_zip" "$DIFFUSE_URL"; then + error_exit "Failed to download Diffuse from $DIFFUSE_URL" + fi + elif command -v wget > /dev/null 2>&1; then + if ! wget -O "$diffuse_zip" "$DIFFUSE_URL"; then + error_exit "Failed to download Diffuse from $DIFFUSE_URL" + fi + else + error_exit "Neither curl nor wget found. Please install one to download Diffuse." + fi + + # Verify ZIP download was successful (ZIP should be > 100KB) + local zip_size + zip_size=$(stat -f%z "$diffuse_zip" 2>/dev/null || stat -c%s "$diffuse_zip" 2>/dev/null || echo "0") + if [ "$zip_size" -lt 100000 ]; then + error_exit "Downloaded ZIP appears corrupted (${zip_size} bytes). Expected > 100KB." + fi + log_verbose "Downloaded ${zip_size} bytes" + + # Extract JAR from ZIP + log_verbose "Extracting JAR from ZIP..." + if ! command -v unzip > /dev/null 2>&1; then + error_exit "unzip is required to extract Diffuse. Please install unzip." + fi + + # Extract to a temporary directory first + local extract_dir="${DIFFUSE_CACHE_DIR}/extract" + rm -rf "$extract_dir" + mkdir -p "$extract_dir" + + if ! unzip -q "$diffuse_zip" -d "$extract_dir"; then + error_exit "Failed to extract Diffuse ZIP" + fi + + # Find the executable script in the extracted contents + local extracted_bin + extracted_bin=$(find "$extract_dir" -path "*/bin/diffuse" -type f | head -1) + if [ -z "$extracted_bin" ] || [ ! -f "$extracted_bin" ]; then + error_exit "Diffuse executable not found in downloaded ZIP" + fi + + # Copy the entire diffuse directory structure to cache + local diffuse_extracted_dir + diffuse_extracted_dir=$(dirname "$(dirname "$extracted_bin")") + local diffuse_cache_extracted="${DIFFUSE_CACHE_DIR}/diffuse-${DIFFUSE_VERSION}" + + # Remove old extracted directory if it exists + rm -rf "$diffuse_cache_extracted" + + # Copy the extracted directory + cp -r "$diffuse_extracted_dir" "$diffuse_cache_extracted" + + # The binary should be at bin/diffuse relative to the extracted directory + DIFFUSE_BINARY="${diffuse_cache_extracted}/bin/diffuse" + + # Make sure it's executable + chmod +x "$DIFFUSE_BINARY" + + # Clean up temporary extraction directory and ZIP + rm -rf "$extract_dir" "$diffuse_zip" + + # Verify the binary exists and is executable + if [ ! -f "$DIFFUSE_BINARY" ] || [ ! -x "$DIFFUSE_BINARY" ]; then + error_exit "Diffuse binary not found or not executable at: $DIFFUSE_BINARY" + fi + log_verbose "Extracted Diffuse binary: $DIFFUSE_BINARY" + + log_verbose "✓ Diffuse ${DIFFUSE_VERSION} downloaded and ready" +} + +# Run Diffuse command +run_diffuse() { + local old_aar="$1" + local new_aar="$2" + + if [ ! -f "$old_aar" ]; then + error_exit "Target AAR not found: $old_aar" + fi + + if [ ! -f "$new_aar" ]; then + error_exit "Source AAR not found: $new_aar" + fi + + # Compare AAR files directly using --aar flag + # Filter output to show only summary tables (size differences including JAR table) + # Skip detailed lists of classes, methods, and fields + local temp_output + temp_output=$(mktemp) + local saved_log="${DIFFUSE_CACHE_DIR}/last-diffuse.log" + + # Run Diffuse and capture output + # Note: Diffuse may return non-zero exit code when differences are found (which is expected) + # So we capture the output regardless of exit code + local diffuse_exit_code=0 + set +e # Temporarily disable exit on error to capture exit code + set +o pipefail + "$DIFFUSE_BINARY" diff --aar "$old_aar" "$new_aar" > "$temp_output" 2>&1 + diffuse_exit_code=$? + set -e # Re-enable exit on error + set -o pipefail + # Save full raw log for debugging + cp "$temp_output" "$saved_log" >/dev/null 2>&1 || true + + # Check if the output contains actual errors (not just differences) + if [ $diffuse_exit_code -ne 0 ]; then + # Always print the full Diffuse output on non-zero exit to aid debugging + echo -e "${YELLOW}Diffuse exited with code ${diffuse_exit_code}. Full log saved at: ${saved_log}${NC}" >&2 + cat "$temp_output" >&2 || true + fi + + # In verbose or full mode, show full output; otherwise filter to show only key tables + if [ "$VERBOSE" = true ] || [ "$FULL" = true ]; then + # Show complete unfiltered Diffuse output + cat "$temp_output" || true + else + # Filter to show only the tables we care about: + # - AAR summary table + # - JAR summary table + # - AAR file list section + # Skip: file paths, hashes, MANIFEST section, CLASSES/METHODS/FIELDS details + set +e # Temporarily disable exit on error for awk + set +o pipefail + awk ' + BEGIN { + in_aar_table = 0 + in_jar_table = 0 + in_aar_section = 0 + skip_until_next_section = 0 + } + + # Skip OLD/NEW file path lines with hashes + /^OLD: .*\.aar \(.*bytes.*sha256:/ { next } + /^NEW: .*\.aar \(.*bytes.*sha256:/ { next } + + # Skip simple OLD:/NEW: filename lines + /^OLD: [^\/]*\.aar$/ { next } + /^NEW: [^\/]*\.aar$/ { next } + + # Stop at detailed sections + /^CLASSES:$/ { exit 0 } + /^METHODS:$/ { exit 0 } + /^FIELDS:$/ { exit 0 } + + # Detect AAR summary table start + / AAR / && /old/ && /new/ && /diff/ { + in_aar_table = 1 + print + next + } + + # Detect JAR summary table start + / JAR / && /old/ && /new/ && /diff/ { + in_jar_table = 1 + print + next + } + + # Continue printing AAR table until blank line + in_aar_table == 1 { + print + if ($0 == "") { + in_aar_table = 0 + } + next + } + + # Continue printing JAR table until blank line + in_jar_table == 1 { + print + if ($0 == "") { + in_jar_table = 0 + } + next + } + + # Handle section headers (=====) + /^=+$/ { + # Save the separator line + sep_line = $0 + # Read the next line to see what section it is + if (getline > 0) { + if ($0 ~ /====.*AAR.*====/) { + # Start of AAR file list section + in_aar_section = 1 + skip_until_next_section = 0 + print sep_line + print + # Read and print the next separator line (there are 2 around the header) + if (getline > 0 && $0 ~ /^=+$/) { + print + } + } else if ($0 ~ /====.*MANIFEST.*====/) { + # Skip MANIFEST section + skip_until_next_section = 1 + } else if ($0 ~ /====.*JAR.*====/ && !in_aar_section) { + # Skip final JAR section (after MANIFEST) + skip_until_next_section = 1 + } + } + next + } + + # Continue printing AAR section content + in_aar_section == 1 { + # Check if this is the "(total)" line (end of AAR section) + if ($0 ~ /\(total\)/) { + print + in_aar_section = 0 + skip_until_next_section = 1 # Skip everything until we see CLASSES/METHODS/FIELDS or EOF + next + } + print + next + } + + # Skip lines when we are skipping a section + skip_until_next_section == 1 { + next + } + ' "$temp_output" || true + set -e # Re-enable exit on error + set -o pipefail + fi + + # Clean up + rm -f "$temp_output" || true + + # Return success (even if Diffuse found differences, that's a successful comparison) + return 0 +} + +# Build AAR for a specific branch/tag/commit +build_aar() { + local clone_dir="$1" + local ref="$2" + local output_aar="$3" + + log_verbose "Building AAR for ref: ${ref}" + + # Change to cloned repository directory + cd "$clone_dir" || error_exit "Failed to change to cloned repository directory" + + # Verify ref exists (handles branches, tags, and commits) + # At this point clone_repo has already fetched from origin, so the + # ref should exist if it exists on the remote. + if ! git rev-parse --verify "$ref" > /dev/null 2>&1; then + error_exit "Ref does not exist in cloned repo: $ref (ensure this branch/tag/SHA exists on origin)" + fi + + # Checkout the ref (works for branches, tags, and commits) + log_verbose "Checking out ref: $ref" + if ! git checkout "$ref" >/dev/null 2>&1; then + # Try with -f flag for tags that might have conflicts + if ! git checkout -f "$ref" >/dev/null 2>&1; then + error_exit "Failed to checkout ref: $ref" + fi + fi + # Log resolved commit for traceability + if git rev-parse --short HEAD >/dev/null 2>&1; then + RESOLVED_SHA="$(git rev-parse --short HEAD || true)" + log_verbose "Resolved ref '$ref' to commit: $RESOLVED_SHA" + fi + + # Find the Gradle project root after checkout (structure might be different) + local gradle_root + gradle_root=$(find_gradle_root) + + # Check for Gradle wrapper in the project root + local gradlew_path + if [ -f "$gradle_root/gradlew" ]; then + gradlew_path="$gradle_root/gradlew" + elif [ -f "./gradlew" ]; then + gradlew_path="./gradlew" + else + error_exit "Gradle wrapper (gradlew) not found. Expected in: $gradle_root or current directory" + fi + + # Make gradlew executable + chmod +x "$gradlew_path" 2>/dev/null || true + + # Determine the working directory for gradle + # If gradle root is different, we might need to run from there or use -p flag + local gradle_cmd + if [ "$gradle_root" != "$PWD" ]; then + # Run from gradle root, but need to specify the project + # For now, try running from current directory with the gradlew from root + gradle_cmd="$gradlew_path" + log_verbose "Using Gradle wrapper from: $gradle_root" + else + gradle_cmd="./gradlew" + fi + + # Handle parent settings.gradle interference + # If current directory has build.gradle but no settings.gradle, and parent has settings.gradle, + # create a minimal settings.gradle to isolate this build + local temp_settings_created=false + if [ -f "build.gradle" ] && [ ! -f "settings.gradle" ]; then + local parent_settings=$(dirname "$PWD")/settings.gradle + if [ -f "$parent_settings" ]; then + # Create a minimal settings.gradle to prevent Gradle from using parent + echo "rootProject.name = 'android-client'" > "settings.gradle" + temp_settings_created=true + log_verbose "Created temporary settings.gradle to isolate build from parent" + fi + fi + + # Build the fused AAR using the first available Gradle task + # Clean first to avoid cross-branch artifacts affecting selection + # Use aggressive cleaning to ensure no cached artifacts remain + log_verbose "Running: $gradle_cmd clean" + if [ "$VERBOSE" = true ]; then + "$gradle_cmd" clean --no-daemon || true + else + "$gradle_cmd" clean --quiet --no-daemon >/dev/null 2>&1 || true + fi + + # Additional cleanup: remove build directories to ensure fresh build + log_verbose "Removing build directories for clean slate..." + rm -rf build/ */build/ .gradle/ */.gradle/ 2>/dev/null || true + local build_ok=false + local used_task="" + for task in "${GRADLE_TASK_CANDIDATES[@]}"; do + log_verbose "Running: $gradle_cmd $task" + if [ "$VERBOSE" = true ]; then + if "$gradle_cmd" "$task" --no-daemon; then + build_ok=true + used_task="$task" + break + fi + else + if "$gradle_cmd" "$task" --quiet --no-daemon > /dev/null 2>&1; then + build_ok=true + used_task="$task" + break + fi + fi + done + if [ "$build_ok" != true ]; then + error_exit "Failed to build AAR for ref: $ref (tried: ${GRADLE_TASK_CANDIDATES[*]})" + fi + log_verbose "Selected Gradle build task for ref '${ref}': $used_task" + + # Find the AAR file - search in multiple possible locations + local aar_path="" + local possible_paths=() + # Add exact candidate names first (both relative and explicit) + for name in "${AAR_CANDIDATE_NAMES[@]}"; do + possible_paths+=("${AAR_OUTPUT_DIR}/${name}") + possible_paths+=("build/outputs/aar/${name}") + done + # Include fused plugin bundle directory + possible_paths+=("build/bundle/bundle.aar") + possible_paths+=("build/bundle/*.aar") + # Then pattern-based fallbacks (root only; try to avoid picking submodule AARs) + possible_paths+=("build/outputs/aar/android-client-*.aar") + possible_paths+=("build/outputs/aar/*-release.aar") + possible_paths+=("build/outputs/aar/*.aar") + + # Also check relative to gradle root if different + if [ "$gradle_root" != "$PWD" ]; then + for name in "${AAR_CANDIDATE_NAMES[@]}"; do + possible_paths+=("$gradle_root/${AAR_OUTPUT_DIR}/${name}") + possible_paths+=("$gradle_root/build/outputs/aar/${name}") + done + possible_paths+=("$gradle_root/build/bundle/bundle.aar") + possible_paths+=("$gradle_root/build/bundle/*.aar") + possible_paths+=("$gradle_root/build/outputs/aar/android-client-*.aar") + possible_paths+=("$gradle_root/build/outputs/aar/*-release.aar") + possible_paths+=("$gradle_root/build/outputs/aar/*.aar") + fi + + # Find the AAR file - priority-based selection across all locations + log_verbose "Searching for AAR files in build directory..." + local all_aars + all_aars=$(find . -type f -name "*.aar" 2>/dev/null | sort || true) + pick_first() { printf "%s\n" "$all_aars" | grep -E "$1" | head -1 || true; } + aar_path=$(pick_first "/build/outputs/aar/android-client\\.aar$") + [ -z "$aar_path" ] && aar_path=$(pick_first "/build/outputs/aar/android-client-release\\.aar$") + [ -z "$aar_path" ] && aar_path=$(pick_first "/build/outputs/aar/android-client-debug\\.aar$") + # Fused plugin bundle output + [ -z "$aar_path" ] && aar_path=$(pick_first "/build/bundle/bundle\\.aar$") + # Locally published fused artifact + [ -z "$aar_path" ] && aar_path=$(pick_first "/build/publishing/.*/android-client-[^/]+\\.aar$") + # Any remaining android-client*.aar anywhere + [ -z "$aar_path" ] && aar_path=$(pick_first "/android-client[^/]*\\.aar$") + # Fallback: any root build/outputs release AAR not from logger/main (handles older artifactIds like 'repo-release.aar') + [ -z "$aar_path" ] && aar_path=$(printf "%s\n" "$all_aars" | grep -E "/build/outputs/aar/[^/]*-release\\.aar$" | grep -Ev "/(logger|main)/" | head -1 || true) + if [ -z "$aar_path" ] || [ ! -f "$aar_path" ]; then + echo -e "${YELLOW}Debug: Could not locate AAR. First 50 *.aar files found:${NC}" + echo "$all_aars" | head -50 || true + error_exit "AAR not found after build." + fi + + log_verbose "Selected fused AAR artifact for ref '${ref}': $aar_path" + + # Copy AAR to temp location + cp "$aar_path" "$output_aar" + + # Verify the AAR was copied and log its size and checksum + local aar_size + aar_size=$(stat -f%z "$output_aar" 2>/dev/null || stat -c%s "$output_aar" 2>/dev/null || echo "unknown") + local aar_sha="" + if command -v shasum >/dev/null 2>&1; then + aar_sha=$(shasum -a 256 "$output_aar" 2>/dev/null | awk '{print $1}') + elif command -v openssl >/dev/null 2>&1; then + aar_sha=$(openssl dgst -sha256 "$output_aar" 2>/dev/null | awk '{print $2}') + fi + log_verbose "✓ AAR built and copied for ref '${ref}': $(basename "$output_aar") (${aar_size} bytes${aar_sha:+, sha256: $aar_sha})" + + # Clean up temporary settings.gradle if we created one + # (This is cleaned up here, but also safe if script fails - temp dir will be removed) + if [ "$temp_settings_created" = true ] && [ -f "settings.gradle" ]; then + rm -f "settings.gradle" + fi +} + +# Main execution +main() { + # Parse arguments + parse_args "$@" + + # Get repository path + get_repo_path + + # Setup Diffuse (use absolute path for cache since we'll be in temp dir) + local original_dir="$PWD" + setup_diffuse + + # Clone repository twice - once for each branch to ensure complete isolation + # This prevents any build cache or Gradle state from affecting the comparison + clone_repo "$SOURCE_CLONE_DIR" "source" + clone_repo "$TARGET_CLONE_DIR" "target" + + # Build source branch AAR first (newer version) + # This ensures we build the newer version first, which may have more dependencies + build_aar "$SOURCE_CLONE_DIR" "$SOURCE_BRANCH" "$SOURCE_AAR" + + # Build target branch AAR second (older version) in separate clone directory + build_aar "$TARGET_CLONE_DIR" "$TARGET_BRANCH" "$TARGET_AAR" + + # Summarize what will be compared (paths, sizes, hashes) to ensure distinct artifacts + # Only show in verbose mode + if [ "$VERBOSE" = true ]; then + summarize_aar() { + local label="$1" + local file="$2" + local size + size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || wc -c < "$file" 2>/dev/null || echo "unknown") + local sha="" + if command -v shasum >/dev/null 2>&1; then + sha=$(shasum -a 256 "$file" 2>/dev/null | awk '{print $1}') + elif command -v openssl >/dev/null 2>&1; then + sha=$(openssl dgst -sha256 "$file" 2>/dev/null | awk '{print $2}') + fi + echo -e "${BLUE}${label}: ${NC}$file (${size} bytes${sha:+, sha256: $sha})" + } + summarize_aar "OLD (target)" "$TARGET_AAR" + summarize_aar "NEW (source)" "$SOURCE_AAR" + fi + + # Run Diffuse comparison + # Note: Diffuse shows OLD -> NEW, so we pass target (old) first, then source (new) + # We can run from any directory since we use absolute paths + set +e # Temporarily disable exit on error + set +o pipefail + run_diffuse "$TARGET_AAR" "$SOURCE_AAR" + local run_exit=$? + set -e # Re-enable exit on error + set -o pipefail + + # Return success (run_diffuse returns 0 on success) + return 0 +} + +# Run main function +main "$@" diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..b584365a6 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,7 @@ +rootProject.name = 'android-client' + +include ':api' +include ':logger' +include ':main' +include ':events' +include ':events-domain' diff --git a/sonar-project.properties b/sonar-project.properties index 930a7d2dc..f598dd559 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,20 +2,41 @@ sonar.projectKey=splitio_android-client sonar.projectName=android-client -# Path to source directories -sonar.sources=src/main/java +# Path to source directories (multi-module) +# Root project contains modules: main, events, logger +sonar.sources=main/src/main/java,events/src/main/java,logger/src/main/java -# Path to compiled classes -sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug +# Path to compiled classes (multi-module) +# Include binary paths for all modules: main, events, logger +sonar.java.binaries=\ + main/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + events/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + logger/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes -# Path to test directories -sonar.tests=src/test/java,src/androidTest/java,src/sharedTest/java +# Path to dependency/libraries jars (multi-module) +sonar.java.libraries=\ + main/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + main/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + main/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + main/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + events/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + events/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + events/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + events/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + logger/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + logger/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + logger/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + logger/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar + +# Path to test directories (multi-module) +# Only include test source folders that are guaranteed to exist in all environments +sonar.tests=main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java # Encoding of the source code sonar.sourceEncoding=UTF-8 -# Include test coverage reports - prioritize combined report -sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml +# Include aggregate test coverage report from all modules +sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml # Exclusions sonar.exclusions=**/R.class,**/R$*.class,**/BuildConfig.*,**/Manifest*.*,**/*Test*.*,android/**/*.* diff --git a/split-proguard-rules.pro b/split-proguard-rules.pro index 8dc624df9..de3caa063 100644 --- a/split-proguard-rules.pro +++ b/split-proguard-rules.pro @@ -13,6 +13,7 @@ -keep class io.split.android.client.service.sseclient.SseAuthenticationResponse { *; } -keep class io.split.android.client.service.sseclient.notifications.** { *; } -keepattributes Signature +-keepattributes MethodParameters -keep class com.google.gson.reflect.TypeToken { *; } -keep class * extends com.google.gson.reflect.TypeToken -dontwarn java.beans.BeanInfo @@ -26,6 +27,9 @@ # removes such information by default, so configure it to keep all of it. -keepattributes Signature +# Preserve method parameter names so consumers see actual parameter names instead of s0, s1, s2, etc. +-keepattributes MethodParameters + # For using GSON @Expose annotation -keepattributes *Annotation* diff --git a/src/androidTest/java/tests/service/EventsManagerTest.java b/src/androidTest/java/tests/service/EventsManagerTest.java deleted file mode 100644 index 0d3c01d13..000000000 --- a/src/androidTest/java/tests/service/EventsManagerTest.java +++ /dev/null @@ -1,226 +0,0 @@ -package tests.service; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import fake.SplitEventExecutorResourcesMock; -import helper.TestingHelper; -import io.split.android.client.SplitClientConfig; -import io.split.android.client.events.SplitEvent; -import io.split.android.client.events.SplitEventsManager; -import io.split.android.client.events.SplitInternalEvent; -import io.split.android.client.service.executor.SplitTaskExecutorImpl; -import io.split.android.client.service.synchronizer.ThreadUtils; - -public class EventsManagerTest { - @Test - public void testSdkUpdateSplits() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - CountDownLatch updateLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - - updateLatch.await(5, TimeUnit.SECONDS); - - Assert.assertTrue(updateTask.onExecutedCalled); - } - - @Test - public void testSdkFetchedUpdatedSplits() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - CountDownLatch updateLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - - updateLatch.await(5, TimeUnit.SECONDS); - - Assert.assertTrue(updateTask.onExecutedCalled); - } - - @Test - public void testSdkUpdatedFetchedSplits() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - CountDownLatch updateLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - - - updateLatch.await(5, TimeUnit.SECONDS); - - Assert.assertFalse(updateTask.onExecutedCalled); - } - - @Test - public void testSdkUpdateSegments() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - CountDownLatch updateLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - - updateLatch.await(5, TimeUnit.SECONDS); - - Assert.assertTrue(updateTask.onExecutedCalled); - } - - @Test - public void testSdkFetchedUpdatedSegments() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - CountDownLatch updateLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - - updateLatch.await(5, TimeUnit.SECONDS); - - Assert.assertTrue(updateTask.onExecutedCalled); - } - - @Test - public void testSdkUpdatedFetchedSegments() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - CountDownLatch updateLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); - - - updateLatch.await(5, TimeUnit.SECONDS); - - Assert.assertFalse(updateTask.onExecutedCalled); - } - - @Test - public void testKilledSplit() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - CountDownLatch updateLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); - - updateLatch.await(5, TimeUnit.SECONDS); - - Assert.assertTrue(updateTask.onExecutedCalled); - } - - @Test - public void testKilledSplitBeforeReady() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - - - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(null); - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); - - TestingHelper.delay(1000); - - Assert.assertFalse(updateTask.onExecutedCalled); - } - - @Test - public void testTimeoutSplitsUpdated() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().ready(2000).build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - CountDownLatch timeoutLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(null); - TestingHelper.TestEventTask timeoutTask = TestingHelper.testTask(timeoutLatch); - - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - eventManager.register(SplitEvent.SDK_READY_TIMED_OUT, timeoutTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - - timeoutLatch.await(5, TimeUnit.SECONDS); - - Assert.assertFalse(updateTask.onExecutedCalled); - Assert.assertTrue(timeoutTask.onExecutedCalled); - } - - @Test - public void testTimeoutMySegmentsUpdated() throws InterruptedException { - - SplitClientConfig cfg = SplitClientConfig.builder().ready(2000).build(); - SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorImpl()); - eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); - CountDownLatch timeoutLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(null); - TestingHelper.TestEventTask timeoutTask = TestingHelper.testTask(timeoutLatch); - - eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - eventManager.register(SplitEvent.SDK_READY_TIMED_OUT, timeoutTask); - - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - - timeoutLatch.await(5, TimeUnit.SECONDS); - - Assert.assertFalse(updateTask.onExecutedCalled); - Assert.assertTrue(timeoutTask.onExecutedCalled); - } -} diff --git a/src/main/java/io/split/android/client/events/BaseEventsManager.java b/src/main/java/io/split/android/client/events/BaseEventsManager.java deleted file mode 100644 index e22b9a7dc..000000000 --- a/src/main/java/io/split/android/client/events/BaseEventsManager.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.split.android.client.events; - -import androidx.annotation.NonNull; - -import java.util.Collections; -import java.util.Set; -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.utils.logger.Logger; -import io.split.android.engine.scheduler.PausableThreadPoolExecutor; -import io.split.android.engine.scheduler.PausableThreadPoolExecutorImpl; - -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; - - 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()); - } - }); - return thread; - } - }; - } - - public BaseEventsManager() { - mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); - mTriggered = Collections.newSetFromMap(new ConcurrentHashMap<>()); - launch(EVENTS_THREAD_FACTORY); - } - - @Override - public void run() { - // This code was intentionally designed this way - // noinspection InfiniteLoopStatement - while (true) { - triggerEventsWhenAreAvailable(); - } - } - - private void launch(ThreadFactory threadFactory) { - PausableThreadPoolExecutor mScheduler = PausableThreadPoolExecutorImpl.newSingleThreadExecutor(threadFactory); - mScheduler.submit(this); - mScheduler.resume(); - } - - protected abstract void triggerEventsWhenAreAvailable(); - - protected abstract void notifyInternalEvent(SplitInternalEvent event); -} diff --git a/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java b/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java deleted file mode 100644 index 3d3af8bf3..000000000 --- a/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.split.android.client.events; - -import static io.split.android.client.utils.Utils.checkNotNull; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import io.split.android.client.api.Key; -import io.split.android.client.utils.logger.Logger; - -/** - * Special case event manager which handles events that should be shared among all client instances. - */ -public class EventsManagerCoordinator extends BaseEventsManager implements ISplitEventsManager, EventsManagerRegistry { - - private final ConcurrentMap mChildren = new ConcurrentHashMap<>(); - private final Object mEventLock = new Object(); - - @Override - public void notifyInternalEvent(SplitInternalEvent internalEvent) { - checkNotNull(internalEvent); - try { - mQueue.add(internalEvent); - } catch (IllegalStateException e) { - Logger.d("Internal events queue is full"); - } - } - - @Override - protected void triggerEventsWhenAreAvailable() { - try { - SplitInternalEvent event = mQueue.take(); //Blocking method (waiting if necessary until an element becomes available.) - synchronized (mEventLock) { - mTriggered.add(event); - switch (event) { - case SPLITS_UPDATED: - case RULE_BASED_SEGMENTS_UPDATED: - case SPLITS_FETCHED: - case SPLITS_LOADED_FROM_STORAGE: - case SPLIT_KILLED_NOTIFICATION: - case ENCRYPTION_MIGRATION_DONE: - for (ISplitEventsManager child : mChildren.values()) { - child.notifyInternalEvent(event); - } - break; - } - } - } catch (InterruptedException e) { - //Catching the InterruptedException that can be thrown by _queue.take() if interrupted while waiting - // for further information read https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ArrayBlockingQueue.html#take() - Logger.d(e.getMessage()); - } - } - - @Override - public void registerEventsManager(Key key, ISplitEventsManager splitEventsManager) { - mChildren.put(key, splitEventsManager); - - // Inform the newly registered events manager of any events that occurred prior to registration - propagateTriggeredEvents(splitEventsManager); - } - - @Override - public void unregisterEventsManager(Key key) { - mChildren.remove(key); - } - - private void propagateTriggeredEvents(ISplitEventsManager splitEventsManager) { - synchronized (mEventLock) { - for (SplitInternalEvent event : mTriggered) { - splitEventsManager.notifyInternalEvent(event); - } - } - } -} diff --git a/src/main/java/io/split/android/client/events/ISplitEventsManager.java b/src/main/java/io/split/android/client/events/ISplitEventsManager.java deleted file mode 100644 index 31b72ffd0..000000000 --- a/src/main/java/io/split/android/client/events/ISplitEventsManager.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.split.android.client.events; - -public interface ISplitEventsManager { - - void notifyInternalEvent(SplitInternalEvent internalEvent); -} diff --git a/src/main/java/io/split/android/client/events/SplitEventTask.java b/src/main/java/io/split/android/client/events/SplitEventTask.java deleted file mode 100644 index 5a5dd6db9..000000000 --- a/src/main/java/io/split/android/client/events/SplitEventTask.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.split.android.client.events; - -import io.split.android.client.SplitClient; - -/** - * Created by sarrubia on 3/26/18. - */ - -public class SplitEventTask { - public void onPostExecution(SplitClient client) { - throw new SplitEventTaskMethodNotImplementedException(); - } - - public void onPostExecutionView(SplitClient client) { - throw new SplitEventTaskMethodNotImplementedException(); - } -} diff --git a/src/main/java/io/split/android/client/events/SplitEventsManager.java b/src/main/java/io/split/android/client/events/SplitEventsManager.java deleted file mode 100644 index 9fc54670b..000000000 --- a/src/main/java/io/split/android/client/events/SplitEventsManager.java +++ /dev/null @@ -1,232 +0,0 @@ -package io.split.android.client.events; - -import static io.split.android.client.utils.Utils.checkNotNull; - -import androidx.annotation.VisibleForTesting; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import io.split.android.client.SplitClientConfig; -import io.split.android.client.events.executors.SplitEventExecutor; -import io.split.android.client.events.executors.SplitEventExecutorFactory; -import io.split.android.client.events.executors.SplitEventExecutorResources; -import io.split.android.client.events.executors.SplitEventExecutorResourcesImpl; -import io.split.android.client.service.executor.SplitTaskExecutor; -import io.split.android.client.utils.logger.Logger; - -public class SplitEventsManager extends BaseEventsManager implements ISplitEventsManager, ListenableEventsManager, Runnable { - - private final Map> mSubscriptions; - - private SplitEventExecutorResources mResources; - - private final Map mExecutionTimes; - - private final SplitTaskExecutor mSplitTaskExecutor; - - public SplitEventsManager(SplitClientConfig config, SplitTaskExecutor splitTaskExecutor) { - this(splitTaskExecutor, config.blockUntilReady()); - } - - public SplitEventsManager(SplitTaskExecutor splitTaskExecutor, final int blockUntilReady) { - super(); - mSplitTaskExecutor = splitTaskExecutor; - mSubscriptions = new ConcurrentHashMap<>(); - mExecutionTimes = new ConcurrentHashMap<>(); - mResources = new SplitEventExecutorResourcesImpl(); - registerMaxAllowedExecutionTimesPerEvent(); - - Runnable SDKReadyTimeout = new Runnable() { - @Override - public void run() { - try { - if (blockUntilReady > 0) { - Thread.sleep(blockUntilReady); - notifyInternalEvent(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED); - } - } catch (InterruptedException e) { - //InterruptedException could be thrown by Thread.sleep trying to wait before check if sdk is ready - Logger.d("Waiting before to check if SDK is READY has been interrupted", e.getMessage()); - notifyInternalEvent(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED); - } catch (Throwable e) { - Logger.d("Waiting before to check if SDK is READY interrupted ", e.getMessage()); - notifyInternalEvent(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED); - } - } - }; - new Thread(SDKReadyTimeout).start(); - } - - @VisibleForTesting - public void setExecutionResources(SplitEventExecutorResources resources) { - mResources = resources; - } - - /** - * This method should register the allowed maximum times of event trigger - * EXAMPLE: SDK_READY should be triggered only once - */ - private void registerMaxAllowedExecutionTimesPerEvent() { - mExecutionTimes.put(SplitEvent.SDK_READY, 1); - mExecutionTimes.put(SplitEvent.SDK_READY_TIMED_OUT, 1); - mExecutionTimes.put(SplitEvent.SDK_READY_FROM_CACHE, 1); - mExecutionTimes.put(SplitEvent.SDK_UPDATE, -1); - } - - @Override - public SplitEventExecutorResources getExecutorResources() { - return mResources; - } - - @Override - public void notifyInternalEvent(SplitInternalEvent internalEvent) { - checkNotNull(internalEvent); - // Avoid adding to queue for fetched events if sdk is ready - // These events were added to handle updated event logic in this component - // and also to fix some issues when processing queue that made sdk update - // fire on init - - if ((internalEvent == SplitInternalEvent.SPLITS_FETCHED - || internalEvent == SplitInternalEvent.MY_SEGMENTS_FETCHED) && - isTriggered(SplitEvent.SDK_READY)) { - return; - } - try { - mQueue.add(internalEvent); - } catch (IllegalStateException e) { - Logger.d("Internal events queue is full"); - } - } - - public void register(SplitEvent event, SplitEventTask task) { - - checkNotNull(event); - checkNotNull(task); - - // If event is already triggered, execute the task - if (mExecutionTimes.containsKey(event) && mExecutionTimes.get(event) == 0) { - executeTask(event, task); - return; - } - - if (!mSubscriptions.containsKey(event)) { - mSubscriptions.put(event, new ArrayList<>()); - } - mSubscriptions.get(event).add(task); - } - - public boolean eventAlreadyTriggered(SplitEvent event) { - return isTriggered(event); - } - - private boolean wasTriggered(SplitInternalEvent event) { - return mTriggered.contains(event); - } - - @Override - protected void triggerEventsWhenAreAvailable() { - try { - SplitInternalEvent event = mQueue.take(); //Blocking method (waiting if necessary until an element becomes available.) - mTriggered.add(event); - switch (event) { - case SPLITS_UPDATED: - case MY_SEGMENTS_UPDATED: - case MY_LARGE_SEGMENTS_UPDATED: - case RULE_BASED_SEGMENTS_UPDATED: - if (isTriggered(SplitEvent.SDK_READY)) { - trigger(SplitEvent.SDK_UPDATE); - return; - } - triggerSdkReadyIfNeeded(); - break; - - case SPLITS_FETCHED: - case MY_SEGMENTS_FETCHED: - if (isTriggered(SplitEvent.SDK_READY)) { - return; - } - triggerSdkReadyIfNeeded(); - break; - - case SPLITS_LOADED_FROM_STORAGE: - case MY_SEGMENTS_LOADED_FROM_STORAGE: - case ATTRIBUTES_LOADED_FROM_STORAGE: - case ENCRYPTION_MIGRATION_DONE: - if (wasTriggered(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE) && - wasTriggered(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE) && - wasTriggered(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE) && - wasTriggered(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE)) { - trigger(SplitEvent.SDK_READY_FROM_CACHE); - } - break; - - case SPLIT_KILLED_NOTIFICATION: - if (isTriggered(SplitEvent.SDK_READY)) { - trigger(SplitEvent.SDK_UPDATE); - } - break; - - case SDK_READY_TIMEOUT_REACHED: - if (!isTriggered(SplitEvent.SDK_READY)) { - trigger(SplitEvent.SDK_READY_TIMED_OUT); - } - break; - } - } catch (InterruptedException e) { - //Catching the InterruptedException that can be thrown by _queue.take() if interrupted while waiting - // for further information read https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ArrayBlockingQueue.html#take() - Logger.d(e.getMessage()); - } - } - - // MARK: Helper functions. - private boolean isTriggered(SplitEvent event) { - Integer times = mExecutionTimes.get(event); - return times != null ? times == 0 : false; - } - - private void triggerSdkReadyIfNeeded() { - if ((wasTriggered(SplitInternalEvent.MY_SEGMENTS_UPDATED) || wasTriggered(SplitInternalEvent.MY_SEGMENTS_FETCHED) || wasTriggered(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED)) && - (wasTriggered(SplitInternalEvent.SPLITS_UPDATED) || wasTriggered(SplitInternalEvent.SPLITS_FETCHED)) && - !isTriggered(SplitEvent.SDK_READY)) { - if (!isTriggered(SplitEvent.SDK_READY_FROM_CACHE)) { - trigger(SplitEvent.SDK_READY_FROM_CACHE); - } - trigger(SplitEvent.SDK_READY); - } - } - - private void trigger(SplitEvent event) { - // If executionTimes is zero, maximum executions has been reached - if (mExecutionTimes.get(event) == 0) { - return; - // If executionTimes is grater than zero, maximum executions decrease 1 - } else if (mExecutionTimes.get(event) > 0) { - mExecutionTimes.put(event, mExecutionTimes.get(event) - 1); - } //If executionTimes is lower than zero, execute it without limitation - if (event != null) { - Logger.d(event.name() + " event triggered"); - } - if (mSubscriptions.containsKey(event)) { - List toExecute = mSubscriptions.get(event); - if (toExecute != null) { - for (SplitEventTask task : toExecute) { - executeTask(event, task); - } - } - } - } - - private void executeTask(SplitEvent event, SplitEventTask task) { - if (task != null) { - SplitEventExecutor executor = SplitEventExecutorFactory.factory(mSplitTaskExecutor, event, task, mResources); - - if (executor != null) { - executor.execute(); - } - } - } -} diff --git a/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java b/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java deleted file mode 100644 index 16fb4cb73..000000000 --- a/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.split.android.client.service.synchronizer; - -import java.util.Collections; -import java.util.List; - -public class MySegmentsChangeChecker { - public boolean mySegmentsHaveChanged(final List oldSegments, final List newSegments) { - Collections.sort(oldSegments); - Collections.sort(newSegments); - return !oldSegments.equals(newSegments); - } -} diff --git a/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java deleted file mode 100644 index 60978714b..000000000 --- a/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package io.split.android.client.events; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import io.split.android.client.api.Key; - -public class EventsManagerCoordinatorTest { - - @Mock - private ISplitEventsManager mMockChildEventsManager; - private EventsManagerCoordinator mEventsManager; - - @Before - public void setUp() { - MockitoAnnotations.openMocks(this); - mEventsManager = new EventsManagerCoordinator(); - } - - @Test - public void SPLITS_UPDATEDEventIsPassedDownToChildren() { - mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - - delay(); - - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - } - - @Test - public void RULE_BASED_SEGMENTEventIsPassedDownToChildren() { - mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); - - delay(); - - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); - } - - @Test - public void SPLITS_FETCHEDEventIsPassedDownToChildren() { - mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - - delay(); - - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - } - - @Test - public void SPLITS_LOADED_FROM_STORAGEEventIsPassedDownToChildren() { - mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); - - delay(); - - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); - } - - @Test - public void SPLIT_KILLED_NOTIFICATIONEventIsPassedDownToChildren() { - mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); - - delay(); - - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); - } - - @Test - public void EventIsPassedDownToChildrenIfRegisteredAfterEmission() { - ISplitEventsManager newMockChildEventsManager = mock(ISplitEventsManager.class); - mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - - delay(); - - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - - mEventsManager.registerEventsManager(new Key("new_key", "bucketing"), newMockChildEventsManager); - verify(newMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - } - - private void delay() { - boolean shouldStop = false; - long maxExecutionTime = System.currentTimeMillis() + 1000; - long intervalExecutionTime = 100; - - while (!shouldStop) { - try { - Thread.sleep(intervalExecutionTime); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail(); - } - - maxExecutionTime -= intervalExecutionTime; - - if (System.currentTimeMillis() > maxExecutionTime) { - shouldStop = true; - } - } - } -} diff --git a/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java b/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java deleted file mode 100644 index c01b5c279..000000000 --- a/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.split.android.client.service; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import io.split.android.client.service.synchronizer.MySegmentsChangeChecker; - -public class MySegmentsChangesCheckerTest { - - MySegmentsChangeChecker mMySegmentsChangeChecker = new MySegmentsChangeChecker(); - - @Test - public void testChangesArrived() { - - List old = Arrays.asList("s1", "s2", "s3"); - List newSegments = Arrays.asList("s1"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertTrue(result); - } - - @Test - public void testNewChangesArrived() { - - List newSegments = Arrays.asList("s1", "s2", "s3"); - List old = Arrays.asList("s1"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertTrue(result); - } - - @Test - public void testNoChangesArrived() { - - List old = Arrays.asList("s1", "s2", "s3"); - List newSegments = Arrays.asList("s1", "s2", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertFalse(result); - } - - @Test - public void testNoChangesDifferentOrder() { - - List old = Arrays.asList("s1", "s2", "s3"); - List newSegments = Arrays.asList("s2", "s1", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertFalse(result); - } - - @Test - public void testNoChangesDifferentOrderInverted() { - - List newSegments = Arrays.asList("s1", "s2", "s3"); - List old = Arrays.asList("s2", "s1", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertFalse(result); - } - - @Test - public void testNoChangesArrivedEmpty() { - - List newSegments = new ArrayList<>(); - List old = new ArrayList<>(); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertFalse(result); - } - - @Test - public void testEmptyChangesArrived() { - - List newSegments = new ArrayList<>(); - List old = Arrays.asList("s1", "s2", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertTrue(result); - } -} diff --git a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java deleted file mode 100644 index 9a4eb8580..000000000 --- a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package io.split.android.client.service; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.longThat; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; - -import io.split.android.client.dtos.SplitChange; -import io.split.android.client.events.SplitEventsManager; -import io.split.android.client.events.SplitInternalEvent; -import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.service.http.HttpFetcherException; -import io.split.android.client.service.splits.SplitsSyncHelper; -import io.split.android.client.service.splits.SplitsSyncTask; -import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; -import io.split.android.client.storage.splits.SplitsStorage; -import io.split.android.client.telemetry.model.OperationType; -import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; -import io.split.android.helpers.FileHelper; - -public class SplitSyncTaskTest { - - private static final long OLD_TIMESTAMP = 1546300800L; //2019-01-01 - - SplitsStorage mSplitsStorage; - SplitChange mSplitChange = null; - SplitsSyncHelper mSplitsSyncHelper; - RuleBasedSegmentStorageProducer mRuleBasedSegmentStorage; - - SplitsSyncTask mTask; - String mQueryString = "qs=1"; - - SplitEventsManager mEventsManager; - - TelemetryRuntimeProducer mTelemetryRuntimeProducer; - - @Before - public void setup() { - mTelemetryRuntimeProducer = mock(TelemetryRuntimeProducer.class); - - mSplitsStorage = mock(SplitsStorage.class); - mSplitsSyncHelper = mock(SplitsSyncHelper.class); - mEventsManager = mock(SplitEventsManager.class); - mRuleBasedSegmentStorage = mock(RuleBasedSegmentStorageProducer.class); - - when(mSplitsSyncHelper.sync(notNull(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL)); - - loadSplitChanges(); - } - - @Test - public void correctExecution() throws HttpFetcherException { - // Check that syncing is done with changeNum retrieved from db - // Querystring is the same, so no clear sould be called - // And updateTimestamp is 0 - // Retry is off, so splitSyncHelper.sync should be called - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, - mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(-1L); - when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(-1L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - - mTask.execute(); - - verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == -1L && argument.getRbsSince() == -1L), eq(false), eq(false), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); - } - - @Test - public void cleanSplitsWhenQueryStringHasChanged() throws HttpFetcherException { - // Splits have to be cleared when query string on db is != than current one on current sdk client instance - // Setting up cache not expired - // splits change param should be -1 - - String otherQs = "q=other"; - Map params = new HashMap<>(); - params.put("since", 100L); - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, - otherQs, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(100L); - when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - - mTask.execute(); - - verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == -1 && argument.getRbsSince() == 200), eq(true), eq(true), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); - verify(mSplitsStorage, times(1)).updateSplitsFilterQueryString(otherQs); - } - - @Test - public void noClearSplitsWhenQueryStringHasNotChanged() throws HttpFetcherException { - // Splits have to be cleared when query string on db is != than current one on current sdk client instance - // Setting up cache not expired - - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, - mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(100L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - - mTask.execute(); - - verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == 100L), eq(false), eq(false), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); - verify(mSplitsStorage, never()).updateSplitsFilterQueryString(anyString()); - } - - @Test - public void splitUpdatedNotified() throws HttpFetcherException { - // Check that syncing is done with changeNum retrieved from db - // Querystring is the same, so no clear sould be called - // And updateTimestamp is 0 - // Retry is off, so splitSyncHelper.sync should be called - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, - mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); - - mTask.execute(); - - verify(mEventsManager, times(1)).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - } - - @Test - public void splitFetchdNotified() throws HttpFetcherException { - // Check that syncing is done with changeNum retrieved from db - // Querystring is the same, so no clear sould be called - // And updateTimestamp is 0 - // Retry is off, so splitSyncHelper.sync should be called - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, - mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(100L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); - - mTask.execute(); - - verify(mEventsManager, times(1)).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - } - - @Test - public void syncIsTrackedInTelemetry() { - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, - mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(100L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); - - mTask.execute(); - - verify(mTelemetryRuntimeProducer).recordSyncLatency(eq(OperationType.SPLITS), anyLong()); - } - - @Test - public void recordSuccessInTelemetry() { - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, - mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(-1L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - - mTask.execute(); - - verify(mTelemetryRuntimeProducer).recordSuccessfulSync(eq(OperationType.SPLITS), longThat(arg -> arg > 0)); - } - - @After - public void tearDown() { - reset(mSplitsStorage); - } - - private void loadSplitChanges() { - if (mSplitChange == null) { - FileHelper fileHelper = new FileHelper(); - mSplitChange = fileHelper.loadSplitChangeFromFile("split_changes_1.json"); - } - } -} diff --git a/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java deleted file mode 100644 index 677030c0c..000000000 --- a/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.split.android.client.service; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.Mockito; - -import io.split.android.client.dtos.SplitChange; -import io.split.android.client.events.SplitEventsManager; -import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.service.http.HttpFetcherException; -import io.split.android.client.service.splits.SplitsSyncHelper; -import io.split.android.client.service.splits.SplitsUpdateTask; -import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; -import io.split.android.client.storage.splits.SplitsStorage; -import io.split.android.helpers.FileHelper; - -public class SplitUpdateTaskTest { - - SplitsStorage mSplitsStorage; - RuleBasedSegmentStorage mRuleBasedSegmentStorage; - SplitChange mSplitChange = null; - SplitsSyncHelper mSplitsSyncHelper; - SplitEventsManager mEventsManager; - - SplitsUpdateTask mTask; - - long mChangeNumber = 234567833L; - long mRbsChangeNumber = 234567830L; - - @Before - public void setup() { - mSplitsStorage = Mockito.mock(SplitsStorage.class); - mRuleBasedSegmentStorage = Mockito.mock(RuleBasedSegmentStorage.class); - mSplitsSyncHelper = Mockito.mock(SplitsSyncHelper.class); - mEventsManager = Mockito.mock(SplitEventsManager.class); - mTask = new SplitsUpdateTask(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mChangeNumber, mRbsChangeNumber, mEventsManager); - when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); - when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); - loadSplitChanges(); - } - - @Test - public void correctExecution() throws HttpFetcherException { - when(mSplitsStorage.getTill()).thenReturn(-1L); - when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(10L); - - mTask.execute(); - - verify(mSplitsSyncHelper).sync(argThat(new ArgumentMatcher() { - @Override - public boolean matches(SplitsSyncHelper.SinceChangeNumbers argument) { - return argument.getFlagsSince() == 234567833L && argument.getRbsSince() == 234567830L; - } - }), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); - } - - @Test - public void storedChangeNumBigger() throws HttpFetcherException { - when(mSplitsStorage.getTill()).thenReturn(mChangeNumber + 100L); - - mTask.execute(); - - verify(mSplitsSyncHelper, never()).sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); - } - - @Test - public void storedRbsChangeNumBigger() throws HttpFetcherException { - when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(mRbsChangeNumber + 100L); - - mTask.execute(); - - verify(mSplitsSyncHelper, never()).sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); - } - - @After - public void tearDown() { - reset(mSplitsStorage); - } - - private void loadSplitChanges() { - if (mSplitChange == null) { - FileHelper fileHelper = new FileHelper(); - mSplitChange = fileHelper.loadSplitChangeFromFile("split_changes_1.json"); - } - } -}