From 11173bca7ec8bf5b2ec00e70b5f21832400c3487 Mon Sep 17 00:00:00 2001
From: Kevin Lind <40409666+kevinlind@users.noreply.github.com>
Date: Tue, 13 Dec 2022 11:12:08 -0800
Subject: [PATCH] [MOB-17661] Lifecycle v2 set BCP 47 style language tag for
XDM field environment._dc.language (#293)
* LifecycleV2 sets langauge code using Locale.toLanguageTag from SDK >=21.
Fixes issue where some locale strings fail XDM validation. The failure is due to the use of Locale.toString, which doesn't always produce a valid locale tag. For SDK level 21 and higher, use Locale.toLanguageTag to set Environment._dc.language, and for lower API levels use simple concatenation of Locale.getLanguage and Locale.getCountry.
* Add validation for _dc.language string in XDMLifecycleEnvironment class
* Remove regex pattern tests from LifecycleUtilLocaleTest, tests moved to XDMLifecycleEnvironmentLanguageTest
* refactor LifecycleUtil.formatLocaleXDM for readability
* Update comment for LifecycleUtil.formatLocale to clarify the response format
* Refactor LifecycleUtil.formatLocaleXDM to conform to checkstyle rules
* use DataReader in test case to avoid NPE
* fix format
* Reduce nesting statements in getlocaleBCPString
---
.../mobile/lifecycle/LifecycleUtil.java | 49 +++-
.../lifecycle/LifecycleV2MetricsBuilder.java | 3 +-
.../lifecycle/XDMLifecycleEnvironment.java | 36 ++-
.../lifecycle/LifecycleUtilLocaleTest.java | 218 ++++++++++++++++++
.../XDMLifecycleEnvironmentLanguageTest.java | 156 +++++++++++++
5 files changed, 454 insertions(+), 8 deletions(-)
create mode 100644 code/android-lifecycle-library/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtilLocaleTest.java
create mode 100644 code/android-lifecycle-library/src/test/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironmentLanguageTest.java
diff --git a/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java b/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java
index 10c1474b1..f8b5e905e 100644
--- a/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java
+++ b/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java
@@ -11,6 +11,9 @@
package com.adobe.marketing.mobile.lifecycle;
+import android.annotation.SuppressLint;
+import android.os.Build;
+import androidx.annotation.VisibleForTesting;
import com.adobe.marketing.mobile.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -59,12 +62,56 @@ private static String dateToISO8601String(final Date timestamp, final String tim
}
/**
- * Formats the locale value from SystemInfoService and replaces '_' with '-'
+ * Formats the locale value by replacing '_' with '-'. Uses {@link Locale#toString()} to
+ * retrieve the language tag.
*
+ *
Note. the use of {@code Locale#toString()} does not return a value formatted to BCP 47.
+ * For example, script codes are appended to the locale as "-#scriptCode", as in "zh-HK-#Hant",
+ * where BCP 47 requires the format as "zh-Hant-HK".
+ *
+ * @see #formatLocaleXDM(Locale)
* @param locale active locale value
* @return string representation of the locale
*/
static String formatLocale(final Locale locale) {
return locale == null ? null : locale.toString().replace('_', '-');
}
+
+ /**
+ * Format the locale to the string format used in XDM. For Android API version >= Lollipop (21),
+ * returns {@link Locale#toLanguageTag()}. For Android API version < 21, returns a concatenation
+ * of {@link Locale#getLanguage()} and {@link Locale#getCountry()}, separated by '-'.
+ *
+ * @param locale active Locale value
+ * @return String representation of the locale
+ */
+ @SuppressLint("NewApi")
+ static String formatLocaleXDM(final Locale locale) {
+ if (locale == null) {
+ return null;
+ }
+
+ if (isLollipopOrGreater.check()) {
+ return locale.toLanguageTag();
+ }
+
+ String language = locale.getLanguage();
+ String region = locale.getCountry();
+
+ if (StringUtils.isNullOrEmpty(language)) {
+ return null;
+ }
+
+ return StringUtils.isNullOrEmpty(region)
+ ? language
+ : String.format("%s-%s", language, region);
+ }
+
+ interface BuildVersionCheck {
+ boolean check();
+ }
+
+ @VisibleForTesting
+ static BuildVersionCheck isLollipopOrGreater =
+ () -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
diff --git a/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java b/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java
index 392356d32..aaa85986c 100644
--- a/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java
+++ b/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java
@@ -30,7 +30,6 @@
*
*/
class LifecycleV2MetricsBuilder {
-
private static final String SELF_LOG_TAG = "LifecycleV2MetricsBuilder";
private final DeviceInforming deviceInfoService;
private XDMLifecycleDevice xdmDeviceInfo;
@@ -195,7 +194,7 @@ private XDMLifecycleEnvironment computeEnvironmentData() {
xdmEnvironmentInfo.setOperatingSystem(deviceInfoService.getOperatingSystemName());
xdmEnvironmentInfo.setOperatingSystemVersion(deviceInfoService.getOperatingSystemVersion());
xdmEnvironmentInfo.setLanguage(
- LifecycleUtil.formatLocale(deviceInfoService.getActiveLocale()));
+ LifecycleUtil.formatLocaleXDM(deviceInfoService.getActiveLocale()));
return xdmEnvironmentInfo;
}
diff --git a/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java b/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java
index 75a6974af..8f4db8d4b 100644
--- a/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java
+++ b/code/android-lifecycle-library/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java
@@ -11,8 +11,12 @@
package com.adobe.marketing.mobile.lifecycle;
+import androidx.annotation.NonNull;
+import com.adobe.marketing.mobile.services.Log;
+import com.adobe.marketing.mobile.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
+import java.util.regex.Pattern;
/**
* Class {@code Environment} representing a subset of the XDM Environment data type fields.
@@ -21,12 +25,15 @@
*/
@SuppressWarnings("unused")
class XDMLifecycleEnvironment {
-
+ private final String LOG_SOURCE = "XDMLifecycleEnvironment";
private String carrier;
private String language;
private String operatingSystem;
private String operatingSystemVersion;
private XDMLifecycleEnvironmentTypeEnum type;
+ private final String languageRegex =
+ "^(((([A-Za-z]{2,3}(-([A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-([A-Za-z]{4}))?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-([0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(x(-[A-Za-z0-9]{1,8})+))?)|(x(-[A-Za-z0-9]{1,8})+)|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$";
+ private final Pattern languagePattern = Pattern.compile(languageRegex);
XDMLifecycleEnvironment() {}
@@ -37,10 +44,19 @@ Map serializeToXdm() {
map.put("carrier", this.carrier);
}
- if (this.language != null) {
- Map dublinCoreLanguage = new HashMap();
- dublinCoreLanguage.put("language", this.language);
- map.put("_dc", dublinCoreLanguage);
+ if (!StringUtils.isNullOrEmpty(this.language)) {
+ if (isValidLanguageTag(this.language)) {
+ Map dublinCoreLanguage = new HashMap();
+ dublinCoreLanguage.put("language", this.language);
+ map.put("_dc", dublinCoreLanguage);
+ } else {
+ Log.warning(
+ LifecycleConstants.LOG_TAG,
+ LOG_SOURCE,
+ "Language tag '%s' failed validation and will be dropped. Values for XDM"
+ + " field 'environment._dc.language' must conform to BCP 47.",
+ this.language);
+ }
}
if (this.operatingSystem != null) {
@@ -164,4 +180,14 @@ XDMLifecycleEnvironmentTypeEnum getType() {
void setType(final XDMLifecycleEnvironmentTypeEnum newValue) {
this.type = newValue;
}
+
+ /**
+ * Validate the language tag is formatted per the XDM Environment Schema required pattern.
+ *
+ * @param tag the language tag to validate
+ * @return true if the language tag matches the pattern.
+ */
+ private boolean isValidLanguageTag(@NonNull final String tag) {
+ return languagePattern.matcher(tag).matches();
+ }
}
diff --git a/code/android-lifecycle-library/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtilLocaleTest.java b/code/android-lifecycle-library/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtilLocaleTest.java
new file mode 100644
index 000000000..9948b82ab
--- /dev/null
+++ b/code/android-lifecycle-library/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtilLocaleTest.java
@@ -0,0 +1,218 @@
+/*
+ Copyright 2022 Adobe. All rights reserved.
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License. You may obtain a copy
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software distributed under
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ OF ANY KIND, either express or implied. See the License for the specific language
+ governing permissions and limitations under the License.
+*/
+
+package com.adobe.marketing.mobile.lifecycle;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Build;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LifecycleUtilLocaleTest {
+
+ @After
+ public void teardown() {
+ // Reset build check version for tests outside this class
+ LifecycleUtil.isLollipopOrGreater =
+ () -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+ }
+
+ @Parameterized.Parameters(name = "{index}: {0}")
+ public static Collection