diff --git a/code/core/api/core.api b/code/core/api/core.api index 5cb483abc..3983609a2 100644 --- a/code/core/api/core.api +++ b/code/core/api/core.api @@ -610,6 +610,7 @@ public abstract interface class com/adobe/marketing/mobile/services/DeviceInform public abstract fun getOperatingSystemVersion ()Ljava/lang/String; public abstract fun getPropertyFromManifest (Ljava/lang/String;)Ljava/lang/String; public abstract fun getRunMode ()Ljava/lang/String; + public abstract fun getSystemLocale ()Ljava/util/Locale; public abstract fun registerOneTimeNetworkConnectionActiveListener (Lcom/adobe/marketing/mobile/services/DeviceInforming$NetworkConnectionActiveListener;)Z } diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/DeviceInfoServiceTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/DeviceInfoServiceTests.kt index 50a10daea..4261bbe4f 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/DeviceInfoServiceTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/DeviceInfoServiceTests.kt @@ -39,6 +39,7 @@ class DeviceInfoServiceTests { ServiceProviderModifier.setAppContextService(MockAppContextService()) deviceInfoService = ServiceProvider.getInstance().deviceInfoService assertNull(deviceInfoService.activeLocale) + assertTrue(deviceInfoService.systemLocale.displayLanguage.isNotEmpty()) assertNull(deviceInfoService.displayInformation) assertEquals( DeviceInforming.DeviceType.UNKNOWN, @@ -73,6 +74,11 @@ class DeviceInfoServiceTests { assertTrue(deviceInfoService.activeLocale.displayLanguage.isNotEmpty()) } + @Test + fun testGetSystemLocale() { + assertTrue(deviceInfoService.systemLocale.displayLanguage.isNotEmpty()) + } + @Test fun testGetCurrentOrientation() { assertTrue(deviceInfoService.currentOrientation >= 0) diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java index 625fdb881..8feda6073 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java @@ -12,9 +12,11 @@ package com.adobe.marketing.mobile.services; import android.content.Context; +import android.database.sqlite.SQLiteDatabase; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.File; +import java.io.FileOutputStream; import java.util.List; import org.junit.After; import org.junit.Assert; @@ -25,19 +27,19 @@ @RunWith(AndroidJUnit4.class) public class SqliteDataQueueTests { - private File dbFile; private DataQueue dataQueue; private static final String QUEUE_NAME = "test.dataQueue"; @Before public void setUp() { Context context = ApplicationProvider.getApplicationContext(); - dbFile = context.getDatabasePath(QUEUE_NAME); dataQueue = new SQLiteDataQueue(context.getDatabasePath(QUEUE_NAME).getPath()); } @After public void tearDown() { + Context context = ApplicationProvider.getApplicationContext(); + File dbFile = getDatabase(); if (dbFile != null && dbFile.exists()) { dbFile.delete(); } @@ -98,4 +100,84 @@ public void testClear() { List results = dataQueue.peek(4); Assert.assertEquals(0, results.size()); } + + @Test + public void testAddToCorruptedDatabase() { + dataQueue.add(new DataEntity("test_data_1")); + dataQueue.add(new DataEntity("test_data_2")); + Assert.assertEquals(2, dataQueue.count()); + + corruptDatabase(); + Assert.assertTrue(isDatabaseCorrupt()); + + // After detecting db is corrupt, resets the database and then adds the new entry. + Assert.assertTrue(dataQueue.add(new DataEntity("test_data_3"))); + Assert.assertEquals(1, dataQueue.count()); + Assert.assertEquals("test_data_3", dataQueue.peek().getData()); + Assert.assertFalse(isDatabaseCorrupt()); + } + + @Test + public void testRemoveFromCorruptedDatabase() { + dataQueue.add(new DataEntity("test_data_1")); + dataQueue.add(new DataEntity("test_data_2")); + Assert.assertEquals(2, dataQueue.count()); + + corruptDatabase(); + Assert.assertTrue(isDatabaseCorrupt()); + + // After detecting db is corrupt, resets the database. + Assert.assertFalse(dataQueue.remove()); + Assert.assertEquals(0, dataQueue.count()); + Assert.assertFalse(isDatabaseCorrupt()); + } + + @Test + public void testClearCorruptedDatabase() { + dataQueue.add(new DataEntity("test_data_1")); + dataQueue.add(new DataEntity("test_data_2")); + Assert.assertEquals(2, dataQueue.count()); + + corruptDatabase(); + Assert.assertTrue(isDatabaseCorrupt()); + + // After detecting db is corrupt, resets the database. + Assert.assertTrue(dataQueue.clear()); + Assert.assertEquals(0, dataQueue.count()); + Assert.assertFalse(isDatabaseCorrupt()); + } + + private File getDatabase() { + Context context = ApplicationProvider.getApplicationContext(); + return context.getDatabasePath(QUEUE_NAME); + } + + private void corruptDatabase() { + File dbFile = getDatabase(); + if (dbFile == null) { + return; + } + try { + FileOutputStream fos = new FileOutputStream(getDatabase()); + fos.write(new byte[1024]); + fos.close(); + } catch (Exception e) { + + } + } + + private boolean isDatabaseCorrupt() { + File dbFile = getDatabase(); + if (dbFile == null) { + return true; + } + try { + SQLiteDatabase database = + SQLiteDatabase.openDatabase( + dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY); + return database == null || !database.isDatabaseIntegrityOk(); + } catch (Exception e) { + return true; + } + } } diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt index 445fa530c..0ff66f4d0 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt @@ -13,7 +13,7 @@ package com.adobe.marketing.mobile.internal internal object CoreConstants { const val LOG_TAG = "MobileCore" - const val VERSION = "2.2.3" + const val VERSION = "2.3.0" object EventDataKeys { /** diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/AppIdManager.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/AppIdManager.kt index 430151512..54fe1cf1e 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/AppIdManager.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/AppIdManager.kt @@ -84,7 +84,7 @@ internal class AppIdManager { * @return the existing appId stored in shared preferences if it exists, * null otherwise. */ - private fun getAppIDFromPersistence(): String? { + internal fun getAppIDFromPersistence(): String? { return configStateStoreCollection?.getString( ConfigurationStateManager.PERSISTED_APPID, null diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtension.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtension.kt index c9013d04f..dc7e34f58 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtension.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtension.kt @@ -249,6 +249,14 @@ internal class ConfigurationExtension : Extension { return } + // Check if this is an internal request ovewriting explicit configure with appId request. + val isInternalEvent = DataReader.optBoolean(event.eventData, CONFIGURATION_REQUEST_CONTENT_IS_INTERNAL_EVENT, false) + if (isStaleAppIdUpdateRequest(appId, isInternalEvent)) { + Log.trace(TAG, TAG, "An explicit configure with AppId request has preceded this internal event.") + sharedStateResolver?.resolve(configurationStateManager.environmentAwareConfiguration) + return + } + // Stop all event processing for the extension until new configuration download is attempted api.stopEvents() @@ -540,4 +548,24 @@ internal class ConfigurationExtension : Extension { } } } + + /** + * Determines if the current AppID update request is stale. + * A request is considered stale if it is a configuration request sent internally + * and there is a newer request that has been sent externally via {@link MobileCore#configureWithAppId(String)} + * + * @param newAppId the new app ID with which the configuration update is being requested + * @param isInternalEvent whether the current request is an initial configuration request + * @return true if the current request is stale, false otherwise + */ + private fun isStaleAppIdUpdateRequest(newAppId: String, isInternalEvent: Boolean): Boolean { + // Because events are dispatched and processed serially, external config with app id events + // cannot be stale. + if (!isInternalEvent) return false + + // Load the currently persisted app id for validation + val persistedAppId = appIdManager.getAppIDFromPersistence() + + return !persistedAppId.isNullOrBlank() && newAppId != persistedAppId + } } diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/services/DeviceInforming.java b/code/core/src/main/java/com/adobe/marketing/mobile/services/DeviceInforming.java index d665682f0..249b240fe 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/services/DeviceInforming.java +++ b/code/core/src/main/java/com/adobe/marketing/mobile/services/DeviceInforming.java @@ -142,12 +142,20 @@ interface DisplayInformation { String getApplicationVersionCode(); /** - * Returns the currently selected / active locale value (as set by the user on the system). + * Returns the currently selected / active locale value with respect to the application context. * * @return A {@link Locale} value, if available, null otherwise */ Locale getActiveLocale(); + /** + * Returns the currently selected / active locale value on the device settings as set by the + * user. + * + * @return A {@link Locale} value, if available, null otherwise + */ + Locale getSystemLocale(); + /** * Returns information about the display hardware, as returned by the underlying OS. * diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/DeviceInfoService.java b/code/core/src/phone/java/com/adobe/marketing/mobile/services/DeviceInfoService.java index 7293cc64d..f6e596f25 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/DeviceInfoService.java +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/DeviceInfoService.java @@ -45,9 +45,9 @@ class DeviceInfoService implements DeviceInforming { DeviceInfoService() {} /** - * Returns the currently selected / active locale value (as set by the user on the system). + * Returns the currently selected / active locale value with respect to the application context. * - * @return A {@link Locale} value, if available, null otherwise. + * @return A {@link Locale} value, if available, null otherwise */ public Locale getActiveLocale() { final Context context = getApplicationContext(); @@ -56,23 +56,18 @@ public Locale getActiveLocale() { return null; } - final Resources resources = context.getResources(); - - if (resources == null) { - return null; - } - - final Configuration configuration = resources.getConfiguration(); - - if (configuration == null) { - return null; - } + return getLocaleFromResources(context.getResources()); + } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return configuration.locale; - } else { - return configuration.getLocales().get(0); - } + /** + * Returns the currently selected / active locale value on the device settings as set by the + * user. + * + * @return A {@link Locale} value, if available, null otherwise + */ + @Override + public Locale getSystemLocale() { + return getLocaleFromResources(Resources.getSystem()); } @Override @@ -581,6 +576,29 @@ public String getLocaleString() { return result; } + /** + * Returns the preferred locale value from the Resources object. + * + * @return A {@link Locale} value, if available, null otherwise + */ + private Locale getLocaleFromResources(final Resources resources) { + if (resources == null) { + return null; + } + + final Configuration configuration = resources.getConfiguration(); + + if (configuration == null) { + return null; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return configuration.locale; + } else { + return configuration.getLocales().get(0); + } + } + /** * Checks if a {@code String} is null, empty or it only contains whitespaces. * diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/SQLiteDataQueue.java b/code/core/src/phone/java/com/adobe/marketing/mobile/services/SQLiteDataQueue.java index 875bbfa63..1203fabc7 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/SQLiteDataQueue.java +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/SQLiteDataQueue.java @@ -16,7 +16,9 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; +import com.adobe.marketing.mobile.internal.util.FileUtils; import com.adobe.marketing.mobile.internal.util.SQLiteDatabaseHelper; +import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -57,39 +59,15 @@ public boolean add(final DataEntity dataEntity) { return false; } - return SQLiteDatabaseHelper.process( - databasePath, - SQLiteDatabaseHelper.DatabaseOpenMode.READ_WRITE, - database -> { - if (database == null) { - return false; - } - final int INDEX_UUID = 1; - final int INDEX_TIMESTAMP = 2; - final int INDEX_DATA = 3; - try (SQLiteStatement insertStatement = - database.compileStatement( - "INSERT INTO " - + TABLE_NAME - + " (uniqueIdentifier, timestamp, data) VALUES (?," - + " ?, ?)")) { - insertStatement.bindString( - INDEX_UUID, dataEntity.getUniqueIdentifier()); - insertStatement.bindLong( - INDEX_TIMESTAMP, dataEntity.getTimestamp().getTime()); - insertStatement.bindString( - INDEX_DATA, - dataEntity.getData() != null ? dataEntity.getData() : ""); - long rowId = insertStatement.executeInsert(); - return rowId >= 0; - } catch (Exception e) { - Log.debug( - ServiceConstants.LOG_TAG, - LOG_PREFIX, - "add - Returning false: " + e.getLocalizedMessage()); - return false; - } - }); + boolean result = tryAddEntity(dataEntity); + + if (!result) { + resetDatabase(); + // Retry adding the data after resetting the database. + result = tryAddEntity(dataEntity); + } + + return result; } } @@ -226,44 +204,52 @@ public boolean remove(final int n) { return false; } - return SQLiteDatabaseHelper.process( - databasePath, - SQLiteDatabaseHelper.DatabaseOpenMode.READ_WRITE, - database -> { - int deletedRowsCount = -1; - if (database == null) { - return false; - } - String builder = - "DELETE FROM " - + TABLE_NAME - + " WHERE id in (" - + "SELECT id from " - + TABLE_NAME - + " order by id ASC" - + " limit " - + n - + ')'; - try (SQLiteStatement statement = database.compileStatement(builder)) { - deletedRowsCount = statement.executeUpdateDelete(); - Log.trace( - ServiceConstants.LOG_TAG, - LOG_PREFIX, - String.format( - "remove n - Removed %d DataEntities", - deletedRowsCount)); - return deletedRowsCount > -1; - } catch (final SQLiteException e) { - Log.warning( - ServiceConstants.LOG_TAG, - LOG_PREFIX, - String.format( - "removeRows - Error in deleting rows from table(%s)." - + " Returning 0. Error: (%s)", - TABLE_NAME, e.getMessage())); - return false; - } - }); + boolean result = + SQLiteDatabaseHelper.process( + databasePath, + SQLiteDatabaseHelper.DatabaseOpenMode.READ_WRITE, + database -> { + int deletedRowsCount = -1; + if (database == null) { + return false; + } + String builder = + "DELETE FROM " + + TABLE_NAME + + " WHERE id in (" + + "SELECT id from " + + TABLE_NAME + + " order by id ASC" + + " limit " + + n + + ')'; + try (SQLiteStatement statement = + database.compileStatement(builder)) { + deletedRowsCount = statement.executeUpdateDelete(); + Log.trace( + ServiceConstants.LOG_TAG, + LOG_PREFIX, + String.format( + "remove n - Removed %d DataEntities", + deletedRowsCount)); + return deletedRowsCount > -1; + } catch (final SQLiteException e) { + Log.warning( + ServiceConstants.LOG_TAG, + LOG_PREFIX, + String.format( + "removeRows - Error in deleting rows from" + + " table(%s). Returning 0. Error: (%s)", + TABLE_NAME, e.getMessage())); + return false; + } + }); + + if (!result) { + resetDatabase(); + } + + return result; } } @@ -290,7 +276,11 @@ public boolean clear() { String.format( "clear - %s in clearing Table %s", (result ? "Successful" : "Failed"), TABLE_NAME)); - return result; + + if (!result) { + resetDatabase(); + } + return true; } } @@ -349,4 +339,63 @@ private void createTableIfNotExists() { "createTableIfNotExists - Error creating/accessing table (%s) ", TABLE_NAME)); } + + /** + * Add a new {@link DataEntity} Object to {@link DataQueue}. NOTE: The caller must hold the + * dbMutex. + */ + private boolean tryAddEntity(final DataEntity dataEntity) { + return SQLiteDatabaseHelper.process( + databasePath, + SQLiteDatabaseHelper.DatabaseOpenMode.READ_WRITE, + database -> { + if (database == null) { + return false; + } + final int INDEX_UUID = 1; + final int INDEX_TIMESTAMP = 2; + final int INDEX_DATA = 3; + try (SQLiteStatement insertStatement = + database.compileStatement( + "INSERT INTO " + + TABLE_NAME + + " (uniqueIdentifier, timestamp, data) VALUES (?," + + " ?, ?)")) { + insertStatement.bindString(INDEX_UUID, dataEntity.getUniqueIdentifier()); + insertStatement.bindLong( + INDEX_TIMESTAMP, dataEntity.getTimestamp().getTime()); + insertStatement.bindString( + INDEX_DATA, + dataEntity.getData() != null ? dataEntity.getData() : ""); + long rowId = insertStatement.executeInsert(); + return rowId >= 0; + } catch (Exception e) { + Log.debug( + ServiceConstants.LOG_TAG, + LOG_PREFIX, + "add - Returning false: " + e.getLocalizedMessage()); + return false; + } + }); + } + + /** Resets the database. NOTE: The caller must hold the dbMutex. */ + private void resetDatabase() { + Log.warning( + ServiceConstants.LOG_TAG, + LOG_PREFIX, + "resetDatabase - Resetting database (%s) as it is corrupted", + databasePath); + + try { + FileUtils.deleteFile(new File(databasePath), false); + createTableIfNotExists(); + } catch (Exception ex) { + Log.warning( + ServiceConstants.LOG_TAG, + LOG_PREFIX, + "resetDatabase - Error resetting database (%s) ", + databasePath); + } + } } diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPMessage.java b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPMessage.java index c80f075a4..c03a761c5 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPMessage.java +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPMessage.java @@ -18,13 +18,13 @@ import android.content.Intent; import android.graphics.Color; import android.view.View; -import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.TranslateAnimation; import android.webkit.WebSettings; import android.webkit.WebView; +import android.widget.FrameLayout; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.cardview.widget.CardView; @@ -68,7 +68,7 @@ class AEPMessage implements FullscreenMessage { // private vars private WebView webView; private CardView webViewFrame; - private ViewGroup.LayoutParams params; + private FrameLayout.LayoutParams params; private final String html; private MessageSettings settings; private Animation dismissAnimation; @@ -143,20 +143,20 @@ void setWebView(final WebView webView) { } /** - * Returns the {@link ViewGroup.LayoutParams} created for this message. + * Returns the {@link FrameLayout.LayoutParams} created for this message. * - * @return the created {@code ViewGroup.LayoutParams} + * @return the created {@code FrameLayout.LayoutParams} */ - ViewGroup.LayoutParams getParams() { + FrameLayout.LayoutParams getParams() { return params; } /** - * Sets the {@link ViewGroup.LayoutParams} for this message. + * Sets the {@link FrameLayout.LayoutParams} for this message. * - * @param params the {@code ViewGroup.LayoutParams} to be set + * @param params the {@code FrameLayout.LayoutParams} to be set */ - void setParams(final ViewGroup.LayoutParams params) { + void setParams(final FrameLayout.LayoutParams params) { this.params = params; } diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageFragment.java b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageFragment.java index f4a3083fe..0a5abeee4 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageFragment.java +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageFragment.java @@ -17,15 +17,14 @@ import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; import android.os.Bundle; -import android.util.TypedValue; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; +import android.widget.FrameLayout; import androidx.annotation.VisibleForTesting; import androidx.cardview.widget.CardView; import androidx.fragment.app.DialogFragment; @@ -383,7 +382,7 @@ private void applyBackdropColor() { /** Add the IAM WebView as the {@link MessageFragment} Dialog's content view. */ private void updateDialogView() { final Dialog dialog = getDialog(); - final ViewGroup.LayoutParams params = message.getParams(); + final FrameLayout.LayoutParams params = message.getParams(); final CardView webViewFrame = message.getWebViewFrame(); if (dialog == null || webViewFrame == null || params == null) { @@ -395,16 +394,6 @@ private void updateDialogView() { return; } - // use a gradient drawable to set the rounded corners on the message - final GradientDrawable roundedDrawable = new GradientDrawable(); - final float calculatedRadius = - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - message.getMessageSettings().getCornerRadius(), - dialog.getContext().getResources().getDisplayMetrics()); - roundedDrawable.setCornerRadius(calculatedRadius); - webViewFrame.setBackground(roundedDrawable); - dialog.setContentView(webViewFrame, params); webViewFrame.setOnTouchListener(this); } diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtil.java b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtil.java index 637c77566..7f0849a18 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtil.java +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtil.java @@ -13,6 +13,8 @@ import android.content.Context; import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.util.TypedValue; import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.AlphaAnimation; @@ -21,6 +23,7 @@ import android.view.animation.TranslateAnimation; import android.webkit.WebSettings; import android.webkit.WebView; +import android.widget.FrameLayout; import androidx.cardview.widget.CardView; import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.services.ServiceConstants; @@ -38,6 +41,7 @@ class MessageWebViewUtil { private static final String UNEXPECTED_NULL_VALUE = "Unexpected Null Value"; private static final int FULLSCREEN_PERCENTAGE = 100; private static final int ANIMATION_DURATION = 300; + private static final float WORKAROUND_ALPHA_VALUE = 0.99f; private static final String BASE_URL = "file:///android_asset/"; private static final String MIME_TYPE = "text/html"; @@ -152,9 +156,22 @@ void show(final AEPMessage message) { webviewSettings.setUseWideViewPort(true); } - webViewFrame.addView(webView); + // use a gradient drawable to set the rounded corners on the message + final GradientDrawable roundedDrawable = new GradientDrawable(); + final float calculatedRadius = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + message.getMessageSettings().getCornerRadius(), + context.getResources().getDisplayMetrics()); + roundedDrawable.setCornerRadius(calculatedRadius); + webViewFrame.setBackground(roundedDrawable); + + // set webview alpha to 99% to allow rounded corners to be applied on messages on API22 + // and below + webView.setAlpha(WORKAROUND_ALPHA_VALUE); // add the created cardview containing the webview to the message object + webViewFrame.addView(webView); message.setWebViewFrame(webViewFrame); setMessageLayoutParameters(message); @@ -233,8 +250,7 @@ private Animation setupDisplayAnimation(final AEPMessage message, final WebView * @param message {@link AEPMessage} containing the in-app message payload */ private void setMessageLayoutParameters(final AEPMessage message) { - ViewGroup.MarginLayoutParams params = - new ViewGroup.MarginLayoutParams(messageWidth, messageHeight); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(messageWidth, messageHeight); params.topMargin = originY; params.leftMargin = originX; message.setParams(params); diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/MobileCoreTests.kt b/code/core/src/test/java/com/adobe/marketing/mobile/MobileCoreTests.kt index c6e7b82ff..84d0da242 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/MobileCoreTests.kt +++ b/code/core/src/test/java/com/adobe/marketing/mobile/MobileCoreTests.kt @@ -35,7 +35,7 @@ import kotlin.test.assertTrue @RunWith(MockitoJUnitRunner.Silent::class) class MobileCoreTests { - private var EXTENSION_VERSION = "2.2.3" + private var EXTENSION_VERSION = "2.3.0" @Mock private lateinit var mockedEventHub: EventHub diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtensionTests.kt b/code/core/src/test/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtensionTests.kt index b717d776d..7eafbbf33 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtensionTests.kt +++ b/code/core/src/test/java/com/adobe/marketing/mobile/internal/configuration/ConfigurationExtensionTests.kt @@ -18,6 +18,7 @@ import com.adobe.marketing.mobile.ExtensionApi import com.adobe.marketing.mobile.ExtensionHelper import com.adobe.marketing.mobile.SharedStateResolver import com.adobe.marketing.mobile.internal.configuration.ConfigurationExtension.Companion.CONFIGURATION_REQUEST_CONTENT_CLEAR_UPDATED_CONFIG +import com.adobe.marketing.mobile.internal.configuration.ConfigurationExtension.Companion.CONFIGURATION_REQUEST_CONTENT_IS_INTERNAL_EVENT import com.adobe.marketing.mobile.internal.configuration.ConfigurationExtension.Companion.CONFIGURATION_REQUEST_CONTENT_JSON_APP_ID import com.adobe.marketing.mobile.internal.configuration.ConfigurationExtension.Companion.CONFIGURATION_REQUEST_CONTENT_JSON_ASSET_FILE import com.adobe.marketing.mobile.internal.configuration.ConfigurationExtension.Companion.CONFIGURATION_REQUEST_CONTENT_JSON_FILE_PATH @@ -59,7 +60,7 @@ import kotlin.test.assertTrue @RunWith(MockitoJUnitRunner.Silent::class) class ConfigurationExtensionTests { - private var EXTENSION_VERSION = "2.2.3" + private var EXTENSION_VERSION = "2.3.0" @Mock private lateinit var mockServiceProvider: ServiceProvider @@ -159,7 +160,7 @@ class ConfigurationExtensionTests { EventSource.REQUEST_CONTENT, mapOf( CONFIGURATION_REQUEST_CONTENT_JSON_APP_ID to "SampleAppID", - ConfigurationExtension.CONFIGURATION_REQUEST_CONTENT_IS_INTERNAL_EVENT to true + CONFIGURATION_REQUEST_CONTENT_IS_INTERNAL_EVENT to true ), null ) @@ -252,7 +253,7 @@ class ConfigurationExtensionTests { EventSource.REQUEST_CONTENT, mapOf( CONFIGURATION_REQUEST_CONTENT_JSON_APP_ID to "SampleAppID", - "config.isinternalevent" to true + CONFIGURATION_REQUEST_CONTENT_IS_INTERNAL_EVENT to true ), null ) @@ -461,7 +462,7 @@ class ConfigurationExtensionTests { EventSource.REQUEST_CONTENT, mapOf( CONFIGURATION_REQUEST_CONTENT_JSON_APP_ID to "SampleAppID", - "config.isinternalevent" to true + CONFIGURATION_REQUEST_CONTENT_IS_INTERNAL_EVENT to true ), null ) diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java b/code/core/src/test/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java index 1f900b76e..ba6d9d220 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java +++ b/code/core/src/test/java/com/adobe/marketing/mobile/services/SqliteDataQueueTests.java @@ -215,7 +215,7 @@ public void clearTableWithDatabaseOpenError() { boolean result = dataQueue.clear(); // Assertions - Assert.assertFalse(result); + Assert.assertTrue(result); } } diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/AEPMessageTests.java b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/AEPMessageTests.java index 15dcb258a..8d6a0bfdf 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/AEPMessageTests.java +++ b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/AEPMessageTests.java @@ -686,6 +686,54 @@ public void aepMessageIsDismissed_When_MessagingDelegateSet_And_MessageDismissed .startAnimation(ArgumentMatchers.any(Animation.class)); } + @Test + public void + aepMessageIsDismissed_When_MessagingDelegateSet_And_MessageDismissedWithBackButton() { + // setup + Mockito.when(mockAEPMessageSettings.getDismissAnimation()) + .thenReturn(MessageAnimation.BOTTOM); + Mockito.when(mockMessageMonitor.isDisplayed()).thenReturn(true); + Mockito.when(mockMessageMonitor.dismiss()).thenReturn(true); + Mockito.when( + mockMessagingDelegate.shouldShowMessage( + ArgumentMatchers.any(AEPMessage.class))) + .thenReturn(true); + setupFragmentTransactionMocks(); + + try { + message = + new AEPMessage( + "html", + mockFullscreenMessageDelegate, + false, + mockMessageMonitor, + mockAEPMessageSettings, + mockExecutor); + } catch (MessageCreationException ex) { + Assert.fail(ex.getMessage()); + } + + Mockito.when(mockMessageFragment.isDismissedWithGesture()).thenReturn(false); + message.setMessageFragment(mockMessageFragment); + message.setWebView(mockWebView); + message.setWebViewFrame(mockCardView); + Mockito.when(mockViewGroup.getMeasuredWidth()).thenReturn(1000); + Mockito.when(mockViewGroup.getMeasuredHeight()).thenReturn(1000); + // test + message.dismiss(true); + message.getAnimationListener().onAnimationEnd(mockAnimation); + // verify listeners are called for a message dismiss and device back button press + Mockito.verify(mockMessageMonitor, Mockito.times(1)).dismiss(); + Mockito.verify(mockMessagingDelegate, Mockito.times(1)) + .onDismiss(any(FullscreenMessage.class)); + Mockito.verify(mockFullscreenMessageDelegate, Mockito.times(1)) + .onBackPressed(any(FullscreenMessage.class)); + Mockito.verify(mockFullscreenMessageDelegate, Mockito.times(1)) + .onDismiss(any(FullscreenMessage.class)); + Mockito.verify(mockCardView, Mockito.times(1)) + .startAnimation(ArgumentMatchers.any(Animation.class)); + } + // mock fragment setup helper void setupFragmentTransactionMocks() { Mockito.when(mockActivity.getFragmentManager()).thenReturn(mockFragmentManager); diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtilTests.java b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtilTests.java index f788f2dc2..6c831fcf3 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtilTests.java +++ b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/MessageWebViewUtilTests.java @@ -18,9 +18,9 @@ import android.content.Context; import android.content.res.Resources; import android.util.DisplayMetrics; -import android.view.ViewGroup; import android.webkit.WebSettings; import android.webkit.WebView; +import android.widget.FrameLayout; import androidx.cardview.widget.CardView; import com.adobe.marketing.mobile.services.AppContextService; import com.adobe.marketing.mobile.services.ServiceProviderModifier; @@ -107,7 +107,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -128,7 +128,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -149,7 +149,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -170,7 +170,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -191,7 +191,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -212,7 +212,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -233,7 +233,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -254,7 +254,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -275,7 +275,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -296,7 +296,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -317,7 +317,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -338,7 +338,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -357,7 +357,7 @@ public void testRunnable_WithValidAEPMessage_ThenWebviewLoadDataCalled() { Mockito.verify(mockWebview, Mockito.times(1)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(1)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } @@ -376,7 +376,7 @@ public void testRunnable_WithInvalidAEPMessage_ThenWebviewLoadDataNotCalled() { Mockito.verify(mockWebview, Mockito.times(0)) .setOnTouchListener(any(MessageFragment.class)); Mockito.verify(mockAEPMessage, Mockito.times(0)) - .setParams(any(ViewGroup.MarginLayoutParams.class)); + .setParams(any(FrameLayout.LayoutParams.class)); } } } diff --git a/code/gradle.properties b/code/gradle.properties index 88068d1b5..e8144efde 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -6,7 +6,7 @@ android.useAndroidX=true # #Maven artifacts #Core extension -coreExtensionVersion=2.2.3 +coreExtensionVersion=2.3.0 coreExtensionName=core coreExtensionAARName=core-phone-release.aar coreMavenRepoName=AdobeMobileCoreSdk @@ -18,7 +18,7 @@ signalExtensionAARName=signal-phone-release.aar signalMavenRepoName=AdobeMobileSignalSdk signalMavenRepoDescription=Android Signal Extension for Adobe Mobile Marketing #Lifecycle extension -lifecycleExtensionVersion=2.0.3 +lifecycleExtensionVersion=2.0.4 lifecycleExtensionName=lifecycle lifecycleExtensionAARName=lifecycle-phone-release.aar lifecycleMavenRepoName=AdobeMobileLifecycleSdk diff --git a/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/SDKHelper.kt b/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/SDKHelper.kt index e3683a308..d4652f36a 100644 --- a/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/SDKHelper.kt +++ b/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/SDKHelper.kt @@ -40,7 +40,7 @@ object SDKHelper { private const val LIFECYCLE_DATA_STORE = "AdobeMobile_Lifecycle" - private fun setupNetworkService( + fun setupNetworkService( configURL: String, mockConfigResponse: Map, rulesURL: String?, @@ -72,6 +72,9 @@ object SDKHelper { HttpURLConnection.HTTP_OK, "OK", emptyMap(), rulesStream, urlMonitor ) } + else -> { + connection = MockNetworkResponse(HttpURLConnection.HTTP_NOT_FOUND, "NOT FOUND", emptyMap(), "".byteInputStream(), urlMonitor) + } } if (callback != null && connection != null) { diff --git a/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/integration/core/ConfigurationIntegrationTests.kt b/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/integration/core/ConfigurationIntegrationTests.kt index 25c272beb..393fcf309 100644 --- a/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/integration/core/ConfigurationIntegrationTests.kt +++ b/code/integration-tests/src/androidTest/java/com/adobe/marketing/mobile/integration/core/ConfigurationIntegrationTests.kt @@ -11,6 +11,8 @@ package com.adobe.marketing.mobile.integration.core +import android.content.Context +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.adobe.marketing.mobile.MobileCore import com.adobe.marketing.mobile.MobilePrivacyStatus @@ -28,6 +30,7 @@ import org.junit.runner.RunWith class ConfigurationIntegrationTests { companion object { const val TEST_APP_ID = "appId" + const val CONFIGURATION_STATE_PREF = "AdobeMobile_ConfigState" const val TEST_RULES_RESOURCE = "rules_configuration_tests.zip" const val WAIT_TIME_MILLIS = 5000L } @@ -112,6 +115,45 @@ class ConfigurationIntegrationTests { validatePrivacyStatus(MobilePrivacyStatus.OPT_IN) } + @Test + fun testConfigurationWithAppIDIsNotOverwrittenByCache() { + SDKHelper.setupConfiguration(TEST_APP_ID, emptyMap(), TEST_RULES_RESOURCE) + + val context = ApplicationProvider.getApplicationContext() + // get config state + val configSharedPreference = context.getSharedPreferences(CONFIGURATION_STATE_PREF, 0) + val persistedAppId = configSharedPreference.getString("config.appID", null) + Assert.assertEquals(TEST_APP_ID, persistedAppId) + + //Simulate shut down and initialize SDK again. Ensure that the cached configuration is retained + SDKHelper.resetSDK(false) + + // === Custom initialization with new app id=== + val newAppID = "NEW_APP_ID" + val newConfigURL = "https://assets.adobedtm.com/$newAppID.json" + val newRulesURL = "https://assets.adobedtm.com/$newAppID-rules.zip" + + // Configure with new app id before registering extensions + MobileCore.configureWithAppID("NEW_APP_ID") + SDKHelper.initializeSDK(listOf(Signal.EXTENSION)) + + val newConfigUrlValidationLatch = CountDownLatch(1) + val newRulesUrlValidationLatch = CountDownLatch(1) + SDKHelper.setupNetworkService(newConfigURL, emptyMap(), newRulesURL, TEST_RULES_RESOURCE) { + when(it) { + newConfigURL -> newConfigUrlValidationLatch.countDown() + newRulesURL -> newRulesUrlValidationLatch.countDown() + } + } + newConfigUrlValidationLatch.await(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS) + newRulesUrlValidationLatch.await(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS) + + // Verify configuration is updated and cached appId does not overwrite the new configuration + val newConfigSharedPreference = context.getSharedPreferences(CONFIGURATION_STATE_PREF, 0) + val newPersistedAppId = newConfigSharedPreference.getString("config.appID", null) + Assert.assertEquals(newAppID, newPersistedAppId) + } + @Test fun testClearUpdatedConfiguration() { SDKHelper.setupConfiguration(TEST_APP_ID, mapOf("global.privacy" to "optedin"), TEST_RULES_RESOURCE) diff --git a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java index e5bf1bef6..36bd9e9a3 100644 --- a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java +++ b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java @@ -76,6 +76,8 @@ public class LifecycleFunctionalTest { private static final String RUN_MODE = "runmode"; private static final String SESSION_EVENT = "sessionevent"; private static final String SESSION_START_TIMESTAMP = "starttimestampmillis"; + + private static final String SYSTEM_LOCALE = "systemlocale"; private static final String UPGRADE_EVENT = "upgradeevent"; private static final String DATA_STORE_NAME = "AdobeMobile_Lifecycle"; private static final String LIFECYCLE_CONFIG_SESSION_TIMEOUT = "lifecycle.sessionTimeout"; @@ -141,7 +143,8 @@ public int getDensityDpi() { mockDeviceInfoService.operatingSystemName = "TEST_OS"; mockDeviceInfoService.operatingSystemVersion = "5.55"; mockDeviceInfoService.mobileCarrierName = "TEST_CARRIER"; - mockDeviceInfoService.activeLocale = new Locale("en", "US"); + mockDeviceInfoService.activeLocale = Locale.US; + mockDeviceInfoService.systemLocale = Locale.FRANCE; mockDeviceInfoService.runMode = "APPLICATION"; } @@ -192,6 +195,7 @@ private String getDayOfWeek(long timestampMillis) { put(LAUNCHES, "1"); put(OPERATING_SYSTEM, "TEST_OS 5.55"); put(LOCALE, "en-US"); + put(SYSTEM_LOCALE, "fr-FR"); put(DEVICE_RESOLUTION, "100x100"); put(CARRIER_NAME, "TEST_CARRIER"); put(DEVICE_NAME, "deviceName"); @@ -331,6 +335,7 @@ public void testLifecycle__When__SecondLaunch_BeforeSessionTimeout__Then__GetNoL put(PREVIOUS_OS, "TEST_OS 5.55"); put(OPERATING_SYSTEM, "TEST_OS 5.55"); put(LOCALE, "en-US"); + put(SYSTEM_LOCALE, "fr-FR"); put(DEVICE_RESOLUTION, "100x100"); put(CARRIER_NAME, "TEST_CARRIER"); put(DEVICE_NAME, "deviceName"); @@ -393,6 +398,7 @@ public void testLifecycle__When__SecondLaunch_AfterSessionTimeout__Then__GetLaun put(LAUNCHES, "2"); put(OPERATING_SYSTEM, "TEST_OS 5.55"); put(LOCALE, "en-US"); + put(SYSTEM_LOCALE, "fr-FR"); put(DEVICE_RESOLUTION, "100x100"); put(CARRIER_NAME, "TEST_CARRIER"); put(DEVICE_NAME, "deviceName"); @@ -559,6 +565,7 @@ public void testLifecycle__When__SecondLaunch_VersionNumberChanged__Then__GetUpg put(LAUNCHES, "2"); put(OPERATING_SYSTEM, "TEST_OS 5.55"); put(LOCALE, "en-US"); + put(SYSTEM_LOCALE, "fr-FR"); put(DEVICE_RESOLUTION, "100x100"); put(CARRIER_NAME, "TEST_CARRIER"); put(DEVICE_NAME, "deviceName"); @@ -798,6 +805,7 @@ public void testLifecycle__When__ThreeDaysAfterUpgrade__Then__DaysSinceLastUpgra put(LAUNCHES, "1"); put(OPERATING_SYSTEM, "TEST_OS 5.55"); put(LOCALE, "en-US"); + put(SYSTEM_LOCALE, "fr-FR"); put(DEVICE_RESOLUTION, "100x100"); put(CARRIER_NAME, "TEST_CARRIER"); put(DEVICE_NAME, "deviceName"); diff --git a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java index 994d96b15..31a87d704 100644 --- a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java +++ b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java @@ -89,9 +89,13 @@ public void beforeEach() { expectedEnvironmentInfo.put("operatingSystemVersion", "5.55"); expectedEnvironmentInfo.put("operatingSystem", "TEST_OS"); expectedEnvironmentInfo.put("type", "application"); - Map localeMap = new HashMap<>(); - localeMap.put("language", "en-US"); - expectedEnvironmentInfo.put("_dc", localeMap); + expectedEnvironmentInfo.put( + "_dc", + new HashMap() { + { + put("language", "fr-FR"); + } + }); expectedDeviceInfo.put("manufacturer", "Android"); expectedDeviceInfo.put("model", "deviceName"); @@ -128,7 +132,8 @@ public int getDensityDpi() { mockDeviceInfoService.operatingSystemName = "TEST_OS"; mockDeviceInfoService.operatingSystemVersion = "5.55"; mockDeviceInfoService.mobileCarrierName = "TEST_CARRIER"; - mockDeviceInfoService.activeLocale = new Locale("en", "US"); + mockDeviceInfoService.activeLocale = Locale.US; + mockDeviceInfoService.systemLocale = Locale.FRANCE; mockDeviceInfoService.runMode = "APPLICATION"; mockDeviceInfoService.deviceManufacturer = "Android"; mockDeviceInfoService.applicationPackageName = "TEST_PACKAGE_NAME"; @@ -164,6 +169,13 @@ private void initTimestamps() { expectedApplicationInfo.put("isInstall", true); expectedApplicationInfo.put("isLaunch", true); expectedApplicationInfo.put("id", "TEST_PACKAGE_NAME"); + expectedApplicationInfo.put( + "_dc", + new HashMap() { + { + put("language", "en-US"); + } + }); Map expectedXDMData = new HashMap() { @@ -287,6 +299,13 @@ public void testLifecycleV2__When__Pause__Then__DispatchLifecycleApplicationClos expectedApplicationInfo.put("isUpgrade", true); expectedApplicationInfo.put("isLaunch", true); expectedApplicationInfo.put("id", "TEST_PACKAGE_NAME"); + expectedApplicationInfo.put( + "_dc", + new HashMap() { + { + put("language", "en-US"); + } + }); Map expectedXDMData = new HashMap() { @@ -353,6 +372,13 @@ public void testLifecycleV2__When__Pause__Then__DispatchLifecycleApplicationClos expectedApplicationInfo.put("version", "1.1 (12345)"); expectedApplicationInfo.put("isLaunch", true); expectedApplicationInfo.put("id", "TEST_PACKAGE_NAME"); + expectedApplicationInfo.put( + "_dc", + new HashMap() { + { + put("language", "en-US"); + } + }); Map expectedXDMData = new HashMap() { diff --git a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/MockDeviceInfoService.java b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/MockDeviceInfoService.java index 139506169..0e6ea5f29 100644 --- a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/MockDeviceInfoService.java +++ b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/MockDeviceInfoService.java @@ -90,6 +90,13 @@ public Locale getActiveLocale() { return activeLocale; } + public Locale systemLocale = Locale.FRANCE; + + @Override + public Locale getSystemLocale() { + return systemLocale; + } + public DeviceInforming.DisplayInformation displayInformation; @Override diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleConstants.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleConstants.java index 90492ea63..1103754e7 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleConstants.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleConstants.java @@ -118,6 +118,7 @@ static final class Lifecycle { static final String RUN_MODE = "runmode"; static final String SESSION_EVENT = "sessionevent"; static final String SESSION_START_TIMESTAMP = "starttimestampmillis"; + static final String SYSTEM_LOCALE = "systemlocale"; static final String UPGRADE_EVENT = "upgradeevent"; private Lifecycle() {} diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleMetricsBuilder.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleMetricsBuilder.java index 77b70192d..21b0ea49a 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleMetricsBuilder.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleMetricsBuilder.java @@ -322,6 +322,12 @@ LifecycleMetricsBuilder addCoreData() { lifecycleData.put(LifecycleConstants.EventDataKeys.Lifecycle.LOCALE, locale); } + final String systemLocale = LifecycleUtil.formatLocale(deviceInfoService.getSystemLocale()); + if (!StringUtils.isNullOrEmpty(systemLocale)) { + lifecycleData.put( + LifecycleConstants.EventDataKeys.Lifecycle.SYSTEM_LOCALE, systemLocale); + } + final String runMode = deviceInfoService.getRunMode(); if (!StringUtils.isNullOrEmpty(runMode)) { diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java index aaa85986c..78498b349 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java @@ -135,6 +135,8 @@ private XDMLifecycleApplication computeAppLaunchData( xdmApplicationInfoLaunch.setName(deviceInfoService.getApplicationName()); xdmApplicationInfoLaunch.setId(deviceInfoService.getApplicationPackageName()); xdmApplicationInfoLaunch.setVersion(getAppVersion()); + xdmApplicationInfoLaunch.setLanguage( + LifecycleUtil.formatLocaleXDM(deviceInfoService.getActiveLocale())); return xdmApplicationInfoLaunch; } @@ -194,7 +196,7 @@ private XDMLifecycleEnvironment computeEnvironmentData() { xdmEnvironmentInfo.setOperatingSystem(deviceInfoService.getOperatingSystemName()); xdmEnvironmentInfo.setOperatingSystemVersion(deviceInfoService.getOperatingSystemVersion()); xdmEnvironmentInfo.setLanguage( - LifecycleUtil.formatLocaleXDM(deviceInfoService.getActiveLocale())); + LifecycleUtil.formatLocaleXDM(deviceInfoService.getSystemLocale())); return xdmEnvironmentInfo; } diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLanguage.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLanguage.java new file mode 100644 index 000000000..a12104093 --- /dev/null +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLanguage.java @@ -0,0 +1,53 @@ +/* + Copyright 2023 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 androidx.annotation.NonNull; +import com.adobe.marketing.mobile.util.StringUtils; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +class XDMLanguage { + 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); + private final String language; + + XDMLanguage(final String language) { + if (StringUtils.isNullOrEmpty(language) || !isValidLanguageTag(language)) { + throw new IllegalArgumentException("Language tag failed validation"); + } + + this.language = language; + } + + String getLanguage() { + return this.language; + } + + Map serializeToXdm() { + Map map = new HashMap(); + map.put("language", this.language); + return map; + } + + /** + * 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/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleApplication.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleApplication.java index c97b3daec..7fa485bf8 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleApplication.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleApplication.java @@ -11,6 +11,7 @@ package com.adobe.marketing.mobile.lifecycle; +import com.adobe.marketing.mobile.services.Log; import java.util.HashMap; import java.util.Map; @@ -18,12 +19,14 @@ @SuppressWarnings("unused") class XDMLifecycleApplication { + private final String LOG_SOURCE = "XDMLifecycleApplication"; private XDMLifecycleCloseTypeEnum closeType; private String id; private boolean isClose; private boolean isInstall; private boolean isLaunch; private boolean isUpgrade; + private XDMLanguage language; private String name; private int sessionLength; private String version; @@ -69,6 +72,10 @@ Map serializeToXdm() { map.put("sessionLength", this.sessionLength); } + if (this.language != null) { + map.put("_dc", this.language.serializeToXdm()); + } + return map; } @@ -186,6 +193,36 @@ void setIsUpgrade(final boolean newValue) { this.isUpgrade = newValue; } + /** + * Returns the Language property The language of the environment to represent the user's + * linguistic, geographical, or cultural preferences for data presentation. + * + * @return {@link String} value or null if the property is not set + */ + String getLanguage() { + return this.language != null ? this.language.getLanguage() : null; + } + + /** + * Sets the Language property The language of the environment to represent the user's + * linguistic, geographical, or cultural preferences for data presentation (according to IETF + * RFC 3066). + * + * @param newValue the new Language value + */ + void setLanguage(final String newValue) { + try { + this.language = new XDMLanguage(newValue); + } catch (IllegalArgumentException ex) { + Log.warning( + LifecycleConstants.LOG_TAG, + LOG_SOURCE, + "Language tag '%s' failed validation and will be dropped. Values for XDM" + + " field 'application._dc.language' must conform to BCP 47.", + newValue); + } + } + /** * Returns the Name property Name of the application. * diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java index 8f4db8d4b..2c441a0b3 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/XDMLifecycleEnvironment.java @@ -11,12 +11,9 @@ 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. @@ -27,13 +24,10 @@ class XDMLifecycleEnvironment { private final String LOG_SOURCE = "XDMLifecycleEnvironment"; private String carrier; - private String language; + private XDMLanguage 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() {} @@ -44,19 +38,8 @@ Map serializeToXdm() { map.put("carrier", this.carrier); } - 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.language != null) { + map.put("_dc", this.language.serializeToXdm()); } if (this.operatingSystem != null) { @@ -105,7 +88,7 @@ void setCarrier(final String newValue) { * @return {@link String} value or null if the property is not set */ String getLanguage() { - return this.language; + return this.language != null ? this.language.getLanguage() : null; } /** @@ -116,7 +99,16 @@ String getLanguage() { * @param newValue the new Language value */ void setLanguage(final String newValue) { - this.language = newValue; + try { + this.language = new XDMLanguage(newValue); + } catch (IllegalArgumentException ex) { + 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.", + newValue); + } } /** @@ -180,14 +172,4 @@ 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/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java b/code/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java index 1bcd845ca..843191832 100644 --- a/code/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java +++ b/code/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java @@ -17,7 +17,7 @@ public class Lifecycle { - private static final String EXTENSION_VERSION = "2.0.3"; + private static final String EXTENSION_VERSION = "2.0.4"; public static final Class EXTENSION = LifecycleExtension.class; diff --git a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/LifecycleAPITests.java b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/LifecycleAPITests.java index 783bcdd81..88bd1bb8d 100644 --- a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/LifecycleAPITests.java +++ b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/LifecycleAPITests.java @@ -25,7 +25,7 @@ @RunWith(MockitoJUnitRunner.Silent.class) public class LifecycleAPITests { - private static final String EXTENSION_VERSION = "2.0.3"; + private static final String EXTENSION_VERSION = "2.0.4"; @Test public void test_extensionVersion() { diff --git a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleExtensionTests.java b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleExtensionTests.java index 9c41fc17c..703b8e104 100644 --- a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleExtensionTests.java +++ b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleExtensionTests.java @@ -38,7 +38,7 @@ @RunWith(MockitoJUnitRunner.Silent.class) public class LifecycleExtensionTests { - private static final String EXTENSION_VERSION = "2.0.3"; + private static final String EXTENSION_VERSION = "2.0.4"; @Mock ExtensionApi extensionApi; diff --git a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleTestHelper.java b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleTestHelper.java index feb72cfaf..e21c232f9 100644 --- a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleTestHelper.java +++ b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleTestHelper.java @@ -46,6 +46,7 @@ public int getDensityDpi() { when(deviceInfoService.getOperatingSystemVersion()).thenReturn("5.55"); when(deviceInfoService.getMobileCarrierName()).thenReturn("TEST_CARRIER"); when(deviceInfoService.getActiveLocale()).thenReturn(new Locale("en", "US")); + when(deviceInfoService.getSystemLocale()).thenReturn(new Locale("fr", "FR")); when(deviceInfoService.getRunMode()).thenReturn("APPLICATION"); } } diff --git a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilderTest.java b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilderTest.java index 4c6b212a2..6263da6d3 100644 --- a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilderTest.java +++ b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilderTest.java @@ -48,7 +48,7 @@ public static void beforeAll() { "_dc", new HashMap() { { - put("language", "en-US"); + put("language", "fr-FR"); } }); } @@ -92,6 +92,13 @@ public void testBuildAppLaunchXDMData_returnsCorrectData_whenIsInstall() { put("version", "1.1 (12345)"); put("isInstall", true); put("isLaunch", true); + put( + "_dc", + new HashMap() { + { + put("language", "en-US"); + } + }); } }); expectedData.put("eventType", "application.launch"); @@ -117,6 +124,13 @@ public void testBuildAppLaunchXDMData_returnsCorrectData_whenIsUpgradeEvent() { put("version", "1.1 (12345)"); put("isUpgrade", true); put("isLaunch", true); + put( + "_dc", + new HashMap() { + { + put("language", "en-US"); + } + }); } }); expectedData.put("eventType", "application.launch"); @@ -164,6 +178,13 @@ public void testBuildAppLaunchXDMData_returnsCorrectData_whenIsLaunch() { put("id", "test.package.name"); put("version", "1.1 (12345)"); put("isLaunch", true); + put( + "_dc", + new HashMap() { + { + put("language", "en-US"); + } + }); } }); expectedData.put("eventType", "application.launch"); diff --git a/code/testapp/src/main/java/com/adobe/testapp/PlatformServicesFragment.java b/code/testapp/src/main/java/com/adobe/testapp/PlatformServicesFragment.java index 0ddf3728e..5a011b76b 100644 --- a/code/testapp/src/main/java/com/adobe/testapp/PlatformServicesFragment.java +++ b/code/testapp/src/main/java/com/adobe/testapp/PlatformServicesFragment.java @@ -57,6 +57,7 @@ public void onClick(View view) { stringBuffer.append("\ngetApplicationBaseDir() - " + deviceInforming.getApplicationBaseDir()); stringBuffer.append("\ngetApplicationCacheDir() - " + deviceInforming.getApplicationCacheDir()); stringBuffer.append("\ngetActiveLocale() - " + deviceInforming.getActiveLocale()); + stringBuffer.append("\ngetSystemLocale() - " + deviceInforming.getSystemLocale()); stringBuffer.append("\ngetCanonicalPlatformName() - " + deviceInforming.getCanonicalPlatformName()); stringBuffer.append("\ngetDefaultUserAgent() - " + deviceInforming.getDefaultUserAgent()); stringBuffer.append("\ngetDeviceBuildId() - " + deviceInforming.getDeviceBuildId());