Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 Content feature method `previewContent(String contentId)` (Experimental!).
* Improved content display and refresh mechanics.

* Mitigated an issue about health checks storage in explicit storage mode.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package ly.count.android.sdk;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class ModuleContentTests {

Countly mCountly;
List<String> capturedRequests;
List<String> capturedEndpoints;

@Before
public void setUp() {
TestUtils.getCountlyStore().clear();
capturedRequests = new ArrayList<>();
capturedEndpoints = new ArrayList<>();
}

@After
public void tearDown() {
}

private ImmediateRequestGenerator createCapturingIRGenerator() {
return new ImmediateRequestGenerator() {
@Override public ImmediateRequestI CreateImmediateRequestMaker() {
return (requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log) -> {
capturedRequests.add(requestData);
capturedEndpoints.add(customEndpoint);
};
}

@Override public ImmediateRequestI CreatePreflightRequestMaker() {
return (requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log) -> {
};
}
};
}

private Countly initWithConsent(boolean contentConsent) {
CountlyConfig config = TestUtils.createBaseConfig();
config.setRequiresConsent(true);
if (contentConsent) {
config.setConsentEnabled(new String[] { Countly.CountlyFeatureNames.content });
}
config.disableHealthCheck();
config.immediateRequestGenerator = createCapturingIRGenerator();

mCountly = new Countly();
mCountly.init(config);
mCountly.moduleContent.countlyTimer = null;
capturedRequests.clear();
capturedEndpoints.clear();
return mCountly;
}

private void setIsCurrentlyInContentZone(ModuleContent module, boolean value) throws Exception {
java.lang.reflect.Field field = ModuleContent.class.getDeclaredField("isCurrentlyInContentZone");
field.setAccessible(true);
field.set(module, value);
}

// ======== previewContent public API tests ========

/**
* Null and empty contentId should be rejected at the public API level.
* No request should be made.
*/
@Test
public void previewContent_invalidContentId() {
Countly countly = initWithConsent(true);

countly.contents().previewContent(null);
Assert.assertEquals(0, capturedRequests.size());

countly.contents().previewContent("");
Assert.assertEquals(0, capturedRequests.size());
}

/**
* Valid contentId with consent should make a request to /o/sdk/content
* containing content_id and preview=true parameters
*/
@Test
public void previewContent_validContentId() {
Countly countly = initWithConsent(true);

countly.contents().previewContent("test_content_123");

Assert.assertEquals(1, capturedRequests.size());
Assert.assertEquals("/o/sdk/content", capturedEndpoints.get(0));

String request = capturedRequests.get(0);
Assert.assertTrue(request.contains("content_id=test_content_123"));
Assert.assertTrue(request.contains("preview=true"));
}

/**
* Without content consent, no request should be made
*/
@Test
public void previewContent_noConsent() {
Countly countly = initWithConsent(false);

countly.contents().previewContent("test_content_id");

Assert.assertEquals(0, capturedRequests.size());
}

/**
* When content is already being displayed, no new request should be made
*/
@Test
public void previewContent_alreadyInContentZone() throws Exception {
Countly countly = initWithConsent(true);
setIsCurrentlyInContentZone(countly.moduleContent, true);

countly.contents().previewContent("test_content_id");

Assert.assertEquals(0, capturedRequests.size());
}

// ======== validateResponse tests ========

/**
* validateResponse returns true only when both "geo" and "html" are present,
* false for missing geo, missing html, or empty response
*/
@Test
public void validateResponse() throws JSONException {
Countly countly = initWithConsent(true);
ModuleContent mc = countly.moduleContent;

// empty
Assert.assertFalse(mc.validateResponse(new JSONObject()));

// missing geo
JSONObject noGeo = new JSONObject();
noGeo.put("html", "<html></html>");
Assert.assertFalse(mc.validateResponse(noGeo));

// missing html
JSONObject noHtml = new JSONObject();
noHtml.put("geo", new JSONObject());
Assert.assertFalse(mc.validateResponse(noHtml));

// valid
JSONObject valid = new JSONObject();
valid.put("geo", new JSONObject());
valid.put("html", "<html></html>");
Assert.assertTrue(mc.validateResponse(valid));
}
}
10 changes: 8 additions & 2 deletions sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,7 @@ public String prepareHealthCheckRequest(String preparedMetrics) {
return prepareCommonRequestData() + "&metrics=" + preparedMetrics;
}

public String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories, String language, String deviceType) {
public String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories, String language, String deviceType, @Nullable String contentId) {

JSONObject json = new JSONObject();
try {
Expand All @@ -896,7 +896,13 @@ public String prepareFetchContents(int portraitWidth, int portraitHeight, int la
L.e("Error while preparing fetch contents request");
}

return prepareCommonRequestData() + "&method=queue" + "&category=" + Arrays.asList(categories) + "&resolution=" + UtilsNetworking.urlEncodeString(json.toString()) + "&la=" + language + "&dt=" + deviceType;
String request = prepareCommonRequestData() + "&method=queue" + "&category=" + Arrays.asList(categories) + "&resolution=" + UtilsNetworking.urlEncodeString(json.toString()) + "&la=" + language + "&dt=" + deviceType;

if (contentId != null) {
request += "&content_id=" + UtilsNetworking.urlEncodeString(contentId) + "&preview=true";
}

return request;
}

@Override
Expand Down
49 changes: 43 additions & 6 deletions sdk/src/main/java/ly/count/android/sdk/ModuleContent.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ void onActivityStopped(int updatedActivityCount) {
}
}

void fetchContentsInternal(@NonNull String[] categories, @Nullable Runnable callbackOnFailure) {
L.d("[ModuleContent] fetchContentsInternal, shouldFetchContents: [" + shouldFetchContents + "], categories: [" + Arrays.toString(categories) + "]");
void fetchContentsInternal(@NonNull String[] categories, @Nullable Runnable callbackOnFailure, @Nullable String contentId) {
L.d("[ModuleContent] fetchContentsInternal, shouldFetchContents: [" + shouldFetchContents + "], categories: [" + Arrays.toString(categories) + "], contentId: [" + contentId + "]");

DisplayMetrics displayMetrics = deviceInfo.mp.getDisplayMetrics(_cly.context_);
String requestData = prepareContentFetchRequest(displayMetrics, categories);
String requestData = prepareContentFetchRequest(displayMetrics, categories, contentId);

ConnectionProcessor cp = requestQueueProvider.createConnectionProcessor();
final boolean networkingIsEnabled = cp.configProvider_.getNetworkingEnabled();
Expand Down Expand Up @@ -206,7 +206,7 @@ private void enterContentZoneInternal(@Nullable String[] categories, final int i
return;
}

fetchContentsInternal(validCategories, callbackOnFailure);
fetchContentsInternal(validCategories, callbackOnFailure, null);
}
}, L);
}
Expand Down Expand Up @@ -308,7 +308,7 @@ void notifyAfterContentIsClosed() {
}

@NonNull
private String prepareContentFetchRequest(@NonNull DisplayMetrics displayMetrics, @NonNull String[] categories) {
private String prepareContentFetchRequest(@NonNull DisplayMetrics displayMetrics, @NonNull String[] categories, @Nullable String contentId) {
Resources resources = _cly.context_.getResources();
int currentOrientation = resources.getConfiguration().orientation;
boolean portrait = currentOrientation == Configuration.ORIENTATION_PORTRAIT;
Expand Down Expand Up @@ -353,7 +353,7 @@ private String prepareContentFetchRequest(@NonNull DisplayMetrics displayMetrics
String language = Locale.getDefault().getLanguage().toLowerCase();
String deviceType = deviceInfo.mp.getDeviceType(_cly.context_);

return requestQueueProvider.prepareFetchContents(portraitWidth, portraitHeight, landscapeWidth, landscapeHeight, categories, language, deviceType);
return requestQueueProvider.prepareFetchContents(portraitWidth, portraitHeight, landscapeWidth, landscapeHeight, categories, language, deviceType, contentId);
}

boolean validateResponse(@NonNull JSONObject response) {
Expand Down Expand Up @@ -472,6 +472,27 @@ private void exitContentZoneInternal() {
pendingContentConfigs = null;
}

void previewContentInternal(@NonNull String contentId) {
L.d("[ModuleContent] previewContentInternal, contentId: [" + contentId + "]");

if (!consentProvider.getConsent(Countly.CountlyFeatureNames.content)) {
L.w("[ModuleContent] previewContentInternal, Consent is not granted, skipping");
return;
}

if (deviceIdProvider.isTemporaryIdEnabled()) {
L.w("[ModuleContent] previewContentInternal, temporary device ID is enabled, skipping");
return;
}

if (isCurrentlyInContentZone) {
L.w("[ModuleContent] previewContentInternal, content is already being displayed, skipping");
return;
}

fetchContentsInternal(new String[] {}, null, contentId);
}

void refreshContentZoneInternal(boolean callRQFlush) {
if (!configProvider.getRefreshContentZoneEnabled()) {
return;
Expand Down Expand Up @@ -528,6 +549,22 @@ public void exitContentZone() {
exitContentZoneInternal();
}

/**
* Previews a specific content by its ID.
* This performs a one-time fetch for the given content
* without starting periodic content updates.
*
* @param contentId the ID of the content to preview
*/
public void previewContent(@Nullable String contentId) {
if (Utils.isNullOrEmpty(contentId)) {
L.w("[ModuleContent] previewContent, contentId is null or empty, skipping");
return;
}

previewContentInternal(contentId);
}

/**
* Triggers a manual refresh of the content zone.
* This method forces an update by fetching the latest content,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ interface RequestQueueProvider {

String prepareHealthCheckRequest(String preparedMetrics);

String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories, String language, String deviceType);
String prepareFetchContents(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, String[] categories, String language, String deviceType, @Nullable String contentId);

void registerInternalGlobalRequestCallbackAction(Runnable runnable);
}
Loading