Skip to content

Commit 9455362

Browse files
authored
Merge branch 'master' into evan/MOB-10674-prepare-3.5.10-release
2 parents 06f4f8f + 6da9aab commit 9455362

10 files changed

+240
-5
lines changed

iterableapi/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,4 @@ afterEvaluate {
119119
javadoc.classpath += files(android.libraryVariants.collect { variant -> variant.javaCompile.classpath.files })
120120
javadoc.classpath += files(android.libraryVariants.collect { variant -> "build/generated/source/r/${variant.flavorName}/${variant.buildType.name}" })
121121
javadoc.classpath += files(android.libraryVariants.collect { variant -> "build/generated/source/buildConfig/${variant.flavorName}/${variant.buildType.name}" })
122-
}
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.iterable.iterableapi;
2+
3+
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
5+
6+
public class IterableAPIMobileFrameworkInfo {
7+
@NonNull private final IterableAPIMobileFrameworkType frameworkType;
8+
@Nullable private final String iterableSdkVersion;
9+
10+
public IterableAPIMobileFrameworkInfo(@NonNull IterableAPIMobileFrameworkType frameworkType, @Nullable String iterableSdkVersion) {
11+
this.frameworkType = frameworkType;
12+
this.iterableSdkVersion = iterableSdkVersion;
13+
}
14+
15+
@NonNull
16+
public IterableAPIMobileFrameworkType getFrameworkType() {
17+
return frameworkType;
18+
}
19+
20+
@Nullable
21+
public String getIterableSdkVersion() {
22+
return iterableSdkVersion;
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.iterable.iterableapi;
2+
3+
public enum IterableAPIMobileFrameworkType {
4+
FLUTTER("flutter"),
5+
REACT_NATIVE("reactnative"),
6+
NATIVE("native");
7+
8+
private final String value;
9+
10+
IterableAPIMobileFrameworkType(String value) {
11+
this.value = value;
12+
}
13+
14+
public String getValue() {
15+
return value;
16+
}
17+
}

iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ public static void initialize(@NonNull Context context, @NonNull String apiKey,
641641
try {
642642
JSONObject dataFields = new JSONObject();
643643
JSONObject deviceDetails = new JSONObject();
644-
DeviceInfoUtils.populateDeviceDetails(deviceDetails, context, sharedInstance.getDeviceId());
644+
DeviceInfoUtils.populateDeviceDetails(deviceDetails, context, sharedInstance.getDeviceId(), null);
645645
dataFields.put(IterableConstants.KEY_FIRETV, deviceDetails);
646646
sharedInstance.apiClient.updateUser(dataFields, false);
647647
} catch (JSONException e) {

iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java

+16-2
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,21 @@ protected void registerDeviceToken(@Nullable String email, @Nullable String user
556556

557557
dataFields.put(IterableConstants.FIREBASE_TOKEN_TYPE, IterableConstants.MESSAGING_PLATFORM_FIREBASE);
558558
dataFields.put(IterableConstants.FIREBASE_COMPATIBLE, true);
559-
DeviceInfoUtils.populateDeviceDetails(dataFields, context, authProvider.getDeviceId());
559+
560+
IterableAPIMobileFrameworkInfo frameworkInfo = IterableApi.sharedInstance.config.mobileFrameworkInfo;
561+
if (frameworkInfo == null) {
562+
IterableAPIMobileFrameworkType detectedFramework = IterableMobileFrameworkDetector.detectFramework(context);
563+
String sdkVersion = detectedFramework == IterableAPIMobileFrameworkType.NATIVE
564+
? IterableConstants.ITBL_KEY_SDK_VERSION_NUMBER
565+
: null;
566+
567+
frameworkInfo = new IterableAPIMobileFrameworkInfo(
568+
detectedFramework,
569+
sdkVersion
570+
);
571+
}
572+
573+
DeviceInfoUtils.populateDeviceDetails(dataFields, context, authProvider.getDeviceId(), frameworkInfo);
560574
dataFields.put(IterableConstants.DEVICE_NOTIFICATIONS_ENABLED, NotificationManagerCompat.from(context).areNotificationsEnabled());
561575

562576
JSONObject device = new JSONObject();
@@ -684,4 +698,4 @@ void onLogout() {
684698
getRequestProcessor().onLogout(authProvider.getContext());
685699
authProvider.resetAuth();
686700
}
687-
}
701+
}

iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java

+20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.iterable.iterableapi;
22

33
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
45
import android.util.Log;
56

67
/**
@@ -86,6 +87,7 @@ public class IterableConfig {
8687
* By default, the SDK will save in-apps to disk.
8788
*/
8889
final boolean useInMemoryStorageForInApps;
90+
8991
/**
9092
* Allows for fetching embedded messages.
9193
*/
@@ -97,6 +99,12 @@ public class IterableConfig {
9799
*/
98100
final IterableDecryptionFailureHandler decryptionFailureHandler;
99101

102+
/**
103+
* Mobile framework information for the app
104+
*/
105+
@Nullable
106+
final IterableAPIMobileFrameworkInfo mobileFrameworkInfo;
107+
100108
private IterableConfig(Builder builder) {
101109
pushIntegrationName = builder.pushIntegrationName;
102110
urlHandler = builder.urlHandler;
@@ -114,6 +122,7 @@ private IterableConfig(Builder builder) {
114122
useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps;
115123
enableEmbeddedMessaging = builder.enableEmbeddedMessaging;
116124
decryptionFailureHandler = builder.decryptionFailureHandler;
125+
mobileFrameworkInfo = builder.mobileFrameworkInfo;
117126
}
118127

119128
public static class Builder {
@@ -133,6 +142,7 @@ public static class Builder {
133142
private boolean useInMemoryStorageForInApps = false;
134143
private boolean enableEmbeddedMessaging = false;
135144
private IterableDecryptionFailureHandler decryptionFailureHandler;
145+
private IterableAPIMobileFrameworkInfo mobileFrameworkInfo;
136146

137147
public Builder() {}
138148

@@ -304,6 +314,16 @@ public Builder setDecryptionFailureHandler(@NonNull IterableDecryptionFailureHan
304314
return this;
305315
}
306316

317+
/**
318+
* Set mobile framework information for the app
319+
* @param mobileFrameworkInfo Mobile framework information
320+
*/
321+
@NonNull
322+
public Builder setMobileFrameworkInfo(@NonNull IterableAPIMobileFrameworkInfo mobileFrameworkInfo) {
323+
this.mobileFrameworkInfo = mobileFrameworkInfo;
324+
return this;
325+
}
326+
307327
@NonNull
308328
public IterableConfig build() {
309329
return new IterableConfig(this);

iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java

+2
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ public final class IterableConstants {
144144
public static final String DEVICE_APP_BUILD = "appBuild";
145145
public static final String DEVICE_NOTIFICATIONS_ENABLED = "notificationsEnabled";
146146
public static final String DEVICE_ITERABLE_SDK_VERSION = "iterableSdkVersion";
147+
public static final String DEVICE_MOBILE_FRAMEWORK_INFO = "mobileFrameworkInfo";
148+
public static final String DEVICE_FRAMEWORK_TYPE = "frameworkType";
147149

148150
public static final String INSTANCE_ID_CLASS = "com.google.android.gms.iid.InstanceID";
149151
public static final String ICON_FOLDER_IDENTIFIER = "drawable";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.iterable.iterableapi
2+
3+
import android.content.Context
4+
import android.content.pm.PackageManager
5+
6+
object IterableMobileFrameworkDetector {
7+
private const val TAG = "FrameworkDetector"
8+
private lateinit var context: Context
9+
10+
@Volatile
11+
private var cachedFrameworkType: IterableAPIMobileFrameworkType? = null
12+
13+
private var hasClass: (String) -> Boolean = { className ->
14+
try {
15+
Class.forName(className)
16+
true
17+
} catch (e: ClassNotFoundException) {
18+
false
19+
}
20+
}
21+
22+
fun initialize(context: Context) {
23+
if (context.applicationContext != null) {
24+
this.context = context.applicationContext
25+
} else {
26+
this.context = context
27+
}
28+
if (cachedFrameworkType == null) {
29+
cachedFrameworkType = detectFrameworkInternal(context)
30+
}
31+
}
32+
33+
@JvmStatic
34+
fun detectFramework(context: Context): IterableAPIMobileFrameworkType {
35+
return cachedFrameworkType ?: synchronized(this) {
36+
cachedFrameworkType ?: detectFrameworkInternal(context).also {
37+
cachedFrameworkType = it
38+
}
39+
}
40+
}
41+
42+
fun frameworkType(): IterableAPIMobileFrameworkType {
43+
return cachedFrameworkType ?: detectFramework(context)
44+
}
45+
46+
private fun detectFrameworkInternal(context: Context): IterableAPIMobileFrameworkType {
47+
val hasFlutter = hasFrameworkClasses(FrameworkClasses.flutter)
48+
val hasReactNative = hasFrameworkClasses(FrameworkClasses.reactNative)
49+
50+
return when {
51+
hasFlutter && hasReactNative -> {
52+
IterableLogger.d(TAG, "Both Flutter and React Native frameworks detected. This is unexpected.")
53+
when {
54+
context.packageName.endsWith(".flutter") -> IterableAPIMobileFrameworkType.FLUTTER
55+
hasManifestMetadata(context, ManifestMetadata.flutter) -> IterableAPIMobileFrameworkType.FLUTTER
56+
hasManifestMetadata(context, ManifestMetadata.reactNative) -> IterableAPIMobileFrameworkType.REACT_NATIVE
57+
else -> IterableAPIMobileFrameworkType.REACT_NATIVE
58+
}
59+
}
60+
hasFlutter -> IterableAPIMobileFrameworkType.FLUTTER
61+
hasReactNative -> IterableAPIMobileFrameworkType.REACT_NATIVE
62+
else -> {
63+
when {
64+
hasManifestMetadata(context, ManifestMetadata.flutter) -> IterableAPIMobileFrameworkType.FLUTTER
65+
hasManifestMetadata(context, ManifestMetadata.reactNative) -> IterableAPIMobileFrameworkType.REACT_NATIVE
66+
else -> IterableAPIMobileFrameworkType.NATIVE
67+
}
68+
}
69+
}
70+
}
71+
72+
private object FrameworkClasses {
73+
val flutter = listOf(
74+
"io.flutter.embedding.engine.FlutterEngine",
75+
"io.flutter.plugin.common.MethodChannel",
76+
"io.flutter.embedding.android.FlutterActivity",
77+
"io.flutter.embedding.android.FlutterFragment"
78+
)
79+
80+
val reactNative = listOf(
81+
"com.facebook.react.ReactRootView",
82+
"com.facebook.react.bridge.ReactApplicationContext",
83+
"com.facebook.react.ReactActivity",
84+
"com.facebook.react.ReactApplication",
85+
"com.facebook.react.bridge.ReactContext"
86+
)
87+
}
88+
89+
private object ManifestMetadata {
90+
val flutter = listOf(
91+
"flutterEmbedding",
92+
"io.flutter.embedding.android.NormalTheme",
93+
"io.flutter.embedding.android.SplashScreenDrawable"
94+
)
95+
96+
val reactNative = listOf(
97+
"react_native_version",
98+
"expo.modules.updates.ENABLED",
99+
"com.facebook.react.selected.ReactActivity"
100+
)
101+
}
102+
103+
private fun hasFrameworkClasses(classNames: List<String>): Boolean {
104+
return classNames.any { hasClass(it) }
105+
}
106+
107+
private fun hasManifestMetadata(context: Context, metadataKeys: List<String>): Boolean {
108+
return try {
109+
val packageInfo = context.packageManager.getPackageInfo(
110+
context.packageName,
111+
PackageManager.GET_META_DATA
112+
)
113+
val metadata = packageInfo.applicationInfo.metaData
114+
metadataKeys.any { key -> metadata?.containsKey(key) == true }
115+
} catch (e: Exception) {
116+
IterableLogger.e(TAG, "Error checking manifest metadata: ${e.message}")
117+
false
118+
}
119+
}
120+
}

iterableapi/src/main/java/com/iterable/iterableapi/util/DeviceInfoUtils.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import com.iterable.iterableapi.IterableConstants;
88
import com.iterable.iterableapi.IterableUtil;
9+
import com.iterable.iterableapi.IterableAPIMobileFrameworkInfo;
910

1011
import org.json.JSONException;
1112
import org.json.JSONObject;
@@ -20,7 +21,7 @@ public static boolean isFireTV(PackageManager packageManager) {
2021
String amazonModel = Build.MODEL;
2122
return amazonModel.matches("AFTN") || packageManager.hasSystemFeature(amazonFireTvHardware);
2223
}
23-
public static void populateDeviceDetails(JSONObject dataFields, Context context, String deviceId) throws JSONException {
24+
public static void populateDeviceDetails(JSONObject dataFields, Context context, String deviceId, IterableAPIMobileFrameworkInfo frameworkInfo) throws JSONException {
2425
dataFields.put(IterableConstants.DEVICE_BRAND, Build.BRAND); //brand: google
2526
dataFields.put(IterableConstants.DEVICE_MANUFACTURER, Build.MANUFACTURER); //manufacturer: samsung
2627
dataFields.put(IterableConstants.DEVICE_SYSTEM_NAME, Build.DEVICE); //device name: toro
@@ -33,5 +34,13 @@ public static void populateDeviceDetails(JSONObject dataFields, Context context,
3334
dataFields.put(IterableConstants.DEVICE_APP_VERSION, IterableUtil.getAppVersion(context));
3435
dataFields.put(IterableConstants.DEVICE_APP_BUILD, IterableUtil.getAppVersionCode(context));
3536
dataFields.put(IterableConstants.DEVICE_ITERABLE_SDK_VERSION, IterableConstants.ITBL_KEY_SDK_VERSION_NUMBER);
37+
38+
if (frameworkInfo != null && frameworkInfo.getFrameworkType() != null) {
39+
JSONObject mobileFrameworkJson = new JSONObject();
40+
mobileFrameworkJson.put(IterableConstants.DEVICE_FRAMEWORK_TYPE, frameworkInfo.getFrameworkType().getValue());
41+
mobileFrameworkJson.put(IterableConstants.DEVICE_ITERABLE_SDK_VERSION,
42+
frameworkInfo.getIterableSdkVersion() != null ? frameworkInfo.getIterableSdkVersion() : "unknown");
43+
dataFields.put(IterableConstants.DEVICE_MOBILE_FRAMEWORK_INFO, mobileFrameworkJson);
44+
}
3645
}
3746
}

iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java

+29
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,35 @@ public void testPushRegistrationDeviceFields() throws Exception {
363363
assertEquals("321", dataFields.getString("appBuild"));
364364
assertEquals(IterableConstants.ITBL_KEY_SDK_VERSION_NUMBER, dataFields.getString("iterableSdkVersion"));
365365
assertEquals(true, dataFields.getBoolean("notificationsEnabled"));
366+
367+
// Verify mobile framework info
368+
JSONObject mobileFrameworkInfo = dataFields.getJSONObject("mobileFrameworkInfo");
369+
assertNotNull(mobileFrameworkInfo);
370+
assertEquals("native", mobileFrameworkInfo.getString("frameworkType"));
371+
assertEquals(IterableConstants.ITBL_KEY_SDK_VERSION_NUMBER, mobileFrameworkInfo.getString("iterableSdkVersion"));
372+
}
373+
374+
@Test
375+
public void testPushRegistrationDeviceFieldsWithCustomFramework() throws Exception {
376+
server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
377+
IterableConfig config = new IterableConfig.Builder()
378+
.setAutoPushRegistration(false)
379+
.setMobileFrameworkInfo(new IterableAPIMobileFrameworkInfo(IterableAPIMobileFrameworkType.FLUTTER, "1.0.0"))
380+
.build();
381+
IterableApi.initialize(getContext(), "apiKey", config);
382+
IterableApi.getInstance().setEmail("[email protected]");
383+
IterableApi.getInstance().registerDeviceToken("token");
384+
Thread.sleep(100); // Since the network request is queued from a background thread, we need to wait
385+
shadowOf(getMainLooper()).idle();
386+
RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
387+
assertNotNull(request);
388+
389+
JSONObject requestJson = new JSONObject(request.getBody().readUtf8());
390+
JSONObject dataFields = requestJson.getJSONObject("device").getJSONObject("dataFields");
391+
JSONObject mobileFrameworkInfo = dataFields.getJSONObject("mobileFrameworkInfo");
392+
assertNotNull(mobileFrameworkInfo);
393+
assertEquals("flutter", mobileFrameworkInfo.getString("frameworkType"));
394+
assertEquals("1.0.0", mobileFrameworkInfo.getString("iterableSdkVersion"));
366395
}
367396

368397
@Test

0 commit comments

Comments
 (0)