From bec9f800f8a16d540f2c883571dce913a5489513 Mon Sep 17 00:00:00 2001 From: runner Date: Mon, 21 Mar 2022 21:49:52 +0000 Subject: [PATCH] Release 4.1.0 --- app/build.gradle | 4 +- build.gradle | 2 +- unity-ads/build.gradle | 4 +- unity-ads/jacoco.gradle | 2 +- .../ads/test/InstrumentationTestSuite.java | 32 +- .../ads/operation/LoadModuleTests.java | 8 +- .../ShowModuleDecoratorTimeoutTests.java | 2 +- .../ads/operation/ShowModuleTests.java | 8 +- .../ConfigurationRequestFactoryTest.java | 93 ++++ .../core/configuration/ExperimentsTest.java | 59 +++ .../core/device/AsyncTokenStorageTest.java | 412 ++++++++++++++++++ .../DeviceInfoReaderCompressorTest.java | 50 +++ .../DeviceInfoReaderFilterProviderTest.java | 80 ++++ .../core/device/DeviceInfoReaderTest.java | 25 ++ .../DeviceInfoReaderUrlEncoderTest.java | 31 ++ .../DeviceInfoReaderWithFilterTest.java | 72 +++ .../device/DeviceInfoReaderWithPIITest.java | 86 ++++ .../DeviceInfoReaderWithStorageInfoTest.java | 73 ++++ .../core/device/NativeTokenGeneratorTest.java | 71 +++ .../core/device/pii/PiiDataSelectorTest.java | 133 ++++++ .../core/device/pii/PiiDecisionDataTest.java | 68 +++ .../pii/PiiTrackingStatusReaderTest.java | 117 +++++ .../services/core/misc/JsonFlattenerTest.java | 49 +++ .../core/webview/WebViewUrlBuilderTest.java | 64 +++ .../ads/test/integration/SDKMetricsTest.java | 4 +- .../ads/test/legacy/InitializeThreadTest.java | 20 - .../ads/test/legacy/SdkPropertiesTest.java | 75 ++++ unity-ads/src/main/AndroidManifest.xml | 1 + .../unity3d/ads/IUnityAdsTokenListener.java | 10 + .../main/java/com/unity3d/ads/UnityAds.java | 7 + .../com/unity3d/services/UnityServices.java | 2 + .../services/ads/UnityAdsImplementation.java | 11 + .../services/ads/adunit/AdUnitOpen.java | 2 +- .../configuration/AdsModuleConfiguration.java | 6 + .../services/ads/operation/AdModule.java | 2 +- .../services/ads/operation/IAdModule.java | 2 +- .../ads/operation/load/LoadModule.java | 11 +- .../operation/load/LoadModuleDecorator.java | 2 +- .../load/LoadModuleDecoratorTimeout.java | 4 +- .../ads/operation/show/ShowModule.java | 11 +- .../operation/show/ShowModuleDecorator.java | 2 +- .../show/ShowModuleDecoratorTimeout.java | 4 +- .../services/ads/token/AsyncTokenStorage.java | 212 +++++++++ .../ads/token/INativeTokenGenerator.java | 5 + .../token/INativeTokenGeneratorListener.java | 5 + .../ads/token/NativeTokenGenerator.java | 39 ++ .../services/ads/token/TokenStorage.java | 38 +- .../unity3d/services/core/api/DeviceInfo.java | 10 +- .../com/unity3d/services/core/api/Sdk.java | 4 +- .../services/core/cache/CacheDirectory.java | 2 +- .../core/configuration/Configuration.java | 88 +++- .../configuration/ConfigurationLoader.java | 68 +++ .../configuration/ConfigurationReader.java | 49 +++ .../ConfigurationRequestFactory.java | 74 ++++ .../CoreModuleConfiguration.java | 6 +- .../core/configuration/Experiments.java | 83 ++++ .../IConfigurationLoaderListener.java | 6 + .../IInitializeEventsMetricSender.java | 30 ++ .../InitializeEventsMetricSender.java | 150 +++++++ .../core/configuration/InitializeThread.java | 71 ++- .../unity3d/services/core/device/Device.java | 24 + .../services/core/device/TokenType.java | 6 + .../core/device/reader/DeviceInfoReader.java | 83 ++++ .../reader/DeviceInfoReaderBuilder.java | 48 ++ .../reader/DeviceInfoReaderCompressor.java | 54 +++ ...DeviceInfoReaderCompressorWithMetrics.java | 63 +++ .../DeviceInfoReaderFilterProvider.java | 44 ++ .../reader/DeviceInfoReaderUrlEncoder.java | 28 ++ .../reader/DeviceInfoReaderWithFilter.java | 25 ++ .../reader/DeviceInfoReaderWithLifecycle.java | 25 ++ .../reader/DeviceInfoReaderWithMetrics.java | 35 ++ .../reader/DeviceInfoReaderWithPII.java | 58 +++ .../DeviceInfoReaderWithStorageInfo.java | 60 +++ .../reader/IDeviceInfoDataCompressor.java | 7 + .../reader/IDeviceInfoDataContainer.java | 8 + .../core/device/reader/IDeviceInfoReader.java | 7 + .../device/reader/JsonStorageKeyNames.java | 13 + .../device/reader/pii/DataSelectorResult.java | 6 + .../device/reader/pii/PiiDataProvider.java | 10 + .../device/reader/pii/PiiDataSelector.java | 71 +++ .../device/reader/pii/PiiDecisionData.java | 46 ++ .../device/reader/pii/PiiPrivacyMode.java | 23 + .../reader/pii/PiiTrackingStatusReader.java | 72 +++ .../core/lifecycle/CachedLifecycle.java | 36 ++ .../core/lifecycle/LifecycleCache.java | 65 +++ .../core/misc/IJsonStorageReader.java | 8 + .../services/core/misc/JsonFlattener.java | 78 ++++ .../services/core/misc/JsonStorage.java | 4 +- .../core/misc/JsonStorageAggregator.java | 42 ++ .../unity3d/services/core/misc/Utilities.java | 19 + .../InitializationStatusReader.java | 24 + .../core/properties/SdkProperties.java | 36 +- .../core/request/ISDKMetricSender.java | 8 - .../services/core/request/ISDKMetrics.java | 8 - .../services/core/request/SDKMetrics.java | 119 ----- .../services/core/request/WebRequest.java | 26 +- .../metrics/IMetricSenderWithBatch.java | 6 + .../request/metrics/ISDKMetricSender.java | 8 + .../core/request/metrics/ISDKMetrics.java | 13 + .../services/core/request/metrics/Metric.java | 63 +++ .../request/metrics/MetricCommonTags.java | 37 ++ .../core/request/metrics/MetricSender.java | 109 +++++ .../metrics/MetricSenderWithBatch.java | 72 +++ .../request/metrics/MetricsContainer.java | 34 ++ .../{ => metrics}/SDKMetricEvents.java | 2 +- .../{ => metrics}/SDKMetricSender.java | 10 +- .../core/request/metrics/SDKMetrics.java | 92 ++++ .../core/request/metrics/TSIMetric.java | 128 ++++++ .../services/core/webview/WebViewApp.java | 41 +- .../core/webview/WebViewUrlBuilder.java | 39 ++ .../src/test/java/android/text/TextUtils.java | 8 + unity-ads/src/test/java/android/util/Log.java | 24 + .../InitializeEventsMetricSenderTest.java | 100 +++++ .../metrics/MetricSenderWithBatchTest.java | 124 ++++++ .../v1920/signals/SignalsReaderTest.java | 6 +- .../v1950/signals/SignalsReaderTest.java | 6 +- .../v2000/signals/SignalsReaderTest.java | 6 +- 117 files changed, 4601 insertions(+), 277 deletions(-) create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ConfigurationRequestFactoryTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ExperimentsTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/AsyncTokenStorageTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderCompressorTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderFilterProviderTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderUrlEncoderTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithFilterTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithPIITest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithStorageInfoTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/NativeTokenGeneratorTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDataSelectorTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDecisionDataTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiTrackingStatusReaderTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/misc/JsonFlattenerTest.java create mode 100644 unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/webview/WebViewUrlBuilderTest.java create mode 100644 unity-ads/src/main/java/com/unity3d/ads/IUnityAdsTokenListener.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/ads/token/AsyncTokenStorage.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGenerator.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGeneratorListener.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/ads/token/NativeTokenGenerator.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationLoader.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationReader.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationRequestFactory.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/configuration/Experiments.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/configuration/IConfigurationLoaderListener.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/configuration/IInitializeEventsMetricSender.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeEventsMetricSender.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/TokenType.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReader.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderBuilder.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressor.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressorWithMetrics.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderFilterProvider.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderUrlEncoder.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithFilter.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithLifecycle.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithMetrics.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithPII.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithStorageInfo.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataCompressor.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataContainer.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoReader.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/JsonStorageKeyNames.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/DataSelectorResult.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataProvider.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataSelector.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDecisionData.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiPrivacyMode.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiTrackingStatusReader.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/lifecycle/CachedLifecycle.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/lifecycle/LifecycleCache.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/misc/IJsonStorageReader.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/misc/JsonFlattener.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorageAggregator.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/properties/InitializationStatusReader.java delete mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetricSender.java delete mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetrics.java delete mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetrics.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/IMetricSenderWithBatch.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetricSender.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetrics.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/Metric.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricCommonTags.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSender.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatch.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricsContainer.java rename unity-ads/src/main/java/com/unity3d/services/core/request/{ => metrics}/SDKMetricEvents.java (80%) rename unity-ads/src/main/java/com/unity3d/services/core/request/{ => metrics}/SDKMetricSender.java (55%) create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetrics.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/request/metrics/TSIMetric.java create mode 100644 unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewUrlBuilder.java create mode 100644 unity-ads/src/test/java/android/text/TextUtils.java create mode 100644 unity-ads/src/test/java/android/util/Log.java create mode 100644 unity-ads/src/test/java/com/unity3d/services/core/configuration/InitializeEventsMetricSenderTest.java create mode 100644 unity-ads/src/test/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatchTest.java diff --git a/app/build.gradle b/app/build.gradle index 5c1a476f..0b121b62 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.unity3d.ads.example" minSdkVersion 19 targetSdkVersion 30 - versionCode = 4010 - versionName = "4.0.1" + versionCode = 4100 + versionName = "4.1.0" } buildTypes { diff --git a/build.gradle b/build.gradle index 767a89d1..b7704f18 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.2.1' - classpath 'org.jacoco:org.jacoco.core:0.8.1' + classpath 'org.jacoco:org.jacoco.core:0.8.5' classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.20.0' classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' } diff --git a/unity-ads/build.gradle b/unity-ads/build.gradle index dddc06f1..c75ddb3b 100644 --- a/unity-ads/build.gradle +++ b/unity-ads/build.gradle @@ -10,8 +10,8 @@ if (project.rootProject.file('local.properties').exists()) { ext { GROUP_ID = "com.unity3d.ads" ARTIFACT_ID = "unity-ads" - VERSION_ID = "4.0.1" - VERSION_CODE = 4010 + VERSION_ID = "4.1.0" + VERSION_CODE = 4100 SIGN_AAR = properties.getProperty("SIGN_AAR") ?: false } diff --git a/unity-ads/jacoco.gradle b/unity-ads/jacoco.gradle index 83126d87..af625a81 100644 --- a/unity-ads/jacoco.gradle +++ b/unity-ads/jacoco.gradle @@ -1,7 +1,7 @@ apply plugin: 'jacoco' jacoco { - toolVersion = '0.8.1' + toolVersion = '0.8.5' } tasks.withType(Test) { diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/InstrumentationTestSuite.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/InstrumentationTestSuite.java index c7254068..beec85d8 100644 --- a/unity-ads/src/androidTest/java/com/unity3d/ads/test/InstrumentationTestSuite.java +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/InstrumentationTestSuite.java @@ -11,7 +11,22 @@ import com.unity3d.ads.test.instrumentation.services.ads.webplayer.WebPlayerViewCacheTest; import com.unity3d.ads.test.instrumentation.services.ads.webplayer.WebPlayerViewSettingsCacheTest; import com.unity3d.ads.test.instrumentation.services.banners.BannerViewCacheTests; +import com.unity3d.ads.test.instrumentation.services.core.configuration.ExperimentsTest; import com.unity3d.ads.test.instrumentation.services.core.configuration.InitializationNotificationCenterTest; +import com.unity3d.ads.test.instrumentation.services.core.device.AsyncTokenStorageTest; +import com.unity3d.ads.test.instrumentation.services.core.device.DeviceInfoReaderCompressorTest; +import com.unity3d.ads.test.instrumentation.services.core.device.DeviceInfoReaderFilterProviderTest; +import com.unity3d.ads.test.instrumentation.services.core.device.DeviceInfoReaderTest; +import com.unity3d.ads.test.instrumentation.services.core.device.DeviceInfoReaderUrlEncoderTest; +import com.unity3d.ads.test.instrumentation.services.core.device.DeviceInfoReaderWithFilterTest; +import com.unity3d.ads.test.instrumentation.services.core.device.DeviceInfoReaderWithPIITest; +import com.unity3d.ads.test.instrumentation.services.core.device.DeviceInfoReaderWithStorageInfoTest; +import com.unity3d.ads.test.instrumentation.services.core.device.NativeTokenGeneratorTest; +import com.unity3d.ads.test.instrumentation.services.core.device.pii.PiiDataSelectorTest; +import com.unity3d.ads.test.instrumentation.services.core.device.pii.PiiDecisionDataTest; +import com.unity3d.ads.test.instrumentation.services.core.device.pii.PiiTrackingStatusReaderTest; +import com.unity3d.ads.test.instrumentation.services.core.misc.JsonFlattenerTest; +import com.unity3d.ads.test.instrumentation.services.core.webview.WebViewUrlBuilderTest; import com.unity3d.ads.test.instrumentation.services.core.webview.bridge.WebViewBridgeSharedObjectTests; import com.unity3d.ads.test.instrumentation.services.core.webview.bridge.invocation.WebViewBridgeInvocationRunnableTests; import com.unity3d.ads.test.instrumentation.services.core.webview.bridge.invocation.WebViewBridgeInvocationTests; @@ -27,6 +42,8 @@ UnityAnalyticsTest.class, AcquisitionTypeTest.class, InitializationNotificationCenterTest.class, + ExperimentsTest.class, + WebViewUrlBuilderTest.class, WebPlayerViewSettingsCacheTest.class, WebPlayerViewCacheTest.class, BannerViewCacheTests.class, @@ -41,7 +58,20 @@ AdOperationTests.class, ShowModuleTests.class, ConfigurationTest.class, - GmaScarTestSuite.class + GmaScarTestSuite.class, + AsyncTokenStorageTest.class, + NativeTokenGeneratorTest.class, + PiiTrackingStatusReaderTest.class, + PiiDecisionDataTest.class, + PiiDataSelectorTest.class, + DeviceInfoReaderTest.class, + DeviceInfoReaderCompressorTest.class, + DeviceInfoReaderUrlEncoderTest.class, + DeviceInfoReaderFilterProviderTest.class, + DeviceInfoReaderWithStorageInfoTest.class, + DeviceInfoReaderWithPIITest.class, + DeviceInfoReaderWithFilterTest.class, + JsonFlattenerTest.class }) public class InstrumentationTestSuite {} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/LoadModuleTests.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/LoadModuleTests.java index b384a2ab..2b273162 100644 --- a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/LoadModuleTests.java +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/LoadModuleTests.java @@ -14,8 +14,8 @@ import com.unity3d.services.ads.operation.load.LoadModule; import com.unity3d.services.ads.operation.load.LoadOperation; import com.unity3d.services.ads.operation.load.LoadOperationState; -import com.unity3d.services.core.request.ISDKMetricSender; -import com.unity3d.services.core.request.SDKMetricEvents; +import com.unity3d.services.core.request.metrics.ISDKMetricSender; +import com.unity3d.services.core.request.metrics.SDKMetricEvents; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; import com.unity3d.services.core.webview.bridge.invocation.IWebViewBridgeInvocation; @@ -72,7 +72,7 @@ public Object answer(InvocationOnMock invocation) { loadModule.executeAdOperation(webViewBridgeInvokerMock, loadOperationState); Mockito.verify(loadListenerMock, timeout(maxWaitTime).times(1)).onUnityAdsFailedToLoad(placementId, UnityAds.UnityAdsLoadError.INTERNAL_ERROR, "[UnityAds] Internal communication failure"); - Mockito.verify(sdkMetricSender, times(1)).SendSDKMetricEventWithTag(SDKMetricEvents.native_load_callback_error, new HashMap (){{ + Mockito.verify(sdkMetricSender, times(1)).sendSDKMetricEventWithTag(SDKMetricEvents.native_load_callback_error, new HashMap (){{ put("cbs", "invocationFailure"); }}); } @@ -91,7 +91,7 @@ public Object answer(InvocationOnMock invocation) { loadModule.executeAdOperation(webViewBridgeInvokerMock, loadOperationState); Mockito.verify(loadListenerMock, timeout(maxWaitTime).times(1)).onUnityAdsFailedToLoad(placementId, UnityAds.UnityAdsLoadError.INTERNAL_ERROR, "[UnityAds] Internal communication timeout"); - Mockito.verify(sdkMetricSender, times(1)).SendSDKMetricEvent(eq(SDKMetricEvents.native_load_callback_timeout)); + Mockito.verify(sdkMetricSender, times(1)).sendSDKMetricEvent(eq(SDKMetricEvents.native_load_callback_timeout)); } @Test diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleDecoratorTimeoutTests.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleDecoratorTimeoutTests.java index d718c127..391c1f9f 100644 --- a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleDecoratorTimeoutTests.java +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleDecoratorTimeoutTests.java @@ -17,7 +17,7 @@ import com.unity3d.services.ads.operation.show.ShowModule; import com.unity3d.services.ads.operation.show.ShowModuleDecoratorTimeout; import com.unity3d.services.ads.operation.show.ShowOperationState; -import com.unity3d.services.core.request.ISDKMetricSender; +import com.unity3d.services.core.request.metrics.ISDKMetricSender; import com.unity3d.services.core.webview.bridge.CallbackStatus; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; import com.unity3d.services.core.webview.bridge.invocation.WebViewBridgeInvocationRunnable; diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleTests.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleTests.java index e01a6d79..2446abff 100644 --- a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleTests.java +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/ads/operation/ShowModuleTests.java @@ -17,8 +17,8 @@ import com.unity3d.services.ads.operation.show.ShowModule; import com.unity3d.services.ads.operation.show.ShowOperation; import com.unity3d.services.ads.operation.show.ShowOperationState; -import com.unity3d.services.core.request.ISDKMetricSender; -import com.unity3d.services.core.request.SDKMetricEvents; +import com.unity3d.services.core.request.metrics.ISDKMetricSender; +import com.unity3d.services.core.request.metrics.SDKMetricEvents; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; import com.unity3d.services.core.webview.bridge.invocation.IWebViewBridgeInvocation; @@ -75,7 +75,7 @@ public Object answer(InvocationOnMock invocation) { _showModule.executeAdOperation(_webViewBridgeInvokerMock, showOperationState); Mockito.verify((_showListenerMock), timeout(maxWaitTime).times(1)).onUnityAdsShowFailure(placementId, UnityAds.UnityAdsShowError.INTERNAL_ERROR, "WebViewBridgeInvocationRunnable:run: invokeMethod failure"); - Mockito.verify(_sdkMetricSender, timeout(maxWaitTime).times(1)).SendSDKMetricEventWithTag(SDKMetricEvents.native_show_callback_error, new HashMap (){{ + Mockito.verify(_sdkMetricSender, timeout(maxWaitTime).times(1)).sendSDKMetricEventWithTag(SDKMetricEvents.native_show_callback_error, new HashMap (){{ put("cbs", "invocationFailure"); }}); } @@ -94,7 +94,7 @@ public Object answer(InvocationOnMock invocation) { _showModule.executeAdOperation(_webViewBridgeInvokerMock, showOperationState); Mockito.verify((_showListenerMock), timeout(maxWaitTime).times(1)).onUnityAdsShowFailure(placementId, UnityAds.UnityAdsShowError.INTERNAL_ERROR, "[UnityAds] Show Invocation Timeout"); - Mockito.verify(_sdkMetricSender, timeout(maxWaitTime).times(1)).SendSDKMetricEvent(eq(SDKMetricEvents.native_show_callback_timeout)); + Mockito.verify(_sdkMetricSender, timeout(maxWaitTime).times(1)).sendSDKMetricEvent(eq(SDKMetricEvents.native_show_callback_timeout)); } @Test diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ConfigurationRequestFactoryTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ConfigurationRequestFactoryTest.java new file mode 100644 index 00000000..967b55e8 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ConfigurationRequestFactoryTest.java @@ -0,0 +1,93 @@ +package com.unity3d.ads.test.instrumentation.services.core.configuration; + +import com.unity3d.services.core.configuration.Configuration; +import com.unity3d.services.core.configuration.ConfigurationRequestFactory; +import com.unity3d.services.core.configuration.Experiments; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; +import com.unity3d.services.core.request.WebRequest; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.MalformedURLException; +import java.util.List; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class ConfigurationRequestFactoryTest { + + @Mock + Experiments _experimentsMock; + + @Mock + Configuration _configurationMock; + + @Mock + IDeviceInfoReader _deviceInfoReaderMock; + + static final String CONFIG_URL = "http://configurl/"; + + @Test + public void testConfigurationRequestFactoryPost() throws MalformedURLException { + Mockito.when(_experimentsMock.isTwoStageInitializationEnabled()).thenReturn(true); + Mockito.when(_experimentsMock.isPOSTMethodInConfigRequestEnabled()).thenReturn(true); + Mockito.when(_configurationMock.getExperiments()).thenReturn(_experimentsMock); + + ConfigurationRequestFactory configurationRequestFactory = new ConfigurationRequestFactory(_configurationMock, _deviceInfoReaderMock, CONFIG_URL); + WebRequest webRequest = configurationRequestFactory.getWebRequest(); + Assert.assertEquals("POST", webRequest.getRequestType()); + Map> headers = webRequest.getHeaders(); + Assert.assertEquals("gzip", headers.get("Content-Encoding").get(0)); + } + + @Test + public void testConfigurationRequestFactoryGetIfTsiDisabled() throws MalformedURLException { + Mockito.when(_experimentsMock.isTwoStageInitializationEnabled()).thenReturn(false); + Mockito.when(_experimentsMock.isPOSTMethodInConfigRequestEnabled()).thenReturn(true); + Mockito.when(_configurationMock.getExperiments()).thenReturn(_experimentsMock); + + ConfigurationRequestFactory configurationRequestFactory = new ConfigurationRequestFactory(_configurationMock, _deviceInfoReaderMock, CONFIG_URL); + WebRequest webRequest = configurationRequestFactory.getWebRequest(); + Assert.assertEquals("GET", webRequest.getRequestType()); + Assert.assertTrue("ts missing from query", webRequest.getQuery().contains("ts")); + Assert.assertTrue("sdkVersion missing from query", webRequest.getQuery().contains("sdkVersion")); + Assert.assertTrue("sdkVersionName missing from query", webRequest.getQuery().contains("sdkVersionName")); + Assert.assertTrue("gameId missing from query", webRequest.getQuery().contains("gameId")); + } + + @Test + public void testConfigurationRequestFactoryGetIfTsiEnabled() throws MalformedURLException { + Mockito.when(_experimentsMock.isTwoStageInitializationEnabled()).thenReturn(true); + Mockito.when(_experimentsMock.isPOSTMethodInConfigRequestEnabled()).thenReturn(false); + Mockito.when(_configurationMock.getExperiments()).thenReturn(_experimentsMock); + + ConfigurationRequestFactory configurationRequestFactory = new ConfigurationRequestFactory(_configurationMock, _deviceInfoReaderMock, CONFIG_URL); + WebRequest webRequest = configurationRequestFactory.getWebRequest(); + Assert.assertEquals("GET", webRequest.getRequestType()); + Assert.assertEquals("c=H4sIAAAAAAAAAKuuBQBDv6ajAgAAAA%3D%3D", webRequest.getQuery()); + } + + @Test + public void testConfigurationRequestFactoryGetIfPostDisabled() throws MalformedURLException { + Mockito.when(_experimentsMock.isTwoStageInitializationEnabled()).thenReturn(true); + Mockito.when(_experimentsMock.isPOSTMethodInConfigRequestEnabled()).thenReturn(false); + Mockito.when(_configurationMock.getExperiments()).thenReturn(_experimentsMock); + + ConfigurationRequestFactory configurationRequestFactory = new ConfigurationRequestFactory(_configurationMock, _deviceInfoReaderMock, CONFIG_URL); + WebRequest webRequest = configurationRequestFactory.getWebRequest(); + Assert.assertEquals("GET", webRequest.getRequestType()); + } + + @Test + public void testConfigurationRequestFactoryGetNullExperiments() throws MalformedURLException { + Mockito.when(_configurationMock.getExperiments()).thenReturn(null); + + ConfigurationRequestFactory configurationRequestFactory = new ConfigurationRequestFactory(_configurationMock, _deviceInfoReaderMock, CONFIG_URL); + WebRequest webRequest = configurationRequestFactory.getWebRequest(); + Assert.assertEquals("GET", webRequest.getRequestType()); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ExperimentsTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ExperimentsTest.java new file mode 100644 index 00000000..7efbace4 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/configuration/ExperimentsTest.java @@ -0,0 +1,59 @@ +package com.unity3d.ads.test.instrumentation.services.core.configuration; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.unity3d.services.core.configuration.Experiments; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ExperimentsTest { + + @Test + public void testExperimentsWithData() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("fff", false); + jsonObject.put("tsi", false); + jsonObject.put("tsi_dc", true); + jsonObject.put("tsi_epii", false); + jsonObject.put("tsi_p", false); + + Experiments experiments = new Experiments(jsonObject); + Assert.assertTrue(experiments.isHandleDeveloperConsent()); + Assert.assertFalse(experiments.isTwoStageInitializationEnabled()); + } + + @Test + public void testExperimentsWithMissingData() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("fff", false); + + Experiments experiments = new Experiments(jsonObject); + Assert.assertFalse(experiments.isHandleDeveloperConsent()); + } + + @Test + public void testExperimentsWithEmptyData() { + JSONObject jsonObject = new JSONObject(); + + Experiments experiments = new Experiments(jsonObject); + Assert.assertFalse(experiments.isHandleDeveloperConsent()); + } + + @Test + public void testExperimentsWithNullData() { + Experiments experiments = new Experiments(null); + Assert.assertFalse(experiments.isHandleDeveloperConsent()); + } + + @Test + public void testExperimentsDefault() { + Experiments experiments = new Experiments(); + Assert.assertFalse(experiments.isHandleDeveloperConsent()); + } + +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/AsyncTokenStorageTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/AsyncTokenStorageTest.java new file mode 100644 index 00000000..7fab5d40 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/AsyncTokenStorageTest.java @@ -0,0 +1,412 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import static com.unity3d.services.core.device.TokenType.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; + +import android.os.Handler; +import android.os.Message; + +import com.unity3d.ads.IUnityAdsTokenListener; +import com.unity3d.services.ads.token.AsyncTokenStorage; +import com.unity3d.services.ads.token.INativeTokenGenerator; +import com.unity3d.services.ads.token.INativeTokenGeneratorListener; +import com.unity3d.services.ads.token.TokenStorage; +import com.unity3d.services.core.configuration.Configuration; +import com.unity3d.services.core.configuration.Experiments; +import com.unity3d.services.core.device.TokenType; +import com.unity3d.services.core.properties.ClientProperties; +import com.unity3d.services.core.properties.SdkProperties; +import com.unity3d.services.core.webview.WebViewApp; + +import org.json.JSONArray; +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.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class AsyncTokenStorageTest { + @Mock + INativeTokenGenerator _nativeTokenGenerator; + @Mock + Handler _handler; + @Mock + IUnityAdsTokenListener _listener; + + AsyncTokenStorage _asyncTokenStorage; + Runnable timeoutRunnable; + List handlerRunnable = new ArrayList<>(); + + @Before + public void Before() { + ClientProperties.setApplicationContext(androidx.test.core.app.ApplicationProvider.getApplicationContext()); + TokenStorage.deleteTokens(); + TokenStorage.setInitToken(null); + MockWebViewApp webViewApp = new MockWebViewApp(); + WebViewApp.setCurrentApp(webViewApp); + SdkProperties.setInitializeState(SdkProperties.InitializationState.INITIALIZING); + + _asyncTokenStorage = new AsyncTokenStorage(_nativeTokenGenerator, _handler); + _asyncTokenStorage.setConfiguration(new Configuration()); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + Message message = invocation.getArgument(0); + timeoutRunnable = message.getCallback(); + handlerRunnable.add(message.getCallback()); + return true; + } + }).when(_handler).sendMessageAtTime(any(Message.class), anyLong()); + } + + @After + public void After() { + ClientProperties.setApplicationContext(null); + WebViewApp.setCurrentApp(null); + SdkProperties.setInitializeState(SdkProperties.InitializationState.NOT_INITIALIZED); + } + + @Test + public void testNotInitializedSdk() { + SdkProperties.setInitializeState(SdkProperties.InitializationState.NOT_INITIALIZED); + + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(null); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenBeforeInit() { + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + } + + @Test + public void testGetTokenBeforeInitAndTimeout() { + _asyncTokenStorage.getToken(_listener); + + timeoutRunnable.run(); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(null); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenBeforeInitAndTimeoutGuard() { + _asyncTokenStorage.getToken(_listener); + + timeoutRunnable.run(); + timeoutRunnable.run(); + timeoutRunnable.run(); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(null); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenBeforeInitAndInitTimeTokenReady() { + _asyncTokenStorage.getToken(_listener); + + TokenStorage.setInitToken("init_time_token"); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("init_time_token"); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenBeforeInitAndCreateEmptyQueue() throws JSONException { + _asyncTokenStorage.getToken(_listener); + + TokenStorage.createTokens(new JSONArray()); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenBeforeInitAndCreateQueueTokenReady() throws JSONException { + _asyncTokenStorage.getToken(_listener); + + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_1"); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenAfterInitTokenReady() throws JSONException { + TokenStorage.setInitToken("init_time_token"); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("init_time_token"); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + + } + + @Test + public void testGetTokenAfterQueueTokenReady() throws JSONException { + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_1"); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testMultipleCallsBeforeInit() throws JSONException { + _asyncTokenStorage.getToken(_listener); + _asyncTokenStorage.getToken(_listener); + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_handler, times(3)).sendMessageAtTime(any(Message.class), anyLong()); + + TokenStorage.setInitToken("init_time_token"); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(3)).onUnityAdsTokenReady("init_time_token"); + Mockito.verify(_listener, Mockito.times(3)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testMultipleCallsBeforeInitAndQueue() throws JSONException { + _asyncTokenStorage.getToken(_listener); + _asyncTokenStorage.getToken(_listener); + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_handler, times(3)).sendMessageAtTime(any(Message.class), anyLong()); + + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + array.put("queue_token_2"); + array.put("queue_token_3"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + InOrder order = Mockito.inOrder(_listener); + order.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_1"); + order.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_2"); + order.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_3"); + Mockito.verify(_listener, Mockito.times(3)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testMultipleCallsBeforeInitAndSingleAfterInit() throws JSONException { + _asyncTokenStorage.getToken(_listener); + _asyncTokenStorage.getToken(_listener); + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_handler, times(3)).sendMessageAtTime(any(Message.class), anyLong()); + + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + array.put("queue_token_2"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(2)).onUnityAdsTokenReady(nullable(String.class)); + + array = new JSONArray(); + array.put("queue_token_3"); + TokenStorage.appendTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_3"); + Mockito.verify(_listener, Mockito.times(3)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenFailedInit() throws JSONException { + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + SdkProperties.setInitializeState(SdkProperties.InitializationState.INITIALIZED_FAILED); + + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_handler, times(0)).sendMessageAtTime(any(Message.class), anyLong()); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(null); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testGetTokenFailedInitCancelAllRequests() { + _asyncTokenStorage.getToken(_listener); + _asyncTokenStorage.getToken(_listener); + + SdkProperties.setInitializeState(SdkProperties.InitializationState.INITIALIZED_FAILED); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testNativeToken() throws JSONException { + Configuration configuration = Mockito.mock(Configuration.class); + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("tsi_nt", true); + Mockito.when(configuration.getExperiments()).thenReturn(new Experiments(jsonObject)); + _asyncTokenStorage.setConfiguration(configuration); + + doAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) { + invocation.getArgument(0, INativeTokenGeneratorListener.class).onReady("native_token"); + return null; + }}).when(_nativeTokenGenerator).generateToken(any(INativeTokenGeneratorListener.class)); + + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_handler, times(2)).sendMessageAtTime(any(Message.class), anyLong()); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + + handlerRunnable.get(1).run(); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("native_token"); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testDoNotUseNativeToken() throws JSONException { + Configuration configuration = Mockito.mock(Configuration.class); + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("tsi_nt", true); + Mockito.when(configuration.getExperiments()).thenReturn(new Experiments(jsonObject)); + _asyncTokenStorage.setConfiguration(configuration); + + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_nativeTokenGenerator, times(0)).generateToken(any(INativeTokenGeneratorListener.class)); + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_1"); + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testDelayedConfiguration() throws JSONException { + _asyncTokenStorage = new AsyncTokenStorage(_nativeTokenGenerator, _handler); + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + + _asyncTokenStorage.setConfiguration(new Configuration()); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady(nullable(String.class)); + } + + @Test + public void testDelayedConfigurationTwoConfigSets() throws JSONException { + _asyncTokenStorage = new AsyncTokenStorage(_nativeTokenGenerator, _handler); + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + + _asyncTokenStorage.setConfiguration(new Configuration()); + _asyncTokenStorage.setConfiguration(new Configuration()); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("queue_token_1"); + } + + @Test + public void testDelayedConfigurationConfigurationArrivedFirst() throws JSONException { + _asyncTokenStorage = new AsyncTokenStorage(_nativeTokenGenerator, _handler); + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + + _asyncTokenStorage.setConfiguration(new Configuration()); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + + TokenStorage.setInitToken("init_token"); + _asyncTokenStorage.onTokenAvailable(TOKEN_NATIVE); + + Mockito.verify(_listener, Mockito.times(1)).onUnityAdsTokenReady("init_token"); + } + + @Test + public void testNullConfiguration() throws JSONException { + _asyncTokenStorage = new AsyncTokenStorage(_nativeTokenGenerator, _handler); + _asyncTokenStorage.getToken(_listener); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + Mockito.verify(_handler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); + + JSONArray array = new JSONArray(); + array.put("queue_token_1"); + TokenStorage.createTokens(array); + _asyncTokenStorage.onTokenAvailable(TOKEN_REMOTE); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + + _asyncTokenStorage.setConfiguration(null); + + Mockito.verify(_listener, Mockito.times(0)).onUnityAdsTokenReady(nullable(String.class)); + } + + private class MockWebViewApp extends WebViewApp { + @Override + public boolean sendEvent(Enum eventCategory, Enum eventId, Object... params) { + return true; + } + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderCompressorTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderCompressorTest.java new file mode 100644 index 00000000..f15c643a --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderCompressorTest.java @@ -0,0 +1,50 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderCompressor; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class DeviceInfoReaderCompressorTest { + private static final Map TESTDATA = new HashMap() {{ + put("Key", 0); + }}; + + private static byte[] TESTDATA_COMPRESSED = new byte[] {31, -117, 8, 0, 0, 0, 0, 0, 0, 0, -85, 86, -14, 78, -83, 84, -78, 50, -88, 5, 0, -40, 45, 110, 23, 9, 0, 0, 0}; + + @Test + public void testDeviceInfoReaderCompressor() { + IDeviceInfoReader deviceInfoReaderMock = Mockito.mock(IDeviceInfoReader.class); + Mockito.when(deviceInfoReaderMock.getDeviceInfoData()).thenReturn(TESTDATA); + DeviceInfoReaderCompressor deviceInfoReaderCompressor = new DeviceInfoReaderCompressor(deviceInfoReaderMock); + byte[] compressedData = deviceInfoReaderCompressor.getDeviceData(); + Assert.assertArrayEquals(TESTDATA_COMPRESSED, compressedData); + } + + @Test + public void testDeviceInfoReaderCompressorWithEmptyMap() { + IDeviceInfoReader deviceInfoReaderMock = Mockito.mock(IDeviceInfoReader.class); + Mockito.when(deviceInfoReaderMock.getDeviceInfoData()).thenReturn(new HashMap()); + DeviceInfoReaderCompressor deviceInfoReaderCompressor = new DeviceInfoReaderCompressor(deviceInfoReaderMock); + byte[] compressedData = deviceInfoReaderCompressor.getDeviceData(); + Assert.assertNotNull(compressedData); + } + + @Test + public void testDeviceInfoReaderCompressorWithNullDeviceData() { + IDeviceInfoReader deviceInfoReaderMock = Mockito.mock(IDeviceInfoReader.class); + Mockito.when(deviceInfoReaderMock.getDeviceInfoData()).thenReturn(null); + DeviceInfoReaderCompressor deviceInfoReaderCompressor = new DeviceInfoReaderCompressor(deviceInfoReaderMock); + byte[] compressedData = deviceInfoReaderCompressor.getDeviceData(); + Assert.assertNull(compressedData); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderFilterProviderTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderFilterProviderTest.java new file mode 100644 index 00000000..0fb11b37 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderFilterProviderTest.java @@ -0,0 +1,80 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderFilterProvider; +import com.unity3d.services.core.misc.IJsonStorageReader; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class DeviceInfoReaderFilterProviderTest { + @Mock + IJsonStorageReader _jsonStorageReaderMock; + + @Test + public void testDeviceInfoReaderFilterProvider() throws JSONException { + Mockito.when(_jsonStorageReaderMock.getData()).thenReturn(getStorageSimpleMockData()); + DeviceInfoReaderFilterProvider deviceInfoReaderFilterProvider = new DeviceInfoReaderFilterProvider(_jsonStorageReaderMock); + List filteredKeys = deviceInfoReaderFilterProvider.getFilterList(); + Assert.assertNotNull(filteredKeys); + Assert.assertEquals(Collections.singletonList("field1"), filteredKeys); + } + + @Test + public void testDeviceInfoReaderFilterProviderWithComplex() throws JSONException { + Mockito.when(_jsonStorageReaderMock.getData()).thenReturn(getStorageComplexMockData()); + DeviceInfoReaderFilterProvider deviceInfoReaderFilterProvider = new DeviceInfoReaderFilterProvider(_jsonStorageReaderMock); + List filteredKeys = deviceInfoReaderFilterProvider.getFilterList(); + Assert.assertNotNull(filteredKeys); + Assert.assertEquals(Collections.singletonList("field1"), filteredKeys); + } + + @Test + public void testDeviceInfoReaderFilterProviderWithEmptyList() { + Mockito.when(_jsonStorageReaderMock.getData()).thenReturn(new JSONObject()); + DeviceInfoReaderFilterProvider deviceInfoReaderFilterProvider = new DeviceInfoReaderFilterProvider(_jsonStorageReaderMock); + List filteredKeys = deviceInfoReaderFilterProvider.getFilterList(); + Assert.assertNotNull(filteredKeys); + Assert.assertEquals(0, filteredKeys.size()); + } + + @Test + public void testDeviceInfoReaderFilterProviderWithNullData() { + Mockito.when(_jsonStorageReaderMock.getData()).thenReturn(null); + DeviceInfoReaderFilterProvider deviceInfoReaderFilterProvider = new DeviceInfoReaderFilterProvider(_jsonStorageReaderMock); + List filteredKeys = deviceInfoReaderFilterProvider.getFilterList(); + Assert.assertNotNull(filteredKeys); + Assert.assertEquals(0, filteredKeys.size()); + } + + @Test + public void testDeviceInfoReaderFilterProviderWithCommas() throws JSONException { + Mockito.when(_jsonStorageReaderMock.getData()).thenReturn(getStorageCommaSeparatedMockData()); + DeviceInfoReaderFilterProvider deviceInfoReaderFilterProvider = new DeviceInfoReaderFilterProvider(_jsonStorageReaderMock); + List filteredKeys = deviceInfoReaderFilterProvider.getFilterList(); + Assert.assertNotNull(filteredKeys); + Assert.assertEquals(Arrays.asList("field1", "field2"), filteredKeys); + } + + private JSONObject getStorageSimpleMockData() throws JSONException { + return new JSONObject("{\"unifiedconfig\": {\"exclude\": \"field1\"}}"); + } + + private JSONObject getStorageCommaSeparatedMockData() throws JSONException { + return new JSONObject("{\"unifiedconfig\": {\"exclude\": \"field1, field2\"}}"); + } + + private JSONObject getStorageComplexMockData() throws JSONException { + return new JSONObject("{\"key1\": \"value1\", \"unifiedconfig\": {\"exclude\": \"field1\"}}"); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderTest.java new file mode 100644 index 00000000..30745bb7 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderTest.java @@ -0,0 +1,25 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.unity3d.services.core.device.reader.DeviceInfoReader; +import com.unity3d.services.core.properties.ClientProperties; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class DeviceInfoReaderTest { + + @Test + public void testDeviceInfoReader() { + ClientProperties.setApplicationContext(InstrumentationRegistry.getInstrumentation().getContext()); + DeviceInfoReader deviceInfoReader = new DeviceInfoReader(); + Map deviceData = deviceInfoReader.getDeviceInfoData(); + Assert.assertEquals(47, deviceData.size()); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderUrlEncoderTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderUrlEncoderTest.java new file mode 100644 index 00000000..ec289979 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderUrlEncoderTest.java @@ -0,0 +1,31 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import android.util.Base64; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderCompressor; +import com.unity3d.services.core.device.reader.DeviceInfoReaderUrlEncoder; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +@RunWith(AndroidJUnit4.class) +public class DeviceInfoReaderUrlEncoderTest { + @Test + public void testDeviceInfoReaderUrlEncoderDecorator() throws UnsupportedEncodingException { + DeviceInfoReaderCompressor deviceInfoReaderCompressor = Mockito.mock(DeviceInfoReaderCompressor.class); + Mockito.when(deviceInfoReaderCompressor.getDeviceData()).thenReturn("test".getBytes()); + DeviceInfoReaderUrlEncoder deviceInfoReaderUrlEncoder = new DeviceInfoReaderUrlEncoder(deviceInfoReaderCompressor); + String urlQueryString = deviceInfoReaderUrlEncoder.getUrlEncodedData(); + String decodedValue = URLDecoder.decode(urlQueryString, "UTF-8"); + byte[] base64Decoded = Base64.decode(decodedValue, Base64.NO_WRAP); + Assert.assertEquals("dGVzdA%3D%3D", urlQueryString); + Assert.assertArrayEquals("test".getBytes(), base64Decoded); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithFilterTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithFilterTest.java new file mode 100644 index 00000000..cf6bf672 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithFilterTest.java @@ -0,0 +1,72 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderWithFilter; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class DeviceInfoReaderWithFilterTest { + @Mock + IDeviceInfoReader _deviceInfoReaderMock; + + @Test + public void testDeviceInfoReaderSingleWithFilterSingleKey() { + Mockito.when(_deviceInfoReaderMock.getDeviceInfoData()).thenReturn(getTestDataWithSingleEntry()); + DeviceInfoReaderWithFilter deviceInfoReaderWithFilter = new DeviceInfoReaderWithFilter(_deviceInfoReaderMock, Collections.singletonList("key1")); + Assert.assertTrue(deviceInfoReaderWithFilter.getDeviceInfoData().isEmpty()); + } + + @Test + public void testDeviceInfoReaderMultipleWithFilterSingleKey() { + Mockito.when(_deviceInfoReaderMock.getDeviceInfoData()).thenReturn(getTestDataWithMultipleEntry()); + DeviceInfoReaderWithFilter deviceInfoReaderWithFilter = new DeviceInfoReaderWithFilter(_deviceInfoReaderMock, Collections.singletonList("key1")); + Assert.assertNull(deviceInfoReaderWithFilter.getDeviceInfoData().get("key1")); + Assert.assertEquals(2, deviceInfoReaderWithFilter.getDeviceInfoData().size()); + } + + @Test + public void testDeviceInfoReaderMultipleWithFilterMissingKey() { + Mockito.when(_deviceInfoReaderMock.getDeviceInfoData()).thenReturn(getTestDataWithMultipleEntry()); + DeviceInfoReaderWithFilter deviceInfoReaderWithFilter = new DeviceInfoReaderWithFilter(_deviceInfoReaderMock, Collections.singletonList("missing")); + Assert.assertEquals(getTestDataWithMultipleEntry(), deviceInfoReaderWithFilter.getDeviceInfoData()); + } + + @Test + public void testDeviceInfoReaderMultipleWithFilterEmpty() { + Mockito.when(_deviceInfoReaderMock.getDeviceInfoData()).thenReturn(getTestDataWithMultipleEntry()); + DeviceInfoReaderWithFilter deviceInfoReaderWithFilter = new DeviceInfoReaderWithFilter(_deviceInfoReaderMock, new ArrayList()); + Assert.assertEquals(getTestDataWithMultipleEntry(), deviceInfoReaderWithFilter.getDeviceInfoData()); + } + + @Test + public void testDeviceInfoReaderMultipleWithFilterNull() { + Mockito.when(_deviceInfoReaderMock.getDeviceInfoData()).thenReturn(getTestDataWithMultipleEntry()); + DeviceInfoReaderWithFilter deviceInfoReaderWithFilter = new DeviceInfoReaderWithFilter(_deviceInfoReaderMock, null); + Assert.assertEquals(getTestDataWithMultipleEntry(), deviceInfoReaderWithFilter.getDeviceInfoData()); + } + + private Map getTestDataWithSingleEntry() { + return new HashMap() {{ + put("key1", "value1"); + }}; + } + + private Map getTestDataWithMultipleEntry() { + return new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + put("key3", "value3"); + }}; + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithPIITest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithPIITest.java new file mode 100644 index 00000000..01919b61 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithPIITest.java @@ -0,0 +1,86 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.ADVERTISING_TRACKING_ID_NORMALIZED_KEY; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderWithPII; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; +import com.unity3d.services.core.device.reader.pii.DataSelectorResult; +import com.unity3d.services.core.device.reader.pii.PiiDataProvider; +import com.unity3d.services.core.device.reader.pii.PiiDataSelector; +import com.unity3d.services.core.device.reader.pii.PiiDecisionData; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class DeviceInfoReaderWithPIITest { + + private static final String PII_STORAGE_IDFA = "test-advertiser-id"; + private static final String PII_DEVICE_IDFA = "test-device-advertiser-id"; + + @Mock + private IDeviceInfoReader _deviceInfoReaderMock; + + @Mock + private PiiDecisionData _piiDecisionDataMock; + + @Mock + private PiiDataSelector _piiDataSelectorMock; + + @Mock + private PiiDataProvider _piiDataProviderMock; + + @Before + public void setup() { + Mockito.when(_deviceInfoReaderMock.getDeviceInfoData()).thenReturn(getMockDeviceData()); + Mockito.when(_piiDataSelectorMock.whatToDoWithPII()).thenReturn(_piiDecisionDataMock); + Mockito.when(_piiDataProviderMock.getAdvertisingTrackingId()).thenReturn(PII_DEVICE_IDFA); + } + + @Test + public void testDeviceInfoReaderWithPiiUpdate() { + Mockito.when(_piiDecisionDataMock.getResultType()).thenReturn(DataSelectorResult.UPDATE); + DeviceInfoReaderWithPII deviceInfoReaderWithPII = new DeviceInfoReaderWithPII(_deviceInfoReaderMock, _piiDataSelectorMock, _piiDataProviderMock); + Map deviceInfoData = deviceInfoReaderWithPII.getDeviceInfoData(); + Assert.assertEquals(PII_DEVICE_IDFA, deviceInfoData.get(ADVERTISING_TRACKING_ID_NORMALIZED_KEY)); + } + + @Test + public void testDeviceInfoReaderWithPiiInclude() { + Mockito.when(_piiDecisionDataMock.getResultType()).thenReturn(DataSelectorResult.INCLUDE); + Mockito.when(_piiDecisionDataMock.getAttributes()).thenReturn(getPiiTestData()); + DeviceInfoReaderWithPII deviceInfoReaderWithPII = new DeviceInfoReaderWithPII(_deviceInfoReaderMock, _piiDataSelectorMock, _piiDataProviderMock); + Map deviceInfoData = deviceInfoReaderWithPII.getDeviceInfoData(); + Assert.assertEquals(PII_STORAGE_IDFA, deviceInfoData.get(ADVERTISING_TRACKING_ID_NORMALIZED_KEY)); + } + + @Test + public void testDeviceInfoReaderWithPiiExclude() { + Mockito.when(_piiDecisionDataMock.getResultType()).thenReturn(DataSelectorResult.EXCLUDE); + Mockito.when(_piiDecisionDataMock.getAttributes()).thenReturn(getPiiTestData()); + DeviceInfoReaderWithPII deviceInfoReaderWithPII = new DeviceInfoReaderWithPII(_deviceInfoReaderMock, _piiDataSelectorMock, _piiDataProviderMock); + Map deviceInfoData = deviceInfoReaderWithPII.getDeviceInfoData(); + Assert.assertNull(deviceInfoData.get(ADVERTISING_TRACKING_ID_NORMALIZED_KEY)); + Assert.assertEquals(getMockDeviceData(), deviceInfoData); + } + + private Map getPiiTestData() { + Map piiTestData = new HashMap<>(); + piiTestData.put(ADVERTISING_TRACKING_ID_NORMALIZED_KEY, PII_STORAGE_IDFA); + return piiTestData; + } + + private Map getMockDeviceData() { + Map piiTestData = new HashMap<>(); + piiTestData.put("deviceKey", "deviceValue"); + return piiTestData; + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithStorageInfoTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithStorageInfoTest.java new file mode 100644 index 00000000..f46173e5 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/DeviceInfoReaderWithStorageInfoTest.java @@ -0,0 +1,73 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderWithStorageInfo; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; +import com.unity3d.services.core.misc.IJsonStorageReader; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.HashMap; +import java.util.Map; + +public class DeviceInfoReaderWithStorageInfoTest { + private final static Map DEVICE_INFO_TEST_DATA = new HashMap() {{ + put("test", true); + }}; + + private final static JSONObject PUBLIC_STORAGE_TEST_DATA = new JSONObject() {{ + try { + put("privacy", true); + } catch (JSONException e) { + e.printStackTrace(); + } + }}; + + private final static JSONObject PRIVATE_STORAGE_TEST_DATA = new JSONObject() {{ + try { + put("gdpr", true); + } catch (JSONException e) { + e.printStackTrace(); + } + }}; + + + @Test + public void testDeviceInfoReaderWithStorageInfoNoStorage() { + IDeviceInfoReader deviceInfoReader = Mockito.mock(IDeviceInfoReader.class); + Mockito.when(deviceInfoReader.getDeviceInfoData()).thenReturn(DEVICE_INFO_TEST_DATA); + DeviceInfoReaderWithStorageInfo deviceInfoReaderWithStorageInfo = new DeviceInfoReaderWithStorageInfo(deviceInfoReader); + Map resultData = deviceInfoReaderWithStorageInfo.getDeviceInfoData(); + Assert.assertEquals(DEVICE_INFO_TEST_DATA.get("test"), resultData.get("test")); + } + + @Test + public void testDeviceInfoReaderWithStorageInfoWithSingleStorage() { + IDeviceInfoReader deviceInfoReader = Mockito.mock(IDeviceInfoReader.class); + Mockito.when(deviceInfoReader.getDeviceInfoData()).thenReturn(DEVICE_INFO_TEST_DATA); + IJsonStorageReader jsonStorageReaderPublic = Mockito.mock(IJsonStorageReader.class); + Mockito.when(jsonStorageReaderPublic.getData()).thenReturn(PUBLIC_STORAGE_TEST_DATA); + DeviceInfoReaderWithStorageInfo deviceInfoReaderWithStorageInfo = new DeviceInfoReaderWithStorageInfo(deviceInfoReader, jsonStorageReaderPublic); + Map resultData = deviceInfoReaderWithStorageInfo.getDeviceInfoData(); + Assert.assertEquals(DEVICE_INFO_TEST_DATA.get("test"), resultData.get("test")); + Assert.assertEquals(PUBLIC_STORAGE_TEST_DATA.opt("privacy"), resultData.get("privacy")); + } + + @Test + public void testDeviceInfoReaderWithStorageInfoWithMultipleStorage() { + IDeviceInfoReader deviceInfoReader = Mockito.mock(IDeviceInfoReader.class); + Mockito.when(deviceInfoReader.getDeviceInfoData()).thenReturn(DEVICE_INFO_TEST_DATA); + IJsonStorageReader jsonStorageReaderPublic = Mockito.mock(IJsonStorageReader.class); + Mockito.when(jsonStorageReaderPublic.getData()).thenReturn(PUBLIC_STORAGE_TEST_DATA); + IJsonStorageReader jsonStorageReaderPrivate = Mockito.mock(IJsonStorageReader.class); + Mockito.when(jsonStorageReaderPrivate.getData()).thenReturn(PRIVATE_STORAGE_TEST_DATA); + DeviceInfoReaderWithStorageInfo deviceInfoReaderWithStorageInfo = new DeviceInfoReaderWithStorageInfo(deviceInfoReader, jsonStorageReaderPublic, jsonStorageReaderPrivate); + Map resultData = deviceInfoReaderWithStorageInfo.getDeviceInfoData(); + Assert.assertEquals(DEVICE_INFO_TEST_DATA.get("test"), resultData.get("test")); + Assert.assertEquals(PUBLIC_STORAGE_TEST_DATA.opt("privacy"), resultData.get("privacy")); + Assert.assertEquals(PRIVATE_STORAGE_TEST_DATA.opt("gdpr"), resultData.get("gdpr")); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/NativeTokenGeneratorTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/NativeTokenGeneratorTest.java new file mode 100644 index 00000000..2305eaa9 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/NativeTokenGeneratorTest.java @@ -0,0 +1,71 @@ +package com.unity3d.ads.test.instrumentation.services.core.device; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import com.unity3d.services.ads.token.INativeTokenGeneratorListener; +import com.unity3d.services.ads.token.NativeTokenGenerator; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.util.HashMap; +import java.util.concurrent.ExecutorService; + +@RunWith(MockitoJUnitRunner.class) +public class NativeTokenGeneratorTest { + @Mock + ExecutorService _executorService; + @Mock + IDeviceInfoReader _deviceInfoReader; + @Mock + INativeTokenGeneratorListener _callback; + + @Test + public void testGenerateToken() { + doAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) { + invocation.getArgument(0, Runnable.class).run(); + return null; + }}).when(_executorService).execute(any(Runnable.class)); + + when(_deviceInfoReader.getDeviceInfoData()).thenReturn(new HashMap()); + + NativeTokenGenerator nativeTokenGenerator = new NativeTokenGenerator(_executorService, _deviceInfoReader); + nativeTokenGenerator.generateToken(_callback); + + Mockito.verify(_callback, times(1)) + .onReady("1:H4sIAAAAAAAAAKuuBQBDv6ajAgAAAA=="); + Mockito.verify(_callback, times(1)) + .onReady(anyString()); + } + + @Test + public void testGenerateTokenWithException() { + doAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) { + invocation.getArgument(0, Runnable.class).run(); + return null; + }}).when(_executorService).execute(any(Runnable.class)); + + when(_deviceInfoReader.getDeviceInfoData()).thenThrow(new RuntimeException()); + + NativeTokenGenerator nativeTokenGenerator = new NativeTokenGenerator(_executorService, _deviceInfoReader); + nativeTokenGenerator.generateToken(_callback); + + Mockito.verify(_callback, times(1)) + .onReady(null); + Mockito.verify(_callback, times(1)) + .onReady(nullable(String.class)); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDataSelectorTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDataSelectorTest.java new file mode 100644 index 00000000..e1371e23 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDataSelectorTest.java @@ -0,0 +1,133 @@ +package com.unity3d.ads.test.instrumentation.services.core.device.pii; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.ADVERTISING_TRACKING_ID_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.UNIFIED_CONFIG_PII_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_KEY; + +import com.unity3d.services.core.configuration.Experiments; +import com.unity3d.services.core.device.reader.pii.DataSelectorResult; +import com.unity3d.services.core.device.reader.pii.PiiDataSelector; +import com.unity3d.services.core.device.reader.pii.PiiDecisionData; +import com.unity3d.services.core.device.reader.pii.PiiPrivacyMode; +import com.unity3d.services.core.device.reader.pii.PiiTrackingStatusReader; +import com.unity3d.services.core.misc.IJsonStorageReader; +import com.unity3d.services.core.misc.JsonFlattener; +import com.unity3d.services.core.misc.Utilities; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class PiiDataSelectorTest { + private static final String PII_STORAGE_IDFA = "test-advertiser-id"; + + @Mock + private PiiTrackingStatusReader _piiTrackingStatusReaderMock; + + @Mock + private IJsonStorageReader _jsonStorageMock; + + @Mock + private Experiments _experimentsMock; + + @Before + public void setup() throws JSONException { + Mockito.when(_jsonStorageMock.get(UNIFIED_CONFIG_PII_KEY)).thenReturn(getPiiInternalData()); + } + + @Test + public void testPiiDataSelectorNoUpdatePrivacyNoneNonBehaviorTrue() throws JSONException { + PiiDecisionData expectedDecision = new PiiDecisionData(DataSelectorResult.INCLUDE, getFlattenedTestData(false, false)); + runTest(PiiPrivacyMode.NONE, false, false, false, expectedDecision); + } + + @Test + public void testPiiDataSelectorForceUpdatePrivacyMixedNonBehariorFalse() throws JSONException { + PiiDecisionData expectedDecision = new PiiDecisionData(DataSelectorResult.UPDATE, getFlattenedTestData(true, false)); + runTest(PiiPrivacyMode.MIXED, false, true, false, expectedDecision); + } + + @Test + public void testPiiDataSelectorNoUpdatePrivacyMixedNonBehariorFalse() throws JSONException { + PiiDecisionData expectedDecision = new PiiDecisionData(DataSelectorResult.INCLUDE, getFlattenedTestData(true, false)); + runTest(PiiPrivacyMode.MIXED, false, false, false, expectedDecision); + } + + @Test + public void testPiiDataSelectorNoUpdatePrivacyMixedNonBehaviorTrue() throws JSONException { + PiiDecisionData expectedDecision = new PiiDecisionData(DataSelectorResult.INCLUDE, new HashMap(){{put(USER_NON_BEHAVIORAL_KEY, true);}}); + runTest(PiiPrivacyMode.MIXED, true, false, true, expectedDecision); + } + + @Test + public void testPiiDataSelectorNoUpdatePrivacyAppNonBehaviorFalse() throws JSONException { + PiiDecisionData expectedDecision = new PiiDecisionData(DataSelectorResult.EXCLUDE, new HashMap()); + runTest(PiiPrivacyMode.APP, false, false, false, expectedDecision); + } + + @Test + public void testPiiDataSelectorUpdatePrivacyNoneNonBehaviorFalse() throws JSONException { + PiiDecisionData expectedDecision = new PiiDecisionData(DataSelectorResult.UPDATE, getFlattenedTestData(false, false)); + runTest(PiiPrivacyMode.NONE, false, true, false, expectedDecision); + } + + @Test + public void testPiiDataSelectorNoUpdatePrivacyUndefinedNonBehaviorFalse() throws JSONException { + PiiDecisionData expectedDecision = new PiiDecisionData(DataSelectorResult.EXCLUDE, new HashMap()); + runTest(PiiPrivacyMode.UNDEFINED, false, false, false, expectedDecision); + } + + private void helpSetMocks(boolean updatePiiFieldsExp, PiiPrivacyMode piiPrivacyMode, boolean userNonBehavioralFlag) { + Mockito.when(_experimentsMock.isUpdatePiiFields()).thenReturn(updatePiiFieldsExp); + Mockito.when(_piiTrackingStatusReaderMock.getPrivacyMode()).thenReturn(piiPrivacyMode); + Mockito.when(_piiTrackingStatusReaderMock.getUserNonBehavioralFlag()).thenReturn(userNonBehavioralFlag); + } + + private void runTest(PiiPrivacyMode piiPrivacyMode, boolean userNonBehavioral, boolean forcedUpdate, boolean expectNonBehavioural, PiiDecisionData expectedDecision) { + helpSetMocks(forcedUpdate, piiPrivacyMode, userNonBehavioral); + PiiDataSelector piiDataSelector = new PiiDataSelector(_piiTrackingStatusReaderMock, _jsonStorageMock, _experimentsMock); + PiiDecisionData piiDecisionData = piiDataSelector.whatToDoWithPII(); + Assert.assertEquals(expectedDecision.getResultType(), piiDecisionData.getResultType()); + Assert.assertEquals(expectedDecision.getAttributes(), piiDecisionData.getAttributes()); + if (piiDecisionData.getUserNonBehavioralFlag() == null && !expectNonBehavioural) { + Assert.assertNull(piiDecisionData.getUserNonBehavioralFlag()); + } else { + Assert.assertEquals(expectNonBehavioural, piiDecisionData.getUserNonBehavioralFlag()); + } + } + + private Map getFlattenedTestData(boolean includeNonBehavioralFlag, boolean expectedFlag) throws JSONException { + JsonFlattener jsonFlattener = new JsonFlattener(getPiiTestData()); + JSONObject flattenedTestData; + flattenedTestData = jsonFlattener.flattenJson(".", Collections.singletonList(UNIFIED_CONFIG_PII_KEY), new ArrayList(), new ArrayList()); + if (includeNonBehavioralFlag) { + flattenedTestData.put(USER_NON_BEHAVIORAL_KEY, expectedFlag); + } + return Utilities.convertJsonToMap(flattenedTestData); + } + + private JSONObject getPiiTestData() throws JSONException { + JSONObject piiTestData = new JSONObject(); + piiTestData.put(UNIFIED_CONFIG_PII_KEY, getPiiInternalData()); + return piiTestData; + } + + private JSONObject getPiiInternalData() throws JSONException { + JSONObject piiInternalData = new JSONObject(); + piiInternalData.put(ADVERTISING_TRACKING_ID_KEY, PII_STORAGE_IDFA); + return piiInternalData; + } + +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDecisionDataTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDecisionDataTest.java new file mode 100644 index 00000000..5b0ccbb4 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiDecisionDataTest.java @@ -0,0 +1,68 @@ +package com.unity3d.ads.test.instrumentation.services.core.device.pii; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_KEY; + +import com.unity3d.services.core.device.reader.pii.DataSelectorResult; +import com.unity3d.services.core.device.reader.pii.PiiDecisionData; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class PiiDecisionDataTest { + + @Test + public void testPiiDecisionData() { + PiiDecisionData piiDecisionData = new PiiDecisionData(DataSelectorResult.EXCLUDE, getOriginalDummyData()); + Assert.assertEquals(DataSelectorResult.EXCLUDE, piiDecisionData.getResultType()); + Assert.assertEquals(getOriginalDummyData(), piiDecisionData.getAttributes()); + } + + @Test + public void testPiiDecisionAppendData() { + PiiDecisionData piiDecisionData = new PiiDecisionData(DataSelectorResult.INCLUDE, getOriginalDummyData()); + piiDecisionData.appendData(new HashMap() {{ + put("key3", "value3"); + }}); + Assert.assertEquals(3, piiDecisionData.getAttributes().size()); + } + + @Test + public void testPiiDecisionUserNonBehaviouralFlagTrue() { + PiiDecisionData piiDecisionData = new PiiDecisionData(DataSelectorResult.EXCLUDE, getOriginalDummyDataWithNonBehavioralFlag(true)); + Assert.assertEquals(DataSelectorResult.EXCLUDE, piiDecisionData.getResultType()); + Assert.assertTrue(piiDecisionData.getUserNonBehavioralFlag()); + } + + @Test + public void testPiiDecisionUserNonBehaviouralFlagFalse() { + PiiDecisionData piiDecisionData = new PiiDecisionData(DataSelectorResult.EXCLUDE, getOriginalDummyDataWithNonBehavioralFlag(false)); + Assert.assertEquals(DataSelectorResult.EXCLUDE, piiDecisionData.getResultType()); + Assert.assertFalse(piiDecisionData.getUserNonBehavioralFlag()); + } + + @Test + public void testPiiDecisionUserNonBehaviouralFlagMissing() { + PiiDecisionData piiDecisionData = new PiiDecisionData(DataSelectorResult.EXCLUDE, getOriginalDummyData()); + Assert.assertEquals(DataSelectorResult.EXCLUDE, piiDecisionData.getResultType()); + Assert.assertNull(piiDecisionData.getUserNonBehavioralFlag()); + } + + private Map getOriginalDummyData() { + return new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + } + + private Map getOriginalDummyDataWithNonBehavioralFlag(boolean userNonBehavioralFlag) { + Map dataWithNonBehavioralFlag = getOriginalDummyData(); + dataWithNonBehavioralFlag.put(USER_NON_BEHAVIORAL_KEY, userNonBehavioralFlag); + return dataWithNonBehavioralFlag; + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiTrackingStatusReaderTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiTrackingStatusReaderTest.java new file mode 100644 index 00000000..13ae8961 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/device/pii/PiiTrackingStatusReaderTest.java @@ -0,0 +1,117 @@ +package com.unity3d.ads.test.instrumentation.services.core.device.pii; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.PRIVACY_MODE_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.PRIVACY_SPM_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_VALUE_ALT_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_VALUE_KEY; + +import com.unity3d.services.core.device.reader.pii.PiiPrivacyMode; +import com.unity3d.services.core.device.reader.pii.PiiTrackingStatusReader; +import com.unity3d.services.core.misc.IJsonStorageReader; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PiiTrackingStatusReaderTest { + private enum PrivacyStorageType { + SPM, + USER + } + + @Mock + private IJsonStorageReader jsonStorageReaderMock; + + @Test + public void testPiiTrackingStatusReaderNull() { + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(jsonStorageReaderMock); + PiiPrivacyMode privacyMode = piiTrackingStatusReader.getPrivacyMode(); + Assert.assertEquals(PiiPrivacyMode.NULL, privacyMode); + } + + @Test + public void testPiiTrackingStatusReaderSpmApp() { + setAndVerifyPrivacyMode("App", PrivacyStorageType.SPM); + } + + @Test + public void testPiiTrackingStatusReaderSpmNone() { + setAndVerifyPrivacyMode("None", PrivacyStorageType.SPM); + } + + @Test + public void testPiiTrackingStatusReaderSpmMixed() { + setAndVerifyPrivacyMode("Mixed", PrivacyStorageType.SPM); + } + + @Test + public void testPiiTrackingStatusReaderUserPrivacyApp() { + setAndVerifyPrivacyMode("App", PrivacyStorageType.USER); + } + + @Test + public void testPiiTrackingStatusReaderUserPrivacyNone() { + setAndVerifyPrivacyMode("None", PrivacyStorageType.USER); + } + + @Test + public void testPiiTrackingStatusReaderUserPrivacyMixed() { + setAndVerifyPrivacyMode("Mixed", PrivacyStorageType.USER); + } + + @Test + public void testPiiTrackingStatusReaderUserNonePrivacySpmMixed() { + Mockito.when(jsonStorageReaderMock.get(PRIVACY_SPM_KEY)).thenReturn("None"); + Mockito.when(jsonStorageReaderMock.get(PRIVACY_MODE_KEY)).thenReturn("Mixed"); + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(jsonStorageReaderMock); + PiiPrivacyMode privacyMode = piiTrackingStatusReader.getPrivacyMode(); + Assert.assertEquals(PiiPrivacyMode.MIXED, privacyMode); + } + + @Test + public void testPiiTrackingStatusReaderNonBehavioral() { + mockUserNonBehavioralFlagData(true, false); + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(jsonStorageReaderMock); + boolean nonBehavioralFlag = piiTrackingStatusReader.getUserNonBehavioralFlag(); + Assert.assertTrue(nonBehavioralFlag); + } + + @Test + public void testPiiTrackingStatusReaderNonBehavioralString() { + mockUserNonBehavioralFlagData("True", false); + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(jsonStorageReaderMock); + boolean nonBehavioralFlag = piiTrackingStatusReader.getUserNonBehavioralFlag(); + Assert.assertTrue(nonBehavioralFlag); + } + + @Test + public void testPiiTrackingStatusReaderNonBehavioralAlt() { + mockUserNonBehavioralFlagData(true, true); + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(jsonStorageReaderMock); + boolean nonBehavioralFlag = piiTrackingStatusReader.getUserNonBehavioralFlag(); + Assert.assertTrue(nonBehavioralFlag); + } + + @Test + public void testPiiTrackingStatusReaderNonBehavioralFalse() { + mockUserNonBehavioralFlagData(false, false); + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(jsonStorageReaderMock); + boolean nonBehavioralFlag = piiTrackingStatusReader.getUserNonBehavioralFlag(); + Assert.assertFalse(nonBehavioralFlag); + } + + private void setAndVerifyPrivacyMode(String privacyModeStr, PrivacyStorageType privacyType) { + Mockito.when(jsonStorageReaderMock.get(privacyType == PrivacyStorageType.SPM ? PRIVACY_SPM_KEY : PRIVACY_MODE_KEY)).thenReturn(privacyModeStr); + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(jsonStorageReaderMock); + PiiPrivacyMode privacyMode = piiTrackingStatusReader.getPrivacyMode(); + Assert.assertEquals(PiiPrivacyMode.getPiiPrivacyMode(privacyModeStr), privacyMode); + } + + private void mockUserNonBehavioralFlagData(Object nonBehavioral, boolean alternateKey) { + Mockito.when(jsonStorageReaderMock.get(alternateKey ? USER_NON_BEHAVIORAL_VALUE_ALT_KEY : USER_NON_BEHAVIORAL_VALUE_KEY)).thenReturn(nonBehavioral); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/misc/JsonFlattenerTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/misc/JsonFlattenerTest.java new file mode 100644 index 00000000..39eca5b9 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/misc/JsonFlattenerTest.java @@ -0,0 +1,49 @@ +package com.unity3d.ads.test.instrumentation.services.core.misc; + +import com.unity3d.services.core.misc.JsonFlattener; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +public class JsonFlattenerTest { + + @Test + public void testJsonFlattenerSimple() throws JSONException { + JSONObject jsonObject = new JSONObject("{\"mediation\":{\"ordinal\":{\"value\":1,\"ts\":1642792098742}}}"); + JsonFlattener jsonFlattener = new JsonFlattener(jsonObject); + JSONObject result = jsonFlattener.flattenJson(".", Collections.singletonList("mediation"), Collections.singletonList("value"), Collections.singletonList("ts")); + Assert.assertEquals(1, result.get("mediation.ordinal")); + Assert.assertEquals("{\"mediation.ordinal\":1}", result.toString()); + } + + @Test + public void testJsonFlattenerComplex() throws JSONException { + JSONObject jsonObject = new JSONObject("{\"configuration\":{\"hasInitialized\":true},\"analytics\":{\"appstartinfo\":{\"latestUTCTransactionDate\":{\"year\":2022,\"month\":0,\"day\":21},\"todayCount\":5,\"totalCount\":5\"},\"installhour\":1642780800000,\"userid\":\"247bd7e7fe1849a6b92eeeb211679fdc\",\"sessionid\":199406426711996,\"appversion\":\"4.0.0\",\"osversion\":\"12\"},\"user\":{\"requestCount\":14,\"requestToReadyTime\":1035,\"hasSentAvailabilityFeatures\":true}}"); + JsonFlattener jsonFlattener = new JsonFlattener(jsonObject); + JSONObject result = jsonFlattener.flattenJson(".", Collections.singletonList("configuration"), Collections.singletonList(""), Collections.singletonList("")); + Assert.assertEquals("{\"configuration.hasInitialized\":true}", result.toString()); + } + + @Test + public void testJsonFlattenerMultipleKeys() throws JSONException { + JSONObject jsonObject = new JSONObject("{\"mediation\":{\"ordinal\":{\"value\":1,\"ts\":1642792098742}},\"configuration\":{\"hasInitialized\":true},\"analytics\":{\"appstartinfo\":{\"latestUTCTransactionDate\":{\"year\":2022,\"month\":0,\"day\":21},\"todayCount\":5,\"totalCount\":5\"},\"installhour\":1642780800000,\"userid\":\"247bd7e7fe1849a6b92eeeb211679fdc\",\"sessionid\":199406426711996,\"appversion\":\"4.0.0\",\"osversion\":\"12\"},\"user\":{\"requestCount\":14,\"requestToReadyTime\":1035,\"hasSentAvailabilityFeatures\":true}}"); + JsonFlattener jsonFlattener = new JsonFlattener(jsonObject); + JSONObject result = jsonFlattener.flattenJson(".", Arrays.asList("mediation", "configuration"), Collections.singletonList("value"), Collections.singletonList("ts")); + Assert.assertEquals(1, result.get("mediation.ordinal")); + Assert.assertEquals(true, result.get("configuration.hasInitialized")); + Assert.assertEquals("{\"mediation.ordinal\":1,\"configuration.hasInitialized\":true}", result.toString()); + } + + @Test + public void testJsonFlattenerWithCompleteSample() throws JSONException { + JSONObject jsonObject = new JSONObject("{\"configuration\":{\"hasInitialized\":true},\"analytics\":{\"appstartinfo\":\"{\\\"latestUTCTransactionDate\\\":{\\\"year\\\":2022,\\\"month\\\":0,\\\"day\\\":21},\\\"todayCount\\\":4,\\\"totalCount\\\":4}\",\"installhour\":1642780800000,\"userid\":\"cdae1fdb677748efbf8d000f149618a2\",\"sessionid\":4562428049642,\"appversion\":\"4.0.0\",\"osversion\":\"11\"},\"user\":{\"requestCount\":8,\"requestToReadyTime\":946,\"hasSentAvailabilityFeatures\":true},\"cache\":{\"files\":{\"8daf1c6ed98f3a5e611405014f4ffbc74b34ba6cc9f88a0c32eab5b534aaa940\":{\"fullyDownloaded\":true,\"size\":1246574,\"totalSize\":1246574,\"extension\":\"webm\"},\"be8344e28eda78a17a7c72b0ce828d5da57e5ab8871efc3c0284fed05a69e340\":{\"fullyDownloaded\":true,\"size\":1927,\"totalSize\":-1,\"extension\":\"png\"},\"8439a0eb930900d782d6cec1d4cec7d76f320f3038bf8140c1829f947aa745fe\":{\"fullyDownloaded\":true,\"size\":75544,\"totalSize\":75544,\"extension\":\"jpg\"},\"75d9d06ab6399c8970077567e5919ac64ddab28ae4cc713b88c7925427c56dad\":{\"fullyDownloaded\":true,\"size\":763494,\"totalSize\":763494,\"extension\":\"webm\"},\"72ba0b2698f65fe52e41863bc17a6fe237c4e1b93eb1bc28fa1c51ef917cccf4\":{\"fullyDownloaded\":true,\"size\":8397,\"totalSize\":-1,\"extension\":\"jpg\"},\"0f7755520aa8d6f4cfea157c16cd9253901ce2ce3498b68052c24cdae9da9fa7\":{\"fullyDownloaded\":true,\"size\":181464,\"totalSize\":181464,\"extension\":\"png\"},\"3e911d142fb6cae824047ab650191c912984e0d28e745f70632b3842b0ff3107\":{\"fullyDownloaded\":true,\"size\":3378505,\"totalSize\":3378505,\"extension\":\"webm\"},\"908775f950b9c73d28897f89ee975d44c965b03b301d30e4bd90aab923fde0e5\":{\"fullyDownloaded\":true,\"size\":63630,\"totalSize\":-1,\"extension\":\"png\"},\"b7b1e91e658151c123f09a97cffa8c6d0993f1aa995a105254ca6484f0b8b9b9\":{\"fullyDownloaded\":true,\"size\":140280,\"totalSize\":140280,\"extension\":\"jpg\"},\"4b388ee9c22af10a6e431212294b582b0446cd75dbc4de90797202a054bcb362\":{\"fullyDownloaded\":true,\"size\":114113,\"totalSize\":114113,\"extension\":\"jpg\"},\"8453b617f78aafbd5e2b5d4645a802025a5a5d77ef5c7a733f5bf271f0debe00\":{\"fullyDownloaded\":true,\"size\":134924,\"totalSize\":134924,\"extension\":\"mp4\"},\"54b8286986daadccca34640c2dfe2f3cd3caaed40d475b3976552ea40e5be59d\":{\"fullyDownloaded\":true,\"size\":7216,\"totalSize\":7216,\"extension\":\"png\"},\"053fda185f385334924fec869bf7d58823feae55aa90090eb5cf80ad7e3d6cb1\":{\"fullyDownloaded\":true,\"size\":134174,\"totalSize\":134174,\"extension\":\"png\"},\"c903f40f6a87c083a9deb3302d3020e5fa4e88e8bb67c48b0577c3e4933c454d\":{\"fullyDownloaded\":true,\"size\":105591,\"totalSize\":105591,\"extension\":\"png\"}},\"campaigns\":{\"61deb306ff039da35a7a6a0d-6194d17998127d145cc1e8e4\":{\"8daf1c6ed98f3a5e611405014f4ffbc74b34ba6cc9f88a0c32eab5b534aaa940\":{\"extension\":\"webm\"}},\"61e3c375f1950b7abd849c81-61dfa77c532dfc930cdd56be\":{\"75d9d06ab6399c8970077567e5919ac64ddab28ae4cc713b88c7925427c56dad\":{\"extension\":\"webm\"}},\"5c6adee1a4f58800185757b0-61bb5b4e255c8e7ae26e4a24\":{\"3e911d142fb6cae824047ab650191c912984e0d28e745f70632b3842b0ff3107\":{\"extension\":\"webm\"}},\"000000000000000000000000\":{\"8453b617f78aafbd5e2b5d4645a802025a5a5d77ef5c7a733f5bf271f0debe00\":{\"extension\":\"mp4\"}}}},\"session\":{\"46e78c9a-278f-49a2-9509-1ab8a9814ab3\":{\"ts\":1642784021215},\"f54b72c4-6f2c-422d-ae0d-b474e019be88\":{\"ts\":1642784038564},\"1ea54b76-8867-4fd9-9534-937f00d8fa9b\":{\"ts\":1642784072042},\"282c4e09-8192-47e6-b638-dc1754bbd71e\":{\"ts\":1642784155396},\"f87b0dec-cd32-40b2-b671-c03ff4a2f435\":{\"ts\":1642784519094},\"ba8b86f4-897f-4c4d-b6b2-6d3b89f51b11\":{\"ts\":1642784758483},\"4121f536-ecc5-474d-9c6d-3fe8258d3538\":{\"ts\":1642784774885},\"783a16e4-c95f-47e4-98fb-b5f58630c1df\":{\"ts\":1642784866453}},\"unity\":{\"privacy\":{\"permissions\":{\"ads\":true,\"external\":true,\"gameExp\":true}}}}"); + JsonFlattener jsonFlattener = new JsonFlattener(jsonObject); + JSONObject result = jsonFlattener.flattenJson(".", Arrays.asList("configuration", "unity"), Collections.singletonList("value"), Collections.singletonList("ts")); + Assert.assertEquals(true, result.get("configuration.hasInitialized")); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/webview/WebViewUrlBuilderTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/webview/WebViewUrlBuilderTest.java new file mode 100644 index 00000000..71977e65 --- /dev/null +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/instrumentation/services/core/webview/WebViewUrlBuilderTest.java @@ -0,0 +1,64 @@ +package com.unity3d.ads.test.instrumentation.services.core.webview; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.unity3d.services.core.configuration.Configuration; +import com.unity3d.services.core.configuration.Experiments; +import com.unity3d.services.core.webview.WebViewUrlBuilder; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +@RunWith(AndroidJUnit4.class) +public class WebViewUrlBuilderTest { + private final static String TEST_BASE_URL = "file://testbase"; + + @Test + public void testWebViewUrlBuilder() throws JSONException { + Configuration configMock = Mockito.mock(Configuration.class); + Mockito.when(configMock.getExperiments()).thenReturn(new Experiments(new JSONObject("{\"fff\":true}"))); + WebViewUrlBuilder webViewUrlBuilder = new WebViewUrlBuilder(TEST_BASE_URL, configMock); + String result = webViewUrlBuilder.getUrlWithQueryString(); + Assert.assertEquals(TEST_BASE_URL + "?platform=android&experiments=%7B%22fff%22%3Atrue%7D", result); + } + + @Test + public void testWebViewUrlBuilderWithoutExperiments() { + Configuration configMock = Mockito.mock(Configuration.class); + Mockito.when(configMock.getExperiments()).thenReturn(null); + WebViewUrlBuilder webViewUrlBuilder = new WebViewUrlBuilder(TEST_BASE_URL, configMock); + String result = webViewUrlBuilder.getUrlWithQueryString(); + Assert.assertEquals(TEST_BASE_URL + "?platform=android", result); + } + + @Test + public void testWebViewUrlBuilderWithExperimentsButFeatureDisabled() throws JSONException { + Configuration configMock = Mockito.mock(Configuration.class); + Mockito.when(configMock.getExperiments()).thenReturn(new Experiments(new JSONObject("{\"fff\":false, \"tsi\":true}"))); + WebViewUrlBuilder webViewUrlBuilder = new WebViewUrlBuilder(TEST_BASE_URL, configMock); + String result = webViewUrlBuilder.getUrlWithQueryString(); + Assert.assertEquals(TEST_BASE_URL + "?platform=android", result); + } + + @Test + public void testWebViewUrlBuilderWithWebviewOrigin() { + Configuration configMock = Mockito.mock(Configuration.class); + Mockito.when(configMock.getWebViewUrl()).thenReturn("someplace.html"); + WebViewUrlBuilder webViewUrlBuilder = new WebViewUrlBuilder(TEST_BASE_URL, configMock); + String result = webViewUrlBuilder.getUrlWithQueryString(); + Assert.assertEquals(TEST_BASE_URL + "?platform=android&origin=someplace.html", result); + } + + @Test + public void testWebViewUrlBuilderWithWebviewVersion() { + Configuration configMock = Mockito.mock(Configuration.class); + Mockito.when(configMock.getWebViewVersion()).thenReturn("1.0"); + WebViewUrlBuilder webViewUrlBuilder = new WebViewUrlBuilder(TEST_BASE_URL, configMock); + String result = webViewUrlBuilder.getUrlWithQueryString(); + Assert.assertEquals(TEST_BASE_URL + "?platform=android&version=1.0", result); + } +} diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/integration/SDKMetricsTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/integration/SDKMetricsTest.java index 40927f20..0b5af1db 100644 --- a/unity-ads/src/androidTest/java/com/unity3d/ads/test/integration/SDKMetricsTest.java +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/integration/SDKMetricsTest.java @@ -1,8 +1,8 @@ package com.unity3d.ads.test.integration; import com.unity3d.services.core.configuration.Configuration; -import com.unity3d.services.core.request.ISDKMetrics; -import com.unity3d.services.core.request.SDKMetrics; +import com.unity3d.services.core.request.metrics.ISDKMetrics; +import com.unity3d.services.core.request.metrics.SDKMetrics; import org.json.JSONObject; import org.junit.Test; diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/InitializeThreadTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/InitializeThreadTest.java index abb011e2..7663c8b7 100644 --- a/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/InitializeThreadTest.java +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/InitializeThreadTest.java @@ -112,26 +112,6 @@ public void testInitializeStateLoadConfigFile() throws Exception { assertEquals("No fill timeout not properly overridden", 6, configuration.getWebViewBridgeTimeout()); } - @Test - public void testInitializeStateLoadConfigFileWrongSdkVersion() throws Exception { - String filePath = SdkProperties.getLocalConfigurationFilepath(); - JSONObject json = getJSONObject("fake-url", "fake-hash", "fake-version"); - json.put("mr", 1); - json.put("sdkv", "fake-sdkv"); - Configuration initConfig = new Configuration(json); - boolean fileWritten = Utilities.writeFile(new File(filePath), initConfig.getJSONString()); - assertTrue("File was not written properly", fileWritten); - - InitializeThread.InitializeStateLoadConfigFile state = new InitializeThread.InitializeStateLoadConfigFile(new Configuration()); - Object nextState = state.execute(); - - assertTrue("Next state is not InitializeStateReset", nextState instanceof InitializeThread.InitializeStateReset); - - Configuration configuration = ((InitializeThread.InitializeStateReset)nextState).getConfiguration(); - assertNotEquals("Max Retries should equal the default value", 1, configuration.getMaxRetries()); - assertNotEquals("SDKVersion should equal the default value", "fake-sdkv", configuration.getSdkVersion()); - } - @Test public void testInitializeStateLoadConfigFileNoConfigExists() { String filePath = SdkProperties.getLocalConfigurationFilepath(); diff --git a/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/SdkPropertiesTest.java b/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/SdkPropertiesTest.java index 076b92e9..2081a453 100644 --- a/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/SdkPropertiesTest.java +++ b/unity-ads/src/androidTest/java/com/unity3d/ads/test/legacy/SdkPropertiesTest.java @@ -20,6 +20,12 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; @RunWith(AndroidJUnit4.class) public class SdkPropertiesTest { @@ -245,4 +251,73 @@ public void testNotifyInitializationFailed() { assertEquals(SdkProperties.InitializationState.INITIALIZED_FAILED, SdkProperties.getCurrentInitializationState()); Mockito.verify(initializationListener, times(1)).onInitializationFailed(UnityAds.UnityAdsInitializationError.INTERNAL_ERROR, "Sdk failed to initialize."); } + + @Test + public void testConfigVersionWithNullContext() { + String actual = SdkProperties.getConfigVersion(null); + + assertEquals("configv2", actual); + } + + @Test + public void testConfigVersionWithException() throws PackageManager.NameNotFoundException { + Context context = Mockito.mock(Context.class); + PackageManager packageManager = Mockito.mock(PackageManager.class); + + when(context.getPackageManager()).thenReturn(packageManager); + when(context.getPackageName()).thenReturn("com.test.app"); + when(packageManager.getApplicationInfo("com.test.app", PackageManager.GET_META_DATA)).thenThrow(new PackageManager.NameNotFoundException()); + + String actual = SdkProperties.getConfigVersion(context); + + assertEquals("configv2", actual); + } + + @Test + public void testConfigVersionBundleNull() throws PackageManager.NameNotFoundException { + Context context = Mockito.mock(Context.class); + PackageManager packageManager = Mockito.mock(PackageManager.class); + ApplicationInfo applicationInfo = new ApplicationInfo(); + + when(context.getPackageManager()).thenReturn(packageManager); + when(context.getPackageName()).thenReturn("com.test.app"); + when(packageManager.getApplicationInfo("com.test.app", PackageManager.GET_META_DATA)).thenReturn(applicationInfo); + + String actual = SdkProperties.getConfigVersion(context); + + assertEquals("configv2", actual); + } + + @Test + public void testConfigVersionNoValueInBundle() throws PackageManager.NameNotFoundException { + Context context = Mockito.mock(Context.class); + PackageManager packageManager = Mockito.mock(PackageManager.class); + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.metaData = new Bundle(); + + when(context.getPackageManager()).thenReturn(packageManager); + when(context.getPackageName()).thenReturn("com.test.app"); + when(packageManager.getApplicationInfo("com.test.app", PackageManager.GET_META_DATA)).thenReturn(applicationInfo); + + String actual = SdkProperties.getConfigVersion(context); + + assertEquals("configv2", actual); + } + + @Test + public void testConfigVersionWithValueInBundle() throws PackageManager.NameNotFoundException { + Context context = Mockito.mock(Context.class); + PackageManager packageManager = Mockito.mock(PackageManager.class); + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.metaData = new Bundle(); + applicationInfo.metaData.putString("com.unity3d.ads.configversion", "testConfig"); + + when(context.getPackageManager()).thenReturn(packageManager); + when(context.getPackageName()).thenReturn("com.test.app"); + when(packageManager.getApplicationInfo("com.test.app", PackageManager.GET_META_DATA)).thenReturn(applicationInfo); + + String actual = SdkProperties.getConfigVersion(context); + + assertEquals("testConfig", actual); + } } diff --git a/unity-ads/src/main/AndroidManifest.xml b/unity-ads/src/main/AndroidManifest.xml index 8cce5a9d..011a3024 100644 --- a/unity-ads/src/main/AndroidManifest.xml +++ b/unity-ads/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + (){{ + _sdkMetricSender.sendSDKMetricEventWithTag(SDKMetricEvents.native_load_callback_error, new HashMap(){{ put("cbs", cbs); }}); @@ -69,7 +68,7 @@ public void onFailure(String message, CallbackStatus callbackStatus) { @Override public void onTimeout() { sendOnUnityAdsFailedToLoad(state, UnityAds.UnityAdsLoadError.INTERNAL_ERROR, errorMsgInternalCommunicationTimeout); - getMetricSender().SendSDKMetricEvent(SDKMetricEvents.native_load_callback_timeout); + getMetricSender().sendSDKMetricEvent(SDKMetricEvents.native_load_callback_timeout); remove(state.id); } })); diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecorator.java b/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecorator.java index 9d7ac955..88c45f6d 100644 --- a/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecorator.java +++ b/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecorator.java @@ -1,7 +1,7 @@ package com.unity3d.services.ads.operation.load; import com.unity3d.ads.UnityAds; -import com.unity3d.services.core.request.ISDKMetricSender; +import com.unity3d.services.core.request.metrics.ISDKMetricSender; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; public class LoadModuleDecorator implements ILoadModule { diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecoratorTimeout.java b/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecoratorTimeout.java index b7c6bc01..8caeb409 100644 --- a/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecoratorTimeout.java +++ b/unity-ads/src/main/java/com/unity3d/services/ads/operation/load/LoadModuleDecoratorTimeout.java @@ -3,7 +3,7 @@ import android.os.ConditionVariable; import com.unity3d.ads.UnityAds; -import com.unity3d.services.core.request.SDKMetricEvents; +import com.unity3d.services.core.request.metrics.SDKMetricEvents; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; import java.util.concurrent.ExecutorService; @@ -61,6 +61,6 @@ private void releaseOperationTimeoutLock(String operationId) { private void onOperationTimeout(final LoadOperationState state) { remove(state.id); state.listener.onUnityAdsFailedToLoad(state.placementId, UnityAds.UnityAdsLoadError.TIMEOUT, errorMsgTimeoutLoading + state.placementId); - getMetricSender().SendSDKMetricEvent(SDKMetricEvents.native_load_timeout_error); + getMetricSender().sendSDKMetricEvent(SDKMetricEvents.native_load_timeout_error); } } diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModule.java b/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModule.java index b59af5e1..90b525c9 100644 --- a/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModule.java +++ b/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModule.java @@ -11,14 +11,13 @@ import com.unity3d.services.core.device.Device; import com.unity3d.services.core.misc.Utilities; import com.unity3d.services.core.properties.ClientProperties; -import com.unity3d.services.core.request.ISDKMetricSender; -import com.unity3d.services.core.request.SDKMetricEvents; -import com.unity3d.services.core.request.SDKMetricSender; +import com.unity3d.services.core.request.metrics.ISDKMetricSender; +import com.unity3d.services.core.request.metrics.SDKMetricEvents; +import com.unity3d.services.core.request.metrics.SDKMetricSender; import com.unity3d.services.core.webview.bridge.CallbackStatus; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; import com.unity3d.services.core.webview.bridge.invocation.IWebViewBridgeInvocationCallback; import com.unity3d.services.core.webview.bridge.invocation.WebViewBridgeInvocation; -import com.unity3d.services.core.webview.bridge.invocation.WebViewBridgeInvocationSingleThreadedExecutor; import org.json.JSONException; import org.json.JSONObject; @@ -56,7 +55,7 @@ public void onFailure(String message, CallbackStatus callbackStatus) { sendOnUnityAdsFailedToShow(state, message, UnityAds.UnityAdsShowError.INTERNAL_ERROR); final String cbs = callbackStatus == null ? "invocationFailure" : callbackStatus.toString(); - _sdkMetricSender.SendSDKMetricEventWithTag(SDKMetricEvents.native_show_callback_error, new HashMap(){{ + _sdkMetricSender.sendSDKMetricEventWithTag(SDKMetricEvents.native_show_callback_error, new HashMap(){{ put("cbs", cbs); }}); @@ -65,7 +64,7 @@ public void onFailure(String message, CallbackStatus callbackStatus) { @Override public void onTimeout() { sendOnUnityAdsFailedToShow(state, "[UnityAds] Show Invocation Timeout", UnityAds.UnityAdsShowError.INTERNAL_ERROR); - _sdkMetricSender.SendSDKMetricEvent(SDKMetricEvents.native_show_callback_timeout); + _sdkMetricSender.sendSDKMetricEvent(SDKMetricEvents.native_show_callback_timeout); remove(state.id); } })); diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecorator.java b/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecorator.java index 71fecc70..d0b625f8 100644 --- a/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecorator.java +++ b/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecorator.java @@ -1,7 +1,7 @@ package com.unity3d.services.ads.operation.show; import com.unity3d.ads.UnityAds; -import com.unity3d.services.core.request.ISDKMetricSender; +import com.unity3d.services.core.request.metrics.ISDKMetricSender; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; public class ShowModuleDecorator implements IShowModule { diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecoratorTimeout.java b/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecoratorTimeout.java index 13244c9a..e9140a5b 100644 --- a/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecoratorTimeout.java +++ b/unity-ads/src/main/java/com/unity3d/services/ads/operation/show/ShowModuleDecoratorTimeout.java @@ -3,7 +3,7 @@ import android.os.ConditionVariable; import com.unity3d.ads.UnityAds; -import com.unity3d.services.core.request.SDKMetricEvents; +import com.unity3d.services.core.request.metrics.SDKMetricEvents; import com.unity3d.services.core.webview.bridge.IWebViewBridgeInvoker; import java.util.concurrent.ExecutorService; @@ -67,6 +67,6 @@ private void releaseOperationTimeoutLock(String operationId) { private void onOperationTimeout(final ShowOperationState state, UnityAds.UnityAdsShowError error, String message) { remove(state.id); state.listener.onUnityAdsShowFailure(state.placementId, error, message); - getMetricSender().SendSDKMetricEvent(SDKMetricEvents.native_show_timeout_error); + getMetricSender().sendSDKMetricEvent(SDKMetricEvents.native_show_timeout_error); } } diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/token/AsyncTokenStorage.java b/unity-ads/src/main/java/com/unity3d/services/ads/token/AsyncTokenStorage.java new file mode 100644 index 00000000..4a166ba5 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/ads/token/AsyncTokenStorage.java @@ -0,0 +1,212 @@ +package com.unity3d.services.ads.token; + +import static com.unity3d.services.core.device.TokenType.*; + +import android.os.Handler; +import android.os.Looper; + +import com.unity3d.ads.IUnityAdsTokenListener; +import com.unity3d.services.core.configuration.Configuration; +import com.unity3d.services.core.configuration.ConfigurationReader; +import com.unity3d.services.core.device.TokenType; +import com.unity3d.services.core.device.reader.DeviceInfoReaderBuilder; +import com.unity3d.services.core.log.DeviceLog; +import com.unity3d.services.core.properties.InitializationStatusReader; +import com.unity3d.services.core.properties.SdkProperties; +import com.unity3d.services.core.request.metrics.SDKMetrics; +import com.unity3d.services.core.request.metrics.TSIMetric; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; + +public class AsyncTokenStorage { + private final List _tokenListeners = new LinkedList<>(); + private final Handler _handler; + private boolean _tokenAvailable = false; + private boolean _configurationWasSet = false; + private Configuration _configuration = new Configuration(); + private INativeTokenGenerator _nativeTokenGenerator; + private final InitializationStatusReader _initStatusReader = new InitializationStatusReader(); + + private static AsyncTokenStorage _instance; + + public static AsyncTokenStorage getInstance() { + if (_instance == null) { + _instance = new AsyncTokenStorage( + null, + new Handler(Looper.getMainLooper())); + } + return _instance; + } + + class TokenListenerState + { + public IUnityAdsTokenListener listener; + public Runnable runnable; + public boolean invoked; + } + + public AsyncTokenStorage(INativeTokenGenerator nativeTokenGenerator, Handler handler) { + _handler = handler; + _nativeTokenGenerator = nativeTokenGenerator; + } + + public synchronized void setConfiguration(Configuration configuration) { + _configuration = configuration; + _configurationWasSet = isValidConfig(_configuration); + + if (!_configurationWasSet) { + return; + } + + if (_nativeTokenGenerator == null) { + _nativeTokenGenerator = new NativeTokenGenerator(Executors.newSingleThreadExecutor(), new DeviceInfoReaderBuilder(new ConfigurationReader()).build()); + } + + ArrayList tempList = new ArrayList<>(_tokenListeners); + for(TokenListenerState state: tempList) { + handleTokenInvocation(state); + } + } + + public synchronized void onTokenAvailable(TokenType type) { + _tokenAvailable = true; + + if (!_configurationWasSet) { + return; + } + + notifyListenersTokenReady(type); + } + + public synchronized void getToken(IUnityAdsTokenListener listener) { + if (SdkProperties.getCurrentInitializationState() == SdkProperties.InitializationState.INITIALIZED_FAILED) { + listener.onUnityAdsTokenReady(null); + sendTokenMetrics(null, TOKEN_REMOTE); + return; + } + + if (SdkProperties.getCurrentInitializationState() == SdkProperties.InitializationState.NOT_INITIALIZED) { + listener.onUnityAdsTokenReady(null); + sendTokenMetrics(null, TOKEN_REMOTE); + return; + } + + final AsyncTokenStorage.TokenListenerState state = addTimeoutHandler(listener); + + if (!_configurationWasSet) { + return; + } + + handleTokenInvocation(state); + } + + private synchronized AsyncTokenStorage.TokenListenerState addTimeoutHandler(IUnityAdsTokenListener listener) { + final AsyncTokenStorage.TokenListenerState state = new AsyncTokenStorage.TokenListenerState(); + state.listener = listener; + state.runnable = new Runnable() { + @Override + public void run() { + notifyTokenReady(state, null, TOKEN_NATIVE); + } + }; + + _tokenListeners.add(state); + _handler.postDelayed(state.runnable, _configuration.getTokenTimeout()); + + return state; + } + + private synchronized void notifyListenersTokenReady(TokenType type) { + while(!_tokenListeners.isEmpty()) { + String token = TokenStorage.getToken(); + + if (token == null) { + break; + } + + notifyTokenReady(_tokenListeners.get(0), token, type); + } + } + + private void handleTokenInvocation(final AsyncTokenStorage.TokenListenerState state) { + if (state.invoked) { + return; + } + state.invoked = true; + + if (!_tokenAvailable && _configuration.getExperiments().isNativeTokenEnabled()) { + _nativeTokenGenerator.generateToken(new INativeTokenGeneratorListener() { + @Override + public void onReady(final String token) { + _handler.post(new Runnable() { + @Override + public void run() { + notifyTokenReady(state, token, TOKEN_NATIVE); + } + }); + } + }); + } else { + String token = TokenStorage.getToken(); + + if (token == null) { + return; + } + + notifyTokenReady(state, token, TOKEN_REMOTE); + } + } + + private synchronized void notifyTokenReady(AsyncTokenStorage.TokenListenerState state, String token, TokenType type) { + if (_tokenListeners.remove(state)) { + state.listener.onUnityAdsTokenReady(token); + try { + _handler.removeCallbacks(state.runnable); + } catch (Exception ex) { + DeviceLog.exception("Failed to remove callback from a handler", ex); + } + } + sendTokenMetrics(token, type); + } + + private void sendTokenMetrics(String token, TokenType type) { + switch (type) { + case TOKEN_NATIVE: + sendNativeTokenMetrics(token); + break; + case TOKEN_REMOTE: + sendRemoteTokenMetrics(token); + break; + default: + DeviceLog.error("Unknown token type passed to sendTokenMetrics"); + } + } + + private void sendNativeTokenMetrics(String token) { + if (token == null) { + SDKMetrics.getInstance().sendMetric(TSIMetric.newNativeGeneratedTokenNull(getMetricTags())); + } else { + SDKMetrics.getInstance().sendMetric(TSIMetric.newNativeGeneratedTokenAvailable(getMetricTags())); + } + } + + private void sendRemoteTokenMetrics(String token) { + if (token == null) { + SDKMetrics.getInstance().sendMetric(TSIMetric.newAsyncTokenNull(getMetricTags())); + } + } + + private Map getMetricTags() { + Map tags = _configuration.getMetricTags(); + tags.put("state", _initStatusReader.getInitializationStateString(SdkProperties.getCurrentInitializationState())); + return tags; + } + + private boolean isValidConfig(Configuration configuration) { + return configuration != null && configuration.getExperiments() != null; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGenerator.java b/unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGenerator.java new file mode 100644 index 00000000..9c6227ca --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGenerator.java @@ -0,0 +1,5 @@ +package com.unity3d.services.ads.token; + +public interface INativeTokenGenerator { + void generateToken(final INativeTokenGeneratorListener callback); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGeneratorListener.java b/unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGeneratorListener.java new file mode 100644 index 00000000..e9ae1836 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/ads/token/INativeTokenGeneratorListener.java @@ -0,0 +1,5 @@ +package com.unity3d.services.ads.token; + +public interface INativeTokenGeneratorListener { + void onReady(String token); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/token/NativeTokenGenerator.java b/unity-ads/src/main/java/com/unity3d/services/ads/token/NativeTokenGenerator.java new file mode 100644 index 00000000..8a0ed55c --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/ads/token/NativeTokenGenerator.java @@ -0,0 +1,39 @@ +package com.unity3d.services.ads.token; + +import android.util.Base64; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderCompressor; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; +import com.unity3d.services.core.log.DeviceLog; + +import java.util.concurrent.ExecutorService; + +public class NativeTokenGenerator implements INativeTokenGenerator { + private ExecutorService _executorService; + private IDeviceInfoReader _deviceInfoReader; + + public NativeTokenGenerator(ExecutorService executorService, IDeviceInfoReader deviceInfoReader) { + _executorService = executorService; + _deviceInfoReader = deviceInfoReader; + } + + public void generateToken(final INativeTokenGeneratorListener callback) { + _executorService.execute(new Runnable() { + @Override + public void run() { + try { + String queryData = Base64.encodeToString(new DeviceInfoReaderCompressor(_deviceInfoReader).getDeviceData(), Base64.NO_WRAP); + + StringBuilder stringBuilder = new StringBuilder(2 + queryData.length()); + stringBuilder.append("1:"); + stringBuilder.append(queryData); + + callback.onReady(stringBuilder.toString()); + } catch (Exception e) { + DeviceLog.exception("Unity Ads failed to generate token.", e); + callback.onReady(null); + } + } + }); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/ads/token/TokenStorage.java b/unity-ads/src/main/java/com/unity3d/services/ads/token/TokenStorage.java index f90a6823..036f96fc 100644 --- a/unity-ads/src/main/java/com/unity3d/services/ads/token/TokenStorage.java +++ b/unity-ads/src/main/java/com/unity3d/services/ads/token/TokenStorage.java @@ -1,5 +1,12 @@ package com.unity3d.services.ads.token; +import static com.unity3d.services.core.device.TokenType.TOKEN_REMOTE; +import static com.unity3d.services.core.device.TokenType.TOKEN_NATIVE; + +import android.text.TextUtils; + +import com.unity3d.services.core.configuration.InitializeEventsMetricSender; +import com.unity3d.services.core.device.TokenType; import com.unity3d.services.core.webview.WebViewApp; import com.unity3d.services.core.webview.WebViewEventCategory; @@ -12,6 +19,7 @@ public class TokenStorage { private static ConcurrentLinkedQueue _queue; private static int _accessCounter = 0; private static boolean _peekMode = false; + private static String _initToken = null; public synchronized static void createTokens(JSONArray tokens) throws JSONException { _queue = new ConcurrentLinkedQueue(); @@ -20,6 +28,11 @@ public synchronized static void createTokens(JSONArray tokens) throws JSONExcept for(int i = 0; i < tokens.length(); i++) { _queue.add(tokens.getString(i)); } + + if (!_queue.isEmpty()) { + triggerTokenAvailable(); + AsyncTokenStorage.getInstance().onTokenAvailable(TOKEN_REMOTE); + } } public synchronized static void appendTokens(JSONArray tokens) throws JSONException { @@ -31,6 +44,11 @@ public synchronized static void appendTokens(JSONArray tokens) throws JSONExcept for(int i = 0; i < tokens.length(); i++) { _queue.add(tokens.getString(i)); } + + if (!_queue.isEmpty()) { + triggerTokenAvailable(); + AsyncTokenStorage.getInstance().onTokenAvailable(TOKEN_REMOTE); + } } public synchronized static void deleteTokens() { @@ -40,7 +58,7 @@ public synchronized static void deleteTokens() { public synchronized static String getToken() { if(_queue == null) { - return null; + return _initToken; } if(_queue.isEmpty()) { @@ -58,4 +76,22 @@ public synchronized static String getToken() { public synchronized static void setPeekMode(boolean mode) { _peekMode = mode; } + + public synchronized static void setInitToken(String value) { + _initToken = value; + + if (_initToken != null) { + AsyncTokenStorage.getInstance().onTokenAvailable(TOKEN_REMOTE); + triggerTokenAvailable(); + } + } + + private static void triggerTokenAvailable() { + if (!TextUtils.isEmpty(_initToken)) { + InitializeEventsMetricSender.getInstance().sdkTokenDidBecomeAvailableWithConfig(true); + } else if (_queue != null && _queue.size() > 0) { + InitializeEventsMetricSender.getInstance().sdkTokenDidBecomeAvailableWithConfig(false); + } + } + } diff --git a/unity-ads/src/main/java/com/unity3d/services/core/api/DeviceInfo.java b/unity-ads/src/main/java/com/unity3d/services/core/api/DeviceInfo.java index c850915e..7e2f7caf 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/api/DeviceInfo.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/api/DeviceInfo.java @@ -111,15 +111,7 @@ public static void getTimeZoneOffset(WebViewCallback callback) { @WebViewExposed public static void getConnectionType(WebViewCallback callback) { - String connectionType; - if(Device.isUsingWifi()) { - connectionType = "wifi"; - } else if(Device.isActiveNetworkConnected()) { - connectionType = "cellular"; - } else { - connectionType = "none"; - } - callback.invoke(connectionType); + callback.invoke(Device.getConnectionType()); } @WebViewExposed diff --git a/unity-ads/src/main/java/com/unity3d/services/core/api/Sdk.java b/unity-ads/src/main/java/com/unity3d/services/core/api/Sdk.java index 089da92d..5a48008c 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/api/Sdk.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/api/Sdk.java @@ -33,7 +33,8 @@ public static void loadComplete(WebViewCallback callback) { // perPlacementLoadEnabled is now always true true, SdkProperties.getLatestConfiguration() != null, - Device.getElapsedRealtime() + Device.getElapsedRealtime(), + WebViewApp.getCurrentApp().getConfiguration().getStateId() }; callback.invoke(parameters); @@ -44,7 +45,6 @@ public static void initComplete(WebViewCallback callback) { DeviceLog.debug("Web Application initialized"); SdkProperties.setInitialized(true); WebViewApp.getCurrentApp().setWebAppInitialized(true); - callback.invoke(); } diff --git a/unity-ads/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java b/unity-ads/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java index f5770650..7188d72b 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java @@ -22,7 +22,7 @@ public CacheDirectory(String cacheDirName) { _cacheDirName = cacheDirName; } - public File getCacheDirectory(Context context) { + public synchronized File getCacheDirectory(Context context) { if(_initialized) { return _cacheDirectory; } else { diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/Configuration.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/Configuration.java index 66a1cf2a..92f4b819 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/configuration/Configuration.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/Configuration.java @@ -2,17 +2,23 @@ import android.text.TextUtils; -import com.unity3d.services.core.log.DeviceLog; +import com.unity3d.services.ads.token.TokenStorage; +import com.unity3d.services.core.device.reader.DeviceInfoReaderBuilder; +import com.unity3d.services.core.misc.Utilities; import com.unity3d.services.core.properties.SdkProperties; import com.unity3d.services.core.request.WebRequest; +import com.unity3d.services.core.request.metrics.SDKMetrics; +import com.unity3d.services.core.request.metrics.TSIMetric; import org.json.JSONException; import org.json.JSONObject; +import java.io.File; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -39,6 +45,12 @@ public class Configuration { private String _configJsonString; private String _configUrl; + private String _unifiedAuctionToken; + private String _stateId; + private Experiments _experiments; + private int _tokenTimeout; + private String _src; + private ConfigurationRequestFactory _configurationRequestFactory; private Map _moduleConfigurations; @@ -57,14 +69,21 @@ public Configuration () { } public Configuration (String configUrl) { + this(); _configUrl = configUrl; - this.setOptionalFields(new JSONObject()); } - public Configuration (JSONObject configData) throws MalformedURLException { + public Configuration (JSONObject configData) throws MalformedURLException, JSONException { handleConfigurationData(configData); } + public Configuration(String configUrl, Experiments experiments) { + this(configUrl); + DeviceInfoReaderBuilder deviceInfoReaderBuilder = new DeviceInfoReaderBuilder(new ConfigurationReader()); + _configurationRequestFactory = new ConfigurationRequestFactory(this, deviceInfoReaderBuilder.build(), _configUrl); + _experiments = experiments; + } + public String getConfigUrl () { return _configUrl; } public Class[] getWebAppApiClassList () { @@ -121,6 +140,16 @@ public Class[] getWebAppApiClassList () { public long getWebViewAppCreateTimeout() { return _webViewAppCreateTimeout; } + public String getStateId() { return (_stateId != null) ? _stateId : ""; } + + public String getUnifiedAuctionToken() { return _unifiedAuctionToken; } + + public Experiments getExperiments() { return _experiments; } + + public int getTokenTimeout() { return _tokenTimeout; } + + public String getSrc() {return (_src != null) ? _src : ""; } + public IModuleConfiguration getModuleConfiguration(String moduleName) { if (_moduleConfigurations != null && _moduleConfigurations.containsKey(moduleName)) { return _moduleConfigurations.get(moduleName); @@ -145,21 +174,13 @@ public IModuleConfiguration getModuleConfiguration(String moduleName) { public String getJSONString() { return _configJsonString; } - protected String buildQueryString () { - return "?ts=" + System.currentTimeMillis() - + "&sdkVersion=" + SdkProperties.getVersionCode() - + "&sdkVersionName=" + SdkProperties.getVersionName(); - } - protected void makeRequest () throws Exception { if (_configUrl == null) { throw new MalformedURLException("Base URL is null"); } - String url = _configUrl + buildQueryString(); - DeviceLog.debug("Requesting configuration with: " + url); - - WebRequest request = new WebRequest(url, "GET", null); + WebRequest request = _configurationRequestFactory.getWebRequest(); + InitializeEventsMetricSender.getInstance().didConfigRequestStart(); String data = request.makeRequest(); try { @@ -167,9 +188,10 @@ protected void makeRequest () throws Exception { } catch (Exception e) { throw e; } + saveToDisk(); } - private void handleConfigurationData(JSONObject configData) throws MalformedURLException { + protected void handleConfigurationData(JSONObject configData) throws MalformedURLException, JSONException { String url = null; @@ -190,9 +212,11 @@ private void handleConfigurationData(JSONObject configData) throws MalformedURLE _webViewHash = null; } - this.setOptionalFields(configData); + _unifiedAuctionToken = !configData.isNull("tkn") ? configData.optString("tkn") : null; + _stateId = !configData.isNull("sid") ? configData.optString("sid") : null; - _configJsonString = configData.toString(); + this.setOptionalFields(configData); + _configJsonString = getFilteredConfigJson(configData).toString(); } private void setOptionalFields(JSONObject configData) { @@ -212,6 +236,9 @@ private void setOptionalFields(JSONObject configData) { _metricsUrl = configData.optString("murl", ""); _metricSampleRate = configData.optDouble("msr", 100d); _webViewAppCreateTimeout = configData.optLong("wct", 60000L); + _tokenTimeout = configData.optInt("tto", 5000); + _src = configData.optString("src", null); + _experiments = new Experiments(configData.optJSONObject("exp")); } private void createWebAppApiClassList() { @@ -228,4 +255,33 @@ private void createWebAppApiClassList() { _webAppApiClassList = apiList.toArray(new Class[apiList.size()]); } + + public void saveToDisk() { + + Utilities.writeFile(new File(SdkProperties.getLocalConfigurationFilepath()), getJSONString()); + } + + private JSONObject getFilteredConfigJson(JSONObject jsonConfig) throws JSONException { + JSONObject filteredConfig = new JSONObject(); + for (Iterator it = jsonConfig.keys(); it.hasNext(); ) { + String currentKey = it.next(); + Object currentValue = jsonConfig.opt(currentKey); + if (!currentKey.equals("tkn") && !currentKey.equals("sid")) + filteredConfig.put(currentKey, currentValue); + + } + return filteredConfig; + } + + public Map getMetricTags() { + Map tags = new HashMap<>(); + + if (_experiments != null) { + tags.putAll(_experiments.getExperimentTags()); + } + + tags.put("src", getSrc()); + + return tags; + } } diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationLoader.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationLoader.java new file mode 100644 index 00000000..16acf0d8 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationLoader.java @@ -0,0 +1,68 @@ +package com.unity3d.services.core.configuration; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderBuilder; +import com.unity3d.services.core.request.WebRequest; +import com.unity3d.services.core.request.metrics.SDKMetrics; +import com.unity3d.services.core.request.metrics.TSIMetric; + +import org.json.JSONObject; + +import java.util.Map; + +public class ConfigurationLoader { + private final Configuration _localConfiguration; + private final ConfigurationRequestFactory _configurationRequestFactory; + + public ConfigurationLoader(Configuration localConfiguration) { + _localConfiguration = localConfiguration; + DeviceInfoReaderBuilder deviceInfoReaderBuilder = new DeviceInfoReaderBuilder(new ConfigurationReader()); + _configurationRequestFactory = new ConfigurationRequestFactory(localConfiguration, deviceInfoReaderBuilder.build(), localConfiguration.getConfigUrl()); + + } + + public void loadConfiguration(IConfigurationLoaderListener configurationLoaderListener) throws Exception { + if (_localConfiguration.getConfigUrl() == null) { + configurationLoaderListener.onError("Base URL is null"); + return; + } + + WebRequest request; + try { + request = _configurationRequestFactory.getWebRequest(); + } catch (Exception e) { + configurationLoaderListener.onError("Could not create web request"); + return; + } + + InitializeEventsMetricSender.getInstance().didConfigRequestStart(); + + String data = request.makeRequest(); + boolean is2XXResponseCode = (request.getResponseCode() / 100) == 2; + if (!is2XXResponseCode) { + configurationLoaderListener.onError("Non 2xx HTTP status received from ads configuration request."); + return; + } + try { + _localConfiguration.handleConfigurationData(new JSONObject(data)); + } catch (Exception e) { + configurationLoaderListener.onError("Could not create web request"); + return; + } + sendConfigMetrics(_localConfiguration.getUnifiedAuctionToken(), _localConfiguration.getStateId()); + configurationLoaderListener.onSuccess(_localConfiguration); + } + + private void sendConfigMetrics(String unifiedAuctionToken, String stateId) { + Map tags = _localConfiguration.getMetricTags(); + + if (_localConfiguration.getExperiments() != null && _localConfiguration.getExperiments().isTwoStageInitializationEnabled()) { + if (unifiedAuctionToken == null || unifiedAuctionToken.isEmpty()) { + SDKMetrics.getInstance().sendMetric(TSIMetric.newMissingToken(tags)); + } + + if (stateId == null || stateId.isEmpty()) { + SDKMetrics.getInstance().sendMetric(TSIMetric.newMissingStateId(tags)); + } + } + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationReader.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationReader.java new file mode 100644 index 00000000..a7e8c53e --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationReader.java @@ -0,0 +1,49 @@ +package com.unity3d.services.core.configuration; + +import com.unity3d.services.core.log.DeviceLog; +import com.unity3d.services.core.misc.Utilities; +import com.unity3d.services.core.properties.SdkProperties; +import com.unity3d.services.core.webview.WebViewApp; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; + +public class ConfigurationReader { + private Configuration localConfiguration; + + public Configuration getCurrentConfiguration() { + if (getRemoteConfiguration() != null) { + return getRemoteConfiguration(); + } + return getLocalConfiguration(); + } + + private Configuration getRemoteConfiguration() { + if (WebViewApp.getCurrentApp() == null) return null; + return WebViewApp.getCurrentApp().getConfiguration(); + } + + private Configuration getLocalConfiguration() { + if (localConfiguration != null) { + return localConfiguration; + } + + File configFile = new File(SdkProperties.getLocalConfigurationFilepath()); + if (configFile.exists()) { + try { + String fileContent = new String(Utilities.readFileBytes(configFile)); + JSONObject loadedJson = new JSONObject(fileContent); + localConfiguration = new Configuration(loadedJson); + } catch (IOException | JSONException exception) { + DeviceLog.debug("Unable to read configuration from storage"); + localConfiguration = null; + } + } + + return localConfiguration; + + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationRequestFactory.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationRequestFactory.java new file mode 100644 index 00000000..6962c4ad --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/ConfigurationRequestFactory.java @@ -0,0 +1,74 @@ +package com.unity3d.services.core.configuration; + +import com.unity3d.services.core.device.reader.DeviceInfoReaderCompressor; +import com.unity3d.services.core.device.reader.DeviceInfoReaderCompressorWithMetrics; +import com.unity3d.services.core.device.reader.DeviceInfoReaderUrlEncoder; +import com.unity3d.services.core.device.reader.IDeviceInfoReader; +import com.unity3d.services.core.log.DeviceLog; +import com.unity3d.services.core.properties.ClientProperties; +import com.unity3d.services.core.properties.SdkProperties; +import com.unity3d.services.core.request.WebRequest; + +import java.net.MalformedURLException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ConfigurationRequestFactory { + private final String _configUrl; + private final Configuration _configuration; + private final IDeviceInfoReader _deviceInfoReader; + + + public ConfigurationRequestFactory(Configuration configuration, IDeviceInfoReader deviceInfoReader , String configUrl) { + _configuration = configuration; + _deviceInfoReader = deviceInfoReader; + _configUrl = configUrl; + } + + public WebRequest getWebRequest() throws MalformedURLException { + String url = _configUrl + buildQueryString(); + DeviceLog.debug("Requesting configuration with: " + url); + Experiments experiments = _configuration.getExperiments(); + WebRequest webRequest; + if (experiments != null && experiments.isTwoStageInitializationEnabled() && experiments.isPOSTMethodInConfigRequestEnabled()) { + Map> headers = new HashMap<>(); + headers.put("Content-Encoding", Collections.singletonList("gzip")); + webRequest = new WebRequest(url, "POST", headers); + DeviceInfoReaderCompressorWithMetrics infoReaderCompressor = new DeviceInfoReaderCompressorWithMetrics(new DeviceInfoReaderCompressor(_deviceInfoReader), experiments); + byte[] queryData = infoReaderCompressor.getDeviceData(); + webRequest.setBody(queryData); + } else { + webRequest = new WebRequest(url, "GET"); + } + return webRequest; + } + + private String buildQueryString () { + StringBuilder queryString = new StringBuilder(); + queryString.append("?"); + Experiments experiments = _configuration.getExperiments(); + if (experiments != null && experiments.isTwoStageInitializationEnabled()) { + queryString.append(buildCompressedQueryStringIfNeeded()); + } else { + queryString.append("ts=").append(System.currentTimeMillis()); + queryString.append("&sdkVersion=").append(SdkProperties.getVersionCode()); + queryString.append("&sdkVersionName=").append(SdkProperties.getVersionName()); + queryString.append("&gameId=").append(ClientProperties.getGameId()); + } + return queryString.toString(); + } + + private String buildCompressedQueryStringIfNeeded() { + String compressedQueryString = ""; + Experiments experiments = _configuration.getExperiments(); + if (experiments != null) { + if (experiments.isTwoStageInitializationEnabled() && !experiments.isPOSTMethodInConfigRequestEnabled()) { + String queryData = new DeviceInfoReaderUrlEncoder(new DeviceInfoReaderCompressorWithMetrics(new DeviceInfoReaderCompressor(_deviceInfoReader), experiments)).getUrlEncodedData(); + compressedQueryString = "c=" + queryData; + } + } + return compressedQueryString; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/CoreModuleConfiguration.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/CoreModuleConfiguration.java index 8805fd38..c3cc0875 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/configuration/CoreModuleConfiguration.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/CoreModuleConfiguration.java @@ -11,7 +11,7 @@ import com.unity3d.services.core.misc.Utilities; import com.unity3d.services.core.properties.ClientProperties; import com.unity3d.services.core.properties.SdkProperties; -import com.unity3d.services.core.request.SDKMetrics; +import com.unity3d.services.core.request.metrics.SDKMetrics; import com.unity3d.services.core.request.WebRequestThread; public class CoreModuleConfiguration implements IModuleConfiguration { @@ -38,6 +38,7 @@ public Class[] getWebAppApiClassList() { public boolean resetState(Configuration configuration) { SDKMetrics.setConfiguration(configuration); + InitializeEventsMetricSender.getInstance().setMetricTags(configuration.getMetricTags()); BroadcastMonitor.removeAllBroadcastListeners(); CacheThread.cancel(); WebRequestThread.cancel(); @@ -53,11 +54,13 @@ public boolean resetState(Configuration configuration) { public boolean initModuleState(Configuration configuration) { SDKMetrics.setConfiguration(configuration); + InitializeEventsMetricSender.getInstance().setMetricTags(configuration.getMetricTags()); return true; } public boolean initErrorState(Configuration configuration, String state, String errorMessage) { SDKMetrics.setConfiguration(configuration); + InitializeEventsMetricSender.getInstance().setMetricTags(configuration.getMetricTags()); final String message; final UnityAds.UnityAdsInitializationError error; switch (state) { @@ -87,6 +90,7 @@ public void run() { public boolean initCompleteState(Configuration configuration) { SDKMetrics.setConfiguration(configuration); + InitializeEventsMetricSender.getInstance().setMetricTags(configuration.getMetricTags()); InitializationNotificationCenter.getInstance().triggerOnSdkInitialized(); Utilities.runOnUiThread(new Runnable() { diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/Experiments.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/Experiments.java new file mode 100644 index 00000000..e2ba2f97 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/Experiments.java @@ -0,0 +1,83 @@ +package com.unity3d.services.core.configuration; + +import com.unity3d.services.core.log.DeviceLog; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class Experiments { + + private static final String TSI_TAG_INIT_ENABLED = "tsi"; + private static final String TSI_TAG_INIT_POST = "tsi_p"; + private static final String TSI_TAG_FORWARD_FEATURE_FLAGS = "fff"; + private static final String TSI_TAG_UPDATE_PII_FIELDS = "tsi_upii"; + private static final String TSI_TAG_DEVELOPER_CONSENT = "tsi_dc"; + private static final String TSI_TAG_NATIVE_TOKEN = "tsi_nt"; + + private final JSONObject _experimentData; + + public Experiments() { + this(null); + } + + public Experiments(JSONObject experimentData) { + if (experimentData == null) { + _experimentData = new JSONObject(); + } else { + _experimentData = experimentData; + } + } + + public void setTwoStageInitializationEnabled(boolean tsiEnabled) { + try { + _experimentData.put(TSI_TAG_INIT_ENABLED, tsiEnabled); + } catch (JSONException e) { + DeviceLog.warning("Could not set TSI flag to " + tsiEnabled); + } + } + + public boolean isTwoStageInitializationEnabled() { + return _experimentData.optBoolean(TSI_TAG_INIT_ENABLED, false); + } + + public boolean isPOSTMethodInConfigRequestEnabled() { + return _experimentData.optBoolean(TSI_TAG_INIT_POST, false); + } + + public boolean isForwardExperimentsToWebViewEnabled() { + return _experimentData.optBoolean(TSI_TAG_FORWARD_FEATURE_FLAGS, false); + } + + public boolean isNativeTokenEnabled() { + return _experimentData.optBoolean(TSI_TAG_NATIVE_TOKEN, false); + } + + public boolean isUpdatePiiFields() { + return _experimentData.optBoolean(TSI_TAG_UPDATE_PII_FIELDS, false); + } + + public boolean isHandleDeveloperConsent() { + return _experimentData.optBoolean(TSI_TAG_DEVELOPER_CONSENT, false); + } + + public JSONObject getExperimentData() { + return _experimentData; + } + + public Map getExperimentTags() { + Map map = new HashMap<>(); + + map.put(TSI_TAG_INIT_ENABLED, String.valueOf(isTwoStageInitializationEnabled())); + map.put(TSI_TAG_INIT_POST, String.valueOf(isPOSTMethodInConfigRequestEnabled())); + map.put(TSI_TAG_FORWARD_FEATURE_FLAGS, String.valueOf(isForwardExperimentsToWebViewEnabled())); + map.put(TSI_TAG_UPDATE_PII_FIELDS, String.valueOf(isUpdatePiiFields())); + map.put(TSI_TAG_DEVELOPER_CONSENT, String.valueOf(isHandleDeveloperConsent())); + map.put(TSI_TAG_NATIVE_TOKEN, String.valueOf(isNativeTokenEnabled())); + + return map; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/IConfigurationLoaderListener.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/IConfigurationLoaderListener.java new file mode 100644 index 00000000..cdb07195 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/IConfigurationLoaderListener.java @@ -0,0 +1,6 @@ +package com.unity3d.services.core.configuration; + +public interface IConfigurationLoaderListener { + void onSuccess(Configuration configuration); + void onError(String errorMsg); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/IInitializeEventsMetricSender.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/IInitializeEventsMetricSender.java new file mode 100644 index 00000000..e4a8559c --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/IInitializeEventsMetricSender.java @@ -0,0 +1,30 @@ +package com.unity3d.services.core.configuration; + +import com.unity3d.services.core.request.metrics.Metric; + +import java.util.Map; + +public interface IInitializeEventsMetricSender { + + void didInitStart(); + + void didConfigRequestStart(); + + void sdkDidInitialize(); + + Long initializationStartTimeStamp(); + + void sdkInitializeFailed(String message); + + void sdkTokenDidBecomeAvailableWithConfig(boolean withConfig); + + Long duration(); + + Long tokenDuration(); + + void setMetricTags(Map metricTags); + + Map getMetricTags(); + + void sendMetric(Metric metric); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeEventsMetricSender.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeEventsMetricSender.java new file mode 100644 index 00000000..7067c11a --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeEventsMetricSender.java @@ -0,0 +1,150 @@ +package com.unity3d.services.core.configuration; + + +import com.unity3d.services.core.log.DeviceLog; +import com.unity3d.services.core.request.metrics.Metric; +import com.unity3d.services.core.request.metrics.SDKMetrics; +import com.unity3d.services.core.request.metrics.TSIMetric; + +import java.util.HashMap; +import java.util.Map; + +public class InitializeEventsMetricSender implements IInitializeEventsMetricSender, IInitializationListener{ + + private static InitializeEventsMetricSender _instance; + + private Map _metricTags; + + private long _startTime = 0L; + private long _configStartTime = 0L; + private boolean _initMetricSent = false; + private boolean _tokenMetricSent = false; + + public static IInitializeEventsMetricSender getInstance() { + if (_instance == null) { + _instance = new InitializeEventsMetricSender(); + } + return _instance; + } + + private InitializeEventsMetricSender() { + InitializationNotificationCenter.getInstance().addListener(this); + } + + @Override + public void didInitStart() { + _startTime = System.currentTimeMillis(); + sendMetric(TSIMetric.newInitStarted(getMetricTags())); + } + + @Override + public void didConfigRequestStart() { + _configStartTime = System.currentTimeMillis(); + } + + @Override + public synchronized void sdkDidInitialize() { + if (_startTime == 0L) { + DeviceLog.debug("sdkDidInitialize called before didInitStart, skipping metric"); + return; + } + + if (!_initMetricSent) { + sendMetric(TSIMetric.newInitTimeSuccess(duration(), getMetricTags())); + _initMetricSent = true; + } + } + + @Override + public Long initializationStartTimeStamp() { + return _startTime; + } + + @Override + public synchronized void sdkInitializeFailed(String message) { + if (_startTime == 0L) { + DeviceLog.debug("sdkInitializeFailed called before didInitStart, skipping metric"); + return; + } + + if (!_initMetricSent) { + sendMetric(TSIMetric.newInitTimeFailure(duration(), getMetricTags())); + _initMetricSent = true; + } + } + + @Override + public synchronized void sdkTokenDidBecomeAvailableWithConfig(boolean withConfig) { + if (!_tokenMetricSent) { + sendTokenAvailabilityMetricWithConfig(withConfig); + + if (withConfig) { + sendTokenResolutionRequestMetricIfNeeded(); + } + _tokenMetricSent = true; + } + } + + private void sendTokenAvailabilityMetricWithConfig(boolean withConfig) { + if (_startTime == 0L) { + DeviceLog.debug("sendTokenAvailabilityMetricWithConfig called before didInitStart, skipping metric"); + return; + } + + Long duration = System.currentTimeMillis() - _startTime; + Map tags = getMetricTags(); + Metric metric = withConfig + ? TSIMetric.newTokenAvailabilityLatencyConfig(duration, tags) + : TSIMetric.newTokenAvailabilityLatencyWebview(duration, tags); + + sendMetric(metric); + } + + private void sendTokenResolutionRequestMetricIfNeeded() { + if (_configStartTime == 0L) { + DeviceLog.debug("sendTokenResolutionRequestMetricIfNeeded called before didInitStart, skipping metric"); + return; + } + + sendMetric(TSIMetric.newTokenResolutionRequestLatency(tokenDuration(), getMetricTags())); + } + + @Override + public Long duration() { + return System.currentTimeMillis() - _startTime; + } + + @Override + public Long tokenDuration() { + return System.currentTimeMillis() - _configStartTime; + } + + @Override + public void setMetricTags(Map metricTags) { + _metricTags = metricTags; + } + + @Override + public Map getMetricTags() { + if (_metricTags != null) { + return _metricTags; + } else { + return new HashMap<>(); + } + } + + @Override + public void sendMetric(Metric metric) { + SDKMetrics.getInstance().sendMetric(metric); + } + + @Override + public void onSdkInitialized() { + sdkDidInitialize(); + } + + @Override + public void onSdkInitializationFailed(String message, int code) { + sdkInitializeFailed(message); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeThread.java b/unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeThread.java index ecf85310..d69cea2e 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeThread.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/configuration/InitializeThread.java @@ -6,16 +6,20 @@ import android.text.TextUtils; import com.unity3d.ads.UnityAds; +import com.unity3d.services.ads.token.TokenStorage; import com.unity3d.services.core.api.DownloadLatestWebViewStatus; import com.unity3d.services.core.api.Lifecycle; import com.unity3d.services.core.connectivity.ConnectivityMonitor; import com.unity3d.services.core.connectivity.IConnectivityListener; +import com.unity3d.services.core.lifecycle.CachedLifecycle; +import com.unity3d.services.core.lifecycle.LifecycleCache; import com.unity3d.services.core.log.DeviceLog; import com.unity3d.services.core.misc.Utilities; import com.unity3d.services.core.properties.ClientProperties; import com.unity3d.services.core.properties.SdkProperties; -import com.unity3d.services.core.request.SDKMetrics; +import com.unity3d.services.core.request.metrics.SDKMetrics; import com.unity3d.services.core.request.WebRequest; +import com.unity3d.services.core.request.metrics.TSIMetric; import com.unity3d.services.core.webview.WebViewApp; import org.json.JSONObject; @@ -75,6 +79,8 @@ public void quit() { public static synchronized void initialize(Configuration configuration) { if (_thread == null) { + InitializeEventsMetricSender.getInstance().didInitStart(); + CachedLifecycle.register(); _thread = new InitializeThread(new InitializeStateLoadConfigFile(configuration)); _thread.setName("UnityAdsInitializeThread"); _thread.start(); @@ -132,11 +138,7 @@ public InitializeState execute() { String fileContent = new String(Utilities.readFileBytes(configFile)); JSONObject loadedJson = new JSONObject(fileContent); localConfig = new Configuration(loadedJson); - - // Only accept local configurations from the same version - if (SdkProperties.getVersionName().equals(localConfig.getSdkVersion())) { - _configuration = localConfig; - } + _configuration = localConfig; } catch (Exception e) { DeviceLog.debug("Unity Ads init: Using default configuration parameters"); } finally { @@ -270,14 +272,16 @@ public static class InitializeStateConfig extends InitializeState { private long _retryDelay; private int _maxRetries; private double _scalingFactor; + private InitializeState _nextState; public InitializeStateConfig(Configuration localConfiguration) { - _configuration = new Configuration(SdkProperties.getConfigUrl()); + _configuration = new Configuration(SdkProperties.getConfigUrl(), localConfiguration.getExperiments()); _retries = 0; _retryDelay = localConfiguration.getRetryDelay(); _maxRetries = localConfiguration.getMaxRetries(); _scalingFactor = localConfiguration.getRetryScalingFactor(); _localConfig = localConfiguration; + _nextState = null; } public Configuration getConfiguration() { @@ -287,9 +291,18 @@ public Configuration getConfiguration() { @Override public InitializeState execute() { DeviceLog.info("Unity Ads init: load configuration from " + SdkProperties.getConfigUrl()); + InitializeState nextState; + if (_configuration.getExperiments() != null && _configuration.getExperiments().isTwoStageInitializationEnabled()) { + nextState = executeWithLoader(); + } else { + nextState = executeLegacy(_configuration); + } + return nextState; + } + public InitializeState executeLegacy(Configuration configuration) { try { - _configuration.makeRequest(); + configuration.makeRequest(); } catch (Exception e) { if (_retries < _maxRetries) { _retryDelay *= _scalingFactor; @@ -300,11 +313,45 @@ public InitializeState execute() { return new InitializeStateNetworkError("network config request", e, this, _localConfig); } - if (_configuration.getDelayWebViewUpdate()) { - return new InitializeStateLoadCacheConfigAndWebView(_configuration, _localConfig); + if (configuration.getDelayWebViewUpdate()) { + return new InitializeStateLoadCacheConfigAndWebView(configuration, _localConfig); } - return new InitializeStateLoadCache(_configuration); + return new InitializeStateLoadCache(configuration); + } + + public InitializeState executeWithLoader() { + ConfigurationLoader configurationLoader = new ConfigurationLoader(_configuration); + final Configuration legacyConfiguration = new Configuration(SdkProperties.getConfigUrl(), new Experiments()); + try { + configurationLoader.loadConfiguration(new IConfigurationLoaderListener() { + @Override + public void onSuccess(Configuration configuration) { + _configuration = configuration; + if (_configuration.getDelayWebViewUpdate()) { + _nextState = new InitializeStateLoadCacheConfigAndWebView(_configuration, _localConfig); + } + TokenStorage.setInitToken(_configuration.getUnifiedAuctionToken()); + _configuration.saveToDisk(); + _nextState = new InitializeStateLoadCache(_configuration); + } + + @Override + public void onError(String errorMsg) { + SDKMetrics.getInstance().sendMetric(TSIMetric.newEmergencySwitchOff(_configuration.getMetricTags())); + _nextState = executeLegacy(legacyConfiguration); + } + }); + return _nextState; + } catch (Exception e) { + if (_retries < _maxRetries) { + _retryDelay *= _scalingFactor; + _retries++; + return new InitializeStateRetry(this, _retryDelay); + } + + return new InitializeStateNetworkError("network config request", e, this, _configuration); + } } } @@ -502,7 +549,7 @@ public InitializeState execute() { } // TODO: Fix _state.replaceAll... with Enum values to ensure future compatibility with tag values - This works for now - SDKMetrics.getInstance().sendEventWithTags("native_initialization_failed", new HashMap (){{ + SDKMetrics.getInstance().sendEvent("native_initialization_failed", new HashMap (){{ put("stt", _state.replaceAll(" ", "_")); }}); return null; diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/Device.java b/unity-ads/src/main/java/com/unity3d/services/core/device/Device.java index 69e5e7cc..0a55d816 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/device/Device.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/Device.java @@ -23,6 +23,7 @@ import com.unity3d.services.core.log.DeviceLog; import com.unity3d.services.core.misc.Utilities; +import com.unity3d.services.core.preferences.AndroidPreferences; import com.unity3d.services.core.properties.ClientProperties; import org.json.JSONException; @@ -115,6 +116,29 @@ public static boolean isUsingWifi () { return false; } + public static String getIdfi() { + String idfi = AndroidPreferences.getString("unityads-installinfo", "unityads-idfi"); + + if (idfi == null) { + idfi = Device.getUniqueEventId(); + AndroidPreferences.setString("unityads-installinfo", "unityads-idfi", idfi); + } + + return idfi; + } + + public static String getConnectionType() { + String connectionType; + if (isUsingWifi()) { + connectionType = "wifi"; + } else if (isActiveNetworkConnected()) { + connectionType = "cellular"; + } else { + connectionType = "none"; + } + return connectionType; + } + public static int getNetworkType() { if (ClientProperties.getApplicationContext() != null) { TelephonyManager tm = (TelephonyManager)ClientProperties.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE); diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/TokenType.java b/unity-ads/src/main/java/com/unity3d/services/core/device/TokenType.java new file mode 100644 index 00000000..2f94fb57 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/TokenType.java @@ -0,0 +1,6 @@ +package com.unity3d.services.core.device; + +public enum TokenType { + TOKEN_NATIVE, + TOKEN_REMOTE +} \ No newline at end of file diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReader.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReader.java new file mode 100644 index 00000000..8eb4bc54 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReader.java @@ -0,0 +1,83 @@ +package com.unity3d.services.core.device.reader; + +import android.webkit.WebSettings; + +import com.unity3d.services.core.device.Device; +import com.unity3d.services.core.log.DeviceLog; +import com.unity3d.services.core.properties.ClientProperties; +import com.unity3d.services.core.properties.SdkProperties; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +public class DeviceInfoReader implements IDeviceInfoReader { + + @Override + public Map getDeviceInfoData() { + Map deviceInfoData = new HashMap<>(); + deviceInfoData.put("bundleId", ClientProperties.getAppName()); + deviceInfoData.put("encrypted", ClientProperties.isAppDebuggable()); + deviceInfoData.put("rooted", Device.isRooted()); + deviceInfoData.put("platform", "android"); + deviceInfoData.put("sdkVersion", SdkProperties.getVersionCode()); + deviceInfoData.put("osVersion", Device.getOsVersion()); + deviceInfoData.put("deviceModel", Device.getModel()); + deviceInfoData.put("language", Locale.getDefault().toString()); + deviceInfoData.put("connectionType", Device.getConnectionType()); + deviceInfoData.put("screenHeight", Device.getScreenHeight()); + deviceInfoData.put("screenWidth", Device.getScreenWidth()); + deviceInfoData.put("deviceMake", Device.getManufacturer()); + deviceInfoData.put("screenDensity", Device.getScreenDensity()); + deviceInfoData.put("screenSize", Device.getScreenLayout()); + deviceInfoData.put("limitAdTracking", Device.isLimitAdTrackingEnabled()); + deviceInfoData.put("idfi", Device.getIdfi()); + deviceInfoData.put("networkOperator", Device.getNetworkOperator()); + deviceInfoData.put("volume", Device.getStreamVolume(1)); + deviceInfoData.put("deviceFreeSpace", Device.getFreeSpace(ClientProperties.getApplicationContext().getCacheDir())); + + // PRELOAD required data. + deviceInfoData.put("apiLevel", String.valueOf(Device.getApiLevel())); + deviceInfoData.put("networkType", Device.getNetworkType()); + deviceInfoData.put("bundleVersion", ClientProperties.getAppVersion()); + deviceInfoData.put("timeZone", TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT, Locale.US)); + deviceInfoData.put("timeZoneOffset", TimeZone.getDefault().getOffset(System.currentTimeMillis()) / 1000); + deviceInfoData.put("webviewUa", WebSettings.getDefaultUserAgent(ClientProperties.getApplicationContext())); + deviceInfoData.put("networkOperatorName", Device.getNetworkOperatorName()); + deviceInfoData.put("wiredHeadset", Device.isWiredHeadsetOn()); + deviceInfoData.put("versionCode", SdkProperties.getVersionCode()); + deviceInfoData.put("stores", "google"); + deviceInfoData.put("appStartTime", SdkProperties.getInitializationTimeEpoch() / 1000); + + // Native Config + deviceInfoData.put("sdkVersionName", SdkProperties.getVersionName()); + + // Glyph signals related + deviceInfoData.put("eventTimeStamp", System.currentTimeMillis() / 1000); + deviceInfoData.put("cpuCount", Device.getCPUCount()); + deviceInfoData.put("usbConnected", Device.isUSBConnected()); + try { + deviceInfoData.put("apkHash", Device.getApkDigest()); + } catch (Exception e) { + DeviceLog.error("Could not get APK Digest"); + } + deviceInfoData.put("apkDeveloperSigningCertificateHash", Device.getCertificateFingerprint()); + deviceInfoData.put("deviceUpTime", Device.getUptime()); + deviceInfoData.put("deviceElapsedRealtime", Device.getElapsedRealtime()); + deviceInfoData.put("adbEnabled", Device.isAdbEnabled()); + deviceInfoData.put("androidFingerprint", Device.getFingerprint()); + deviceInfoData.put("batteryStatus", Device.getBatteryStatus()); + deviceInfoData.put("batteryLevel", Device.getBatteryLevel()); + deviceInfoData.put("networkMetered", Device.getNetworkMetered()); + + // Misc + deviceInfoData.put("ts", System.currentTimeMillis()); + deviceInfoData.put("gameId", ClientProperties.getGameId()); + deviceInfoData.put("test", SdkProperties.isTestMode()); + deviceInfoData.put("callType", "token"); + return deviceInfoData; + } + + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderBuilder.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderBuilder.java new file mode 100644 index 00000000..b596d7c6 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderBuilder.java @@ -0,0 +1,48 @@ +package com.unity3d.services.core.device.reader; + +import com.unity3d.services.core.configuration.ConfigurationReader; +import com.unity3d.services.core.configuration.Experiments; +import com.unity3d.services.core.device.Storage; +import com.unity3d.services.core.device.StorageManager; +import com.unity3d.services.core.device.reader.pii.PiiDataProvider; +import com.unity3d.services.core.device.reader.pii.PiiDataSelector; +import com.unity3d.services.core.device.reader.pii.PiiTrackingStatusReader; +import com.unity3d.services.core.lifecycle.CachedLifecycle; +import com.unity3d.services.core.misc.IJsonStorageReader; +import com.unity3d.services.core.misc.JsonStorageAggregator; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class DeviceInfoReaderBuilder { + private ConfigurationReader _configurationReader; + + public DeviceInfoReaderBuilder(ConfigurationReader configurationReader) { + _configurationReader = configurationReader; + } + + public IDeviceInfoReader build() { + Storage privateStorage = StorageManager.getStorage(StorageManager.StorageType.PRIVATE); + Storage publicStorage = StorageManager.getStorage(StorageManager.StorageType.PUBLIC); + JsonStorageAggregator storageAggregator = new JsonStorageAggregator(Arrays.asList(publicStorage, privateStorage)); + DeviceInfoReaderFilterProvider deviceInfoReaderFilterProvider = new DeviceInfoReaderFilterProvider(privateStorage); + DeviceInfoReaderWithLifecycle deviceInfoReaderWithLifecycle = new DeviceInfoReaderWithLifecycle(new DeviceInfoReader(), CachedLifecycle.getLifecycleListener()); + DeviceInfoReaderWithStorageInfo deviceInfoReaderWithStorageInfo = new DeviceInfoReaderWithStorageInfo(deviceInfoReaderWithLifecycle, privateStorage, publicStorage); + PiiTrackingStatusReader piiTrackingStatusReader = new PiiTrackingStatusReader(storageAggregator); + PiiDataSelector piiDataSelector = new PiiDataSelector(piiTrackingStatusReader, privateStorage, getCurrentExperiments()); + DeviceInfoReaderWithPII deviceInfoReaderWithPII = new DeviceInfoReaderWithPII(deviceInfoReaderWithStorageInfo, piiDataSelector, new PiiDataProvider()); + DeviceInfoReaderWithFilter deviceInfoReaderWithFilter = new DeviceInfoReaderWithFilter(deviceInfoReaderWithPII, deviceInfoReaderFilterProvider.getFilterList()); + return new DeviceInfoReaderWithMetrics(deviceInfoReaderWithFilter, getCurrentMetricTags()); + } + + private Experiments getCurrentExperiments() { + if (_configurationReader == null || _configurationReader.getCurrentConfiguration() == null) return new Experiments(); + return _configurationReader.getCurrentConfiguration().getExperiments(); + } + + private Map getCurrentMetricTags() { + if (_configurationReader == null || _configurationReader.getCurrentConfiguration() == null) return new HashMap<>(); + return _configurationReader.getCurrentConfiguration().getMetricTags(); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressor.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressor.java new file mode 100644 index 00000000..69d3f3f0 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressor.java @@ -0,0 +1,54 @@ +package com.unity3d.services.core.device.reader; + +import com.unity3d.services.core.log.DeviceLog; + +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +public class DeviceInfoReaderCompressor implements IDeviceInfoDataCompressor { + private final IDeviceInfoReader _deviceInfoReader; + + public DeviceInfoReaderCompressor(IDeviceInfoReader deviceInfoReader) { + _deviceInfoReader = deviceInfoReader; + } + + @Override + public byte[] getDeviceData() { + Map deviceDataMap = getDeviceInfo(); + return compressDeviceInfo(deviceDataMap); + } + + @Override + public Map getDeviceInfo() { + return _deviceInfoReader.getDeviceInfoData(); + } + + @Override + public byte[] compressDeviceInfo(Map deviceData) { + byte[] zippedData = null; + if (deviceData != null) { + JSONObject jsonData = new JSONObject(deviceData); + String jsonString = jsonData.toString(); + ByteArrayOutputStream os = new ByteArrayOutputStream(jsonString.length()); + GZIPOutputStream gos; + try { + gos = new GZIPOutputStream(os); + gos.write(jsonString.getBytes()); + gos.flush(); + gos.close(); + os.close(); + zippedData = os.toByteArray(); + } catch (IOException e) { + DeviceLog.error("Error occurred while trying to compress device data."); + } + } else { + DeviceLog.error("Invalid DeviceInfoData: Expected non null map provided by reader"); + } + return zippedData; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressorWithMetrics.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressorWithMetrics.java new file mode 100644 index 00000000..aa57b6ec --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderCompressorWithMetrics.java @@ -0,0 +1,63 @@ +package com.unity3d.services.core.device.reader; + +import com.unity3d.services.core.configuration.Experiments; +import com.unity3d.services.core.request.metrics.SDKMetrics; +import com.unity3d.services.core.request.metrics.TSIMetric; + +import java.util.Map; + +public class DeviceInfoReaderCompressorWithMetrics implements IDeviceInfoDataCompressor { + + private final IDeviceInfoDataCompressor _deviceInfoDataCompressor; + private final Experiments _experiments; + + private long _startTimeInfo; + private long _startTimeCompression; + private long _endTime; + + public DeviceInfoReaderCompressorWithMetrics(IDeviceInfoDataCompressor deviceInfoDataCompressor, Experiments experiments) { + _deviceInfoDataCompressor = deviceInfoDataCompressor; + _experiments = experiments; + } + + @Override + public byte[] getDeviceData() { + if (_deviceInfoDataCompressor == null) return new byte[0]; + _startTimeInfo = System.currentTimeMillis(); + Map deviceInfo = getDeviceInfo(); + byte[] zippedData = compressDeviceInfo(deviceInfo); + sendDeviceInfoMetrics(); + return zippedData; + } + + @Override + public Map getDeviceInfo() { + return _deviceInfoDataCompressor.getDeviceInfo(); + } + + @Override + public byte[] compressDeviceInfo(Map deviceData) { + _startTimeCompression = System.currentTimeMillis(); + byte[] zippedData = _deviceInfoDataCompressor.compressDeviceInfo(getDeviceInfo()); + _endTime = System.currentTimeMillis(); + return zippedData; + } + + private long getDeviceInfoCollectionDuration() { + return _startTimeCompression - _startTimeInfo; + } + + private long getCompressionDuration() { + return _endTime - _startTimeCompression; + } + + private void sendDeviceInfoMetrics() { + Map tags = null; + if (_experiments != null) { + tags = _experiments.getExperimentTags(); + } + + SDKMetrics.getInstance().sendMetric(TSIMetric.newDeviceInfoCollectionLatency(getDeviceInfoCollectionDuration(), tags)); + SDKMetrics.getInstance().sendMetric(TSIMetric.newDeviceInfoCompressionLatency(getCompressionDuration(), tags)); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderFilterProvider.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderFilterProvider.java new file mode 100644 index 00000000..cf67e26b --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderFilterProvider.java @@ -0,0 +1,44 @@ +package com.unity3d.services.core.device.reader; + +import com.unity3d.services.core.misc.IJsonStorageReader; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class DeviceInfoReaderFilterProvider { + private static final String UNIFIED_CONFIG_KEY = "unifiedconfig"; + private static final String FILTER_EXCLUDE_KEY = "exclude"; + private IJsonStorageReader _storage; + + public DeviceInfoReaderFilterProvider(IJsonStorageReader jsonStorageReader) { + _storage = jsonStorageReader; + } + + public List getFilterList() { + List filterList = new ArrayList<>(); + if (_storage != null && _storage.getData() != null) { + Object unifiedConfigData = _storage.getData().opt(UNIFIED_CONFIG_KEY); + if (unifiedConfigData != null) { + if (unifiedConfigData instanceof JSONObject) { + Object filterExcludeData = ((JSONObject) unifiedConfigData).opt(FILTER_EXCLUDE_KEY); + if (filterExcludeData instanceof String) { + filterList = (Arrays.asList(((String) filterExcludeData).split(","))); + filterList = trimWhiteSpaces(filterList); + } + } + } + } + return filterList; + } + + private List trimWhiteSpaces(List original) { + List trimmedStrings = new ArrayList<>(); + for(String originalString : original) { + trimmedStrings.add(originalString.trim()); + } + return trimmedStrings; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderUrlEncoder.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderUrlEncoder.java new file mode 100644 index 00000000..8f33c36f --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderUrlEncoder.java @@ -0,0 +1,28 @@ +package com.unity3d.services.core.device.reader; + +import android.util.Base64; + +import com.unity3d.services.core.log.DeviceLog; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +public class DeviceInfoReaderUrlEncoder { + private final IDeviceInfoDataContainer _deviceInfoDataContainer; + + public DeviceInfoReaderUrlEncoder(IDeviceInfoDataContainer deviceInfoDataContainer) { + _deviceInfoDataContainer = deviceInfoDataContainer; + } + + public String getUrlEncodedData() { + byte[] rawData = _deviceInfoDataContainer.getDeviceData(); + String queryStringData = ""; + try { + queryStringData = URLEncoder.encode(Base64.encodeToString(rawData, Base64.NO_WRAP), "UTF-8"); + } catch (UnsupportedEncodingException e) { + DeviceLog.error("Could not encode device data using UTF-8."); + } + return queryStringData; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithFilter.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithFilter.java new file mode 100644 index 00000000..863e904d --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithFilter.java @@ -0,0 +1,25 @@ +package com.unity3d.services.core.device.reader; + +import java.util.List; +import java.util.Map; + +public class DeviceInfoReaderWithFilter implements IDeviceInfoReader { + IDeviceInfoReader _deviceInfoReader; + List _keysToExclude; + + public DeviceInfoReaderWithFilter(IDeviceInfoReader deviceInfoReader, List keysToExclude) { + _deviceInfoReader = deviceInfoReader; + _keysToExclude = keysToExclude; + } + + @Override + public Map getDeviceInfoData() { + Map filteredDeviceInfoData = _deviceInfoReader.getDeviceInfoData(); + if (_keysToExclude != null) { + for (String keyToExclude : _keysToExclude) { + filteredDeviceInfoData.remove(keyToExclude); + } + } + return filteredDeviceInfoData; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithLifecycle.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithLifecycle.java new file mode 100644 index 00000000..62a97b4a --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithLifecycle.java @@ -0,0 +1,25 @@ +package com.unity3d.services.core.device.reader; + + +import com.unity3d.services.core.lifecycle.LifecycleCache; + +import java.util.Map; + +public class DeviceInfoReaderWithLifecycle implements IDeviceInfoReader { + + private final IDeviceInfoReader _deviceInfoReader; + private final LifecycleCache _lifecycleCache; + + public DeviceInfoReaderWithLifecycle(IDeviceInfoReader deviceInfoReader, LifecycleCache lifecycleCache) { + _deviceInfoReader = deviceInfoReader; + _lifecycleCache = lifecycleCache; + } + + @Override + public Map getDeviceInfoData() { + Map deviceInfoData = _deviceInfoReader.getDeviceInfoData(); + deviceInfoData.put("appActive", _lifecycleCache.isAppActive()); + return deviceInfoData; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithMetrics.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithMetrics.java new file mode 100644 index 00000000..90221a3d --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithMetrics.java @@ -0,0 +1,35 @@ +package com.unity3d.services.core.device.reader; + +import com.unity3d.services.core.request.metrics.SDKMetrics; +import com.unity3d.services.core.request.metrics.TSIMetric; + +import java.util.Map; + +public class DeviceInfoReaderWithMetrics implements IDeviceInfoReader{ + + private final IDeviceInfoReader _deviceInfoReader; + private final Map _metricTags; + + public DeviceInfoReaderWithMetrics(IDeviceInfoReader deviceInfoReader, Map metricTags) { + _deviceInfoReader = deviceInfoReader; + _metricTags = metricTags; + } + + @Override + public Map getDeviceInfoData() { + if (_deviceInfoReader == null) return null; + Map deviceInfo = _deviceInfoReader.getDeviceInfoData(); + sendMetrics(deviceInfo); + return deviceInfo; + } + + private void sendMetrics(Map deviceInfoData) { + if (deviceInfoData != null && _metricTags != null) { + Object gameSessionId = deviceInfoData.get("unifiedconfig.data.gameSessionId"); + if (gameSessionId instanceof Long && ((Long) gameSessionId) == 0L) { + SDKMetrics.getInstance().sendMetric(TSIMetric.newMissingGameSessionId(_metricTags)); + } + } + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithPII.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithPII.java new file mode 100644 index 00000000..8d100356 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithPII.java @@ -0,0 +1,58 @@ +package com.unity3d.services.core.device.reader; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.ADVERTISING_TRACKING_ID_NORMALIZED_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_KEY; + +import com.unity3d.services.core.device.reader.pii.DataSelectorResult; +import com.unity3d.services.core.device.reader.pii.PiiDataProvider; +import com.unity3d.services.core.device.reader.pii.PiiDataSelector; +import com.unity3d.services.core.device.reader.pii.PiiDecisionData; + +import java.util.HashMap; +import java.util.Map; + +public class DeviceInfoReaderWithPII implements IDeviceInfoReader { + private IDeviceInfoReader _deviceInfoReader; + private PiiDataProvider _piiDataProvider; + private PiiDataSelector _piiDataSelector; + + public DeviceInfoReaderWithPII(IDeviceInfoReader deviceInfoReader, PiiDataSelector piiDataSelector, PiiDataProvider piiDataProvider) { + _deviceInfoReader = deviceInfoReader; + _piiDataSelector = piiDataSelector; + _piiDataProvider = piiDataProvider; + } + + @Override + public Map getDeviceInfoData() { + Map originalData = _deviceInfoReader.getDeviceInfoData(); + PiiDecisionData piiDecisionData = _piiDataSelector.whatToDoWithPII(); + DataSelectorResult dataSelectorResult = piiDecisionData.getResultType(); + switch (dataSelectorResult) { + case INCLUDE: + originalData.putAll(getPiiAttributesFromStorage(piiDecisionData)); + break; + case UPDATE: + originalData.putAll(getPiiAttributesFromDevice(piiDecisionData)); + break; + case EXCLUDE: + return originalData; + } + return originalData; + } + + private Map getPiiAttributesFromStorage(PiiDecisionData decisionData) { + return decisionData.getAttributes(); + } + + private Map getPiiAttributesFromDevice(PiiDecisionData decisionData) { + Map piiData = new HashMap<>(); + Object advertisingId = _piiDataProvider.getAdvertisingTrackingId(); + if (advertisingId != null) { + piiData.put(ADVERTISING_TRACKING_ID_NORMALIZED_KEY, advertisingId); + } + if (decisionData.getUserNonBehavioralFlag() != null) { + piiData.put(USER_NON_BEHAVIORAL_KEY, decisionData.getUserNonBehavioralFlag()); + } + return piiData; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithStorageInfo.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithStorageInfo.java new file mode 100644 index 00000000..9cb871ad --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/DeviceInfoReaderWithStorageInfo.java @@ -0,0 +1,60 @@ +package com.unity3d.services.core.device.reader; + +import com.unity3d.services.core.misc.IJsonStorageReader; +import com.unity3d.services.core.misc.JsonFlattener; +import com.unity3d.services.core.misc.JsonStorageAggregator; +import com.unity3d.services.core.misc.Utilities; + +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class DeviceInfoReaderWithStorageInfo implements IDeviceInfoReader { + + private final IDeviceInfoReader _deviceInfoReader; + private final List _storageReaders; + + private static final List _includedKeys = Arrays.asList( + "privacy", + "gdpr", + "framework", + "adapter", + "mediation", + "unity", + "pipl", + "configuration", + "user", + "unifiedconfig" + ); + + private static final List _blackListOfKeys = Arrays.asList( + "ts", + "exclude", + "pii", + "nonBehavioral", + "nonbehavioral" + ); + + public DeviceInfoReaderWithStorageInfo(IDeviceInfoReader deviceInfoReader, IJsonStorageReader... storageReaders) { + _deviceInfoReader = deviceInfoReader; + _storageReaders = Arrays.asList(storageReaders); + } + + @Override + public Map getDeviceInfoData() { + Map deviceInfoData = _deviceInfoReader.getDeviceInfoData(); + if (deviceInfoData != null) { + JsonStorageAggregator jsonStorageAggregator = new JsonStorageAggregator(_storageReaders); + JSONObject aggregatedData = jsonStorageAggregator.getData(); + JsonFlattener jsonFlattener = new JsonFlattener(aggregatedData); + JSONObject resultingJson = jsonFlattener.flattenJson(".", _includedKeys, Collections.singletonList("value"), _blackListOfKeys); + deviceInfoData = Utilities.combineJsonIntoMap(deviceInfoData, resultingJson); + } + return deviceInfoData; + } + + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataCompressor.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataCompressor.java new file mode 100644 index 00000000..2e99c2fe --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataCompressor.java @@ -0,0 +1,7 @@ +package com.unity3d.services.core.device.reader; + +import java.util.Map; + +public interface IDeviceInfoDataCompressor extends IDeviceInfoDataContainer{ + byte[] compressDeviceInfo(Map deviceData); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataContainer.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataContainer.java new file mode 100644 index 00000000..f80ebed3 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoDataContainer.java @@ -0,0 +1,8 @@ +package com.unity3d.services.core.device.reader; + +import java.util.Map; + +public interface IDeviceInfoDataContainer { + byte[] getDeviceData(); + Map getDeviceInfo(); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoReader.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoReader.java new file mode 100644 index 00000000..991c322e --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/IDeviceInfoReader.java @@ -0,0 +1,7 @@ +package com.unity3d.services.core.device.reader; + +import java.util.Map; + +public interface IDeviceInfoReader { + Map getDeviceInfoData(); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/JsonStorageKeyNames.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/JsonStorageKeyNames.java new file mode 100644 index 00000000..db5e6c63 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/JsonStorageKeyNames.java @@ -0,0 +1,13 @@ +package com.unity3d.services.core.device.reader; + +public class JsonStorageKeyNames { + public static final String UNIFIED_CONFIG_KEY = "unifiedconfig"; + public static final String UNIFIED_CONFIG_PII_KEY = "unifiedconfig.pii"; + public static final String ADVERTISING_TRACKING_ID_KEY = "advertisingTrackingId"; + public static final String ADVERTISING_TRACKING_ID_NORMALIZED_KEY = UNIFIED_CONFIG_PII_KEY + "." + ADVERTISING_TRACKING_ID_KEY; + public static final String PRIVACY_SPM_KEY = "privacy.spm.value"; + public static final String PRIVACY_MODE_KEY = "privacy.mode.value"; + public static final String USER_NON_BEHAVIORAL_KEY = "user.nonBehavioral"; + public static final String USER_NON_BEHAVIORAL_VALUE_KEY = "user.nonbehavioral.value"; + public static final String USER_NON_BEHAVIORAL_VALUE_ALT_KEY = "user.nonBehavioral.value"; +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/DataSelectorResult.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/DataSelectorResult.java new file mode 100644 index 00000000..b5ec3b90 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/DataSelectorResult.java @@ -0,0 +1,6 @@ +package com.unity3d.services.core.device.reader.pii; +public enum DataSelectorResult { + INCLUDE, + EXCLUDE, + UPDATE +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataProvider.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataProvider.java new file mode 100644 index 00000000..30bf99a8 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataProvider.java @@ -0,0 +1,10 @@ +package com.unity3d.services.core.device.reader.pii; + +import com.unity3d.services.core.device.Device; + +public class PiiDataProvider { + + public String getAdvertisingTrackingId() { + return Device.getAdvertisingTrackingId(); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataSelector.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataSelector.java new file mode 100644 index 00000000..4bd320d9 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDataSelector.java @@ -0,0 +1,71 @@ +package com.unity3d.services.core.device.reader.pii; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.UNIFIED_CONFIG_PII_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_KEY; + +import com.unity3d.services.core.configuration.Experiments; +import com.unity3d.services.core.misc.IJsonStorageReader; +import com.unity3d.services.core.misc.Utilities; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class PiiDataSelector { + private final PiiTrackingStatusReader _piiTrackingStatusReader; + private final IJsonStorageReader _jsonStorageReader; + private final Experiments _experiments; + + public PiiDataSelector(PiiTrackingStatusReader piiTrackingStatusReader, IJsonStorageReader jsonStorageReader, Experiments experiments) { + _piiTrackingStatusReader = piiTrackingStatusReader; + _jsonStorageReader = jsonStorageReader; + _experiments = experiments; + } + + public PiiDecisionData whatToDoWithPII() { + switch (_piiTrackingStatusReader.getPrivacyMode()) { + case NONE: + case NULL: + return allowTrackingDecision(); + case MIXED: + return mixedModeDecision(); + default: + return notAllowedDecision(); + } + } + + private PiiDecisionData allowTrackingDecision() { + return new PiiDecisionData(_experiments.isUpdatePiiFields() ? DataSelectorResult.UPDATE : DataSelectorResult.INCLUDE, getPiiContentFromStorage()); + } + + private PiiDecisionData notAllowedDecision() { + return new PiiDecisionData(DataSelectorResult.EXCLUDE); + } + + private PiiDecisionData mixedModeDecision() { + if (_piiTrackingStatusReader.getUserNonBehavioralFlag()) { + return new PiiDecisionData(DataSelectorResult.INCLUDE, getUserBehavioralAttribute()); + } else { + PiiDecisionData mixedDecision = allowTrackingDecision(); + mixedDecision.appendData(getUserBehavioralAttribute()); + return mixedDecision; + } + } + + private HashMap getUserBehavioralAttribute() { + return new HashMap() {{ + put(USER_NON_BEHAVIORAL_KEY, _piiTrackingStatusReader.getUserNonBehavioralFlag()); + }}; + } + + private Map getPiiContentFromStorage() { + Object piiData = _jsonStorageReader.get(UNIFIED_CONFIG_PII_KEY); + Map piiDataMap = new HashMap<>(); + if (piiData instanceof JSONObject) { + piiDataMap = Utilities.combineJsonIntoMap(piiDataMap, (JSONObject) piiData, UNIFIED_CONFIG_PII_KEY + "."); + } + return piiDataMap; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDecisionData.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDecisionData.java new file mode 100644 index 00000000..8235468a --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiDecisionData.java @@ -0,0 +1,46 @@ +package com.unity3d.services.core.device.reader.pii; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_KEY; + +import java.util.HashMap; +import java.util.Map; + +public class PiiDecisionData { + private final Map _attributes; + private final DataSelectorResult _resultType; + + public PiiDecisionData(DataSelectorResult resultType) { + this(resultType, new HashMap()); + } + + public PiiDecisionData(DataSelectorResult resultType, Map attributes) { + _resultType = resultType; + _attributes = attributes; + } + + public void appendData(Map appendedAttributes) { + if (_attributes != null) { + _attributes.putAll(appendedAttributes); + } + } + + public DataSelectorResult getResultType() { + return _resultType; + } + + public Map getAttributes() { + return _attributes; + } + + public Boolean getUserNonBehavioralFlag() { + Boolean userNonBehavioralFlag = null; + if (_attributes != null) { + Object userNonBehavioralFlagObj = _attributes.get(USER_NON_BEHAVIORAL_KEY); + if (userNonBehavioralFlagObj instanceof Boolean) { + userNonBehavioralFlag = (Boolean) userNonBehavioralFlagObj; + } + } + return userNonBehavioralFlag; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiPrivacyMode.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiPrivacyMode.java new file mode 100644 index 00000000..449cd0cf --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiPrivacyMode.java @@ -0,0 +1,23 @@ +package com.unity3d.services.core.device.reader.pii; + +import java.util.Locale; + +public enum PiiPrivacyMode { + APP, + NONE, + MIXED, + UNDEFINED, + NULL; + + public static PiiPrivacyMode getPiiPrivacyMode(String privacyModeStr) { + if (privacyModeStr == null) return NULL; + PiiPrivacyMode piiPrivacyMode = UNDEFINED; + try { + piiPrivacyMode = valueOf(privacyModeStr.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + // If it can't find the value, it will default to UNDEFINED. + } + return piiPrivacyMode; + + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiTrackingStatusReader.java b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiTrackingStatusReader.java new file mode 100644 index 00000000..9d41fecd --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/device/reader/pii/PiiTrackingStatusReader.java @@ -0,0 +1,72 @@ +package com.unity3d.services.core.device.reader.pii; + +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.PRIVACY_MODE_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.PRIVACY_SPM_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_VALUE_ALT_KEY; +import static com.unity3d.services.core.device.reader.JsonStorageKeyNames.USER_NON_BEHAVIORAL_VALUE_KEY; + +import com.unity3d.services.core.misc.IJsonStorageReader; + +public class PiiTrackingStatusReader { + + private final IJsonStorageReader _jsonStorageReader; + + public PiiTrackingStatusReader(IJsonStorageReader jsonStorageReader) { + _jsonStorageReader = jsonStorageReader; + } + + public PiiPrivacyMode getPrivacyMode() { + if (getUserPrivacyMode() == PiiPrivacyMode.NULL && getSpmPrivacyMode() == PiiPrivacyMode.NULL) { + return PiiPrivacyMode.NULL; + } + + if (getUserPrivacyMode() == PiiPrivacyMode.APP || getSpmPrivacyMode() == PiiPrivacyMode.APP) { + return PiiPrivacyMode.APP; + } + + if (getUserPrivacyMode() == PiiPrivacyMode.MIXED || getSpmPrivacyMode() == PiiPrivacyMode.MIXED) { + return PiiPrivacyMode.MIXED; + } + + if (getUserPrivacyMode() == PiiPrivacyMode.NONE || getSpmPrivacyMode() == PiiPrivacyMode.NONE) { + return PiiPrivacyMode.NONE; + } + + return PiiPrivacyMode.UNDEFINED; + } + + public boolean getUserNonBehavioralFlag() { + boolean userNonBehavioralFlag = false; + if (_jsonStorageReader != null) { + Object privacyModeObj = _jsonStorageReader.get(USER_NON_BEHAVIORAL_VALUE_KEY); + if (privacyModeObj == null) { + privacyModeObj = _jsonStorageReader.get(USER_NON_BEHAVIORAL_VALUE_ALT_KEY); + } + if (privacyModeObj instanceof String) { + userNonBehavioralFlag = Boolean.parseBoolean((String)privacyModeObj); + } else if (privacyModeObj instanceof Boolean) { + userNonBehavioralFlag = (boolean) privacyModeObj; + } + } + return userNonBehavioralFlag; + } + + private PiiPrivacyMode getUserPrivacyMode() { + return getPrivacyMode(PRIVACY_MODE_KEY); + } + + private PiiPrivacyMode getSpmPrivacyMode() { + return getPrivacyMode(PRIVACY_SPM_KEY); + } + + private PiiPrivacyMode getPrivacyMode(String storageKey) { + String privacyMode = null; + if (_jsonStorageReader != null) { + Object privacyModeObj = _jsonStorageReader.get(storageKey); + if (privacyModeObj instanceof String) { + privacyMode = (String)privacyModeObj; + } + } + return PiiPrivacyMode.getPiiPrivacyMode(privacyMode); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/lifecycle/CachedLifecycle.java b/unity-ads/src/main/java/com/unity3d/services/core/lifecycle/CachedLifecycle.java new file mode 100644 index 00000000..a705a58b --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/lifecycle/CachedLifecycle.java @@ -0,0 +1,36 @@ +package com.unity3d.services.core.lifecycle; + +import android.annotation.TargetApi; + +import com.unity3d.services.core.properties.ClientProperties; + +public class CachedLifecycle { + + private static LifecycleCache _listener; + + public static void register() { + if (ClientProperties.getApplication() != null) { + if (getLifecycleListener() == null) { + setLifecycleListener(new LifecycleCache()); + ClientProperties.getApplication().registerActivityLifecycleCallbacks(getLifecycleListener()); + } + } + } + + public static void unregister() { + if (ClientProperties.getApplication() != null) { + if (getLifecycleListener() != null) { + ClientProperties.getApplication().unregisterActivityLifecycleCallbacks(getLifecycleListener()); + setLifecycleListener(null); + } + } + } + + public static LifecycleCache getLifecycleListener () { + return _listener; + } + + public static void setLifecycleListener (LifecycleCache listener) { + _listener = listener; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/lifecycle/LifecycleCache.java b/unity-ads/src/main/java/com/unity3d/services/core/lifecycle/LifecycleCache.java new file mode 100644 index 00000000..0ccaa280 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/lifecycle/LifecycleCache.java @@ -0,0 +1,65 @@ +package com.unity3d.services.core.lifecycle; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.util.Log; + +import com.unity3d.services.core.webview.WebViewApp; +import com.unity3d.services.core.webview.WebViewEventCategory; + +import java.util.ArrayList; + +@TargetApi(14) +public class LifecycleCache implements Application.ActivityLifecycleCallbacks { + + private LifecycleEvent _currentState = LifecycleEvent.RESUMED; + private boolean _appActive = true; + + @Override + public void onActivityCreated(Activity activity, Bundle bundle) { + _currentState = LifecycleEvent.CREATED; + } + + @Override + public void onActivityStarted(Activity activity) { + _currentState = LifecycleEvent.STARTED; + } + + @Override + public void onActivityResumed(Activity activity) { + _currentState = LifecycleEvent.RESUMED; + _appActive = true; + } + + @Override + public void onActivityPaused(Activity activity) { + _currentState = LifecycleEvent.PAUSED; + _appActive = false; + } + + @Override + public void onActivityStopped(Activity activity) { + _currentState = LifecycleEvent.STOPPED; + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { + _currentState = LifecycleEvent.SAVE_INSTANCE_STATE; + } + + @Override + public void onActivityDestroyed(Activity activity) { + _currentState = LifecycleEvent.DESTROYED; + } + + public LifecycleEvent getCurrentState() { + return _currentState; + } + + public boolean isAppActive() { + return _appActive; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/misc/IJsonStorageReader.java b/unity-ads/src/main/java/com/unity3d/services/core/misc/IJsonStorageReader.java new file mode 100644 index 00000000..11761ef0 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/misc/IJsonStorageReader.java @@ -0,0 +1,8 @@ +package com.unity3d.services.core.misc; + +import org.json.JSONObject; + +public interface IJsonStorageReader { + JSONObject getData(); + Object get(String key); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonFlattener.java b/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonFlattener.java new file mode 100644 index 00000000..2b858f89 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonFlattener.java @@ -0,0 +1,78 @@ +package com.unity3d.services.core.misc; + +import com.unity3d.services.core.log.DeviceLog; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.List; + +public class JsonFlattener { + private final JSONObject _jsonData; + + public JsonFlattener(JSONObject jsonData) { + _jsonData = jsonData; + } + + public JSONObject flattenJson(String separator, List topLevelToInclude, List reduceKeys, List skipKeys) { + JSONObject flattenedJson = new JSONObject(); + try { + for (Iterator keyItor = _jsonData.keys(); keyItor.hasNext(); ) { + String key = keyItor.next(); + if (!shouldIncludeKey(key, topLevelToInclude, skipKeys)) { + continue; + } + Object value = _jsonData.opt(key); + if (value instanceof JSONObject) { + // Flatten from this JSONObject + JsonFlattener jsonFlattener = new JsonFlattener((JSONObject) value); + jsonFlattener.flattenJson(separator, key, flattenedJson, reduceKeys, skipKeys); + } else { + flattenedJson.put(key, value); + } + } + } catch (JSONException e) { + DeviceLog.error("Could not flatten JSON: %s", e.getMessage()); + } + return flattenedJson; + } + + + public boolean shouldIncludeKey(String keyToInclude, List includeList, List skipKeys) { + if (skipKeys.contains(keyToInclude)) { + return false; + } + + if (includeList.size() <= 0) { + return false; + } + + return includeList.contains(keyToInclude); + } + + public void flattenJson(String separator, String parentName, JSONObject outputDictionary, List reduceKeys, List skipKeys) throws JSONException { + for (Iterator keyItor = _jsonData.keys(); keyItor.hasNext(); ) { + String key = keyItor.next(); + if (skipKeys.contains(key)) { + continue; + } + + Object value = _jsonData.get(key); + String newKey; + if (reduceKeys.contains(key)) { + newKey = parentName; + } else { + newKey = String.format("%s%s%s", parentName, separator, key); + } + + if (value instanceof JSONObject) { + // Flatten from this JSONObject + JsonFlattener jsonFlattener = new JsonFlattener((JSONObject) value); + jsonFlattener.flattenJson(separator, newKey, outputDictionary, reduceKeys, skipKeys); + } else { + outputDictionary.put(newKey, value); + } + } + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorage.java b/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorage.java index 1528b301..091cd110 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorage.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorage.java @@ -12,7 +12,7 @@ import java.util.Iterator; import java.util.List; -public class JsonStorage { +public class JsonStorage implements IJsonStorageReader { private JSONObject _data; public synchronized boolean initData () { @@ -28,6 +28,7 @@ public synchronized void setData (JSONObject data) { _data = data; } + @Override public synchronized JSONObject getData () { return _data; } @@ -72,6 +73,7 @@ public synchronized boolean set (String key, Object value) { return true; } + @Override public synchronized Object get (String key) { if (_data == null) { DeviceLog.error("Data is NULL, readStorage probably not called"); diff --git a/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorageAggregator.java b/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorageAggregator.java new file mode 100644 index 00000000..b133868d --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/misc/JsonStorageAggregator.java @@ -0,0 +1,42 @@ +package com.unity3d.services.core.misc; + +import com.unity3d.services.core.log.DeviceLog; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +public class JsonStorageAggregator implements IJsonStorageReader { + private final List _jsonStorageReaders; + + public JsonStorageAggregator(List jsonStorageReaders) { + _jsonStorageReaders = jsonStorageReaders; + } + + @Override + public JSONObject getData() { + JSONObject mergedData = new JSONObject(); + for(IJsonStorageReader jsonStorageReader : _jsonStorageReaders) { + try { + if (jsonStorageReader != null) { + mergedData = Utilities.mergeJsonObjects(mergedData, jsonStorageReader.getData()); + } + } catch (JSONException e) { + DeviceLog.error("Failed to merge storage: " + jsonStorageReader); + } + } + return mergedData; + } + + @Override + public Object get(String key) { + Object foundValue = null; + for(IJsonStorageReader jsonStorageReader : _jsonStorageReaders) { + foundValue = jsonStorageReader.get(key); + if (foundValue != null) break; + } + return foundValue; + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/misc/Utilities.java b/unity-ads/src/main/java/com/unity3d/services/core/misc/Utilities.java index 15c870d4..ca1bef97 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/misc/Utilities.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/misc/Utilities.java @@ -17,7 +17,9 @@ import java.nio.file.Files; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; public class Utilities { public static void runOnUiThread(Runnable runnable) { @@ -187,4 +189,21 @@ public static JSONObject mergeJsonObjects(JSONObject primary, JSONObject seconda return newJsonObject; } + + public static Map combineJsonIntoMap(Map outputMap, JSONObject jsonObject, String prependToKey) { + Map combinedMap = new HashMap<>(outputMap); + for (Iterator it = jsonObject.keys(); it.hasNext(); ) { + String key = it.next(); + combinedMap.put(prependToKey + key, jsonObject.opt(key)); + } + return combinedMap; + } + + public static Map combineJsonIntoMap(Map inputMap, JSONObject jsonObject) { + return combineJsonIntoMap(inputMap, jsonObject, ""); + } + + public static Map convertJsonToMap(JSONObject jsonObject) { + return combineJsonIntoMap(new HashMap(), jsonObject); + } } diff --git a/unity-ads/src/main/java/com/unity3d/services/core/properties/InitializationStatusReader.java b/unity-ads/src/main/java/com/unity3d/services/core/properties/InitializationStatusReader.java new file mode 100644 index 00000000..af8065d2 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/properties/InitializationStatusReader.java @@ -0,0 +1,24 @@ +package com.unity3d.services.core.properties; + +public class InitializationStatusReader { + + private static final String STATE_NOT_INITIALIZED = "not_initialized"; + private static final String STATE_INITIALIZING = "initializing"; + private static final String STATE_INITIALIZED_SUCCESSFULLY = "initialized_successfully"; + private static final String STATE_INITIALIZED_FAILED = "initialized_failed"; + + public String getInitializationStateString(SdkProperties.InitializationState state) { + switch (state) { + case NOT_INITIALIZED: + return STATE_NOT_INITIALIZED; + case INITIALIZING: + return STATE_INITIALIZING; + case INITIALIZED_SUCCESSFULLY: + return STATE_INITIALIZED_SUCCESSFULLY; + case INITIALIZED_FAILED: + return STATE_INITIALIZED_FAILED; + default: + return null; + } + } +} \ No newline at end of file diff --git a/unity-ads/src/main/java/com/unity3d/services/core/properties/SdkProperties.java b/unity-ads/src/main/java/com/unity3d/services/core/properties/SdkProperties.java index bfc1b126..4b41771e 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/properties/SdkProperties.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/properties/SdkProperties.java @@ -1,6 +1,9 @@ package com.unity3d.services.core.properties; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; import com.unity3d.ads.BuildConfig; import com.unity3d.ads.IUnityAdsInitializationListener; @@ -25,7 +28,10 @@ public class SdkProperties { private static final String LOCAL_STORAGE_FILE_PREFIX = "UnityAdsStorage-"; private static final String CHINA_ISO_ALPHA_2_CODE = "CN"; private static final String CHINA_ISO_ALPHA_3_CODE = "CHN"; + private static final String DEFAULT_CONFIG_VERSION = "configv2"; + private static final String CONFIG_VERSION_METADATA_KEY = "com.unity3d.ads.configversion"; private static long _initializationTime = 0; + private static long _initializationTimeEpochMs = 0; private static Configuration _latestConfiguration; private static LinkedHashSet _initializationListeners = new LinkedHashSet(); @@ -128,13 +134,31 @@ public static String getConfigUrl() { public static String getDefaultConfigUrl(String flavor) { boolean isChinaLocale = isChinaLocale(Device.getNetworkCountryISO()); - String baseURI = "https://config.unityads.unity3d.com/webview/"; + String baseURI = "https://" + getConfigVersion(ClientProperties.getApplicationContext()) + ".unityads.unity3d.com/webview/"; if (isChinaLocale) { - baseURI = "https://config.unityads.unitychina.cn/webview/"; + baseURI = "https://" + getConfigVersion(ClientProperties.getApplicationContext()) + ".unityads.unitychina.cn/webview/"; } return baseURI + getWebViewBranch() + "/" + flavor + "/config.json"; } + public static String getConfigVersion(Context context) { + String configVersion = DEFAULT_CONFIG_VERSION; + + try { + if (context != null) { + ApplicationInfo app = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = app.metaData; + if (bundle != null) { + configVersion = bundle.getString(CONFIG_VERSION_METADATA_KEY, DEFAULT_CONFIG_VERSION); + } + } + } catch (PackageManager.NameNotFoundException e) { + DeviceLog.warning("Failed to retrieve application info for current package"); + } + + return configVersion; + } + private static String getWebViewBranch() { if (BuildConfig.DEBUG) { return BuildConfig.WEBVIEW_BRANCH; @@ -190,6 +214,14 @@ public static long getInitializationTime() { return _initializationTime; } + public static void setInitializationTimeSinceEpoch(long initializationTimeEpochMs) { + _initializationTimeEpochMs = initializationTimeEpochMs; + } + + public static long getInitializationTimeEpoch() { + return _initializationTimeEpochMs; + } + public static void setReinitialized(boolean status) { _reinitialized = status; } diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetricSender.java b/unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetricSender.java deleted file mode 100644 index a5590483..00000000 --- a/unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetricSender.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.unity3d.services.core.request; - -import java.util.HashMap; - -public interface ISDKMetricSender { - void SendSDKMetricEvent(SDKMetricEvents metricEvent); - void SendSDKMetricEventWithTag(SDKMetricEvents metricEvent, HashMap tags); -} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetrics.java b/unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetrics.java deleted file mode 100644 index 9de72ea5..00000000 --- a/unity-ads/src/main/java/com/unity3d/services/core/request/ISDKMetrics.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.unity3d.services.core.request; - -import java.util.Map; - -public interface ISDKMetrics { - void sendEvent(final String event); - void sendEventWithTags(final String event, final Map tags); -} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetrics.java b/unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetrics.java deleted file mode 100644 index b58057bd..00000000 --- a/unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetrics.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.unity3d.services.core.request; - -import android.text.TextUtils; - -import com.unity3d.services.core.configuration.Configuration; -import com.unity3d.services.core.device.Device; -import com.unity3d.services.core.log.DeviceLog; -import com.unity3d.services.core.properties.SdkProperties; - -import org.json.JSONObject; - -import java.util.Map; -import java.util.Random; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public final class SDKMetrics { - private static ISDKMetrics _instance = new NullInstance(); - - public static void setConfiguration(Configuration configuration) { - - if (configuration == null) { - DeviceLog.debug("Metrics will not be sent from the device for this session due to misconfiguration"); - return; - } - - // Always attempt to shutdown previous MetricInstance ExecutorService Thread - if (_instance instanceof MetricInstance) { - ((MetricInstance) _instance).shutdown(); - } - - if (configuration.getMetricSampleRate() >= new Random().nextInt(99) + 1) { - _instance = new MetricInstance(configuration.getMetricsUrl()); - } else { - DeviceLog.debug("Metrics will not be sent from the device for this session"); - _instance = new NullInstance(); - } - } - - public static ISDKMetrics getInstance() { - return _instance; - } - - private final static class NullInstance implements ISDKMetrics { - public void sendEvent(final String event) { - DeviceLog.debug("Metric " + event + " was skipped from being sent"); - } - public void sendEventWithTags(final String event, final Map tags) { - sendEvent(event); - } - } - - private final static class MetricInstance implements ISDKMetrics { - - private final String _metricsUrl; - private final ExecutorService _executorService; - - public MetricInstance(String url) { - _metricsUrl = url; - _executorService = Executors.newSingleThreadExecutor(); - } - - protected void shutdown() { - // Allow scheduled tasks to finish execution - _executorService.shutdown(); - } - - public void sendEvent(final String event) { - sendEventWithTags(event, null); - } - - public void sendEventWithTags(final String event, final Map tags) { - if (TextUtils.isEmpty(event)) { - DeviceLog.debug("Metric event not sent due to being nil or empty: " + event); - return; - } - - if (TextUtils.isEmpty(_metricsUrl)) { - DeviceLog.debug("Metric: " + event + " was not sent to nil or empty endpoint: " + _metricsUrl); - return; - } - - if (_executorService.isShutdown()) { - DeviceLog.debug("Metric " + event + " was not sent due to misconfiguration"); - return; - } - - _executorService.submit(new Runnable() { - @Override - public void run() { - try { - String tagString = ""; - if (tags != null) { - JSONObject jsonTags = new JSONObject(tags); - tagString = ",\"t\":" + jsonTags.toString(); - } - String postBody = "{\"m\":[{\"n\":\"" + event + "\"" + tagString + "}],\"t\":{\"iso\":\"" - + Device.getNetworkCountryISO() + "\",\"plt\":\"android\",\"sdv\":\"" - + SdkProperties.getVersionName() + "\"}}"; - - WebRequest request = new WebRequest(_metricsUrl, "POST", null); - request.setBody(postBody); - request.makeRequest(); - - boolean is2XXResponseCode = (request.getResponseCode() / 100) == 2; - if (is2XXResponseCode) { - DeviceLog.debug("Metric " + event + " sent to " + _metricsUrl); - } else { - DeviceLog.debug("Metric " + event + " failed to send with response code: " + request.getResponseCode()); - } - } catch (Exception e) { - DeviceLog.debug("Metric " + event + " failed to send from exception: " + e.getMessage()); - } - } - }); - } - } -} - diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/WebRequest.java b/unity-ads/src/main/java/com/unity3d/services/core/request/WebRequest.java index 718d791a..0b682a56 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/request/WebRequest.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/WebRequest.java @@ -13,6 +13,7 @@ import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -20,8 +21,8 @@ public class WebRequest { private URL _url; - private String _requestType = RequestType.GET.name(); - private String _body; + private String _requestType; + private byte[] _body; private Map> _headers; private Map> _responseHeaders; private int _responseCode = -1; @@ -40,6 +41,10 @@ public enum RequestType { HEAD } + public WebRequest (String url, String requestType) throws MalformedURLException { + this(url, requestType, null); + } + public WebRequest (String url, String requestType, Map> headers) throws MalformedURLException { this(url, requestType, headers, 30000, 30000); } @@ -68,11 +73,15 @@ public String getRequestType () { return _requestType; } - public String getBody () { + public byte[] getBody () { return _body; } public void setBody (String body) { + _body = body.getBytes(StandardCharsets.UTF_8); + } + + public void setBody (byte[] body) { _body = body; } @@ -124,15 +133,18 @@ public long makeStreamRequest(OutputStream outputStream) throws Exception { if (getRequestType().equals(RequestType.POST.name())) { connection.setDoOutput(true); - PrintWriter pout = null; + OutputStream pout = null; try { - pout = new PrintWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8"), true); + pout = connection.getOutputStream(); if (getBody() == null) { - pout.print(getQuery()); + String query = getQuery(); + if (query != null) { + pout.write(query.getBytes(StandardCharsets.UTF_8)); + } } else { - pout.print(getBody()); + pout.write(getBody()); } pout.flush(); diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/IMetricSenderWithBatch.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/IMetricSenderWithBatch.java new file mode 100644 index 00000000..e5862047 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/IMetricSenderWithBatch.java @@ -0,0 +1,6 @@ +package com.unity3d.services.core.request.metrics; + +public interface IMetricSenderWithBatch extends ISDKMetrics { + void updateOriginal(ISDKMetrics metrics); + void sendQueueIfNeeded(); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetricSender.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetricSender.java new file mode 100644 index 00000000..73dfb030 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetricSender.java @@ -0,0 +1,8 @@ +package com.unity3d.services.core.request.metrics; + +import java.util.Map; + +public interface ISDKMetricSender { + void sendSDKMetricEvent(SDKMetricEvents metricEvent); + void sendSDKMetricEventWithTag(SDKMetricEvents metricEvent, Map tags); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetrics.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetrics.java new file mode 100644 index 00000000..900ff6e5 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/ISDKMetrics.java @@ -0,0 +1,13 @@ +package com.unity3d.services.core.request.metrics; + +import java.util.List; +import java.util.Map; + +public interface ISDKMetrics { + void sendEvent(final String event); + void sendEvent(final String event, String value, final Map tags); + void sendEvent(final String event, final Map tags); + void sendMetric(final Metric metric); + void sendMetrics(final List metrics); + String getMetricEndPoint(); +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/Metric.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/Metric.java new file mode 100644 index 00000000..92658e60 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/Metric.java @@ -0,0 +1,63 @@ +package com.unity3d.services.core.request.metrics; + + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Metric { + + private static final String METRIC_NAME = "n"; + private static final String METRIC_VALUE = "v"; + private static final String METRIC_TAGS = "t"; + + private final String name; + private final Object value; + private final Map tags; + + public Metric(String name, Object value, Map tags) { + this.name = name; + this.value = value; + this.tags = tags; + } + + Map asMap() { + Map result = new HashMap<>(); + + if (name != null) { + result.put(METRIC_NAME, name); + } + + if (value != null) { + result.put(METRIC_VALUE, value); + } + + if (tags != null) { + result.put(METRIC_TAGS, tags); + } + + return result; + } + + @Override + public String toString() { + return "Metric{" + + "name='" + name + '\'' + + ", value='" + value + '\'' + + ", tags=" + tags + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Metric metric = (Metric) o; + return name.equals(metric.name) && value.equals(metric.value) && tags.equals(metric.tags); + } + + @Override + public int hashCode() { + return Objects.hash(name, value, tags); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricCommonTags.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricCommonTags.java new file mode 100644 index 00000000..dc9fe418 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricCommonTags.java @@ -0,0 +1,37 @@ +package com.unity3d.services.core.request.metrics; + +import java.util.HashMap; +import java.util.Map; + +public class MetricCommonTags { + + private static final String METRIC_COMMON_TAG_COUNTRY_ISO = "iso"; + private static final String METRIC_COMMON_TAG_PLATFORM = "plt"; + private static final String METRIC_COMMON_TAG_SDK_VERSION = "sdk"; + private static final String METRIC_COMMON_TAG_SYSTEM_VERSION = "system"; + + public static final String METRIC_COMMON_TAG_PLATFORM_ANDROID = "android"; + + private String _countryISO; + private String _platform; + private String _sdkVersion; + private String _systemVersion; + + public MetricCommonTags(String countryISO, String platform, String sdkVersion, String systemVersion) { + this._countryISO = countryISO; + this._platform = platform; + this._sdkVersion = sdkVersion; + this._systemVersion = systemVersion; + } + + public Map asMap() { + Map result = new HashMap<>(); + + result.put(METRIC_COMMON_TAG_COUNTRY_ISO, _countryISO); + result.put(METRIC_COMMON_TAG_PLATFORM, _platform); + result.put(METRIC_COMMON_TAG_SDK_VERSION, _sdkVersion); + result.put(METRIC_COMMON_TAG_SYSTEM_VERSION, _systemVersion); + + return result; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSender.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSender.java new file mode 100644 index 00000000..ac42ad13 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSender.java @@ -0,0 +1,109 @@ +package com.unity3d.services.core.request.metrics; + +import static com.unity3d.services.core.request.metrics.MetricCommonTags.METRIC_COMMON_TAG_PLATFORM_ANDROID; + +import android.text.TextUtils; + +import com.unity3d.services.core.device.Device; +import com.unity3d.services.core.log.DeviceLog; +import com.unity3d.services.core.properties.SdkProperties; +import com.unity3d.services.core.request.WebRequest; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class MetricSender implements ISDKMetrics { + + private final MetricCommonTags _commonTags; + private final String _metricEndpoint; + private final ExecutorService _executorService; + + public MetricSender(String url) { + _metricEndpoint = url; + _executorService = Executors.newSingleThreadExecutor(); + _commonTags = new MetricCommonTags( + Device.getNetworkCountryISO(), + METRIC_COMMON_TAG_PLATFORM_ANDROID, + SdkProperties.getVersionName(), + Device.getOsVersion()); + } + + + public void sendEvent(String event) { + sendEvent(event, null); + } + + public void sendEvent(String event, Map tags) { + if (event == null || event.isEmpty()) { + DeviceLog.debug("Metric event not sent due to being null or empty: " + event); + return; + } + + sendEvent(event, null, tags); + } + + public void sendEvent(String event, String value, Map tags) { + sendMetrics(new ArrayList<>(Collections.singletonList(new Metric(event, value, tags)))); + } + + public void sendMetric(Metric metric) { + sendMetrics(new ArrayList<>(Collections.singletonList(metric))); + } + + public void sendMetrics(final List metrics) { + + if (metrics == null || metrics.size() <= 0) { + DeviceLog.debug("Metrics event not send due to being null or empty"); + return; + } + + if (TextUtils.isEmpty(_metricEndpoint)) { + DeviceLog.debug("Metrics: " + metrics + " was not sent to null or empty endpoint: " + _metricEndpoint); + return; + } + + if (_executorService.isShutdown()) { + DeviceLog.debug("Metrics " + metrics + " was not sent due to misconfiguration"); + return; + } + + _executorService.submit(new Runnable() { + @Override + public void run() { + try { + MetricsContainer container = new MetricsContainer(_commonTags, metrics); + String postBody = new JSONObject(container.asMap()).toString(); + + WebRequest request = new WebRequest(_metricEndpoint, "POST", null); + request.setBody(postBody); + request.makeRequest(); + + boolean is2XXResponseCode = (request.getResponseCode() / 100) == 2; + if (is2XXResponseCode) { + DeviceLog.debug("Metric " + metrics + " sent to " + _metricEndpoint); + } else { + DeviceLog.debug("Metric " + metrics + " failed to send with response code: " + request.getResponseCode()); + } + } catch (Exception e) { + DeviceLog.debug("Metric " + metrics + " failed to send from exception: " + e.getMessage()); + } + } + }); + + } + + public String getMetricEndPoint() { + return _metricEndpoint; + } + + void shutdown() { + // Allow scheduled tasks to finish execution + _executorService.shutdown(); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatch.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatch.java new file mode 100644 index 00000000..94b5b278 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatch.java @@ -0,0 +1,72 @@ +package com.unity3d.services.core.request.metrics; + +import android.text.TextUtils; + +import com.unity3d.services.core.log.DeviceLog; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +public class MetricSenderWithBatch implements IMetricSenderWithBatch { + + private final LinkedBlockingQueue _queue = new LinkedBlockingQueue<>(); + private ISDKMetrics _original; + + public MetricSenderWithBatch(ISDKMetrics metrics) { + this._original = metrics; + } + @Override + public void updateOriginal(ISDKMetrics metrics) { + _original = metrics; + } + + @Override + public void sendEvent(String event) { + sendEvent(event, null); + } + + @Override + public void sendEvent(String event, String value, Map tags) { + if (event == null || event.isEmpty()) { + DeviceLog.debug("Metric event not sent due to being null or empty: " + event); + return; + } + + sendMetrics(new ArrayList<>(Collections.singletonList(new Metric(event, value, tags)))); + } + + @Override + public void sendEvent(String event, Map tags) { + sendEvent(event, null, tags); + } + + @Override + public void sendMetric(Metric metric) { + sendMetrics(new ArrayList<>(Collections.singletonList(metric))); + } + + @Override + public synchronized void sendMetrics(List metrics) { + _queue.addAll(metrics); + + if (!TextUtils.isEmpty(_original.getMetricEndPoint()) && _queue.size() > 0) { + List eventsToSend = new ArrayList<>(); + _queue.drainTo(eventsToSend); + _original.sendMetrics(eventsToSend); + } + + } + + @Override + public String getMetricEndPoint() { + return _original == null ? null : _original.getMetricEndPoint(); + } + + @Override + public void sendQueueIfNeeded() { + sendMetrics(new ArrayList()); + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricsContainer.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricsContainer.java new file mode 100644 index 00000000..086c3fa7 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/MetricsContainer.java @@ -0,0 +1,34 @@ +package com.unity3d.services.core.request.metrics; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MetricsContainer { + + private static final String METRICS_CONTAINER = "m"; + private static final String METRICS_CONTAINER_TAGS = "t"; + + private final MetricCommonTags _commonTags; + private final List _metrics; + + public MetricsContainer(MetricCommonTags commonTags, List metrics) { + this._commonTags = commonTags; + this._metrics = metrics; + } + + public Map asMap() { + Map result = new HashMap<>(); + List metricsMaps = new ArrayList<>(); + + for (Metric metric: _metrics) { + metricsMaps.add(metric.asMap()); + } + + result.put(METRICS_CONTAINER, metricsMaps); + result.put(METRICS_CONTAINER_TAGS, _commonTags.asMap()); + + return result; + } +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetricEvents.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetricEvents.java similarity index 80% rename from unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetricEvents.java rename to unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetricEvents.java index eef9c9f0..4984508b 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetricEvents.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetricEvents.java @@ -1,4 +1,4 @@ -package com.unity3d.services.core.request; +package com.unity3d.services.core.request.metrics; public enum SDKMetricEvents { native_load_callback_error, diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetricSender.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetricSender.java similarity index 55% rename from unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetricSender.java rename to unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetricSender.java index c394c27f..9ec54d76 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/request/SDKMetricSender.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetricSender.java @@ -1,19 +1,19 @@ -package com.unity3d.services.core.request; +package com.unity3d.services.core.request.metrics; -import java.util.HashMap; +import java.util.Map; public class SDKMetricSender implements ISDKMetricSender { - public void SendSDKMetricEvent(SDKMetricEvents metricEvent) { + public void sendSDKMetricEvent(SDKMetricEvents metricEvent) { if (metricEvent == null) return; ISDKMetrics sdkMetric = SDKMetrics.getInstance(); if (sdkMetric == null) return; sdkMetric.sendEvent(metricEvent.toString()); } - public void SendSDKMetricEventWithTag(SDKMetricEvents metricEvent, HashMap tags) { + public void sendSDKMetricEventWithTag(SDKMetricEvents metricEvent, Map tags) { if (metricEvent == null) return; ISDKMetrics sdkMetric = SDKMetrics.getInstance(); if (sdkMetric == null) return; - sdkMetric.sendEventWithTags(metricEvent.toString(), tags); + sdkMetric.sendEvent(metricEvent.toString(), tags); } } diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetrics.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetrics.java new file mode 100644 index 00000000..321e8463 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/SDKMetrics.java @@ -0,0 +1,92 @@ +package com.unity3d.services.core.request.metrics; + + +import com.unity3d.services.core.configuration.Configuration; +import com.unity3d.services.core.log.DeviceLog; + +import java.util.List; +import java.util.Map; +import java.util.Random; + +public final class SDKMetrics { + + private static final String NULL_INSTANCE_METRICS_URL = "nullInstanceMetricsUrl"; + + private static ISDKMetrics _instance; + private static MetricSenderWithBatch _batchedSender; + + public static void setConfiguration(Configuration configuration) { + + if (configuration == null) { + DeviceLog.debug("Metrics will not be sent from the device for this session due to misconfiguration"); + return; + } + + // Always attempt to shutdown previous MetricInstance ExecutorService Thread + if (_instance instanceof MetricSender) { + ((MetricSender) _instance).shutdown(); + } + + if (configuration.getMetricSampleRate() >= new Random().nextInt(99) + 1) { + _instance = new MetricSender(configuration.getMetricsUrl()); + } else { + DeviceLog.debug("Metrics will not be sent from the device for this session"); + _instance = new NullInstance(NULL_INSTANCE_METRICS_URL); + } + + if (_batchedSender == null) { + _batchedSender = new MetricSenderWithBatch(_instance); + } else { + _batchedSender.updateOriginal(_instance); + } + + _batchedSender.sendQueueIfNeeded(); + } + + public static synchronized ISDKMetrics getInstance() { + + if (_instance == null) { + _instance = new NullInstance(null); + } + + if (_batchedSender == null) { + _batchedSender = new MetricSenderWithBatch(_instance); + } + + return _batchedSender; + } + + private final static class NullInstance implements ISDKMetrics { + + private final String _metricEndpoint; + + public NullInstance(String url) { + _metricEndpoint = url; + } + + public void sendEvent(final String event) { + DeviceLog.debug("Metric " + event + " was skipped from being sent"); + } + + public void sendEvent(String event, String value, Map tags) { + sendEvent(event); + } + + public void sendEvent(final String event, final Map tags) { + sendEvent(event); + } + + public void sendMetric(Metric metric) { + DeviceLog.debug("Metric " + metric + " was skipped from being sent"); + } + + public void sendMetrics(List metrics) { + DeviceLog.debug("Metrics: " + metrics + " was skipped from being sent"); + } + + public String getMetricEndPoint() { + return _metricEndpoint; + } + } +} + diff --git a/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/TSIMetric.java b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/TSIMetric.java new file mode 100644 index 00000000..89704df7 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/request/metrics/TSIMetric.java @@ -0,0 +1,128 @@ +package com.unity3d.services.core.request.metrics; + +import java.util.Map; + +public class TSIMetric { + + private static final String TSI_METRIC_MISSING_TOKEN = "native_missing_token"; + private static final String TSI_METRIC_MISSING_STATE_ID = "native_missing_state_id"; + private static final String TSI_METRIC_MISSING_GAME_SESSION_ID = "native_missing_game_session_id"; + private static final String TSI_METRIC_INIT_TIME_SUCCESS = "native_initialization_time_success"; + private static final String TSI_METRIC_INIT_TIME_FAILURE = "native_initialization_time_failure"; + private static final String TSI_METRIC_INIT_STARTED = "native_initialization_started"; + private static final String TSI_METRIC_TOKEN_CONFIG = "native_token_availability_latency_config"; + private static final String TSI_METRIC_TOKEN_WEBVIEW = "native_token_availability_latency_webview"; + private static final String TSI_METRIC_TOKEN_RESOLUTION = "native_token_resolution_request_latency"; + private static final String TSI_METRIC_EMERGENCY_OFF = "native_emergency_switch_off"; + private static final String TSI_METRIC_COLLECTION_LATENCY = "native_device_info_collection_latency"; + private static final String TSI_METRIC_COMPRESSION_LATENCY = "native_device_info_compression_latency"; + private static final String TSI_METRIC_TOKEN_AVAILABLE = "native_generated_token_available"; + private static final String TSI_METRIC_TOKEN_NULL = "native_generated_token_null"; + private static final String TSI_METRIC_TOKEN_ASYNC_NULL = "native_async_token_null"; + + public static Metric newMissingToken(Map tags) { + return new Metric( + TSI_METRIC_MISSING_TOKEN, + null, + tags); + } + + public static Metric newMissingStateId(Map tags) { + return new Metric( + TSI_METRIC_MISSING_STATE_ID, + null, + tags); + } + + public static Metric newMissingGameSessionId(Map tags) { + return new Metric( + TSI_METRIC_MISSING_GAME_SESSION_ID, + null, + tags); + } + + public static Metric newInitTimeSuccess(Long value, Map tags) { + return new Metric( + TSI_METRIC_INIT_TIME_SUCCESS, + value, + tags); + } + + public static Metric newInitTimeFailure(Long value, Map tags) { + return new Metric( + TSI_METRIC_INIT_TIME_FAILURE, + value, + tags); + } + + public static Metric newInitStarted(Map tags) { + return new Metric( + TSI_METRIC_INIT_STARTED, + null, + tags); + } + + public static Metric newTokenAvailabilityLatencyConfig(Long value, Map tags) { + return new Metric( + TSI_METRIC_TOKEN_CONFIG, + value, + tags); + } + + public static Metric newTokenAvailabilityLatencyWebview(Long value, Map tags) { + return new Metric( + TSI_METRIC_TOKEN_WEBVIEW, + value, + tags); + } + + public static Metric newTokenResolutionRequestLatency(Long value, Map tags) { + return new Metric( + TSI_METRIC_TOKEN_RESOLUTION, + value, + tags); + } + + public static Metric newEmergencySwitchOff(Map tags) { + return new Metric( + TSI_METRIC_EMERGENCY_OFF, + null, + tags); + } + + public static Metric newDeviceInfoCollectionLatency(Long value, Map tags) { + return new Metric( + TSI_METRIC_COLLECTION_LATENCY, + value, + tags); + } + + public static Metric newDeviceInfoCompressionLatency(Long value, Map tags) { + return new Metric( + TSI_METRIC_COMPRESSION_LATENCY, + value, + tags); + } + + public static Metric newNativeGeneratedTokenAvailable(Map tags) { + return new Metric( + TSI_METRIC_TOKEN_AVAILABLE, + null, + tags); + } + + public static Metric newNativeGeneratedTokenNull(Map tags) { + return new Metric( + TSI_METRIC_TOKEN_NULL, + null, + tags); + } + + public static Metric newAsyncTokenNull(Map tags) { + return new Metric( + TSI_METRIC_TOKEN_ASYNC_NULL, + null, + tags); + } + +} diff --git a/unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewApp.java b/unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewApp.java index 4543f6bd..00b9f592 100644 --- a/unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewApp.java +++ b/unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewApp.java @@ -18,7 +18,7 @@ import com.unity3d.services.core.misc.ViewUtilities; import com.unity3d.services.core.properties.ClientProperties; import com.unity3d.services.core.properties.SdkProperties; -import com.unity3d.services.core.request.SDKMetrics; +import com.unity3d.services.core.request.metrics.SDKMetrics; import com.unity3d.services.core.webview.bridge.CallbackStatus; import com.unity3d.services.core.webview.bridge.Invocation; import com.unity3d.services.core.webview.bridge.NativeCallback; @@ -299,25 +299,34 @@ public void run() { return; } - String queryString = "?platform=android"; + WebViewUrlBuilder webViewUrlBuilder = new WebViewUrlBuilder("file://" + SdkProperties.getLocalWebViewFile(), configuration); + String baseUrl = webViewUrlBuilder.getUrlWithQueryString(); - try { - if(configuration.getWebViewUrl() != null) { - queryString = queryString + "&origin=" + URLEncoder.encode(configuration.getWebViewUrl(), "UTF-8"); + if (baseUrl == null) { + // Just in case of error, fallback to the old way. + String queryString = "?platform=android"; + + try { + if (configuration.getWebViewUrl() != null) { + queryString = queryString + "&origin=" + URLEncoder.encode(configuration.getWebViewUrl(), "UTF-8"); + } + } catch (UnsupportedEncodingException e) { + DeviceLog.exception("Unsupported charset when encoding origin url", e); } - } catch(UnsupportedEncodingException e) { - DeviceLog.exception("Unsupported charset when encoding origin url", e); - } - try { - if(configuration.getWebViewVersion() != null) { - queryString = queryString + "&version=" + URLEncoder.encode(configuration.getWebViewVersion(), "UTF-8"); + try { + if (configuration.getWebViewVersion() != null) { + queryString = queryString + "&version=" + URLEncoder.encode(configuration.getWebViewVersion(), "UTF-8"); + } + } catch (UnsupportedEncodingException e) { + DeviceLog.exception("Unsupported charset when encoding webview version", e); } - } catch(UnsupportedEncodingException e) { - DeviceLog.exception("Unsupported charset when encoding webview version", e); + webViewApp.getWebView().loadDataWithBaseURL("file://" + SdkProperties.getLocalWebViewFile() + queryString, configuration.getWebViewData(), "text/html", "UTF-8", null); + + } else { + webViewApp.getWebView().loadDataWithBaseURL(baseUrl, configuration.getWebViewData(), "text/html", "UTF-8", null); } - webViewApp.getWebView().loadDataWithBaseURL("file://" + SdkProperties.getLocalWebViewFile() + queryString, configuration.getWebViewData(), "text/html", "UTF-8", null); setCurrentApp(webViewApp); } @@ -331,7 +340,7 @@ public void run() { boolean createdSuccessfully = webViewCreateDidNotTimeout && webAppDefined && webAppInitialized; if (!createdSuccessfully) { - SDKMetrics.getInstance().sendEventWithTags("native_webview_creation_failed", new HashMap() {{ + SDKMetrics.getInstance().sendEvent("native_webview_creation_failed", new HashMap() {{ put("wto", "" + !webViewCreateDidNotTimeout); put("wad", "" + webAppDefined); put("wai", "" + webAppInitialized); @@ -366,7 +375,7 @@ public void run() { }); DeviceLog.error("UnityAds Sdk WebView onRenderProcessGone : " + detail.toString()); - SDKMetrics.getInstance().sendEventWithTags("native_webview_render_process_gone", new HashMap() {{ + SDKMetrics.getInstance().sendEvent("native_webview_render_process_gone", new HashMap() {{ // Only apply tags if minimum API Level applies if (Build.VERSION.SDK_INT >= 26) { put("dc", "" + detail.didCrash()); diff --git a/unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewUrlBuilder.java b/unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewUrlBuilder.java new file mode 100644 index 00000000..79828867 --- /dev/null +++ b/unity-ads/src/main/java/com/unity3d/services/core/webview/WebViewUrlBuilder.java @@ -0,0 +1,39 @@ +package com.unity3d.services.core.webview; + +import com.unity3d.services.core.configuration.Configuration; +import com.unity3d.services.core.log.DeviceLog; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +public class WebViewUrlBuilder { + private final String _urlWithQueryString; + + public WebViewUrlBuilder(String baseUrl, Configuration configuration) { + String queryString = "?platform=android"; + + queryString += buildQueryParam("origin", configuration.getWebViewUrl()); + queryString += buildQueryParam("version", configuration.getWebViewVersion()); + if (configuration.getExperiments() != null && configuration.getExperiments().isForwardExperimentsToWebViewEnabled()) { + queryString += buildQueryParam("experiments", configuration.getExperiments().getExperimentData().toString()); + } + + _urlWithQueryString = baseUrl + queryString; + } + + public String getUrlWithQueryString() { + return _urlWithQueryString; + } + + private String buildQueryParam(String fieldName, String fieldData) { + String queryString = ""; + try { + if (fieldData != null) { + queryString += "&" + fieldName + "=" + URLEncoder.encode(fieldData, "UTF-8"); + } + } catch (UnsupportedEncodingException e) { + DeviceLog.exception(String.format("Unsupported charset when encoding %s", fieldName), e); + } + return queryString; + } +} diff --git a/unity-ads/src/test/java/android/text/TextUtils.java b/unity-ads/src/test/java/android/text/TextUtils.java new file mode 100644 index 00000000..d750c026 --- /dev/null +++ b/unity-ads/src/test/java/android/text/TextUtils.java @@ -0,0 +1,8 @@ +package android.text; + + +public class TextUtils { + public static boolean isEmpty(CharSequence str) { + return str == null || str.length() == 0; + } +} \ No newline at end of file diff --git a/unity-ads/src/test/java/android/util/Log.java b/unity-ads/src/test/java/android/util/Log.java new file mode 100644 index 00000000..2788b961 --- /dev/null +++ b/unity-ads/src/test/java/android/util/Log.java @@ -0,0 +1,24 @@ +package android.util; + +public class Log { + public static int d(String tag, String msg) { + System.out.println("DEBUG: " + tag + ": " + msg); + return 0; + } + + public static int i(String tag, String msg) { + System.out.println("INFO: " + tag + ": " + msg); + return 0; + } + + public static int w(String tag, String msg) { + System.out.println("WARN: " + tag + ": " + msg); + return 0; + } + + public static int e(String tag, String msg) { + System.out.println("ERROR: " + tag + ": " + msg); + return 0; + } + +} \ No newline at end of file diff --git a/unity-ads/src/test/java/com/unity3d/services/core/configuration/InitializeEventsMetricSenderTest.java b/unity-ads/src/test/java/com/unity3d/services/core/configuration/InitializeEventsMetricSenderTest.java new file mode 100644 index 00000000..2e2cc734 --- /dev/null +++ b/unity-ads/src/test/java/com/unity3d/services/core/configuration/InitializeEventsMetricSenderTest.java @@ -0,0 +1,100 @@ +package com.unity3d.services.core.configuration; + + +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.never; + +import com.unity3d.services.core.request.metrics.Metric; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class InitializeEventsMetricSenderTest { + + @Mock + public InitializeEventsMetricSender _metricSenderMock; + + @Test + public void testSendsMetricWhenInitSuccessfulOnlyOnce() { + doCallRealMethod().when(_metricSenderMock).didInitStart(); + doCallRealMethod().when(_metricSenderMock).sdkDidInitialize(); + + _metricSenderMock.didInitStart(); + _metricSenderMock.sdkDidInitialize(); + Mockito.verify(_metricSenderMock, Mockito.times(2)).sendMetric(Mockito.any(Metric.class)); + + _metricSenderMock.sdkDidInitialize(); + Mockito.verify(_metricSenderMock, Mockito.times(2)).sendMetric(Mockito.any(Metric.class)); + } + + @Test + public void testSendsMetricWhenInitFailsOnlyOnce() { + doCallRealMethod().when(_metricSenderMock).didInitStart(); + doCallRealMethod().when(_metricSenderMock).sdkInitializeFailed(Mockito.anyString()); + + _metricSenderMock.didInitStart(); + _metricSenderMock.sdkInitializeFailed(Mockito.anyString()); + Mockito.verify(_metricSenderMock, Mockito.times(2)).sendMetric(Mockito.any(Metric.class)); + + _metricSenderMock.sdkInitializeFailed(Mockito.anyString()); + Mockito.verify(_metricSenderMock, Mockito.times(2)).sendMetric(Mockito.any(Metric.class)); + } + + @Test + public void testSendsMetricWhenGetsTokenOnlyOnce() { + doCallRealMethod().when(_metricSenderMock).didInitStart(); + doCallRealMethod().when(_metricSenderMock).didConfigRequestStart(); + doCallRealMethod().when(_metricSenderMock).sdkTokenDidBecomeAvailableWithConfig(true); + + _metricSenderMock.didInitStart(); + _metricSenderMock.didConfigRequestStart(); + _metricSenderMock.sdkTokenDidBecomeAvailableWithConfig(true); + + Mockito.verify(_metricSenderMock, Mockito.times(3)).sendMetric(Mockito.any(Metric.class)); + + _metricSenderMock.sdkTokenDidBecomeAvailableWithConfig(false); + Mockito.verify(_metricSenderMock, Mockito.times(3)).sendMetric(Mockito.any(Metric.class)); + } + + @Test + public void testSendsMetricWhenGetsTokenWithWebview() { + doCallRealMethod().when(_metricSenderMock).didInitStart(); + doCallRealMethod().when(_metricSenderMock).didConfigRequestStart(); + doCallRealMethod().when(_metricSenderMock).sdkTokenDidBecomeAvailableWithConfig(false); + + _metricSenderMock.didInitStart(); + _metricSenderMock.didConfigRequestStart(); + _metricSenderMock.sdkTokenDidBecomeAvailableWithConfig(false); + Mockito.verify(_metricSenderMock, Mockito.times(2)).sendMetric(Mockito.any(Metric.class)); + + _metricSenderMock.sdkTokenDidBecomeAvailableWithConfig(false); + Mockito.verify(_metricSenderMock, Mockito.times(2)).sendMetric(Mockito.any(Metric.class)); + } + + @Test + public void testDoesNotSendLatencyMetricWhenStartConfigNotCalled() { + doCallRealMethod().when(_metricSenderMock).didInitStart(); + doCallRealMethod().when(_metricSenderMock).sdkTokenDidBecomeAvailableWithConfig(true); + + _metricSenderMock.didInitStart(); + _metricSenderMock.sdkTokenDidBecomeAvailableWithConfig(true); + Mockito.verify(_metricSenderMock, Mockito.times(2)).sendMetric(Mockito.any(Metric.class)); + } + + @Test + public void testDoesNotSendInitMetricWhenInitStartedNotCalled() { + doCallRealMethod().when(_metricSenderMock).sdkDidInitialize(); + doCallRealMethod().when(_metricSenderMock).sdkInitializeFailed(Mockito.anyString()); + + _metricSenderMock.sdkDidInitialize(); + Mockito.verify(_metricSenderMock, never()).sendMetric(Mockito.any(Metric.class)); + + _metricSenderMock.sdkInitializeFailed(Mockito.anyString()); + Mockito.verify(_metricSenderMock, never()).sendMetric(Mockito.any(Metric.class)); + } + +} \ No newline at end of file diff --git a/unity-ads/src/test/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatchTest.java b/unity-ads/src/test/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatchTest.java new file mode 100644 index 00000000..2c169d3e --- /dev/null +++ b/unity-ads/src/test/java/com/unity3d/services/core/request/metrics/MetricSenderWithBatchTest.java @@ -0,0 +1,124 @@ +package com.unity3d.services.core.request.metrics; + + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import static java.lang.Thread.currentThread; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@RunWith(MockitoJUnitRunner.class) +public class MetricSenderWithBatchTest{ + + @Mock + public ISDKMetrics _original; + @InjectMocks + public MetricSenderWithBatch _batchedSender; + + @Test + public void testBatchEventsWhenNoMetricUrlIsSet() { + + _batchedSender.sendEvent("test1"); + _batchedSender.sendEvent("test2"); + + Mockito.verify(_original, never()).sendMetrics(Mockito.anyList()); + } + + @Test + public void testDoesNotSendEventsWhenQueueIfEmptyAfterUrlSet() { + Mockito.when(_original.getMetricEndPoint()).thenReturn("url"); + + _batchedSender.sendQueueIfNeeded(); + + Mockito.verify(_original, never()).sendMetrics(Mockito.anyList()); + assertEquals("url", _batchedSender.getMetricEndPoint()); + } + + @Test + public void testQueueSentWhenMetricEndpointGetsUpdated() { + _batchedSender.sendEvent("test1"); + _batchedSender.sendEvent("test2"); + + Mockito.when(_original.getMetricEndPoint()).thenReturn("url"); + + _batchedSender.sendQueueIfNeeded(); + + Mockito.verify(_original, Mockito.times(1)).sendMetrics(Mockito.anyList()); + } + + @Test + public void testSendMetricsNoConcurrency() { + final int METRICS_SIZE = 1000; + final ArgumentCaptor> metricsCapture = ArgumentCaptor.forClass((Class) List.class); + + Mockito.when(_original.getMetricEndPoint()).thenReturn("url"); + + MetricSenderWithBatch senderWithBatch = new MetricSenderWithBatch(_original); + List metrics = new ArrayList<>(); + + for (int i = 0; i < METRICS_SIZE; i++) { + metrics.add(new Metric(String.valueOf(i), i, null)); + } + + senderWithBatch.sendMetrics(metrics); + + Mockito.verify(_original, Mockito.times(1)).sendMetrics(Mockito.anyList()); + Mockito.verify(_original).sendMetrics(metricsCapture.capture()); + assertEquals(METRICS_SIZE, metricsCapture.getValue().size()); + } + + @Test + public void testSendMetricsConcurrency() throws InterruptedException{ + final int METRICS_SIZE = 1000; + final int NUM_THREADS = 100; + + final ExecutorService service = Executors.newFixedThreadPool(NUM_THREADS); + final CountDownLatch latch = new CountDownLatch(NUM_THREADS); + final ArgumentCaptor> metricsCapture = ArgumentCaptor.forClass((Class) List.class); + + Mockito.when(_original.getMetricEndPoint()).thenReturn("url"); + + final MetricSenderWithBatch senderWithBatch = new MetricSenderWithBatch(_original); + + for (int i = 0; i < NUM_THREADS; i++) { + service.execute(new Runnable() { + @Override + public void run() { + List metrics = new ArrayList<>(); + for (int i = 0; i < METRICS_SIZE / NUM_THREADS; i++) { + metrics.add(new Metric(currentThread().getName(), i, null)); + } + senderWithBatch.sendMetrics(metrics); + latch.countDown(); + } + }); + } + latch.await(); + + Mockito.verify(_original, times(NUM_THREADS)).sendMetrics(Mockito.anyList()); + Mockito.verify(_original, times(NUM_THREADS)).sendMetrics(metricsCapture.capture()); + List> metricsSent = metricsCapture.getAllValues(); + int metricsCount = 0; + for (List lm: metricsSent) { + for (Metric m: lm) { + metricsCount++; + } + } + assertEquals(METRICS_SIZE, metricsCount); + } +} \ No newline at end of file diff --git a/unity-scaradapter-1920/src/androidTest/java/com/unity3d/scar/adapter/v1920/signals/SignalsReaderTest.java b/unity-scaradapter-1920/src/androidTest/java/com/unity3d/scar/adapter/v1920/signals/SignalsReaderTest.java index 7daee56e..9a33ec25 100644 --- a/unity-scaradapter-1920/src/androidTest/java/com/unity3d/scar/adapter/v1920/signals/SignalsReaderTest.java +++ b/unity-scaradapter-1920/src/androidTest/java/com/unity3d/scar/adapter/v1920/signals/SignalsReaderTest.java @@ -28,21 +28,21 @@ public void before() { public void testGetScarSignals() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{"video"}, new String[]{"rewarded"}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } @Test public void testGetScarSignalsNoRewarded() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{"video"}, new String[]{}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } @Test public void testGetScarSignalsNoInterstitial() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{}, new String[]{"rewarded"}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } } diff --git a/unity-scaradapter-1950/src/androidTest/java/com/unity3d/scar/adapter/v1950/signals/SignalsReaderTest.java b/unity-scaradapter-1950/src/androidTest/java/com/unity3d/scar/adapter/v1950/signals/SignalsReaderTest.java index a373dcaa..ad6da555 100644 --- a/unity-scaradapter-1950/src/androidTest/java/com/unity3d/scar/adapter/v1950/signals/SignalsReaderTest.java +++ b/unity-scaradapter-1950/src/androidTest/java/com/unity3d/scar/adapter/v1950/signals/SignalsReaderTest.java @@ -28,21 +28,21 @@ public void before() { public void testGetScarSignals() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{"video"}, new String[]{"rewarded"}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } @Test public void testGetScarSignalsNoRewarded() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{"video"}, new String[]{}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } @Test public void testGetScarSignalsNoInterstitial() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{}, new String[]{"rewarded"}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } } diff --git a/unity-scaradapter-2000/src/androidTest/java/com/unity3d/scar/adapter/v2000/signals/SignalsReaderTest.java b/unity-scaradapter-2000/src/androidTest/java/com/unity3d/scar/adapter/v2000/signals/SignalsReaderTest.java index 62fd1986..e9671135 100644 --- a/unity-scaradapter-2000/src/androidTest/java/com/unity3d/scar/adapter/v2000/signals/SignalsReaderTest.java +++ b/unity-scaradapter-2000/src/androidTest/java/com/unity3d/scar/adapter/v2000/signals/SignalsReaderTest.java @@ -28,21 +28,21 @@ public void before() { public void testGetScarSignals() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{"video"}, new String[]{"rewarded"}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } @Test public void testGetScarSignalsNoRewarded() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{"video"}, new String[]{}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } @Test public void testGetScarSignalsNoInterstitial() { SignalsReader signalsReader = new SignalsReader(new SignalsStorage()); signalsReader.getSCARSignals(context, new String[]{}, new String[]{"rewarded"}, _signalCollectionListener); - Mockito.verify(_signalCollectionListener, Mockito.timeout(5000).times(1)).onSignalsCollected(any(String.class)); + Mockito.verify(_signalCollectionListener, Mockito.timeout(10000).times(1)).onSignalsCollected(any(String.class)); } }