Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## XX.XX.XX
* Added support for multiple independent SDK instances, each with isolated storage, request queue, and device ID. Access a named instance with `Countly.instance(name)` and initialize it yourself, manage instances with `Countly.getInstance(name)`, `Countly.listInstances()`, `Countly.haltAllInstances()`, and `Countly.removeInstance(name)`, and optionally record the intended name via `CountlyConfig.setInstanceName(String)`. `Countly.sharedInstance()` is unchanged, so existing integrations keep working.
* Improved the security of content, feedback widget, and push notification links by blocking the `data:`, `zip:`, and `intent:` URI schemes by default, both for opening links and for loading web view resources. They can be allowed with `setAllowedIntentSchemes(List)`.

## 26.1.5
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@
android:name=".ActivityExampleSessions"
android:label="@string/activity_name_sessions"
android:configChanges="orientation|screenSize"/>

<activity
android:name=".ActivityExampleMultiInstance"
android:label="@string/activity_name_multi_instance"
android:configChanges="orientation|screenSize"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package ly.count.android.demo;

import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import java.util.List;

import ly.count.android.sdk.Countly;
import ly.count.android.sdk.CountlyConfig;

/**
* Demonstrates running several independent Countly instances alongside the default (shared) one.
*
* Each named instance keeps its own request queue, event queue, device ID, consent state, logging
* state, and stored configuration, fully isolated from {@code Countly.sharedInstance()}. For demo
* simplicity the named instances are pointed at the same server and app key as the default instance
* (each with a distinct device ID); a real integration would use a separate Countly application's
* credentials.
*/
public class ActivityExampleMultiInstance extends AppCompatActivity {
private static final String ANALYTICS = "analytics";
private static final String BILLING = "billing";
private boolean analyticsLogging = true;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_example_multi_instance);

// --- analytics instance ---
findViewById(R.id.btnCreateAnalytics).setOnClickListener(v -> createAndInit(ANALYTICS, "analytics-device"));

findViewById(R.id.btnRecordEventAnalytics).setOnClickListener(v -> {
if (!requireInit(ANALYTICS)) {
return;
}
Countly.instance(ANALYTICS).events().recordEvent("analytics_event");
toast("Recorded 'analytics_event' on '" + ANALYTICS + "'");
});

findViewById(R.id.btnRecordViewAnalytics).setOnClickListener(v -> {
if (!requireInit(ANALYTICS)) {
return;
}
Countly.instance(ANALYTICS).views().startAutoStoppedView("AnalyticsScreen");
toast("Started view 'AnalyticsScreen' on '" + ANALYTICS + "'");
});

findViewById(R.id.btnToggleLogAnalytics).setOnClickListener(v -> {
if (!requireInit(ANALYTICS)) {
return;
}
analyticsLogging = !analyticsLogging;
Countly.instance(ANALYTICS).setLoggingEnabled(analyticsLogging);
toast("'" + ANALYTICS + "' logging " + (analyticsLogging ? "ENABLED" : "DISABLED") + " (default instance logging unaffected)");
});

findViewById(R.id.btnRemoveAnalytics).setOnClickListener(v -> {
if (Countly.getInstance(ANALYTICS) == null) {
toast("'" + ANALYTICS + "' is not registered, nothing to remove");
return;
}
Countly.removeInstance(ANALYTICS);
toast("Removed the '" + ANALYTICS + "' instance");
});

// --- billing instance (second named instance) ---
findViewById(R.id.btnCreateBilling).setOnClickListener(v -> createAndInit(BILLING, "billing-device"));

findViewById(R.id.btnRecordEventBilling).setOnClickListener(v -> {
if (!requireInit(BILLING)) {
return;
}
Countly.instance(BILLING).events().recordEvent("billing_event");
toast("Recorded 'billing_event' on '" + BILLING + "'");
});

// --- default instance (contrast) ---
findViewById(R.id.btnRecordEventDefault).setOnClickListener(v -> {
Countly.sharedInstance().events().recordEvent("default_event");
toast("Recorded 'default_event' on the default (shared) instance");
});

// --- registry ---
findViewById(R.id.btnList).setOnClickListener(v -> {
List<String> names = Countly.listInstances();
toast("Named instances: " + (names.isEmpty() ? "(none)" : names));
});

findViewById(R.id.btnGetAnalytics).setOnClickListener(v -> {
Countly existing = Countly.getInstance(ANALYTICS);
if (existing == null) {
toast("'" + ANALYTICS + "' is not registered");
} else {
toast("'" + ANALYTICS + "' exists, initialized: " + existing.isInitialized());
}
});

findViewById(R.id.btnHaltAll).setOnClickListener(v -> {
Countly.haltAllInstances();
toast("Halted all instances (each reset but still registered)");
});
}

private void createAndInit(String name, String deviceId) {
Countly instance = Countly.instance(name);
if (instance.isInitialized()) {
toast("'" + name + "' is already initialized");
return;
}

// The name passed to Countly.instance(name) is what isolates this instance's storage. The
// distinct device ID keeps its identity separate. No application is set, so this instance
// records manually rather than tracking sessions/views automatically.
CountlyConfig config = new CountlyConfig(getApplicationContext(), App.getAppKey(), App.getServerUrl())
.setInstanceName(name)
.setDeviceId(deviceId)
.setLoggingEnabled(true);

instance.init(config);
toast("Initialized '" + name + "' (device id: " + deviceId + ")");
}

private boolean requireInit(String name) {
Countly instance = Countly.getInstance(name);
if (instance == null || !instance.isInitialized()) {
toast("Create and initialize the '" + name + "' instance first");
return false;
}
return true;
}

private void toast(String message) {
Log.d(Countly.TAG, "[MultiInstanceDemo] " + message);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
10 changes: 10 additions & 0 deletions app/src/main/java/ly/count/android/demo/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ public class App extends Application {

private final static long applicationStartTimestamp = System.currentTimeMillis();

// Exposed so example activities (such as the multi-instance demo) can spin up additional named
// instances pointed at the same server and app key without duplicating the configuration.
public static String getServerUrl() {
return COUNTLY_SERVER_URL;
}

public static String getAppKey() {
return COUNTLY_APP_KEY;
}

@Override
public void onCreate() {
super.onCreate();
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/ly/count/android/demo/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,8 @@ public void onClickButtonLocation(View v) {
public void onClickButtonSessions(View v) {
startActivity(new Intent(this, ActivityExampleSessions.class));
}

public void onClickButtonMultiInstance(View v) {
startActivity(new Intent(this, ActivityExampleMultiInstance.class));
}
}
154 changes: 154 additions & 0 deletions app/src/main/res/layout/activity_example_multi_instance.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<com.google.android.material.card.MaterialCardView
style="@style/FormCard"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Multiple Instances"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="4dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Independent instances 'analytics' and 'billing', each with its own queues, device ID, and logging, isolated from the default instance. They reuse this demo's server and app key for simplicity."
android:textColor="#FF9800"
android:textSize="13sp"
android:layout_marginBottom="16dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="analytics instance"
android:textStyle="bold"
android:textSize="14sp"
android:layout_marginBottom="8dp" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnCreateAnalytics"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Create &amp; Init 'analytics'" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnRecordEventAnalytics"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Record Event on 'analytics'" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnRecordViewAnalytics"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Record View on 'analytics'" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnToggleLogAnalytics"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Toggle 'analytics' Logging" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnRemoveAnalytics"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="Remove 'analytics'" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="billing instance"
android:textStyle="bold"
android:textSize="14sp"
android:layout_marginBottom="8dp" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnCreateBilling"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Create &amp; Init 'billing'" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnRecordEventBilling"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="Record Event on 'billing'" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="default instance &amp; registry"
android:textStyle="bold"
android:textSize="14sp"
android:layout_marginBottom="8dp" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnRecordEventDefault"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Record Event on Default Instance" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnList"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="List Instances" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnGetAnalytics"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Get 'analytics' Instance" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnHaltAll"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Halt All Instances" />

</LinearLayout>

</com.google.android.material.card.MaterialCardView>

</LinearLayout>

</ScrollView>
41 changes: 41 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,47 @@
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

<com.google.android.material.card.MaterialCardView
style="@style/MenuCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClickButtonMultiInstance">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<View
android:layout_width="5dp"
android:layout_height="match_parent"
android:background="@color/cardStripAdvanced" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Multiple Instances"
android:textColor="@color/colorOnSurface"
android:textSize="16sp"
android:textStyle="bold" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="Run independent SDK instances"
android:textColor="@color/textSecondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

<com.google.android.material.card.MaterialCardView
style="@style/MenuCard"
android:layout_width="match_parent"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@
<string name="activity_name_consent">Consent Management</string>
<string name="activity_name_location">Location</string>
<string name="activity_name_sessions">Sessions</string>
<string name="activity_name_multi_instance">Multiple Instances</string>
</resources>
Loading
Loading