Skip to content

Commit 26ae83d

Browse files
authored
Merge pull request #319 from Countly/np24.11.2
Np24.11.2
2 parents d910382 + ee4df93 commit 26ae83d

20 files changed

+346
-59
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 24.11.2-np
2+
* Improved view tracking capabilities in iOS.
3+
4+
* Mitigated issues where:
5+
* On Android 35 and above, the navigation bar was overlapping with the content display in Android.
6+
* An automatically closed autoStopped view's duration could have increased when opening new views in Android.
7+
* A concurrent modification error could have happen when starting multiple stopped views in iOS.
8+
9+
* Updated underlying Android SDK version to 24.7.7
10+
* Updated underlying iOS SDK version to 24.7.9
11+
112
## 24.11.1-np
213
* Added content configuration interface that has `setGlobalContentCallback` to get notified about content changes.
314
* Added support for localization of content blocks.
@@ -10,7 +21,6 @@
1021
* Mitigated issues where:
1122
* Passing the global content callback was not possible in Android.
1223
* The user provided URLSessionConfiguration was not applied to direct requests in iOS.
13-
* A concurrent modification error could have happen when starting multiple stopped views in iOS.
1424

1525
* Updated underlying Android SDK version to 24.7.6
1626
* Updated underlying iOS SDK version to 24.7.8

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@ android {
3838
}
3939

4040
dependencies {
41-
implementation 'ly.count.android:sdk:24.7.6'
41+
implementation 'ly.count.android:sdk:24.7.7'
4242
}

android/src/main/java/ly/count/dart/countly_flutter/CountlyFlutterPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
*/
6363
public class CountlyFlutterPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware, DefaultLifecycleObserver {
6464
private static final String TAG = "CountlyFlutterPlugin";
65-
private final String COUNTLY_FLUTTER_SDK_VERSION_STRING = "24.11.1";
65+
private final String COUNTLY_FLUTTER_SDK_VERSION_STRING = "24.11.2";
6666
private final String COUNTLY_FLUTTER_SDK_NAME = "dart-flutterb-android";
6767
private final String COUNTLY_FLUTTER_SDK_NAME_NO_PUSH = "dart-flutterbnp-android";
6868

example/integration_test/experimental_tests/visibility_prev_names_recording_test.dart

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ void main() {
4848

4949
// events and views in background and then after we come to foreground
5050
// depending on which view is started first after fg, we can have 9 or 10 events
51-
expect(eventList.length, anyOf([9, 10]));
51+
// But for android, we have 11 events or 12 because end view calls are not recorded to the RQ
52+
expect(eventList.length, Platform.isAndroid ? anyOf([11, 12]) : anyOf([9, 10]));
5253

5354
for (var entry in requestList.asMap().entries) {
5455
int index = entry.key;
@@ -64,13 +65,13 @@ void main() {
6465
// when going to bg
6566
if (index == 2) {
6667
expect(queryParams['end_session']?[0], '1');
67-
expect(queryParams['session_duration']?[0], '2');
68+
expect(queryParams['session_duration']?[0], anyOf(['2', '3']));
6869
}
6970
if (index == 1) {
7071
var rqEvents = jsonDecode(queryParams['events']![0]);
71-
expect(rqEvents.length, 7);
72+
expect(rqEvents.length, Platform.isAndroid ? 6 : 7);
7273

73-
// events nad views at initial fg
74+
// events and views at initial fg
7475
checkEventAndViews(rqEvents, true);
7576
// events and views after going to bg and coming back to fg
7677
checkEventAndViews(
@@ -87,41 +88,60 @@ void main() {
8788

8889
void checkEventAndViews(eventArray, isFGEvents) {
8990
if (isFGEvents) {
90-
checkEvent(eventArray[0], 'E1_FG', true);
91-
checkViewStart(eventArray[1], 'V1_FG', true);
92-
checkEvent(eventArray[2], 'E2_FG', true);
93-
checkViewStart((eventArray[3]), 'V2_FG', true);
94-
checkEvent((eventArray[4]), 'E3_FG', true);
91+
int index = 0;
92+
if (Platform.isAndroid) {
93+
// in Android 0th element is orientation event
94+
index = 1;
95+
expect(eventArray[0]['key'], "[CLY]_orientation");
96+
}
97+
checkEvent(eventArray[index], 'E1_FG', true);
98+
checkViewStart(eventArray[index + 1], 'V1_FG', true);
99+
checkEvent(eventArray[index + 2], 'E2_FG', true);
100+
checkViewStart((eventArray[index + 3]), 'V2_FG', true);
101+
checkEvent((eventArray[index + 4]), 'E3_FG', true);
95102
// closed in random order
96-
try {
97-
checkViewEnd((eventArray[5]), 'V2_FG', true);
98-
checkViewEnd((eventArray[6]), 'V1_FG', true);
99-
} catch (e) {
100-
checkViewEnd((eventArray[5]), 'V1_FG', true);
101-
checkViewEnd((eventArray[6]), 'V2_FG', true);
103+
if (!Platform.isAndroid) {
104+
try {
105+
checkViewEnd((eventArray[5]), 'V2_FG', true);
106+
checkViewEnd((eventArray[6]), 'V1_FG', true);
107+
} catch (e) {
108+
checkViewEnd((eventArray[5]), 'V1_FG', true);
109+
checkViewEnd((eventArray[6]), 'V2_FG', true);
110+
}
102111
}
103112
} else {
104-
checkEvent(jsonDecode(eventArray[0]), 'E1_BG', false);
105-
checkViewStart(jsonDecode(eventArray[1]), 'V1_BG', false);
106-
checkEvent(jsonDecode(eventArray[2]), 'E2_BG', false);
107-
checkViewStart(jsonDecode(eventArray[3]), 'V2_BG', false);
108-
checkEvent(jsonDecode(eventArray[4]), 'E3_BG', false);
109-
expect(jsonDecode(eventArray[5])['key'], '[CLY]_orientation');
110-
checkViewEnd(jsonDecode(eventArray[6]), 'V2_BG', false);
113+
int index = 0;
114+
if (Platform.isAndroid) {
115+
try {
116+
checkViewEnd(jsonDecode(eventArray[index]), 'V2_FG', true);
117+
checkViewEnd(jsonDecode(eventArray[index + 1]), 'V1_FG', true);
118+
} catch (e) {
119+
checkViewEnd(jsonDecode(eventArray[index]), 'V1_FG', true);
120+
checkViewEnd(jsonDecode(eventArray[index + 1]), 'V2_FG', true);
121+
}
122+
index = 2;
123+
}
124+
checkEvent(jsonDecode(eventArray[index]), 'E1_BG', false);
125+
checkViewStart(jsonDecode(eventArray[index + 1]), 'V1_BG', false);
126+
checkEvent(jsonDecode(eventArray[index + 2]), 'E2_BG', false);
127+
checkViewStart(jsonDecode(eventArray[index + 3]), 'V2_BG', false);
128+
checkEvent(jsonDecode(eventArray[index + 4]), 'E3_BG', false);
129+
expect(jsonDecode(eventArray[index + 5])['key'], '[CLY]_orientation');
130+
checkViewEnd(jsonDecode(eventArray[index + 6]), 'V2_BG', false);
111131
// this part is random as autoStopped or normal view can start first
112132
try {
113-
checkRestartedView(jsonDecode(eventArray[7]), 'V2_FG', true, false);
133+
checkRestartedView(jsonDecode(eventArray[index + 7]), 'V2_FG', true, false);
114134

115-
expect(jsonDecode(eventArray[8])['segmentation']['name'], 'V2_FG');
116-
expect(jsonDecode(eventArray[8])['segmentation']['fg_events'], false);
117-
expect(jsonDecode(eventArray[8])['segmentation']['cly_v'], isNull);
118-
expect(jsonDecode(eventArray[8])['segmentation']['visit'], isNull);
119-
expect(jsonDecode(eventArray[8])['segmentation']['cly_pvn'], cvn_end);
135+
expect(jsonDecode(eventArray[index + 8])['segmentation']['name'], 'V2_FG');
136+
expect(jsonDecode(eventArray[index + 8])['segmentation']['fg_events'], false);
137+
expect(jsonDecode(eventArray[index + 8])['segmentation']['cly_v'], isNull);
138+
expect(jsonDecode(eventArray[index + 8])['segmentation']['visit'], isNull);
139+
expect(jsonDecode(eventArray[index + 8])['segmentation']['cly_pvn'], cvn_end);
120140

121-
checkRestartedView(jsonDecode(eventArray[9]), 'V1_FG', true, false);
141+
checkRestartedView(jsonDecode(eventArray[index + 9]), 'V1_FG', true, false);
122142
} catch (e) {
123-
checkRestartedView(jsonDecode(eventArray[7]), 'V1_FG', true, false);
124-
checkRestartedView(jsonDecode(eventArray[8]), 'V2_FG', true, false);
143+
checkRestartedView(jsonDecode(eventArray[index + 7]), 'V1_FG', true, false);
144+
checkRestartedView(jsonDecode(eventArray[index + 8]), 'V2_FG', true, false);
125145
}
126146
}
127147
}
@@ -138,7 +158,7 @@ void checkViewStart(view, name, isVisible) {
138158
expect(view['segmentation']['name'], name);
139159
expect(view['segmentation']['fg_events'], isVisible);
140160
expect(view['segmentation']['cly_v'], isVisible ? 1 : 0);
141-
expect(view['segmentation']['visit'], 1);
161+
expect(view['segmentation']['visit'], Platform.isAndroid ? '1' : 1);
142162
expect(view['segmentation']['cly_pvn'], cvn);
143163
cvn_end = cvn;
144164
cvn = name;
@@ -156,7 +176,7 @@ void checkRestartedView(view, name, isVisible, globalSegmentation) {
156176
expect(view['segmentation']['name'], name);
157177
expect(view['segmentation']['fg_events'], globalSegmentation);
158178
expect(view['segmentation']['cly_v'], isVisible ? 1 : 0);
159-
expect(view['segmentation']['visit'], 1);
179+
expect(view['segmentation']['visit'], Platform.isAndroid ? '1' : 1);
160180
expect(view['segmentation']['cly_pvn'], cvn);
161181
cvn_end = cvn;
162182
cvn = name;

example/integration_test/utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Future<List<String>> getEventQueue() async {
2929
void testCommonRequestParams(Map<String, List<String>> requestObject) {
3030
expect(requestObject['app_key']?[0], APP_KEY);
3131
expect(requestObject['sdk_name']?[0], "dart-flutterbnp-${Platform.isIOS ? "ios" : "android"}");
32-
expect(requestObject['sdk_version']?[0], '24.11.1');
32+
expect(requestObject['sdk_version']?[0], '24.11.2');
3333
expect(requestObject['av']?[0], Platform.isIOS ? '0.0.1' : '1.0.0');
3434
assert(requestObject['timestamp']?[0] != null);
3535

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import 'dart:convert';
2+
import 'dart:math';
3+
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
4+
import 'dart:io';
5+
6+
import 'package:countly_flutter/countly_flutter.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:integration_test/integration_test.dart';
9+
import '../utils.dart';
10+
11+
///
12+
/// This test is to check the flow of views is auto stopped and restarted
13+
/// and also when the app is in background and then comes to foreground
14+
/// and also to check if the previous view names are recorded
15+
///
16+
void main() {
17+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
18+
testWidgets('Start auto stopped views and stop views', (WidgetTester tester) async {
19+
// Initialize the SDK
20+
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
21+
await Countly.initWithConfig(config);
22+
23+
await Countly.instance.views.startView("V1");
24+
await Countly.instance.views.startAutoStoppedView("V2");
25+
26+
FlutterForegroundTask.minimizeApp();
27+
sleep(Duration(seconds: 2));
28+
29+
await Countly.instance.views.startAutoStoppedView("V4");
30+
await Countly.instance.views.startView("V3");
31+
32+
sleep(Duration(seconds: 2));
33+
34+
FlutterForegroundTask.launchApp();
35+
if (Platform.isIOS) {
36+
printMessageMultipleTimes('waiting for 3 seconds, now go to foreground', 3);
37+
}
38+
39+
sleep(Duration(seconds: 5));
40+
41+
// REQUESTS: begin session, events before going in to the background, and end session
42+
// EVENTS: end view for V1, V2, start view of V4, end view for V4, start view V3, orientation, start view for V1, V2
43+
List<String> requestList = await getRequestQueue();
44+
List<String> eventList = await getEventQueue();
45+
46+
printQueues(requestList, eventList);
47+
expect(requestList.length, Platform.isAndroid ? 4 : 3);
48+
expect(eventList.length, Platform.isAndroid ? anyOf(8, 9) : 3); // 3 for iOS
49+
validateBeginSessionRequest(requestList[0]); // validate begin session on 0th idx
50+
51+
Map<String, List<String>> queryParams = Uri.parse('?${requestList[1]}').queryParametersAll;
52+
var rqEvents = jsonDecode(queryParams['events']![0]);
53+
expect(rqEvents.length, Platform.isAndroid ? 3 : 4);
54+
int index = 0;
55+
if(Platform.isAndroid){
56+
validateEvent("[CLY]_orientation", <String, dynamic>{'mode': 'portrait'}, eventGiven: rqEvents[index++]);
57+
}
58+
validateView("V1", true, true, viewGiven: rqEvents[index++]);
59+
validateView("V2", false, true, viewGiven: rqEvents[index++]);
60+
if(Platform.isIOS){
61+
validateView("V1", false, false, viewGiven: rqEvents[index++]);
62+
validateView("V2", false, false, viewGiven: rqEvents[index++]);
63+
}
64+
65+
validateEndSessionRequest(requestList[2]); // validate end session on 2nd idx
66+
if(Platform.isAndroid) {
67+
validateBeginSessionRequest(requestList[3]); // validate begin session on 3rd idx
68+
}
69+
70+
index = 0;
71+
if (Platform.isAndroid) {
72+
try {
73+
validateView("V1", false, false, viewStr: eventList[index++]);
74+
validateView("V2", false, false, viewStr: eventList[index++]);
75+
} catch (e) {
76+
index = 0;
77+
validateView("V2", false, false, viewStr: eventList[index++]);
78+
validateView("V1", false, false, viewStr: eventList[index++]);
79+
}
80+
}
81+
82+
validateView("V4", false, true, viewStr: eventList[index++]);
83+
validateView("V4", false, false, viewStr: eventList[index++]);
84+
validateView("V3", false, true, viewStr: eventList[index++]);
85+
if(Platform.isAndroid){
86+
validateEvent("[CLY]_orientation", <String, dynamic>{'mode': 'portrait'}, eventStr: eventList[index++]);
87+
}
88+
89+
int iCached = index;
90+
if (Platform.isAndroid) {
91+
try {
92+
validateView("V2", true, true, viewStr: eventList[index++]);
93+
validateView("V2", false, false, viewStr: eventList[index++]);
94+
validateView("V1", false, true, viewStr: eventList[index++]);
95+
} catch (e) {
96+
index = iCached;
97+
validateView("V1", true, true, viewStr: eventList[index++]);
98+
validateView("V2", false, true, viewStr: eventList[index++]);
99+
}
100+
}
101+
});
102+
}
103+
104+
void validateView(String name, bool start, bool visit, {String? viewStr, Map<String, dynamic>? viewGiven}) {
105+
Map<String, dynamic> segmentation = <String, dynamic>{'name': name, 'segment': Platform.isAndroid ? 'Android' : 'iOS'};
106+
107+
if (visit) {
108+
segmentation['visit'] = Platform.isAndroid ? '1': 1;
109+
}
110+
if (start) {
111+
segmentation['start'] = Platform.isAndroid ? '1': 1;
112+
}
113+
validateEvent("[CLY]_view", segmentation, eventGiven: viewGiven, eventStr: viewStr);
114+
}
115+
116+
void validateBeginSessionRequest(String request) {
117+
Map<String, List<String>> queryParams = Uri.parse('?$request').queryParametersAll;
118+
testCommonRequestParams(queryParams);
119+
120+
expect(queryParams['begin_session'], ['1']);
121+
expect(queryParams['metrics'], isNotNull);
122+
}
123+
124+
void validateEndSessionRequest(String request) {
125+
Map<String, List<String>> queryParams = Uri.parse('?$request').queryParametersAll;
126+
testCommonRequestParams(queryParams);
127+
128+
expect(queryParams['end_session'], ['1']);
129+
expect(queryParams['metrics'], isNull);
130+
}
131+
132+
void validateEvent(String key, Map<String, dynamic> segmentation, {String? eventStr, Map<String, dynamic>? eventGiven}) {
133+
Map<String, dynamic> event = eventStr != null ? jsonDecode(eventStr) : eventGiven!;
134+
print("================");
135+
print(event);
136+
expect(event['key'], key);
137+
expect(segmentation.length, event['segmentation'].length);
138+
for (var key in segmentation.keys) {
139+
expect(event['segmentation'][key], segmentation[key]);
140+
}
141+
}

0 commit comments

Comments
 (0)