diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml
index 290d5b93..c22fafcb 100644
--- a/example/src/main/AndroidManifest.xml
+++ b/example/src/main/AndroidManifest.xml
@@ -14,7 +14,7 @@
android:theme="@style/Theme.EppoExample"
tools:targetApi="30">
@@ -24,7 +24,12 @@
+ android:name="cloud.eppo.androidexample.StandardClientActivity"
+ android:parentActivityName="cloud.eppo.androidexample.HomeActivity" />
+
+
\ No newline at end of file
diff --git a/example/src/main/java/cloud/eppo/androidexample/MainActivity.java b/example/src/main/java/cloud/eppo/androidexample/HomeActivity.java
similarity index 60%
rename from example/src/main/java/cloud/eppo/androidexample/MainActivity.java
rename to example/src/main/java/cloud/eppo/androidexample/HomeActivity.java
index c64f62d4..92c50bc1 100644
--- a/example/src/main/java/cloud/eppo/androidexample/MainActivity.java
+++ b/example/src/main/java/cloud/eppo/androidexample/HomeActivity.java
@@ -14,16 +14,17 @@
import cloud.eppo.android.EppoClient;
import com.geteppo.androidexample.BuildConfig;
import com.geteppo.androidexample.R;
+import java.io.File;
-public class MainActivity extends AppCompatActivity {
+public class HomeActivity extends AppCompatActivity {
private static final String API_KEY = BuildConfig.API_KEY; // Set in root-level local.properties
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
+ setContentView(R.layout.activity_home);
Button button = findViewById(R.id.button_start_assigner);
- Intent launchAssigner = new Intent(MainActivity.this, SecondActivity.class);
+ Intent launchAssigner = new Intent(HomeActivity.this, StandardClientActivity.class);
button.setOnClickListener(view -> startActivity(launchAssigner));
@@ -32,15 +33,39 @@ protected void onCreate(Bundle savedInstanceState) {
view ->
startActivity(launchAssigner.putExtra(this.getPackageName() + ".offlineMode", false)));
+ Button precomputedButton = findViewById(R.id.button_start_precomputed);
+ Intent launchPrecomputed = new Intent(HomeActivity.this, PrecomputedActivity.class);
+ precomputedButton.setOnClickListener(view -> startActivity(launchPrecomputed));
+
Button clearCacheButton = findViewById(R.id.button_clear_cache);
clearCacheButton.setOnClickListener(view -> clearCacheFile());
}
private void clearCacheFile() {
+ // Clear standard client cache
String cacheFileNameSuffix = safeCacheKey(API_KEY);
ConfigCacheFile cacheFile = new ConfigCacheFile(getApplication(), cacheFileNameSuffix);
cacheFile.delete();
- Toast.makeText(this, "Cache Cleared", Toast.LENGTH_SHORT).show();
+
+ // Clear all precomputed client caches (they include subject key hash in filename)
+ File filesDir = getApplication().getFilesDir();
+ File[] precomputedCaches =
+ filesDir.listFiles(
+ (dir, name) -> name.startsWith("eppo-sdk-precomputed-") && name.endsWith(".json"));
+ int precomputedCount = 0;
+ if (precomputedCaches != null) {
+ for (File file : precomputedCaches) {
+ if (file.delete()) {
+ precomputedCount++;
+ }
+ }
+ }
+
+ String message =
+ precomputedCount > 0
+ ? "Cache Cleared (" + precomputedCount + " precomputed)"
+ : "Cache Cleared";
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
diff --git a/example/src/main/java/cloud/eppo/androidexample/PrecomputedActivity.java b/example/src/main/java/cloud/eppo/androidexample/PrecomputedActivity.java
new file mode 100644
index 00000000..14089de5
--- /dev/null
+++ b/example/src/main/java/cloud/eppo/androidexample/PrecomputedActivity.java
@@ -0,0 +1,277 @@
+package cloud.eppo.androidexample;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.RadioGroup;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import cloud.eppo.android.EppoPrecomputedClient;
+import cloud.eppo.api.Attributes;
+import cloud.eppo.api.EppoValue;
+import com.geteppo.androidexample.BuildConfig;
+import com.geteppo.androidexample.R;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Example activity demonstrating the EppoPrecomputedClient. The precomputed client computes all
+ * flag assignments server-side for a specific subject, providing instant lookups.
+ */
+public class PrecomputedActivity extends AppCompatActivity {
+ private static final String TAG = PrecomputedActivity.class.getSimpleName();
+ private static final String API_KEY = BuildConfig.API_KEY;
+
+ private EditText subjectInput;
+ private EditText flagKeyInput;
+ private RadioGroup flagTypeGroup;
+ private TextView assignmentLog;
+ private ScrollView assignmentLogScrollView;
+ private TextView statusText;
+ private LinearLayout attributesContainer;
+ private Button getAssignmentButton;
+ private List attributeRows = new ArrayList<>();
+
+ private EppoPrecomputedClient precomputedClient;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_precomputed);
+
+ // Enable the action bar back button
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle("Precomputed Client");
+ }
+
+ subjectInput = findViewById(R.id.precomputed_subject);
+ flagKeyInput = findViewById(R.id.precomputed_flag_key);
+ flagTypeGroup = findViewById(R.id.flag_type_group);
+ assignmentLog = findViewById(R.id.precomputed_assignment_log);
+ assignmentLogScrollView = findViewById(R.id.precomputed_assignment_log_scrollview);
+ statusText = findViewById(R.id.precomputed_status);
+ attributesContainer = findViewById(R.id.attributes_container);
+
+ findViewById(R.id.btn_init_server).setOnClickListener(view -> initializeClient(false));
+ findViewById(R.id.btn_init_disk).setOnClickListener(view -> initializeClient(true));
+ getAssignmentButton = findViewById(R.id.btn_get_assignment);
+ getAssignmentButton.setEnabled(false);
+ getAssignmentButton.setOnClickListener(view -> getAssignment());
+ findViewById(R.id.btn_add_attribute).setOnClickListener(view -> addAttributeRow("", ""));
+
+ // Add default attributes
+ addAttributeRow("platform", "android");
+ addAttributeRow("appVersion", BuildConfig.VERSION_NAME);
+ }
+
+ private void addAttributeRow(String key, String value) {
+ LinearLayout row = new LinearLayout(this);
+ row.setOrientation(LinearLayout.HORIZONTAL);
+ row.setLayoutParams(
+ new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
+
+ EditText keyInput = new EditText(this);
+ keyInput.setHint("Key");
+ keyInput.setText(key);
+ LinearLayout.LayoutParams keyParams =
+ new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
+ keyInput.setLayoutParams(keyParams);
+ keyInput.setTag("key");
+
+ EditText valueInput = new EditText(this);
+ valueInput.setHint("Value");
+ valueInput.setText(value);
+ LinearLayout.LayoutParams valueParams =
+ new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
+ valueParams.setMarginStart(8);
+ valueInput.setLayoutParams(valueParams);
+ valueInput.setTag("value");
+
+ Button removeButton = new Button(this);
+ removeButton.setText("X");
+ removeButton.setMinWidth(0);
+ removeButton.setMinHeight(0);
+ removeButton.setMinimumWidth(0);
+ removeButton.setMinimumHeight(0);
+ removeButton.setPadding(16, 8, 16, 8);
+ LinearLayout.LayoutParams removeParams =
+ new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ removeParams.setMarginStart(8);
+ removeButton.setLayoutParams(removeParams);
+ removeButton.setOnClickListener(
+ v -> {
+ attributesContainer.removeView(row);
+ attributeRows.remove(row);
+ });
+
+ row.addView(keyInput);
+ row.addView(valueInput);
+ row.addView(removeButton);
+
+ attributesContainer.addView(row);
+ attributeRows.add(row);
+ }
+
+ private Attributes collectAttributes() {
+ Attributes attributes = new Attributes();
+ for (View row : attributeRows) {
+ EditText keyInput = row.findViewWithTag("key");
+ EditText valueInput = row.findViewWithTag("value");
+ if (keyInput != null && valueInput != null) {
+ String key = keyInput.getText().toString().trim();
+ String value = valueInput.getText().toString().trim();
+ if (!key.isEmpty()) {
+ // Try to parse as number first
+ try {
+ double numValue = Double.parseDouble(value);
+ attributes.put(key, EppoValue.valueOf(numValue));
+ } catch (NumberFormatException e) {
+ // Use as string
+ attributes.put(key, EppoValue.valueOf(value));
+ }
+ }
+ }
+ }
+ return attributes;
+ }
+
+ private void initializeClient(boolean offlineMode) {
+ String subjectKey = subjectInput.getText().toString();
+ if (TextUtils.isEmpty(subjectKey)) {
+ appendToLog("Subject ID is required");
+ return;
+ }
+
+ String source = offlineMode ? "disk" : "server";
+ statusText.setText("Initializing from " + source + "...");
+ appendToLog(
+ "Initializing precomputed client for subject: " + subjectKey + " (from " + source + ")");
+
+ // Collect subject attributes from the UI
+ Attributes subjectAttributes = collectAttributes();
+ appendToLog("Subject attributes: " + subjectAttributes.size() + " attributes");
+
+ new EppoPrecomputedClient.Builder(API_KEY, getApplication())
+ .subjectKey(subjectKey)
+ .subjectAttributes(subjectAttributes)
+ .isGracefulMode(true)
+ .forceReinitialize(true)
+ .offlineMode(offlineMode)
+ .assignmentLogger(
+ assignment -> {
+ Log.d(
+ TAG,
+ "Assignment logged: "
+ + assignment.getFeatureFlag()
+ + " -> "
+ + assignment.getVariation());
+ })
+ .buildAndInitAsync()
+ .thenAccept(
+ client -> {
+ precomputedClient = client;
+ runOnUiThread(
+ () -> {
+ statusText.setText("Initialized for: " + subjectKey + " (from " + source + ")");
+ appendToLog("Client initialized successfully from " + source + "!");
+ getAssignmentButton.setEnabled(true);
+ });
+ })
+ .exceptionally(
+ error -> {
+ Log.e(TAG, "Failed to initialize", error);
+ runOnUiThread(
+ () -> {
+ statusText.setText("Initialization failed");
+ appendToLog("Error: " + error.getMessage());
+ });
+ return null;
+ });
+ }
+
+ private void getAssignment() {
+ if (precomputedClient == null) {
+ appendToLog("Client not initialized. Click 'From Server' or 'From Disk' first.");
+ return;
+ }
+
+ String flagKey = flagKeyInput.getText().toString();
+ if (TextUtils.isEmpty(flagKey)) {
+ appendToLog("Flag key is required");
+ return;
+ }
+
+ int selectedTypeId = flagTypeGroup.getCheckedRadioButtonId();
+ String result;
+
+ try {
+ if (selectedTypeId == R.id.type_string) {
+ result = precomputedClient.getStringAssignment(flagKey, "(default)");
+ appendToLog("String assignment for '" + flagKey + "': " + result);
+ } else if (selectedTypeId == R.id.type_boolean) {
+ boolean boolResult = precomputedClient.getBooleanAssignment(flagKey, false);
+ appendToLog("Boolean assignment for '" + flagKey + "': " + boolResult);
+ } else if (selectedTypeId == R.id.type_integer) {
+ int intResult = precomputedClient.getIntegerAssignment(flagKey, 0);
+ appendToLog("Integer assignment for '" + flagKey + "': " + intResult);
+ } else if (selectedTypeId == R.id.type_numeric) {
+ double numericResult = precomputedClient.getNumericAssignment(flagKey, 0.0);
+ appendToLog("Numeric assignment for '" + flagKey + "': " + numericResult);
+ } else if (selectedTypeId == R.id.type_json) {
+ // JSON assignments return JsonNode - for simplicity, we show as string
+ appendToLog("JSON assignment for '" + flagKey + "': (use getJSONAssignment() API)");
+ } else {
+ appendToLog("Please select a flag type");
+ }
+ } catch (Exception e) {
+ appendToLog("Error getting assignment: " + e.getMessage());
+ }
+ }
+
+ private void appendToLog(String message) {
+ assignmentLog.append(message + "\n\n");
+ assignmentLogScrollView.post(() -> assignmentLogScrollView.fullScroll(View.FOCUS_DOWN));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (precomputedClient != null) {
+ precomputedClient.pausePolling();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (precomputedClient != null) {
+ precomputedClient.resumePolling();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (precomputedClient != null) {
+ precomputedClient.stopPolling();
+ }
+ }
+}
diff --git a/example/src/main/java/cloud/eppo/androidexample/SecondActivity.java b/example/src/main/java/cloud/eppo/androidexample/StandardClientActivity.java
similarity index 85%
rename from example/src/main/java/cloud/eppo/androidexample/SecondActivity.java
rename to example/src/main/java/cloud/eppo/androidexample/StandardClientActivity.java
index 6c64d21b..28833165 100644
--- a/example/src/main/java/cloud/eppo/androidexample/SecondActivity.java
+++ b/example/src/main/java/cloud/eppo/androidexample/StandardClientActivity.java
@@ -6,6 +6,7 @@
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
+import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.ScrollView;
@@ -16,8 +17,8 @@
import com.geteppo.androidexample.BuildConfig;
import com.geteppo.androidexample.R;
-public class SecondActivity extends AppCompatActivity {
- private static final String TAG = SecondActivity.class.getSimpleName();
+public class StandardClientActivity extends AppCompatActivity {
+ private static final String TAG = StandardClientActivity.class.getSimpleName();
private static final String API_KEY = BuildConfig.API_KEY; // Set in root-level local.properties
private EditText experiment;
private EditText subject;
@@ -60,6 +61,12 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_assigner);
+ // Enable the action bar back button
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle("Standard Client");
+ }
+
experiment = findViewById(R.id.experiment);
subject = findViewById(R.id.subject);
assignmentLog = findViewById(R.id.assignment_log);
@@ -95,6 +102,15 @@ private void appendToAssignmentLogView(String message) {
assignmentLogScrollView.post(() -> assignmentLogScrollView.fullScroll(View.FOCUS_DOWN));
}
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
// Tie into the activity's lifecycle and pause/resume polling where appropriate.
@Override
diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_home.xml
similarity index 82%
rename from example/src/main/res/layout/activity_main.xml
rename to example/src/main/res/layout/activity_home.xml
index 80f4152f..bc1b2bd4 100644
--- a/example/src/main/res/layout/activity_main.xml
+++ b/example/src/main/res/layout/activity_home.xml
@@ -24,10 +24,15 @@
android:id="@+id/button_start_offline_assigner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
-
-
android:text="@string/start_offline_assignment_activity" />
+
+