Skip to content

Commit 28fb2b4

Browse files
feat: update Journeylitics.start to accept Activity and throttle scroll events
1 parent db92bb1 commit 28fb2b4

File tree

4 files changed

+103
-92
lines changed

4 files changed

+103
-92
lines changed

example-app/src/main/res/layout/activity_main.xml

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,69 +11,78 @@
1111
android:paddingTop="60dp"
1212
tools:context=".MainActivity">
1313

14-
<com.google.android.flexbox.FlexboxLayout
14+
<HorizontalScrollView
1515
android:layout_width="match_parent"
16-
android:layout_height="wrap_content"
17-
app:flexDirection="row"
18-
app:flexWrap="wrap"
19-
app:alignItems="center">
20-
<Spinner
21-
android:id="@+id/sizes"
22-
android:theme="@style/ThemeOverlay.AppCompat.Light"
23-
android:spinnerMode="dropdown"
24-
android:layout_width="wrap_content"
25-
android:layout_height="wrap_content"
26-
android:layout_marginVertical="12dp" />
27-
<CheckBox
28-
android:id="@+id/loading"
29-
android:layout_width="wrap_content"
30-
android:layout_height="wrap_content"
31-
android:minHeight="48dp"
32-
android:checked="true"
33-
android:text="@string/loading"
34-
style="@style/CheckBoxText" />
35-
<CheckBox
36-
android:id="@+id/hide_dialog"
37-
android:layout_width="wrap_content"
38-
android:layout_height="wrap_content"
39-
android:minHeight="48dp"
40-
android:checked="false"
41-
android:text="@string/hide_dialog"
42-
style="@style/CheckBoxText" />
43-
<CheckBox
44-
android:id="@+id/webViewDebug"
45-
android:layout_width="wrap_content"
46-
android:layout_height="wrap_content"
47-
android:minHeight="48dp"
48-
android:checked="false"
49-
android:text="@string/web_view_debug"
50-
style="@style/CheckBoxText" />
51-
<CheckBox
52-
android:id="@+id/hwAccel"
53-
android:layout_width="wrap_content"
54-
android:layout_height="wrap_content"
55-
android:minHeight="48dp"
56-
android:checked="true"
57-
android:text="@string/hw_accel"
58-
style="@style/CheckBoxText" />
16+
android:layout_height="50dp"
17+
android:fillViewport="false"
18+
android:fadingEdgeLength="64dp"
19+
android:requiresFadingEdge="horizontal">
5920

60-
<CheckBox
61-
android:id="@+id/themeDark"
62-
android:layout_width="wrap_content"
63-
android:layout_height="wrap_content"
64-
android:minHeight="48dp"
65-
android:checked="false"
66-
android:text="@string/theme_dark"
67-
style="@style/CheckBoxText" />
68-
<CheckBox
69-
android:id="@+id/userJourney"
21+
<LinearLayout
7022
android:layout_width="wrap_content"
71-
android:layout_height="wrap_content"
72-
android:minHeight="48dp"
73-
android:checked="false"
74-
android:text="@string/user_journey"
75-
style="@style/CheckBoxText" />
76-
</com.google.android.flexbox.FlexboxLayout>
23+
android:layout_height="match_parent"
24+
android:orientation="horizontal"
25+
android:gravity="center_vertical">
26+
27+
<Spinner
28+
android:id="@+id/sizes"
29+
android:theme="@style/ThemeOverlay.AppCompat.Light"
30+
android:spinnerMode="dropdown"
31+
android:layout_width="wrap_content"
32+
android:layout_height="wrap_content" />
33+
<CheckBox
34+
android:id="@+id/loading"
35+
android:layout_width="wrap_content"
36+
android:layout_height="wrap_content"
37+
android:layout_gravity="center_vertical"
38+
android:minHeight="48dp"
39+
android:checked="true"
40+
android:text="@string/loading"
41+
style="@style/CheckBoxText" />
42+
<CheckBox
43+
android:id="@+id/hide_dialog"
44+
android:layout_width="wrap_content"
45+
android:layout_height="wrap_content"
46+
android:minHeight="48dp"
47+
android:checked="false"
48+
android:text="@string/hide_dialog"
49+
style="@style/CheckBoxText" />
50+
<CheckBox
51+
android:id="@+id/webViewDebug"
52+
android:layout_width="wrap_content"
53+
android:layout_height="wrap_content"
54+
android:minHeight="48dp"
55+
android:checked="false"
56+
android:text="@string/web_view_debug"
57+
style="@style/CheckBoxText" />
58+
<CheckBox
59+
android:id="@+id/hwAccel"
60+
android:layout_width="wrap_content"
61+
android:layout_height="wrap_content"
62+
android:minHeight="48dp"
63+
android:checked="true"
64+
android:text="@string/hw_accel"
65+
style="@style/CheckBoxText" />
66+
67+
<CheckBox
68+
android:id="@+id/themeDark"
69+
android:layout_width="wrap_content"
70+
android:layout_height="wrap_content"
71+
android:minHeight="48dp"
72+
android:checked="false"
73+
android:text="@string/theme_dark"
74+
style="@style/CheckBoxText" />
75+
<CheckBox
76+
android:id="@+id/userJourney"
77+
android:layout_width="wrap_content"
78+
android:layout_height="wrap_content"
79+
android:minHeight="48dp"
80+
android:checked="false"
81+
android:text="@string/user_journey"
82+
style="@style/CheckBoxText" />
83+
</LinearLayout>
84+
85+
</HorizontalScrollView>
7786

7887
<!-- Phone input row -->
7988
<LinearLayout

sdk/src/main/java/com/hcaptcha/sdk/journeylitics/Journeylitics.java

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import android.app.Activity;
44
import android.app.Application;
5-
import android.content.Context;
65
import android.os.Bundle;
6+
import android.os.SystemClock;
77
import android.view.View;
88
import android.view.ViewGroup;
99
import android.view.ViewParent;
@@ -42,6 +42,8 @@ public class Journeylitics {
4242
private static JLConfig sConfig = JLConfig.DEFAULT;
4343
private static final CopyOnWriteArrayList<JLSink> SINKS = new CopyOnWriteArrayList<>();
4444
private static final WeakHashMap<View, Boolean> INSTRUMENTED = new WeakHashMap<>();
45+
private static final WeakHashMap<View, Long> LAST_SCROLL_EVENT_AT = new WeakHashMap<>();
46+
private static final long SCROLL_EVENT_MIN_INTERVAL_MS = 250;
4547

4648
private static final class ListenerLookup<T> {
4749
private final T listener;
@@ -117,24 +119,21 @@ public void onActivityDestroyed(Activity activity) {
117119
};
118120

119121
@MainThread
120-
public static void start(Context context) {
121-
start(context, JLConfig.DEFAULT);
122+
public static void start(Activity activity) {
123+
start(activity, JLConfig.DEFAULT);
122124
}
123125

124126
@MainThread
125-
public static void start(Context context, JLConfig configuration) {
127+
public static void start(Activity activity, JLConfig configuration) {
126128
if (!STARTED.compareAndSet(false, true)) {
127129
return;
128130
}
129-
final Context appCtx = context.getApplicationContext();
130-
if (!(appCtx instanceof Application)) {
131-
throw new IllegalArgumentException("context must be Application or provide applicationContext");
132-
}
133-
sApp = (Application) appCtx;
131+
sApp = activity.getApplication();
134132
sConfig = configuration;
135133
SINKS.clear();
136134
SINKS.addAll(configuration.getSinks());
137135
sApp.registerActivityLifecycleCallbacks(LIFECYCLE_CALLBACKS);
136+
instrumentViews(activity);
138137
}
139138

140139
public static boolean isStarted() {
@@ -207,9 +206,9 @@ private static void traverseAndHook(View view) {
207206
} else if (view instanceof SearchView) {
208207
hookSearch((SearchView) view);
209208
} else if (view instanceof ScrollView) {
210-
hookScrollView((ScrollView) view);
209+
hookScrollView(view);
211210
} else if (view instanceof HorizontalScrollView) {
212-
hookHScrollView((HorizontalScrollView) view);
211+
hookScrollView(view);
213212
}
214213

215214
if (view instanceof ViewGroup) {
@@ -534,28 +533,19 @@ public boolean onEditorAction(TextView textView, int actionId,
534533
}
535534
}
536535

537-
private static void hookScrollView(ScrollView scrollView) {
538-
scrollView.getViewTreeObserver().addOnScrollChangedListener(
539-
new ViewTreeObserver.OnScrollChangedListener() {
540-
@Override
541-
public void onScrollChanged() {
542-
if (sConfig.isEnableScrolls()) {
543-
final Map<String, Object> meta = MetaMapHelper.createMetaMap(
544-
new AbstractMap.SimpleEntry<>(FieldKey.ID, viewIdName(scrollView)),
545-
new AbstractMap.SimpleEntry<>(FieldKey.ACTION, "scroll")
546-
);
547-
emit(EventKind.drag, scrollView.getClass().getSimpleName(), meta);
548-
}
549-
}
550-
});
551-
}
552-
553-
private static void hookHScrollView(HorizontalScrollView scrollView) {
536+
private static void hookScrollView(View scrollView) {
554537
scrollView.getViewTreeObserver().addOnScrollChangedListener(
555538
new ViewTreeObserver.OnScrollChangedListener() {
556539
@Override
557540
public void onScrollChanged() {
558541
if (sConfig.isEnableScrolls()) {
542+
final long now = SystemClock.uptimeMillis();
543+
final Long lastEventAt = LAST_SCROLL_EVENT_AT.get(scrollView);
544+
if (lastEventAt != null
545+
&& now - lastEventAt < SCROLL_EVENT_MIN_INTERVAL_MS) {
546+
return;
547+
}
548+
LAST_SCROLL_EVENT_AT.put(scrollView, now);
559549
final Map<String, Object> meta = MetaMapHelper.createMetaMap(
560550
new AbstractMap.SimpleEntry<>(FieldKey.ID, viewIdName(scrollView)),
561551
new AbstractMap.SimpleEntry<>(FieldKey.ACTION, "scroll")

sdk/src/test/java/com/hcaptcha/sdk/HCaptchaStopEventsTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ private static void resetJourneyliticsState() throws Exception {
4545
final Field instrumentedField = Journeylitics.class.getDeclaredField("INSTRUMENTED");
4646
instrumentedField.setAccessible(true);
4747
((Map<?, ?>) instrumentedField.get(null)).clear();
48+
49+
final Field scrollEventField = Journeylitics.class.getDeclaredField("LAST_SCROLL_EVENT_AT");
50+
scrollEventField.setAccessible(true);
51+
((Map<?, ?>) scrollEventField.get(null)).clear();
4852
}
4953

5054
@Test
@@ -55,6 +59,7 @@ public void stopEvents_unregisters_sink() throws Exception {
5559
final Application app = mock(Application.class);
5660
when(app.getApplicationContext()).thenReturn(app);
5761
when(activity.getApplicationContext()).thenReturn(app);
62+
when(activity.getApplication()).thenReturn(app);
5863

5964
final HCaptchaConfig config = HCaptchaConfig.builder()
6065
.siteKey(HCaptchaConfigTest.MOCK_SITE_KEY)

sdk/src/test/java/com/hcaptcha/sdk/journeylitics/JourneyliticsTest.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.hcaptcha.sdk.journeylitics;
22

3+
import android.app.Activity;
34
import android.app.Application;
45

56
import com.fasterxml.jackson.databind.JsonNode;
@@ -40,6 +41,10 @@ private static void resetJourneyliticsState() throws Exception {
4041
final Field instrumentedField = Journeylitics.class.getDeclaredField("INSTRUMENTED");
4142
instrumentedField.setAccessible(true);
4243
((Map<?, ?>) instrumentedField.get(null)).clear();
44+
45+
final Field scrollEventField = Journeylitics.class.getDeclaredField("LAST_SCROLL_EVENT_AT");
46+
scrollEventField.setAccessible(true);
47+
((Map<?, ?>) scrollEventField.get(null)).clear();
4348
}
4449

4550
@Test
@@ -81,8 +86,9 @@ public void metadata_serializes_as_object() throws Exception {
8186
public void addSink_afterStart_receivesEvents() throws Exception {
8287
resetJourneyliticsState();
8388
final Application app = Mockito.mock(Application.class);
84-
Mockito.when(app.getApplicationContext()).thenReturn(app);
85-
Journeylitics.start(app, new JLConfig());
89+
final Activity activity = Mockito.mock(Activity.class);
90+
Mockito.when(activity.getApplication()).thenReturn(app);
91+
Journeylitics.start(activity, new JLConfig());
8692

8793
final List<JLEvent> events = new ArrayList<>();
8894
final JLSink sink = events::add;
@@ -99,8 +105,9 @@ public void addSink_afterStart_receivesEvents() throws Exception {
99105
public void removeSink_stopsEvents() throws Exception {
100106
resetJourneyliticsState();
101107
final Application app = Mockito.mock(Application.class);
102-
Mockito.when(app.getApplicationContext()).thenReturn(app);
103-
Journeylitics.start(app, new JLConfig());
108+
final Activity activity = Mockito.mock(Activity.class);
109+
Mockito.when(activity.getApplication()).thenReturn(app);
110+
Journeylitics.start(activity, new JLConfig());
104111

105112
final List<JLEvent> events = new ArrayList<>();
106113
final JLSink sink = events::add;

0 commit comments

Comments
 (0)