Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ce6d43f
fix(sdk): decode evaluation-reason wire spellings (isExperiment/prere…
JackDanger Oct 11, 2026
a243c23
fix(conn): hold event posts while configured FDv2 mode resolves OFFLINE
JackDanger Oct 11, 2026
c4bf163
seat(local lane): CP/stdio seat + hook callback bus for the centinel …
JackDanger Oct 11, 2026
3a73e59
seat: serve the SDK's own reason serialization on the response arm (n…
JackDanger Oct 11, 2026
be6fb88
seat: the coverage-lane capability face lands (the C-CO-WRAPPER/C-PY-…
JackDanger Oct 13, 2026
e9aec8f
seat: the hook series chain rides the SDK's own arm (the coverage ban…
JackDanger Oct 14, 2026
30bcb87
seat: the x-cp-set-streaming arm rides the SDK's own mode-resolution …
JackDanger Oct 17, 2026
bddf56f
fold origin/main d7d156a7 into the cp-seat lane (W10: the family face…
JackDanger Oct 17, 2026
3643790
seat (the K3 PR review ride's fix face): the 3 BLOCK findings close —…
JackDanger Oct 17, 2026
551dc55
fix(seat): decouple the contract-tests module from the seat module (t…
JackDanger Oct 17, 2026
3cce215
fix(hooks,conn): the family's own red faces close (the K3 rides' CI f…
JackDanger Oct 17, 2026
5a7f7b0
revert(seat): the connector event-gate dedupe over-repressed main's o…
JackDanger Oct 17, 2026
1deba84
fix(conn): the event-gate dedupe's last-stage face closes — updateEve…
JackDanger Oct 18, 2026
b725748
fix(seat): the CP ready envelope rides FIRST (Seat.main emits the mac…
JackDanger Oct 18, 2026
884efd2
fix(seat): the CP seat's runtime classpath rides okhttp-eventsource 5…
JackDanger Oct 18, 2026
0f46c5b
fix(hooks): the deduping after face prices ONLY its own marker identi…
JackDanger Oct 18, 2026
ec5a4d8
fix(seat): the closeClient reply prices CP-SPEC 4.4's EMPTY-2xx face …
JackDanger Oct 20, 2026
32f6b2a
declare(honors): the two implemented-but-undeclared faces crossed the…
JackDanger Oct 20, 2026
591eab7
ci: land the ConnectivityManagerTest gate's apply-click bytes — docs/…
JackDanger Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,13 @@ build
.project
.settings/
.vscode/

# CENTINEL seat lane (local branches; never pushed upstream): build outputs and probe leftovers
/gen/
/sdk-classes/
/seat-classes/
/sdk.jar
tools/seat-test/classes/
tools/seat-test/extracted-cons/
tools/seat-test/stub-src/
tools/seat-test/stub-src-tmp/
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,10 @@ We encourage pull requests and other contributions from the community. Check out
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
* [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
* [launchdarkly.com/blog](https://launchdarkly.com/blog/ "LaunchDarkly Blog Documentation") for the latest product updates

## Continuous integration

`docs/ci/contract-tests.yml.template` holds the CI gate's bytes: an operator applies
them at `.github/workflows/contract-tests.yml`, and the landed workflow prices the
`ConnectivityManagerTest` replay faces — the `a243c235` FDv2-OFFLINE double-window
trio — on every pull request to `main` (Ubuntu Actions runner, ubuntu-22.04, JDK 17).
Original file line number Diff line number Diff line change
@@ -1,10 +1,86 @@
package com.launchdarkly.sdk.android;

import com.launchdarkly.sdk.LDContext;

import java.util.concurrent.ConcurrentHashMap;

public class ConfigHelper {
// This method exists because Config.Builder.persistentDataStore is currently package-private -
// using a custom persistence component is not yet part of the public API.
public static LDConfig.Builder configureIsolatedInMemoryPersistence(LDConfig.Builder builder) {
builder.persistentDataStore(new InMemoryPersistentDataStore());
return builder;
}

/**
* CENTINEL client-lane extension (campaign client-INTERFACE section 5, PERSIST.5):
* with a storage scope every client created under the same scope tag shares one local
* store (successive launches of one app); without one each client stays isolated as
* before. Returns the store wired into the builder so the caller can preload data.
*/
private static final ConcurrentHashMap<String, InMemoryPersistentDataStore> SCOPED_STORES =
new ConcurrentHashMap<String, InMemoryPersistentDataStore>();

public static InMemoryPersistentDataStore configureInMemoryPersistence(LDConfig.Builder builder,
String storageScope) {
InMemoryPersistentDataStore store = storageScope == null
? new InMemoryPersistentDataStore()
: SCOPED_STORES.computeIfAbsent(storageScope, k -> new InMemoryPersistentDataStore());
builder.persistentDataStore(store);
return store;
}

/**
* CENTINEL client-lane extension: writes bootstrap flag data (the campaign's
* x-cp-bootstrap all-flags-state JSON) through the SDK's own persisted-store
* pathway, so the real {@code FDv2CacheInitializer} serves the values during
* client initialization — exactly the LD offline-cache bootstrapping contract.
*
* The all-flags-state shape carries values at the top level with per-flag
* metadata under {@code $flagsState}; the store format is the flat
* {@code {flagKey: Flag}} map, so the conversion keeps key/value/version/variation.
*/
public static void preloadBootstrapData(InMemoryPersistentDataStore store,
String mobileKey, java.util.Map<String, Object> bootData, LDContext initialContext) {
if (store == null || bootData == null || initialContext == null) {
return;
}
// Mirrors PersistentDataStoreWrapper's own namespacing: the per-environment
// namespace hashes the mobile key; the per-context key hashes the context's
// fully-qualified key. The literal prefixes live in the wrapper (private); the
// SDK's hash helpers are package-visible to this same-package caller.
String namespace = "LaunchDarkly_" + LDUtil.urlSafeBase64Hash(mobileKey == null ? "" : mobileKey);
String key = "flags_" + LDUtil.urlSafeBase64HashedContextId(initialContext);

com.google.gson.Gson gson = new com.google.gson.Gson();
com.google.gson.JsonObject flags = new com.google.gson.JsonObject();
Object stateRaw = bootData.get("$flagsState");
java.util.Map<?, ?> flagsState = stateRaw instanceof java.util.Map ? (java.util.Map<?, ?>) stateRaw : null;
for (java.util.Map.Entry<String, Object> e : bootData.entrySet()) {
String flagKey = e.getKey();
if (flagKey.startsWith("$")) {
continue; // $flagsState / $valid metadata
}
com.google.gson.JsonObject f = new com.google.gson.JsonObject();
f.addProperty("key", flagKey);
f.add("value", gson.toJsonTree(e.getValue()));
int version = 1;
Integer variation = null;
if (flagsState != null && flagsState.get(flagKey) instanceof java.util.Map<?, ?>) {
java.util.Map<?, ?> fs = (java.util.Map<?, ?>) flagsState.get(flagKey);
if (fs.get("version") instanceof Number) {
version = ((Number) fs.get("version")).intValue();
}
if (fs.get("variation") instanceof Number) {
variation = ((Number) fs.get("variation")).intValue();
}
}
f.addProperty("version", version);
if (variation != null) {
f.addProperty("variation", variation);
}
flags.add(flagKey, f);
}
store.setValue(namespace, key, gson.toJson(flags));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ public HookCallbackService(URI serviceUri) {
}

public void post(Object params) {
// CENTINEL seat extension (CP-SPEC 7.2): a cp-callback:// callback URI rides
// the CP/stdio callback wheel — the seat MUST NOT dial it; it emits a
// `callback` envelope and awaits the runner's reply on stdin before the
// SDK-side call completes. Any other scheme keeps the harness's own
// HTTP receiver arm (used when the seat runs under CP/http on a device).
if (serviceUri != null && "cp-callback".equals(serviceUri.getScheme())) {
SdkClientEntity.seatPostCallbackHTTPFace(serviceUri, params);
return;
}
RequestBody body = RequestBody.create(
TestService.gson.toJson(params == null ? "{}" : params),
MediaType.parse("application/json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static class SdkConfigParams {
SdkConfigServiceEndpointParams serviceEndpoints;
SdkConfigHookParams hooks;
SdkConfigDataSystemParams dataSystem;
SdkConfigWrapperParams wrapper;
}

public static class SdkConfigStreamParams {
Expand Down Expand Up @@ -74,11 +75,25 @@ public static class SdkConfigServiceEndpointParams {
String events;
}

/** cp.schema sdkConfiguration/properties/wrapper (v3:L384): the
* X-LaunchDarkly-Wrapper header config. Declared under the
* `wrapper` capability. */
public static class SdkConfigWrapperParams {
String name;
String version;
}

public static class SdkConfigClientSideParams {
LDContext initialContext;
boolean evaluationReasons;
boolean useReport;
boolean includeEnvironmentAttributes;

// CENTINEL seat extensions (campaign client-INTERFACE section 5): a blind storage
// scope tag whose clients share one local store (PERSIST.5), and bootstrap flag
// data in the all-flags-state shape for the SDK's pre-connection serve.
@SerializedName("x-cp-storage-scope") String xCpStorageScope;
@SerializedName("x-cp-bootstrap") java.util.Map<String, Object> xCpBootstrap;
}

public static class SdkConfigHookParams {
Expand All @@ -95,6 +110,9 @@ public static class HookConfig {
public static class HookData {
Map<String, Object> beforeEvaluation;
Map<String, Object> afterEvaluation;
Map<String, Object> afterTrack;
Map<String, Object> beforeIdentify;
Map<String, Object> afterIdentify;
}

public static class HookErrors {
Expand Down Expand Up @@ -137,6 +155,52 @@ public static class CommandParams {
CustomEventParams customEvent;
ContextBuildParams contextBuild;
ContextConvertParams contextConvert;
RegisterFlagChangeListenerParams registerFlagChangeListener;
UnregisterListenerParams unregisterListener;
ContextComparisonParams contextComparison;
@com.google.gson.annotations.SerializedName("x-cp-create-no-credential") XcpCreateNoCredentialParams xcpCreateNoCredential;
SetOverrideParams setOverride;
RemoveOverrideParams removeOverride;
@com.google.gson.annotations.SerializedName("x-cp-closed-evaluate") EvaluateFlagParams xcpClosedEvaluate;
@com.google.gson.annotations.SerializedName("x-cp-set-streaming") SetStreamingParams xcpSetStreaming;
}

/** x-cp-set-streaming arm params (cp.schema x-cp-set-streaming: `state`
* boolean|null — true forces streaming, false forces polling, null resets
* the SDK's automatic mode resolution). */
public static class SetStreamingParams {
Boolean state;
}

public static class SetOverrideParams {
String flagKey;
LDValue value;
}

public static class RemoveOverrideParams {
String flagKey;
}

/** x-cp-create-no-credential request arm (the harness's own x-cp face). */
public static class XcpCreateNoCredentialParams {
LDContext initialContext;
}

/** The context-comparison response (spec): `equals` prices the SDK's own
* LDContext equality (kind+key+attrs). */
public static class ContextComparisonResponse {
Boolean equals;
}

/** service_spec listener command params (L706/L754): the seat echoes
* listenerId in every notification for the listener. */
public static class RegisterFlagChangeListenerParams {
String listenerId;
URI callbackUri;
}

public static class UnregisterListenerParams {
String listenerId;
}

public static class EvaluateFlagParams {
Expand Down Expand Up @@ -169,10 +233,18 @@ public static class EvaluationSeriesCallbackParams {
String stage;
}

/** The identify hook series' callback payload (spec HOOK 1.4.x). */
public static class IdentifySeriesCallbackParams {
public com.launchdarkly.sdk.android.integrations.IdentifySeriesContext identifySeriesContext;
public Map<String, Object> identifySeriesData;
public com.launchdarkly.sdk.android.integrations.IdentifySeriesResult identifyResult;
public String stage;
}

public static class TrackSeriesCallbackParams {
public Map<String, Object> evaluationSeriesData;
TrackSeriesContext trackSeriesContext;
String stage;
}
String stage; }

public static class IdentifyEventParams {
LDContext context;
Expand Down Expand Up @@ -205,6 +277,11 @@ public static class ContextBuildResponse {
String error;
}

public static class ContextComparisonParams {
ContextBuildParams context1;
ContextBuildParams context2;
}

public static class ContextConvertParams {
String input;
}
Expand Down
Loading
Loading