From 953f9dbf7d39c2d731c9a2f540b5ad0e84f08d93 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 21 Oct 2024 14:11:21 -0700 Subject: [PATCH 1/5] Validate and fallback to known manifest We are seeing occasional instances where the app AndroidManifest content being sent by the Assurance SDK as part of client event is not the actual manifest of the app- and it is for the WebView library. Currently this is being seen in Android 15 physical device and automation tests and we have not be able to root cause this issue but it seems to be some case of bad android manifest read from the system itself. Given that the package name can be programmatically retrieved (as opposed to the current way of reading the entire manifest), this change adds a device side detection of this wrong package name scenario and then force correct it on device before sending the client event as a best effort these cases. This PR also changes the tests to Kotlin from Java for AssuranceClientInfo --- code/assurance/build.gradle | 1 + .../mobile/assurance/AssuranceClientInfo.java | 91 ++- .../assurance/AssuranceClientInfoTest.java | 247 ------- .../assurance/AssuranceClientInfoTest.kt | 631 ++++++++++++++++++ 4 files changed, 722 insertions(+), 248 deletions(-) delete mode 100644 code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.java create mode 100644 code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.kt diff --git a/code/assurance/build.gradle b/code/assurance/build.gradle index 9a8e019..8fbd2b2 100644 --- a/code/assurance/build.gradle +++ b/code/assurance/build.gradle @@ -254,6 +254,7 @@ dependencies { testImplementation 'com.google.code.gson:gson:2.8.5' testImplementation 'org.mockito:mockito-core:4.5.1' testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0' + testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation 'org.mockito:mockito-inline:4.5.1' testImplementation 'net.sf.kxml:kxml2:2.3.0@jar' testImplementation 'org.json:json:20171018' diff --git a/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfo.java b/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfo.java index f306886..225c9a4 100644 --- a/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfo.java +++ b/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfo.java @@ -17,6 +17,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.location.LocationManager; @@ -25,12 +27,15 @@ import android.os.PowerManager; import android.provider.Settings; import android.telephony.TelephonyManager; +import android.util.Log; +import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import com.adobe.marketing.mobile.Assurance; import com.adobe.marketing.mobile.services.ServiceProvider; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import org.json.JSONException; import org.json.JSONObject; final class AssuranceClientInfo { @@ -38,6 +43,12 @@ final class AssuranceClientInfo { private static final String VALUE_UNKNOWN = "Unknown"; private static final String MANIFEST_FILE_NAME = "AndroidManifest.xml"; private static final String EVENT_TYPE_CONNECT = "connect"; + private static final String KEY_MANIFEST = "manifest"; + private static final String KEY_PACKAGE = "package"; + private static final String FALLBACK_KEY_VERSION_NAME = "versionName"; + private static final String FALLBACK_KEY_VERSION_CODE = "versionCode"; + private static final String FALLBACK_KEY_APPLICATION = "application"; + private static final String FALLBACK_KEY_APPLICATION_NAME = "name"; private final JSONObject manifestData; @@ -45,7 +56,10 @@ final class AssuranceClientInfo { // parse the manifest file and store it in a JSONObject for later use as this does not // change // during the lifetime of the application - manifestData = AssuranceIOUtils.parseXMLResourceFileToJson(MANIFEST_FILE_NAME); + final JSONObject parsedManifest = + AssuranceIOUtils.parseXMLResourceFileToJson(MANIFEST_FILE_NAME); + final boolean isValid = validateManifestData(parsedManifest); + manifestData = isValid ? parsedManifest : getFallbackManifestData(); } /** @@ -363,4 +377,79 @@ private boolean isPowerSaveModeEnabled() { return powerManager.isPowerSaveMode(); } + + /** + * Checks that the manifest data provided is associated with the current application. + * + * @param manifestData the manifest data JSONObject to validate + * @return true if the manifest data is valid, false otherwise + */ + @VisibleForTesting + boolean validateManifestData(final JSONObject manifestData) { + if (manifestData == null) { + return false; + } + final JSONObject manifest = manifestData.optJSONObject(KEY_MANIFEST); + if (manifest == null) { + return false; + } + + final String parsedPackageName = manifest.optString(KEY_PACKAGE, ""); + final Application app = + ServiceProvider.getInstance().getAppContextService().getApplication(); + return (app != null && parsedPackageName.equals(app.getPackageName())); + } + + /** + * Returns a curated JSONObject with fallback manifest data. This is typically is to be used + * when the manifest data parsed is invalid (i.e not associated with the application) + * + *

The fallback manifest data has the following structure: { "manifest": { "package": + * "com.mygroup.myapp", "versionName": "1.0", "versionCode": "1", "application": { "name": + * "MyAppName" } } } + * + * @return a JSONObject with fallback manifest data + */ + @VisibleForTesting + JSONObject getFallbackManifestData() { + final Application app = + ServiceProvider.getInstance().getAppContextService().getApplication(); + final JSONObject result = new JSONObject(); + final JSONObject manifestJson = new JSONObject(); + if (app == null) { + return manifestJson; + } + + try { + final String appPackageName = app.getPackageName(); + manifestJson.put(KEY_PACKAGE, appPackageName); + + final PackageManager pm = app.getApplicationContext().getPackageManager(); + try { + final PackageInfo packageInfo = + pm.getPackageInfo(appPackageName, PackageManager.GET_PERMISSIONS); + final String versionName = packageInfo.versionName; + final String versionCode = String.valueOf(packageInfo.versionCode); + manifestJson.put(FALLBACK_KEY_VERSION_NAME, versionName); + manifestJson.put(FALLBACK_KEY_VERSION_CODE, versionCode); + + } catch (PackageManager.NameNotFoundException e) { + Log.d(Assurance.LOG_TAG, "Failed to get package info for " + appPackageName); + } + + final ApplicationInfo appInfo = app.getApplicationInfo(); + if (appInfo != null) { + final String applicationName = appInfo.name; + final JSONObject applicationJson = new JSONObject(); + applicationJson.put(FALLBACK_KEY_APPLICATION_NAME, applicationName); + manifestJson.put(FALLBACK_KEY_APPLICATION, applicationJson); + } + + result.put(KEY_MANIFEST, manifestJson); + } catch (JSONException e) { + Log.d(Assurance.LOG_TAG, "Failed to put version details into fallbackManifestData"); + } + + return result; + } } diff --git a/code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.java b/code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.java deleted file mode 100644 index d84e8a1..0000000 --- a/code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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.assurance; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import android.Manifest; -import android.app.Application; -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.LocationManager; -import android.os.BatteryManager; -import android.os.PowerManager; -import android.telephony.TelephonyManager; -import androidx.core.app.ActivityCompat; -import com.adobe.marketing.mobile.Assurance; -import com.adobe.marketing.mobile.services.AppContextService; -import com.adobe.marketing.mobile.services.ServiceProvider; -import java.util.Map; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -@Config(sdk = 28) -public class AssuranceClientInfoTest { - @Mock private AppContextService mockAppContextService; - - @Mock private BatteryManager mockBatteryManager; - - @Mock private LocationManager mockLocationManager; - - @Mock private PowerManager mockPowerManager; - - @Mock private TelephonyManager mockTelephonyManager; - - @Mock private ServiceProvider mockServiceProvider; - - @Mock private Context mockAppContext; - - private AssuranceClientInfo assuranceClientInfo; - - private MockedStatic mockedStaticAssuranceIOUtils; - private MockedStatic mockedStaticServiceProvider; - private MockedStatic mockedStaticActivityCompat; - - private JSONObject appSettings; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.openMocks(this); - - mockedStaticActivityCompat = Mockito.mockStatic(ActivityCompat.class); - mockedStaticAssuranceIOUtils = Mockito.mockStatic(AssuranceIOUtils.class); - mockedStaticServiceProvider = Mockito.mockStatic(ServiceProvider.class); - - appSettings = mockManifestData(); - mockedStaticAssuranceIOUtils - .when(() -> AssuranceIOUtils.parseXMLResourceFileToJson(any())) - .thenReturn(appSettings); - - assuranceClientInfo = new AssuranceClientInfo(); - } - - @Test - public void testGetData_happy() throws JSONException { - mockAppContextService(); - mockTelephonyManager("MyNetworkCarrier"); - mockBatteryLevel(95); - mockLocationManager(true, true); - mockPowerManager(false); - - final Map data = assuranceClientInfo.getData(); - assertEquals( - Assurance.EXTENSION_VERSION, data.get(AssuranceConstants.ClientInfoKeys.VERSION)); - assertEquals("connect", data.get(AssuranceConstants.PayloadDataKeys.TYPE)); - assertEquals(appSettings, data.get(AssuranceConstants.ClientInfoKeys.APP_SETTINGS)); - - final Map obtainedDeviceInfo = - (Map) data.get(AssuranceConstants.ClientInfoKeys.DEVICE_INFO); - assertNotNull(obtainedDeviceInfo); - assertEquals( - "MyNetworkCarrier", - obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.CARRIER_NAME)); - assertEquals(95, obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.BATTERY_LEVEL)); - assertEquals( - "Always", - obtainedDeviceInfo.get( - AssuranceConstants.DeviceInfoKeys.LOCATION_AUTHORIZATION_STATUS)); - assertEquals( - true, - obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.LOCATION_SERVICE_ENABLED)); - assertEquals( - false, - obtainedDeviceInfo.get( - AssuranceConstants.DeviceInfoKeys.LOW_POWER_BATTERY_ENABLED)); - } - - @Test - public void testGetData_locationAuthorizationDenied() throws JSONException { - mockAppContextService(); - mockTelephonyManager("MyNetworkCarrier"); - mockBatteryLevel(95); - mockLocationManager(false, false); - mockPowerManager(false); - - final Map data = assuranceClientInfo.getData(); - assertEquals( - Assurance.EXTENSION_VERSION, data.get(AssuranceConstants.ClientInfoKeys.VERSION)); - assertEquals("connect", data.get(AssuranceConstants.PayloadDataKeys.TYPE)); - assertEquals(appSettings, data.get(AssuranceConstants.ClientInfoKeys.APP_SETTINGS)); - - final Map obtainedDeviceInfo = - (Map) data.get(AssuranceConstants.ClientInfoKeys.DEVICE_INFO); - assertNotNull(obtainedDeviceInfo); - assertEquals( - "MyNetworkCarrier", - obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.CARRIER_NAME)); - assertEquals(95, obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.BATTERY_LEVEL)); - assertEquals( - "Denied", - obtainedDeviceInfo.get( - AssuranceConstants.DeviceInfoKeys.LOCATION_AUTHORIZATION_STATUS)); - assertEquals( - false, - obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.LOCATION_SERVICE_ENABLED)); - assertEquals( - false, - obtainedDeviceInfo.get( - AssuranceConstants.DeviceInfoKeys.LOW_POWER_BATTERY_ENABLED)); - } - - @Test - public void testGetData_lowPowerModeEnabled() throws JSONException { - mockAppContextService(); - mockTelephonyManager("MyNetworkCarrier"); - mockBatteryLevel(95); - mockLocationManager(false, false); - mockPowerManager(true); - - final Map data = assuranceClientInfo.getData(); - assertEquals( - Assurance.EXTENSION_VERSION, data.get(AssuranceConstants.ClientInfoKeys.VERSION)); - assertEquals("connect", data.get(AssuranceConstants.PayloadDataKeys.TYPE)); - assertEquals(appSettings, data.get(AssuranceConstants.ClientInfoKeys.APP_SETTINGS)); - - final Map obtainedDeviceInfo = - (Map) data.get(AssuranceConstants.ClientInfoKeys.DEVICE_INFO); - assertNotNull(obtainedDeviceInfo); - assertEquals( - "MyNetworkCarrier", - obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.CARRIER_NAME)); - assertEquals(95, obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.BATTERY_LEVEL)); - assertEquals( - "Denied", - obtainedDeviceInfo.get( - AssuranceConstants.DeviceInfoKeys.LOCATION_AUTHORIZATION_STATUS)); - assertEquals( - false, - obtainedDeviceInfo.get(AssuranceConstants.DeviceInfoKeys.LOCATION_SERVICE_ENABLED)); - assertEquals( - true, - obtainedDeviceInfo.get( - AssuranceConstants.DeviceInfoKeys.LOW_POWER_BATTERY_ENABLED)); - } - - @After - public void teardown() { - mockedStaticServiceProvider.close(); - mockedStaticActivityCompat.close(); - mockedStaticAssuranceIOUtils.close(); - } - - private JSONObject mockManifestData() throws JSONException { - final JSONObject appSettings = new JSONObject(); - final JSONObject manifest = new JSONObject(); - manifest.put("package", "com.assurance.testapp"); - appSettings.put("manifest", manifest); - appSettings.put("versionCode", "1"); - appSettings.put("versionName", "1.0"); - return appSettings; - } - - private void mockAppContextService() { - mockedStaticServiceProvider - .when(ServiceProvider::getInstance) - .thenReturn(mockServiceProvider); - when(mockServiceProvider.getAppContextService()).thenReturn(mockAppContextService); - when(mockAppContextService.getApplicationContext()).thenReturn(mockAppContext); - } - - private void mockTelephonyManager(final String carrierName) { - when(mockAppContext.getSystemService(Application.TELEPHONY_SERVICE)) - .thenReturn(mockTelephonyManager); - when(mockTelephonyManager.getNetworkOperatorName()).thenReturn(carrierName); - } - - private void mockBatteryLevel(int level) { - when(mockAppContext.getSystemService(Context.BATTERY_SERVICE)) - .thenReturn(mockBatteryManager); - when(mockBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)) - .thenReturn(level); - } - - private void mockLocationManager( - final boolean locationEnabled, final boolean accessFineLocation) { - when(mockAppContext.getSystemService(Context.LOCATION_SERVICE)) - .thenReturn(mockLocationManager); - when(mockLocationManager.isLocationEnabled()).thenReturn(locationEnabled); - - mockedStaticActivityCompat - .when( - () -> - ActivityCompat.checkSelfPermission( - mockAppContext, Manifest.permission.ACCESS_FINE_LOCATION)) - .thenReturn( - accessFineLocation - ? PackageManager.PERMISSION_GRANTED - : PackageManager.PERMISSION_DENIED); - } - - private void mockPowerManager(boolean isPowerSaveModeEnabled) { - when(mockAppContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mockPowerManager); - when(mockPowerManager.isPowerSaveMode()).thenReturn(isPowerSaveModeEnabled); - } -} diff --git a/code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.kt b/code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.kt new file mode 100644 index 0000000..acb8632 --- /dev/null +++ b/code/assurance/src/test/java/com/adobe/marketing/mobile/assurance/AssuranceClientInfoTest.kt @@ -0,0 +1,631 @@ +/* + Copyright 2024 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.assurance + +import android.Manifest +import android.app.Application +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.location.LocationManager +import android.os.BatteryManager +import android.os.PowerManager +import android.telephony.TelephonyManager +import androidx.core.app.ActivityCompat +import com.adobe.marketing.mobile.Assurance +import com.adobe.marketing.mobile.services.AppContextService +import com.adobe.marketing.mobile.services.ServiceProvider +import com.adobe.marketing.mobile.util.JSONUtils +import org.json.JSONObject +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.MockedStatic +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28]) +class AssuranceClientInfoTest { + @Mock + private lateinit var mockAppContextService: AppContextService + + @Mock + private lateinit var mockBatteryManager: BatteryManager + + @Mock + private lateinit var mockLocationManager: LocationManager + + @Mock + private lateinit var mockPowerManager: PowerManager + + @Mock + private lateinit var mockTelephonyManager: TelephonyManager + + @Mock + private lateinit var mockServiceProvider: ServiceProvider + + @Mock + private lateinit var mockAppContext: Context + + @Mock + private lateinit var mockApp: Application + + private lateinit var mockedStaticAssuranceIOUtils: MockedStatic + private lateinit var mockedStaticServiceProvider: MockedStatic + private lateinit var mockedStaticActivityCompat: MockedStatic + + companion object { + private const val TEST_APP_PACKAGE_NAME = "com.assurance.testapp" + private const val TEST_APP_NAME = "TestApp" + private const val TEST_BATTERY_LEVEL = 95 + private const val TEST_NETWORK_CARRIER = "MyNetworkCarrier" + private const val TEST_LOCATION_AUTHORIZATION_STATUS_ALWAYS = "Always" + private const val TEST_LOCATION_AUTHORIZATION_STATUS_DENIED = "Denied" + private const val SOME_INCORRECT_PACKAGE_NAME = "com.corrupt.testapp" + } + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + mockedStaticActivityCompat = Mockito.mockStatic(ActivityCompat::class.java) + mockedStaticAssuranceIOUtils = Mockito.mockStatic(AssuranceIOUtils::class.java) + mockedStaticServiceProvider = Mockito.mockStatic( + ServiceProvider::class.java + ) + } + + @Test + fun `Test getData with all valid values`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockManifestData(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + mockTelephonyManager(TEST_NETWORK_CARRIER) + mockBatteryLevel(TEST_BATTERY_LEVEL) + mockLocationManager(true, true) + mockPowerManager(false) + + val assuranceClientInfo = AssuranceClientInfo() + val data = assuranceClientInfo.data + + // Check "version" key + Assert.assertEquals( + Assurance.EXTENSION_VERSION, + data[AssuranceConstants.ClientInfoKeys.VERSION] + ) + + // Check "type" key + Assert.assertEquals("connect", data[AssuranceConstants.PayloadDataKeys.TYPE]) + + // Check "appSettings" object + val expectedAppSettings = createManifestJson(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + Assert.assertEquals( + JSONUtils.toMap(expectedAppSettings), + JSONUtils.toMap(data[AssuranceConstants.ClientInfoKeys.APP_SETTINGS] as JSONObject) + ) + + // Check "deviceInfo" object + val obtainedDeviceInfo = + data[AssuranceConstants.ClientInfoKeys.DEVICE_INFO] as Map? + Assert.assertNotNull(obtainedDeviceInfo) + Assert.assertEquals( + TEST_NETWORK_CARRIER, + obtainedDeviceInfo!![AssuranceConstants.DeviceInfoKeys.CARRIER_NAME] + ) + Assert.assertEquals(95, obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.BATTERY_LEVEL]) + Assert.assertEquals( + TEST_LOCATION_AUTHORIZATION_STATUS_ALWAYS, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_AUTHORIZATION_STATUS] + ) + Assert.assertEquals( + true, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_SERVICE_ENABLED] + ) + Assert.assertEquals( + false, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOW_POWER_BATTERY_ENABLED] + ) + } + + @Test + fun `Test #getData when the parsed manifest is not the desired app manifest`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockManifestData(SOME_INCORRECT_PACKAGE_NAME, TEST_APP_NAME) + mockTelephonyManager(TEST_NETWORK_CARRIER) + mockBatteryLevel(TEST_BATTERY_LEVEL) + mockLocationManager(true, true) + mockPowerManager(false) + mockAppDetails( + TEST_APP_PACKAGE_NAME, + TEST_APP_NAME, + 1, + "1.0" + ) + + val assuranceClientInfo = AssuranceClientInfo() + val data = assuranceClientInfo.data + + // Check version key + Assert.assertEquals( + Assurance.EXTENSION_VERSION, + data[AssuranceConstants.ClientInfoKeys.VERSION] + ) + + // Check type key + Assert.assertEquals("connect", data[AssuranceConstants.PayloadDataKeys.TYPE]) + + // Check app settings object + val expectedAppSettings = createManifestJson(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + assertEquals( + JSONUtils.toMap(expectedAppSettings), + JSONUtils.toMap(data[AssuranceConstants.ClientInfoKeys.APP_SETTINGS] as JSONObject) + ) + + // Check device info object + val obtainedDeviceInfo = + data[AssuranceConstants.ClientInfoKeys.DEVICE_INFO] as Map? + Assert.assertNotNull(obtainedDeviceInfo) + Assert.assertEquals( + TEST_NETWORK_CARRIER, + obtainedDeviceInfo!![AssuranceConstants.DeviceInfoKeys.CARRIER_NAME] + ) + Assert.assertEquals( + TEST_BATTERY_LEVEL, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.BATTERY_LEVEL] + ) + Assert.assertEquals( + TEST_LOCATION_AUTHORIZATION_STATUS_ALWAYS, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_AUTHORIZATION_STATUS] + ) + Assert.assertEquals( + true, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_SERVICE_ENABLED] + ) + Assert.assertEquals( + false, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOW_POWER_BATTERY_ENABLED] + ) + } + + @Test + fun `Test #getData when Location permissions are denied`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockManifestData(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + mockTelephonyManager(TEST_NETWORK_CARRIER) + mockBatteryLevel(TEST_BATTERY_LEVEL) + mockLocationManager(false, false) + mockPowerManager(false) + + val assuranceClientInfo = AssuranceClientInfo() + val data = assuranceClientInfo.data + + // Check "version" key + Assert.assertEquals( + Assurance.EXTENSION_VERSION, + data[AssuranceConstants.ClientInfoKeys.VERSION] + ) + + // Check "type" key + Assert.assertEquals("connect", data[AssuranceConstants.PayloadDataKeys.TYPE]) + + // Check "appSettings" object + val expectedAppSettings = createManifestJson(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + Assert.assertEquals( + JSONUtils.toMap(expectedAppSettings), + JSONUtils.toMap(data[AssuranceConstants.ClientInfoKeys.APP_SETTINGS] as JSONObject) + ) + + // Check "deviceInfo" object + val obtainedDeviceInfo = + data[AssuranceConstants.ClientInfoKeys.DEVICE_INFO] as Map? + Assert.assertNotNull(obtainedDeviceInfo) + Assert.assertEquals( + TEST_NETWORK_CARRIER, + obtainedDeviceInfo!![AssuranceConstants.DeviceInfoKeys.CARRIER_NAME] + ) + Assert.assertEquals( + TEST_BATTERY_LEVEL, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.BATTERY_LEVEL] + ) + Assert.assertEquals( + TEST_LOCATION_AUTHORIZATION_STATUS_DENIED, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_AUTHORIZATION_STATUS] + ) + Assert.assertEquals( + false, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_SERVICE_ENABLED] + ) + Assert.assertEquals( + false, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOW_POWER_BATTERY_ENABLED] + ) + } + + @Test + fun `Test #getData when Low Power mode is enabled`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockManifestData(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + mockTelephonyManager(TEST_NETWORK_CARRIER) + mockBatteryLevel(TEST_BATTERY_LEVEL) + mockLocationManager(false, false) + mockPowerManager(true) + + val assuranceClientInfo = AssuranceClientInfo() + val data = assuranceClientInfo.data + + // Check "version" key + Assert.assertEquals( + Assurance.EXTENSION_VERSION, + data[AssuranceConstants.ClientInfoKeys.VERSION] + ) + + // Check "type" key + Assert.assertEquals("connect", data[AssuranceConstants.PayloadDataKeys.TYPE]) + + // Check "appSettings" object + val expectedAppSettings = createManifestJson(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + Assert.assertEquals( + JSONUtils.toMap(expectedAppSettings), + JSONUtils.toMap(data[AssuranceConstants.ClientInfoKeys.APP_SETTINGS] as JSONObject) + ) + + // Check "deviceInfo" object + val obtainedDeviceInfo = + data[AssuranceConstants.ClientInfoKeys.DEVICE_INFO] as Map? + Assert.assertNotNull(obtainedDeviceInfo) + Assert.assertEquals( + TEST_NETWORK_CARRIER, + obtainedDeviceInfo!![AssuranceConstants.DeviceInfoKeys.CARRIER_NAME] + ) + Assert.assertEquals( + TEST_BATTERY_LEVEL, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.BATTERY_LEVEL] + ) + Assert.assertEquals( + TEST_LOCATION_AUTHORIZATION_STATUS_DENIED, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_AUTHORIZATION_STATUS] + ) + Assert.assertEquals( + false, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOCATION_SERVICE_ENABLED] + ) + Assert.assertEquals( + true, + obtainedDeviceInfo[AssuranceConstants.DeviceInfoKeys.LOW_POWER_BATTERY_ENABLED] + ) + } + + @Test + fun `Test #getFallbackManifestData when app is null`() { + mockedStaticServiceProvider + .`when` { ServiceProvider.getInstance() } + .thenReturn(mockServiceProvider) + `when`(mockServiceProvider.appContextService).thenReturn(mockAppContextService) + `when`(mockAppContextService.applicationContext).thenReturn(mockAppContext) + `when`(mockAppContextService.application).thenReturn(null) + + val assuranceClientInfo = AssuranceClientInfo() + val fallbackManifestData = assuranceClientInfo.getFallbackManifestData() + assertEquals(0, fallbackManifestData.length()) + } + + @Test + fun `Test #getFallbackManifestData when package info throws exception`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockAppDetails( + TEST_APP_PACKAGE_NAME, + TEST_APP_NAME, + 1, + "1.0" + ) + + // Simulate valid app info + val mockAppInfo = ApplicationInfo() + mockAppInfo.name = TEST_APP_NAME + `when`(mockApp.getApplicationInfo()).thenReturn(mockAppInfo) + + // Simulate package manager throwing exception + val mockPackageManager = Mockito.mock(PackageManager::class.java) + `when`(mockAppContext.packageManager).thenReturn(mockPackageManager) + `when`( + mockPackageManager.getPackageInfo( + ArgumentMatchers.eq(TEST_APP_PACKAGE_NAME), + ArgumentMatchers.anyInt() + ) + ).thenThrow(PackageManager.NameNotFoundException()) + + val assuranceClientInfo = AssuranceClientInfo() + val fallbackManifestData = assuranceClientInfo.getFallbackManifestData() + + val expectedFallbackManifestData = JSONObject( + """ + { + "manifest": { + "package": "$TEST_APP_PACKAGE_NAME", + "application": { + "name": "$TEST_APP_NAME" + } + } + } + """.trimIndent() + ) + + assertEquals(JSONUtils.toMap(expectedFallbackManifestData), JSONUtils.toMap(fallbackManifestData)) + } + + @Test + fun `Test #getFallbackManifestData when app info is null`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + + // Simulate valid package info + val mockPackageManager = Mockito.mock(PackageManager::class.java) + `when`(mockAppContext.packageManager).thenReturn(mockPackageManager) + val mockPackageInfo = Mockito.mock( + PackageInfo::class.java + ) + mockPackageInfo.versionCode = 1 + mockPackageInfo.versionName = "1.0" + `when`( + mockPackageManager.getPackageInfo( + ArgumentMatchers.eq(TEST_APP_PACKAGE_NAME), + ArgumentMatchers.anyInt() + ) + ).thenReturn(mockPackageInfo) + + // Simulate null app info + `when`(mockApp.applicationInfo).thenReturn(null) + + val assuranceClientInfo = AssuranceClientInfo() + val fallbackManifestData = assuranceClientInfo.getFallbackManifestData() + + val expectedFallbackManifestData = JSONObject( + """ + { + "manifest": { + "package": "$TEST_APP_PACKAGE_NAME", + "versionCode": "1", + "versionName": "1.0" + } + } + """.trimIndent() + ) + + assertEquals(JSONUtils.toMap(expectedFallbackManifestData), JSONUtils.toMap(fallbackManifestData)) + } + + @Test + fun `Test #getFallbackManifestData when all data is available`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockAppDetails( + TEST_APP_PACKAGE_NAME, + TEST_APP_NAME, + 1, + "1.0" + ) + val assuranceClientInfo = AssuranceClientInfo() + val fallbackManifestData = assuranceClientInfo.getFallbackManifestData() + val expectedFallbackManifestData = createManifestJson(TEST_APP_PACKAGE_NAME, TEST_APP_NAME) + assertEquals(JSONUtils.toMap(expectedFallbackManifestData), JSONUtils.toMap(fallbackManifestData)) + } + + @Test + fun `Test #validateManifestData when manifest data is null`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockAppDetails(TEST_APP_PACKAGE_NAME, TEST_APP_NAME, 1, "1.0") + + val assuranceClientInfo = AssuranceClientInfo() + val result = assuranceClientInfo.validateManifestData(null) + + assertEquals(false, result) + } + + @Test + fun `Test #validateManifestData when manifest data is empty`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockAppDetails(TEST_APP_PACKAGE_NAME, TEST_APP_NAME, 1, "1.0") + + val assuranceClientInfo = AssuranceClientInfo() + val result = assuranceClientInfo.validateManifestData(JSONObject()) + + assertEquals(false, result) + } + + @Test + fun `Test #validateManifestData when manifest data is missing package name`() { + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockAppDetails(TEST_APP_PACKAGE_NAME, TEST_APP_NAME, 1, "1.0") + + val manifestData = JSONObject( + """ + { + "manifest": { + "application": { + "name": "$TEST_APP_NAME" + } + } + } + """.trimIndent() + ) + val assuranceClientInfo = AssuranceClientInfo() + val result = assuranceClientInfo.validateManifestData(manifestData) + assertEquals(false, result) + } + + @Test + fun `Test #validateManifestData when packake when manifest package does not match app package`() { + val manifestData = JSONObject( + """ + { + "manifest": { + "package": "$SOME_INCORRECT_PACKAGE_NAME", + "application": { + "name": "TestApp" + } + } + } + """.trimIndent() + ) + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockAppDetails(TEST_APP_PACKAGE_NAME, TEST_APP_NAME, 1, "1.0") + + val assuranceClientInfo = AssuranceClientInfo() + val result = assuranceClientInfo.validateManifestData(manifestData) + assertEquals(false, result) + } + + @Test + fun `Test #validateManifestData when manifest data is valid`() { + val manifestData = JSONObject( + """ + { + "manifest": { + "package": "$TEST_APP_PACKAGE_NAME", + "application": { + "name": "$TEST_APP_NAME" + } + } + } + """.trimIndent() + ) + mockAppContextService(TEST_APP_PACKAGE_NAME) + mockAppDetails(TEST_APP_PACKAGE_NAME, TEST_APP_NAME, 1, "1.0") + + val assuranceClientInfo = AssuranceClientInfo() + val result = assuranceClientInfo.validateManifestData(manifestData) + assertEquals(true, result) + } + + @After + fun teardown() { + mockedStaticServiceProvider.close() + mockedStaticActivityCompat.close() + mockedStaticAssuranceIOUtils.close() + } + + private fun mockManifestData(packageName: String, appName: String): JSONObject { + val appSettings = createManifestJson(packageName, appName) + mockedStaticAssuranceIOUtils.`when` { AssuranceIOUtils.parseXMLResourceFileToJson(any()) } + ?.thenReturn(appSettings) + return appSettings + } + + /** + * Creates a JSON object representing the manifest file. + * @param packageName the package name of the app + * @param appName the name of the app + */ + private fun createManifestJson(packageName: String, appName: String) = JSONObject( + """ + { + "manifest": { + "package": "$packageName", + "versionCode": "1", + "versionName": "1.0", + "application": { + "name": "$appName" + } + } + } + """.trimIndent() + ) + + private fun mockAppContextService(appPackage: String) { + mockedStaticServiceProvider + .`when` { ServiceProvider.getInstance() } + .thenReturn(mockServiceProvider) + `when`(mockServiceProvider.appContextService).thenReturn(mockAppContextService) + `when`(mockAppContextService.applicationContext).thenReturn(mockAppContext) + + `when`(mockAppContextService.application).thenReturn(mockApp) + `when`(mockApp.applicationContext).thenReturn(mockAppContext) + `when`(mockApp.packageName).thenReturn(appPackage) + } + + private fun mockAppDetails( + packageName: String, + appName: String, + versionCode: Int, + versionName: String + ) { + val mockPackageManager = Mockito.mock(PackageManager::class.java) + `when`(mockAppContext.packageManager).thenReturn(mockPackageManager) + val mockPackageInfo = Mockito.mock( + PackageInfo::class.java + ) + mockPackageInfo.versionCode = versionCode + mockPackageInfo.versionName = versionName + `when`( + mockPackageManager.getPackageInfo( + ArgumentMatchers.eq(packageName), + ArgumentMatchers.anyInt() + ) + ).thenReturn(mockPackageInfo) + + val mockAppInfo = ApplicationInfo() + mockAppInfo.name = appName + `when`(mockApp.getApplicationInfo()).thenReturn(mockAppInfo) + } + + private fun mockTelephonyManager(carrierName: String) { + `when`(mockAppContext.getSystemService(Application.TELEPHONY_SERVICE)) + .thenReturn(mockTelephonyManager) + `when`(mockTelephonyManager.networkOperatorName).thenReturn(carrierName) + } + + private fun mockBatteryLevel(level: Int) { + `when`(mockAppContext.getSystemService(Context.BATTERY_SERVICE)) + .thenReturn(mockBatteryManager) + `when`(mockBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)) + .thenReturn(level) + } + + private fun mockLocationManager( + locationEnabled: Boolean, + accessFineLocation: Boolean + ) { + `when`(mockAppContext.getSystemService(Context.LOCATION_SERVICE)) + .thenReturn(mockLocationManager) + `when`(mockLocationManager.isLocationEnabled).thenReturn(locationEnabled) + + mockedStaticActivityCompat + .`when` { + ActivityCompat.checkSelfPermission( + mockAppContext, + Manifest.permission.ACCESS_FINE_LOCATION + ) + } + ?.thenReturn( + if (accessFineLocation + ) { + PackageManager.PERMISSION_GRANTED + } else { + PackageManager.PERMISSION_DENIED + } + ) + } + + private fun mockPowerManager(isPowerSaveModeEnabled: Boolean) { + `when`(mockAppContext.getSystemService(Context.POWER_SERVICE)) + .thenReturn(mockPowerManager) + `when`(mockPowerManager.isPowerSaveMode).thenReturn(isPowerSaveModeEnabled) + } +} From a9152e870e248c7bab7ca5deba2529bec6840e29 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 21 Oct 2024 14:27:25 -0700 Subject: [PATCH 2/5] Update CI scripts for 2.x --- .circleci/config.yml | 6 +++--- .github/workflows/maven-release.yml | 2 +- .github/workflows/maven-snapshot.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 147cce5..da69a70 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - android: circleci/android@2.0 + android: circleci/android@2.4.0 workflows: version: 2 @@ -15,7 +15,7 @@ jobs: executor: name: android/android-machine resource-class: large - tag: 2022.01.1 + tag: 2024.01.1 steps: - checkout @@ -46,7 +46,7 @@ jobs: executor: name: android/android-machine resource-class: large - tag: 2022.01.1 + tag: 2024.01.1 steps: - checkout diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index db449f3..eb7572c 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -2,7 +2,7 @@ name: Publish package to the Maven Central Repository on: push: branches: - - main + - main-v2.x jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/maven-snapshot.yml b/.github/workflows/maven-snapshot.yml index 453a1d9..cea48dd 100644 --- a/.github/workflows/maven-snapshot.yml +++ b/.github/workflows/maven-snapshot.yml @@ -2,7 +2,7 @@ name: Publish package to the Maven Central Staging Repository on: push: branches: - - staging + - staging-v2.x jobs: publish: runs-on: ubuntu-latest From 6a6d2f5692e88c557da676debedb6bfd744eafc2 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 21 Oct 2024 14:39:41 -0700 Subject: [PATCH 3/5] Use google java format 1.15.0 for compatibility --- .../com/adobe/marketing/mobile/assurance/AssuranceBlob.java | 4 ++-- code/codeformat.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceBlob.java b/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceBlob.java index 41561f6..e9b8a6a 100644 --- a/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceBlob.java +++ b/code/assurance/src/main/java/com/adobe/marketing/mobile/assurance/AssuranceBlob.java @@ -170,8 +170,8 @@ public void run() { if (value.isEmpty()) { uploadFailure( callback, - "Uploading Blob failed, Invalid BlobId" - + " returned from the fileStorage server"); + "Uploading Blob failed, Invalid BlobId returned" + + " from the fileStorage server"); return; } diff --git a/code/codeformat.gradle b/code/codeformat.gradle index e20dbf1..d38f102 100644 --- a/code/codeformat.gradle +++ b/code/codeformat.gradle @@ -2,7 +2,7 @@ spotless { java { toggleOffOn("format:off", "format:on") target "src/*/java/**/*.java" - googleJavaFormat('1.8').aosp().reflowLongStrings() + googleJavaFormat('1.15.0').aosp().reflowLongStrings() importOrder() removeUnusedImports() endWithNewline() From b1f2d0c8a4cf9712130392b4c72d0b964a26965a Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 21 Oct 2024 15:06:51 -0700 Subject: [PATCH 4/5] Fix robolectric errors --- code/assurance/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/assurance/build.gradle b/code/assurance/build.gradle index 8fbd2b2..4deb7b2 100644 --- a/code/assurance/build.gradle +++ b/code/assurance/build.gradle @@ -258,7 +258,7 @@ dependencies { testImplementation 'org.mockito:mockito-inline:4.5.1' testImplementation 'net.sf.kxml:kxml2:2.3.0@jar' testImplementation 'org.json:json:20171018' - testImplementation 'org.robolectric:robolectric:4.2' + testImplementation 'org.robolectric:robolectric:4.12.2' androidTestImplementation "androidx.test:core:1.4.0" androidTestImplementation 'androidx.test.ext:junit:1.1.1' From 51c1f8e1206f3e951da57e38dc0ceee77ea7e517 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 21 Oct 2024 15:44:46 -0700 Subject: [PATCH 5/5] Update version to 2.2.2 --- .../src/main/java/com/adobe/marketing/mobile/Assurance.java | 2 +- code/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/assurance/src/main/java/com/adobe/marketing/mobile/Assurance.java b/code/assurance/src/main/java/com/adobe/marketing/mobile/Assurance.java index 0290c2b..3d607b6 100644 --- a/code/assurance/src/main/java/com/adobe/marketing/mobile/Assurance.java +++ b/code/assurance/src/main/java/com/adobe/marketing/mobile/Assurance.java @@ -24,7 +24,7 @@ public class Assurance { public static final Class EXTENSION = AssuranceExtension.class; public static final String LOG_TAG = "Assurance"; - public static final String EXTENSION_VERSION = "2.2.1"; + public static final String EXTENSION_VERSION = "2.2.2"; public static final String EXTENSION_NAME = "com.adobe.assurance"; public static final String EXTENSION_FRIENDLY_NAME = "Assurance"; diff --git a/code/gradle.properties b/code/gradle.properties index dccd87e..7b23d72 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -21,7 +21,7 @@ org.gradle.configureondemand = false moduleProjectName=assurance moduleName=assurance moduleAARName=assurance-phone-release.aar -moduleVersion=2.2.1 +moduleVersion=2.2.2 mavenRepoName=AdobeMobileAssurance mavenRepoDescription=Android Assurance Extension for Adobe Mobile Marketing mavenUploadDryRunFlag=false