Skip to content

Commit eb63b5a

Browse files
authored
Merge pull request #538 from Countly/preview_content
Preview content
2 parents 22f8a74 + 62fc7b0 commit eb63b5a

5 files changed

Lines changed: 214 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## XX.XX.XX
2+
* Added Content feature method `previewContent(String contentId)` (Experimental!).
23
* Improved content display and refresh mechanics.
34

45
* Mitigated an issue about health checks storage in explicit storage mode.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package ly.count.android.sdk;
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import org.json.JSONException;
7+
import org.json.JSONObject;
8+
import org.junit.After;
9+
import org.junit.Assert;
10+
import org.junit.Before;
11+
import org.junit.Test;
12+
import org.junit.runner.RunWith;
13+
14+
@RunWith(AndroidJUnit4.class)
15+
public class ModuleContentTests {
16+
17+
Countly mCountly;
18+
List<String> capturedRequests;
19+
List<String> capturedEndpoints;
20+
21+
@Before
22+
public void setUp() {
23+
TestUtils.getCountlyStore().clear();
24+
capturedRequests = new ArrayList<>();
25+
capturedEndpoints = new ArrayList<>();
26+
}
27+
28+
@After
29+
public void tearDown() {
30+
}
31+
32+
private ImmediateRequestGenerator createCapturingIRGenerator() {
33+
return new ImmediateRequestGenerator() {
34+
@Override public ImmediateRequestI CreateImmediateRequestMaker() {
35+
return (requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log) -> {
36+
capturedRequests.add(requestData);
37+
capturedEndpoints.add(customEndpoint);
38+
};
39+
}
40+
41+
@Override public ImmediateRequestI CreatePreflightRequestMaker() {
42+
return (requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log) -> {
43+
};
44+
}
45+
};
46+
}
47+
48+
private Countly initWithConsent(boolean contentConsent) {
49+
CountlyConfig config = TestUtils.createBaseConfig();
50+
config.setRequiresConsent(true);
51+
if (contentConsent) {
52+
config.setConsentEnabled(new String[] { Countly.CountlyFeatureNames.content });
53+
}
54+
config.disableHealthCheck();
55+
config.immediateRequestGenerator = createCapturingIRGenerator();
56+
57+
mCountly = new Countly();
58+
mCountly.init(config);
59+
mCountly.moduleContent.countlyTimer = null;
60+
capturedRequests.clear();
61+
capturedEndpoints.clear();
62+
return mCountly;
63+
}
64+
65+
private void setIsCurrentlyInContentZone(ModuleContent module, boolean value) throws Exception {
66+
java.lang.reflect.Field field = ModuleContent.class.getDeclaredField("isCurrentlyInContentZone");
67+
field.setAccessible(true);
68+
field.set(module, value);
69+
}
70+
71+
// ======== previewContent public API tests ========
72+
73+
/**
74+
* Null and empty contentId should be rejected at the public API level.
75+
* No request should be made.
76+
*/
77+
@Test
78+
public void previewContent_invalidContentId() {
79+
Countly countly = initWithConsent(true);
80+
81+
countly.contents().previewContent(null);
82+
Assert.assertEquals(0, capturedRequests.size());
83+
84+
countly.contents().previewContent("");
85+
Assert.assertEquals(0, capturedRequests.size());
86+
}
87+
88+
/**
89+
* Valid contentId with consent should make a request to /o/sdk/content
90+
* containing content_id and preview=true parameters
91+
*/
92+
@Test
93+
public void previewContent_validContentId() {
94+
Countly countly = initWithConsent(true);
95+
96+
countly.contents().previewContent("test_content_123");
97+
98+
Assert.assertEquals(1, capturedRequests.size());
99+
Assert.assertEquals("/o/sdk/content", capturedEndpoints.get(0));
100+
101+
String request = capturedRequests.get(0);
102+
Assert.assertTrue(request.contains("content_id=test_content_123"));
103+
Assert.assertTrue(request.contains("preview=true"));
104+
}
105+
106+
/**
107+
* Without content consent, no request should be made
108+
*/
109+
@Test
110+
public void previewContent_noConsent() {
111+
Countly countly = initWithConsent(false);
112+
113+
countly.contents().previewContent("test_content_id");
114+
115+
Assert.assertEquals(0, capturedRequests.size());
116+
}
117+
118+
/**
119+
* When content is already being displayed, no new request should be made
120+
*/
121+
@Test
122+
public void previewContent_alreadyInContentZone() throws Exception {
123+
Countly countly = initWithConsent(true);
124+
setIsCurrentlyInContentZone(countly.moduleContent, true);
125+
126+
countly.contents().previewContent("test_content_id");
127+
128+
Assert.assertEquals(0, capturedRequests.size());
129+
}
130+
131+
// ======== validateResponse tests ========
132+
133+
/**
134+
* validateResponse returns true only when both "geo" and "html" are present,
135+
* false for missing geo, missing html, or empty response
136+
*/
137+
@Test
138+
public void validateResponse() throws JSONException {
139+
Countly countly = initWithConsent(true);
140+
ModuleContent mc = countly.moduleContent;
141+
142+
// empty
143+
Assert.assertFalse(mc.validateResponse(new JSONObject()));
144+
145+
// missing geo
146+
JSONObject noGeo = new JSONObject();
147+
noGeo.put("html", "<html></html>");
148+
Assert.assertFalse(mc.validateResponse(noGeo));
149+
150+
// missing html
151+
JSONObject noHtml = new JSONObject();
152+
noHtml.put("geo", new JSONObject());
153+
Assert.assertFalse(mc.validateResponse(noHtml));
154+
155+
// valid
156+
JSONObject valid = new JSONObject();
157+
valid.put("geo", new JSONObject());
158+
valid.put("html", "<html></html>");
159+
Assert.assertTrue(mc.validateResponse(valid));
160+
}
161+
}

sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,7 @@ public String prepareHealthCheckRequest(String preparedMetrics) {
878878
return prepareCommonRequestData() + "&metrics=" + preparedMetrics;
879879
}
880880

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

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

899-
return prepareCommonRequestData() + "&method=queue" + "&category=" + Arrays.asList(categories) + "&resolution=" + UtilsNetworking.urlEncodeString(json.toString()) + "&la=" + language + "&dt=" + deviceType;
899+
String request = prepareCommonRequestData() + "&method=queue" + "&category=" + Arrays.asList(categories) + "&resolution=" + UtilsNetworking.urlEncodeString(json.toString()) + "&la=" + language + "&dt=" + deviceType;
900+
901+
if (contentId != null) {
902+
request += "&content_id=" + UtilsNetworking.urlEncodeString(contentId) + "&preview=true";
903+
}
904+
905+
return request;
900906
}
901907

902908
@Override

sdk/src/main/java/ly/count/android/sdk/ModuleContent.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@ void onActivityStopped(int updatedActivityCount) {
105105
}
106106
}
107107

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

111111
DisplayMetrics displayMetrics = deviceInfo.mp.getDisplayMetrics(_cly.context_);
112-
String requestData = prepareContentFetchRequest(displayMetrics, categories);
112+
String requestData = prepareContentFetchRequest(displayMetrics, categories, contentId);
113113

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

209-
fetchContentsInternal(validCategories, callbackOnFailure);
209+
fetchContentsInternal(validCategories, callbackOnFailure, null);
210210
}
211211
}, L);
212212
}
@@ -308,7 +308,7 @@ void notifyAfterContentIsClosed() {
308308
}
309309

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

356-
return requestQueueProvider.prepareFetchContents(portraitWidth, portraitHeight, landscapeWidth, landscapeHeight, categories, language, deviceType);
356+
return requestQueueProvider.prepareFetchContents(portraitWidth, portraitHeight, landscapeWidth, landscapeHeight, categories, language, deviceType, contentId);
357357
}
358358

359359
boolean validateResponse(@NonNull JSONObject response) {
@@ -472,6 +472,27 @@ private void exitContentZoneInternal() {
472472
pendingContentConfigs = null;
473473
}
474474

475+
void previewContentInternal(@NonNull String contentId) {
476+
L.d("[ModuleContent] previewContentInternal, contentId: [" + contentId + "]");
477+
478+
if (!consentProvider.getConsent(Countly.CountlyFeatureNames.content)) {
479+
L.w("[ModuleContent] previewContentInternal, Consent is not granted, skipping");
480+
return;
481+
}
482+
483+
if (deviceIdProvider.isTemporaryIdEnabled()) {
484+
L.w("[ModuleContent] previewContentInternal, temporary device ID is enabled, skipping");
485+
return;
486+
}
487+
488+
if (isCurrentlyInContentZone) {
489+
L.w("[ModuleContent] previewContentInternal, content is already being displayed, skipping");
490+
return;
491+
}
492+
493+
fetchContentsInternal(new String[] {}, null, contentId);
494+
}
495+
475496
void refreshContentZoneInternal(boolean callRQFlush) {
476497
if (!configProvider.getRefreshContentZoneEnabled()) {
477498
return;
@@ -528,6 +549,22 @@ public void exitContentZone() {
528549
exitContentZoneInternal();
529550
}
530551

552+
/**
553+
* Previews a specific content by its ID.
554+
* This performs a one-time fetch for the given content
555+
* without starting periodic content updates.
556+
*
557+
* @param contentId the ID of the content to preview
558+
*/
559+
public void previewContent(@Nullable String contentId) {
560+
if (Utils.isNullOrEmpty(contentId)) {
561+
L.w("[ModuleContent] previewContent, contentId is null or empty, skipping");
562+
return;
563+
}
564+
565+
previewContentInternal(contentId);
566+
}
567+
531568
/**
532569
* Triggers a manual refresh of the content zone.
533570
* This method forces an update by fetching the latest content,

sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ interface RequestQueueProvider {
7474

7575
String prepareHealthCheckRequest(String preparedMetrics);
7676

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

7979
void registerInternalGlobalRequestCallbackAction(Runnable runnable);
8080
}

0 commit comments

Comments
 (0)