From 1c8e4abf7d8a7e605250c6aace01d7c13a10d4fe Mon Sep 17 00:00:00 2001 From: nelsitoPuglisi Date: Fri, 13 Oct 2023 13:15:23 -0300 Subject: [PATCH] first commit: 6.0.0 --- .github/CODEOWNERS | 15 + .github/PULL_REQUEST_TEMPLATE.md | 16 + .github/dependabot.yml | 6 + .github/release.yml | 11 + .github/workflows/ci-gradle.yml | 84 + .github/workflows/functional-tests.yml | 128 + .github/workflows/measure-sdk-size.yaml | 12 + .github/workflows/publish-snapshot.yml | 100 + .github/workflows/release-workflow.yml | 132 + .gitignore | 64 + README.md | 62 + UPGRADING.md | 108 + build.gradle | 31 + buildSrc/README.md | 4 + buildSrc/build.gradle.kts | 28 + .../embrace/gradle/EmbracePluginExtension.kt | 19 + .../embrace/gradle/InternalEmbracePlugin.kt | 203 ++ .../main/kotlin/io/embrace/gradle/Versions.kt | 20 + config/checkstyle/google_checks.xml | 371 +++ config/detekt/detekt.yml | 62 + embrace-android-compose/.gitignore | 1 + .../api/embrace-android-compose.api | 29 + embrace-android-compose/build.gradle.kts | 17 + embrace-android-compose/lint-baseline.xml | 4 + .../src/main/AndroidManifest.xml | 2 + .../compose/ActivityLifeCycleCallbacks.kt | 31 + .../compose/ComposeActivityListener.kt | 41 + .../compose/internal/ClickedView.kt | 10 + .../internal/ComposeClickedTargetIterator.kt | 38 + .../internal/ComposeInternalErrorLogger.kt | 12 + .../internal/EmbraceClickedTargetIterator.kt | 18 + .../internal/EmbraceGestureListener.kt | 47 + .../compose/internal/EmbraceNodeIterator.kt | 82 + .../compose/internal/EmbraceWindowCallback.kt | 32 + embrace-android-fcm/.gitignore | 1 + .../api/embrace-android-fcm.api | 4 + embrace-android-fcm/build.gradle | 14 + embrace-android-fcm/lint-baseline.xml | 4 + .../src/main/AndroidManifest.xml | 2 + .../android/fcm/FirebaseSwazzledHooks.java | 110 + embrace-android-okhttp3/CREDITS.md | 13 + embrace-android-okhttp3/README.md | 5 + .../api/embrace-android-okhttp3.api | 23 + embrace-android-okhttp3/build.gradle | 17 + embrace-android-okhttp3/lint-baseline.xml | 103 + .../src/main/AndroidManifest.xml | 4 + .../okhttp3/EmbraceCustomPathException.java | 24 + .../EmbraceOkHttp3ApplicationInterceptor.java | 102 + .../EmbraceOkHttp3NetworkInterceptor.java | 253 ++ .../EmbraceOkHttp3PathOverrideRequest.java | 28 + .../android/embracesdk/okhttp3/SdkFacade.java | 13 + .../callback/okhttp3/OkHttpClient.java | 106 + .../okhttp3/EmbraceOkHttp3InterceptorsTest.kt | 535 ++++ .../okhttp3/TestInspectionInterceptor.kt | 22 + embrace-android-sdk/.gitignore | 3 + embrace-android-sdk/CMakeLists.txt | 5 + .../api/embrace-android-sdk.api | 308 ++ embrace-android-sdk/build.gradle | 154 + .../config/detekt/baseline.xml | 6 + embrace-android-sdk/embrace-proguard.cfg | 24 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + embrace-android-sdk/gradlew | 172 + embrace-android-sdk/gradlew.bat | 84 + embrace-android-sdk/lint-baseline.xml | 1830 +++++++++++ embrace-android-sdk/proguard-rules.pro | 21 + .../src/androidTest/AndroidManifest.xml | 6 + .../expected-webview-core-vital.json | 22 + .../assets/golden-files/log-error-event.json | 36 + ...rror-with-exception-and-message-event.json | 40 + .../log-error-with-exception-event.json | 38 + .../log-error-with-property-event.json | 39 + .../golden-files/log-handled-exception.json | 41 + .../assets/golden-files/log-info-event.json | 36 + .../golden-files/log-info-fail-event.json | 36 + .../log-info-with-property-event.json | 39 + .../golden-files/log-warning-event.json | 36 + .../golden-files/moment-custom-end-event.json | 16 + .../moment-custom-start-event.json | 31 + ...ment-custom-with-properties-end-event.json | 16 + ...nt-custom-with-properties-start-event.json | 35 + .../moment-startup-end-event.json | 16 + .../moment-startup-late-event.json | 16 + .../moment-startup-start-event.json | 31 + .../assets/golden-files/session-end.json | 109 + .../assets/golden-files/session-start.json | 30 + .../android/embracesdk/AnrIntegrationTest.kt | 315 ++ .../android/embracesdk/LogMessageTest.kt | 136 + .../android/embracesdk/MomentMessageTest.kt | 126 + .../android/embracesdk/SessionMessageTest.kt | 35 + .../res/layout/web_view_activity.xml | 13 + .../src/androidTest/res/raw/mparticle_js_sdk | 0 .../embracesdk/NullParametersTest.java | 323 ++ .../android/embracesdk/PreSdkStartTest.java | 41 + .../android/embracesdk/IntegrationTestRule.kt | 208 ++ .../IntegrationTestRuleExtensions.kt | 107 + .../android/embracesdk/SessionApiTest.kt | 33 + .../assertions/InternalErrorAssertions.kt | 43 + .../assertions/LogMessageAssertions.kt | 40 + .../embracesdk/assertions/SpanAssertions.kt | 47 + .../embracesdk/testcases/LoggingApiTest.kt | 252 ++ .../testcases/NetworkRequestApiTest.kt | 275 ++ .../embracesdk/testcases/PublicApiTest.kt | 139 + .../embracesdk/testcases/TracingApiTest.kt | 195 ++ .../src/main/AndroidManifest.xml | 12 + .../src/main/baseline-prof.txt | 2835 +++++++++++++++++ .../libunwind/include/__libunwind_config.h | 55 + .../3rdparty/libunwind/include/libunwind.h | 508 +++ .../libunwindstack-ndk/AndroidUnwinder.cpp | 240 ++ .../3rdparty/libunwindstack-ndk/ArmExidx.cpp | 863 +++++ .../3rdparty/libunwindstack-ndk/ArmExidx.h | 123 + .../libunwindstack-ndk/AsmGetRegsX86.S | 62 + .../libunwindstack-ndk/AsmGetRegsX86_64.S | 62 + .../cpp/3rdparty/libunwindstack-ndk/Check.h | 31 + .../3rdparty/libunwindstack-ndk/DexFile.cpp | 152 + .../cpp/3rdparty/libunwindstack-ndk/DexFile.h | 81 + .../3rdparty/libunwindstack-ndk/DexFiles.cpp | 56 + .../3rdparty/libunwindstack-ndk/DwarfCfa.cpp | 776 +++++ .../3rdparty/libunwindstack-ndk/DwarfCfa.h | 271 ++ .../libunwindstack-ndk/DwarfDebugFrame.h | 47 + .../libunwindstack-ndk/DwarfEhFrame.h | 46 + .../DwarfEhFrameWithHdr.cpp | 225 ++ .../libunwindstack-ndk/DwarfEhFrameWithHdr.h | 83 + .../libunwindstack-ndk/DwarfEncoding.h | 48 + .../libunwindstack-ndk/DwarfMemory.cpp | 252 ++ .../3rdparty/libunwindstack-ndk/DwarfOp.cpp | 1940 +++++++++++ .../cpp/3rdparty/libunwindstack-ndk/DwarfOp.h | 141 + .../libunwindstack-ndk/DwarfSection.cpp | 827 +++++ .../cpp/3rdparty/libunwindstack-ndk/Elf.cpp | 450 +++ .../libunwindstack-ndk/ElfInterface.cpp | 629 ++++ .../libunwindstack-ndk/ElfInterfaceArm.cpp | 185 ++ .../libunwindstack-ndk/ElfInterfaceArm.h | 95 + .../3rdparty/libunwindstack-ndk/Global.cpp | 101 + .../libunwindstack-ndk/GlobalDebugImpl.h | 434 +++ .../3rdparty/libunwindstack-ndk/JitDebug.cpp | 42 + .../cpp/3rdparty/libunwindstack-ndk/LICENSE | 232 ++ .../cpp/3rdparty/libunwindstack-ndk/Log.cpp | 57 + .../libunwindstack-ndk/LogAndroid.cpp | 77 + .../3rdparty/libunwindstack-ndk/MapInfo.cpp | 460 +++ .../cpp/3rdparty/libunwindstack-ndk/Maps.cpp | 255 ++ .../3rdparty/libunwindstack-ndk/Memory.cpp | 578 ++++ .../libunwindstack-ndk/MemoryBuffer.h | 57 + .../3rdparty/libunwindstack-ndk/MemoryCache.h | 92 + .../libunwindstack-ndk/MemoryFileAtOffset.h | 46 + .../3rdparty/libunwindstack-ndk/MemoryLocal.h | 34 + .../3rdparty/libunwindstack-ndk/MemoryMte.cpp | 44 + .../libunwindstack-ndk/MemoryOffline.h | 59 + .../libunwindstack-ndk/MemoryOfflineBuffer.h | 40 + .../3rdparty/libunwindstack-ndk/MemoryRange.h | 63 + .../libunwindstack-ndk/MemoryRemote.h | 43 + .../cpp/3rdparty/libunwindstack-ndk/OWNERS | 1 + .../cpp/3rdparty/libunwindstack-ndk/README.md | 14 + .../cpp/3rdparty/libunwindstack-ndk/Regs.cpp | 225 ++ .../3rdparty/libunwindstack-ndk/RegsArm.cpp | 174 + .../3rdparty/libunwindstack-ndk/RegsArm64.cpp | 206 ++ .../3rdparty/libunwindstack-ndk/RegsInfo.h | 65 + .../3rdparty/libunwindstack-ndk/RegsX86.cpp | 179 ++ .../libunwindstack-ndk/RegsX86_64.cpp | 168 + .../3rdparty/libunwindstack-ndk/Symbols.cpp | 245 ++ .../cpp/3rdparty/libunwindstack-ndk/Symbols.h | 79 + .../3rdparty/libunwindstack-ndk/TEST_MAPPING | 22 + .../libunwindstack-ndk/ThreadEntry.cpp | 113 + .../3rdparty/libunwindstack-ndk/ThreadEntry.h | 77 + .../libunwindstack-ndk/ThreadUnwinder.cpp | 184 ++ .../3rdparty/libunwindstack-ndk/Unwinder.cpp | 468 +++ .../libunwindstack-ndk/android-base/file.cpp | 569 ++++ .../android-base/log_main.h | 392 +++ .../android-base/stringprintf.cpp | 85 + .../android-base/stringprintf.h | 56 + .../android-base/strings.cpp | 136 + .../android-base/threads.cpp | 54 + .../android-base/unique_fd.h | 104 + .../libunwindstack-ndk/cmake/CMakeLists.txt | 61 + .../include/GlobalDebugInterface.h | 42 + .../include/android-base/errno_restorer.h | 42 + .../include/android-base/file.h | 86 + .../include/android-base/macros.h | 148 + .../include/android-base/off64_t.h | 22 + .../include/android-base/stringprintf.h | 40 + .../include/android-base/strings.h | 153 + .../include/android-base/threads.h | 30 + .../include/android-base/unique_fd.h | 301 ++ .../include/android-base/utf8.h | 104 + .../include/art_api/dex_file_external.h | 158 + .../include/art_api/dex_file_support.h | 137 + .../include/log/android/log.h | 378 +++ .../include/procinfo/process.h | 120 + .../include/procinfo/process_map.h | 337 ++ .../include/unwindstack/AndroidUnwinder.h | 162 + .../include/unwindstack/Arch.h | 41 + .../include/unwindstack/DexFiles.h | 37 + .../include/unwindstack/DwarfError.h | 41 + .../include/unwindstack/DwarfLocation.h | 50 + .../include/unwindstack/DwarfMemory.h | 73 + .../include/unwindstack/DwarfSection.h | 180 ++ .../include/unwindstack/DwarfStructs.h | 52 + .../include/unwindstack/Elf.h | 143 + .../include/unwindstack/ElfInterface.h | 226 ++ .../include/unwindstack/Error.h | 91 + .../include/unwindstack/Global.h | 60 + .../include/unwindstack/JitDebug.h | 37 + .../include/unwindstack/Log.h | 37 + .../include/unwindstack/MachineArm.h | 47 + .../include/unwindstack/MachineArm64.h | 71 + .../include/unwindstack/MachineX86.h | 48 + .../include/unwindstack/MachineX86_64.h | 49 + .../include/unwindstack/MapInfo.h | 233 ++ .../include/unwindstack/Maps.h | 139 + .../include/unwindstack/Memory.h | 66 + .../include/unwindstack/Regs.h | 121 + .../include/unwindstack/RegsArm.h | 57 + .../include/unwindstack/RegsArm64.h | 74 + .../include/unwindstack/RegsGetLocal.h | 136 + .../include/unwindstack/RegsX86.h | 60 + .../include/unwindstack/RegsX86_64.h | 60 + .../include/unwindstack/SharedString.h | 77 + .../include/unwindstack/UcontextArm.h | 60 + .../include/unwindstack/UcontextArm64.h | 66 + .../include/unwindstack/UcontextX86.h | 74 + .../include/unwindstack/UcontextX86_64.h | 79 + .../include/unwindstack/Unwinder.h | 197 ++ .../include/unwindstack/UserArm.h | 37 + .../include/unwindstack/UserArm64.h | 40 + .../include/unwindstack/UserX86.h | 53 + .../include/unwindstack/UserX86_64.h | 63 + .../3rdparty/libunwindstack-ndk/unistdfix.h | 7 + .../src/main/cpp/3rdparty/parson/parson.c | 2424 ++++++++++++++ .../src/main/cpp/3rdparty/parson/parson.h | 256 ++ .../src/main/cpp/CMakeLists.txt | 91 + .../src/main/cpp/CrashSampleClass.cpp | 44 + .../src/main/cpp/CrashSamplesImplClass.cpp | 60 + embrace-android-sdk/src/main/cpp/anr.c | 253 ++ embrace-android-sdk/src/main/cpp/anr.h | 19 + .../src/main/cpp/base_64_encoder.c | 60 + .../src/main/cpp/base_64_encoder.h | 12 + embrace-android-sdk/src/main/cpp/cpuinfo.c | 36 + .../src/main/cpp/emb_anr_manager.c | 24 + embrace-android-sdk/src/main/cpp/emb_log.c | 11 + embrace-android-sdk/src/main/cpp/emb_log.h | 36 + .../src/main/cpp/emb_ndk_crash_samples.cpp | 86 + .../src/main/cpp/emb_ndk_manager.c | 340 ++ .../src/main/cpp/emb_ndk_manager.h | 44 + .../src/main/cpp/file_marker.c | 22 + .../src/main/cpp/file_marker.h | 24 + .../src/main/cpp/file_writer.c | 264 ++ .../src/main/cpp/file_writer.h | 25 + embrace-android-sdk/src/main/cpp/jni_util.c | 29 + embrace-android-sdk/src/main/cpp/jni_util.h | 12 + .../src/main/cpp/safejni/safe_jni.c | 112 + .../src/main/cpp/safejni/safe_jni.h | 68 + .../src/main/cpp/sampler/emb_timer.c | 58 + .../src/main/cpp/sampler/emb_timer.h | 30 + .../src/main/cpp/sampler/sampler_structs.h | 92 + .../cpp/sampler/sampler_unwinder_stack.cpp | 50 + .../main/cpp/sampler/sampler_unwinder_stack.h | 25 + .../cpp/sampler/sampler_unwinder_unwind.c | 113 + .../cpp/sampler/sampler_unwinder_unwind.h | 14 + .../src/main/cpp/sampler/stacktrace_sampler.c | 306 ++ .../src/main/cpp/sampler/stacktrace_sampler.h | 39 + .../main/cpp/sampler/stacktrace_sampler_jni.c | 351 ++ .../src/main/cpp/sampler/unwinder_dlinfo.c | 118 + .../src/main/cpp/sampler/unwinder_dlinfo.h | 44 + .../src/main/cpp/signals/signal_utils.c | 33 + .../src/main/cpp/signals/signal_utils.h | 28 + .../src/main/cpp/signals/signals_c.c | 234 ++ .../src/main/cpp/signals/signals_c.h | 29 + .../src/main/cpp/signals/signals_cpp.cpp | 141 + .../src/main/cpp/signals/signals_cpp.h | 23 + .../src/main/cpp/stack_frames.h | 104 + .../src/main/cpp/unwinders/unwinder.c | 38 + .../src/main/cpp/unwinders/unwinder.h | 24 + .../src/main/cpp/unwinders/unwinder_stack.cpp | 38 + .../src/main/cpp/unwinders/unwinder_stack.h | 16 + embrace-android-sdk/src/main/cpp/utilities.c | 100 + embrace-android-sdk/src/main/cpp/utilities.h | 53 + .../src/main/cpp/utils/system_clock.c | 45 + .../src/main/cpp/utils/system_clock.h | 9 + .../io/embrace/android/embracesdk/BetaApi.kt | 10 + .../embrace/android/embracesdk/Embrace.java | 787 +++++ .../android/embracesdk/EmbraceAndroidApi.java | 98 + .../android/embracesdk/EmbraceApi.java | 90 + .../EmbraceAutomaticVerification.kt | 339 ++ .../android/embracesdk/EmbraceEvent.kt | 64 + .../android/embracesdk/EmbraceImpl.java | 1703 ++++++++++ .../embracesdk/EmbraceInternalInterface.java | 215 ++ .../EmbraceInternalInterfaceImpl.kt | 254 ++ .../android/embracesdk/EmbraceSamples.kt | 75 + .../embracesdk/FlutterInternalInterface.kt | 40 + .../FlutterInternalInterfaceImpl.kt | 78 + .../embracesdk/HttpPathOverrideRequest.java | 12 + .../embrace/android/embracesdk/InternalApi.kt | 8 + .../embracesdk/InternalInterfaceModule.kt | 57 + .../android/embracesdk/LogExceptionType.kt | 13 + .../embrace/android/embracesdk/LogType.java | 25 + .../embrace/android/embracesdk/LogsApi.java | 175 + .../android/embracesdk/MomentsApi.java | 102 + .../android/embracesdk/NetworkRequestApi.kt | 17 + .../ReactNativeInternalInterface.kt | 42 + .../ReactNativeInternalInterfaceImpl.kt | 111 + .../embrace/android/embracesdk/SessionApi.kt | 52 + .../embrace/android/embracesdk/Severity.java | 22 + .../embracesdk/UnityInternalInterface.kt | 31 + .../embracesdk/UnityInternalInterfaceImpl.kt | 108 + .../io/embrace/android/embracesdk/UserApi.kt | 76 + .../android/embracesdk/ViewSwazzledHooks.java | 64 + .../WebViewChromeClientSwazzledHooks.java | 19 + .../WebViewClientSwazzledHooks.java | 16 + .../annotation/StartupActivity.java | 27 + .../android/embracesdk/anr/AnrService.kt | 43 + .../embracesdk/anr/AnrStacktraceSampler.kt | 149 + .../embracesdk/anr/BlockedThreadListener.kt | 25 + .../embracesdk/anr/EmbraceAnrService.kt | 200 ++ .../android/embracesdk/anr/NoOpAnrService.kt | 33 + .../embracesdk/anr/ThreadInfoCollector.kt | 99 + .../anr/detection/AnrProcessErrorChecker.kt | 64 + .../anr/detection/AnrProcessErrorSampler.kt | 229 ++ .../anr/detection/BlockedThreadDetector.kt | 152 + .../anr/detection/LivenessCheckScheduler.kt | 152 + .../anr/detection/LooperCompat.java | 46 + .../anr/detection/TargetThreadHandler.kt | 90 + .../anr/detection/ThreadMonitoringState.kt | 54 + .../anr/detection/UnbalancedCallDetector.kt | 49 + .../ndk/EmbraceNativeThreadSamplerService.kt | 269 ++ .../anr/ndk/NativeThreadSamplerInstaller.kt | 129 + .../anr/ndk/NativeThreadSamplerService.kt | 30 + .../embracesdk/anr/sigquit/FilesDelegate.kt | 13 + .../anr/sigquit/FindGoogleThread.kt | 28 + .../anr/sigquit/GetThreadCommand.kt | 21 + .../anr/sigquit/GetThreadsInCurrentProcess.kt | 21 + .../sigquit/GoogleAnrHandlerNativeDelegate.kt | 27 + .../sigquit/GoogleAnrTimestampRepository.kt | 56 + .../anr/sigquit/SigquitDetectionService.kt | 83 + .../embracesdk/arch/DataCaptureService.kt | 24 + .../capture/EmbracePerformanceInfoService.kt | 83 + .../capture/PerformanceInfoService.kt | 59 + .../capture/aei/ApplicationExitInfoService.kt | 6 + .../aei/EmbraceApplicationExitInfoService.kt | 239 ++ .../aei/NoOpApplicationExitInfoService.kt | 14 + .../EmbraceNetworkConnectivityService.kt | 187 ++ .../NetworkConnectivityListener.kt | 11 + .../NetworkConnectivityService.kt | 40 + .../NoOpNetworkConnectivityService.kt | 25 + .../embracesdk/capture/cpu/CpuInfoDelegate.kt | 16 + .../capture/cpu/EmbraceCpuInfoDelegate.kt | 36 + .../embracesdk/capture/crash/CrashService.kt | 25 + .../capture/crash/EmbraceCrashService.kt | 168 + .../crash/EmbraceUncaughtExceptionHandler.kt | 36 + .../embracesdk/capture/crumbs/Breadcrumb.kt | 14 + .../capture/crumbs/BreadcrumbService.kt | 229 ++ .../capture/crumbs/BreadcrumbsSanitizer.kt | 98 + .../crumbs/EmbraceBreadcrumbService.kt | 562 ++++ .../crumbs/PushNotificationCaptureService.kt | 149 + .../ActivityLifecycleBreadcrumbService.kt | 7 + ...braceActivityLifecycleBreadcrumbService.kt | 146 + .../capture/memory/EmbraceMemoryService.kt | 47 + .../capture/memory/MemoryService.kt | 18 + .../capture/memory/NoOpMemoryService.kt | 16 + .../metadata/EmbraceMetadataService.kt | 767 +++++ .../capture/metadata/MetadataService.kt | 171 + .../capture/metadata/MetadataUtils.java | 267 ++ .../orientation/EmbraceOrientationService.kt | 28 + .../orientation/NoOpOrientationService.kt | 16 + .../capture/orientation/OrientationService.kt | 8 + .../powersave/EmbracePowerSaveModeService.kt | 104 + .../powersave/NoOpPowerSaveModeService.kt | 16 + .../capture/powersave/PowerSaveModeService.kt | 7 + .../strictmode/EmbraceStrictModeService.kt | 54 + .../strictmode/NoOpStrictModeService.kt | 12 + .../capture/strictmode/StrictModeService.kt | 8 + .../EmbraceThermalStatusService.kt | 65 + .../thermalstate/NoOpThermalStatusService.kt | 14 + .../thermalstate/ThermalStatusService.kt | 7 + .../capture/user/EmbraceUserService.kt | 147 + .../embracesdk/capture/user/UserService.kt | 93 + .../capture/webview/EmbraceWebViewService.kt | 120 + .../capture/webview/WebViewService.kt | 19 + .../embrace/android/embracesdk/clock/Clock.kt | 9 + .../clock/NormalizedIntervalClock.kt | 18 + .../android/embracesdk/clock/SystemClock.kt | 7 + .../android/embracesdk/comms/api/ApiClient.kt | 178 ++ .../embracesdk/comms/api/ApiRequest.kt | 65 + .../embracesdk/comms/api/ApiResponse.kt | 7 + .../embracesdk/comms/api/ApiResponseCache.kt | 102 + .../embracesdk/comms/api/ApiService.kt | 8 + .../embracesdk/comms/api/ApiUrlBuilder.kt | 59 + .../embracesdk/comms/api/CachedConfig.kt | 10 + .../embracesdk/comms/api/EmbraceApiService.kt | 83 + .../comms/api/EmbraceConnection.java | 64 + .../comms/api/EmbraceConnectionImpl.java | 118 + .../embracesdk/comms/api/EmbraceUrl.kt | 40 + .../embracesdk/comms/api/EmbraceUrlAdapter.kt | 30 + .../embracesdk/comms/api/EmbraceUrlImpl.java | 50 + .../embracesdk/comms/delivery/CacheService.kt | 82 + .../comms/delivery/DeliveryCacheManager.kt | 268 ++ .../comms/delivery/DeliveryFailedApiCalls.kt | 8 + .../comms/delivery/DeliveryNetworkManager.kt | 524 +++ .../comms/delivery/DeliveryService.kt | 30 + .../comms/delivery/EmbraceCacheService.kt | 169 + .../comms/delivery/EmbraceDeliveryService.kt | 306 ++ .../comms/delivery/NetworkStatus.kt | 8 + .../embracesdk/config/ConfigListener.kt | 14 + .../embracesdk/config/ConfigService.kt | 147 + .../embracesdk/config/EmbraceConfigService.kt | 362 +++ .../embracesdk/config/behavior/AnrBehavior.kt | 240 ++ .../config/behavior/AppExitInfoBehavior.kt | 49 + .../behavior/AutoDataCaptureBehavior.kt | 109 + .../behavior/BackgroundActivityBehavior.kt | 55 + .../config/behavior/BehaviorThresholdCheck.kt | 69 + .../config/behavior/BreadcrumbBehavior.kt | 64 + .../behavior/DataCaptureEventBehavior.kt | 48 + .../config/behavior/LogMessageBehavior.kt | 31 + .../config/behavior/MergedConfigBehavior.kt | 48 + .../config/behavior/NetworkBehavior.kt | 132 + .../behavior/NetworkSpanForwardingBehavior.kt | 25 + .../config/behavior/SdkEndpointBehavior.kt | 36 + .../config/behavior/SdkModeBehavior.kt | 90 + .../config/behavior/SessionBehavior.kt | 153 + .../config/behavior/SpansBehavior.kt | 22 + .../config/behavior/StartupBehavior.kt | 25 + .../config/behavior/UnimplementedConfig.kt | 6 + .../config/behavior/WebViewVitalsBehavior.kt | 31 + .../embracesdk/config/local/AnrLocalConfig.kt | 11 + .../config/local/AppExitInfoLocalConfig.kt | 14 + .../embracesdk/config/local/AppLocalConfig.kt | 9 + .../local/AutomaticDataCaptureLocalConfig.kt | 17 + .../local/BackgroundActivityLocalConfig.kt | 20 + .../config/local/BaseUrlLocalConfig.kt | 20 + .../config/local/ComposeLocalConfig.kt | 8 + .../config/local/CrashHandlerLocalConfig.kt | 11 + .../config/local/DomainLocalConfig.kt | 21 + .../embracesdk/config/local/LocalConfig.kt | 116 + .../config/local/NetworkLocalConfig.kt | 29 + .../embracesdk/config/local/SdkLocalConfig.kt | 116 + .../config/local/SessionLocalConfig.kt | 42 + .../config/local/StartupMomentLocalConfig.kt | 12 + .../config/local/TapsLocalConfig.kt | 8 + .../config/local/ViewLocalConfig.kt | 9 + .../config/local/WebViewLocalConfig.kt | 11 + .../config/remote/AnrRemoteConfig.kt | 101 + .../config/remote/AppExitInfoConfig.kt | 18 + .../remote/BackgroundActivityRemoteConfig.kt | 11 + .../config/remote/KillSwitchRemoteConfig.kt | 14 + .../config/remote/LogRemoteConfig.kt | 33 + .../remote/NetworkCaptureRuleRemoteConfig.kt | 62 + .../config/remote/NetworkRemoteConfig.kt | 24 + .../NetworkSpanForwardingRemoteConfig.kt | 8 + .../embracesdk/config/remote/RemoteConfig.kt | 116 + .../config/remote/SessionRemoteConfig.kt | 31 + .../config/remote/SpansRemoteConfig.kt | 13 + .../config/remote/UiRemoteConfig.kt | 19 + .../embracesdk/config/remote/WebViewVitals.kt | 11 + .../embracesdk/event/EmbraceEventService.kt | 309 ++ .../embracesdk/event/EmbraceRemoteLogger.kt | 504 +++ .../android/embracesdk/event/EventHandler.kt | 239 ++ .../android/embracesdk/event/EventService.kt | 129 + .../embracesdk/gating/EmbraceGatingService.kt | 88 + .../embracesdk/gating/EventSanitizer.kt | 57 + .../embracesdk/gating/EventSanitizerFacade.kt | 27 + .../embracesdk/gating/GatingService.kt | 25 + .../gating/PerformanceInfoSanitizer.kt | 63 + .../android/embracesdk/gating/Sanitizable.kt | 6 + .../embracesdk/gating/SessionGatingKeys.kt | 33 + .../embracesdk/gating/SessionSanitizer.kt | 102 + .../gating/SessionSanitizerFacade.kt | 30 + .../embracesdk/gating/UserInfoSanitizer.kt | 34 + .../injection/AndroidServicesModule.kt | 31 + .../android/embracesdk/injection/AnrModule.kt | 143 + .../embracesdk/injection/CoreModule.kt | 92 + .../embracesdk/injection/CrashModule.kt | 61 + .../embracesdk/injection/CustomerLogModule.kt | 64 + .../injection/DataCaptureServiceModule.kt | 182 ++ .../injection/DataContainerModule.kt | 87 + .../embracesdk/injection/DeliveryModule.kt | 71 + .../injection/DependencyInjection.kt | 63 + .../injection/EssentialServiceModule.kt | 167 + .../embracesdk/injection/InitModule.kt | 28 + .../injection/SdkObservabilityModule.kt | 34 + .../embracesdk/injection/SessionModule.kt | 99 + .../injection/SystemServiceModule.kt | 52 + .../internal/AndroidResourcesService.kt | 15 + .../embracesdk/internal/ApkToolsConfig.kt | 31 + .../android/embracesdk/internal/BuildInfo.kt | 74 + .../embracesdk/internal/CacheableValue.kt | 44 + .../internal/ConstantNameThreadFactory.kt | 19 + .../embracesdk/internal/DeviceArchitecture.kt | 6 + .../internal/DeviceArchitectureImpl.kt | 15 + .../EmbraceAndroidResourcesService.kt | 15 + .../embracesdk/internal/EmbraceSerializer.kt | 63 + .../embracesdk/internal/EventDescription.kt | 9 + .../embracesdk/internal/MessageType.kt | 5 + .../embracesdk/internal/OpenTelemetryClock.kt | 29 + .../embracesdk/internal/PatternCache.kt | 15 + .../embracesdk/internal/SharedObjectLoader.kt | 17 + .../embracesdk/internal/StartupEventInfo.kt | 6 + .../android/embracesdk/internal/Systrace.kt | 48 + .../internal/ThreadEnforcementCheck.kt | 21 + .../internal/TraceparentGenerator.kt | 32 + .../internal/crash/CrashFileMarker.kt | 103 + .../internal/crash/LastRunCrashVerifier.kt | 51 + .../internal/spans/EmbraceExtensions.kt | 220 ++ .../internal/spans/EmbraceSpanData.kt | 68 + .../internal/spans/EmbraceSpanExporter.kt | 24 + .../internal/spans/EmbraceSpanImpl.kt | 125 + .../internal/spans/EmbraceSpanProcessor.kt | 35 + .../internal/spans/EmbraceSpansService.kt | 172 + .../internal/spans/EmbraceTracer.kt | 115 + .../spans/FeatureDisabledSpansService.kt | 36 + .../internal/spans/Initializable.kt | 17 + .../embracesdk/internal/spans/SpansService.kt | 74 + .../internal/spans/SpansServiceImpl.kt | 294 ++ .../embracesdk/internal/utils/MessageUtils.kt | 56 + .../internal/utils/ThrowableUtils.kt | 37 + .../android/embracesdk/internal/utils/Uuid.kt | 38 + .../internal/utils/VersionChecker.kt | 14 + .../embracesdk/logging/AndroidLogger.kt | 24 + .../logging/EmbraceInternalErrorService.kt | 142 + .../logging/InternalEmbraceLogger.kt | 108 + .../embracesdk/logging/InternalErrorLogger.kt | 39 + .../logging/InternalStaticEmbraceLogger.kt | 88 + .../embracesdk/ndk/EmbraceNdkService.java | 736 +++++ .../ndk/EmbraceNdkServiceRepository.kt | 111 + .../android/embracesdk/ndk/NativeModule.kt | 80 + .../android/embracesdk/ndk/NdkService.kt | 19 + .../embracesdk/ndk/NdkServiceDelegate.kt | 56 + .../network/EmbraceNetworkRequest.java | 499 +++ .../network/http/ConnectionState.java | 12 + .../http/CountingInputStreamWithCallback.java | 129 + .../network/http/CountingOutputStream.java | 64 + .../network/http/EmbraceHttpPathOverride.java | 83 + .../http/EmbraceHttpUrlConnection.java | 297 ++ .../EmbraceHttpUrlConnectionOverride.java | 41 + .../http/EmbraceHttpUrlStreamHandler.java | 69 + .../http/EmbraceHttpsUrlConnection.java | 355 +++ .../http/EmbraceHttpsUrlStreamHandler.java | 70 + .../http/EmbraceSslUrlConnectionService.java | 36 + .../http/EmbraceUrlConnectionOverride.java | 904 ++++++ .../http/EmbraceUrlConnectionService.java | 155 + .../network/http/EmbraceUrlStreamHandler.java | 122 + .../http/EmbraceUrlStreamHandlerFactory.java | 59 + .../embracesdk/network/http/HttpMethod.java | 80 + .../network/http/HttpUrlConnectionTracker.kt | 8 + .../network/http/NetworkCaptureData.kt | 22 + .../http/StreamHandlerFactoryInstaller.java | 199 ++ .../network/logging/DomainSettings.kt | 6 + .../logging/EmbraceNetworkCaptureService.kt | 167 + .../logging/EmbraceNetworkLoggingService.kt | 252 ++ .../NetworkCaptureEncryptionManager.java | 165 + .../network/logging/NetworkCaptureService.kt | 29 + .../network/logging/NetworkLoggingService.kt | 73 + .../payload/ActivityLifecycleBreadcrumb.kt | 25 + .../payload/ActivityLifecycleData.kt | 11 + .../payload/ActivityLifecycleState.kt | 14 + .../android/embracesdk/payload/AnrInterval.kt | 105 + .../android/embracesdk/payload/AnrSample.kt | 38 + .../embracesdk/payload/AnrSampleList.kt | 21 + .../embracesdk/payload/AppExitInfoData.kt | 45 + .../android/embracesdk/payload/AppInfo.kt | 147 + .../embracesdk/payload/BackgroundActivity.kt | 174 + .../payload/BackgroundActivityMessage.kt | 54 + .../embracesdk/payload/BetaFeatures.kt | 19 + .../android/embracesdk/payload/BlobMessage.kt | 24 + .../android/embracesdk/payload/BlobSession.kt | 8 + .../android/embracesdk/payload/Breadcrumbs.kt | 57 + .../android/embracesdk/payload/Crash.kt | 94 + .../embracesdk/payload/CustomBreadcrumb.kt | 73 + .../android/embracesdk/payload/DeviceInfo.kt | 71 + .../android/embracesdk/payload/DiskUsage.kt | 21 + .../android/embracesdk/payload/Event.kt | 79 + .../embracesdk/payload/EventMessage.kt | 33 + .../embracesdk/payload/ExceptionError.kt | 59 + .../embracesdk/payload/ExceptionErrorInfo.kt | 26 + .../embracesdk/payload/ExceptionInfo.kt | 78 + .../embracesdk/payload/FragmentBreadcrumb.kt | 24 + .../android/embracesdk/payload/Interval.kt | 16 + .../android/embracesdk/payload/JsException.kt | 10 + .../embracesdk/payload/MemoryWarning.kt | 17 + .../android/embracesdk/payload/NativeCrash.kt | 12 + .../embracesdk/payload/NativeCrashData.kt | 19 + .../payload/NativeCrashDataError.kt | 8 + .../embracesdk/payload/NativeCrashMetadata.kt | 26 + .../embracesdk/payload/NativeSymbols.kt | 33 + .../payload/NativeThreadAnrInterval.kt | 59 + .../payload/NativeThreadAnrSample.kt | 38 + .../payload/NativeThreadAnrStackframe.kt | 36 + .../embracesdk/payload/NetworkCallV2.kt | 53 + .../embracesdk/payload/NetworkCapturedCall.kt | 126 + .../embracesdk/payload/NetworkEvent.kt | 29 + .../embracesdk/payload/NetworkRequests.kt | 7 + .../embracesdk/payload/NetworkSessionV2.kt | 21 + .../android/embracesdk/payload/Orientation.kt | 18 + .../embracesdk/payload/PerformanceInfo.kt | 78 + .../embracesdk/payload/PowerModeInterval.kt | 14 + .../payload/PushNotificationBreadcrumb.kt | 53 + .../embracesdk/payload/RnActionBreadcrumb.kt | 67 + .../android/embracesdk/payload/Session.kt | 168 + .../embracesdk/payload/SessionMessage.kt | 54 + .../android/embracesdk/payload/Stacktraces.kt | 59 + .../embracesdk/payload/StrictModeViolation.kt | 19 + .../embracesdk/payload/TapBreadcrumb.kt | 57 + .../embracesdk/payload/ThermalState.kt | 12 + .../android/embracesdk/payload/ThreadInfo.kt | 70 + .../android/embracesdk/payload/ThreadState.kt | 22 + .../android/embracesdk/payload/UserInfo.kt | 69 + .../embracesdk/payload/ViewBreadcrumb.kt | 40 + .../embracesdk/payload/WebViewBreadcrumb.kt | 17 + .../android/embracesdk/payload/WebViewInfo.kt | 20 + .../android/embracesdk/payload/WebVital.kt | 27 + .../embracesdk/payload/WebVitalType.kt | 14 + .../prefs/EmbracePreferencesService.kt | 360 +++ .../embracesdk/prefs/PreferencesService.kt | 186 ++ .../embracesdk/registry/ServiceRegistry.kt | 82 + .../samples/AutomaticVerificationChecker.kt | 81 + .../AutomaticVerificationExceptionHandler.kt | 26 + .../embracesdk/samples/ComparableVersion.java | 404 +++ .../samples/CrashSamplesNdkDelegate.kt | 9 + .../embracesdk/samples/EmbraceCrashSamples.kt | 132 + .../EmbraceCrashSamplesNdkDelegateImpl.kt | 9 + .../samples/EmbraceSampleCodeException.kt | 3 + .../embracesdk/samples/VerificationActions.kt | 208 ++ .../embracesdk/samples/VerificationResult.kt | 5 + .../samples/VerifyIntegrationException.kt | 3 + .../embracesdk/session/ActivityListener.kt | 50 + .../embracesdk/session/ActivityService.kt | 50 + .../session/BackgroundActivityService.kt | 22 + .../session/EmbraceActivityService.kt | 303 ++ .../EmbraceBackgroundActivityService.kt | 369 +++ .../session/EmbraceMemoryCleanerService.kt | 36 + .../session/EmbraceSessionProperties.kt | 116 + .../session/EmbraceSessionService.kt | 250 ++ .../session/MemoryCleanerListener.kt | 9 + .../session/MemoryCleanerService.kt | 20 + .../embracesdk/session/SessionHandler.kt | 613 ++++ .../session/SessionMessageSerializer.kt | 95 + .../embracesdk/session/SessionService.kt | 60 + .../android/embracesdk/spans/EmbraceSpan.kt | 76 + .../embracesdk/spans/EmbraceSpanEvent.kt | 44 + .../android/embracesdk/spans/ErrorCode.kt | 28 + .../android/embracesdk/spans/TracingApi.kt | 144 + .../android/embracesdk/utils/Consumer.kt | 9 + .../utils/ExecutorServiceExtensions.kt | 48 + .../android/embracesdk/utils/Function.kt | 9 + .../embracesdk/utils/ListExtensions.kt | 14 + .../android/embracesdk/utils/NetworkUtils.kt | 108 + .../android/embracesdk/utils/PropertyUtils.kt | 71 + .../android/embracesdk/utils/StreamUtils.kt | 25 + .../android/embracesdk/utils/ThreadUtils.kt | 27 + .../embracesdk/utils/VersionChecker.kt | 14 + .../embracesdk/utils/exceptions/Unchecked.kt | 92 + .../exceptions/function/CheckedSupplier.kt | 13 + .../embracesdk/worker/WorkerThreadModule.kt | 46 + .../worker/WorkerThreadModuleImpl.kt | 36 + .../src/main/res/values/strings.xml | 16 + .../android/embracesdk/AnrSampleListTest.kt | 30 + .../android/embracesdk/AnrSampleTest.kt | 44 + .../android/embracesdk/ApiUrlBuilderTest.kt | 32 + .../embrace/android/embracesdk/AppInfoTest.kt | 69 + .../android/embracesdk/BetaFeaturesTest.kt | 22 + .../android/embracesdk/CacheableValueTest.kt | 38 + .../android/embracesdk/ConfigRoundTripTest.kt | 22 + .../embracesdk/DeliveryCacheManagerTest.kt | 343 ++ .../android/embracesdk/DeviceInfoTest.kt | 56 + .../android/embracesdk/DiskUsageTest.kt | 37 + ...eActivityLifecycleBreadcrumbServiceTest.kt | 143 + .../embracesdk/EmbraceAnrServiceRule.kt | 92 + .../embracesdk/EmbraceAnrServiceTest.kt | 505 +++ .../embracesdk/EmbraceAnrServiceTimingTest.kt | 55 + .../EmbraceApplicationExitInfoServiceTest.kt | 407 +++ .../EmbraceAutomaticVerificationTest.kt | 115 + .../embracesdk/EmbraceCacheServiceTest.kt | 223 ++ .../embracesdk/EmbraceConfigServiceTest.kt | 316 ++ .../embracesdk/EmbraceCrashServiceTest.kt | 224 ++ .../android/embracesdk/EmbraceEventTest.kt | 63 + .../embracesdk/EmbraceGatingServiceTest.kt | 486 +++ .../EmbraceInternalInterfaceImplTest.kt | 300 ++ .../embracesdk/EmbraceMemoryServiceTest.kt | 77 + .../EmbraceNativeThreadSamplerServiceTest.kt | 441 +++ .../EmbraceNetworkConnectivityServiceTest.kt | 259 ++ .../EmbraceOrientationServiceTest.kt | 49 + .../EmbracePerformanceInfoServiceTest.kt | 110 + .../EmbracePowerSaveModeServiceTest.kt | 182 ++ .../EmbraceSessionPropertiesTest.kt | 273 ++ .../embrace/android/embracesdk/EmbraceTest.kt | 29 + .../EmbraceThermalStatusServiceTest.kt | 58 + .../EmbraceUncaughtExceptionHandlerTest.kt | 86 + .../embracesdk/EmbraceUserServiceTest.kt | 198 ++ .../embracesdk/EmbraceWebViewServiceTest.kt | 177 + .../EssentialServiceModuleImplTest.kt | 77 + .../embracesdk/EventSanitizerFacadeTest.kt | 91 + .../android/embracesdk/EventSanitizerTest.kt | 132 + .../embracesdk/ExceptionErrorInfoTest.kt | 47 + .../android/embracesdk/ExceptionInfoTest.kt | 82 + .../embracesdk/FakeBackgroundActivity.kt | 35 + .../embracesdk/FakeBreadcrumbService.kt | 106 + .../android/embracesdk/FakeDeliveryService.kt | 90 + .../android/embracesdk/FakeNdkService.kt | 34 + .../android/embracesdk/FakeSessionService.kt | 39 + .../embracesdk/FakeWorkerThreadModule.kt | 45 + .../FlutterInternalInterfaceImplTest.kt | 112 + .../embracesdk/FragmentBreadcrumbTest.kt | 39 + .../InternalInterfaceModuleImplTest.kt | 27 + .../android/embracesdk/JsExceptionTest.kt | 42 + .../android/embracesdk/LocalConfigTest.kt | 523 +++ .../android/embracesdk/MemoryWarningTest.kt | 33 + .../android/embracesdk/MessageUtilsTest.kt | 94 + .../NativeThreadSamplerInstallerTest.kt | 118 + .../android/embracesdk/NetworkUtilsTest.kt | 150 + .../android/embracesdk/OrientationTest.kt | 40 + .../PerformanceInfoSanitizerTest.kt | 49 + .../android/embracesdk/PerformanceInfoTest.kt | 76 + .../android/embracesdk/PropertiesTest.kt | 38 + .../PushNotificationBreadcrumbTest.kt | 47 + .../ReactNativeInternalInterfaceImplTest.kt | 185 ++ .../android/embracesdk/ResourceReader.kt | 15 + .../SessionPerformanceInfoSanitizerTest.kt | 55 + .../embracesdk/SessionRemoteConfigTest.kt | 18 + .../embracesdk/SessionSanitizerFacadeTest.kt | 159 + .../embracesdk/SessionSanitizerTest.kt | 74 + .../SessionStacktraceSampleJsonTest.kt | 104 + .../embrace/android/embracesdk/SessionTest.kt | 115 + .../android/embracesdk/TestCacheService.kt | 69 + .../android/embracesdk/ThreadInfoTest.kt | 63 + .../UnityInternalInterfaceImplTest.kt | 110 + .../embracesdk/UserInfoSanitizerTest.kt | 36 + .../android/embracesdk/UserInfoTest.kt | 120 + .../io/embrace/android/embracesdk/UuidTest.kt | 14 + .../android/embracesdk/ViewBreadcrumbTest.kt | 40 + .../embracesdk/WebViewBreadcrumbTest.kt | 37 + .../WebViewClientSwazzledHooksTest.kt | 25 + .../android/embracesdk/anr/AnrIntervalTest.kt | 109 + .../anr/AnrStacktraceSamplerTest.kt | 185 ++ .../anr/EmbraceSigquitDetectionServiceTest.kt | 105 + .../anr/EmbraceStrictModeServiceTest.kt | 56 + .../embracesdk/anr/FindGoogleThreadTest.kt | 53 + .../embracesdk/anr/ThreadInfoCollectorTest.kt | 131 + .../detection/AnrProcessErrorSamplerTest.kt | 389 +++ .../detection/BlockedThreadDetectorTest.kt | 78 + .../detection/LivenessCheckSchedulerTest.kt | 176 + .../anr/detection/TargetThreadHandlerTest.kt | 142 + .../detection/UnbalancedCallDetectorTest.kt | 79 + .../anr/ndk/NativeThreadAnrSampleJsonTest.kt | 55 + .../ndk/NativeThreadAnrStackframeJsonTest.kt | 28 + .../anr/sigquit/FilesDelegateTest.kt | 18 + .../anr/sigquit/GetThreadCommandTest.kt | 52 + .../sigquit/GetThreadsInCurrentProcessTest.kt | 51 + .../GoogleAnrTimestampRepositoryTest.kt | 94 + .../capture/cpu/EmbraceCpuInfoDelegateTest.kt | 37 + .../crumbs/BreadcrumbsSanitizerTest.kt | 53 + .../crumbs/EmbraceBreadcrumbServiceTest.kt | 588 ++++ .../PushNotificationCaptureServiceTest.kt | 206 ++ .../EmbraceMetadataReactNativeTest.kt | 154 + .../metadata/EmbraceMetadataServiceTest.kt | 364 +++ .../metadata/EmbraceMetadataUnityTest.kt | 143 + .../embracesdk/comms/api/ApiClientTest.kt | 205 ++ .../embracesdk/comms/api/ApiRequestTest.kt | 95 + .../embracesdk/comms/api/CachedConfigTest.kt | 16 + .../comms/api/EmbraceApiServiceTest.kt | 79 + .../delivery/DeliveryNetworkManagerTest.kt | 366 +++ .../delivery/EmbraceDeliveryServiceTest.kt | 281 ++ .../concurrency/BlockableExecutorService.kt | 104 + .../BlockableExecutorServiceTests.kt | 141 + .../BlockingScheduledExecutorService.kt | 280 ++ .../BlockingScheduledExecutorServiceTests.kt | 385 +++ .../SingleThreadTestScheduledExecutor.kt | 63 + .../SingleThreadTestScheduledExecutorTest.kt | 44 + .../ApplicationExitInfoRemoteConfigTest.kt | 46 + .../embracesdk/config/BgActivityConfigTest.kt | 38 + .../config/KillSwitchRemoteConfigTest.kt | 19 + .../embracesdk/config/LogRemoteConfigTest.kt | 54 + .../config/NetworkRemoteConfigTest.kt | 47 + .../embracesdk/config/UiRemoteConfigTest.kt | 58 + .../config/behavior/AnrBehaviorTest.kt | 135 + .../behavior/AppExitInfoBehaviorTest.kt | 43 + .../behavior/AutoDataCaptureBehaviorTest.kt | 116 + .../BackgroundActivityBehaviorTest.kt | 53 + .../config/behavior/BreadcrumbBehaviorTest.kt | 71 + .../behavior/DataCaptureEventBehaviorTest.kt | 45 + .../config/behavior/LogMessageBehaviorTest.kt | 36 + .../config/behavior/NetworkBehaviorTest.kt | 134 + .../NetworkSpanForwardingBehaviorTest.kt | 33 + .../behavior/SdkEndpointBehaviorTest.kt | 34 + .../config/behavior/SdkModeBehaviorTest.kt | 138 + .../config/behavior/SessionBehaviorTest.kt | 96 + .../config/behavior/SpansBehaviorTest.kt | 27 + .../config/behavior/StartupBehaviorTest.kt | 28 + .../config/behavior/WebVitalsBehaviorTest.kt | 30 + .../config/local/AnrLocalConfigTest.kt | 32 + .../config/local/AppLocalConfigTest.kt | 29 + .../ApplicationExitInfoLocalConfigTest.kt | 33 + .../AutomaticDataCaptureLocalConfigTest.kt | 40 + .../BackgroundActivityLocalConfigTest.kt | 40 + .../config/local/BaseUrlLocalConfigTest.kt | 39 + .../local/CrashHandlerLocalConfigTest.kt | 29 + .../config/local/DomainLocalConfigTest.kt | 35 + .../config/local/NetworkLocalConfigTest.kt | 46 + .../config/local/SessionLocalConfigTest.kt | 42 + .../local/StartupMomentLocalConfigTest.kt | 29 + .../config/local/TapsLocalConfigTest.kt | 29 + .../config/local/ViewLocalConfigTest.kt | 29 + .../config/local/WebViewLocalConfigTest.kt | 32 + .../event/EmbraceEventServiceTest.kt | 470 +++ .../event/EmbraceRemoteLoggerTest.kt | 469 +++ .../embracesdk/event/EventHandlerTest.kt | 671 ++++ .../android/embracesdk/fakes/BehaviorFakes.kt | 161 + .../embracesdk/fakes/FakeActivityService.kt | 69 + .../fakes/FakeAndroidMetadataService.kt | 132 + .../fakes/FakeAndroidResourcesService.kt | 31 + .../embracesdk/fakes/FakeAnrService.kt | 39 + .../embracesdk/fakes/FakeApiService.kt | 15 + .../fakes/FakeApplicationExitInfoService.kt | 15 + .../android/embracesdk/fakes/FakeClock.kt | 21 + .../embracesdk/fakes/FakeConfigService.kt | 67 + .../embracesdk/fakes/FakeCpuInfoDelegate.kt | 12 + .../embracesdk/fakes/FakeCrashService.kt | 17 + .../fakes/FakeDataCaptureService.kt | 13 + .../fakes/FakeDeviceArchitecture.kt | 8 + .../embracesdk/fakes/FakeEventService.kt | 67 + .../embracesdk/fakes/FakeGatingService.kt | 30 + .../embracesdk/fakes/FakeLoggerAction.kt | 26 + .../fakes/FakeMemoryCleanerListener.kt | 13 + .../fakes/FakeMemoryCleanerService.kt | 19 + .../embracesdk/fakes/FakeMemoryService.kt | 11 + .../fakes/FakeNetworkConnectivityService.kt | 31 + .../fakes/FakeNetworkLoggingService.kt | 42 + .../fakes/FakeOpenTelemetryClock.kt | 20 + .../fakes/FakeOrientationService.kt | 10 + .../fakes/FakePerformanceInfoService.kt | 22 + .../fakes/FakePowerSaveModeService.kt | 10 + .../embracesdk/fakes/FakePreferenceService.kt | 51 + .../android/embracesdk/fakes/FakeSession.kt | 13 + .../embracesdk/fakes/FakeSessionProperties.kt | 10 + .../embracesdk/fakes/FakeStrictModeService.kt | 10 + .../fakes/FakeThermalStatusService.kt | 9 + .../embracesdk/fakes/FakeUserService.kt | 63 + .../embracesdk/fakes/FakeVersionChecker.kt | 7 + .../embracesdk/fakes/FakeWebViewService.kt | 10 + .../injection/FakeAndroidServicesModule.kt | 9 + .../fakes/injection/FakeAnrModule.kt | 14 + .../fakes/injection/FakeCoreModule.kt | 31 + .../fakes/injection/FakeCrashModule.kt | 14 + .../fakes/injection/FakeCustomerLogModule.kt | 38 + .../injection/FakeDataCaptureServiceModule.kt | 36 + .../injection/FakeDataContainerModule.kt | 15 + .../fakes/injection/FakeDeliveryModule.kt | 29 + .../injection/FakeEssentialServiceModule.kt | 56 + .../fakes/injection/FakeInitModule.kt | 14 + .../fakes/injection/FakeNativeModule.kt | 13 + .../injection/FakeSdkObservabilityModule.kt | 19 + .../fakes/injection/FakeSessionModule.kt | 15 + .../injection/FakeSystemServiceModule.kt | 16 + .../embracesdk/fixtures/SpansTestFixtures.kt | 65 + .../AndroidServicesModuleImplTest.kt | 21 + .../embracesdk/injection/AnrModuleImplTest.kt | 67 + .../injection/CoreModuleImplTest.kt | 36 + .../injection/CrashModuleImplTest.kt | 31 + .../injection/CustomerLogModuleImplTest.kt | 32 + .../DataCaptureServiceModuleImplTest.kt | 133 + .../injection/DataContainerModuleImplTest.kt | 39 + .../injection/DeliveryModuleImplTest.kt | 27 + .../injection/DependencyInjectionKtTest.kt | 34 + .../injection/InitModuleImplTest.kt | 34 + .../SdkObservabilityModuleImplTest.kt | 18 + .../injection/SystemServiceModuleImplTest.kt | 65 + .../internal/ConstantNameThreadFactoryTest.kt | 61 + .../internal/EmbraceSerializerTest.kt | 36 + .../internal/OpenTelemetryClockTest.kt | 37 + .../internal/ThreadEnforcementCheckTest.kt | 91 + .../internal/TraceparentGeneratorTest.kt | 67 + .../internal/crash/CrashFileMarkerTest.kt | 121 + .../crash/LastRunCrashVerifierTest.kt | 62 + .../internal/spans/EmbraceSpanDataTest.kt | 40 + .../internal/spans/EmbraceSpanImplTest.kt | 144 + .../internal/spans/EmbraceSpansServiceTest.kt | 224 ++ .../internal/spans/EmbraceTracerTest.kt | 113 + .../internal/spans/SpansServiceImplTest.kt | 703 ++++ .../internal/utils/ThrowableUtilsTest.kt | 59 + .../EmbraceInternalErrorServiceTest.kt | 129 + .../logging/InternalEmbraceLoggerTest.kt | 80 + .../logging/InternalErrorLoggerTest.kt | 141 + .../ndk/EmbraceNdkServiceRepositoryTest.kt | 199 ++ .../embracesdk/ndk/EmbraceNdkServiceTest.kt | 583 ++++ .../embracesdk/ndk/NativeModuleImplTest.kt | 27 + .../network/EmbraceNetworkRequestTest.kt | 178 ++ .../http/EmbraceUrlConnectionOverrideTest.kt | 218 ++ .../http/EmbraceUrlStreamHandlerTest.kt | 118 + .../EmbraceNetworkCaptureServiceTest.kt | 217 ++ .../EmbraceNetworkLoggingServiceTest.kt | 252 ++ .../NetworkCaptureEncryptionManagerTest.kt | 149 + .../networking/EmbraceUrlAdapterTest.kt | 33 + .../payload/BackgroundActivityMessageTest.kt | 61 + .../payload/BackgroundActivityTest.kt | 81 + .../embracesdk/payload/BreadcrumbsTest.kt | 75 + .../payload/CustomBreadcrumbTest.kt | 37 + .../payload/EmbraceEventMessageTest.kt | 63 + .../embracesdk/payload/ExceptionErrorTest.kt | 59 + .../payload/NativeCrashDataErrorTest.kt | 38 + .../embracesdk/payload/NativeCrashDataTest.kt | 63 + .../payload/NativeCrashMetadataTest.kt | 52 + .../embracesdk/payload/NativeCrashTest.kt | 53 + .../embracesdk/payload/NativeSymbolsTest.kt | 66 + .../payload/RnActionBreadcrumbTest.kt | 45 + .../embracesdk/payload/SessionMessageTest.kt | 72 + .../embracesdk/payload/TapBreadcrumbTest.kt | 42 + .../prefs/EmbracePreferencesServiceTest.kt | 316 ++ .../registry/ServiceRegistryTest.kt | 81 + .../samples/EmbraceCrashSamplesTest.kt | 73 + .../android/embracesdk/samples/VersionTest.kt | 81 + .../session/EmbraceActivityServiceTest.kt | 321 ++ .../EmbraceBackgroundActivityServiceTest.kt | 322 ++ .../EmbraceMemoryCleanerServiceTest.kt | 99 + .../session/EmbraceSessionServiceTest.kt | 526 +++ .../embracesdk/session/SessionHandlerTest.kt | 616 ++++ .../session/SessionMessageSerializerTest.kt | 48 + .../session/SessionModuleImplTest.kt | 80 + .../utils/ExecutorExtensionsTest.kt | 42 + .../embracesdk/utils/JsonComparisonUtils.kt | 12 + .../embracesdk/utils/ListExtensionsKtTest.kt | 17 + .../embracesdk/utils/PropertyUtilsTest.kt | 41 + .../worker/WorkerThreadModuleImplTest.kt | 32 + .../src/test/resources/anr_config.json | 4 + .../resources/anr_default_config_expected.txt | 15 + .../test/resources/anr_interval_expected.json | 28 + .../anr_override_config_expected.txt | 17 + .../src/test/resources/anr_tick_expected.json | 17 + .../src/test/resources/api_request.json | 16 + .../src/test/resources/app_config.json | 3 + .../src/test/resources/app_info_expected.json | 20 + .../application_exit_info_local_config.json | 4 + .../application_exit_info_remote_config.json | 5 + .../resources/auto_data_capture_config.json | 6 + .../resources/background_activity_config.json | 6 + .../src/test/resources/base_url_config.json | 6 + .../test/resources/bg_activity_config.json | 3 + .../test/resources/bg_activity_expected.json | 36 + .../bg_activity_message_expected.json | 44 + .../src/test/resources/breadcrumb_custom.json | 27 + .../src/test/resources/breadcrumb_empty.json | 22 + .../test/resources/breadcrumb_fragment.json | 33 + .../src/test/resources/breadcrumb_view.json | 33 + .../resources/breadcrumb_view_custom.json | 38 + .../test/resources/breadcrumb_webview.json | 31 + .../test/resources/breadcrumbs_expected.json | 54 + .../src/test/resources/crash_expected.json | 26 + .../test/resources/crash_handler_config.json | 3 + .../resources/custom_breadcrumb_expected.json | 4 + .../test/resources/device_info_expected.json | 15 + .../test/resources/disk_usage_expected.json | 4 + .../src/test/resources/domain_config.json | 4 + .../src/test/resources/empty_file.txt | 0 .../src/test/resources/event_expected.json | 15 + .../test/resources/eventmessage_expected.json | 34 + .../exception_error_info_expected.json | 14 + .../resources/exception_info_expected.json | 8 + ...d_core_vital_repeated_elements_script.json | 85 + .../resources/expected_core_vital_script.json | 47 + .../expected_core_vital_script1.json | 47 + .../expected_core_vital_script_repeated.json | 47 + .../fragment_breadcrumb_expected.json | 5 + .../test/resources/js_exception_expected.json | 6 + .../test/resources/local_network_config.json | 11 + .../src/test/resources/log_config.json | 6 + .../resources/memory_warning_expected.json | 3 + .../resources/metadata_appinfo_expected.json | 21 + ...etadata_react_native_appinfo_expected.json | 21 + .../native_crash_data_error_expected.json | 4 + .../resources/native_crash_data_expected.json | 24 + .../test/resources/native_crash_expected.json | 15 + .../native_crash_metadata_expected.json | 14 + .../src/test/resources/native_crash_raw.txt | 53 + .../resources/native_symbols_expected.json | 12 + .../src/test/resources/network_config.json | 6 + .../test/resources/orientation_expected.json | 4 + .../test/resources/perf_info_expected.json | 15 + .../src/test/resources/public_key_config.json | 3 + ...push_notification_breadcrumb_expected.json | 9 + .../resources/remote_config_response.json | 15 + .../rn_action_breadcrumb_expected.json | 10 + .../src/test/resources/session_config.json | 7 + .../src/test/resources/session_expected.json | 63 + .../resources/session_message_expected.json | 49 + .../src/test/resources/span_expected.json | 28 + .../test/resources/startup_moment_config.json | 3 + .../resources/startup_sampling_config.json | 7 + ...artup_sampling_default_config_expected.txt | 5 + ...rtup_sampling_override_config_expected.txt | 5 + .../resources/tap_breadcrumb_expected.json | 6 + .../src/test/resources/taps_config.json | 3 + .../src/test/resources/test_screenshot.jpg | Bin 0 -> 81667 bytes .../test/resources/thread_info_expected.json | 10 + .../src/test/resources/ui_config.json | 7 + .../test/resources/user_info_expected.json | 8 + .../resources/view_breadcrumb_expected.json | 5 + .../src/test/resources/view_config.json | 3 + .../src/test/resources/web_view_config.json | 4 + .../webview_breadcrumb_expected.json | 4 + embrace-lint/.gitignore | 1 + embrace-lint/README.md | 5 + embrace-lint/build.gradle | 21 + .../android/lint/EmbraceLintRegistry.kt | 23 + .../lint/EmbracePublicApiPackageRule.kt | 59 + ...ndroid.tools.lint.client.api.IssueRegistry | 1 + .../lint/EmbracePublicApiPackageRuleTest.kt | 74 + gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 + gradlew.bat | 84 + scripts/release.gradle | 106 + settings.gradle | 6 + sonar-project.properties | 24 + test-server/.gitignore | 1 + test-server/README.md | 124 + test-server/build.gradle | 40 + test-server/consumer-rules.pro | 0 test-server/lint-baseline.xml | 491 +++ test-server/proguard-rules.pro | 21 + test-server/src/main/AndroidManifest.xml | 5 + .../androidx/lifecycle/MockReportFragment.kt | 22 + .../lifecycle/ProcessLifecycleOwnerAccess.kt | 18 + .../embracesdk/ActivityServiceHooks.java | 13 + .../io/embrace/android/embracesdk/BaseTest.kt | 342 ++ .../android/embracesdk/BuildInfoHooks.java | 21 + .../android/embracesdk/ConfigHooks.java | 106 + .../android/embracesdk/EmbraceContext.kt | 214 ++ .../android/embracesdk/EmbraceFileObserver.kt | 22 + .../android/embracesdk/FakePackageManager.kt | 460 +++ .../embrace/android/embracesdk/TestServer.kt | 79 + .../embracesdk/internal/MockActivity.kt | 33 + .../internal/MockFragmentManager.kt | 122 + .../android/embracesdk/internal/MockView.kt | 20 + .../android/embracesdk/internal/MockWindow.kt | 113 + .../internal/PauseProcessListener.kt | 21 + .../android/embracesdk/utils/BitmapFactory.kt | 28 + .../android/embracesdk/utils/FailureLatch.kt | 40 + .../android/embracesdk/utils/JsonValidator.kt | 146 + .../android/embracesdk/utils/Mutable.kt | 6 + 1035 files changed, 102788 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/ci-gradle.yml create mode 100644 .github/workflows/functional-tests.yml create mode 100644 .github/workflows/measure-sdk-size.yaml create mode 100644 .github/workflows/publish-snapshot.yml create mode 100644 .github/workflows/release-workflow.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 UPGRADING.md create mode 100644 build.gradle create mode 100644 buildSrc/README.md create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/io/embrace/gradle/EmbracePluginExtension.kt create mode 100644 buildSrc/src/main/kotlin/io/embrace/gradle/InternalEmbracePlugin.kt create mode 100644 buildSrc/src/main/kotlin/io/embrace/gradle/Versions.kt create mode 100644 config/checkstyle/google_checks.xml create mode 100644 config/detekt/detekt.yml create mode 100644 embrace-android-compose/.gitignore create mode 100644 embrace-android-compose/api/embrace-android-compose.api create mode 100644 embrace-android-compose/build.gradle.kts create mode 100644 embrace-android-compose/lint-baseline.xml create mode 100644 embrace-android-compose/src/main/AndroidManifest.xml create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ComposeActivityListener.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ClickedView.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeClickedTargetIterator.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeInternalErrorLogger.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceClickedTargetIterator.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceGestureListener.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt create mode 100644 embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceWindowCallback.kt create mode 100644 embrace-android-fcm/.gitignore create mode 100644 embrace-android-fcm/api/embrace-android-fcm.api create mode 100644 embrace-android-fcm/build.gradle create mode 100644 embrace-android-fcm/lint-baseline.xml create mode 100644 embrace-android-fcm/src/main/AndroidManifest.xml create mode 100644 embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java create mode 100644 embrace-android-okhttp3/CREDITS.md create mode 100644 embrace-android-okhttp3/README.md create mode 100644 embrace-android-okhttp3/api/embrace-android-okhttp3.api create mode 100644 embrace-android-okhttp3/build.gradle create mode 100644 embrace-android-okhttp3/lint-baseline.xml create mode 100644 embrace-android-okhttp3/src/main/AndroidManifest.xml create mode 100644 embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceCustomPathException.java create mode 100644 embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java create mode 100644 embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java create mode 100644 embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3PathOverrideRequest.java create mode 100644 embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/SdkFacade.java create mode 100644 embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient.java create mode 100644 embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt create mode 100644 embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/TestInspectionInterceptor.kt create mode 100644 embrace-android-sdk/.gitignore create mode 100644 embrace-android-sdk/CMakeLists.txt create mode 100644 embrace-android-sdk/api/embrace-android-sdk.api create mode 100644 embrace-android-sdk/build.gradle create mode 100644 embrace-android-sdk/config/detekt/baseline.xml create mode 100644 embrace-android-sdk/embrace-proguard.cfg create mode 100644 embrace-android-sdk/gradle/wrapper/gradle-wrapper.jar create mode 100644 embrace-android-sdk/gradle/wrapper/gradle-wrapper.properties create mode 100644 embrace-android-sdk/gradlew create mode 100644 embrace-android-sdk/gradlew.bat create mode 100644 embrace-android-sdk/lint-baseline.xml create mode 100644 embrace-android-sdk/proguard-rules.pro create mode 100644 embrace-android-sdk/src/androidTest/AndroidManifest.xml create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/expected-webview-core-vital.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-error-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-and-message-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-property-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-handled-exception.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-info-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-info-fail-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-info-with-property-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/log-warning-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-end-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-start-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-end-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-start-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-end-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-late-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-start-event.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/session-end.json create mode 100644 embrace-android-sdk/src/androidTest/assets/golden-files/session-start.json create mode 100644 embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/AnrIntegrationTest.kt create mode 100644 embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/LogMessageTest.kt create mode 100644 embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/MomentMessageTest.kt create mode 100644 embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/SessionMessageTest.kt create mode 100644 embrace-android-sdk/src/androidTest/res/layout/web_view_activity.xml create mode 100644 embrace-android-sdk/src/androidTest/res/raw/mparticle_js_sdk create mode 100644 embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/NullParametersTest.java create mode 100644 embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/PreSdkStartTest.java create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRule.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRuleExtensions.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/SessionApiTest.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/InternalErrorAssertions.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/LogMessageAssertions.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/SpanAssertions.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/LoggingApiTest.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/PublicApiTest.kt create mode 100644 embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/TracingApiTest.kt create mode 100644 embrace-android-sdk/src/main/AndroidManifest.xml create mode 100644 embrace-android-sdk/src/main/baseline-prof.txt create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/__libunwind_config.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/libunwind.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AndroidUnwinder.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86.S create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86_64.S create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Check.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFiles.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfDebugFrame.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrame.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEncoding.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfMemory.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfSection.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Elf.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterface.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Global.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/GlobalDebugImpl.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/JitDebug.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LICENSE create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Log.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LogAndroid.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MapInfo.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Maps.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Memory.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryBuffer.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryCache.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryFileAtOffset.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryLocal.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryMte.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOffline.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOfflineBuffer.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRange.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRemote.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/OWNERS create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/README.md create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Regs.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm64.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsInfo.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86_64.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/TEST_MAPPING create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadUnwinder.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Unwinder.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/file.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/log_main.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/strings.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/threads.cpp create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/unique_fd.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/cmake/CMakeLists.txt create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/GlobalDebugInterface.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/errno_restorer.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/file.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/macros.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/off64_t.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/stringprintf.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/strings.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/threads.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/unique_fd.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/utf8.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_external.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_support.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/log/android/log.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process_map.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/AndroidUnwinder.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Arch.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DexFiles.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfError.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfLocation.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfMemory.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfSection.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfStructs.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Elf.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/ElfInterface.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Error.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Global.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/JitDebug.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Log.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86_64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MapInfo.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Maps.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Memory.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Regs.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsGetLocal.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86_64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/SharedString.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86_64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Unwinder.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86_64.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/unistdfix.h create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.c create mode 100644 embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.h create mode 100644 embrace-android-sdk/src/main/cpp/CMakeLists.txt create mode 100644 embrace-android-sdk/src/main/cpp/CrashSampleClass.cpp create mode 100644 embrace-android-sdk/src/main/cpp/CrashSamplesImplClass.cpp create mode 100644 embrace-android-sdk/src/main/cpp/anr.c create mode 100644 embrace-android-sdk/src/main/cpp/anr.h create mode 100644 embrace-android-sdk/src/main/cpp/base_64_encoder.c create mode 100644 embrace-android-sdk/src/main/cpp/base_64_encoder.h create mode 100644 embrace-android-sdk/src/main/cpp/cpuinfo.c create mode 100644 embrace-android-sdk/src/main/cpp/emb_anr_manager.c create mode 100644 embrace-android-sdk/src/main/cpp/emb_log.c create mode 100644 embrace-android-sdk/src/main/cpp/emb_log.h create mode 100644 embrace-android-sdk/src/main/cpp/emb_ndk_crash_samples.cpp create mode 100644 embrace-android-sdk/src/main/cpp/emb_ndk_manager.c create mode 100644 embrace-android-sdk/src/main/cpp/emb_ndk_manager.h create mode 100644 embrace-android-sdk/src/main/cpp/file_marker.c create mode 100644 embrace-android-sdk/src/main/cpp/file_marker.h create mode 100644 embrace-android-sdk/src/main/cpp/file_writer.c create mode 100644 embrace-android-sdk/src/main/cpp/file_writer.h create mode 100644 embrace-android-sdk/src/main/cpp/jni_util.c create mode 100644 embrace-android-sdk/src/main/cpp/jni_util.h create mode 100644 embrace-android-sdk/src/main/cpp/safejni/safe_jni.c create mode 100644 embrace-android-sdk/src/main/cpp/safejni/safe_jni.h create mode 100644 embrace-android-sdk/src/main/cpp/sampler/emb_timer.c create mode 100644 embrace-android-sdk/src/main/cpp/sampler/emb_timer.h create mode 100644 embrace-android-sdk/src/main/cpp/sampler/sampler_structs.h create mode 100644 embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.cpp create mode 100644 embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.h create mode 100644 embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.c create mode 100644 embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.h create mode 100644 embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.c create mode 100644 embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.h create mode 100644 embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler_jni.c create mode 100644 embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.c create mode 100644 embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.h create mode 100644 embrace-android-sdk/src/main/cpp/signals/signal_utils.c create mode 100644 embrace-android-sdk/src/main/cpp/signals/signal_utils.h create mode 100644 embrace-android-sdk/src/main/cpp/signals/signals_c.c create mode 100644 embrace-android-sdk/src/main/cpp/signals/signals_c.h create mode 100644 embrace-android-sdk/src/main/cpp/signals/signals_cpp.cpp create mode 100644 embrace-android-sdk/src/main/cpp/signals/signals_cpp.h create mode 100644 embrace-android-sdk/src/main/cpp/stack_frames.h create mode 100644 embrace-android-sdk/src/main/cpp/unwinders/unwinder.c create mode 100644 embrace-android-sdk/src/main/cpp/unwinders/unwinder.h create mode 100644 embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.cpp create mode 100644 embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.h create mode 100644 embrace-android-sdk/src/main/cpp/utilities.c create mode 100644 embrace-android-sdk/src/main/cpp/utilities.h create mode 100644 embrace-android-sdk/src/main/cpp/utils/system_clock.c create mode 100644 embrace-android-sdk/src/main/cpp/utils/system_clock.h create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/BetaApi.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAndroidApi.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceApi.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAutomaticVerification.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceEvent.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterface.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceSamples.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/HttpPathOverrideRequest.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalApi.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogExceptionType.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogType.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogsApi.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/MomentsApi.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/NetworkRequestApi.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/SessionApi.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Severity.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UserApi.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ViewSwazzledHooks.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewChromeClientSwazzledHooks.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooks.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/annotation/StartupActivity.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrStacktraceSampler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/BlockedThreadListener.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/EmbraceAnrService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/NoOpAnrService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ThreadInfoCollector.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorChecker.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetector.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LooperCompat.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/ThreadMonitoringState.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/EmbraceNativeThreadSamplerService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerInstaller.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegate.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FindGoogleThread.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommand.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/DataCaptureService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/NoOpApplicationExitInfoService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityListener.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NoOpNetworkConnectivityService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/CpuInfoDelegate.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegate.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/CrashService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceCrashService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceUncaughtExceptionHandler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/Breadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/ActivityLifecycleBreadcrumbService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/EmbraceActivityLifecycleBreadcrumbService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/EmbraceMemoryService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/MemoryService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/NoOpMemoryService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataUtils.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/EmbraceOrientationService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/NoOpOrientationService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/OrientationService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/NoOpPowerSaveModeService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/PowerSaveModeService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/EmbraceStrictModeService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/StrictModeService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/EmbraceThermalStatusService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/NoOpThermalStatusService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/EmbraceUserService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/UserService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/EmbraceWebViewService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/WebViewService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/Clock.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/NormalizedIntervalClock.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/SystemClock.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiClient.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiRequest.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponse.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponseCache.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiUrlBuilder.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/CachedConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceApiService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnection.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlImpl.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/CacheService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceCacheService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/NetworkStatus.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigListener.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/EmbraceConfigService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AnrBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/MergedConfigBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SessionBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SpansBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/StartupBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/UnimplementedConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AnrLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ComposeLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/DomainLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/LocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SdkLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SessionLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/TapsLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ViewLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AnrRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AppExitInfoConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/LogRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkCaptureRuleRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/RemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SessionRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SpansRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/UiRemoteConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/WebViewVitals.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceEventService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceRemoteLogger.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventHandler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EmbraceGatingService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizerFacade.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/GatingService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/PerformanceInfoSanitizer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/Sanitizable.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionGatingKeys.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizerFacade.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/UserInfoSanitizer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AndroidServicesModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AnrModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CoreModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CrashModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CustomerLogModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataContainerModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DeliveryModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DependencyInjection.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/EssentialServiceModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/InitModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SdkObservabilityModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SessionModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SystemServiceModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/AndroidResourcesService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ApkToolsConfig.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/BuildInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/CacheableValue.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactory.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitecture.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitectureImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceSerializer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EventDescription.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/MessageType.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/OpenTelemetryClock.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/PatternCache.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/SharedObjectLoader.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/StartupEventInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/Systrace.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheck.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/TraceparentGenerator.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarker.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceExtensions.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanData.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanExporter.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/Initializable.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/MessageUtils.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtils.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/Uuid.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/VersionChecker.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/AndroidLogger.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalEmbraceLogger.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalErrorLogger.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkService.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NativeModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkServiceDelegate.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequest.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/ConnectionState.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingInputStreamWithCallback.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingOutputStream.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnection.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceSslUrlConnectionService.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionService.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpMethod.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/NetworkCaptureData.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/StreamHandlerFactoryInstaller.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/DomainSettings.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManager.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleData.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleState.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrInterval.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSample.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSampleList.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppExitInfoData.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivity.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessage.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BetaFeatures.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobMessage.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobSession.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Breadcrumbs.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Crash.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/CustomBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DeviceInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DiskUsage.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Event.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/EventMessage.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionError.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionErrorInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/FragmentBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Interval.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/JsException.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/MemoryWarning.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrash.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashData.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashDataError.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashMetadata.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeSymbols.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrInterval.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrSample.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrStackframe.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCallV2.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCapturedCall.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkEvent.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkRequests.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkSessionV2.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Orientation.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PerformanceInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PowerModeInterval.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PushNotificationBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Session.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/SessionMessage.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Stacktraces.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/StrictModeViolation.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/TapBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThermalState.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadState.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/UserInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ViewBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewBreadcrumb.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewInfo.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVital.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVitalType.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/PreferencesService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/registry/ServiceRegistry.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationChecker.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationExceptionHandler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/ComparableVersion.java create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/CrashSamplesNdkDelegate.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamples.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesNdkDelegateImpl.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceSampleCodeException.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationActions.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationResult.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerifyIntegrationException.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityListener.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/BackgroundActivityService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceActivityService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionProperties.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerListener.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionMessageSerializer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionService.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpan.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpanEvent.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/ErrorCode.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/TracingApi.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Consumer.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ExecutorServiceExtensions.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Function.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ListExtensions.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/PropertyUtils.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/StreamUtils.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ThreadUtils.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/VersionChecker.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/Unchecked.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/function/CheckedSupplier.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModule.kt create mode 100644 embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImpl.kt create mode 100644 embrace-android-sdk/src/main/res/values/strings.xml create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleListTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ApiUrlBuilderTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AppInfoTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/BetaFeaturesTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/CacheableValueTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ConfigRoundTripTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeliveryCacheManagerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeviceInfoTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DiskUsageTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceActivityLifecycleBreadcrumbServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceRule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTimingTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceApplicationExitInfoServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAutomaticVerificationTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCacheServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceConfigServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCrashServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceEventTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceGatingServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceMemoryServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNativeThreadSamplerServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNetworkConnectivityServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceOrientationServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePerformanceInfoServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePowerSaveModeServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceSessionPropertiesTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceThermalStatusServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUncaughtExceptionHandlerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUserServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceWebViewServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EssentialServiceModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerFacadeTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionErrorInfoTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionInfoTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBackgroundActivity.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBreadcrumbService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeDeliveryService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeNdkService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeSessionService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeWorkerThreadModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FragmentBreadcrumbTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/JsExceptionTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/LocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MemoryWarningTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MessageUtilsTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NativeThreadSamplerInstallerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NetworkUtilsTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/OrientationTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoSanitizerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PropertiesTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PushNotificationBreadcrumbTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ResourceReader.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionPerformanceInfoSanitizerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionRemoteConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerFacadeTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionStacktraceSampleJsonTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/TestCacheService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ThreadInfoTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UnityInternalInterfaceImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoSanitizerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UuidTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ViewBreadcrumbTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewBreadcrumbTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooksTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrIntervalTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrStacktraceSamplerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceSigquitDetectionServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceStrictModeServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/FindGoogleThreadTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ThreadInfoCollectorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSamplerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetectorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckSchedulerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandlerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetectorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrSampleJsonTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrStackframeJsonTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegateTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommandTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcessTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepositoryTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegateTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataReactNativeTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataUnityTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiClientTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiRequestTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/CachedConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/EmbraceApiServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManagerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorServiceTests.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorServiceTests.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutor.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/ApplicationExitInfoRemoteConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/BgActivityConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/KillSwitchRemoteConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/LogRemoteConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/NetworkRemoteConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/UiRemoteConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AnrBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SessionBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SpansBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/StartupBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/WebVitalsBehaviorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AnrLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AppLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ApplicationExitInfoLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/DomainLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/SessionLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/TapsLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ViewLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfigTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceEventServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceRemoteLoggerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EventHandlerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/BehaviorFakes.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeActivityService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidMetadataService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidResourcesService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAnrService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApiService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApplicationExitInfoService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeConfigService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCpuInfoDelegate.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCrashService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDataCaptureService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDeviceArchitecture.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeEventService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeGatingService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeLoggerAction.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerListener.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkConnectivityService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOpenTelemetryClock.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOrientationService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePerformanceInfoService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePowerSaveModeService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePreferenceService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSession.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSessionProperties.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeStrictModeService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeThermalStatusService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeUserService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeVersionChecker.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeWebViewService.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAndroidServicesModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAnrModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCrashModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCustomerLogModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataCaptureServiceModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataContainerModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeEssentialServiceModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeNativeModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSdkObservabilityModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSessionModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSystemServiceModule.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fixtures/SpansTestFixtures.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AndroidServicesModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AnrModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CoreModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CrashModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CustomerLogModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataContainerModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DeliveryModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DependencyInjectionKtTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/InitModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SdkObservabilityModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SystemServiceModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactoryTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/EmbraceSerializerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/OpenTelemetryClockTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheckTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/TraceparentGeneratorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarkerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifierTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanDataTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtilsTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalEmbraceLoggerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalErrorLoggerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepositoryTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/NativeModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequestTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManagerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/networking/EmbraceUrlAdapterTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessageTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BreadcrumbsTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/CustomBreadcrumbTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/EmbraceEventMessageTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/ExceptionErrorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataErrorTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashMetadataTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeSymbolsTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumbTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/SessionMessageTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/TapBreadcrumbTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/registry/ServiceRegistryTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/VersionTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceActivityServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceSessionServiceTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionHandlerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionMessageSerializerTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ExecutorExtensionsTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/JsonComparisonUtils.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ListExtensionsKtTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/PropertyUtilsTest.kt create mode 100644 embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImplTest.kt create mode 100644 embrace-android-sdk/src/test/resources/anr_config.json create mode 100644 embrace-android-sdk/src/test/resources/anr_default_config_expected.txt create mode 100644 embrace-android-sdk/src/test/resources/anr_interval_expected.json create mode 100644 embrace-android-sdk/src/test/resources/anr_override_config_expected.txt create mode 100644 embrace-android-sdk/src/test/resources/anr_tick_expected.json create mode 100644 embrace-android-sdk/src/test/resources/api_request.json create mode 100644 embrace-android-sdk/src/test/resources/app_config.json create mode 100644 embrace-android-sdk/src/test/resources/app_info_expected.json create mode 100644 embrace-android-sdk/src/test/resources/application_exit_info_local_config.json create mode 100644 embrace-android-sdk/src/test/resources/application_exit_info_remote_config.json create mode 100644 embrace-android-sdk/src/test/resources/auto_data_capture_config.json create mode 100644 embrace-android-sdk/src/test/resources/background_activity_config.json create mode 100644 embrace-android-sdk/src/test/resources/base_url_config.json create mode 100644 embrace-android-sdk/src/test/resources/bg_activity_config.json create mode 100644 embrace-android-sdk/src/test/resources/bg_activity_expected.json create mode 100644 embrace-android-sdk/src/test/resources/bg_activity_message_expected.json create mode 100644 embrace-android-sdk/src/test/resources/breadcrumb_custom.json create mode 100644 embrace-android-sdk/src/test/resources/breadcrumb_empty.json create mode 100644 embrace-android-sdk/src/test/resources/breadcrumb_fragment.json create mode 100644 embrace-android-sdk/src/test/resources/breadcrumb_view.json create mode 100644 embrace-android-sdk/src/test/resources/breadcrumb_view_custom.json create mode 100644 embrace-android-sdk/src/test/resources/breadcrumb_webview.json create mode 100644 embrace-android-sdk/src/test/resources/breadcrumbs_expected.json create mode 100644 embrace-android-sdk/src/test/resources/crash_expected.json create mode 100644 embrace-android-sdk/src/test/resources/crash_handler_config.json create mode 100644 embrace-android-sdk/src/test/resources/custom_breadcrumb_expected.json create mode 100644 embrace-android-sdk/src/test/resources/device_info_expected.json create mode 100644 embrace-android-sdk/src/test/resources/disk_usage_expected.json create mode 100644 embrace-android-sdk/src/test/resources/domain_config.json create mode 100644 embrace-android-sdk/src/test/resources/empty_file.txt create mode 100644 embrace-android-sdk/src/test/resources/event_expected.json create mode 100644 embrace-android-sdk/src/test/resources/eventmessage_expected.json create mode 100644 embrace-android-sdk/src/test/resources/exception_error_info_expected.json create mode 100644 embrace-android-sdk/src/test/resources/exception_info_expected.json create mode 100644 embrace-android-sdk/src/test/resources/expected_core_vital_repeated_elements_script.json create mode 100644 embrace-android-sdk/src/test/resources/expected_core_vital_script.json create mode 100644 embrace-android-sdk/src/test/resources/expected_core_vital_script1.json create mode 100644 embrace-android-sdk/src/test/resources/expected_core_vital_script_repeated.json create mode 100644 embrace-android-sdk/src/test/resources/fragment_breadcrumb_expected.json create mode 100644 embrace-android-sdk/src/test/resources/js_exception_expected.json create mode 100644 embrace-android-sdk/src/test/resources/local_network_config.json create mode 100644 embrace-android-sdk/src/test/resources/log_config.json create mode 100644 embrace-android-sdk/src/test/resources/memory_warning_expected.json create mode 100644 embrace-android-sdk/src/test/resources/metadata_appinfo_expected.json create mode 100644 embrace-android-sdk/src/test/resources/metadata_react_native_appinfo_expected.json create mode 100644 embrace-android-sdk/src/test/resources/native_crash_data_error_expected.json create mode 100644 embrace-android-sdk/src/test/resources/native_crash_data_expected.json create mode 100644 embrace-android-sdk/src/test/resources/native_crash_expected.json create mode 100644 embrace-android-sdk/src/test/resources/native_crash_metadata_expected.json create mode 100644 embrace-android-sdk/src/test/resources/native_crash_raw.txt create mode 100644 embrace-android-sdk/src/test/resources/native_symbols_expected.json create mode 100644 embrace-android-sdk/src/test/resources/network_config.json create mode 100644 embrace-android-sdk/src/test/resources/orientation_expected.json create mode 100644 embrace-android-sdk/src/test/resources/perf_info_expected.json create mode 100644 embrace-android-sdk/src/test/resources/public_key_config.json create mode 100644 embrace-android-sdk/src/test/resources/push_notification_breadcrumb_expected.json create mode 100644 embrace-android-sdk/src/test/resources/remote_config_response.json create mode 100644 embrace-android-sdk/src/test/resources/rn_action_breadcrumb_expected.json create mode 100644 embrace-android-sdk/src/test/resources/session_config.json create mode 100644 embrace-android-sdk/src/test/resources/session_expected.json create mode 100644 embrace-android-sdk/src/test/resources/session_message_expected.json create mode 100644 embrace-android-sdk/src/test/resources/span_expected.json create mode 100644 embrace-android-sdk/src/test/resources/startup_moment_config.json create mode 100644 embrace-android-sdk/src/test/resources/startup_sampling_config.json create mode 100644 embrace-android-sdk/src/test/resources/startup_sampling_default_config_expected.txt create mode 100644 embrace-android-sdk/src/test/resources/startup_sampling_override_config_expected.txt create mode 100644 embrace-android-sdk/src/test/resources/tap_breadcrumb_expected.json create mode 100644 embrace-android-sdk/src/test/resources/taps_config.json create mode 100644 embrace-android-sdk/src/test/resources/test_screenshot.jpg create mode 100644 embrace-android-sdk/src/test/resources/thread_info_expected.json create mode 100644 embrace-android-sdk/src/test/resources/ui_config.json create mode 100644 embrace-android-sdk/src/test/resources/user_info_expected.json create mode 100644 embrace-android-sdk/src/test/resources/view_breadcrumb_expected.json create mode 100644 embrace-android-sdk/src/test/resources/view_config.json create mode 100644 embrace-android-sdk/src/test/resources/web_view_config.json create mode 100644 embrace-android-sdk/src/test/resources/webview_breadcrumb_expected.json create mode 100644 embrace-lint/.gitignore create mode 100644 embrace-lint/README.md create mode 100644 embrace-lint/build.gradle create mode 100644 embrace-lint/src/main/java/io/embrace/android/lint/EmbraceLintRegistry.kt create mode 100644 embrace-lint/src/main/java/io/embrace/android/lint/EmbracePublicApiPackageRule.kt create mode 100644 embrace-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry create mode 100644 embrace-lint/src/test/kotlin/io/embrace/android/lint/EmbracePublicApiPackageRuleTest.kt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 scripts/release.gradle create mode 100644 settings.gradle create mode 100644 sonar-project.properties create mode 100644 test-server/.gitignore create mode 100644 test-server/README.md create mode 100644 test-server/build.gradle create mode 100644 test-server/consumer-rules.pro create mode 100644 test-server/lint-baseline.xml create mode 100644 test-server/proguard-rules.pro create mode 100644 test-server/src/main/AndroidManifest.xml create mode 100644 test-server/src/main/kotlin/androidx/lifecycle/MockReportFragment.kt create mode 100644 test-server/src/main/kotlin/androidx/lifecycle/ProcessLifecycleOwnerAccess.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/ActivityServiceHooks.java create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/BaseTest.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/BuildInfoHooks.java create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/ConfigHooks.java create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceContext.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceFileObserver.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/FakePackageManager.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/TestServer.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockActivity.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockFragmentManager.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockView.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockWindow.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/PauseProcessListener.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/BitmapFactory.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/FailureLatch.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/JsonValidator.kt create mode 100644 test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/Mutable.kt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..72ec400e96 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,15 @@ +# DevOps +.github @embrace-io/team-devops + +# Global owners +* @embrace-io/sdk-android + +# API surface owners +*.api @fractalwrench @fnewberg +**/Embrace.java @fractalwrench @fnewberg +**/EmbraceApi.java @fractalwrench @fnewberg +**/EmbraceAndroidApi.java @fractalwrench @fnewberg +**/EmbraceInternalInterface.java @fractalwrench @fnewberg +**/FlutterInternalInterface.java @fractalwrench @fnewberg +**/ReactNativeInternalInterface.java @fractalwrench @fnewberg +**/UnityInternalInterface.java @fractalwrench @fnewberg diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..9ebeb650ff --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +## Goal + + + +## Testing + + + +## Release Notes + + + +**WHAT**:
+**WHY**:
+**WHO**:
+ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..123014908b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000..6aaefb742c --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,11 @@ +changelog: + categories: + - title: Features + labels: + - '*' + exclude: + labels: + - dependencies + - title: Dependencies + labels: + - dependencies diff --git a/.github/workflows/ci-gradle.yml b/.github/workflows/ci-gradle.yml new file mode 100644 index 0000000000..965935f48f --- /dev/null +++ b/.github/workflows/ci-gradle.yml @@ -0,0 +1,84 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: + release: + types: [ released ] + +jobs: + gradle-test: + runs-on: ubuntu-22.04-4cores + strategy: + matrix: + jdk-version: ["11"] + ndk-version: ["21.4.7075529"] + steps: + - name: Checkout Branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # EMB-11508 - See https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/ + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + ~/.sonar/cache + key: ${{ runner.os }}-gradle-jdk${{ matrix.jdk-version }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle + + - name: Install JDK ${{ matrix.jdk-version }} + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: ${{ matrix.jdk-version }} + + - name: Setup NDK ${{ matrix.ndk-version }} + run: | + export ANDROID_ROOT=/usr/local/lib/android + export ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk + export ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle + ln -sfn $ANDROID_SDK_ROOT/ndk/${{ matrix.ndk-version }} $ANDROID_NDK_ROOT + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + + # Build the entire project, run the tests, and run all static analysis + - name: Gradle Build + run: ./gradlew assembleRelease check --stacktrace + + - name: Archive Test Results + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: android-sdk-test-results + path: embrace-android-sdk/build/reports/tests/ + + - name: Run Kover Code Coverage + run: ./gradlew clean koverXmlReportRelease + + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + file: embrace-android-sdk/build/reports/kover/reportRelease.xml + + - name: Cleanup Gradle Cache + # Based on https://docs.github.com/en/actions/guides/building-and-testing-java-with-gradle#caching-dependencies + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml new file mode 100644 index 0000000000..68891cd43f --- /dev/null +++ b/.github/workflows/functional-tests.yml @@ -0,0 +1,128 @@ +name: Run Functional Tests + +on: + workflow_dispatch: + workflow_call: + secrets: + token: + required: true + schedule: + - cron: '0 3 * * *' + pull_request: + branches: [ master ] + +env: + ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 60 + +jobs: + test: + runs-on: macos-latest + strategy: + matrix: + jdk-version: ["11"] + ndk-version: ["21.4.7075529"] + api-level: [29] + target: [default] + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + token: ${{ secrets.CD_GITHUB_TOKEN || secrets.token }} + fetch-depth: 0 + + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + ~/.sonar/cache + key: ${{ runner.os }}-gradle-jdk${{ matrix.jdk-version }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle + + # Cache the emulator + - name: AVD cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }} + + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: embrace-io/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + force-avd-creation: false + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Install JDK ${{ matrix.jdk-version }} + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: ${{ matrix.jdk-version }} + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Run Functional Tests + uses: embrace-io/android-emulator-runner@v2 + id: runFunctionalTests1 + continue-on-error: true + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86_64 + profile: Nexus 6 + # Grab the failures from the device so we can include them in results. + # Exit with non-zero so GH checks still fail. + script: ./gradlew connectedCheck --stacktrace || (adb pull /storage/emulated/0/Android/data/io.embrace.android.embracesdk.test/cache/test_failure/ && exit 127) + + - name: Retry Functional Tests + uses: embrace-io/android-emulator-runner@v2 + id: runFunctionalTests2 + if: steps.runFunctionalTests1.outcome == 'failure' + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86_64 + profile: Nexus 6 + # Grab the failures from the device so we can include them in results. + # Exit with non-zero so GH checks still fail. + script: ./gradlew connectedCheck --stacktrace || (adb pull /storage/emulated/0/Android/data/io.embrace.android.embracesdk.test/cache/test_failure/ && exit 127) + + - name: Archive Test Results + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: sdk-functional-test-results + path: | + embrace-android-sdk/build/reports/androidTests/connected + test_failure + + - name: Post workflow result + id: slack + if: ${{steps.runFunctionalTests1.outcome == 'failure'}} + uses: slackapi/slack-github-action@v1.24.0 + with: + payload: | + { + "functional_test_retried": "${{steps.runFunctionalTests1.outcome == 'failure'}}", + "workflow_failed": "${{steps.runFunctionalTests1.outcome == 'failure' && steps.runFunctionalTests2.outcome == 'failure'}}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + + - name: Cleanup Gradle Cache + # Based on https://docs.github.com/en/actions/guides/building-and-testing-java-with-gradle#caching-dependencies + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.github/workflows/measure-sdk-size.yaml b/.github/workflows/measure-sdk-size.yaml new file mode 100644 index 0000000000..c5b8bed606 --- /dev/null +++ b/.github/workflows/measure-sdk-size.yaml @@ -0,0 +1,12 @@ +name: Measure SDK Size + +on: + workflow_dispatch: + +jobs: + measure-sdk-size: + uses: embrace-io/android-size-measure/.github/workflows/analyze-sdk-size.yml@main + with: + sdk_version: '5.21.0' + token: ${{ secrets.CD_GITHUB_TOKEN }} + diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml new file mode 100644 index 0000000000..97bcf1a588 --- /dev/null +++ b/.github/workflows/publish-snapshot.yml @@ -0,0 +1,100 @@ +name: Snapshot + +env: + MAVEN_QA_USER: github + MAVEN_QA_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * *' + +jobs: + functional: + name: Run Functional Tests + uses: ./.github/workflows/functional-tests.yml + secrets: + token: ${{ secrets.CD_GITHUB_TOKEN }} +# swazzlertests: +# uses: embrace-io/swazzler-test/.github/workflows/callable_swazzler_test.yml@master +# secrets: +# token: ${{ secrets.CD_GITHUB_TOKEN }} + # baseline-profile: + # name: Update Baseline Profile file + #todo + sdk: + name: Publish SDK to Maven Internal + needs: [functional] # swazzlertests, baseline-profile + runs-on: ubuntu-latest + strategy: + matrix: + jdk-version: ["11"] + ndk-version: ["21.4.7075529"] + steps: + - name: Checkout Branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + ~/.sonar/cache + key: ${{ runner.os }}-gradle-jdk${{ matrix.jdk-version }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle + + - name: Install JDK ${{ matrix.jdk-version }} + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: ${{ matrix.jdk-version }} + + - name: Setup NDK ${{ matrix.ndk-version }} + run: | + export ANDROID_ROOT=/usr/local/lib/android + export ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk + export ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle + ln -sfn $ANDROID_SDK_ROOT/ndk/${{ matrix.ndk-version }} $ANDROID_NDK_ROOT + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + + # Build the entire project, run the tests, and run all static analysis + - name: Gradle Build + run: ./gradlew assembleRelease check --stacktrace + + - name: Archive Test Results + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: android-sdk-test-results + path: embrace-android-sdk/build/reports/tests/ + + - name: Gradlew Release to internal Maven + run: | + ./gradlew clean publishReleasePublicationToSnapshotRepository --stacktrace + + - name: Checkout Swazzler + uses: actions/checkout@v3 + with: + repository: embrace-io/embrace-swazzler3 + ref: master + path: ./embrace-swazzler3 + token: ${{ secrets.CD_GITHUB_TOKEN }} + + - name: Swazzler Release + run: | + cd ./embrace-swazzler3 + ./gradlew clean publishPluginMavenPublicationToSnapshotRepository --stacktrace + + - name: Cleanup Gradle Cache + # Based on https://docs.github.com/en/actions/guides/building-and-testing-java-with-gradle#caching-dependencies + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml new file mode 100644 index 0000000000..b46812e63d --- /dev/null +++ b/.github/workflows/release-workflow.yml @@ -0,0 +1,132 @@ +name: Release + +env: + SONATYPE_USERNAME: embrace-io + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + MAVEN_QA_USER: github + MAVEN_QA_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + mavenSigningKeyId: ${{ secrets.MAVEN_ANDROID_SIGNING_KEY }} + mavenSigningKeyRingFileEncoded: ${{ secrets.MAVEN_ANDROID_GPG_KEY }} + mavenSigningKeyPassword: ${{ secrets.MAVEN_ANDROID_SIGNING_PASSWORD }} + +on: + workflow_dispatch: + inputs: + current_version: + description: 'Version to release' + required: true + next_version: + description: 'Next Version (Do NOT include -SNAPSHOT, will be added automatically)' + required: true + +jobs: + release: + runs-on: ubuntu-latest + strategy: + matrix: + jdk-version: ["11"] + ndk-version: ["21.4.7075529"] + steps: + - name: Decode Keystore + run: | + mkdir "$RUNNER_TEMP"/keystore + echo $mavenSigningKeyRingFileEncoded | base64 -di > "$RUNNER_TEMP"/keystore/2DE631C1.gpg + echo "mavenSigningKeyRingFile=$RUNNER_TEMP/keystore/2DE631C1.gpg" >> $GITHUB_ENV + + - name: Checkout Branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + ~/.sonar/cache + key: ${{ runner.os }}-gradle-jdk${{ matrix.jdk-version }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle + + - name: Install JDK ${{ matrix.jdk-version }} + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: ${{ matrix.jdk-version }} + + - name: Setup NDK ${{ matrix.ndk-version }} + run: | + export ANDROID_ROOT=/usr/local/lib/android + export ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk + export ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle + ln -sfn $ANDROID_SDK_ROOT/ndk/${{ matrix.ndk-version }} $ANDROID_NDK_ROOT + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + + # Build the entire project, run the tests, and run all static analysis + - name: Gradle Build + run: ./gradlew assembleRelease check --stacktrace + + - name: Archive Test Results + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: android-sdk-test-results + path: embrace-android-sdk/build/reports/tests/ + + - name: Gradlew Release + run: | + git config --global user.name "embrace-ci" + git config --global user.email "embrace-ci@users.noreply.github.com" + git config --global url."https://${{ secrets.CD_GITHUB_USER }}:${{ secrets.CD_GITHUB_TOKEN }}@github.com".insteadOf "https://github.com" + sed -i -r "s#version = ([^\']+)#version = ${{ github.event.inputs.current_version }}#" gradle.properties + git add gradle.properties + git commit -m "CI/CD: change version to be released: ${{ github.event.inputs.current_version }}" + git push + ./gradlew clean publishReleasePublicationToSonatypeRepository -Dorg.gradle.parallel=false --stacktrace + sed -i -r "s#version = ([^\']+)#version = ${{ github.event.inputs.next_version }}-SNAPSHOT#" gradle.properties + git add gradle.properties + git commit -m "CI/CD: set next version: ${{ github.event.inputs.next_version }}" + git push + + - name: Generate Documentation + run: ./gradlew dokkaHtml + + - name: Publish gh-pages + run: | + mv docs .docs-newly-generated # new docs generated by previous step + git checkout gh-pages + git rm -rf docs # old docs on gh-pages branch + mv .docs-newly-generated docs + date > docs/version.txt + echo ${{ github.sha }} >> docs/version.txt + echo ${{ github.event.release.tag_name }} >> docs/version.txt + git add -f docs + git push --force origin gh-pages + + - name: Record SDK Version History (${{ github.event.inputs.current_version }}) + run: | + curl -X POST ${{ vars.SDK_VERSION_URL }}/android/version/ -H 'X-Embrace-CI: ${{ secrets.SDK_VERSION_TOKEN }}' -H 'Content-Type: application/json' -d '{"version": "${{ github.event.inputs.current_version }}"}' + + - name: Checkout Swazzler + uses: actions/checkout@v3 + with: + repository: embrace-io/embrace-swazzler3 + ref: master + path: ./embrace-swazzler3 + token: ${{ secrets.CD_GITHUB_TOKEN }} + + - name: Swazzler Release + run: | + cd ./embrace-swazzler3 + ./gradlew clean release -Dorg.gradle.parallel=false -Prelease.useAutomaticVersion=true -Prelease.releaseVersion=${{ github.event.inputs.current_version }} -Prelease.newVersion=${{ github.event.inputs.next_version }}-SNAPSHOT --stacktrace + + - name: Cleanup Gradle Cache + # Based on https://docs.github.com/en/actions/guides/building-and-testing-java-with-gradle#caching-dependencies + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..304876f07c --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/ + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +#javadoc generated folder +docs/ + +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000000..3561bd6cf3 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +

+ + + + + Embrace + + +

+ +The Embrace Android SDK provides instrumentation for Android apps. + +[![codecov](https://codecov.io/gh/embrace-io/embrace-android-sdk/branch/master/graph/badge.svg?token=QycvaJjZgr)](https://codecov.io/gh/embrace-io/embrace-android-sdk) +[![android api](https://img.shields.io/badge/Android_API-16-green.svg "Android min API 21")](https://dash.embrace.io/signup/) +[![build](https://img.shields.io/github/actions/workflow/status/embrace-io/embrace-android-sdk/ci-gradle.yml)](https://github.com/embrace-io/embrace-android-sdk/actions) + +# Getting Started + +> :warning: **This is for native android apps**: Leverage in our Unity, ReactNative and Flutter SDKs for cross-platform apps + +- [Go to our dashboard](https://dash.embrace.io/signup/) to create an account and get your API key +- Check our [guide](https://embrace.io/docs/android/integration/) to integrate the SDK into your app + +## Upgrading from 5.x + +Follow our [upgrading guide](https://github.com/embrace-io/embrace-android-sdk/blob/master/UPGRADING.md) + +# Usage + +- Refer to our [Features page](https://embrace.io/docs/android/features/) to learn about the features Embrace SDK provides + +# Support + +- Join our [Community Slack](https://embraceio-community.slack.com/) +- Contact us [support@embrace.io](mailto:support@embrace.io) + +# Development + +## Code Formatting + +In most of our repos we are using Detekt to analyse our kotlin code. This analysis should be done before the new code is merged to master. It’s considered a good practice to run the command before pushing our code. Github workflows will also be running this check. + +To run the check locally, you can run the following command in the root directory of the project: + +`./gradlew detekt` + +n some cases, the errors get fixed just by running the command, so if you run it again, you could get less errors than the first time. As a result, it will list the errors and you need to go and fix them if you consider appropriate. + +You can run the command until you get no errors or until you get only the errors you don’t want to fix. + +If you have errors you want to ignore, you need to run: + +```bash +./gradlew detektBaseline +``` + +This command will add a line per error to be ignored into the `baseline.xml` file. This way, this file will be updated and the code smell will be also ignored by Github Workflows. + +## License + +See the [LICENSE](https://github.com/embrace-io/embrace-android-sdk/blob/master/LICENSE) +for details. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000000..e6588e5bc4 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,108 @@ +# Upgrade guide + +# Upgrading from 5.x to 6.x + +Version X of the Embrace SDK renames some functions. This has been done to reduce +confusion & increase consistency across our SDKs. + +Functions that have been marked as deprecated will still work as before, but will be removed in +the next major version release. Please upgrade when convenient, and get in touch if you have a +use-case that isn’t supported by the new API. + +| Old API | New API | Comments | +|-------------------------------------------------------|---------------------------------------------------------------------|:----------------------------------------------------------------------| +| `Embrace.getInstance().startFragment(String)` | `Embrace.getInstance().startView(String)` | Renamed function to better describe functionality. | +| `Embrace.getInstance().endFragment(String)` | `Embrace.getInstance().endView(String)` | Renamed function to better describe functionality. | +| `Embrace.getInstance().setUserPersona(String)` | `Embrace.getInstance().addUserPersona(String)` | Renamed function to better describe functionality. | +| `Embrace.getInstance().logBreadcrumb(String)` | `Embrace.getInstance().addBreadcrumb(String)` | Renamed function to better describe functionality. | +| `Embrace.getInstance().startEvent()` | `Embrace.getInstance().startMoment(String)` | Renamed function to better describe functionality. | +| `Embrace.getInstance().endEvent()` | `Embrace.getInstance().endMoment(String)` | Renamed function to better describe functionality. | +| `Embrace.getInstance().logInfo(String, ...)` | `Embrace.getInstance().logMessage(...)` | Altered function signature to standardise behavior. | +| `Embrace.getInstance().logWarning(String, ...)` | `Embrace.getInstance().logMessage(...)` | Altered function signature to standardise behavior. | +| `Embrace.getInstance().logError(String, ...)` | `Embrace.getInstance().logMessage(...)` | Altered function signature to standardise behavior. | +| `Embrace.getInstance().logError(Throwable)` | `Embrace.getInstance().logException()` | Altered function signature to standardise behavior. | +| `Embrace.getInstance().logError(StacktraceElement[])` | `Embrace.getInstance().logCustomStacktrace()` | Altered function signature to standardise behavior. | +| `EmbraceLogger` | `Embrace.getInstance().logMessage()` | Moved function calls to main Embrace interface. | +| `LogType` | `Severity` | Use Severity enum rather than LogType. | +| `PurchaseFlow` | None | Please contact Embrace if you have a use-case for this functionality. | +| `RegistrationFlow` | None | Please contact Embrace if you have a use-case for this functionality. | +| `SubscriptionFlow` | None | Please contact Embrace if you have a use-case for this functionality. | +| `Embrace.getInstance().logNetworkCall()` | `Embrace.getInstance().recordNetworkRequest(EmbraceNetworkRequest)` | Renamed function to better describe functionality. | +| `Embrace.getInstance().logNetworkClientError()` | `Embrace.getInstance().recordNetworkRequest(EmbraceNetworkRequest)` | Renamed function to better describe functionality. | + +### Previously deprecated APIs that have been removed + +| Old API | Comments | +|---------------------------------------------|----------------------------------------------------------| +| `ConnectionQuality` | Deprecated API that is no longer supported. | +| `ConnectionQualityListener` | Deprecated API that is no longer supported. | +| `Embrace.enableStartupTracing()` | Deprecated API that is no longer supported. | +| `Embrace.enableEarlyAnrCapture()` | Deprecated API that is no longer supported. | +| `Embrace.setLogLevel()` | Deprecated API that is no longer supported. | +| `Embrace.enableDebugLogging()` | Deprecated API that is no longer supported. | +| `Embrace.disableDebugLogging()` | Deprecated API that is no longer supported. | +| `Embrace.logUnhandledJsException()` | Deprecated internal API that was unintentionally visible | +| `Embrace.logUnhandledUnityException()` | Deprecated internal API that was unintentionally visible | +| `Embrace.setReactNativeVersionNumber()` | Deprecated internal API that was unintentionally visible | +| `Embrace.setJavaScriptPatchNumber()` | Deprecated internal API that was unintentionally visible | +| `Embrace.setJavaScriptBundleURL()` | Deprecated internal API that was unintentionally visible | +| `Embrace.setUnityMetaData()` | Deprecated internal API that was unintentionally visible | +| `Embrace.logDartError()` | Deprecated internal API that was unintentionally visible | +| `Embrace.logDartErrorWithType()` | Deprecated internal API that was unintentionally visible | +| `Embrace.setEmbraceFlutterSdkVersion()` | Deprecated internal API that was unintentionally visible | +| `Embrace.setDartVersion()` | Deprecated internal API that was unintentionally visible | +| `Embrace.addConnectionQualityListener()` | Deprecated internal API that was unintentionally visible | +| `Embrace.removeConnectionQualityListener()` | Deprecated internal API that was unintentionally visible | +| `Embrace.logPushNotification()` | Deprecated internal API that was unintentionally visible | +| `EmbraceNetworkRequest.withByteIn()` | Use `withBytesIn()` instead | +| `EmbraceNetworkRequest.withByteOut()` | Use `withBytesOut()` instead | +| `EmbraceNetworkRequestV2.withByteIn()` | Use `withBytesIn()` instead | +| `EmbraceNetworkRequestV2.withByteOut()` | Use `withBytesOut()` instead | + +## Hidden symbols + +The following symbols have been hidden in version 6 of the Embrace Android SDK. These were +unintentionally +exposed in previous versions. Please get in touch if you had a use-case for these symbols that isn't +supported with the new API. + +- `Absent` +- `ActivityListener` +- `AndroidToUnityCallback` +- `ApkToolsConfig` +- `BuildInfo` +- `CheckedBiConsumer` +- `CheckedBiFunction` +- `CheckedBinaryOperator` +- `CheckedBiPredicate` +- `CheckedConsumer` +- `CheckedFunction` +- `CheckedPredicate` +- `CheckedRunnable` +- `CheckedSupplier` +- `CountingOutputStream` +- `Embrace.` +- `EmbraceConnection` +- `EmbraceHttpUrlConnection` +- `EmbraceHttpUrlConnectionOverride` +- `EmbraceHttpUrlStreamHandler` +- `EmbraceHttpsUrlConnection` +- `EmbraceHttpsUrlStreamHandler` +- `EmbraceUrl` +- `EmbraceUrlStreamHandler` +- `EmbraceUrlStreamHandlerFactory` +- `EmbraceEvent` +- `EmbraceConnectionImpl` +- `EmbraceUrlAdapter` +- `EmbraceUrlImpl` +- `Event` +- `ExecutorUtils` +- `HandleExceptionError` +- `NetworkCaptureEncryptionManager` +- `Optional` +- `Preconditions` +- `Present` +- `RnActionBreadcrumb` +- `ThreadUtils` +- `Unchecked` +- `Uuid` diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..5329e5d886 --- /dev/null +++ b/build.gradle @@ -0,0 +1,31 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +import io.embrace.gradle.Versions + +buildscript { + repositories { + google() + maven { + url "https://plugins.gradle.org/m2/" + } + } + + dependencies { + classpath("com.android.tools.build:gradle:${Versions.agp}") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}") + classpath("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${Versions.detekt}") + classpath("org.jetbrains.dokka:dokka-gradle-plugin:${Versions.dokka}") + } +} + +allprojects { + repositories { + google() + maven { + url "https://plugins.gradle.org/m2/" + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/buildSrc/README.md b/buildSrc/README.md new file mode 100644 index 0000000000..83c5674943 --- /dev/null +++ b/buildSrc/README.md @@ -0,0 +1,4 @@ +# Internal Embrace Plugin +This [convention plugin](https://docs.gradle.org/current/samples/sample_convention_plugins.html) +is used for all of Embrace's library modules to configure their gradle scripts in a consistent way. + diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000000..80a1b7218f --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `java-gradle-plugin` + `kotlin-dsl` +} + +gradlePlugin { + plugins { + register("internal-embrace-plugin") { + id = "internal-embrace-plugin" + implementationClass = "io.embrace.gradle.InternalEmbracePlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + + // TODO: future - these versions must be kept in sync when updating buildscript deps. + implementation("com.android.tools.build:gradle:7.3.0") + implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32") + implementation("org.jetbrains.kotlinx:binary-compatibility-validator:0.12.1") +} diff --git a/buildSrc/src/main/kotlin/io/embrace/gradle/EmbracePluginExtension.kt b/buildSrc/src/main/kotlin/io/embrace/gradle/EmbracePluginExtension.kt new file mode 100644 index 0000000000..9f53e08ea9 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/embrace/gradle/EmbracePluginExtension.kt @@ -0,0 +1,19 @@ +package io.embrace.gradle + +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +/** + * Options to configure the build for an Embrace module. + */ +open class EmbracePluginExtension(factory: ObjectFactory) { + + /** + * Whether the API should be checked for binary compatibility against a baseline. New public + * symbols will fail the build - you should check whether its necessary to expose these. + * + * True by default. + */ + open val apiBinaryCompatChecks: Property = + factory.property(Boolean::class.java).convention(true) +} diff --git a/buildSrc/src/main/kotlin/io/embrace/gradle/InternalEmbracePlugin.kt b/buildSrc/src/main/kotlin/io/embrace/gradle/InternalEmbracePlugin.kt new file mode 100644 index 0000000000..ad4094af84 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/embrace/gradle/InternalEmbracePlugin.kt @@ -0,0 +1,203 @@ +package io.embrace.gradle + +import com.android.build.api.dsl.LibraryExtension +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import kotlinx.validation.ApiValidationExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.quality.Checkstyle +import org.gradle.api.plugins.quality.CheckstyleExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +@Suppress("UnstableApiUsage") // because most of AGP is unstable :| +class InternalEmbracePlugin : Plugin { + + override fun apply(project: Project) { + configureBuildPlugins(project) + + // TODO: (future) - these scripts should be integrated into this class. + if (project.name != "test-server") { // don't want to release our test code... + project.apply(from = project.file("../scripts/release.gradle")) + } + + project.pluginManager.withPlugin("com.android.library") { + val android = project.extensions.getByType(LibraryExtension::class.java) + configureAndroidExtension(project, android) + configureKotlinOptions(project) + } + + val embrace = + project.extensions.create("embraceOptions", EmbracePluginExtension::class.java) + + configureModuleDependencies(project) + configureDetekt(project) + configureCheckstyle(project) + configureApiValidation(project, embrace) + } + + /** + * Configures behavior of Checkstyle plugin. + */ + private fun configureCheckstyle(project: Project) { + val checkstyle = project.extensions.getByType(CheckstyleExtension::class.java) + checkstyle.run { + toolVersion = "10.3.2" + } + + val checkstyleTaskProvider = project.tasks.register("checkstyle", Checkstyle::class.java) + checkstyleTaskProvider.configure { + configFile = project.rootProject.file("config/checkstyle/google_checks.xml") + ignoreFailures = false + isShowViolations = true + source("src") + include("**/*.java") + classpath = project.files() + maxWarnings = 0 + } + } + + /** + * Configures behavior of API binary compatibility check plugin. + */ + private fun configureApiValidation( + project: Project, + embrace: EmbracePluginExtension + ) { + project.afterEvaluate { + project.configure { + validationDisabled = !embrace.apiBinaryCompatChecks.get() + nonPublicMarkers += mutableSetOf() + } + } + } + + /** + * Configures behavior of Detekt plugin. + */ + private fun configureDetekt(project: Project) { + val detekt = project.extensions.getByType(DetektExtension::class.java) + + detekt.run { + buildUponDefaultConfig = true + autoCorrect = true + config = + project.files("${project.rootDir}/config/detekt/detekt.yml") // overwrite default behaviour here + baseline = + project.file("${project.projectDir}/config/detekt/baseline.xml") // suppress pre-existing issues + } + + project.tasks.withType(Detekt::class.java).configureEach { + jvmTarget = "1.8" + reports { + html.required.set(true) + xml.required.set(false) + txt.required.set(true) + sarif.required.set(false) + md.required.set(false) + } + } + + project.tasks.withType(DetektCreateBaselineTask::class.java).configureEach { + jvmTarget = "1.8" + } + } + + /** + * Configures the Android extension. + */ + private fun configureAndroidExtension(project: Project, android: LibraryExtension) { + android.run { + compileSdk = Versions.compileSdk + + defaultConfig { + minSdk = Versions.minSdk + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + aarMetadata { + minCompileSdk = 16 + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + lint { + abortOnError = true + warningsAsErrors = true + checkAllWarnings = true + checkReleaseBuilds = false // run on CI instead, speeds up release builds + baseline = project.file("lint-baseline.xml") + disable.addAll(mutableSetOf("GradleDependency", "NewerVersionAvailable")) + } + + testOptions { + // Calling Android logging methods will throw exceptions if this is false + // see: http://tools.android.com/tech-docs/unit-testing-support#TOC-Method-...-not-mocked.- + unitTests.isReturnDefaultValues = true + unitTests.isIncludeAndroidResources = true + } + + buildTypes { + named("release") { + isMinifyEnabled = false + } + } + testOptions { + unitTests { + all { test -> + test.maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2) + 1 + } + } + } + } + } + + private fun configureKotlinOptions(project: Project) { + project.tasks.withType(KotlinCompile::class.java).all { + kotlinOptions { + apiVersion = "1.4" + languageVersion = "1.4" + jvmTarget = JavaVersion.VERSION_1_8.toString() + freeCompilerArgs = freeCompilerArgs + "-Xexplicit-api=strict" + + // FIXME: targeting Kotlin 1.4 emits a warning that I can't find a way to suppress. + // Disabling this check for now. + allWarningsAsErrors = false + } + } + } + + /** + * Adds common plugins to the project. + */ + private fun configureBuildPlugins(project: Project) { + val plugins = listOf( + "com.android.library", + "kotlin-android", + "io.gitlab.arturbosch.detekt", + "checkstyle", + "binary-compatibility-validator" + ) + plugins.forEach { project.plugins.apply(it) } + } + + /** + * Adds build-time dependencies to the project. + */ + private fun configureModuleDependencies(project: Project) { + project.dependencies { + add("testImplementation", "junit:junit:${Versions.junit}") + add("implementation", "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlinExposed}") + add("detektPlugins", "io.gitlab.arturbosch.detekt:detekt-formatting:${Versions.detekt}") + add("lintChecks", project.project(":embrace-lint")) + } + } +} diff --git a/buildSrc/src/main/kotlin/io/embrace/gradle/Versions.kt b/buildSrc/src/main/kotlin/io/embrace/gradle/Versions.kt new file mode 100644 index 0000000000..39d63034d4 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/embrace/gradle/Versions.kt @@ -0,0 +1,20 @@ +package io.embrace.gradle + +/** + * Defines dependency versions that are used in the project. + */ +object Versions { + val compileSdk = 33 + val minSdk = 21 + val junit = "4.13.2" + val kotlin = "1.5.20" + // kotin library exposed to the customer + val kotlinExposed = "1.4.32" + val dokka = "1.7.10" + val detekt = "1.21.0" + val binaryCompatValidator = "0.12.1" + val agp = "7.3.0" + val lint = "30.1.0" + val ndk = "21.4.7075529" + val openTelemetry = "1.29.0" +} diff --git a/config/checkstyle/google_checks.xml b/config/checkstyle/google_checks.xml new file mode 100644 index 0000000000..243aeae3fb --- /dev/null +++ b/config/checkstyle/google_checks.xml @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000000..50cb055bae --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,62 @@ +# https://detekt.dev/docs/introduction/configurations/ +build: + maxIssues: 0 +complexity: + NestedBlockDepth: + threshold: 10 + LargeClass: + excludes: + - '**/test/**' + - '**/androidTest/**' + LongMethod: + active: false + LongParameterList: + constructorThreshold: 30 + functionThreshold: 11 + ignoreDataClasses: true + TooManyFunctions: + thresholdInFiles: 31 + thresholdInClasses: 31 + thresholdInInterfaces: 31 + thresholdInObjects: 31 + thresholdInEnums: 31 + ignoreOverridden: true +empty-blocks: + EmptyFunctionBlock: + ignoreOverridden: true +exceptions: + InstanceOfCheckForException: + active: false + SwallowedException: + active: false + TooGenericExceptionCaught: + active: false + TooGenericExceptionThrown: + excludes: + - '**/test/**' + - '**/androidTest/**' +formatting: + ArgumentListWrapping: + maxLineLength: 140 + MaximumLineLength: + maxLineLength: 140 +potential-bugs: + ElseCaseInsteadOfExhaustiveWhen: + active: true +style: + ForbiddenComment: + active: false + MagicNumber: + active: false + MaxLineLength: + maxLineLength: 140 + ReturnCount: + active: false + MandatoryBracesIfStatements: + active: true + MandatoryBracesLoops: + active: true + SpacingBetweenPackageAndImports: + active: true + UnusedImports: + active: true \ No newline at end of file diff --git a/embrace-android-compose/.gitignore b/embrace-android-compose/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/embrace-android-compose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/embrace-android-compose/api/embrace-android-compose.api b/embrace-android-compose/api/embrace-android-compose.api new file mode 100644 index 0000000000..3870935381 --- /dev/null +++ b/embrace-android-compose/api/embrace-android-compose.api @@ -0,0 +1,29 @@ +public abstract interface class io/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks : android/app/Application$ActivityLifecycleCallbacks { + public abstract fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V + public abstract fun onActivityDestroyed (Landroid/app/Activity;)V + public abstract fun onActivityPaused (Landroid/app/Activity;)V + public abstract fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V + public abstract fun onActivityStarted (Landroid/app/Activity;)V + public abstract fun onActivityStopped (Landroid/app/Activity;)V +} + +public final class io/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks$DefaultImpls { + public static fun onActivityCreated (Lio/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks;Landroid/app/Activity;Landroid/os/Bundle;)V + public static fun onActivityDestroyed (Lio/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks;Landroid/app/Activity;)V + public static fun onActivityPaused (Lio/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks;Landroid/app/Activity;)V + public static fun onActivitySaveInstanceState (Lio/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks;Landroid/app/Activity;Landroid/os/Bundle;)V + public static fun onActivityStarted (Lio/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks;Landroid/app/Activity;)V + public static fun onActivityStopped (Lio/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks;Landroid/app/Activity;)V +} + +public final class io/embrace/android/embracesdk/compose/ComposeActivityListener : io/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks { + public fun ()V + public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityDestroyed (Landroid/app/Activity;)V + public fun onActivityPaused (Landroid/app/Activity;)V + public fun onActivityResumed (Landroid/app/Activity;)V + public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityStarted (Landroid/app/Activity;)V + public fun onActivityStopped (Landroid/app/Activity;)V +} + diff --git a/embrace-android-compose/build.gradle.kts b/embrace-android-compose/build.gradle.kts new file mode 100644 index 0000000000..426a9a0088 --- /dev/null +++ b/embrace-android-compose/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("internal-embrace-plugin") +} + +description = "Embrace Android SDK: Jetpack Compose" + +android { + namespace = "io.embrace.android.embracesdk.compose" +} + +dependencies { + implementation("androidx.lifecycle:lifecycle-common-java8:2.5.0") + implementation("androidx.lifecycle:lifecycle-extensions:2.0.0") + //compose + compileOnly("androidx.compose.ui:ui:1.0.5") + compileOnly(project(":embrace-android-sdk")) +} \ No newline at end of file diff --git a/embrace-android-compose/lint-baseline.xml b/embrace-android-compose/lint-baseline.xml new file mode 100644 index 0000000000..39e0abde3a --- /dev/null +++ b/embrace-android-compose/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/embrace-android-compose/src/main/AndroidManifest.xml b/embrace-android-compose/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5c3d3655b5 --- /dev/null +++ b/embrace-android-compose/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks.kt new file mode 100644 index 0000000000..9a59247699 --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ActivityLifeCycleCallbacks.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.compose + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle + +public interface ActivityLifeCycleCallbacks : ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + // no op + } + + override fun onActivityStarted(activity: Activity) { + // no op + } + + override fun onActivityPaused(activity: Activity) { + // no op + } + + override fun onActivityStopped(activity: Activity) { + // no op + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + // no op + } + + override fun onActivityDestroyed(activity: Activity) { + // no op + } +} diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ComposeActivityListener.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ComposeActivityListener.kt new file mode 100644 index 0000000000..3ddea39d31 --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/ComposeActivityListener.kt @@ -0,0 +1,41 @@ +package io.embrace.android.embracesdk.compose + +import android.app.Activity +import android.view.Window +import androidx.core.view.GestureDetectorCompat +import io.embrace.android.embracesdk.compose.internal.ComposeInternalErrorLogger +import io.embrace.android.embracesdk.compose.internal.EmbraceGestureListener +import io.embrace.android.embracesdk.compose.internal.EmbraceWindowCallback +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ThreadFactory + +public class ComposeActivityListener : ActivityLifeCycleCallbacks { + + private val threadFactory: ThreadFactory = ThreadFactory { runnable: Runnable -> + Executors.defaultThreadFactory().newThread(runnable).apply { + this.name = "emb-compose-scheduled-reg" + } + } + + private val service: ScheduledExecutorService = + Executors.newSingleThreadScheduledExecutor(threadFactory) + + private val composeInternalErrorLogger = ComposeInternalErrorLogger() + + override fun onActivityResumed(activity: Activity) { + try { + // Set EmbraceWindowCallback to install Embrace Gesture Listener to capture onClick events + val window: Window = activity.window + if (window.callback == null || window.callback !is EmbraceWindowCallback) { + val gestureDetectorCompat = GestureDetectorCompat(activity, EmbraceGestureListener(activity, service)) + window.callback = EmbraceWindowCallback( + window.callback, + gestureDetectorCompat + ) + } + } catch (e: Throwable) { + composeInternalErrorLogger.logError(e) + } + } +} diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ClickedView.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ClickedView.kt new file mode 100644 index 0000000000..68813cd714 --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ClickedView.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.compose.internal + +/** + * Represents the clicked element to log + */ +internal data class ClickedView( + val tag: String, + val x: Float, + val y: Float +) diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeClickedTargetIterator.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeClickedTargetIterator.kt new file mode 100644 index 0000000000..ecfbc8db8c --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeClickedTargetIterator.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk.compose.internal + +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import java.util.LinkedList +import java.util.Queue +import java.util.concurrent.ScheduledExecutorService + +internal class ComposeClickedTargetIterator : EmbraceClickedTargetIterator { + + private val composeInternalErrorLogger = ComposeInternalErrorLogger() + private val nodeLocator = EmbraceNodeIterator() + + override fun findTarget(decorView: View, x: Float, y: Float, onSingleTapUpBackgroundWorker: ScheduledExecutorService) { + try { + val queue: Queue = LinkedList() + queue.add(decorView) + while (queue.size > 0) { + val view = queue.poll() + view?.let { + if (it is ViewGroup) { + // TODO: define a limit of how many views we want to store and process to avoid processing ridiculously large view + for (i in 0 until it.childCount) { + queue.add(it.getChildAt(i)) + } + } + + if (it.parent is ComposeView) { // this validation is to reduce the locate method execution to the proper view + nodeLocator.findClickedElement(it, x, y, onSingleTapUpBackgroundWorker) + } + } + } + } catch (e: Throwable) { + composeInternalErrorLogger.logError(e) + } + } +} diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeInternalErrorLogger.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeInternalErrorLogger.kt new file mode 100644 index 0000000000..98e718d5b4 --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/ComposeInternalErrorLogger.kt @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.compose.internal + +import io.embrace.android.embracesdk.Embrace + +internal class ComposeInternalErrorLogger { + + fun logError(throwable: Throwable) { + Embrace.getInstance().logInternalError( + throwable + ) + } +} diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceClickedTargetIterator.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceClickedTargetIterator.kt new file mode 100644 index 0000000000..050ca67c6c --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceClickedTargetIterator.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk.compose.internal + +import android.view.View +import java.util.concurrent.ScheduledExecutorService + +/** + * Given a view and a position ( x, y), + * this interface is intended to represent + * functionality that looks for a clicked view in that position. + */ +internal interface EmbraceClickedTargetIterator { + fun findTarget( + decorView: View, + x: Float, + y: Float, + onSingleTapUpBackgroundWorker: ScheduledExecutorService, + ) +} diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceGestureListener.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceGestureListener.kt new file mode 100644 index 0000000000..3940207fea --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceGestureListener.kt @@ -0,0 +1,47 @@ +package io.embrace.android.embracesdk.compose.internal + +import android.app.Activity +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import java.lang.ref.WeakReference +import java.util.concurrent.ScheduledExecutorService + +/** + * EmbraceGestureListener extends SimpleOnGestureListener to listen + * just for onSingleTapUp when the onClick event is triggered + */ +internal class EmbraceGestureListener( + activity: Activity, + private val onSingleTapUpBackgroundWorker: ScheduledExecutorService +) : GestureDetector.SimpleOnGestureListener() { + + private var singleTapUpError: ComposeInternalErrorLogger = ComposeInternalErrorLogger() + private var activityRef: WeakReference + + private val composeClickedTargetIterator = ComposeClickedTargetIterator() + + init { + activityRef = WeakReference(activity) + } + + override fun onSingleTapUp(event: MotionEvent): Boolean { + try { + if (event.actionMasked == MotionEvent.ACTION_UP) { + activityRef.get()?.let { activity -> + activity.window?.let { + logTapUp(it.decorView, event) + } + } + } + } catch (e: Throwable) { + singleTapUpError.logError(e) + } + + return false + } + + private fun logTapUp(decorView: View, event: MotionEvent) { + composeClickedTargetIterator.findTarget(decorView, event.x, event.y, onSingleTapUpBackgroundWorker) + } +} diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt new file mode 100644 index 0000000000..3a8d13497a --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt @@ -0,0 +1,82 @@ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package io.embrace.android.embracesdk.compose.internal + +import android.util.Pair +import android.view.View +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.AndroidComposeView +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.SemanticsConfiguration +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getAllSemanticsNodes +import androidx.compose.ui.semantics.getOrNull +import io.embrace.android.embracesdk.Embrace +import java.util.concurrent.ScheduledExecutorService + +private const val UNKNOWN_ELEMENT_NAME = "Unlabeled Compose element" + +internal class EmbraceNodeIterator { + + /** + * If the received view is AndroidComposeView, + * we collect the compose tree and iterate over it to find the clicked view, + * by comparing with the received position (x,y) + * */ + fun findClickedElement(root: View, x: Float, y: Float, backgroundWorker: ScheduledExecutorService) { + val semanticsOwner = if (root is AndroidComposeView) root.semanticsOwner else return + val semanticsNodes = semanticsOwner.getAllSemanticsNodes(true) + + backgroundWorker.submit { + findClickedElement(semanticsNodes, x, y)?.let { + val clickedView = ClickedView(it, x, y) + Embrace.getInstance().logComposeTap(Pair(clickedView.x, clickedView.y), clickedView.tag) + } + } + } + + /** + * Iterates over the compose tree to find the clicked element and retrieve its tag + * */ + private fun findClickedElement(semanticsNodes: List, x: Float, y: Float): String? { + for (node in semanticsNodes) { + if (isNodeInPosition(node, x, y)) { + val clickableElementName = getClickableElementName(node.config) + if (clickableElementName != null) { + return clickableElementName + } + } + } + return null + } + + private fun getClickableElementName(semanticsConfiguration: SemanticsConfiguration): String? { + val onClickSemanticsConfiguration = semanticsConfiguration.getOrNull(SemanticsActions.OnClick) + if (onClickSemanticsConfiguration != null) { + // The node is clickable. Return accessibilityActionLabel if present. + val accessibilityActionLabel = onClickSemanticsConfiguration.label + if (accessibilityActionLabel != null) { + return accessibilityActionLabel + } + + // If the OnClick configuration doesn't have an accessibilityActionLabel, check for the content description instead. + val contentDescriptionSemanticsConfiguration = semanticsConfiguration.getOrNull(SemanticsProperties.ContentDescription) + if (contentDescriptionSemanticsConfiguration != null) { + val contentDescription = contentDescriptionSemanticsConfiguration.getOrNull(0) + if (contentDescription != null) { + return contentDescription + } + } + + // The view is clickable, so return it with a default name. + return UNKNOWN_ELEMENT_NAME + } + return null + } + + /** + * Validates if a node position is same as x, y + */ + private fun isNodeInPosition(node: SemanticsNode, x: Float, y: Float) = node.boundsInWindow.contains(Offset(x, y)) +} diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceWindowCallback.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceWindowCallback.kt new file mode 100644 index 0000000000..770e41257a --- /dev/null +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceWindowCallback.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk.compose.internal + +import android.view.MotionEvent +import android.view.Window +import androidx.core.view.GestureDetectorCompat + +/** + * Custom Window callback that triggers onTouch event + * when dispatchTouchEvent happens + */ +internal class EmbraceWindowCallback( + private val delegate: Window.Callback, + private val gestureDetector: GestureDetectorCompat +) : + Window.Callback by delegate { + + private val composeInternalErrorLogger = ComposeInternalErrorLogger() + + override fun dispatchTouchEvent(event: MotionEvent?): Boolean { + event?.let { + try { + val copy = MotionEvent.obtain(it) + gestureDetector.onTouchEvent(copy) + copy.recycle() + } catch (e: Throwable) { + composeInternalErrorLogger.logError(e) + } + } + + return delegate.dispatchTouchEvent(event) + } +} diff --git a/embrace-android-fcm/.gitignore b/embrace-android-fcm/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/embrace-android-fcm/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/embrace-android-fcm/api/embrace-android-fcm.api b/embrace-android-fcm/api/embrace-android-fcm.api new file mode 100644 index 0000000000..0e114a45d7 --- /dev/null +++ b/embrace-android-fcm/api/embrace-android-fcm.api @@ -0,0 +1,4 @@ +public final class io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks { + public static fun _onMessageReceived (Lcom/google/firebase/messaging/RemoteMessage;)V +} + diff --git a/embrace-android-fcm/build.gradle b/embrace-android-fcm/build.gradle new file mode 100644 index 0000000000..5f212f300a --- /dev/null +++ b/embrace-android-fcm/build.gradle @@ -0,0 +1,14 @@ +plugins { + id("internal-embrace-plugin") +} + +description = "Embrace Android SDK: Firebase Cloud Messaging" + +android { + namespace = "io.embrace.android.embracesdk.fcm" +} + +dependencies { + compileOnly("com.google.firebase:firebase-messaging:23.1.0") + compileOnly(project(":embrace-android-sdk")) +} diff --git a/embrace-android-fcm/lint-baseline.xml b/embrace-android-fcm/lint-baseline.xml new file mode 100644 index 0000000000..39e0abde3a --- /dev/null +++ b/embrace-android-fcm/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/embrace-android-fcm/src/main/AndroidManifest.xml b/embrace-android-fcm/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5c3d3655b5 --- /dev/null +++ b/embrace-android-fcm/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java b/embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java new file mode 100644 index 0000000000..6f131068f4 --- /dev/null +++ b/embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java @@ -0,0 +1,110 @@ +package io.embrace.android.embracesdk.fcm.swazzle.callback.com.android.fcm; + +import static io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.logDebug; +import static io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.logError; + +import androidx.annotation.NonNull; + +import com.google.firebase.messaging.RemoteMessage; + +import io.embrace.android.embracesdk.Embrace; +import io.embrace.android.embracesdk.InternalApi; + +@InternalApi +public final class FirebaseSwazzledHooks { + + private FirebaseSwazzledHooks() { + } + + @SuppressWarnings("MethodNameCheck") + @InternalApi + public static void _onMessageReceived(@NonNull RemoteMessage message) { + logDebug("Embrace received push notification message"); + + if (!Embrace.getInstance().isStarted()) { + logError("Embrace received push notification data before the SDK was started"); + return; + } + + handleRemoteMessage(message); + } + + private static void handleRemoteMessage(@NonNull RemoteMessage message) { + try { + //flag process is already running to avoid track warm startup + Embrace.getInstance().setProcessStartedByNotification(); + + String messageId = null; + try { + messageId = message.getMessageId(); + } catch (Exception e) { + logError("Failed to capture FCM messageId", e); + } + + String topic = null; + try { + topic = message.getFrom(); + } catch (Exception e) { + logError("Failed to capture FCM topic", e); + } + + Integer messagePriority = null; + try { + messagePriority = message.getPriority(); + } catch (Exception e) { + logError("Failed to capture FCM message priority", e); + } + + RemoteMessage.Notification notification = null; + + try { + notification = message.getNotification(); + } catch (Exception e) { + logError("Failed to capture FCM RemoteMessage Notification", e); + } + + String title = null; + String body = null; + Integer notificationPriority = null; + if (notification != null) { + try { + title = notification.getTitle(); + } catch (Exception e) { + logError("Failed to capture FCM title", e); + } + + try { + body = notification.getBody(); + } catch (Exception e) { + logError("Failed to capture FCM body", e); + } + + try { + notificationPriority = notification.getNotificationPriority(); + } catch (Exception e) { + logError("Failed to capture FCM notificationPriority", e); + } + } + + Boolean hasData = !message.getData().isEmpty(); + Boolean hasNotification = notification != null; + + try { + Embrace.getInstance().logPushNotification( + title, + body, + topic, + messageId, + notificationPriority, + messagePriority, + hasNotification, + hasData + ); + } catch (Exception e) { + logError("Failed to log push Notification", e); + } + } catch (Exception e) { + logError("Push Notification Error", e); + } + } +} diff --git a/embrace-android-okhttp3/CREDITS.md b/embrace-android-okhttp3/CREDITS.md new file mode 100644 index 0000000000..c86173d1ef --- /dev/null +++ b/embrace-android-okhttp3/CREDITS.md @@ -0,0 +1,13 @@ +In an effort to provide fair attribution, the following list contains the names and licensing notices for all sources that have provided significant direct or indirect contributions to this project. + +-------------------------------------------------------------------------------- + +The MIT License (MIT) +Copyright (c) 2016 Raygun Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/embrace-android-okhttp3/README.md b/embrace-android-okhttp3/README.md new file mode 100644 index 0000000000..6f525f036b --- /dev/null +++ b/embrace-android-okhttp3/README.md @@ -0,0 +1,5 @@ +# Embrace.io - OkHttp3 Networking Android SDK + +This optional add-on SDK exposes some additional networking related APIs that extends the functionality of the core Embrace.io Android SDK. + +If your application uses the OkHttp3 library to make HTTP network calls, then this SDK can provide some \ No newline at end of file diff --git a/embrace-android-okhttp3/api/embrace-android-okhttp3.api b/embrace-android-okhttp3/api/embrace-android-okhttp3.api new file mode 100644 index 0000000000..549cffb31d --- /dev/null +++ b/embrace-android-okhttp3/api/embrace-android-okhttp3.api @@ -0,0 +1,23 @@ +public class io/embrace/android/embracesdk/okhttp3/EmbraceCustomPathException : java/io/IOException { + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V + public fun getCustomPath ()Ljava/lang/String; +} + +public class io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor : okhttp3/Interceptor { + public fun ()V + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; +} + +public final class io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor : okhttp3/Interceptor { + public fun ()V + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; +} + +public final class io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient { +} + +public final class io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient$Builder { + public static fun _constructorOnPostBody (Lokhttp3/OkHttpClient$Builder;)V + public static fun _preBuild (Lokhttp3/OkHttpClient$Builder;)V +} + diff --git a/embrace-android-okhttp3/build.gradle b/embrace-android-okhttp3/build.gradle new file mode 100644 index 0000000000..a806e5c640 --- /dev/null +++ b/embrace-android-okhttp3/build.gradle @@ -0,0 +1,17 @@ +plugins { + id("internal-embrace-plugin") +} + +description = "Embrace Android SDK: OkHttp3" + +android { + namespace = "io.embrace.android.embracesdk.okhttp3" +} + +dependencies { + compileOnly("com.squareup.okhttp3:okhttp:4.9.3") + compileOnly(project(":embrace-android-sdk")) + testImplementation(project(":embrace-android-sdk")) + testImplementation "io.mockk:mockk:1.12.2" + testImplementation "com.squareup.okhttp3:mockwebserver:4.9.3" +} diff --git a/embrace-android-okhttp3/lint-baseline.xml b/embrace-android-okhttp3/lint-baseline.xml new file mode 100644 index 0000000000..794b7879ee --- /dev/null +++ b/embrace-android-okhttp3/lint-baseline.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/embrace-android-okhttp3/src/main/AndroidManifest.xml b/embrace-android-okhttp3/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9733c38819 --- /dev/null +++ b/embrace-android-okhttp3/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceCustomPathException.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceCustomPathException.java new file mode 100644 index 0000000000..93ebdf2902 --- /dev/null +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceCustomPathException.java @@ -0,0 +1,24 @@ +package io.embrace.android.embracesdk.okhttp3; + +import java.io.IOException; + +import io.embrace.android.embracesdk.InternalApi; + +/** + * We use the EmbraceCustomPathException to capture the custom path added in the + * intercept chain process for client errors. + */ +@InternalApi +public class EmbraceCustomPathException extends IOException { + + private final String customPath; + + public EmbraceCustomPathException(String customPath, Throwable cause) { + super(cause); + this.customPath = customPath; + } + + public String getCustomPath() { + return customPath; + } +} diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java new file mode 100644 index 0000000000..37f578e0cc --- /dev/null +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java @@ -0,0 +1,102 @@ +package io.embrace.android.embracesdk.okhttp3; + +import static io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.TRACEPARENT_HEADER_NAME; +import static io.embrace.android.embracesdk.internal.utils.ThrowableUtilsKt.causeMessage; +import static io.embrace.android.embracesdk.internal.utils.ThrowableUtilsKt.causeName; + +import java.io.IOException; + +import io.embrace.android.embracesdk.Embrace; +import io.embrace.android.embracesdk.InternalApi; +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; +import io.embrace.android.embracesdk.network.http.EmbraceHttpPathOverride; +import io.embrace.android.embracesdk.network.http.HttpMethod; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * This interceptor will only intercept errors that client app experiences. + *

+ * We used OkHttp3 application interceptor in this case because this interceptor + * will be added first in the OkHttp3 interceptors stack. This allows us to catch network errors. + * OkHttp3 network interceptors are added almost at the end of stack, they are closer to "Wire" + * so they are not able to see network errors. + *

+ * Application interceptors: - Don't need to worry about intermediate responses like + * redirects and retries. - Are always invoked once, even if the HTTP response is served + * from the cache. - Observe the application's original intent. Unconcerned with OkHttp-injected + * headers like If-None-Match. - Permitted to short-circuit and not call + * Chain.proceed(). - Permitted to retry and make multiple calls to Chain.proceed(). + *

+ * We used the EmbraceGraphQLException to capture the custom path added in the intercept + * chain process for client errors on graphql requests. + */ +@InternalApi +public class EmbraceOkHttp3ApplicationInterceptor implements Interceptor { + static final String UNKNOWN_EXCEPTION = "Unknown"; + static final String UNKNOWN_MESSAGE = "An error occurred during the execution of this network request"; + final Embrace embrace; + + private final SdkFacade sdkFacade; + + public EmbraceOkHttp3ApplicationInterceptor() { + this(Embrace.getInstance(), new SdkFacade()); + } + + EmbraceOkHttp3ApplicationInterceptor(Embrace embrace, SdkFacade sdkFacade) { + this.embrace = embrace; + this.sdkFacade = sdkFacade; + } + + @Override + public Response intercept(Chain chain) throws IOException { + long startTime = System.currentTimeMillis(); + Request request = chain.request(); + try { + // we are not interested in response, just proceed + return chain.proceed(request); + } catch (EmbraceCustomPathException e) { + if (embrace.isStarted()) { + String urlString = EmbraceHttpPathOverride.getURLString(new EmbraceOkHttp3PathOverrideRequest(request), e.getCustomPath()); + + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromIncompleteRequest( + urlString, + HttpMethod.fromString(request.method()), + startTime, + System.currentTimeMillis(), + causeName(e, UNKNOWN_EXCEPTION), + causeMessage(e, UNKNOWN_MESSAGE), + request.header(embrace.getTraceIdHeader()), + sdkFacade.isNetworkSpanForwardingEnabled() ? request.header(TRACEPARENT_HEADER_NAME) : null, + null + ) + ); + } + throw e; + } catch (Exception e) { + // we are interested in errors. + if (embrace.isStarted()) { + String urlString = EmbraceHttpPathOverride.getURLString(new EmbraceOkHttp3PathOverrideRequest(request)); + String errorType = e.getClass().getCanonicalName(); + String errorMessage = e.getMessage(); + + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromIncompleteRequest( + urlString, + HttpMethod.fromString(request.method()), + startTime, + System.currentTimeMillis(), + errorType != null ? errorType : UNKNOWN_EXCEPTION, + errorMessage != null ? errorMessage : UNKNOWN_MESSAGE, + request.header(embrace.getTraceIdHeader()), + sdkFacade.isNetworkSpanForwardingEnabled() ? request.header(TRACEPARENT_HEADER_NAME) : null, + null + ) + ); + } + throw e; + } + } +} diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java new file mode 100644 index 0000000000..c667b28419 --- /dev/null +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java @@ -0,0 +1,253 @@ +package io.embrace.android.embracesdk.okhttp3; + +import static io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.TRACEPARENT_HEADER_NAME; +import static io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.logDebug; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.embrace.android.embracesdk.Embrace; +import io.embrace.android.embracesdk.InternalApi; +import io.embrace.android.embracesdk.internal.ApkToolsConfig; +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; +import io.embrace.android.embracesdk.network.http.EmbraceHttpPathOverride; +import io.embrace.android.embracesdk.network.http.HttpMethod; +import io.embrace.android.embracesdk.network.http.NetworkCaptureData; +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.internal.http.HttpHeaders; +import okhttp3.internal.http.RealResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.GzipSource; +import okio.RealBufferedSource; + +/** + * Custom OkHttp3 Interceptor implementation that will log the results of the network call + * to Embrace.io. + *

+ * This interceptor will only intercept network request and responses from client app. + * OkHttp3 network interceptors are added almost at the end of stack, they are closer to "Wire" + * so they are able to see catch "real requests". + *

+ * Network Interceptors + * - Able to operate on intermediate responses like redirects and retries. + * - Not invoked for cached responses that short-circuit the network. + * - Observe the data just as it will be transmitted over the network. + * - Access to the Connection that carries the request. + */ +@InternalApi +public final class EmbraceOkHttp3NetworkInterceptor implements Interceptor { + static final String ENCODING_GZIP = "gzip"; + static final String CONTENT_LENGTH_HEADER_NAME = "Content-Length"; + static final String CONTENT_ENCODING_HEADER_NAME = "Content-Encoding"; + static final String CONTENT_TYPE_HEADER_NAME = "Content-Type"; + static final String CONTENT_TYPE_EVENT_STREAM = "text/event-stream"; + private static final String[] networkCallDataParts = new String[]{ + "Response Headers", + "Request Headers", + "Query Parameters", + "Request Body", + "Response Body" + }; + + final Embrace embrace; + private final SdkFacade sdkFacade; + + public EmbraceOkHttp3NetworkInterceptor() { + this(Embrace.getInstance(), new SdkFacade()); + } + + EmbraceOkHttp3NetworkInterceptor(Embrace embrace, SdkFacade sdkFacade) { + this.embrace = embrace; + this.sdkFacade = sdkFacade; + } + + @Override + public Response intercept(Chain chain) throws IOException { + final Request originalRequest = chain.request(); + + if (ApkToolsConfig.IS_NETWORK_CAPTURE_DISABLED || !embrace.isStarted()) { + return chain.proceed(originalRequest); + } + + boolean networkSpanForwardingEnabled = sdkFacade.isNetworkSpanForwardingEnabled(); + + String traceparent = null; + if (networkSpanForwardingEnabled && originalRequest.header(TRACEPARENT_HEADER_NAME) == null) { + traceparent = embrace.generateW3cTraceparent(); + } + + final Request request = traceparent == null ? + originalRequest : originalRequest.newBuilder().header(TRACEPARENT_HEADER_NAME, traceparent).build(); + + Response networkResponse = chain.proceed(request); + Response.Builder responseBuilder = networkResponse.newBuilder().request(request); + + Long contentLength = null; + // Try to get the content length from the header + if (networkResponse.header(CONTENT_LENGTH_HEADER_NAME) != null) { + try { + contentLength = Long.parseLong(networkResponse.header(CONTENT_LENGTH_HEADER_NAME)); + } catch (Exception ex) { + // Ignore + } + } + + // If we get the body for a server-sent events stream, then we will wait forever + String contentType = networkResponse.header(CONTENT_TYPE_HEADER_NAME); + + // Tolerant of a charset specified in header, + // e.g. Content-Type: text/event-stream;charset=UTF-8 + boolean serverSentEvent = contentType != null && + contentType.startsWith(CONTENT_TYPE_EVENT_STREAM); + + if (!serverSentEvent && contentLength == null) { + try { + BufferedSource source = networkResponse.body().source(); + source.request(Long.MAX_VALUE); + contentLength = source.buffer().size(); + } catch (Exception ex) { + // Ignore + } + } + + if (contentLength == null) { + // Otherwise default to zero + contentLength = 0L; + } + + boolean shouldCaptureNetworkData = embrace.shouldCaptureNetworkBody(request.url().toString(), request.method()); + + if (shouldCaptureNetworkData && + ENCODING_GZIP.equalsIgnoreCase(networkResponse.header(CONTENT_ENCODING_HEADER_NAME)) && + HttpHeaders.promisesBody(networkResponse)) { + ResponseBody body = networkResponse.body(); + if (body != null) { + Headers strippedHeaders = networkResponse.headers().newBuilder() + .removeAll(CONTENT_ENCODING_HEADER_NAME) + .removeAll(CONTENT_LENGTH_HEADER_NAME) + .build(); + RealResponseBody realResponseBody = + new RealResponseBody( + contentType, + -1L, + new RealBufferedSource(new GzipSource(body.source()) + ) + ); + responseBuilder.headers(strippedHeaders); + responseBuilder.body(realResponseBody); + } + } + + Response response = responseBuilder.build(); + + NetworkCaptureData networkCaptureData = null; + if (shouldCaptureNetworkData) { + networkCaptureData = getNetworkCaptureData(request, response); + } + + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromCompletedRequest( + EmbraceHttpPathOverride.getURLString(new EmbraceOkHttp3PathOverrideRequest(request)), + HttpMethod.fromString(request.method()), + response.sentRequestAtMillis(), + response.receivedResponseAtMillis(), + request.body() != null ? request.body().contentLength() : 0, + contentLength, + response.code(), + request.header(embrace.getTraceIdHeader()), + networkSpanForwardingEnabled ? request.header(TRACEPARENT_HEADER_NAME) : null, + networkCaptureData) + ); + + return response; + } + + private NetworkCaptureData getNetworkCaptureData(Request request, Response response) { + Map requestHeaders = null; + String requestQueryParams = null; + Map responseHeaders = null; + byte[] requestBodyBytes = null; + byte[] responseBodyBytes = null; + String dataCaptureErrorMessage = null; + int partsAcquired = 0; + + try { + responseHeaders = getProcessedHeaders(response.headers().toMultimap()); + partsAcquired++; + requestHeaders = getProcessedHeaders(request.headers().toMultimap()); + partsAcquired++; + requestQueryParams = request.url().query(); + partsAcquired++; + requestBodyBytes = getRequestBody(request); + partsAcquired++; + if (HttpHeaders.promisesBody(response)) { + final ResponseBody responseBody = response.body(); + if (responseBody != null) { + BufferedSource okResponseBodySource = responseBody.source(); + okResponseBodySource.request(Integer.MAX_VALUE); + responseBodyBytes = okResponseBodySource.getBuffer().snapshot().toByteArray(); + } + } + } catch (Exception e) { + final StringBuilder errors = new StringBuilder(); + for (int i = partsAcquired; i < 5; i++) { + errors.append("'").append(networkCallDataParts[i]).append("'"); + if (i != 4) { + errors.append(", "); + } + } + + dataCaptureErrorMessage = "There were errors in capturing the following part(s) of the network call: %s" + errors; + logDebug("Failure during the building of NetworkCaptureData. " + dataCaptureErrorMessage, e); + } + + return new NetworkCaptureData( + requestHeaders, + requestQueryParams, + requestBodyBytes, + responseHeaders, + responseBodyBytes, + dataCaptureErrorMessage + ); + } + + private HashMap getProcessedHeaders(Map> properties) { + HashMap headers = new HashMap<>(); + + for (Map.Entry> h : + properties.entrySet()) { + StringBuilder builder = new StringBuilder(); + for (String value : h.getValue()) { + if (value != null) { + builder.append(value); + } + } + headers.put(h.getKey(), builder.toString()); + } + + return headers; + } + + private byte[] getRequestBody(final Request request) { + try { + final Request requestCopy = request.newBuilder().build(); + RequestBody requestBody = requestCopy.body(); + if (requestBody != null) { + final Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); + return buffer.readByteArray(); + } + } catch (final IOException e) { + logDebug("Failed to capture okhttp request body.", e); + } + return null; + } +} diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3PathOverrideRequest.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3PathOverrideRequest.java new file mode 100644 index 0000000000..4c2b9003fd --- /dev/null +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3PathOverrideRequest.java @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.okhttp3; + +import io.embrace.android.embracesdk.HttpPathOverrideRequest; +import okhttp3.Request; + +class EmbraceOkHttp3PathOverrideRequest implements HttpPathOverrideRequest { + + private final Request request; + + EmbraceOkHttp3PathOverrideRequest(Request request) { + this.request = request; + } + + @Override + public String getHeaderByName(String name) { + return request.header(name); + } + + @Override + public String getOverriddenURL(String pathOverride) { + return request.url().newBuilder().encodedPath(pathOverride).build().toString(); + } + + @Override + public String getURLString() { + return request.url().toString(); + } +} diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/SdkFacade.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/SdkFacade.java new file mode 100644 index 0000000000..d55e00268d --- /dev/null +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/SdkFacade.java @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.okhttp3; + +import io.embrace.android.embracesdk.Embrace; +import io.embrace.android.embracesdk.utils.NetworkUtils; + +/** + * Facade to call internal SDK methods that can be mocked for tests + */ +class SdkFacade { + boolean isNetworkSpanForwardingEnabled() { + return NetworkUtils.isNetworkSpanForwardingEnabled(Embrace.getInstance().getConfigService()); + } +} diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient.java new file mode 100644 index 0000000000..80667d8774 --- /dev/null +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient.java @@ -0,0 +1,106 @@ +package io.embrace.android.embracesdk.okhttp3.swazzle.callback.okhttp3; + +import java.util.List; + +import io.embrace.android.embracesdk.InternalApi; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3ApplicationInterceptor; +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3NetworkInterceptor; +import okhttp3.Interceptor; + +/** + * Callback hooks for the okhttp3.OkHttpClient class. + */ +@InternalApi +public final class OkHttpClient { + + private OkHttpClient() { + } + + @InternalApi + public static final class Builder { + + private Builder() { + } + + /** + * As there was a way to clear the injected interceptors during the OkHttpClient + * initialization using the builder, we are hooking the build method as well, instead of + * just the Builder constructor. + *

+ * Once the build method is called, OkHTTP mushes everything and returns the OkHttpClient + * instance, where the developer has no way to alter any of the interceptors during or + * after this point, without having to rebuild the client. + */ + @SuppressWarnings("MethodNameCheck") + public static void _preBuild(okhttp3.OkHttpClient.Builder thiz) { + InternalStaticEmbraceLogger.logDebug("Embrace OkHTTP Wrapper; onPrebuild"); + addEmbraceInterceptors(thiz); + } + + @SuppressWarnings("MethodNameCheck") + public static void _constructorOnPostBody(okhttp3.OkHttpClient.Builder thiz) { + InternalStaticEmbraceLogger.logDebug("Embrace OkHTTP Wrapper; onPostBody"); + addEmbraceInterceptors(thiz); + } + + /** + * Adds embrace interceptors if they don't exist already to the OkHTTPClient provided. + * + * @param thiz the OkHttpClient builder in matter. + */ + private static void addEmbraceInterceptors(okhttp3.OkHttpClient.Builder thiz) { + try { + InternalStaticEmbraceLogger.logDebug("Embrace OkHTTP Wrapper;" + + " Adding interceptors"); + addInterceptor(thiz.interceptors(), new EmbraceOkHttp3ApplicationInterceptor()); + addInterceptor(thiz.networkInterceptors(), new EmbraceOkHttp3NetworkInterceptor()); + } catch (NoSuchMethodError exception) { + // The customer may be overwriting OkHttpClient with their own implementation, and some of the + // methods we use are missing. + InternalStaticEmbraceLogger.logError("Altered OkHttpClient implementation, could not add OkHttp interceptor. ", + exception); + } catch (Exception exception) { + InternalStaticEmbraceLogger.logError("Could not add OkHttp interceptor. ", exception); + } + } + + /** + * Adds the interceptor to the interceptors list if it doesn't exist already. + * + * @param interceptors list of existing interceptors. + * @param interceptor interceptor to be added. + */ + private static void addInterceptor(List interceptors, + Interceptor interceptor) { + if (interceptors != null && !containsInstance(interceptors, interceptor.getClass())) { + interceptors.add(0, interceptor); + } else { + InternalStaticEmbraceLogger.logDebug( + "Not adding interceptor [" + interceptor.getClass().getSimpleName() + "]" + ); + } + } + + /** + * Checks for the existence in the elements list of an instance of the same class as the + * one provided in the arguments. + * + * @param elementsList list of elements. + * @param clazz class of the instance that's being checked if exists. + * @return if an instance of the provided class exists in the list of elements. + */ + private static boolean containsInstance(List elementsList, + Class clazz) { + for (T classInstance : elementsList) { + if (clazz.isInstance(classInstance)) { + InternalStaticEmbraceLogger.logDebug( + "[" + clazz.getSimpleName() + "] already present in list" + ); + return true; + } + } + return false; + } + } +} diff --git a/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt b/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt new file mode 100644 index 0000000000..087efd9f5e --- /dev/null +++ b/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt @@ -0,0 +1,535 @@ +package io.embrace.android.embracesdk.okhttp3 + +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3ApplicationInterceptor.UNKNOWN_EXCEPTION +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3ApplicationInterceptor.UNKNOWN_MESSAGE +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3NetworkInterceptor.CONTENT_ENCODING_HEADER_NAME +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3NetworkInterceptor.CONTENT_LENGTH_HEADER_NAME +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3NetworkInterceptor.CONTENT_TYPE_EVENT_STREAM +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3NetworkInterceptor.CONTENT_TYPE_HEADER_NAME +import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3NetworkInterceptor.ENCODING_GZIP +import io.mockk.CapturingSlot +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.Buffer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.net.SocketException +import java.util.zip.GZIPOutputStream + +internal class EmbraceOkHttp3InterceptorsTest { + + companion object { + private const val requestHeaderName = "requestHeader" + private const val requestHeaderValue = "requestHeaderVal" + private const val defaultQueryString = "param=yesPlease" + private const val defaultPath = "/test/default-path" + private const val customPath = "/test/custom-path" + private const val requestBodyString = "hey body" + private const val requestBodySize = 8 + private const val responseHeaderName = "responseHeader" + private const val responseBody = "{\"bodyString\" = \"stringstringstringstringstringstringstringstringstringstringstringstring\"}" + private const val responseBodySize = 91 + private const val responseBodyGzippedSize = 43 + private const val responseHeaderValue = "responseHeaderVal" + private const val TRACEPARENT_HEADER = "traceparent" + private const val CUSTOM_TRACEPARENT = "00-b583a45b2c7c813e0ebc6aa0835b9d98-b5475c618bb98e67-01" + private const val GENERATED_TRACEPARENT = "00-3c72a77a7b51af6fb3778c06d4c165ce-4c1d710fffc88e35-01" + } + + private lateinit var server: MockWebServer + private lateinit var applicationInterceptor: EmbraceOkHttp3ApplicationInterceptor + private lateinit var networkInterceptor: EmbraceOkHttp3NetworkInterceptor + private lateinit var preNetworkInterceptorTestInterceptor: Interceptor + private lateinit var postNetworkInterceptorTestInterceptor: Interceptor + private lateinit var okHttpClient: OkHttpClient + private lateinit var mockEmbrace: Embrace + private lateinit var mockSdkFacade: SdkFacade + private lateinit var getRequestBuilder: Request.Builder + private lateinit var postRequestBuilder: Request.Builder + private lateinit var capturedEmbraceNetworkRequest: CapturingSlot + private var preNetworkInterceptorBeforeRequestSupplier: (Request) -> Request = { request -> request } + private var preNetworkInterceptorAfterResponseSupplier: (Response) -> Response = { response -> response } + private var postNetworkInterceptorBeforeRequestSupplier: (Request) -> Request = { request -> request } + private var postNetworkInterceptorAfterResponseSupplier: (Response) -> Response = { response -> response } + private var isSDKStarted = true + private var isNetworkSpanForwardingEnabled = false + + @Before + fun setup() { + server = MockWebServer() + mockEmbrace = mockk(relaxed = true) + mockSdkFacade = mockk(relaxed = true) + applicationInterceptor = EmbraceOkHttp3ApplicationInterceptor(mockEmbrace, mockSdkFacade) + preNetworkInterceptorTestInterceptor = TestInspectionInterceptor( + beforeRequestSent = { request -> preNetworkInterceptorBeforeRequestSupplier.invoke(request) }, + afterResponseReceived = { response -> preNetworkInterceptorAfterResponseSupplier.invoke(response) } + ) + networkInterceptor = EmbraceOkHttp3NetworkInterceptor(mockEmbrace, mockSdkFacade) + postNetworkInterceptorTestInterceptor = TestInspectionInterceptor( + beforeRequestSent = { request -> postNetworkInterceptorBeforeRequestSupplier.invoke(request) }, + afterResponseReceived = { response -> postNetworkInterceptorAfterResponseSupplier.invoke(response) } + ) + okHttpClient = OkHttpClient.Builder() + .addInterceptor(applicationInterceptor) + .addNetworkInterceptor(preNetworkInterceptorTestInterceptor) + .addNetworkInterceptor(networkInterceptor) + .addNetworkInterceptor(postNetworkInterceptorTestInterceptor) + .build() + getRequestBuilder = Request.Builder() + .url(server.url("$defaultPath?$defaultQueryString")) + .get() + .header(requestHeaderName, requestHeaderValue) + postRequestBuilder = Request.Builder() + .url(server.url("$defaultPath?$defaultQueryString")) + .post(requestBodyString.toRequestBody()) + .header(requestHeaderName, requestHeaderValue) + capturedEmbraceNetworkRequest = slot() + every { mockEmbrace.isStarted } answers { isSDKStarted } + every { mockEmbrace.shouldCaptureNetworkBody(any(), "POST") } answers { true } + every { mockEmbrace.shouldCaptureNetworkBody(any(), "GET") } answers { false } + every { mockEmbrace.recordNetworkRequest(capture(capturedEmbraceNetworkRequest)) } answers { } + every { mockEmbrace.generateW3cTraceparent() } answers { GENERATED_TRACEPARENT } + every { mockSdkFacade.isNetworkSpanForwardingEnabled } answers { isNetworkSpanForwardingEnabled } + } + + @After + fun teardown() { + server.shutdown() + } + + @Test + fun `completed successful requests with uncompressed responses are recorded properly`() { + preNetworkInterceptorAfterResponseSupplier = ::consumeBody + server.enqueue(createBaseMockResponse().setBody(responseBody)) + runAndValidatePostRequest(responseBodySize) + + server.enqueue(createBaseMockResponse().setBody(responseBody)) + runAndValidateGetRequest(responseBodySize) + } + + @Test + fun `completed successful requests with gzipped responses are recorded properly`() { + preNetworkInterceptorAfterResponseSupplier = ::consumeBody + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + runAndValidatePostRequest(responseBodyGzippedSize) + + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + runAndValidateGetRequest(responseBodyGzippedSize) + } + + @Test + fun `completed unsuccessful requests are recorded properly`() { + server.enqueue(createBaseMockResponse(500).setGzipBody(responseBody)) + runAndValidatePostRequest(expectedResponseBodySize = responseBodyGzippedSize, expectedHttpStatus = 500) + } + + @Test + fun `completed requests with custom paths are recorded properly`() { + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + postRequestBuilder.header("x-emb-path", customPath) + runAndValidatePostRequest(expectedResponseBodySize = responseBodyGzippedSize, expectedPath = customPath) + } + + @Test + fun `incomplete requests with custom paths are recorded properly`() { + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + postRequestBuilder.header("x-emb-path", customPath) + runAndValidatePostRequest(expectedResponseBodySize = responseBodyGzippedSize, expectedPath = customPath) + } + + @Test + fun `completed requests are not recorded if the SDK has not started`() { + isSDKStarted = false + server.enqueue(createBaseMockResponse()) + runGetRequest() + server.enqueue(createBaseMockResponse()) + runPostRequest() + verify(exactly = 0) { mockEmbrace.recordNetworkRequest(any()) } + } + + @Test + fun `incomplete requests are not recorded if the SDK has not started`() { + isSDKStarted = false + preNetworkInterceptorBeforeRequestSupplier = { throw SocketException() } + assertThrows(SocketException::class.java) { runGetRequest() } + postRequestBuilder.header("x-emb-path", customPath) + preNetworkInterceptorBeforeRequestSupplier = { throw EmbraceCustomPathException(customPath, SocketException()) } + assertThrows(EmbraceCustomPathException::class.java) { runPostRequest() } + verify(exactly = 0) { mockEmbrace.recordNetworkRequest(any()) } + } + + @Test + fun `EmbraceOkHttp3NetworkInterceptor does nothing if SDK not started`() { + isSDKStarted = false + server.enqueue(createBaseMockResponse()) + runGetRequest() + verify(exactly = 0) { mockSdkFacade.isNetworkSpanForwardingEnabled } + verify(exactly = 0) { mockEmbrace.shouldCaptureNetworkBody(any(), any()) } + } + + @Test + fun `check content length header intact with not-gzipped response body if network capture not enabled`() { + preNetworkInterceptorAfterResponseSupplier = ::checkUncompressedBodySize + server.enqueue(createBaseMockResponse().setBody(responseBody)) + runGetRequest() + assertNull(capturedEmbraceNetworkRequest.captured.networkCaptureData) + } + + @Test + fun `check content length header intact with gzipped response body if network capture not enabled`() { + preNetworkInterceptorAfterResponseSupplier = ::checkCompressedBodySize + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + runGetRequest() + assertNull(capturedEmbraceNetworkRequest.captured.networkCaptureData) + } + + @Test + fun `check response body is not gzipped and no errors in capturing response body data when body is not gzipped`() { + preNetworkInterceptorAfterResponseSupplier = ::checkUncompressedBodySize + server.enqueue(createBaseMockResponse().setBody(responseBody)) + runAndValidatePostRequest(responseBodySize) + } + + @Test + fun `check response body is not gzipped and no errors in capturing response body data when response body is gzipped`() { + preNetworkInterceptorAfterResponseSupplier = fun(response: Response): Response { + val responseBuilder: Response.Builder = response.newBuilder().request(response.request) + assertNull(response.header(CONTENT_ENCODING_HEADER_NAME)) + val bodySize = response.body?.bytes()?.size + assertEquals(responseBodySize, bodySize) + assertEquals(-1L, response.body?.contentLength()) + assertNull(response.header(CONTENT_LENGTH_HEADER_NAME)) + return responseBuilder.build() + } + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + runAndValidatePostRequest(responseBodyGzippedSize) + } + + @Test + fun `check error when getting response body in network capture`() { + val bodyThatKills: Buffer = mockk(relaxed = true) + every { bodyThatKills.size } answers { 10 } + server.enqueue(createBaseMockResponse().setBody(bodyThatKills)) + runPostRequest() + val networkCaptureData = checkNotNull(capturedEmbraceNetworkRequest.captured.networkCaptureData) + with(networkCaptureData) { + validateDefaultNonBodyNetworkCaptureData(this) + assertTrue(checkNotNull(dataCaptureErrorMessage).contains("Response Body")) + } + } + + @Test + fun `check EmbraceOkHttp3ApplicationInterceptor can handle compressed response without content-length parameter`() { + preNetworkInterceptorAfterResponseSupplier = ::removeContentLengthFromResponse + preNetworkInterceptorAfterResponseSupplier = ::consumeBody + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + runAndValidatePostRequest(responseBodyGzippedSize) + } + + @Test + fun `check EmbraceOkHttp3ApplicationInterceptor can handle uncompressed response without content-length parameter`() { + preNetworkInterceptorAfterResponseSupplier = ::removeContentLengthFromResponse + preNetworkInterceptorAfterResponseSupplier = ::consumeBody + server.enqueue(createBaseMockResponse().setBody(responseBody)) + runAndValidatePostRequest(responseBodySize) + } + + @Test + fun `check EmbraceOkHttp3NetworkInterceptor can handle compressed response without content-length parameter`() { + postNetworkInterceptorAfterResponseSupplier = ::removeContentLengthFromResponse + preNetworkInterceptorAfterResponseSupplier = ::consumeBody + server.enqueue(createBaseMockResponse().setGzipBody(responseBody)) + runAndValidatePostRequest(responseBodyGzippedSize) + } + + @Test + fun `check EmbraceOkHttp3NetworkInterceptor can handle uncompressed response without content-length parameter`() { + postNetworkInterceptorAfterResponseSupplier = ::removeContentLengthFromResponse + preNetworkInterceptorAfterResponseSupplier = ::consumeBody + server.enqueue(createBaseMockResponse().setBody(responseBody)) + runAndValidatePostRequest(responseBodySize) + } + + @Test + fun `streaming requests recorded properly`() { + postNetworkInterceptorAfterResponseSupplier = ::removeContentLengthFromResponse + server.enqueue(createBaseMockResponse().addHeader(CONTENT_TYPE_HEADER_NAME, CONTENT_TYPE_EVENT_STREAM).setBody(responseBody)) + runAndValidatePostRequest(0) + } + + @Test + fun `exceptions with canonical name and message cause incomplete network request to be recorded with those values`() { + preNetworkInterceptorBeforeRequestSupplier = { throw SocketException("bad bad socket") } + assertThrows(SocketException::class.java) { runPostRequest() } + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(SocketException::class.java.canonicalName, errorType) + assertEquals("bad bad socket", errorMessage) + } + } + + @Test + fun `anonymous exception with no message causes incomplete network request to be recorded with empty error type and message values`() { + preNetworkInterceptorBeforeRequestSupplier = { throw object : Exception() {} } + assertThrows(Exception::class.java) { runPostRequest() } + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(UNKNOWN_EXCEPTION, errorType) + assertEquals(UNKNOWN_MESSAGE, errorMessage) + } + } + + @Test + fun `EmbraceCustomPathException records incomplete network request with custom path and the correct error type and message`() { + postRequestBuilder.header("x-emb-path", customPath) + preNetworkInterceptorBeforeRequestSupplier = { throw EmbraceCustomPathException(customPath, IllegalStateException("Burned")) } + assertThrows(EmbraceCustomPathException::class.java) { runPostRequest() } + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(IllegalStateException::class.java.canonicalName, errorType) + assertEquals("Burned", errorMessage) + assertTrue(url.endsWith("$customPath?$defaultQueryString")) + } + } + + @Test + fun `EmbraceCustomPathException with anonymous cause records request with custom path and empty error type and message`() { + postRequestBuilder.header("x-emb-path", customPath) + preNetworkInterceptorBeforeRequestSupplier = { throw EmbraceCustomPathException(customPath, object : Exception() {}) } + assertThrows(EmbraceCustomPathException::class.java) { runPostRequest() } + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(UNKNOWN_EXCEPTION, errorType) + assertEquals(UNKNOWN_MESSAGE, errorMessage) + assertTrue(url.endsWith("$customPath?$defaultQueryString")) + } + } + + @Test + fun `check traceparent not injected or forwarded by default for a complete request `() { + server.enqueue(createBaseMockResponse()) + runPostRequest() + assertEquals(200, capturedEmbraceNetworkRequest.captured.responseCode) + assertNull(capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check existing traceparent not forwarded by default for a complete request`() { + server.enqueue(createBaseMockResponse()) + postRequestBuilder.header(TRACEPARENT_HEADER, CUSTOM_TRACEPARENT) + runPostRequest() + assertEquals(200, capturedEmbraceNetworkRequest.captured.responseCode) + assertNull(capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check traceparent injected and forwarded for a complete request if feature flag is on`() { + isNetworkSpanForwardingEnabled = true + server.enqueue(createBaseMockResponse()) + runPostRequest() + assertEquals(200, capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(GENERATED_TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check existing traceparent is forwarded for a complete request`() { + isNetworkSpanForwardingEnabled = true + server.enqueue(createBaseMockResponse()) + postRequestBuilder.header(TRACEPARENT_HEADER, CUSTOM_TRACEPARENT) + runPostRequest() + assertEquals(200, capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(CUSTOM_TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check traceparent not injected and forwarded for requests that don't complete because of EmbraceCustomPathException`() { + isNetworkSpanForwardingEnabled = true + postRequestBuilder.header("x-emb-path", customPath) + preNetworkInterceptorBeforeRequestSupplier = { throw EmbraceCustomPathException(customPath, IllegalStateException()) } + assertThrows(EmbraceCustomPathException::class.java) { runPostRequest() } + assertNull(capturedEmbraceNetworkRequest.captured.responseCode) + assertNull(capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check traceparent not injected and forwarded for incomplete requests`() { + isNetworkSpanForwardingEnabled = true + preNetworkInterceptorBeforeRequestSupplier = { throw NullPointerException("hell nah") } + assertThrows(NullPointerException::class.java) { runPostRequest() } + assertNull(capturedEmbraceNetworkRequest.captured.responseCode) + assertNull(capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check existing traceparent forwarded for requests that don't complete because of EmbraceCustomPathException`() { + isNetworkSpanForwardingEnabled = true + postRequestBuilder.header("x-emb-path", customPath).header(TRACEPARENT_HEADER, CUSTOM_TRACEPARENT) + preNetworkInterceptorBeforeRequestSupplier = { throw EmbraceCustomPathException(customPath, IllegalStateException()) } + assertThrows(EmbraceCustomPathException::class.java) { runPostRequest() } + assertNull(capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(CUSTOM_TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check existing traceparent forwarded incomplete requests`() { + isNetworkSpanForwardingEnabled = true + postRequestBuilder.header(TRACEPARENT_HEADER, CUSTOM_TRACEPARENT) + preNetworkInterceptorBeforeRequestSupplier = { throw SocketException("hell nah") } + assertThrows(SocketException::class.java) { runPostRequest() } + assertNull(capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(CUSTOM_TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + private fun createBaseMockResponse(httpStatus: Int = 200) = + MockResponse() + .setResponseCode(httpStatus) + .addHeader(responseHeaderName, responseHeaderValue) + + private fun MockResponse.setGzipBody(stringBody: String): MockResponse = + setBody( + Buffer().write( + ByteArrayOutputStream().use { byteArrayStream -> + GZIPOutputStream(byteArrayStream).use { gzipStream -> + gzipStream.write(stringBody.toByteArray()) + gzipStream.finish() + } + byteArrayStream.toByteArray() + } + ) + ).addHeader(CONTENT_ENCODING_HEADER_NAME, ENCODING_GZIP) + + private fun runAndValidatePostRequest( + expectedResponseBodySize: Int, + expectedPath: String = defaultPath, + expectedHttpStatus: Int = 200 + ) { + runPostRequest() + validateWholeRequest( + path = expectedPath, + httpStatus = expectedHttpStatus, + responseBodySize = expectedResponseBodySize, + httpMethod = "POST", + requestSize = requestBodySize, + responseBody = responseBody + ) + } + + private fun runAndValidateGetRequest( + expectedResponseBodySize: Int + ) { + runGetRequest() + validateWholeRequest( + path = defaultPath, + httpStatus = 200, + httpMethod = "GET", + requestSize = 0, + responseBodySize = expectedResponseBodySize, + responseBody = null + ) + } + + private fun runPostRequest() = assertNotNull(okHttpClient.newCall(postRequestBuilder.build()).execute()) + + private fun runGetRequest() = assertNotNull(okHttpClient.newCall(getRequestBuilder.build()).execute()) + + private fun validateWholeRequest( + path: String, + httpMethod: String, + httpStatus: Int, + requestSize: Int, + responseBodySize: Int, + errorType: String? = null, + errorMessage: String? = null, + traceId: String? = null, + w3cTraceparent: String? = null, + responseBody: String? + ) { + with(capturedEmbraceNetworkRequest) { + assertTrue(captured.url.endsWith("$path?$defaultQueryString")) + assertEquals(httpMethod, captured.httpMethod) + + // assert expected start/end times when we fix the issue of not using a custom clock instance. + assertTrue(captured.startTime > 0) + assertTrue(captured.endTime > 0) + + assertEquals(httpStatus, captured.responseCode) + assertEquals(requestSize.toLong(), captured.bytesOut) + assertEquals(responseBodySize.toLong(), captured.bytesIn) + assertEquals(errorType, captured.errorType) + assertEquals(errorMessage, captured.errorMessage) + assertEquals(traceId, captured.traceId) + assertEquals(w3cTraceparent, captured.w3cTraceparent) + if (responseBody != null) { + validateNetworkCaptureData(responseBody) + } + } + } + + private fun validateNetworkCaptureData(responseBody: String) { + with(checkNotNull(capturedEmbraceNetworkRequest.captured.networkCaptureData)) { + validateDefaultNonBodyNetworkCaptureData(this) + assertEquals(responseBody, capturedResponseBody?.toResponseBody()?.string()) + assertNull(dataCaptureErrorMessage) + } + } + + private fun validateDefaultNonBodyNetworkCaptureData(networkCaptureData: NetworkCaptureData?) { + with(checkNotNull(networkCaptureData)) { + assertEquals(requestHeaderValue, requestHeaders?.get(requestHeaderName.toLowerCase())) + assertEquals(responseHeaderValue, responseHeaders?.get(responseHeaderName.toLowerCase())) + assertEquals(defaultQueryString, requestQueryParams) + val buffer = Buffer() + capturedRequestBody?.toRequestBody()?.writeTo(buffer) + assertEquals(requestBodyString, buffer.readUtf8()) + } + } + + private fun checkUncompressedBodySize(response: Response) = checkBodySize(response, responseBodySize, false) + + private fun checkCompressedBodySize(response: Response) = checkBodySize(response, responseBodyGzippedSize, true) + + private fun checkBodySize(response: Response, expectedSize: Int, compressed: Boolean): Response { + val responseBuilder: Response.Builder = response.newBuilder().request(response.request) + if (compressed) { + assertEquals(ENCODING_GZIP, response.header(CONTENT_ENCODING_HEADER_NAME)) + } else { + assertNull(response.header(CONTENT_ENCODING_HEADER_NAME)) + } + val bodySize = response.body?.bytes()?.size + assertEquals(expectedSize, bodySize) + assertEquals(expectedSize.toLong(), response.body?.contentLength()) + assertEquals(expectedSize.toString(), response.header(CONTENT_LENGTH_HEADER_NAME)) + return responseBuilder.build() + } + + private fun removeContentLengthFromResponse(response: Response): Response { + val responseBuilder: Response.Builder = response.newBuilder().request(response.request) + val newHeaders: Headers = response.headers.newBuilder() + .removeAll(CONTENT_LENGTH_HEADER_NAME) + .build() + responseBuilder.headers(newHeaders) + return responseBuilder.build() + } + + private fun consumeBody(response: Response): Response { + checkNotNull(response.body).bytes() + return response + } +} diff --git a/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/TestInspectionInterceptor.kt b/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/TestInspectionInterceptor.kt new file mode 100644 index 0000000000..3ecee35fe2 --- /dev/null +++ b/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/TestInspectionInterceptor.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk.okhttp3 + +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import org.jetbrains.annotations.TestOnly +import java.io.IOException + +/** + * [Interceptor] used for testing that allows you to inspect and modify the request and responses that pass through this chain + */ +internal class TestInspectionInterceptor( + private val beforeRequestSent: (Request) -> Request, + private val afterResponseReceived: (Response) -> Response +) : Interceptor { + + @TestOnly + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + return afterResponseReceived.invoke(chain.proceed(beforeRequestSent.invoke(chain.request()))) + } +} diff --git a/embrace-android-sdk/.gitignore b/embrace-android-sdk/.gitignore new file mode 100644 index 0000000000..1dcf4532ab --- /dev/null +++ b/embrace-android-sdk/.gitignore @@ -0,0 +1,3 @@ +/build +.DS_Store +.cxx/ \ No newline at end of file diff --git a/embrace-android-sdk/CMakeLists.txt b/embrace-android-sdk/CMakeLists.txt new file mode 100644 index 0000000000..d560f2ec64 --- /dev/null +++ b/embrace-android-sdk/CMakeLists.txt @@ -0,0 +1,5 @@ +# Add main CMake files as subdirectory - this allows creation of a test target. + +cmake_minimum_required(VERSION 3.4.1) +project(TEST) +add_subdirectory(src/main/cpp) diff --git a/embrace-android-sdk/api/embrace-android-sdk.api b/embrace-android-sdk/api/embrace-android-sdk.api new file mode 100644 index 0000000000..d8f3eb4158 --- /dev/null +++ b/embrace-android-sdk/api/embrace-android-sdk.api @@ -0,0 +1,308 @@ +public abstract interface annotation class io/embrace/android/embracesdk/BetaApi : java/lang/annotation/Annotation { +} + +public final class io/embrace/android/embracesdk/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public static final field VERSION_CODE Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; + public fun ()V +} + +public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/embracesdk/EmbraceAndroidApi { + public fun addBreadcrumb (Ljava/lang/String;)V + public fun addSessionProperty (Ljava/lang/String;Ljava/lang/String;Z)Z + public fun addUserPersona (Ljava/lang/String;)V + public fun clearAllUserPersonas ()V + public fun clearUserAsPayer ()V + public fun clearUserEmail ()V + public fun clearUserIdentifier ()V + public fun clearUserPersona (Ljava/lang/String;)V + public fun clearUsername ()V + public fun createSpan (Ljava/lang/String;)Lio/embrace/android/embracesdk/spans/EmbraceSpan; + public fun createSpan (Ljava/lang/String;Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Lio/embrace/android/embracesdk/spans/EmbraceSpan; + public fun endAppStartup ()V + public fun endAppStartup (Ljava/util/Map;)V + public fun endMoment (Ljava/lang/String;)V + public fun endMoment (Ljava/lang/String;Ljava/lang/String;)V + public fun endMoment (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public fun endMoment (Ljava/lang/String;Ljava/util/Map;)V + public fun endSession ()V + public fun endSession (Z)V + public fun endView (Ljava/lang/String;)Z + public fun generateW3cTraceparent ()Ljava/lang/String; + public fun getConfigService ()Lio/embrace/android/embracesdk/config/ConfigService; + public fun getCurrentSessionId ()Ljava/lang/String; + public fun getDeviceId ()Ljava/lang/String; + public fun getFlutterInternalInterface ()Lio/embrace/android/embracesdk/FlutterInternalInterface; + public static fun getInstance ()Lio/embrace/android/embracesdk/Embrace; + public fun getLastRunEndState ()Lio/embrace/android/embracesdk/Embrace$LastRunEndState; + public fun getReactNativeInternalInterface ()Lio/embrace/android/embracesdk/ReactNativeInternalInterface; + public fun getSessionProperties ()Ljava/util/Map; + public fun getTraceIdHeader ()Ljava/lang/String; + public fun getUnityInternalInterface ()Lio/embrace/android/embracesdk/UnityInternalInterface; + public fun isStarted ()Z + public fun isTracingAvailable ()Z + public fun logComposeTap (Landroid/util/Pair;Ljava/lang/String;)V + public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;)V + public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;Lio/embrace/android/embracesdk/Severity;)V + public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;)V + public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;Ljava/lang/String;)V + public fun logError (Ljava/lang/String;)V + public fun logException (Ljava/lang/Throwable;)V + public fun logException (Ljava/lang/Throwable;Lio/embrace/android/embracesdk/Severity;)V + public fun logException (Ljava/lang/Throwable;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;)V + public fun logException (Ljava/lang/Throwable;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;Ljava/lang/String;)V + public fun logHandledDartException (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun logInfo (Ljava/lang/String;)V + public fun logInternalError (Ljava/lang/String;Ljava/lang/String;)V + public fun logInternalError (Ljava/lang/Throwable;)V + public fun logMessage (Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;)V + public fun logMessage (Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;)V + public fun logPushNotification (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Boolean;)V + public fun logRnAction (Ljava/lang/String;JJLjava/util/Map;ILjava/lang/String;)V + public fun logRnView (Ljava/lang/String;)V + public fun logUnhandledDartException (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun logWarning (Ljava/lang/String;)V + public fun recordCompletedSpan (Ljava/lang/String;JJ)Z + public fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/EmbraceSpan;)Z + public fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/ErrorCode;)Z + public fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/ErrorCode;Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Z + public fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/ErrorCode;Lio/embrace/android/embracesdk/spans/EmbraceSpan;Ljava/util/Map;Ljava/util/List;)Z + public fun recordCompletedSpan (Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;)Z + public fun recordNetworkRequest (Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest;)V + public fun recordSpan (Ljava/lang/String;Lio/embrace/android/embracesdk/spans/EmbraceSpan;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public fun recordSpan (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public fun removeSessionProperty (Ljava/lang/String;)Z + public fun sampleCurrentThreadDuringAnrs ()V + public fun setAppId (Ljava/lang/String;)Z + public fun setProcessStartedByNotification ()V + public fun setUserAsPayer ()V + public fun setUserEmail (Ljava/lang/String;)V + public fun setUserIdentifier (Ljava/lang/String;)V + public fun setUsername (Ljava/lang/String;)V + public fun shouldCaptureNetworkBody (Ljava/lang/String;Ljava/lang/String;)Z + public fun start (Landroid/content/Context;)V + public fun start (Landroid/content/Context;Z)V + public fun start (Landroid/content/Context;ZLio/embrace/android/embracesdk/Embrace$AppFramework;)V + public fun startMoment (Ljava/lang/String;)V + public fun startMoment (Ljava/lang/String;Ljava/lang/String;)V + public fun startMoment (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public fun startView (Ljava/lang/String;)Z + public fun trackWebViewPerformance (Ljava/lang/String;Landroid/webkit/ConsoleMessage;)V + public fun trackWebViewPerformance (Ljava/lang/String;Ljava/lang/String;)V +} + +public final class io/embrace/android/embracesdk/Embrace$AppFramework : java/lang/Enum { + public static final field FLUTTER Lio/embrace/android/embracesdk/Embrace$AppFramework; + public static final field NATIVE Lio/embrace/android/embracesdk/Embrace$AppFramework; + public static final field REACT_NATIVE Lio/embrace/android/embracesdk/Embrace$AppFramework; + public static final field UNITY Lio/embrace/android/embracesdk/Embrace$AppFramework; + public fun getValue ()I + public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/Embrace$AppFramework; + public static fun values ()[Lio/embrace/android/embracesdk/Embrace$AppFramework; +} + +public final class io/embrace/android/embracesdk/Embrace$LastRunEndState : java/lang/Enum { + public static final field CLEAN_EXIT Lio/embrace/android/embracesdk/Embrace$LastRunEndState; + public static final field CRASH Lio/embrace/android/embracesdk/Embrace$LastRunEndState; + public static final field INVALID Lio/embrace/android/embracesdk/Embrace$LastRunEndState; + public fun getValue ()I + public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/Embrace$LastRunEndState; + public static fun values ()[Lio/embrace/android/embracesdk/Embrace$LastRunEndState; +} + +public final class io/embrace/android/embracesdk/EmbraceSamples { + public static final field INSTANCE Lio/embrace/android/embracesdk/EmbraceSamples; + public static final fun causeNdkIllegalInstruction ()V + public static final fun throwJvmException ()V + public static final fun triggerAnr ()V + public static final fun triggerLongAnr ()V + public static final fun verifyIntegration ()V +} + +public abstract interface class io/embrace/android/embracesdk/HttpPathOverrideRequest { + public abstract fun getHeaderByName (Ljava/lang/String;)Ljava/lang/String; + public abstract fun getOverriddenURL (Ljava/lang/String;)Ljava/lang/String; + public abstract fun getURLString ()Ljava/lang/String; +} + +public abstract interface annotation class io/embrace/android/embracesdk/InternalApi : java/lang/annotation/Annotation { +} + +public final class io/embrace/android/embracesdk/LogExceptionType : java/lang/Enum { + public static final field HANDLED Lio/embrace/android/embracesdk/LogExceptionType; + public static final field NONE Lio/embrace/android/embracesdk/LogExceptionType; + public static final field UNHANDLED Lio/embrace/android/embracesdk/LogExceptionType; + public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/LogExceptionType; + public static fun values ()[Lio/embrace/android/embracesdk/LogExceptionType; +} + +public final class io/embrace/android/embracesdk/LogType : java/lang/Enum { + public static final field ERROR Lio/embrace/android/embracesdk/LogType; + public static final field INFO Lio/embrace/android/embracesdk/LogType; + public static final field WARNING Lio/embrace/android/embracesdk/LogType; + public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/LogType; + public static fun values ()[Lio/embrace/android/embracesdk/LogType; +} + +public final class io/embrace/android/embracesdk/Severity : java/lang/Enum { + public static final field ERROR Lio/embrace/android/embracesdk/Severity; + public static final field INFO Lio/embrace/android/embracesdk/Severity; + public static final field WARNING Lio/embrace/android/embracesdk/Severity; + public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/Severity; + public static fun values ()[Lio/embrace/android/embracesdk/Severity; +} + +public final class io/embrace/android/embracesdk/ViewSwazzledHooks { +} + +public final class io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener { + public static fun _preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V +} + +public final class io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener { + public static fun _preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V +} + +public final class io/embrace/android/embracesdk/WebViewChromeClientSwazzledHooks { + public static fun _preOnConsoleMessage (Landroid/webkit/ConsoleMessage;)V +} + +public final class io/embrace/android/embracesdk/WebViewClientSwazzledHooks { + public static fun _preOnPageStarted (Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V +} + +public abstract interface annotation class io/embrace/android/embracesdk/annotation/StartupActivity : java/lang/annotation/Annotation { +} + +public final class io/embrace/android/embracesdk/network/EmbraceNetworkRequest { + public static fun fromCompletedRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJJJI)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public static fun fromCompletedRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJJJILjava/lang/String;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public static fun fromCompletedRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJJJILjava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public static fun fromCompletedRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJJJILjava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public static fun fromIncompleteRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJLjava/lang/String;Ljava/lang/String;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public static fun fromIncompleteRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public static fun fromIncompleteRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public static fun fromIncompleteRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; + public fun getBytesIn ()Ljava/lang/Long; + public fun getBytesOut ()Ljava/lang/Long; + public fun getBytesReceived ()Ljava/lang/Long; + public fun getBytesSent ()Ljava/lang/Long; + public fun getEndTime ()Ljava/lang/Long; + public fun getError ()Ljava/lang/Throwable; + public fun getErrorMessage ()Ljava/lang/String; + public fun getErrorType ()Ljava/lang/String; + public fun getHttpMethod ()Ljava/lang/String; + public fun getNetworkCaptureData ()Lio/embrace/android/embracesdk/network/http/NetworkCaptureData; + public fun getResponseCode ()Ljava/lang/Integer; + public fun getStartTime ()Ljava/lang/Long; + public fun getTraceId ()Ljava/lang/String; + public fun getUrl ()Ljava/lang/String; + public fun getW3cTraceparent ()Ljava/lang/String; +} + +public class io/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride { + protected static final field PATH_OVERRIDE Ljava/lang/String; + public fun ()V + public static fun getURLString (Lio/embrace/android/embracesdk/HttpPathOverrideRequest;)Ljava/lang/String; + public static fun getURLString (Lio/embrace/android/embracesdk/HttpPathOverrideRequest;Ljava/lang/String;)Ljava/lang/String; +} + +public final class io/embrace/android/embracesdk/network/http/HttpMethod : java/lang/Enum { + public static final field CONNECT Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field DELETE Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field GET Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field HEAD Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field OPTIONS Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field PATCH Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field POST Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field PUT Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static final field TRACE Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static fun fromInt (Ljava/lang/Integer;)Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static fun fromString (Ljava/lang/String;)Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/network/http/HttpMethod; + public static fun values ()[Lio/embrace/android/embracesdk/network/http/HttpMethod; +} + +public final class io/embrace/android/embracesdk/network/http/NetworkCaptureData { + public fun (Ljava/util/Map;Ljava/lang/String;[BLjava/util/Map;[BLjava/lang/String;)V + public synthetic fun (Ljava/util/Map;Ljava/lang/String;[BLjava/util/Map;[BLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()[B + public final fun component4 ()Ljava/util/Map; + public final fun component5 ()[B + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/util/Map;Ljava/lang/String;[BLjava/util/Map;[BLjava/lang/String;)Lio/embrace/android/embracesdk/network/http/NetworkCaptureData; + public static synthetic fun copy$default (Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;Ljava/util/Map;Ljava/lang/String;[BLjava/util/Map;[BLjava/lang/String;ILjava/lang/Object;)Lio/embrace/android/embracesdk/network/http/NetworkCaptureData; + public fun equals (Ljava/lang/Object;)Z + public final fun getCapturedRequestBody ()[B + public final fun getCapturedResponseBody ()[B + public final fun getDataCaptureErrorMessage ()Ljava/lang/String; + public final fun getRequestBodySize ()I + public final fun getRequestHeaders ()Ljava/util/Map; + public final fun getRequestQueryParams ()Ljava/lang/String; + public final fun getResponseBodySize ()I + public final fun getResponseHeaders ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class io/embrace/android/embracesdk/spans/EmbraceSpan { + public abstract fun addAttribute (Ljava/lang/String;Ljava/lang/String;)Z + public abstract fun addEvent (Ljava/lang/String;)Z + public abstract fun addEvent (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;)Z + public abstract fun getParent ()Lio/embrace/android/embracesdk/spans/EmbraceSpan; + public abstract fun getSpanId ()Ljava/lang/String; + public abstract fun getTraceId ()Ljava/lang/String; + public abstract fun isRecording ()Z + public abstract fun start ()Z + public abstract fun stop ()Z + public abstract fun stop (Lio/embrace/android/embracesdk/spans/ErrorCode;)Z +} + +public final class io/embrace/android/embracesdk/spans/EmbraceSpanEvent { + public static final field Companion Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent$Companion; + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()J + public final fun component3 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;JLjava/util/Map;)Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent; + public static synthetic fun copy$default (Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent;Ljava/lang/String;JLjava/util/Map;ILjava/lang/Object;)Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent; + public fun equals (Ljava/lang/Object;)Z + public final fun getAttributes ()Ljava/util/Map; + public final fun getName ()Ljava/lang/String; + public final fun getTimestampNanos ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/embrace/android/embracesdk/spans/EmbraceSpanEvent$Companion { + public final fun create (Ljava/lang/String;JLjava/util/Map;)Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent; +} + +public final class io/embrace/android/embracesdk/spans/ErrorCode : java/lang/Enum, io/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Attribute { + public static final field FAILURE Lio/embrace/android/embracesdk/spans/ErrorCode; + public static final field UNKNOWN Lio/embrace/android/embracesdk/spans/ErrorCode; + public static final field USER_ABANDON Lio/embrace/android/embracesdk/spans/ErrorCode; + public fun getCanonicalName ()Ljava/lang/String; + public fun keyName ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/spans/ErrorCode; + public static fun values ()[Lio/embrace/android/embracesdk/spans/ErrorCode; +} + +public abstract interface class io/embrace/android/embracesdk/spans/TracingApi { + public abstract fun createSpan (Ljava/lang/String;)Lio/embrace/android/embracesdk/spans/EmbraceSpan; + public abstract fun createSpan (Ljava/lang/String;Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Lio/embrace/android/embracesdk/spans/EmbraceSpan; + public abstract fun isTracingAvailable ()Z + public abstract fun recordCompletedSpan (Ljava/lang/String;JJ)Z + public abstract fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/EmbraceSpan;)Z + public abstract fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/ErrorCode;)Z + public abstract fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/ErrorCode;Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Z + public abstract fun recordCompletedSpan (Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/ErrorCode;Lio/embrace/android/embracesdk/spans/EmbraceSpan;Ljava/util/Map;Ljava/util/List;)Z + public abstract fun recordCompletedSpan (Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;)Z + public abstract fun recordSpan (Ljava/lang/String;Lio/embrace/android/embracesdk/spans/EmbraceSpan;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public abstract fun recordSpan (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + diff --git a/embrace-android-sdk/build.gradle b/embrace-android-sdk/build.gradle new file mode 100644 index 0000000000..6a1e53c617 --- /dev/null +++ b/embrace-android-sdk/build.gradle @@ -0,0 +1,154 @@ +import io.embrace.gradle.Versions + +plugins { + id "internal-embrace-plugin" + id("org.jetbrains.kotlinx.kover") version "0.7.1" +} + +description = "Embrace Android SDK: Core" + +apply plugin: "org.jetbrains.dokka" + +def NO_NDK = "noNdk" + +android { + useLibrary "android.test.runner" + useLibrary "android.test.base" + useLibrary "android.test.mock" + ndkVersion "${Versions.ndk}" + + defaultConfig { + namespace = "io.embrace.android.embracesdk" + versionCode 53 + versionName version + consumerProguardFiles "embrace-proguard.cfg" + + // For library projects only, the BuildConfig.VERSION_NAME and BuildConfig.VERSION_CODE properties have been removed from the generated BuildConfig class + // + // https://developer.android.com/studio/releases/gradle-plugin#version_properties_removed_from_buildconfig_class_in_library_projects + buildConfigField "String", "VERSION_NAME", "\"${defaultConfig.versionName}\"" + buildConfigField "String", "VERSION_CODE", "\"${defaultConfig.versionCode}\"" + } + + if (!project.hasProperty(NO_NDK)) { + externalNativeBuild { + cmake { + path file("CMakeLists.txt") + } + } + } + packagingOptions { + pickFirst "**/*.so" + } + + sourceSets { + // Had to add a 'java' directory to store Java test files, as it doesn't get picked up as a test if I put it in + // the kotlin directory. If I've just screwed up somehow and this is actually possible, please consolidate. + test.java.srcDirs += "src/integrationTest/java" + test.kotlin.srcDirs += "src/integrationTest/kotlin" + } + + buildFeatures { + buildConfig = true + } +} + +// See: https://kotlin.github.io/kotlinx-kover/gradle-plugin/configuring#configuring-default-reports +koverReport { + filters { + excludes { + // exclusion rules - classes to exclude from report + classes("io.embrace.android.embracesdk.BuildConfig") + } + } + + androidReports("release") { + filters { + // override report filters for all reports for `release` build variant + // all filters specified by the level above cease to work + } + + xml { /* XML report config for `release` build variant */ } + html { + title = "Embrace Android SDK merged coverage report" + } + verify { + // FUTURE: we can specify a minimum bound of coverage for new code here. + } + } +} + +dependencies { + androidTestImplementation project(":test-server") + androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "com.squareup.okhttp3:mockwebserver:4.9.0" + androidTestImplementation project(path: ":embrace-android-sdk") + + implementation "androidx.lifecycle:lifecycle-common-java8:2.5.0" + implementation "androidx.lifecycle:lifecycle-extensions:2.0.0" + implementation "com.google.code.gson:gson:2.9.0" + + implementation "io.opentelemetry:opentelemetry-api:${Versions.openTelemetry}" + implementation "io.opentelemetry:opentelemetry-sdk:${Versions.openTelemetry}" + implementation "io.opentelemetry:opentelemetry-context:${Versions.openTelemetry}" + + // ProfileInstaller 1.2.0 requires compileSdk 32. 1.1.0 requires compileSdk 31. + // Please, don"t update it until we update compileSdk. + implementation "androidx.profileinstaller:profileinstaller:1.0.0" + + testImplementation "io.mockk:mockk:1.12.2" + testImplementation "androidx.test:core:1.4.0" + testImplementation "androidx.test.ext:junit:1.1.3" + testImplementation "org.robolectric:robolectric:4.10.3" + testImplementation project(path: ":embrace-android-sdk") + testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0" + + dokkaHtmlPlugin("org.jetbrains.dokka:kotlin-as-java-plugin:1.7.10") +} + +dokkaHtml { + outputDirectory.set(rootProject.file("docs")) + dokkaSourceSets { + configureEach { + perPackageOption { + skipDeprecated.set(false) + reportUndocumented.set(true) // Emit warnings about not documented members + includeNonPublic.set(false) + + // Match all packages and suppress them + perPackageOption { + matchingRegex.set(".*.networking.*?") + suppress.set(true) + } + + perPackageOption { + matchingRegex.set(".*.network.*?") + suppress.set(true) + } + + perPackageOption { + matchingRegex.set(".*.utils.*?") + suppress.set(true) + } + + perPackageOption { + matchingRegex.set(".*.internal.*?") + suppress.set(true) + } + + // unsuppress only the ones that we care about + perPackageOption { + matchingRegex.set("embrace-android-sdk") + suppress.set(false) + } + } + } + named("main") { + noAndroidSdkLink.set(false) + } + } +} + +task publishLocal { dependsOn("publishMavenPublicationToMavenLocal") } +task publishSonatype { dependsOn("publishMavenPublicationToMavenSonatype") } +task publishQa { dependsOn("publishMavenPublicationToQaRepository") } diff --git a/embrace-android-sdk/config/detekt/baseline.xml b/embrace-android-sdk/config/detekt/baseline.xml new file mode 100644 index 0000000000..5703cad343 --- /dev/null +++ b/embrace-android-sdk/config/detekt/baseline.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/embrace-android-sdk/embrace-proguard.cfg b/embrace-android-sdk/embrace-proguard.cfg new file mode 100644 index 0000000000..627bb5610b --- /dev/null +++ b/embrace-android-sdk/embrace-proguard.cfg @@ -0,0 +1,24 @@ +-keepattributes Exceptions, InnerClasses, Signature, LineNumberTable, SourceFile + +## Proguard configuration for Embrace +-keep class io.embrace.android.embracesdk.** { *; } +-dontwarn io.embrace.android.embracesdk.** + +## Proguard configuration for OkHTTP3 / Okio +-dontwarn okhttp3.** +-dontwarn okio.** + +## Proguard configuration for Gson +-keepattributes Signature +-keepattributes *Annotation* +-dontwarn sun.misc.** +-keep class com.google.gson.examples.android.model.** { *; } +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken + +## Proguard configuration for Arrow +-keep class java9.** { *; } +-dontwarn java9.** \ No newline at end of file diff --git a/embrace-android-sdk/gradle/wrapper/gradle-wrapper.jar b/embrace-android-sdk/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 GIT binary patch literal 54708 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2girk4u zvO<3q)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^ShTtO;VyD{dezY;XD@Rwl_9#j4Uo!1W&ZHVe0H>f=h#9k>~KUj^iUJ%@wU{Xuy z3FItk0<;}6D02$u(RtEY#O^hrB>qgxnOD^0AJPGC9*WXw_$k%1a%-`>uRIeeAIf3! zbx{GRnG4R$4)3rVmg63gW?4yIWW_>;t3>4@?3}&ct0Tk}<5ljU>jIN1 z&+mzA&1B6`v(}i#vAzvqWH~utZzQR;fCQGLuCN|p0hey7iCQ8^^dr*hi^wC$bTk`8M(JRKtQuXlSf$d(EISvuY0dM z7&ff;p-Ym}tT8^MF5ACG4sZmAV!l;0h&Mf#ZPd--_A$uv2@3H!y^^%_&Iw$*p79Uc5@ZXLGK;edg%)6QlvrN`U7H@e^P*0Atd zQB%>4--B1!9yeF(3vk;{>I8+2D;j`zdR8gd8dHuCQ_6|F(5-?gd&{YhLeyq_-V--4 z(SP#rP=-rsSHJSHDpT1{dMAb7-=9K1-@co_!$dG^?c(R-W&a_C5qy2~m3@%vBGhgnrw|H#g9ABb7k{NE?m4xD?;EV+fPdE>S2g$U(&_zGV+TPvaot>W_ zf8yY@)yP8k$y}UHVgF*uxtjW2zX4Hc3;W&?*}K&kqYpi%FHarfaC$ETHpSoP;A692 zR*LxY1^BO1ry@7Hc9p->hd==U@cuo*CiTnozxen;3Gct=?{5P94TgQ(UJoBb`7z@BqY z;q&?V2D1Y%n;^Dh0+eD)>9<}=A|F5{q#epBu#sf@lRs`oFEpkE%mrfwqJNFCpJC$| zy6#N;GF8XgqX(m2yMM2yq@TxStIR7whUIs2ar$t%Avh;nWLwElVBSI#j`l2$lb-!y zK|!?0hJ1T-wL{4uJhOFHp4?@28J^Oh61DbeTeSWub(|dL-KfxFCp0CjQjV`WaPW|U z=ev@VyC>IS@{ndzPy||b3z-bj5{Y53ff}|TW8&&*pu#?qs?)#&M`ACfb;%m+qX{Or zb+FNNHU}mz!@!EdrxmP_6eb3Cah!mL0ArL#EA1{nCY-!jL8zzz7wR6wAw(8K|IpW; zUvH*b1wbuRlwlUt;dQhx&pgsvJcUpm67rzkNc}2XbC6mZAgUn?VxO6YYg=M!#e=z8 zjX5ZLyMyz(VdPVyosL0}ULO!Mxu>hh`-MItnGeuQ;wGaU0)gIq3ZD=pDc(Qtk}APj z#HtA;?idVKNF)&0r|&w#l7DbX%b91b2;l2=L8q#}auVdk{RuYn3SMDo1%WW0tD*62 zaIj65Y38;?-~@b82AF!?Nra2;PU)t~qYUhl!GDK3*}%@~N0GQH7zflSpfP-ydOwNe zOK~w((+pCD&>f!b!On);5m+zUBFJtQ)mV^prS3?XgPybC2%2LiE5w+S4B|lP z+_>3$`g=%P{IrN|1Oxz30R{kI`}ZL!r|)RS@8Do;ZD3_=PbBrrP~S@EdsD{V+`!4v z{MSF}j!6odl33rA+$odIMaK%ersg%xMz>JQ^R+!qNq$5S{KgmGN#gAApX*3ib)TDsVVi>4ypIX|Ik4d6E}v z=8+hs9J=k3@Eiga^^O|ESMQB-O6i+BL*~*8coxjGs{tJ9wXjGZ^Vw@j93O<&+bzAH z9+N^ALvDCV<##cGoo5fX;wySGGmbH zHsslio)cxlud=iP2y=nM>v8vBn*hJ0KGyNOy7dr8yJKRh zywBOa4Lhh58y06`5>ESYXqLt8ZM1axd*UEp$wl`APU}C9m1H8-ModG!(wfSUQ%}rT3JD*ud~?WJdM}x>84)Cra!^J9wGs6^G^ze~eV(d&oAfm$ z_gwq4SHe=<#*FN}$5(0d_NumIZYaqs|MjFtI_rJb^+ZO?*XQ*47mzLNSL7~Nq+nw8 zuw0KwWITC43`Vx9eB!0Fx*CN9{ea$xjCvtjeyy>yf!ywxvv6<*h0UNXwkEyRxX{!e$TgHZ^db3r;1qhT)+yt@|_!@ zQG2aT`;lj>qjY`RGfQE?KTt2mn=HmSR>2!E38n8PlFs=1zsEM}AMICb z86Dbx(+`!hl$p=Z)*W~+?_HYp+CJacrCS-Fllz!7E>8*!E(yCh-cWbKc7)mPT6xu= zfKpF3I+p%yFXkMIq!ALiXF89-aV{I6v+^k#!_xwtQ*Nl#V|hKg=nP=fG}5VB8Ki7) z;19!on-iq&Xyo#AowvpA)RRgF?YBdDc$J8*)2Wko;Y?V6XMOCqT(4F#U2n1jg*4=< z8$MfDYL|z731iEKB3WW#kz|c3qh7AXjyZ}wtSg9xA(ou-pLoxF{4qk^KS?!d3J0!! zqE#R9NYGUyy>DEs%^xW;oQ5Cs@fomcrsN}rI2Hg^6y9kwLPF`K3llX00aM_r)c?ay zevlHA#N^8N+AI=)vx?4(=?j^ba^{umw140V#g58#vtnh8i7vRs*UD=lge;T+I zl1byCNr5H%DF58I2(rk%8hQ;zuCXs=sipbQy?Hd;umv4!fav@LE4JQ^>J{aZ=!@Gc~p$JudMy%0{=5QY~S8YVP zaP6gRqfZ0>q9nR3p+Wa8icNyl0Zn4k*bNto-(+o@-D8cd1Ed7`}dN3%wezkFxj_#_K zyV{msOOG;n+qbU=jBZk+&S$GEwJ99zSHGz8hF1`Xxa^&l8aaD8OtnIVsdF0cz=Y)? zP$MEdfKZ}_&#AC)R%E?G)tjrKsa-$KW_-$QL}x$@$NngmX2bHJQG~77D1J%3bGK!- zl!@kh5-uKc@U4I_Er;~epL!gej`kdX>tSXVFP-BH#D-%VJOCpM(-&pOY+b#}lOe)Z z0MP5>av1Sy-dfYFy%?`p`$P|`2yDFlv(8MEsa++Qv5M?7;%NFQK0E`Ggf3@2aUwtBpCoh`D}QLY%QAnJ z%qcf6!;cjOTYyg&2G27K(F8l^RgdV-V!~b$G%E=HP}M*Q*%xJV3}I8UYYd)>*nMvw zemWg`K6Rgy+m|y!8&*}=+`STm(dK-#b%)8nLsL&0<8Zd^|# z;I2gR&e1WUS#v!jX`+cuR;+yi(EiDcRCouW0AHNd?;5WVnC_Vg#4x56#0FOwTH6_p z#GILFF0>bb_tbmMM0|sd7r%l{U!fI0tGza&?65_D7+x9G zf3GA{c|mnO(|>}y(}%>|2>p0X8wRS&Eb0g)rcICIctfD_I9Wd+hKuEqv?gzEZBxG-rG~e!-2hqaR$Y$I@k{rLyCccE}3d)7Fn3EvfsEhA|bnJ374&pZDq&i zr(9#eq(g8^tG??ZzVk(#jU+-ce`|yiQ1dgrJ)$|wk?XLEqv&M+)I*OZ*oBCizjHuT zjZ|mW=<1u$wPhyo#&rIO;qH~pu4e3X;!%BRgmX%?&KZ6tNl386-l#a>ug5nHU2M~{fM2jvY*Py< zbR&^o&!T19G6V-pV@CB)YnEOfmrdPG%QByD?=if99ihLxP6iA8$??wUPWzptC{u5H z38Q|!=IW`)5Gef4+pz|9fIRXt>nlW)XQvUXBO8>)Q=$@gtwb1iEkU4EOWI4`I4DN5 zTC-Pk6N>2%7Hikg?`Poj5lkM0T_i zoCXfXB&}{TG%IB)ENSfI_Xg3=lxYc6-P059>oK;L+vGMy_h{y9soj#&^q5E!pl(Oq zl)oCBi56u;YHkD)d`!iOAhEJ0A^~T;uE9~Yp0{E%G~0q|9f34F!`P56-ZF{2hSaWj zio%9RR%oe~he22r@&j_d(y&nAUL*ayBY4#CWG&gZ8ybs#UcF?8K#HzziqOYM-<`C& z1gD?j)M0bp1w*U>X_b1@ag1Fx=d*wlr zEAcpmI#5LtqcX95LeS=LXlzh*l;^yPl_6MKk)zPuTz_p8ynQ5;oIOUAoPED=+M6Q( z8YR!DUm#$zTM9tbNhxZ4)J0L&Hpn%U>wj3z<=g;`&c_`fGufS!o|1%I_sA&;14bRC z3`BtzpAB-yl!%zM{Aiok8*X%lDNrPiAjBnzHbF0=Ua*3Lxl(zN3Thj2x6nWi^H7Jlwd2fxIvnI-SiC%*j z2~wIWWKT^5fYipo-#HSrr;(RkzzCSt?THVEH2EPvV-4c#Gu4&1X% z<1zTAM7ZM(LuD@ZPS?c30Ur`;2w;PXPVevxT)Ti25o}1JL>MN5i1^(aCF3 zbp>RI?X(CkR9*Hnv!({Ti@FBm;`Ip%e*D2tWEOc62@$n7+gWb;;j}@G()~V)>s}Bd zw+uTg^ibA(gsp*|&m7Vm=heuIF_pIukOedw2b_uO8hEbM4l=aq?E-7M_J`e(x9?{5 zpbgu7h}#>kDQAZL;Q2t?^pv}Y9Zlu=lO5e18twH&G&byq9XszEeXt$V93dQ@Fz2DV zs~zm*L0uB`+o&#{`uVYGXd?)Fv^*9mwLW4)IKoOJ&(8uljK?3J`mdlhJF1aK;#vlc zJdTJc2Q>N*@GfafVw45B03)Ty8qe>Ou*=f#C-!5uiyQ^|6@Dzp9^n-zidp*O`YuZ|GO28 zO0bqi;)fspT0dS2;PLm(&nLLV&&=Ingn(0~SB6Fr^AxPMO(r~y-q2>gRWv7{zYW6c zfiuqR)Xc41A7Eu{V7$-yxYT-opPtqQIJzMVkxU)cV~N0ygub%l9iHT3eQtB>nH0c` zFy}Iwd9vocxlm!P)eh0GwKMZ(fEk92teSi*fezYw3qRF_E-EcCh-&1T)?beW?9Q_+pde8&UW*(avPF4P}M#z*t~KlF~#5TT!&nu z>FAKF8vQl>Zm(G9UKi4kTqHj`Pf@Z@Q(bmZkseb1^;9k*`a9lKXceKX#dMd@ds`t| z2~UPsbn2R0D9Nm~G*oc@(%oYTD&yK)scA?36B7mndR9l*hNg!3?6>CR+tF1;6sr?V zzz8FBrZ@g4F_!O2igIGZcWd zRe_0*{d6cyy9QQ(|Ct~WTM1pC3({5qHahk*M*O}IPE6icikx48VZ?!0Oc^FVoq`}eu~ zpRq0MYHaBA-`b_BVID}|oo-bem76;B2zo7j7yz(9JiSY6JTjKz#+w{9mc{&#x}>E? zSS3mY$_|scfP3Mo_F5x;r>y&Mquy*Q1b3eF^*hg3tap~%?@ASeyodYa=dF&k=ZyWy z3C+&C95h|9TAVM~-8y(&xcy0nvl}6B*)j0FOlSz%+bK-}S4;F?P`j55*+ZO0Ogk7D z5q30zE@Nup4lqQoG`L%n{T?qn9&WC94%>J`KU{gHIq?n_L;75kkKyib;^?yXUx6BO zju%DyU(l!Vj(3stJ>!pMZ*NZFd60%oSAD1JUXG0~2GCXpB0Am(YPyhzQda-e)b^+f zzFaEZdVTJRJXPJo%w z$?T;xq^&(XjmO>0bNGsT|1{1UqGHHhasPC;H!oX52(AQ7h9*^npOIRdQbNrS0X5#5G?L4V}WsAYcpq-+JNXhSl)XbxZ)L@5Q+?wm{GAU z9a7X8hAjAo;4r_eOdZfXGL@YpmT|#qECEcPTQ;nsjIkQ;!0}g?T>Zr*Fg}%BZVA)4 zCAzvWr?M&)KEk`t9eyFi_GlPV9a2kj9G(JgiZadd_&Eb~#DyZ%2Zcvrda_A47G&uW z^6TnBK|th;wHSo8ivpScU?AM5HDu2+ayzExMJc@?4{h-c`!b($ExB`ro#vkl<;=BA z961c*n(4OR!ebT*7UV7sqL;rZ3+Z)BYs<1I|9F|TOKebtLPxahl|ZXxj4j!gjj!3*+iSb5Zni&EKVt$S{0?2>A}d@3PSF3LUu)5 z*Y#a1uD6Y!$=_ghsPrOqX!OcIP`IW};tZzx1)h_~mgl;0=n zdP|Te_7)~R?c9s>W(-d!@nzQyxqakrME{Tn@>0G)kqV<4;{Q?Z-M)E-|IFLTc}WQr z1Qt;u@_dN2kru_9HMtz8MQx1aDYINH&3<+|HA$D#sl3HZ&YsjfQBv~S>4=u z7gA2*X6_cI$2}JYLIq`4NeXTz6Q3zyE717#>RD&M?0Eb|KIyF;xj;+3#DhC-xOj~! z$-Kx#pQ)_$eHE3Zg?V>1z^A%3jW0JBnd@z`kt$p@lch?A9{j6hXxt$(3|b>SZiBxOjA%LsIPii{=o(B`yRJ>OK;z_ELTi8xHX)il z--qJ~RWsZ%9KCNuRNUypn~<2+mQ=O)kd59$Lul?1ev3c&Lq5=M#I{ zJby%%+Top_ocqv!jG6O6;r0Xwb%vL6SP{O(hUf@8riADSI<|y#g`D)`x^vHR4!&HY`#TQMqM`Su}2(C|KOmG`wyK>uh@3;(prdL{2^7T3XFGznp{-sNLLJH@mh* z^vIyicj9yH9(>~I-Ev7p=yndfh}l!;3Q65}K}()(jp|tC;{|Ln1a+2kbctWEX&>Vr zXp5=#pw)@-O6~Q|><8rd0>H-}0Nsc|J6TgCum{XnH2@hFB09FsoZ_ow^Nv@uGgz3# z<6dRDt1>>-!kN58&K1HFrgjTZ^q<>hNI#n8=hP&pKAL4uDcw*J66((I?!pE0fvY6N zu^N=X8lS}(=w$O_jlE(;M9F={-;4R(K5qa=P#ZVW>}J&s$d0?JG8DZJwZcx3{CjLg zJA>q-&=Ekous)vT9J>fbnZYNUtvox|!Rl@e^a6ue_4-_v=(sNB^I1EPtHCFEs!>kK6B@-MS!(B zST${=v9q6q8YdSwk4}@c6cm$`qZ86ipntH8G~51qIlsYQ)+2_Fg1@Y-ztI#aa~tFD_QUxb zU-?g5B}wU@`tnc_l+B^mRogRghXs!7JZS=A;In1|f(1T(+xfIi zvjccLF$`Pkv2w|c5BkSj>>k%`4o6#?ygojkV78%zzz`QFE6nh{(SSJ9NzVdq>^N>X zpg6+8u7i(S>c*i*cO}poo7c9%i^1o&3HmjY!s8Y$5aO(!>u1>-eai0;rK8hVzIh8b zL53WCXO3;=F4_%CxMKRN^;ggC$;YGFTtHtLmX%@MuMxvgn>396~ zEp>V(dbfYjBX^!8CSg>P2c5I~HItbe(dl^Ax#_ldvCh;D+g6-%WD|$@S6}Fvv*eHc zaKxji+OG|_KyMe2D*fhP<3VP0J1gTgs6JZjE{gZ{SO-ryEhh;W237Q0 z{yrDobsM6S`bPMUzr|lT|99m6XDI$RzW4tQ$|@C2RjhBYPliEXFV#M*5G4;Kb|J8E z0IH}-d^S-53kFRZ)ZFrd2%~Sth-6BN?hnMa_PC4gdWyW3q-xFw&L^x>j<^^S$y_3_ zdZxouw%6;^mg#jG@7L!g9Kdw}{w^X9>TOtHgxLLIbfEG^Qf;tD=AXozE6I`XmOF=# zGt$Wl+7L<8^VI-eSK%F%dqXieK^b!Z3yEA$KL}X@>fD9)g@=DGt|=d(9W%8@Y@!{PI@`Nd zyF?Us(0z{*u6|X?D`kKSa}}Q*HP%9BtDEA^buTlI5ihwe)CR%OR46b+>NakH3SDbZmB2X>c8na&$lk zYg$SzY+EXtq2~$Ep_x<~+YVl<-F&_fbayzTnf<7?Y-un3#+T~ahT+eW!l83sofNt; zZY`eKrGqOux)+RMLgGgsJdcA3I$!#zy!f<$zL0udm*?M5w=h$Boj*RUk8mDPVUC1RC8A`@7PgoBIU+xjB7 z25vky+^7k_|1n1&jKNZkBWUu1VCmS}a|6_+*;fdUZAaIR4G!wv=bAZEXBhcjch6WH zdKUr&>z^P%_LIx*M&x{!w|gij?nigT8)Ol3VicXRL0tU}{vp2fi!;QkVc#I38op3O z=q#WtNdN{x)OzmH;)j{cor)DQ;2%m>xMu_KmTisaeCC@~rQwQTfMml7FZ_ zU2AR8yCY_CT$&IAn3n#Acf*VKzJD8-aphMg(12O9cv^AvLQ9>;f!4mjyxq_a%YH2+{~=3TMNE1 z#r3@ynnZ#p?RCkPK36?o{ILiHq^N5`si(T_cKvO9r3^4pKG0AgDEB@_72(2rvU^-; z%&@st2+HjP%H)u50t81p>(McL{`dTq6u-{JM|d=G1&h-mtjc2{W0%*xuZVlJpUSP-1=U6@5Q#g(|nTVN0icr-sdD~DWR=s}`$#=Wa zt5?|$`5`=TWZevaY9J9fV#Wh~Fw@G~0vP?V#Pd=|nMpSmA>bs`j2e{)(827mU7rxM zJ@ku%Xqhq!H)It~yXm=)6XaPk=$Rpk*4i4*aSBZe+h*M%w6?3&0>>|>GHL>^e4zR!o%aGzUn40SR+TdN%=Dbn zsRfXzGcH#vjc-}7v6yRhl{V5PhE-r~)dnmNz=sDt?*1knNZ>xI5&vBwrosF#qRL-Y z;{W)4W&cO0XMKy?{^d`Xh(2B?j0ioji~G~p5NQJyD6vouyoFE9w@_R#SGZ1DR4GnN z{b=sJ^8>2mq3W;*u2HeCaKiCzK+yD!^i6QhTU5npwO+C~A#5spF?;iuOE>o&p3m1C zmT$_fH8v+5u^~q^ic#pQN_VYvU>6iv$tqx#Sulc%|S7f zshYrWq7IXCiGd~J(^5B1nGMV$)lo6FCTm1LshfcOrGc?HW7g>pV%#4lFbnt#94&Rg{%Zbg;Rh?deMeOP(du*)HryI zCdhO$3|SeaWK<>(jSi%qst${Z(q@{cYz7NA^QO}eZ$K@%YQ^Dt4CXzmvx~lLG{ef8 zyckIVSufk>9^e_O7*w2z>Q$8me4T~NQDq=&F}Ogo#v1u$0xJV~>YS%mLVYqEf~g*j zGkY#anOI9{(f4^v21OvYG<(u}UM!-k;ziH%GOVU1`$0VuO@Uw2N{$7&5MYjTE?Er) zr?oZAc~Xc==KZx-pmoh9KiF_JKU7u0#b_}!dWgC>^fmbVOjuiP2FMq5OD9+4TKg^2 z>y6s|sQhI`=fC<>BnQYV433-b+jBi+N6unz%6EQR%{8L#=4sktI>*3KhX+qAS>+K#}y5KnJ8YuOuzG(Ea5;$*1P$-9Z+V4guyJ#s) zRPH(JPN;Es;H72%c8}(U)CEN}Xm>HMn{n!d(=r*YP0qo*^APwwU5YTTeHKy#85Xj< zEboiH=$~uIVMPg!qbx~0S=g&LZ*IyTJG$hTN zv%2>XF``@S9lnLPC?|myt#P)%7?%e_j*aU4TbTyxO|3!h%=Udp;THL+^oPp<6;TLlIOa$&xeTG_a*dbRDy+(&n1T=MU z+|G5{2UprrhN^AqODLo$9Z2h(3^wtdVIoSk@}wPajVgIoZipRft}^L)2Y@mu;X-F{LUw|s7AQD-0!otW#W9M@A~08`o%W;Bq-SOQavG*e-sy8) zwtaucR0+64B&Pm++-m56MQ$@+t{_)7l-|`1kT~1s!swfc4D9chbawUt`RUOdoxU|j z$NE$4{Ysr@2Qu|K8pD37Yv&}>{_I5N49a@0<@rGHEs}t zwh_+9T0oh@ptMbjy*kbz<&3>LGR-GNsT8{x1g{!S&V7{5tPYX(GF>6qZh>O&F)%_I zkPE-pYo3dayjNQAG+xrI&yMZy590FA1unQ*k*Zfm#f9Z5GljOHBj-B83KNIP1a?<^1vOhDJkma0o- zs(TP=@e&s6fRrU(R}{7eHL*(AElZ&80>9;wqj{|1YQG=o2Le-m!UzUd?Xrn&qd8SJ0mmEYtW;t(;ncW_j6 zGWh4y|KMK^s+=p#%fWxjXo434N`MY<8W`tNH-aM6x{@o?D3GZM&+6t4V3I*3fZd{a z0&D}DI?AQl{W*?|*%M^D5{E>V%;=-r&uQ>*e)cqVY52|F{ptA*`!iS=VKS6y4iRP6 zKUA!qpElT5vZvN}U5k-IpeNOr6KF`-)lN1r^c@HnT#RlZbi(;yuvm9t-Noh5AfRxL@j5dU-X37(?S)hZhRDbf5cbhDO5nSX@WtApyp` zT$5IZ*4*)h8wShkPI45stQH2Y7yD*CX^Dh@B%1MJSEn@++D$AV^ttKXZdQMU`rxiR z+M#45Z2+{N#uR-hhS&HAMFK@lYBWOzU^Xs-BlqQDyN4HwRtP2$kks@UhAr@wlJii%Rq?qy25?Egs z*a&iAr^rbJWlv+pYAVUq9lor}#Cm|D$_ev2d2Ko}`8kuP(ljz$nv3OCDc7zQp|j6W zbS6949zRvj`bhbO(LN3}Pq=$Ld3a_*9r_24u_n)1)}-gRq?I6pdHPYHgIsn$#XQi~ z%&m_&nnO9BKy;G%e~fa7i9WH#MEDNQ8WCXhqqI+oeE5R7hLZT_?7RWVzEGZNz4*Po ze&*a<^Q*ze72}UM&$c%FuuEIN?EQ@mnILwyt;%wV-MV+|d%>=;3f0(P46;Hwo|Wr0 z>&FS9CCb{?+lDpJMs`95)C$oOQ}BSQEv0Dor%-Qj0@kqlIAm1-qSY3FCO2j$br7_w zlpRfAWz3>Gh~5`Uh?ER?@?r0cXjD0WnTx6^AOFii;oqM?|M9QjHd*GK3WwA}``?dK15`ZvG>_nB2pSTGc{n2hYT6QF^+&;(0c`{)*u*X7L_ zaxqyvVm$^VX!0YdpSNS~reC+(uRqF2o>jqIJQkC&X>r8|mBHvLaduM^Mh|OI60<;G zDHx@&jUfV>cYj5+fAqvv(XSmc(nd@WhIDvpj~C#jhZ6@M3cWF2HywB1yJv2#=qoY| zIiaxLsSQa7w;4YE?7y&U&e6Yp+2m(sb5q4AZkKtey{904rT08pJpanm->Z75IdvW^ z!kVBy|CIUZn)G}92_MgoLgHa?LZJDp_JTbAEq8>6a2&uKPF&G!;?xQ*+{TmNB1H)_ z-~m@CTxDry_-rOM2xwJg{fcZ41YQDh{DeI$4!m8c;6XtFkFyf`fOsREJ`q+Bf4nS~ zKDYs4AE7Gugv?X)tu4<-M8ag{`4pfQ14z<(8MYQ4u*fl*DCpq66+Q1-gxNCQ!c$me zyTrmi7{W-MGP!&S-_qJ%9+e08_9`wWGG{i5yLJ;8qbt-n_0*Q371<^u@tdz|;>fPW zE=&q~;wVD_4IQ^^jyYX;2shIMiYdvIpIYRT>&I@^{kL9Ka2ECG>^l>Ae!GTn{r~o= z|I9=J#wNe)zYRqGZ7Q->L{dfewyC$ZYcLaoNormZ3*gfM=da*{heC)&46{yTS!t10 zn_o0qUbQOs$>YuY>YHi|NG^NQG<_@jD&WnZcW^NTC#mhVE7rXlZ=2>mZkx{bc=~+2 z{zVH=Xs0`*K9QAgq9cOtfQ^BHh-yr=qX8hmW*0~uCup89IJMvWy%#yt_nz@6dTS)L{O3vXye< zW4zUNb6d|Tx`XIVwMMgqnyk?c;Kv`#%F0m^<$9X!@}rI##T{iXFC?(ui{;>_9Din8 z7;(754q!Jx(~sb!6+6Lf*l{fqD7GW*v{>3wp+)@wq2abADBK!kI8To}7zooF%}g-z zJ1-1lp-lQI6w^bov9EfhpxRI}`$PTpJI3uo@ZAV729JJ2Hs68{r$C0U=!d$Bm+s(p z8Kgc(Ixf4KrN%_jjJjTx5`&`Ak*Il%!}D_V)GM1WF!k$rDJ-SudXd_Xhl#NWnET&e-P!rH~*nNZTzxj$?^oo3VWc-Ay^`Phze3(Ft!aNW-f_ zeMy&BfNCP^-FvFzR&rh!w(pP5;z1$MsY9Voozmpa&A}>|a{eu}>^2s)So>&kmi#7$ zJS_-DVT3Yi(z+ruKbffNu`c}s`Uo`ORtNpUHa6Q&@a%I%I;lm@ea+IbCLK)IQ~)JY zp`kdQ>R#J*i&Ljer3uz$m2&Un9?W=Ue|hHv?xlM`I&*-M;2{@so--0OAiraN1TLra z>EYQu#)Q@UszfJj&?kr%RraFyi*eG+HD_(!AWB;hPgB5Gd-#VDRxxv*VWMY0hI|t- zR=;TL%EKEg*oet7GtmkM zgH^y*1bfJ*af(_*S1^PWqBVVbejFU&#m`_69IwO!aRW>Rcp~+7w^ptyu>}WFYUf;) zZrgs;EIN9$Immu`$umY%$I)5INSb}aV-GDmPp!d_g_>Ar(^GcOY%2M)Vd7gY9llJR zLGm*MY+qLzQ+(Whs8-=ty2l)G9#82H*7!eo|B6B$q%ak6eCN%j?{SI9|K$u3)ORoz zw{bAGaWHrMb|X^!UL~_J{jO?l^}lI^|7jIn^p{n%JUq9{tC|{GM5Az3SrrPkuCt_W zq#u0JfDw{`wAq`tAJmq~sz`D_P-8qr>kmms>I|);7Tn zLl^n*Ga7l=U)bQmgnSo5r_&#Pc=eXm~W75X9Cyy0WDO|fbSn5 zLgpFAF4fa90T-KyR4%%iOq6$6BNs@3ZV<~B;7V=u zdlB8$lpe`w-LoS;0NXFFu@;^^bc?t@r3^XTe*+0;o2dt&>eMQeDit(SfDxYxuA$uS z**)HYK7j!vJVRNfrcokVc@&(ke5kJzvi};Lyl7@$!`~HM$T!`O`~MQ1k~ZH??fQr zNP)33uBWYnTntKRUT*5lu&8*{fv>syNgxVzEa=qcKQ86Vem%Lpae2LM=TvcJLs?`=o9%5Mh#k*_7zQD|U7;A%=xo^_4+nX{~b1NJ6@ z*=55;+!BIj1nI+)TA$fv-OvydVQB=KK zrGWLUS_Chm$&yoljugU=PLudtJ2+tM(xj|E>Nk?c{-RD$sGYNyE|i%yw>9gPItE{ zD|BS=M>V^#m8r?-3swQofD8j$h-xkg=F+KM%IvcnIvc)y zl?R%u48Jeq7E*26fqtLe_b=9NC_z|axW#$e0adI#r(Zsui)txQ&!}`;;Z%q?y2Kn! zXzFNe+g7+>>`9S0K1rmd)B_QVMD?syc3e0)X*y6(RYH#AEM9u?V^E0GHlAAR)E^4- zjKD+0K=JKtf5DxqXSQ!j?#2^ZcQoG5^^T+JaJa3GdFeqIkm&)dj76WaqGukR-*&`13ls8lU2ayVIR%;79HYAr5aEhtYa&0}l}eAw~qKjUyz4v*At z?})QplY`3cWB6rl7MI5mZx&#%I0^iJm3;+J9?RA(!JXjl?(XgmA-D#2cY-^?g1c*Q z3GVLh!8Jhe;QqecbMK#XIJxKMb=6dcs?1vbb?@ov-raj`hnYO92y8pv@>RVr=9Y-F zv`BK)9R6!m4Pfllu4uy0WBL+ZaUFFzbZZtI@J8{OoQ^wL-b$!FpGT)jYS-=vf~b-@ zIiWs7j~U2yI=G5;okQz%gh6}tckV5wN;QDbnu|5%%I(#)8Q#)wTq8YYt$#f9=id;D zJbC=CaLUyDIPNOiDcV9+=|$LE9v2;Qz;?L+lG{|g&iW9TI1k2_H;WmGH6L4tN1WL+ zYfSVWq(Z_~u~U=g!RkS|YYlWpKfZV!X%(^I3gpV%HZ_{QglPSy0q8V+WCC2opX&d@eG2BB#(5*H!JlUzl$DayI5_J-n zF@q*Fc-nlp%Yt;$A$i4CJ_N8vyM5fNN`N(CN53^f?rtya=p^MJem>JF2BEG|lW|E) zxf)|L|H3Oh7mo=9?P|Y~|6K`B3>T)Gw`0ESP9R`yKv}g|+qux(nPnU(kQ&&x_JcYg9+6`=; z-EI_wS~l{T3K~8}8K>%Ke`PY!kNt415_x?^3QOvX(QUpW&$LXKdeZM-pCI#%EZ@ta zv(q-(xXIwvV-6~(Jic?8<7ain4itN>7#AqKsR2y(MHMPeL)+f+v9o8Nu~p4ve*!d3 z{Lg*NRTZsi;!{QJknvtI&QtQM_9Cu%1QcD0f!Fz+UH4O#8=hvzS+^(e{iG|Kt7C#u zKYk7{LFc+9Il>d6)blAY-9nMd(Ff0;AKUo3B0_^J&ESV@4UP8PO0no7G6Gp_;Z;YnzW4T-mCE6ZfBy(Y zXOq^Of&?3#Ra?khzc7IJT3!%IKK8P(N$ST47Mr=Gv@4c!>?dQ-&uZihAL1R<_(#T8Y`Ih~soL6fi_hQmI%IJ5qN995<{<@_ z;^N8AGQE+?7#W~6X>p|t<4@aYC$-9R^}&&pLo+%Ykeo46-*Yc(%9>X>eZpb8(_p{6 zwZzYvbi%^F@)-}5%d_z^;sRDhjqIRVL3U3yK0{Q|6z!PxGp?|>!%i(!aQODnKUHsk^tpeB<0Qt7`ZBlzRIxZMWR+|+ z3A}zyRZ%0Ck~SNNov~mN{#niO**=qc(faGz`qM16H+s;Uf`OD1{?LlH!K!+&5xO%6 z5J80-41C{6)j8`nFvDaeSaCu_f`lB z_Y+|LdJX=YYhYP32M556^^Z9MU}ybL6NL15ZTV?kfCFfpt*Pw5FpHp#2|ccrz#zoO zhs=+jQI4fk*H0CpG?{fpaSCmXzU8bB`;kCLB8T{_3t>H&DWj0q0b9B+f$WG=e*89l zzUE)b9a#aWsEpgnJqjVQETpp~R7gn)CZd$1B8=F*tl+(iPH@s9jQtE33$dBDOOr=% ziOpR8R|1eLI?Rn*d+^;_U#d%bi$|#obe0(-HdB;K>=Y=mg{~jTA_WpChe8QquhF`N z>hJ}uV+pH`l_@d>%^KQNm*$QNJ(lufH>zv9M`f+C-y*;hAH(=h;kp@eL=qPBeXrAo zE7my75EYlFB30h9sdt*Poc9)2sNP9@K&4O7QVPQ^m$e>lqzz)IFJWpYrpJs)Fcq|P z5^(gnntu!+oujqGpqgY_o0V&HL72uOF#13i+ngg*YvPcqpk)Hoecl$dx>C4JE4DWp z-V%>N7P-}xWv%9Z73nn|6~^?w$5`V^xSQbZceV<_UMM&ijOoe{Y^<@3mLSq_alz8t zr>hXX;zTs&k*igKAen1t1{pj94zFB;AcqFwV)j#Q#Y8>hYF_&AZ?*ar1u%((E2EfZ zcRsy@s%C0({v=?8oP=DML`QsPgzw3|9|C22Y>;=|=LHSm7~+wQyI|;^WLG0_NSfrf zamq!5%EzdQ&6|aTP2>X=Z^Jl=w6VHEZ@=}n+@yeu^ke2Yurrkg9up3g$0SI8_O-WQu$bCsKc(juv|H;vz6}%7ONww zKF%!83W6zO%0X(1c#BM}2l^ddrAu^*`9g&1>P6m%x{gYRB)}U`40r>6YmWSH(|6Ic zH~QNgxlH*;4jHg;tJiKia;`$n_F9L~M{GiYW*sPmMq(s^OPOKm^sYbBK(BB9dOY`0 z{0!=03qe*Sf`rcp5Co=~pfQyqx|umPHj?a6;PUnO>EZGb!pE(YJgNr{j;s2+nNV(K zDi#@IJ|To~Zw)vqGnFwb2}7a2j%YNYxe2qxLk)VWJIux$BC^oII=xv-_}h@)Vkrg1kpKokCmX({u=lSR|u znu_fA0PhezjAW{#Gu0Mdhe8F4`!0K|lEy+<1v;$ijSP~A9w%q5-4Ft|(l7UqdtKao zs|6~~nmNYS>fc?Nc=yzcvWNp~B0sB5ForO5SsN(z=0uXxl&DQsg|Y?(zS)T|X``&8 z*|^p?~S!vk8 zg>$B{oW}%rYkgXepmz;iqCKY{R@%@1rcjuCt}%Mia@d8Vz5D@LOSCbM{%JU#cmIp! z^{4a<3m%-p@JZ~qg)Szb-S)k{jv92lqB(C&KL(jr?+#ES5=pUH$(;CO9#RvDdErmW z3(|f{_)dcmF-p*D%qUa^yYngNP&Dh2gq5hr4J!B5IrJ?ODsw@*!0p6Fm|(ebRT%l) z#)l22@;4b9RDHl1ys$M2qFc;4BCG-lp2CN?Ob~Be^2wQJ+#Yz}LP#8fmtR%o7DYzoo1%4g4D+=HonK7b!3nvL0f1=oQp93dPMTsrjZRI)HX-T}ApZ%B#B;`s? z9Kng{|G?yw7rxo(T<* z1+O`)GNRmXq3uc(4SLX?fPG{w*}xDCn=iYo2+;5~vhWUV#e5e=Yfn4BoS@3SrrvV9 zrM-dPU;%~+3&>(f3sr$Rcf4>@nUGG*vZ~qnxJznDz0irB(wcgtyATPd&gSuX^QK@+ z)7MGgxj!RZkRnMSS&ypR94FC$;_>?8*{Q110XDZ)L);&SA8n>72s1#?6gL>gydPs` zM4;ert4-PBGB@5E` zBaWT=CJUEYV^kV%@M#3(E8>g8Eg|PXg`D`;K8(u{?}W`23?JgtNcXkUxrH}@H_4qN zw_Pr@g%;CKkgP(`CG6VTIS4ZZ`C22{LO{tGi6+uPvvHkBFK|S6WO{zo1MeK$P zUBe}-)3d{55lM}mDVoU@oGtPQ+a<=wwDol}o=o1z*)-~N!6t09du$t~%MlhM9B5~r zy|zs^LmEF#yWpXZq!+Nt{M;bE%Q8z7L8QJDLie^5MKW|I1jo}p)YW(S#oLf(sWn~* zII>pocNM5#Z+-n2|495>?H?*oyr0!SJIl(}q-?r`Q;Jbqqr4*_G8I7agO298VUr9x z8ZcHdCMSK)ZO@Yr@c0P3{`#GVVdZ{zZ$WTO zuvO4ukug&& ze#AopTVY3$B>c3p8z^Yyo8eJ+(@FqyDWlR;uxy0JnSe`gevLF`+ZN6OltYr>oN(ZV z>76nIiVoll$rDNkck6_eh%po^u16tD)JXcii|#Nn(7=R9mA45jz>v}S%DeMc(%1h> zoT2BlF9OQ080gInWJ3)bO9j$ z`h6OqF0NL4D3Kz?PkE8nh;oxWqz?<3_!TlN_%qy*T7soZ>Pqik?hWWuya>T$55#G9 zxJv=G&=Tm4!|p1#!!hsf*uQe}zWTKJg`hkuj?ADST2MX6fl_HIDL7w`5Dw1Btays1 zz*aRwd&>4*H%Ji2bt-IQE$>sbCcI1Poble0wL`LAhedGRZp>%>X6J?>2F*j>`BX|P zMiO%!VFtr_OV!eodgp-WgcA-S=kMQ^zihVAZc!vdx*YikuDyZdHlpy@Y3i!r%JI85$-udM6|7*?VnJ!R)3Qfm4mMm~Z#cvNrGUy|i0u zb|(7WsYawjBK0u1>@lLhMn}@X>gyDlx|SMXQo|yzkg-!wIcqfGrA!|t<3NC2k` zq;po50dzvvHD>_mG~>W0iecTf@3-)<$PM5W@^yMcu@U;)(^eu@e4jAX7~6@XrSbIE zVG6v2miWY^g8bu5YH$c2QDdLkg2pU8xHnh`EUNT+g->Q8Tp4arax&1$?CH($1W&*} zW&)FQ>k5aCim$`Ph<9Zt?=%|pz&EX@_@$;3lQT~+;EoD(ho|^nSZDh*M0Z&&@9T+e zHYJ;xB*~UcF^*7a_T)9iV5}VTYKda8n*~PSy@>h7c(mH~2AH@qz{LMQCb+-enMhX} z2k0B1JQ+6`?Q3Lx&(*CBQOnLBcq;%&Nf<*$CX2<`8MS9c5zA!QEbUz1;|(Ua%CiuL zF2TZ>@t7NKQ->O#!;0s;`tf$veXYgq^SgG>2iU9tCm5&^&B_aXA{+fqKVQ*S9=58y zddWqy1lc$Y@VdB?E~_B5w#so`r552qhPR649;@bf63_V@wgb!>=ij=%ptnsq&zl8^ zQ|U^aWCRR3TnoKxj0m0QL2QHM%_LNJ(%x6aK?IGlO=TUoS%7YRcY{!j(oPcUq{HP=eR1>0o^(KFl-}WdxGRjsT);K8sGCkK0qVe{xI`# z@f+_kTYmLbOTxRv@wm2TNBKrl+&B>=VaZbc(H`WWLQhT=5rPtHf)#B$Q6m1f8We^)f6ylbO=t?6Y;{?&VL|j$VXyGV!v8eceRk zl>yOWPbk%^wv1t63Zd8X^Ck#12$*|yv`v{OA@2;-5Mj5sk#ptfzeX(PrCaFgn{3*hau`-a+nZhuJxO;Tis51VVeKAwFML#hF9g26NjfzLs8~RiM_MFl1mgDOU z=ywk!Qocatj1Q1yPNB|FW>!dwh=aJxgb~P%%7(Uydq&aSyi?&b@QCBiA8aP%!nY@c z&R|AF@8}p7o`&~>xq9C&X6%!FAsK8gGhnZ$TY06$7_s%r*o;3Y7?CenJUXo#V-Oag z)T$d-V-_O;H)VzTM&v8^Uk7hmR8v0)fMquWHs6?jXYl^pdM#dY?T5XpX z*J&pnyJ<^n-d<0@wm|)2SW9e73u8IvTbRx?Gqfy_$*LI_Ir9NZt#(2T+?^AorOv$j zcsk+t<#!Z!eC|>!x&#l%**sSAX~vFU0|S<;-ei}&j}BQ#ekRB-;c9~vPDIdL5r{~O zMiO3g0&m-O^gB}<$S#lCRxX@c3g}Yv*l)Hh+S^my28*fGImrl<-nbEpOw-BZ;WTHL zgHoq&ftG|~ouV<>grxRO6Z%{!O+j`Cw_4~BIzrjpkdA5jH40{1kDy|pEq#7`$^m*? zX@HxvW`e}$O$mJvm+65Oc4j7W@iVe)rF&-}R>KKz>rF&*Qi3%F0*tz!vNtl@m8L9= zyW3%|X}0KsW&!W<@tRNM-R>~~QHz?__kgnA(G`jWOMiEaFjLzCdRrqzKlP1vYLG`Y zh6_knD3=9$weMn4tBD|5=3a9{sOowXHu(z5y^RYrxJK z|L>TUvbDuO?3=YJ55N5}Kj0lC(PI*Te0>%eLNWLnawD54geX5>8AT(oT6dmAacj>o zC`Bgj-RV0m3Dl2N=w3e0>wWWG5!mcal`Xu<(1=2$b{k(;kC(2~+B}a(w;xaHPk^@V zGzDR|pt%?(1xwNxV!O6`JLCM!MnvpbLoHzKziegT_2LLWAi4}UHIo6uegj#WTQLet z9Dbjyr{8NAk+$(YCw~_@Az9N|iqsliRYtR7Q|#ONIV|BZ7VKcW$phH9`ZAlnMTW&9 zIBqXYuv*YY?g*cJRb(bXG}ts-t0*|HXId4fpnI>$9A?+BTy*FG8f8iRRKYRd*VF_$ zoo$qc+A(d#Lx0@`ck>tt5c$L1y7MWohMnZd$HX++I9sHoj5VXZRZkrq`v@t?dfvC} z>0h!c4HSb8%DyeF#zeU@rJL2uhZ^8dt(s+7FNHJeY!TZJtyViS>a$~XoPOhHsdRH* zwW+S*rIgW0qSPzE6w`P$Jv^5dsyT6zoby;@z=^yWLG^x;e557RnndY>ph!qCF;ov$ ztSW1h3@x{zm*IMRx|3lRWeI3znjpbS-0*IL4LwwkWyPF1CRpQK|s42dJ{ddA#BDDqio-Y+mF-XcP-z4bi zAhfXa2=>F0*b;F0ftEPm&O+exD~=W^qjtv&>|%(4q#H=wbA>7QorDK4X3~bqeeXv3 zV1Q<>_Fyo!$)fD`fd@(7(%6o-^x?&+s=)jjbQ2^XpgyYq6`}ISX#B?{I$a&cRcW?X zhx(i&HWq{=8pxlA2w~7521v-~lu1M>4wL~hDA-j(F2;9ICMg+6;Zx2G)ulp7j;^O_ zQJIRUWQam(*@?bYiRTKR<;l_Is^*frjr-Dj3(fuZtK{Sn8F;d*t*t{|_lnlJ#e=hx zT9?&_n?__2mN5CRQ}B1*w-2Ix_=CF@SdX-cPjdJN+u4d-N4ir*AJn&S(jCpTxiAms zzI5v(&#_#YrKR?B?d~ge1j*g<2yI1kp`Lx>8Qb;aq1$HOX4cpuN{2ti!2dXF#`AG{ zp<iD=Z#qN-yEwLwE7%8w8&LB<&6{WO$#MB-|?aEc@S1a zt%_p3OA|kE&Hs47Y8`bdbt_ua{-L??&}uW zmwE7X4Y%A2wp-WFYPP_F5uw^?&f zH%NCcbw_LKx!c!bMyOBrHDK1Wzzc5n7A7C)QrTj_Go#Kz7%+y^nONjnnM1o5Sw(0n zxU&@41(?-faq?qC^kO&H301%|F9U-Qm(EGd3}MYTFdO+SY8%fCMTPMU3}bY7ML1e8 zrdOF?E~1uT)v?UX(XUlEIUg3*UzuT^g@QAxEkMb#N#q0*;r zF6ACHP{ML*{Q{M;+^4I#5bh#c)xDGaIqWc#ka=0fh*_Hlu%wt1rBv$B z%80@8%MhIwa0Zw$1`D;Uj1Bq`lsdI^g_18yZ9XUz2-u6&{?Syd zHGEh-3~HH-vO<)_2^r|&$(q7wG{@Q~un=3)Nm``&2T99L(P+|aFtu1sTy+|gwL*{z z)WoC4rsxoWhz0H$rG|EwhDT z0zcOAod_k_Ql&Y`YV!#&Mjq{2ln|;LMuF$-G#jX_2~oNioTHb4GqFatn@?_KgsA7T z(ouy$cGKa!m}6$=C1Wmb;*O2p*@g?wi-}X`v|QA4bNDU*4(y8*jZy-Ku)S3iBN(0r ztfLyPLfEPqj6EV}xope=?b0Nyf*~vDz-H-Te@B`{ib?~F<*(MmG+8zoYS77$O*3vayg#1kkKN+Bu9J9;Soev<%2S&J zr8*_PKV4|?RVfb#SfNQ;TZC$8*9~@GR%xFl1 z3MD?%`1PxxupvVO>2w#8*zV<-!m&Lis&B>)pHahPQ@I_;rY~Z$1+!4V1jde&L8y0! zha7@F+rOENF{~0$+a~oId0R|_!PhO=8)$>LcO)ca6YeOQs?ZG;`4O`x=Pd??Bl?Qf zgkaNj7X5@3_==zlQ-u6?omteA!_e-6gfDtw6CBnP2o1wo-7U!Y@89rU1HFb|bIr!I z=qIz=AW(}L^m z=I9RiS{DRtTYS6jsnvt1zs)W;kSVFOK|WMyZ@dxs+8{*W9-aTmS79J4R{Cis>EIqS zw+~gJqwz)(!z>)KDyhS{lM*xQ-8mNvo$A=IwGu+iS564tgX`|MeEuis!aN-=7!L&e zhNs;g1MBqDyx{y@AI&{_)+-?EEg|5C*!=OgD#$>HklRVU+R``HYZZq5{F9C0KKo!d z$bE2XC(G=I^YUxYST+Hk>0T;JP_iAvCObcrPV1Eau865w6d^Wh&B?^#h2@J#!M2xp zLGAxB^i}4D2^?RayxFqBgnZ-t`j+~zVqr+9Cz9Rqe%1a)c*keP#r54AaR2*TH^}7j zmJ48DN);^{7+5|+GmbvY2v#qJy>?$B(lRlS#kyodlxA&Qj#9-y4s&|eq$5} zgI;4u$cZWKWj`VU%UY#SH2M$8?PjO-B-rNPMr=8d=-D(iLW#{RWJ}@5#Z#EK=2(&LvfW&{P4_jsDr^^rg9w#B7h`mBwdL9y)Ni;= zd$jFDxnW7n-&ptjnk#<0zmNNt{;_30vbQW!5CQ7SuEjR1be!vxvO53!30iOermrU1 zXhXaen8=4Q(574KO_h$e$^1khO&tQL59=)Dc^8iPxz8+tC3`G$w|yUzkGd%Wg4(3u zJ<&7r^HAaEfG?F8?2I64j4kPpsNQk7qBJa9_hFT;*j;A%H%;QI@QWqJaiOl=;u>G8 zG`5Ow4K5ifd=OS|7F;EFc1+GzLld0RCQxG>Fn?~5Wl5VHJ=$DeR-2zwBgzSrQsGG0 zBqrILuB+_SgLxh~S~^QNHWW(2P;Z?d!Rd1lnEM=z23xPzyrbO_L0k43zruDkrJO*D zlzN(peBMLji`xfgYUirul-7c#3t(*=x6A^KSU-L|$(0pp9A*43#=Q!cu%9ZHP!$J| zSk8k=Z8cl811Vvn(4p8xx+EdKQV(sjC4_mEvlWeuIfwEVcF2LiC{H!oW)LSW=0ul| zT?$5PCc(pf-zKzUH`p7I7coVvCK;Dv-3_c?%~bPz`#ehbfrSrFf{RAz0I5e*W1S)kTW{0gf5X2v2k=S=W{>pr44tQ?o` zih8gE29VGR_SL~YJtcA)lRLozPg!<3Mh(`Hp)5{bclb)reTScXzJ>7{?i^yR@{(^% z#=$BYXPIX%fhgsofP-T`3b<5#V(TTS)^$vlhV&Kn=(LXOTAADIR1v8UqmW5c`n`S% zC8SOW$e?>&0dwKD%Jt{+67PfCLnqX0{8K^(q_^^2#puPYPkJsyXWMa~?V?p5{flYi z-1!uqI2x%puPG)r7b8y+Pc0Z5C%aA6`Q1_?W9k!YbiVVJVJwGLL?)P0M&vo{^IgEE zrX3eTgrJl_AeXYmiciYX9OP?NPN%-7Ji%z3U`-iXX=T~OI0M=ek|5IvIsvXM$%S&v zKw{`Kj(JVc+Pp^?vLKEyoycfnk)Hd>et78P^Z*{#rBY~_>V7>{gtB$0G99nbNBt+r zyXvEg_2=#jjK+YX1A>cj5NsFz9rjB_LB%hhx4-2I73gr~CW_5pD=H|e`?#CQ2)p4& z^v?Dlxm-_j6bO5~eeYFZGjW3@AGkIxY=XB*{*ciH#mjQ`dgppNk4&AbaRYKKY-1CT z>)>?+ME)AcCM7RRZQsH5)db7y!&jY-qHp%Ex9N|wKbN$!86i>_LzaD=f4JFc6Dp(a z%z>%=q(sXlJ=w$y^|tcTy@j%AP`v1n0oAt&XC|1kA`|#jsW(gwI0vi3a_QtKcL+yh z1Y=`IRzhiUvKeZXH6>>TDej)?t_V8Z7;WrZ_7@?Z=HRhtXY+{hlY?x|;7=1L($?t3 z6R$8cmez~LXopZ^mH9=^tEeAhJV!rGGOK@sN_Zc-vmEr;=&?OBEN)8aI4G&g&gdOb zfRLZ~dVk3194pd;=W|Z*R|t{}Evk&jw?JzVERk%JNBXbMDX82q~|bv%!2%wFP9;~-H?={C1sZ( zuDvY5?M8gGX*DyN?nru)UvdL|Rr&mXzgZ;H<^KYvzIlet!aeFM@I?JduKj=!(+ zM7`37KYhd*^MrKID^Y1}*sZ#6akDBJyKna%xK%vLlBqzDxjQ3}jx8PBOmXkvf@B{@ zc#J;~wQ<6{B;``j+B!#7s$zONYdXunbuKvl@zvaWq;`v2&iCNF2=V9Kl|77-mpCp= z2$SxhcN=pZ?V{GW;t6s)?-cNPAyTi&8O0QMGo#DcdRl#+px!h3ayc*(VOGR95*Anj zL0YaiVN2mifzZ){X+fl`Z^P=_(W@=*cIe~BJd&n@HD@;lRmu8cx7K8}wPbIK)GjF> zQGQ2h#21o6b2FZI1sPl}9_(~R|2lE^h}UyM5A0bJQk2~Vj*O)l-4WC4$KZ>nVZS|d zZv?`~2{uPYkc?254B9**q6tS|>We?uJ&wK3KIww|zzSuj>ncI4D~K z1Y6irVFE{?D-|R{!rLhZxAhs+Ka9*-(ltIUgC;snNek4_5xhO}@+r9Sl*5=7ztnXO zAVZLm$Kdh&rqEtdxxrE9hw`aXW1&sTE%aJ%3VL3*<7oWyz|--A^qvV3!FHBu9B-Jj z4itF)3dufc&2%V_pZsjUnN=;s2B9<^Zc83>tzo)a_Q$!B9jTjS->%_h`ZtQPz@{@z z5xg~s*cz`Tj!ls3-hxgnX}LDGQp$t7#d3E}>HtLa12z&06$xEQfu#k=(4h{+p%aCg zzeudlLc$=MVT+|43#CXUtRR%h5nMchy}EJ;n7oHfTq6wN6PoalAy+S~2l}wK;qg9o zcf#dX>ke;z^13l%bwm4tZcU1RTXnDhf$K3q-cK576+TCwgHl&?9w>>_(1Gxt@jXln zt3-Qxo3ITr&sw1wP%}B>J$Jy>^-SpO#3e=7iZrXCa2!N69GDlD{97|S*og)3hG)Lk zuqxK|PkkhxV$FP45%z*1Z?(LVy+ruMkZx|(@1R(0CoS6`7FWfr4-diailmq&Q#ehn zc)b&*&Ub;7HRtFVjL%((d$)M=^6BV@Kiusmnr1_2&&aEGBpbK7OWs;+(`tRLF8x?n zfKJB3tB^F~N`_ak3^exe_3{=aP)3tuuK2a-IriHcWv&+u7p z_yXsd6kyLV@k=(QoSs=NRiKNYZ>%4wAF;2#iu1p^!6>MZUPd;=2LY~l2ydrx10b#OSAlltILY%OKTp{e{ zzNogSk~SJBqi<_wRa#JqBW8Ok=6vb%?#H(hG}Dv98{JST5^SSh>_GQ@UK-0J`6l#E za}X#ud0W?cp-NQE@jAx>NUv65U~%YYS%BC0Cr$5|2_A)0tW;(nqoGJUHG5R`!-{1M-4T{<^pOE!Dvyuu1x7?Wt#YIgq zA$Vwj`St+M#ZxJXXGkepIF6`xL&XPu^qiFlZcX+@fOAdQ9d(h{^xCiAWJ0Ixp~3&E z(WwdT$O$7ez?pw>Jf{`!T-205_zJv+y~$w@XmQ;CiL8d*-x_z~0@vo4|3xUermJ;Q z9KgxjkN8Vh)xZ2xhX0N@{~@^d@BLoYFW%Uys83=`15+YZ%KecmWXjVV2}YbjBonSh zVOwOfI7^gvlC~Pq$QDHMQ6_Pd10OV{q_Zai^Yg({5XysuT`3}~3K*8u>a2FLBQ%#_YT6$4&6(?ZGwDE*C-p8>bM?hj*XOIoj@C!L5) zH1y!~wZ^dX5N&xExrKV>rEJJjkJDq*$K>qMi`Lrq08l4bQW~!Fbxb>m4qMHu6weTiV6_9(a*mZ23kr9AM#gCGE zBXg8#m8{ad@214=#w0>ylE7qL$4`xm!**E@pw484-VddzN}DK2qg&W~?%hcv3lNHx zg(CE<2)N=p!7->aJ4=1*eB%fbAGJcY65f3=cKF4WOoCgVelH$qh0NpIka5J-6+sY* zBg<5!R=I*5hk*CR@$rY6a8M%yX%o@D%{q1Jn=8wAZ;;}ol>xFv5nXvjFggCQ_>N2} zXHiC~pCFG*oEy!h_sqF$^NJIpQzXhtRU`LR0yU;MqrYUG0#iFW4mbHe)zN&4*Wf)G zV6(WGOq~OpEoq##E{rC?!)8ygAaAaA0^`<8kXmf%uIFfNHAE|{AuZd!HW9C^4$xW; zmIcO#ti!~)YlIU4sH(h&s6}PH-wSGtDOZ+%H2gAO(%2Ppdec9IMViuwwWW)qnqblH9xe1cPQ@C zS4W|atjGDGKKQAQlPUVUi1OvGC*Gh2i&gkh0up%u-9ECa7(Iw}k~0>r*WciZyRC%l z7NX3)9WBXK{mS|=IK5mxc{M}IrjOxBMzFbK59VI9k8Yr$V4X_^wI#R^~RFcme2)l!%kvUa zJ{zpM;;=mz&>jLvON5j>*cOVt1$0LWiV>x)g)KKZnhn=%1|2E|TWNfRQ&n?vZxQh* zG+YEIf33h%!tyVBPj>|K!EB{JZU{+k`N9c@x_wxD7z~eFVw%AyU9htoH6hmo0`%kb z55c#c80D%0^*6y|9xdLG$n4Hn%62KIp`Md9Jhyp8)%wkB8<%RlPEwC&FL z;hrH(yRr(Ke$%TZ09J=gGMC3L?bR2F4ZU!}pu)*8@l(d9{v^^(j>y+GF*nGran5*M z{pl5ig0CVsG1etMB8qlF4MDFRkLAg4N=l{Sc*F>K_^AZQc{dSXkvonBI)qEN1*U&? zKqMr?Wu)q9c>U~CZUG+-ImNrU#c`bS?RpvVgWXqSsOJrCK#HNIJ+k_1Iq^QNr(j|~ z-rz67Lf?}jj^9Ik@VIMBU2tN{Ts>-O%5f?=T^LGl-?iC%vfx{}PaoP7#^EH{6HP!( zG%3S1oaiR;OmlKhLy@yLNns`9K?60Zg7~NyT0JF(!$jPrm^m_?rxt~|J2)*P6tdTU z25JT~k4RH9b_1H3-y?X4=;6mrBxu$6lsb@xddPGKA*6O`Cc^>Ul`f9c&$SHFhHN!* zjj=(Jb`P}R%5X@cC%+1ICCRh1^G&u548#+3NpYTVr54^SbFhjTuO-yf&s%r4VIU!lE!j(JzHSc9zRD_fw@CP0pkL(WX6 zn+}LarmQP9ZGF9So^+jr<(LGLlOxGiCsI^SnuC{xE$S;DA+|z+cUk=j^0ipB(WTZ} zR0osv{abBd)HOjc(SAV&pcP@37SLnsbtADj?bT#cPZq|?W1Ar;4Vg5m!l{@{TA~|g zXYOeU`#h-rT@(#msh%%kH>D=`aN}2Rysez?E@R6|@SB(_gS0}HC>83pE`obNA9vsH zSu^r>6W-FSxJA}?oTuH>-y9!pQg|*<7J$09tH=nq4GTx+5($$+IGlO^bptmxy#=)e zuz^beIPpUB_YK^?eb@gu(D%pJJwj3QUk6<3>S>RN^0iO|DbTZNheFX?-jskc5}Nho zf&1GCbE^maIL$?i=nXwi)^?NiK`Khb6A*kmen^*(BI%Kw&Uv4H;<3ib-2UwG{7M&* zn$qyi8wD9cKOuxWhRmFupwLuFn!G5Vj6PZ#GCNJLlTQuQ?bqAYd7Eva5YR~OBbIim zf(6yXS4pei1Bz4w4rrB6Ke~gKYErlC=l9sm*Zp_vwJe7<+N&PaZe|~kYVO%uChefr%G4-=0eSPS{HNf=vB;p~ z5b9O1R?WirAZqcdRn9wtct>$FU2T8p=fSp;E^P~zR!^C!)WHe=9N$5@DHk6(L|7s@ zcXQ6NM9Q~fan1q-u8{ez;RADoIqwkf4|6LfsMZK6h{ZUGYo>vD%JpY<@w;oIN-*sK zxp4@+d{zxe>Z-pH#_)%|d(AC`fa!@Jq)5K8hd71!;CEG|ZI{I2XI`X~n|ae;B!q{I zJDa#T+fRviR&wAN^Sl{z8Ar1LQOF&$rDs18h0{yMh^pZ#hG?c5OL8v07qRZ-Lj5(0 zjFY(S4La&`3IjOT%Jqx4z~08($iVS;M10d@q~*H=Py)xnKt(+G-*o33c7S3bJ8cmwgj45` zU|b7xCoozC!-7CPOR194J-m9N*g`30ToBo!Io?m>T)S{CusNZx0J^Hu6hOmvv;0~W zFHRYJgyRhP1sM_AQ%pkD!X-dPu_>)`8HunR4_v$4T78~R<})-@K2LBt03PBLnjHzuYY)AK?>0TJe9 zmmOjwSL%CTaLYvYlJ~|w?vc*R+$@vEAYghtgGhZ2LyF+UdOn+v^yvD9R%xbU$fUjK{{VQ4VL&&UqAFa>CZuX4kX zJ)njewLWfKXneB+r}Y$`ezzwDoRT3r{9(@=I3-z>8tT)n3whDyi(r*lAnxQJefj_x z-8lc=r!Vua{b}v;LT)oXW>~6Q03~RAp~R}TZq9sGbeUBMS)?ZrJqiu|E&ZE)uN1uL zXcAj3#aEz zzbcCF)+;Hia#OGBvOatkPQfE{*RtBlO1QFVhi+3q0HeuFa*p+Dj)#8Mq9yGtIx%0A znV5EmN(j!&b%kNz4`Vr-)mX_?$ng&M^a6loFO(G3SA!~eBUEY!{~>C|Ht1Q4cw)X5~dPiEYQJNg?B2&P>bU7N(#e5cr8qc7A{a7J9cdMcRx)N|?;$L~O|E)p~ zIC}oi3iLZKb>|@=ApsDAfa_<$0Nm<3nOPdr+8Y@dnb|u2S<7CUmTGKd{G57JR*JTo zb&?qrusnu}jb0oKHTzh42P00C{i^`v+g=n|Q6)iINjWk4mydBo zf0g=ikV*+~{rIUr%MXdz|9ebUP)<@zR8fgeR_rChk0<^^3^?rfr;-A=x3M?*8|RPz z@}DOF`aXXuZGih9PyAbp|DULSw8PJ`54io)ga6JG@Hgg@_Zo>OfJ)8+TIfgqu%877 z@aFykK*+|%@rSs-t*oAzH6Whyr=TpuQ}B0ptSsMg9p8@ZE5A6LfMk1qdsf8T^zkdC3rUhB$`s zBdanX%L3tF7*YZ4^A8MvOvhfr&B)QOWCLJ^02kw5;P%n~5e`sa6MG{E2N^*2ZX@ge zI2>ve##O?I}sWX)UqK^_bRz@;5HWp5{ziyg?QuEjXfMP!j zpr(McSAQz>ME?M-3NSoCn$91#_iNnULp6tD0NN7Z0s#G~-~xWZFWN-%KUVi^yz~-` zn;AeGvjLJ~{1p#^?$>zM4vu=3mjBI$(_tC~NC0o@6<{zS_*3nGfUsHr3Gdgn%XedF zQUP=j5Mb>9=#f7aPl;cm$=I0u*WP}aVE!lCYw2Ht{Z_j9mp1h>dHGKkEZP6f^6O@J zndJ2+rWjxp|3#<2oO=8v!oHMX{|Vb|^G~pU_A6=ckBQvt>o+dpgYy(D=VCj65GE&jJj{&-*iq?z)PHNee&-@Mie~#LD*={ex8h(-)<@|55 zUr(}L?mz#;d|mrD%zrh<-*=;5*7K$B`zPjJ%m2pwr*G6tf8tN%a

_x$+l{{cH8$W#CT literal 0 HcmV?d00001 diff --git a/embrace-android-sdk/gradle/wrapper/gradle-wrapper.properties b/embrace-android-sdk/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..3442499c52 --- /dev/null +++ b/embrace-android-sdk/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Sep 24 14:41:58 PDT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/embrace-android-sdk/gradlew b/embrace-android-sdk/gradlew new file mode 100644 index 0000000000..cccdd3d517 --- /dev/null +++ b/embrace-android-sdk/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/embrace-android-sdk/gradlew.bat b/embrace-android-sdk/gradlew.bat new file mode 100644 index 0000000000..f9553162f1 --- /dev/null +++ b/embrace-android-sdk/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/embrace-android-sdk/lint-baseline.xml b/embrace-android-sdk/lint-baseline.xml new file mode 100644 index 0000000000..0458e5d1b7 --- /dev/null +++ b/embrace-android-sdk/lint-baseline.xml @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/embrace-android-sdk/proguard-rules.pro b/embrace-android-sdk/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/embrace-android-sdk/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/embrace-android-sdk/src/androidTest/AndroidManifest.xml b/embrace-android-sdk/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..86a482773c --- /dev/null +++ b/embrace-android-sdk/src/androidTest/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/expected-webview-core-vital.json b/embrace-android-sdk/src/androidTest/assets/golden-files/expected-webview-core-vital.json new file mode 100644 index 0000000000..d2e3900564 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/expected-webview-core-vital.json @@ -0,0 +1,22 @@ +{ + "ts": 1111, + "u": "https://embrace.io/", + "vt": [ + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "CLS", + "st": 1111, + "d": 10, + "s": 0.1 + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "LCP", + "st": 2222, + "d": 10, + "s": 0 + } + ] +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-event.json new file mode 100644 index 0000000000..d7b75e029b --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-event.json @@ -0,0 +1,36 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "id": "__EMBRACE_TEST_IGNORE__", + "et": "none", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Test log error", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "error" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-and-message-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-and-message-event.json new file mode 100644 index 0000000000..4db0d9edb7 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-and-message-event.json @@ -0,0 +1,40 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "pr": { + }, + "id": "__EMBRACE_TEST_IGNORE__", + "em": "Exception message", + "en": "NullPointerException", + "f": 1, + "et": "handled", + "li": "__EMBRACE_TEST_IGNORE__", + "n": "log message", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "error" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-event.json new file mode 100644 index 0000000000..3c60b278f6 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-exception-event.json @@ -0,0 +1,38 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "id": "__EMBRACE_TEST_IGNORE__", + "et": "handled", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Another log error", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "error", + "en": "Exception", + "em": "Another log error" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-property-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-property-event.json new file mode 100644 index 0000000000..214cd51887 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-error-with-property-event.json @@ -0,0 +1,39 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "pr": { + "error": "test property" + }, + "id": "__EMBRACE_TEST_IGNORE__", + "et": "none", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Test log error", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "error" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-handled-exception.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-handled-exception.json new file mode 100644 index 0000000000..31f842811a --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-handled-exception.json @@ -0,0 +1,41 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "pr": { + "error": "test property" + }, + "id": "__EMBRACE_TEST_IGNORE__", + "em": "Exception message", + "en": "NullPointerException", + "et": "none", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Exception message", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "error" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-event.json new file mode 100644 index 0000000000..36f1a5afe5 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-event.json @@ -0,0 +1,36 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "id": "__EMBRACE_TEST_IGNORE__", + "et": "none", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Test log info", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "info" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-fail-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-fail-event.json new file mode 100644 index 0000000000..802821b373 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-fail-event.json @@ -0,0 +1,36 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "id": "__EMBRACE_TEST_IGNORE__", + "et": "none", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Test log info fail", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "info" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-with-property-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-with-property-event.json new file mode 100644 index 0000000000..5033b9d2c3 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-info-with-property-event.json @@ -0,0 +1,39 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "pr": { + "info": "test property" + }, + "id": "__EMBRACE_TEST_IGNORE__", + "et": "none", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Test log info with property", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "info" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/log-warning-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/log-warning-event.json new file mode 100644 index 0000000000..6009f3d8e6 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/log-warning-event.json @@ -0,0 +1,36 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "id": "__EMBRACE_TEST_IGNORE__", + "et": "none", + "f": 1, + "li": "__EMBRACE_TEST_IGNORE__", + "n": "Test log warning", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "warning" + }, + "sk": { + "tt": "__EMBRACE_TEST_IGNORE__" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-end-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-end-event.json new file mode 100644 index 0000000000..dcfd105be8 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-end-event.json @@ -0,0 +1,16 @@ +{ + "et": { + "st": "active", + "du": "__EMBRACE_TEST_IGNORE__", + "id": "__EMBRACE_TEST_IGNORE__", + "n": "my_moment", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "end" + }, + "p": "__EMBRACE_TEST_IGNORE__", + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-start-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-start-event.json new file mode 100644 index 0000000000..bb53e9f3d4 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-start-event.json @@ -0,0 +1,31 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "id": "__EMBRACE_TEST_IGNORE__", + "th": 5000, + "n": "my_moment", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "start" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-end-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-end-event.json new file mode 100644 index 0000000000..962c30075d --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-end-event.json @@ -0,0 +1,16 @@ +{ + "et": { + "st": "active", + "du": "__EMBRACE_TEST_IGNORE__", + "id": "__EMBRACE_TEST_IGNORE__", + "n": "my_moment", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "late" + }, + "p": "__EMBRACE_TEST_IGNORE__", + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-start-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-start-event.json new file mode 100644 index 0000000000..a942ec9d05 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-custom-with-properties-start-event.json @@ -0,0 +1,35 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "pr": { + "key1": "value1", + "key2": "value2" + }, + "id": "__EMBRACE_TEST_IGNORE__", + "th": 5000, + "n": "my_moment", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "start" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-end-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-end-event.json new file mode 100644 index 0000000000..d697046940 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-end-event.json @@ -0,0 +1,16 @@ +{ + "et": { + "st": "active", + "du": "__EMBRACE_TEST_IGNORE__", + "id": "__EMBRACE_TEST_IGNORE__", + "n": "_startup", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "end" + }, + "p": "__EMBRACE_TEST_IGNORE__", + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-late-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-late-event.json new file mode 100644 index 0000000000..265ea6c88a --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-late-event.json @@ -0,0 +1,16 @@ +{ + "et": { + "st": "active", + "du": "__EMBRACE_TEST_IGNORE__", + "id": "__EMBRACE_TEST_IGNORE__", + "n": "_startup", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "late" + }, + "p": "__EMBRACE_TEST_IGNORE__", + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-start-event.json b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-start-event.json new file mode 100644 index 0000000000..cd84bacd53 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/moment-startup-start-event.json @@ -0,0 +1,31 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "et": { + "st": "active", + "id": "__EMBRACE_TEST_IGNORE__", + "th": 5000, + "n": "_startup", + "sc": false, + "si": "__EMBRACE_TEST_IGNORE__", + "sp": {}, + "ts": "__EMBRACE_TEST_IGNORE__", + "t": "start" + }, + "u": "__EMBRACE_TEST_IGNORE__", + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/session-end.json b/embrace-android-sdk/src/androidTest/assets/golden-files/session-end.json new file mode 100644 index 0000000000..64d4b8e39b --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/session-end.json @@ -0,0 +1,109 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "br": { + "cb": "__EMBRACE_TEST_IGNORE__", + "cv": [], + "rna": [], + "pn": [], + "tb": [], + "vb": [ + { + "vn": "io.embrace.android.embracesdk.internal.MockActivity", + "st": "__EMBRACE_TEST_IGNORE__" + } + ], + "wv": [] + }, + "d": "__EMBRACE_TEST_IGNORE__", + "p": { + "nr": { + "v2": { + "c": {}, + "r": [] + } + }, + "anr": [], + "anr_pe": [], + "ds": { + "as": "__EMBRACE_TEST_IGNORE__", + "fs": "__EMBRACE_TEST_IGNORE__" + }, + "aei": "__EMBRACE_TEST_IGNORE__", + "ga": [], + "mw": [], + "ns": "__EMBRACE_TEST_IGNORE__", + "lp": "__EMBRACE_TEST_IGNORE__" + }, + "s": { + "as": "__EMBRACE_TEST_IGNORE__", + "ty": "__EMBRACE_TEST_IGNORE__", + "bf": "__EMBRACE_TEST_IGNORE__", + "cs": true, + "et": "__EMBRACE_TEST_IGNORE__", + "em": "s", + "ce": true, + "el": [], + "lec": 0, + "ss": "__EMBRACE_TEST_IGNORE__", + "il": [], + "lic": 0, + "ht": "__EMBRACE_TEST_IGNORE__", + "sn": "__EMBRACE_TEST_IGNORE__", + + "sp": {}, + "si": "__EMBRACE_TEST_IGNORE__", + "id": "__EMBRACE_TEST_IGNORE__", + "st": "__EMBRACE_TEST_IGNORE__", + "sm": "s", + "sd": "__EMBRACE_TEST_IGNORE__", + "sdt": "__EMBRACE_TEST_IGNORE__", + "ue": 0, + "lwc": 0, + "nc": [], + "wl": [], + "wvi_beta": [ + { + "ts": 1111, + "t": "myWebView", + "u": "https://embrace.io/", + "vt": [ + { + "d": 10, + "n": "layout-shift", + "s": 0.0, + "st": 2222, + "t": "LCP" + }, + { + "d": 10, + "n": "layout-shift", + "s": 0.1, + "st": 1111, + "t": "CLS" + } + ] + } + ] + }, + "u": { + "id": "some id", + "em": "user@email.com", + "un": "John Doe", + "per": ["first_day"] + }, + "v": 13 +} diff --git a/embrace-android-sdk/src/androidTest/assets/golden-files/session-start.json b/embrace-android-sdk/src/androidTest/assets/golden-files/session-start.json new file mode 100644 index 0000000000..403e600e1d --- /dev/null +++ b/embrace-android-sdk/src/androidTest/assets/golden-files/session-start.json @@ -0,0 +1,30 @@ +{ + "a": { + "f": 1, + "vu": false, + "vul": false, + "v": "1.1.2", + "fl": "default test build flavor", + "bi": "default test build id", + "bt": "default test build type", + "bv": "5", + "e": "dev", + "ou": false, + "oul": false, + "sdc": "53", + "sdk": "__EMBRACE_TEST_IGNORE__" + }, + "d": "__EMBRACE_TEST_IGNORE__", + "s": { + "as": "__EMBRACE_TEST_IGNORE__", + "ty":"__EMBRACE_TEST_IGNORE__", + "cs": true, + "sn": "__EMBRACE_TEST_IGNORE__", + + "sp": {}, + "id": "__EMBRACE_TEST_IGNORE__", + "st": "__EMBRACE_TEST_IGNORE__", + "sm": "s" + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/AnrIntegrationTest.kt b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/AnrIntegrationTest.kt new file mode 100644 index 0000000000..ab3420d4d4 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/AnrIntegrationTest.kt @@ -0,0 +1,315 @@ +package io.embrace.android.embracesdk + +import android.os.Handler +import android.os.Looper +import android.util.Log +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.payload.AnrInterval +import io.embrace.android.embracesdk.payload.AnrSample +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.payload.ThreadInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPInputStream +import okhttp3.mockwebserver.RecordedRequest + +// number of intervals we create in the test +private const val EXPECTED_INTERVALS = 6 + +// we allow for extra or missing samples to account for natural differences in thread scheduling +private const val SAMPLE_TOLERANCE = 12 + +// allow some tolerance for how long an ANR interval lasts +private const val INTERVAL_DURATION_TOLERANCE = 500 + +// allow the SDK to initialize first +private const val SDK_INIT_TOLERANCE_MS = 1000L + +// gap between intervals we trigger in the test case +private const val INTERVAL_GAP_MS = 1000L + +// how long we wait for the test to complete before aborting +private const val TEST_TIMEOUT_SECS = 60L + +// default config for sampling interval in ms +private const val SAMPLE_INTERVAL_MS = 100 + +// default threshold for creating an ANR interval +private const val ANR_THRESHOLD_MS = SAMPLE_INTERVAL_MS * 10 + +// maximum number of samples capture +private const val MAX_SAMPLES = 80 + +private data class ExpectedIntervalData( + val intervalCode: Int, + val sampleCode: Int, + val expectedSamples: Int, + val expectedMethods: List +) { + val expectedDuration = (expectedSamples * SAMPLE_INTERVAL_MS) + ANR_THRESHOLD_MS +} + +private val firstInterval = ExpectedIntervalData( + AnrInterval.CODE_DEFAULT, + AnrSample.CODE_DEFAULT, + 100, + listOf( + "io.embrace.android.embracesdk.AnrIntegrationTest.sleepThreeSeconds", + "io.embrace.android.embracesdk.AnrIntegrationTest.sleepTwoSeconds", + "io.embrace.android.embracesdk.AnrIntegrationTest.sleepOneSecond", + "io.embrace.android.embracesdk.AnrIntegrationTest.sleepFiveSeconds" + ) +) + +private val secondInterval = ExpectedIntervalData( + AnrInterval.CODE_SAMPLES_CLEARED, + AnrSample.CODE_DEFAULT, + 10, + listOf( + "io.embrace.android.embracesdk.AnrIntegrationTest.sleepTwoSeconds" + ) +) + +private val thirdInterval = ExpectedIntervalData( + AnrInterval.CODE_DEFAULT, + AnrSample.CODE_DEFAULT, + 20, + listOf( + "io.embrace.android.embracesdk.AnrIntegrationTest.sleepThreeSeconds" + ) +) + +private val fourthInterval = thirdInterval.copy() +private val fifthInterval = thirdInterval.copy() +private val sixthInterval = thirdInterval.copy() + +internal class AnrIntegrationTest : BaseTest() { + + private val handler = Handler(Looper.getMainLooper()) + private lateinit var latch: CountDownLatch + private val serializer = EmbraceSerializer() + + @Before + fun setup() { + latch = CountDownLatch(EXPECTED_INTERVALS) + startEmbraceInForeground() + Embrace.getImpl().endAppStartup(null) + } + + private fun readBodyAsSessionMessage(request: RecordedRequest): SessionMessage { + val stream = request.body.inputStream() + GZIPInputStream(stream).bufferedReader().use { + return gson.fromJson(it, SessionMessage::class.java) + } + } + + /** + * Verifies that a session end message is sent and contains ANR information. The + * test triggers 6 ANR intervals by blocking the main thread, with a gap in between + * intervals. This covers a variety of scenarios: + * + * - Exceeding max ANR samples for one interval + * - Exceeding max ANR intervals for session + * - Validates that timestamps are correct & that samples broadly contain the expected + * information. I've used a 'tolerance' for most of these assertions because the number + * of samples/thread traces will vary on each test run + * - Added functions to deserialize received session payloads to allow asserting against the received JSON + * - Bumped max wait for pauseLatch as I noticed a few timeouts when running locally + */ + @Test + fun testAnrIntervalsInSessionEndMessage() { + startAnrIntervals() + + // wait a reasonable time period before assuming the test is deadlocked + latch.await(TEST_TIMEOUT_SECS, TimeUnit.SECONDS) + + // trigger a session + sendBackground() + + // ignore startup moment end request that is validated in other tests + waitForRequest() + + // validate ANRs with JUnit assertions rather than golden file + waitForRequest { request -> + val payload = readBodyAsSessionMessage(request) + assertNotNull(payload) + val perfInfo by lazy { serializer.toJson(payload.performanceInfo) } + val intervals = checkNotNull(payload.performanceInfo?.anrIntervals) { + "No ANR intervals in payload. p=$perfInfo" + } + assertEquals( + "Unexpected number of intervals. $perfInfo", + EXPECTED_INTERVALS, + intervals.size + ) + + validateInterval(0, intervals, firstInterval) + validateInterval(1, intervals, secondInterval) + validateInterval(2, intervals, thirdInterval) + validateInterval(3, intervals, fourthInterval) + validateInterval(4, intervals, fifthInterval) + validateInterval(5, intervals, sixthInterval) + } + } + + private fun validateInterval( + index: Int, + intervals: List, + data: ExpectedIntervalData + ) { + val interval = intervals[index] + val errMsg: String by lazy { + "Assertion failed for interval $index. ${serializer.toJson(intervals)}" + } + + // validate interval code + assertEquals(errMsg, data.intervalCode, interval.code) + + // validate interval type + assertEquals(errMsg, AnrInterval.Type.UI, interval.type) + + // validate interval lastKnownTime is null + assertNull(errMsg, interval.lastKnownTime) + + // validate the duration (calculated via startTime/endTime) is around what we'd expect + val duration = interval.duration() + assertWithTolerance( + errMsg, + data.expectedDuration, + duration.toInt(), + INTERVAL_DURATION_TOLERANCE + ) + + if (interval.code != AnrInterval.CODE_SAMPLES_CLEARED) { + validateSamples(interval, index, errMsg, data) + } + } + + private fun validateSamples( + interval: AnrInterval, + index: Int, + errMsg: String, + data: ExpectedIntervalData + ) { + // validate there was roughly 1 sample every 100ms + val samples = checkNotNull(interval.anrSampleList?.samples) { + "Interval $index was missing samples completely. $errMsg\ninterval=${ + serializer.toJson( + interval + ) + }" + } + assertWithTolerance(errMsg, data.expectedSamples, samples.size, SAMPLE_TOLERANCE) + + // validate the samples all recorded their overhead + assertTrue(errMsg, samples.all { checkNotNull(it.sampleOverheadMs) >= 0 }) + + // validate that all timestamps are ascending + assertTrue(errMsg, samples == samples.sortedBy(AnrSample::timestamp)) + + // validate the samples have the expected code + if (data.expectedSamples <= MAX_SAMPLES) { + assertTrue(errMsg, samples.all { it.code == data.sampleCode }) + } else { + val withStacktraces = samples.count { it.code == data.sampleCode } + val withoutStacktraces = + samples.count { it.code == AnrSample.CODE_SAMPLE_LIMIT_REACHED } + + assertWithTolerance(errMsg, MAX_SAMPLES, withStacktraces, SAMPLE_TOLERANCE) + assertWithTolerance( + errMsg, + data.expectedSamples - MAX_SAMPLES, + withoutStacktraces, + SAMPLE_TOLERANCE + ) + assertTrue( + errMsg, + samples.filter { it.code == AnrSample.CODE_SAMPLE_LIMIT_REACHED } + .all { it.threads == null }) + } + + // validate that threads contains the method names in the expected order + val threads: List> = samples.mapNotNull(AnrSample::threads) + val nonEmptyThreads: List> = threads + .filter(List::isNotEmpty) + .flatten() + .map { checkNotNull(it.lines) } + assertTrue(errMsg, nonEmptyThreads.size >= data.expectedMethods.size) + + data.expectedMethods.forEachIndexed { k, method -> + assertEquals( + errMsg, + 1, + nonEmptyThreads[k].count { it.startsWith(method) }) + } + } + + private fun startAnrIntervals() { + handler.postDelayed(Runnable { + Log.i("Embrace", "Starting first ANR interval") + sleepThreeSeconds() + sleepTwoSeconds() + sleepOneSecond() + sleepFiveSeconds() + latch.countDown() + scheduleNextMainThreadWork { produceSecondAnrInterval() } + }, SDK_INIT_TOLERANCE_MS) + } + + private fun produceSecondAnrInterval() { + Log.i("Embrace", "Starting second ANR interval") + sleepTwoSeconds() + latch.countDown() + scheduleNextMainThreadWork { produceThirdAnrInterval() } + } + + private fun produceThirdAnrInterval() { + Log.i("Embrace", "Starting third ANR interval") + sleepThreeSeconds() + latch.countDown() + scheduleNextMainThreadWork { produceFourthAnrInterval() } + } + + private fun produceFourthAnrInterval() { + Log.i("Embrace", "Starting fourth ANR interval") + sleepThreeSeconds() + latch.countDown() + scheduleNextMainThreadWork { produceFifthAnrInterval() } + } + + private fun produceFifthAnrInterval() { + Log.i("Embrace", "Starting fifth ANR interval") + sleepThreeSeconds() + latch.countDown() + scheduleNextMainThreadWork { produceSixthAnrInterval() } + } + + private fun produceSixthAnrInterval() { + Log.i("Embrace", "Starting sixth ANR interval") + sleepThreeSeconds() + latch.countDown() + } + + private fun scheduleNextMainThreadWork(action: () -> Unit) { + handler.looper.queue.addIdleHandler { + handler.postDelayed(action, INTERVAL_GAP_MS) + false + } + } + + private fun sleepOneSecond() = Thread.sleep(1000) + private fun sleepTwoSeconds() = Thread.sleep(2000) + private fun sleepThreeSeconds() = Thread.sleep(3000) + private fun sleepFiveSeconds() = Thread.sleep(5000) + + private fun assertWithTolerance(msg: String, expected: Int, observed: Int, tolerance: Int) { + val abs = kotlin.math.abs(expected - observed) + assertTrue("Expected $expected but got $observed. $msg", abs < tolerance) + } +} diff --git a/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/LogMessageTest.kt b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/LogMessageTest.kt new file mode 100644 index 0000000000..8c6e0a4a3c --- /dev/null +++ b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/LogMessageTest.kt @@ -0,0 +1,136 @@ +package io.embrace.android.embracesdk + +import com.google.gson.stream.JsonReader +import io.embrace.android.embracesdk.comms.delivery.DeliveryFailedApiCalls +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import java.io.File +import java.io.IOException + +@Suppress("DEPRECATION") +internal class LogMessageTest : BaseTest() { + + @Before + fun setup() { + startEmbraceInForeground() + } + + @After + fun tearDown() { + sendBackground() + } + + @Test + fun logInfoTest() { + Embrace.getInstance().logInfo("Test log info") + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "log-info-event.json") + } + } + + @Test + fun logInfoWithPropertyTest() { + val properties = HashMap() + properties["info"] = "test property" + + Embrace.getInstance().logMessage("Test log info with property", Severity.INFO, properties) + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "log-info-with-property-event.json") + } + } + + @Test + fun logInfoFailRequestTest() { + waitForFailedRequest( + endpoint = EmbraceEndpoint.LOGGING, + request = { Embrace.getInstance().logInfo("Test log info fail") }, + action = { + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "log-info-fail-event.json") + } + }, + validate = { file -> validateFileContent(file) } + ) + } + + private fun validateFileContent(file: File) { + try { + assertTrue(file.exists() && !file.isDirectory) + readFile(file, "/v1/log/logging") + val serializer = EmbraceSerializer() + file.bufferedReader().use { bufferedReader -> + JsonReader(bufferedReader).use { jsonreader -> + jsonreader.isLenient = true + val obj = serializer.loadObject(jsonreader, DeliveryFailedApiCalls::class.java) + if (obj != null) { + val failedCallFileName = obj.element().cachedPayload + assert(failedCallFileName.isNotBlank()) + readFileContent("Test log info fail", failedCallFileName) + } else { + fail("Null object") + } + } + } + } catch (e: IOException) { + fail("IOException error: ${e.message}") + } + } + + @Test + fun logErrorTest() { + Embrace.getInstance().logError("Test log error") + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "log-error-event.json") + } + } + + @Test + fun logErrorWithPropertyTest() { + val properties = HashMap() + properties["error"] = "test property" + + Embrace.getInstance().logMessage("Test log error", Severity.ERROR, properties) + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "log-error-with-property-event.json") + } + } + + @Test + fun logExceptionTest() { + Embrace.getInstance().logException(Exception("Another log error")) + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "log-error-with-exception-event.json") + } + } + + @Test + fun logErrorWithExceptionAndMessageTest() { + val exception = java.lang.NullPointerException("Exception message") + Embrace.getInstance().logException(exception, Severity.ERROR, mapOf(), "log message") + + waitForRequest { request -> + validateMessageAgainstGoldenFile( + request, + "log-error-with-exception-and-message-event.json" + ) + } + } + + @Test + fun logWarningTest() { + Embrace.getInstance().logWarning("Test log warning") + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "log-warning-event.json") + } + } +} diff --git a/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/MomentMessageTest.kt b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/MomentMessageTest.kt new file mode 100644 index 0000000000..737a6d0522 --- /dev/null +++ b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/MomentMessageTest.kt @@ -0,0 +1,126 @@ +package io.embrace.android.embracesdk + +import com.google.gson.stream.JsonReader +import io.embrace.android.embracesdk.comms.delivery.DeliveryFailedApiCalls +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import java.io.File +import java.io.IOException + +private const val MOMENT_NAME = "my_moment" + +internal class MomentMessageTest : BaseTest() { + + @Before + fun setup() { + startEmbraceInForeground() + } + + @After + fun tearDown() { + sendBackground() + } + + /** + * Verifies that a custom moment is sent by the SDK. + */ + @Test + fun customMomentTest() { + // Send start moment + Embrace.getInstance().startMoment(MOMENT_NAME) + + // Validate start moment request + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "moment-custom-start-event.json") + } + + // Send end moment + Embrace.getInstance().endMoment(MOMENT_NAME) + + // Validate end moment request + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "moment-custom-end-event.json") + } + } + + /** + * Verifies that a custom moment with properties is sent by the SDK. + */ + @Test + fun startMomentWithPropertiesTest() { + // ignore startup event + Embrace.getImpl().endAppStartup(null) + waitForRequest() + + val properties = HashMap() + properties["key1"] = "value1" + properties["key2"] = "value2" + + // Send start moment with properties + Embrace.getInstance().startMoment(MOMENT_NAME, MOMENT_NAME, properties) + + // Validate start moment request with properties + waitForRequest { request -> + validateMessageAgainstGoldenFile( + request, + "moment-custom-with-properties-start-event.json" + ) + } + + // Send end moment + Embrace.getInstance().endMoment(MOMENT_NAME) + + // Validate end moment request + waitForRequest { request -> + validateMessageAgainstGoldenFile( + request, + "moment-custom-with-properties-end-event.json" + ) + } + + } + + /** + * Verifies that a custom moment is sent by the SDK. + */ + @Test + fun customMomentFailRequestTest() { + waitForFailedRequest( + endpoint = EmbraceEndpoint.EVENTS, + request = { Embrace.getInstance().startMoment(MOMENT_NAME) }, + action = { + // Validate start moment request + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "moment-custom-start-event.json") + } + }, + validate = { file -> validateFileContent(file) }) + } + + private fun validateFileContent(file: File) { + try { + assertTrue(file.exists() && !file.isDirectory) + readFile(file, EmbraceEndpoint.EVENTS.url) + val serializer = EmbraceSerializer() + file.bufferedReader().use { bufferedReader -> + JsonReader(bufferedReader).use { jsonreader -> + jsonreader.isLenient = true + val obj = serializer.loadObject(jsonreader, DeliveryFailedApiCalls::class.java) + if (obj != null) { + val failedCallFileName = obj.element().cachedPayload + assert(failedCallFileName.isNotBlank()) + readFileContent("\"t\":\"start\"", failedCallFileName) + } else { + fail("Null object") + } + } + } + } catch (e: IOException) { + fail("IOException error: ${e.message}") + } + } +} diff --git a/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/SessionMessageTest.kt b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/SessionMessageTest.kt new file mode 100644 index 0000000000..86bdae0b5c --- /dev/null +++ b/embrace-android-sdk/src/androidTest/java/io/embrace/android/embracesdk/SessionMessageTest.kt @@ -0,0 +1,35 @@ +package io.embrace.android.embracesdk + +import org.junit.Before +import org.junit.Test + +internal class SessionMessageTest : BaseTest() { + + @Before + fun setup() { + startEmbraceInForeground() + } + + /** + * Verifies that a session end message is sent. + */ + @Test + fun sessionEndMessageTest() { + addCoreWebVitals() + sendBackground() + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "moment-startup-end-event.json") + } + + waitForRequest { request -> + validateMessageAgainstGoldenFile(request, "session-end.json") + } + } + + private fun addCoreWebVitals() { + val webViewExpectedLog = + mContext.assets.open("golden-files/${"expected-webview-core-vital.json"}").bufferedReader().readText() + Embrace.getInstance().trackWebViewPerformance("myWebView", webViewExpectedLog) + } +} diff --git a/embrace-android-sdk/src/androidTest/res/layout/web_view_activity.xml b/embrace-android-sdk/src/androidTest/res/layout/web_view_activity.xml new file mode 100644 index 0000000000..98cc24c7ad --- /dev/null +++ b/embrace-android-sdk/src/androidTest/res/layout/web_view_activity.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/embrace-android-sdk/src/androidTest/res/raw/mparticle_js_sdk b/embrace-android-sdk/src/androidTest/res/raw/mparticle_js_sdk new file mode 100644 index 0000000000..e69de29bb2 diff --git a/embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/NullParametersTest.java b/embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/NullParametersTest.java new file mode 100644 index 0000000000..f67880c740 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/NullParametersTest.java @@ -0,0 +1,323 @@ +package io.embrace.android.embracesdk; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static io.embrace.android.embracesdk.Embrace.NULL_PARAMETER_ERROR_MESSAGE_TEMPLATE; +import static io.embrace.android.embracesdk.assertions.InternalErrorAssertionsKt.assertInternalErrorLogged; + +import android.webkit.ConsoleMessage; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.SocketException; +import java.util.Map; + +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; +import io.embrace.android.embracesdk.spans.EmbraceSpan; +import io.embrace.android.embracesdk.spans.ErrorCode; + +/** + * TODO: add a lint rule to verify that all public API methods that have @NonNull parameters have a corresponding test here + */ +@SuppressWarnings("DataFlowIssue") +@RunWith(AndroidJUnit4.class) +public class NullParametersTest { + private static final SocketException EXCEPTION = new SocketException(); + + private static final String NULL_STRING = null; + + @Rule + public IntegrationTestRule testRule = new IntegrationTestRule(); + + @NonNull + private final Embrace embrace = testRule.getEmbrace(); + + @Before + public void before() { + assertTrue(embrace.isStarted()); + } + + @Test + public void testAddSessionProperty() { + embrace.addSessionProperty(null, "value", false); + assertError("addSessionProperty"); + embrace.addSessionProperty("key", null, false); + assertError("addSessionProperty"); + } + + @Test + public void testAddUserPersona() { + embrace.addUserPersona(null); + assertError("addUserPersona"); + } + + @Test + public void testClearUserPersona() { + embrace.clearUserPersona(null); + assertError("clearUserPersona"); + } + + @Test + public void testRemoveSessionProperty() { + embrace.removeSessionProperty(null); + assertError("removeSessionProperty"); + } + + @Test + public void testStartMoment1Parameter() { + embrace.startMoment(null); + assertError("startMoment"); + } + + @Test + public void testStartMoment2Parameters() { + embrace.startMoment(null, null); + assertError("startMoment"); + } + + @Test + public void testStartMoment3ParametersAllowProperties() { + embrace.startMoment(null, null, null); + assertError("startMoment"); + } + + @Test + public void testEndMoment1Parameter() { + embrace.endMoment(null); + assertError("endMoment"); + } + + @Test + public void testEndMoment2ParametersCustomIdentifier() { + embrace.endMoment(null, NULL_STRING); + assertError("endMoment"); + } + + @Test + public void testEndMoment2ParametersAllowProperties() { + embrace.endMoment(null, NULL_STRING); + assertError("endMoment"); + } + + @Test + public void testEndMoment3Parameters() { + embrace.endMoment(null, NULL_STRING, null); + assertError("endMoment"); + } + + @Test + public void testEndAppStartup() { + embrace.endAppStartup(null); + assertError("endAppStartup"); + } + + @Test + public void testRecordNetworkRequest() { + EmbraceNetworkRequest request = null; + embrace.recordNetworkRequest(request); + assertError("recordNetworkRequest"); + } + + @Test + public void testLogInfo() { + embrace.logInfo(null); + assertError("logInfo"); + } + + @Test + public void testLogWarning() { + embrace.logWarning(null); + assertError("logWarning"); + } + + @Test + public void testLogError() { + embrace.logError(NULL_STRING); + assertError("logError"); + } + + @Test + public void testLogException() { + embrace.logException(null); + assertError("logException"); + } + + @Test + public void testLogException2Parameters() { + embrace.logException(null, Severity.ERROR); + assertError("logException"); + embrace.logException(EXCEPTION, null); + assertError("logException"); + } + + @Test + public void testLogException3Parameters() { + embrace.logException(null, Severity.ERROR, null); + assertError("logException"); + embrace.logException(EXCEPTION, null, null); + assertError("logException"); + } + + @Test + public void testLogException4Parameters() { + embrace.logException(null, Severity.ERROR, null, null); + assertError("logException"); + embrace.logException(EXCEPTION, null, null, null); + assertError("logException"); + } + + @Test + public void testLogCustomStacktrace() { + embrace.logCustomStacktrace(null); + assertError("logCustomStacktrace"); + } + + @Test + public void testLogCustomStacktrace2Parameters() { + embrace.logCustomStacktrace(null, Severity.ERROR); + assertError("logCustomStacktrace"); + embrace.logCustomStacktrace(new StackTraceElement[0], null); + assertError("logCustomStacktrace"); + } + + @Test + public void testLogCustomStacktrace3Parameters() { + embrace.logCustomStacktrace(null, Severity.ERROR, null); + assertError("logCustomStacktrace"); + embrace.logCustomStacktrace(new StackTraceElement[0], null, null); + assertError("logCustomStacktrace"); + } + + @Test + public void testLogCustomStacktrace4Parameters() { + embrace.logCustomStacktrace(null, Severity.ERROR, null, null); + assertError("logCustomStacktrace"); + embrace.logCustomStacktrace(new StackTraceElement[0], null, null, null); + assertError("logCustomStacktrace"); + } + + @Test + public void testStartView() { + embrace.startView(null); + assertError("startView"); + } + + @Test + public void testEndView() { + embrace.endView(null); + assertError("endView"); + } + + @Test + public void testAddBreadcrumb() { + embrace.addBreadcrumb(null); + assertError("addBreadcrumb"); + } + + @Test + public void testLogPushNotification() { + embrace.logPushNotification(null, null, null, null, null, null, true, true); + assertError("logPushNotification"); + embrace.logPushNotification(null, null, null, null, null, 1, null, true); + assertError("logPushNotification"); + embrace.logPushNotification(null, null, null, null, null, 1, true, null); + assertError("logPushNotification"); + } + + @Test + public void testTrackWebViewPerformanceWithStringMessage() { + embrace.trackWebViewPerformance(null, "message"); + assertError("trackWebViewPerformance"); + embrace.trackWebViewPerformance("tag", NULL_STRING); + assertError("trackWebViewPerformance"); + } + + @Test + public void testTrackWebViewPerformanceWithConsoleMessage() { + embrace.trackWebViewPerformance(null, new ConsoleMessage("message", "id", 1, ConsoleMessage.MessageLevel.DEBUG)); + assertError("trackWebViewPerformance"); + embrace.trackWebViewPerformance("tag", (ConsoleMessage) null); + assertError("trackWebViewPerformance"); + } + + @Test + public void testCreateSpan() { + assertNull(embrace.createSpan(null)); + assertError("createSpan"); + } + + @Test + public void testCreateSpanWithParent() { + assertNull(embrace.createSpan(null, null)); + assertError("createSpan"); + } + + @Test + public void testRecordSpan() { + assertTrue(embrace.recordSpan(null, () -> true)); + assertError("recordSpan"); + assertNull(embrace.recordSpan("test-span", null)); + assertError("recordSpan"); + } + + @Test + public void testRecordSpan3Parameters() { + assertTrue(embrace.recordSpan(null, null, () -> true)); + assertError("recordSpan"); + assertNull(embrace.recordSpan("test-span", null, null)); + assertError("recordSpan"); + } + + @Test + public void testRecordCompletedSpan() { + assertFalse(embrace.recordCompletedSpan(null, 0, 1)); + assertError("recordCompletedSpan"); + } + + @Test + public void testRecordCompletedSpanWithErrorCode() { + assertFalse(embrace.recordCompletedSpan(null, 0, 1, (ErrorCode) null)); + assertError("recordCompletedSpan"); + } + + @Test + public void testRecordCompletedSpanWithParent() { + assertFalse(embrace.recordCompletedSpan(null, 0, 1, (EmbraceSpan) null)); + assertError("recordCompletedSpan"); + } + + @Test + public void testRecordCompletedSpanWithErrorCodeAndParent() { + assertFalse(embrace.recordCompletedSpan(null, 0, 1, (ErrorCode) null, null)); + assertError("recordCompletedSpan"); + } + + @Test + public void testRecordCompletedSpanWithAttributesAndEvents() { + assertFalse(embrace.recordCompletedSpan(null, 0, 1, (Map) null, null)); + assertError("recordCompletedSpan"); + } + + @Test + public void testRecordCompletedSpanWithEverything() { + assertFalse(embrace.recordCompletedSpan(null, 0, 1, null, null, null, null)); + assertError("recordCompletedSpan"); + } + + private void assertError(@NonNull String functionName) { + assertInternalErrorLogged( + IntegrationTestRuleExtensionsKt.exceptionsService().getCurrentExceptionError(), + IllegalArgumentException.class.getCanonicalName(), + functionName + NULL_PARAMETER_ERROR_MESSAGE_TEMPLATE, + IntegrationTestRule.DEFAULT_SDK_START_TIME_MS + ); + IntegrationTestRuleExtensionsKt.exceptionsService().resetExceptionErrorObject(); + } +} diff --git a/embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/PreSdkStartTest.java b/embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/PreSdkStartTest.java new file mode 100644 index 0000000000..fb0a0a98ca --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/java/io/embrace/android/embracesdk/PreSdkStartTest.java @@ -0,0 +1,41 @@ +package io.embrace.android.embracesdk; + +import static org.junit.Assert.assertFalse; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class PreSdkStartTest { + + @Rule + public IntegrationTestRule testRule = new IntegrationTestRule( + () -> IntegrationTestRule.Companion.newHarness(false) + ); + + @NonNull + private final Embrace embrace = testRule.getEmbrace(); + + @Test + public void testStartWithNullContext() { + embrace.start(null); + embrace.start(null, true); + embrace.start(null, false, Embrace.AppFramework.NATIVE); + assertFalse(embrace.isStarted()); + } + + @Test + public void testStartWithNullAppFramework() { + embrace.start(testRule.harness.getFakeCoreModule().getContext(), false, null); + assertFalse(embrace.isStarted()); + } + + @Test + public void testSetAppId() { + assertFalse(embrace.setAppId(null)); + } +} diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRule.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRule.kt new file mode 100644 index 0000000000..c2f90134f5 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRule.kt @@ -0,0 +1,208 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.IntegrationTestRule.Harness +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.NetworkLocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeNetworkBehavior +import io.embrace.android.embracesdk.fakes.fakeSdkModeBehavior +import io.embrace.android.embracesdk.fakes.fakeSpansBehavior +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeDeliveryModule +import io.embrace.android.embracesdk.fakes.injection.FakeInitModule +import io.embrace.android.embracesdk.injection.AndroidServicesModule +import io.embrace.android.embracesdk.injection.AndroidServicesModuleImpl +import io.embrace.android.embracesdk.injection.CoreModule +import io.embrace.android.embracesdk.injection.DataCaptureServiceModule +import io.embrace.android.embracesdk.injection.DataCaptureServiceModuleImpl +import io.embrace.android.embracesdk.injection.DeliveryModule +import io.embrace.android.embracesdk.injection.EssentialServiceModule +import io.embrace.android.embracesdk.injection.EssentialServiceModuleImpl +import io.embrace.android.embracesdk.injection.InitModule +import io.embrace.android.embracesdk.injection.SystemServiceModule +import io.embrace.android.embracesdk.injection.SystemServiceModuleImpl +import io.embrace.android.embracesdk.internal.BuildInfo +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.worker.WorkerThreadModule +import io.embrace.android.embracesdk.worker.WorkerThreadModuleImpl +import org.junit.rules.ExternalResource + +/** + * A [org.junit.Rule] that is responsible for setting up and tearing down the Embrace SDK for use in + * component/integration testing. Test cases must be run with a test runner that is compatible with Robolectric. + * + * The SDK instance exposed is almost like the one used in production other than the 3 modules that have + * to faked at least in part in order for this to be useful in tests: + * + * 1) [CoreModule]: Requires [FakeCoreModule] to fake Android system objects like [Application], [Context], + * and resources that are normally not available in a JUnit test environment. Also, a [FakeClock] is used + * so we can control time to control timing and execution manually rather than waiting for normal + * time to pass. + * + * 2) [EssentialServiceModule]: Requires an instance of [EssentialServiceModuleImpl] that uses a [FakeConfigService] + * to allow the appropriate feature flags be set to suit the test case. Everything else in the + * module is otherwise real unless provided by [FakeCoreModule] + * + * 3) [DeliveryModule]: Requires [FakeDeliveryModule] so that a [FakeDeliveryService] can be used in order + * for payloads normally sent to the server to be inspected to verify the correctness of the data produced. + * Everything else in [FakeDeliveryModule] is otherwise real unless provided by [FakeCoreModule] + * + * The modules instantiated by default reference each other, e.g. the same instance of [FakeCoreModule] is used + * in all the other modules. This means modifications of one object within a module will be reflected in the + * other modules, e.g. [InitModule.clock]. This allows the system to behave as one unit. + * + * While it is possible to override the default behavior of the [Embrace] instance in the Rule by passing in + * a custom supplier to create an instance of [Harness] with custom overridden constructor parameters, be + * careful when you do so by passing in overridden module instances as that might break the integrity of the + * "one instance" guarantee of using the modules created by default (besides the fakes). + * + * It is also possible to access internal modules & dependencies that are not exposed via the public API. + * For example, it is possible to access the [FakeDeliveryModule] to get event/session payloads the SDK sent. + * + * In general, it is recommended to use functions declared in IntegrationTestRuleExtensions.kt that retrieve + * this useful information for you. If it's not possible to get information from the SDK and you + * need it for a test, please consider adding a new function to IntegrationTestRuleExtensions.kt so that others + * find it easier to write tests in the future. + * + * Because there are parts of the [Embrace] instance being tested that are using fakes, unless you are careful, + * do not use this to verify code paths that are overridden by fakes. For example, do not use this rule to + * verify the default config settings because what's in use is a [FakeConfigService], which is not used in + * production. + */ +internal class IntegrationTestRule( + private val harnessSupplier: () -> Harness = { Harness() } +) : ExternalResource() { + /** + * The [Embrace] instance that can be used for testing + */ + val embrace = Embrace.getInstance() + + /** + * Instance of the test harness that is recreating on every test iteration + */ + lateinit var harness: Harness + + /** + * Setup the Embrace SDK so it's ready for testing. + */ + override fun before() { + harness = harnessSupplier.invoke() + with(harness) { + val embraceImpl = EmbraceImpl( + { initModule }, + { _, _ -> fakeCoreModule }, + { workerThreadModule }, + { _ -> systemServiceModule }, + { _, _, _ -> androidServicesModule }, + { _, _, _, _, _, _, _, _, _, _, _ -> essentialServiceModule }, + { _, _, _, _, _ -> dataCaptureServiceModule }, + { _, _, _, _, _ -> fakeDeliveryModule } + ) + Embrace.setImpl(embraceImpl) + if (startImmediately) { + embrace.start(fakeCoreModule.context, enableIntegrationTesting, appFramework) + } + } + } + + /** + * Teardown the Embrace SDK, closing any resources as required + */ + override fun after() { + InternalStaticEmbraceLogger.logger.setToDefault() + Embrace.getImpl().stop() + } + + /** + * Test harness for which an instance is generated each test run and provided to the test by the Rule + */ + internal class Harness( + currentTimeMs: Long = DEFAULT_SDK_START_TIME_MS, + val fakeClock: FakeClock = FakeClock(currentTime = currentTimeMs), + val enableIntegrationTesting: Boolean = false, + val appFramework: Embrace.AppFramework = Embrace.AppFramework.NATIVE, + val initModule: InitModule = FakeInitModule(clock = fakeClock), + val fakeCoreModule: FakeCoreModule = FakeCoreModule(), + val workerThreadModule: WorkerThreadModule = WorkerThreadModuleImpl(), + val fakeConfigService: FakeConfigService = FakeConfigService( + backgroundActivityCaptureEnabled = true, + sdkModeBehavior = fakeSdkModeBehavior( + isDebug = fakeCoreModule.isDebug, + localCfg = { DEFAULT_LOCAL_CONFIG } + ), + networkBehavior = fakeNetworkBehavior( + localCfg = { DEFAULT_SDK_LOCAL_CONFIG }, + remoteCfg = { DEFAULT_SDK_REMOTE_CONFIG } + ), + spansBehavior = fakeSpansBehavior { + SpansRemoteConfig(pctEnabled = 100f) + } + ), + val systemServiceModule: SystemServiceModule = + SystemServiceModuleImpl( + coreModule = fakeCoreModule + ), + val androidServicesModule: AndroidServicesModule = AndroidServicesModuleImpl( + initModule = initModule, + coreModule = fakeCoreModule, + workerThreadModule = workerThreadModule, + ), + val essentialServiceModule: EssentialServiceModule = + EssentialServiceModuleImpl( + initModule = initModule, + coreModule = fakeCoreModule, + workerThreadModule = workerThreadModule, + systemServiceModule = systemServiceModule, + androidServicesModule = androidServicesModule, + buildInfo = BuildInfo.fromResources(fakeCoreModule.resources, fakeCoreModule.context.packageName), + customAppId = null, + enableIntegrationTesting = enableIntegrationTesting, + configStopAction = { Embrace.getImpl().stop() }, + configServiceProvider = { fakeConfigService } + ), + val dataCaptureServiceModule: DataCaptureServiceModule = + DataCaptureServiceModuleImpl( + initModule = initModule, + coreModule = fakeCoreModule, + systemServiceModule = systemServiceModule, + essentialServiceModule = essentialServiceModule, + workerThreadModule = workerThreadModule + ), + val fakeDeliveryModule: FakeDeliveryModule = + FakeDeliveryModule( + initModule = initModule, + coreModule = fakeCoreModule, + essentialServiceModule = essentialServiceModule, + dataCaptureServiceModule = dataCaptureServiceModule, + workerThreadModule = workerThreadModule + ), + val startImmediately: Boolean = true + ) + + companion object { + const val DEFAULT_SDK_START_TIME_MS = 1692201600L + + fun newHarness(startImmediately: Boolean) = Harness(startImmediately = startImmediately) + + private val DEFAULT_SDK_LOCAL_CONFIG = SdkLocalConfig( + networking = NetworkLocalConfig( + enableNativeMonitoring = false + ), + betaFeaturesEnabled = false + ) + + private val DEFAULT_SDK_REMOTE_CONFIG = RemoteConfig( + disabledUrlPatterns = setOf("dontlogmebro.pizza") + ) + + val DEFAULT_LOCAL_CONFIG = LocalConfig( + appId = "CoYh3", + ndkEnabled = false, + sdkConfig = DEFAULT_SDK_LOCAL_CONFIG + ) + } +} diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRuleExtensions.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRuleExtensions.kt new file mode 100644 index 0000000000..3615134763 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/IntegrationTestRuleExtensions.kt @@ -0,0 +1,107 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.SessionMessage +import org.junit.Assert.assertEquals +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/*** Extension functions that are syntactic sugar for retrieving information from the SDK. ***/ + +/** + * Returns a list of [EventMessage] logs that were sent by the SDK since startup. If [expectedSize] is specified, it will wait up to + * 1 second to validate the number of sent log message equal that size. If a second passes that the size requirement is not met, a + * [TimeoutException] will be thrown. If [expectedSize] is null or not specified, the correct sent log messages will be returned right + * away. + */ +internal fun IntegrationTestRule.Harness.getSentLogMessages(expectedSize: Int? = null): List { + val logs = fakeDeliveryModule.deliveryService.lastSentLogs + return when (expectedSize) { + null -> logs + else -> returnIfConditionMet({ logs }) { + logs.size == expectedSize + } + } +} + +/** + * Returns the last [EventMessage] log that was sent by the SDK. If [expectedSize] is specified, it will wait up to 1 second to validate + * the number of sent log message equal that size. If a second passes that the size requirement is not met, a [TimeoutException] will + * be thrown. If [expectedSize] is null or not specified, the correct sent log messages will be returned right away. + */ +internal fun IntegrationTestRule.Harness.getLastSentLogMessage(expectedSize: Int? = null): EventMessage { + return getSentLogMessages(expectedSize).last() +} + +/** + * Returns a list of [SessionMessage] that were sent by the SDK since startup. + */ +internal fun IntegrationTestRule.Harness.getSentSessionMessages(): List { + return fakeDeliveryModule.deliveryService.lastSentSessions.map { it.first } +} + +/** + * Returns the last [SessionMessage] that was sent by the SDK. + */ +internal fun IntegrationTestRule.Harness.getLastSentSessionMessage(): SessionMessage { + return getSentSessionMessages().last() +} + +/** + * Starts & ends a session for the purposes of testing. An action can be supplied as a lambda + * parameter: any code inside the lambda will be executed, so can be used to add breadcrumbs, + * send log messages etc, while the session is active. The end session message is returned so + * that the caller can perform further assertions if needed. + * + * This function fakes the lifecycle events that trigger a session start & end. The session + * should always be 30s long. Additionally, it performs assertions against fields that + * are guaranteed not to change in the start/end message. + */ +internal fun IntegrationTestRule.Harness.recordSession(action: () -> Unit): SessionMessage { + // get the activity service & simulate the lifecycle event that triggers a new session. + val activityService = checkNotNull(Embrace.getImpl().activityService) + activityService.onForeground() + + // assert a session was started. + val startSession = getLastSentSessionMessage() + assertEquals("st", startSession.session.messageType) + // TODO: future: increase number of assertions on what is always in a start message? + + // perform a custom action during the session boundary, e.g. adding a breadcrumb. + action() + + // end session 30s later by entering background + fakeClock.tick(30000) + activityService.onBackground() + + val endSession = getLastSentSessionMessage() + assertEquals("en", endSession.session.messageType) + // TODO: future: increase number of assertions on what is always in a start message? + + // return the session end message for further assertions. + return endSession +} + +internal fun exceptionsService(): EmbraceInternalErrorService? = Embrace.getImpl().exceptionsService + +/** + * Return the result of [desiredValueSupplier] if [condition] is true before [waitTimeMs] elapses. Otherwise, throws [TimeoutException] + */ +internal fun returnIfConditionMet(desiredValueSupplier: () -> T, waitTimeMs: Int = 1000, condition: () -> Boolean): T { + val tries: Int = waitTimeMs / CHECK_INTERVAL_MS + val countDownLatch = CountDownLatch(1) + + repeat(tries) { + if (!condition()) { + countDownLatch.await(CHECK_INTERVAL_MS.toLong(), TimeUnit.MILLISECONDS) + } else { + return desiredValueSupplier.invoke() + } + } + + throw TimeoutException("Timeout period elapsed before condition met") +} + +private const val CHECK_INTERVAL_MS: Int = 10 diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/SessionApiTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/SessionApiTest.kt new file mode 100644 index 0000000000..bfa964e3b4 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/SessionApiTest.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test for the internal implementation of the Embrace SDK + */ +@RunWith(AndroidJUnit4::class) +internal class SessionApiTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule() + + @Test + fun `session messages are recorded`() { + with(testRule) { + assertTrue(harness.getSentSessionMessages().isEmpty()) + + val session = harness.recordSession { + embrace.addBreadcrumb("Hello, World!") + } + + // perform further assertions that only apply to this individual test case. + val crumb = checkNotNull(session.breadcrumbs?.customBreadcrumbs?.single()) + assertEquals("Hello, World!", crumb.message) + } + } +} diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/InternalErrorAssertions.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/InternalErrorAssertions.kt new file mode 100644 index 0000000000..a339acc804 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/InternalErrorAssertions.kt @@ -0,0 +1,43 @@ +package io.embrace.android.embracesdk.assertions + +import io.embrace.android.embracesdk.payload.ExceptionError +import io.embrace.android.embracesdk.IntegrationTestRule +import org.junit.Assert.assertTrue + +/** + * Return true if at least one exception matching the expected time, exception type, and error message is found in the internal errors + */ +internal fun assertInternalErrorLogged( + exceptionError: ExceptionError?, + exceptionClassName: String, + errorMessage: String, + errorTimeMs: Long = IntegrationTestRule.DEFAULT_SDK_START_TIME_MS +) { + requireNotNull(exceptionError) { "No internal errors found" } + var foundErrorMatch = false + var foundErrorAtTime = false + val unmatchedDetails: MutableList = mutableListOf() + val errors = exceptionError.exceptionErrors.toList() + assertTrue("No exception errors found", errors.isNotEmpty()) + errors.forEach { error -> + if (errorTimeMs == error.timestamp) { + foundErrorAtTime = true + val firstExceptionInfo = checkNotNull(error.exceptions).first() + with(firstExceptionInfo) { + if (exceptionClassName == name && errorMessage == message) { + foundErrorMatch = true + } else { + unmatchedDetails.add("'$exceptionClassName' is not '$name' OR '$errorMessage' is not '$message' \n") + } + } + } + } + + assertTrue("No internal error found matching the expected time", foundErrorAtTime) + + assertTrue( + "Expected exception not found. " + + "Found following ${unmatchedDetails.size} exceptions in ${errors.size} errors instead: $unmatchedDetails", + foundErrorMatch + ) +} diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/LogMessageAssertions.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/LogMessageAssertions.kt new file mode 100644 index 0000000000..14c0140c2e --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/LogMessageAssertions.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.assertions + +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.IntegrationTestRule +import io.embrace.android.embracesdk.LogExceptionType +import org.junit.Assert.assertEquals + +/** + * Asserts that a log message was sent with the given parameters. + */ +internal fun assertLogMessageReceived( + eventMessage: EventMessage, + message: String, + eventType: EmbraceEvent.Type, + logType: LogExceptionType = LogExceptionType.NONE, + timeMs: Long = IntegrationTestRule.DEFAULT_SDK_START_TIME_MS, + properties: Map? = null, + exception: Exception? = null, + stack: Array? = null +) { + with(eventMessage.event) { + assertEquals(message, name) + assertEquals(timeMs, timestamp) + assertEquals(false, screenshotTaken) + assertEquals(logType.value, logExceptionType) + assertEquals(eventType, type) + assertEquals(Embrace.AppFramework.NATIVE.value, framework) + assertEquals(properties, customPropertiesMap) + exception?.let { + assertEquals(it.message, exceptionMessage) + assertEquals(it.javaClass.simpleName, exceptionName) + } + } + + if (stack != null) { + assertEquals(stack.map { it.toString() }, eventMessage.stacktraces?.jvmStacktrace) + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/SpanAssertions.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/SpanAssertions.kt new file mode 100644 index 0000000000..c8b164bb84 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/assertions/SpanAssertions.kt @@ -0,0 +1,47 @@ +package io.embrace.android.embracesdk.assertions + +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData +import io.embrace.android.embracesdk.internal.spans.isKey +import io.embrace.android.embracesdk.internal.spans.isPrivate +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.api.trace.StatusCode +import org.junit.Assert.assertEquals +import java.util.concurrent.TimeUnit + +/** + * Assert the [EmbraceSpanData] is as expected + */ +internal fun assertEmbraceSpanData( + span: EmbraceSpanData?, + expectedStartTimeMs: Long, + expectedEndTimeMs: Long, + expectedParentId: String, + expectedTraceId: String? = null, + expectedStatus: StatusCode = StatusCode.OK, + errorCode: ErrorCode? = null, + expectedCustomAttributes: Map = emptyMap(), + expectedEvents: List = emptyList(), + private: Boolean = false, + key: Boolean = false, +) { + checkNotNull(span) + with(span) { + assertEquals(TimeUnit.MILLISECONDS.toNanos(expectedStartTimeMs), startTimeNanos) + assertEquals(TimeUnit.MILLISECONDS.toNanos(expectedEndTimeMs), endTimeNanos) + assertEquals(expectedParentId, parentSpanId) + if (expectedTraceId != null) { + assertEquals(expectedTraceId, traceId) + } else { + assertEquals(32, traceId.length) + } + assertEquals(expectedStatus, status) + assertEquals(errorCode?.name, attributes[errorCode?.keyName()]) + expectedCustomAttributes.forEach { entry -> + assertEquals(entry.value, attributes[entry.key]) + } + assertEquals(expectedEvents, events) + assertEquals(private, isPrivate()) + assertEquals(key, isKey()) + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/LoggingApiTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/LoggingApiTest.kt new file mode 100644 index 0000000000..4b5bc61c38 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/LoggingApiTest.kt @@ -0,0 +1,252 @@ +package io.embrace.android.embracesdk.testcases + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.IntegrationTestRule +import io.embrace.android.embracesdk.LogExceptionType +import io.embrace.android.embracesdk.Severity +import io.embrace.android.embracesdk.assertions.assertLogMessageReceived +import io.embrace.android.embracesdk.getLastSentLogMessage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.lang.IllegalArgumentException + +@RunWith(AndroidJUnit4::class) +internal class LoggingApiTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule() + + @Test + fun `log info message sent`() { + with(testRule) { + embrace.logInfo("test message") + val eventMessage = harness.getLastSentLogMessage(expectedSize = 1) + assertLogMessageReceived( + eventMessage, + message = "test message", + eventType = EmbraceEvent.Type.INFO_LOG + ) + } + } + + @Test + fun `log warning message sent`() { + with(testRule) { + embrace.logWarning("test message") + val eventMessage = harness.getLastSentLogMessage(expectedSize = 1) + assertLogMessageReceived( + eventMessage, + message = "test message", + eventType = EmbraceEvent.Type.WARNING_LOG + ) + } + } + + @Test + fun `log error message sent`() { + with(testRule) { + embrace.logError("test message") + val eventMessage = harness.getLastSentLogMessage(expectedSize = 1) + assertLogMessageReceived( + eventMessage, + message = "test message", + eventType = EmbraceEvent.Type.ERROR_LOG + ) + } + } + + @Test + fun `log messages with different severities sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + val expectedMessage = "test message ${severity.name}" + embrace.logMessage(expectedMessage, severity) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = expectedMessage, + eventType = EmbraceEvent.Type.fromSeverity(severity) + ) + } + } + } + + @Test + fun `log messages with different severities and properties sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + val expectedMessage = "test message ${severity.name}" + embrace.logMessage(expectedMessage, severity, customProperties) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = expectedMessage, + eventType = EmbraceEvent.Type.fromSeverity(severity), + properties = customProperties + ) + } + } + } + + @Test + fun `log exception message sent`() { + with(testRule) { + embrace.logException(testException) + val eventMessage = harness.getLastSentLogMessage(expectedSize = 1) + assertLogMessageReceived( + eventMessage, + message = checkNotNull(testException.message), + eventType = EmbraceEvent.Type.ERROR_LOG, + logType = LogExceptionType.HANDLED, + exception = testException + ) + } + } + + @Test + fun `log exception with different severities sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + embrace.logException(testException, severity) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = checkNotNull(testException.message), + eventType = EmbraceEvent.Type.fromSeverity(severity), + logType = LogExceptionType.HANDLED, + exception = testException + ) + } + } + } + + @Test + fun `log exception with different severities and properties sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + embrace.logException(testException, severity, customProperties) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = checkNotNull(testException.message), + eventType = EmbraceEvent.Type.fromSeverity(severity), + properties = customProperties, + logType = LogExceptionType.HANDLED, + exception = testException + ) + } + } + } + + @Test + fun `log exception with different severities, properties, and custom message sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + val expectedMessage = "test message ${severity.name}" + embrace.logException(testException, severity, customProperties, expectedMessage) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = expectedMessage, + eventType = EmbraceEvent.Type.fromSeverity(severity), + properties = customProperties, + logType = LogExceptionType.HANDLED, + exception = testException + ) + } + } + } + + @Test + fun `log custom stacktrace message sent`() { + with(testRule) { + embrace.logCustomStacktrace(stacktrace) + val eventMessage = harness.getLastSentLogMessage(expectedSize = 1) + assertLogMessageReceived( + eventMessage, + message = "", + eventType = EmbraceEvent.Type.ERROR_LOG, + logType = LogExceptionType.HANDLED, + stack = stacktrace + ) + } + } + + @Test + fun `log custom stacktrace with different severities sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + embrace.logCustomStacktrace(stacktrace, severity) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = "", + eventType = EmbraceEvent.Type.fromSeverity(severity), + logType = LogExceptionType.HANDLED, + stack = stacktrace + ) + } + } + } + + @Test + fun `log custom stacktrace with different severities and properties sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + embrace.logCustomStacktrace(stacktrace, severity, customProperties) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = "", + eventType = EmbraceEvent.Type.fromSeverity(severity), + properties = customProperties, + logType = LogExceptionType.HANDLED, + stack = stacktrace + ) + } + } + } + + @Test + fun `log custom stacktrace with different severities, properties, and custom message sent`() { + var logsSent = 0 + with(testRule) { + Severity.values().forEach { severity -> + val expectedMessage = "test message ${severity.name}" + embrace.logCustomStacktrace(stacktrace, severity, customProperties, expectedMessage) + logsSent++ + val eventMessage = harness.getLastSentLogMessage(logsSent) + assertLogMessageReceived( + eventMessage, + message = expectedMessage, + eventType = EmbraceEvent.Type.fromSeverity(severity), + properties = customProperties, + logType = LogExceptionType.HANDLED, + stack = stacktrace + ) + } + } + } + + companion object { + private val testException = IllegalArgumentException("nooooooo") + private val customProperties: Map = linkedMapOf(Pair("first", 1), Pair("second", "two"), Pair("third", true)) + private val stacktrace = Thread.currentThread().stackTrace + } +} diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt new file mode 100644 index 0000000000..b648d77d20 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt @@ -0,0 +1,275 @@ +package io.embrace.android.embracesdk.testcases + +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.IntegrationTestRule +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.payload.NetworkCallV2 +import io.embrace.android.embracesdk.recordSession +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import kotlin.math.max + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) +internal class NetworkRequestApiTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule() + + @Test + fun `record basic completed GET request`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + } + + @Test + fun `record completed POST request with traceId`() { + assertSingleNetworkRequestInSession( + expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.POST, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200, + TRACE_ID, + ) + ) + } + + @Test + fun `record completed request that failed with captured response`() { + assertSingleNetworkRequestInSession( + expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 500, + TRACE_ID, + NETWORK_CAPTURE_DATA + ) + ) + } + + @Test + fun `record completed request with traceparent`() { + assertSingleNetworkRequestInSession( + expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200, + TRACE_ID, + TRACEPARENT, + NETWORK_CAPTURE_DATA + ) + ) + } + + @Test + fun `record basic incomplete request`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there" + ), + completed = false + ) + } + + @Test + fun `record incomplete POST request with trace ID`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.POST, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there", + TRACE_ID + ), + completed = false + ) + } + + @Test + fun `record incomplete request with network capture`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there", + TRACE_ID, + NETWORK_CAPTURE_DATA + ), + completed = false + ) + } + + @Test + fun `record incomplete request with traceparent`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there", + TRACE_ID, + TRACEPARENT, + NETWORK_CAPTURE_DATA + ), + completed = false + ) + } + + @Test + fun `disabled URLs not recorded`() { + with(testRule) { + harness.recordSession { + harness.fakeConfigService.updateListeners() + harness.fakeClock.tick(5) + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromCompletedRequest( + DISABLED_URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + harness.fakeClock.tick(5) + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromIncompleteRequest( + DISABLED_URL, + HttpMethod.GET, + START_TIME + 1, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there" + ) + ) + harness.fakeClock.tick(5) + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME + 2, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + } + + val networkCall = validateAndReturnExpectedNetworkCall(harness) + assertEquals(URL, networkCall.url) + } + } + + private fun assertSingleNetworkRequestInSession(expectedRequest: EmbraceNetworkRequest, completed: Boolean = true) { + with(testRule) { + harness.recordSession { + harness.fakeConfigService.updateListeners() + harness.fakeClock.tick(5L) + embrace.recordNetworkRequest(expectedRequest) + } + + val networkCall = validateAndReturnExpectedNetworkCall(harness) + with(networkCall) { + assertEquals(expectedRequest.url, url) + assertEquals(expectedRequest.httpMethod, httpMethod) + assertEquals(expectedRequest.startTime, startTime) + assertEquals(expectedRequest.endTime, endTime) + assertEquals(max(expectedRequest.endTime - expectedRequest.startTime, 0L), duration) + assertEquals(expectedRequest.traceId, traceId) + assertEquals(expectedRequest.w3cTraceparent, w3cTraceparent) + if (completed) { + assertEquals(expectedRequest.responseCode, responseCode) + assertEquals(expectedRequest.bytesSent, bytesSent) + assertEquals(expectedRequest.bytesReceived, bytesReceived) + assertEquals(null, errorType) + assertEquals(null, errorMessage) + } else { + assertEquals(null, responseCode) + assertEquals(0, bytesSent) + assertEquals(0, bytesReceived) + assertEquals(expectedRequest.errorType, errorType) + assertEquals(expectedRequest.errorMessage, errorMessage) + } + } + } + } + + private fun validateAndReturnExpectedNetworkCall(harness: IntegrationTestRule.Harness): NetworkCallV2 { + val lastSavedSessionRequestCount = + harness.fakeDeliveryModule.deliveryService.lastSavedSession?.performanceInfo?.networkRequests?.networkSessionV2?.requests?.size + ?: -1 + val session = harness.fakeDeliveryModule.deliveryService.lastSentSessions[1].first + val requests = checkNotNull(session.performanceInfo?.networkRequests?.networkSessionV2?.requests) + val requestCount = requests.size + val networkCall = requests.first() + + assertEquals( + "Unexpected number of requests in sent session: $requestCount. Last saved session requests: $lastSavedSessionRequestCount", + 1, + requestCount + ) + + return networkCall + } + + companion object { + private const val URL = "https://embrace.io" + private const val DISABLED_URL = "https://dontlogmebro.pizza/yum" + private const val START_TIME = 1692201601L + private const val END_TIME = 1692202600L + private const val BYTES_SENT = 100L + private const val BYTES_RECEIVED = 500L + private const val TRACE_ID = "rAnDoM-traceId" + private const val TRACEPARENT = "00-c4ada96c31e1b6b9e351a1cffc99ae38-331f3a8acf49d295-01" + + private val NETWORK_CAPTURE_DATA = NetworkCaptureData( + requestHeaders = mapOf(Pair("x-emb-test", "holla")), + requestQueryParams = "trackMe=noooooo", + capturedRequestBody = "haha".toByteArray(), + responseHeaders = mapOf(Pair("x-emb-response-header", "alloh")), + capturedResponseBody = "woohoo".toByteArray(), + dataCaptureErrorMessage = null + ) + } +} diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/PublicApiTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/PublicApiTest.kt new file mode 100644 index 0000000000..27e242100c --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/PublicApiTest.kt @@ -0,0 +1,139 @@ +package io.embrace.android.embracesdk.testcases + +import android.os.Build.VERSION_CODES.TIRAMISU +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.Embrace.AppFramework +import io.embrace.android.embracesdk.IntegrationTestRule +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.internal.TraceparentGeneratorTest.Companion.validPattern +import io.embrace.android.embracesdk.recordSession +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +/** + * Validation of the basic and miscellaneous functionality of the Android SDK + */ +@Config(sdk = [TIRAMISU]) +@RunWith(AndroidJUnit4::class) +internal class PublicApiTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule( + harnessSupplier = { + IntegrationTestRule.newHarness(startImmediately = false) + } + ) + + @Before + fun before() { + ApkToolsConfig.IS_SDK_DISABLED = false + } + + @Test + fun `SDK can start`() { + with(testRule) { + assertFalse(embrace.isStarted) + embrace.start(harness.fakeCoreModule.context) + assertEquals(AppFramework.NATIVE, harness.appFramework) + assertFalse(harness.essentialServiceModule.configService.isSdkDisabled()) + assertTrue(embrace.isStarted) + } + } + + @Test + fun `SDK start defaults to native app framework`() { + with(testRule) { + assertFalse(embrace.isStarted) + embrace.start(harness.fakeCoreModule.context, false) + assertEquals(AppFramework.NATIVE, harness.appFramework) + assertTrue(embrace.isStarted) + } + } + + @Test + fun `SDK disabled via the binary cannot start`() { + with(testRule) { + ApkToolsConfig.IS_SDK_DISABLED = true + embrace.start(harness.fakeCoreModule.context) + assertFalse(embrace.isStarted) + } + } + + @Test + fun `SDK disabled via config cannot start`() { + with(testRule) { + harness.fakeConfigService.sdkDisabled = true + embrace.start(harness.fakeCoreModule.context) + assertFalse(embrace.isStarted) + } + } + + @Test + fun `custom appId must be valid`() { + with(testRule) { + assertFalse(embrace.setAppId("")) + assertFalse(embrace.setAppId("abcd")) + assertFalse(embrace.setAppId("abcdef")) + assertTrue(embrace.setAppId("abcde")) + } + } + + @Test + fun `custom appId cannot be set after start`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + assertTrue(embrace.isStarted) + assertFalse(embrace.setAppId("xyz12")) + } + } + + @Test + fun `getCurrentSessionId returns null when SDK is not started`() { + with(testRule) { + assertNull(embrace.currentSessionId) + } + } + + @Test + fun `getCurrentSessionId returns sessionId when SDK is started and foreground session is active`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + harness.recordSession { + assertEquals(embrace.currentSessionId, harness.essentialServiceModule.metadataService.activeSessionId) + assertNotNull(embrace.currentSessionId) + } + } + } + + @Test + fun `getCurrentSessionId returns sessionId when SDK is started and background session is active`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + var foregroundSessionId: String? = null + harness.recordSession { + foregroundSessionId = embrace.currentSessionId + } + val backgroundSessionId = embrace.currentSessionId + assertNotNull(backgroundSessionId) + assertNotEquals(foregroundSessionId, backgroundSessionId) + } + } + + @Test + fun `ensure all generated W3C traceparent conforms to the expected format`() { + with(testRule) { + repeat(100) { + assertTrue(validPattern.matches(embrace.generateW3cTraceparent())) + } + } + } +} diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/TracingApiTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/TracingApiTest.kt new file mode 100644 index 0000000000..a1a4ecd03f --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/TracingApiTest.kt @@ -0,0 +1,195 @@ +package io.embrace.android.embracesdk.testcases + +import android.os.Build.VERSION_CODES.TIRAMISU +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.IntegrationTestRule +import io.embrace.android.embracesdk.assertions.assertEmbraceSpanData +import io.embrace.android.embracesdk.comms.delivery.SessionMessageState +import io.embrace.android.embracesdk.fixtures.TOO_LONG_ATTRIBUTE_KEY +import io.embrace.android.embracesdk.fixtures.TOO_LONG_ATTRIBUTE_VALUE +import io.embrace.android.embracesdk.recordSession +import io.embrace.android.embracesdk.returnIfConditionMet +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.api.trace.SpanId +import io.opentelemetry.api.trace.StatusCode +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.util.concurrent.TimeUnit + +@Config(sdk = [TIRAMISU]) +@RunWith(AndroidJUnit4::class) +internal class TracingApiTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule( + harnessSupplier = { + IntegrationTestRule.newHarness(startImmediately = false) + } + ) + + @Test + fun `check spans logged in the right session when service is initialized after a session starts`() { + val testStartTime = testRule.harness.fakeClock.now() + with(testRule) { + harness.fakeClock.tick(100L) + embrace.start(harness.fakeCoreModule.context) + harness.recordSession { + harness.fakeConfigService.updateListeners() + assertTrue( + returnIfConditionMet(desiredValueSupplier = { true }, waitTimeMs = 1000) { embrace.isTracingAvailable() } + ) + val parentSpan = checkNotNull(embrace.createSpan(name = "test-trace-root")) + assertTrue(parentSpan.start()) + assertTrue(parentSpan.addAttribute("oMg", "OmG")) + assertTrue(embrace.recordSpan(name = "record-span-span", parent = parentSpan) { + harness.fakeClock.tick(100L) + parentSpan.addEvent("parent event") + true + }) + val failedOpStartTime = harness.fakeClock.now() + harness.fakeClock.tick(200L) + parentSpan.addEvent(name = "delayed event", time = harness.fakeClock.now() - 50L, null) + val failedOpEndTime = harness.fakeClock.now() + + assertTrue(parentSpan.stop()) + + val attributes = mapOf( + Pair(TOO_LONG_ATTRIBUTE_KEY, "value"), + Pair("test-attr", "false"), + Pair("myKey", TOO_LONG_ATTRIBUTE_VALUE) + ) + + val events = listOf( + checkNotNull( + EmbraceSpanEvent.create( + name = "failure time", + timestampNanos = failedOpEndTime, + attributes = mapOf( + Pair("retry", "1") + ) + ) + ) + ) + assertTrue( + embrace.recordCompletedSpan( + name = "completed-span", + startTimeNanos = TimeUnit.MILLISECONDS.toNanos(failedOpStartTime), + endTimeNanos = TimeUnit.MILLISECONDS.toNanos(failedOpEndTime), + errorCode = ErrorCode.FAILURE, + parent = parentSpan, + attributes = attributes, + events = events + ) + ) + harness.fakeClock.tick(300L) + embrace.endAppStartup() + assertTrue( + returnIfConditionMet(desiredValueSupplier = { true }, waitTimeMs = 1000) { + checkNotNull(harness.initModule.spansService.completedSpans()).size == 5 + } + ) + } + val sessionEndTime = harness.fakeClock.now() + assertEquals(2, harness.fakeDeliveryModule.deliveryService.lastSentSessions.size) + val startSession = harness.fakeDeliveryModule.deliveryService.lastSentSessions[0] + assertEquals(SessionMessageState.START, startSession.second) + val endSession = harness.fakeDeliveryModule.deliveryService.lastSentSessions[1] + assertEquals(SessionMessageState.END, endSession.second) + with(endSession.first) { + checkNotNull(spans) + assertEquals(6, spans.size) + val spansMap = spans.associateBy { it.name } + val sessionSpan = checkNotNull(spansMap["emb-session-span"]) + val traceRootSpan = checkNotNull(spansMap["test-trace-root"]) + assertEmbraceSpanData( + span = spansMap["emb-sdk-init"], + expectedStartTimeMs = testStartTime + 100, + expectedEndTimeMs = testStartTime + 100, + expectedParentId = SpanId.getInvalid(), + expectedEvents = listOf( + checkNotNull( + EmbraceSpanEvent.create( + name = "start-time", + timestampNanos = TimeUnit.MILLISECONDS.toNanos(testStartTime + 100), + attributes = null + ) + ) + ), + private = true, + key = true + ) + assertEmbraceSpanData( + span = spansMap["emb-startup-moment"], + expectedStartTimeMs = testStartTime + 100, + expectedEndTimeMs = testStartTime + 700, + expectedParentId = SpanId.getInvalid(), + key = true + ) + assertEmbraceSpanData( + span = traceRootSpan, + expectedStartTimeMs = testStartTime + 100, + expectedEndTimeMs = testStartTime + 400, + expectedParentId = SpanId.getInvalid(), + expectedCustomAttributes = mapOf(Pair("oMg", "OmG")), + expectedEvents = listOf( + checkNotNull( + EmbraceSpanEvent.create( + name = "parent event", + timestampNanos = TimeUnit.MILLISECONDS.toNanos(testStartTime + 200), + attributes = null + ) + ), + checkNotNull( + EmbraceSpanEvent.create( + name = "delayed event", + timestampNanos = TimeUnit.MILLISECONDS.toNanos(testStartTime + 350), + attributes = null + ), + ) + ), + key = true + ) + assertEmbraceSpanData( + span = spansMap["record-span-span"], + expectedStartTimeMs = testStartTime + 100, + expectedEndTimeMs = testStartTime + 200, + expectedParentId = traceRootSpan.spanId, + expectedTraceId = traceRootSpan.traceId + ) + + assertEmbraceSpanData( + span = spansMap["completed-span"], + expectedStartTimeMs = testStartTime + 200, + expectedEndTimeMs = testStartTime + 400, + expectedParentId = traceRootSpan.spanId, + expectedTraceId = traceRootSpan.traceId, + expectedStatus = StatusCode.ERROR, + expectedCustomAttributes = mapOf(Pair("test-attr", "false")), + expectedEvents = listOf( + checkNotNull( + EmbraceSpanEvent.create( + name = "failure time", + timestampNanos = testStartTime + 400, + attributes = mapOf( + Pair("retry", "1") + ) + ) + ) + ) + ) + assertEmbraceSpanData( + span = sessionSpan, + expectedStartTimeMs = testStartTime + 100, + expectedEndTimeMs = sessionEndTime, + expectedParentId = SpanId.getInvalid(), + private = true + ) + } + } + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/AndroidManifest.xml b/embrace-android-sdk/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6a4872c3c1 --- /dev/null +++ b/embrace-android-sdk/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/embrace-android-sdk/src/main/baseline-prof.txt b/embrace-android-sdk/src/main/baseline-prof.txt new file mode 100644 index 0000000000..6ff64ccdf6 --- /dev/null +++ b/embrace-android-sdk/src/main/baseline-prof.txt @@ -0,0 +1,2835 @@ +HSPLio/embrace/android/embracesdk/AutomaticVerificationExceptionHandler;->(Ljava/lang/Thread$UncaughtExceptionHandler;)V +HSPLio/embrace/android/embracesdk/BuildInfo$Companion;->()V +HSPLio/embrace/android/embracesdk/BuildInfo$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/BuildInfo$Companion;->fromResources(Lio/embrace/android/embracesdk/internal/AndroidResourcesService;Ljava/lang/String;)Lio/embrace/android/embracesdk/BuildInfo; +HSPLio/embrace/android/embracesdk/BuildInfo$Companion;->getBuildResource(Lio/embrace/android/embracesdk/internal/AndroidResourcesService;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/BuildInfo;->()V +HSPLio/embrace/android/embracesdk/BuildInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/BuildInfo;->fromResources(Lio/embrace/android/embracesdk/internal/AndroidResourcesService;Ljava/lang/String;)Lio/embrace/android/embracesdk/BuildInfo; +HSPLio/embrace/android/embracesdk/BuildInfo;->getBuildFlavor()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/BuildInfo;->getBuildId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/BuildInfo;->getBuildType()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/Embrace$AppFramework;->()V +HSPLio/embrace/android/embracesdk/Embrace$AppFramework;->(Ljava/lang/String;II)V +HSPLio/embrace/android/embracesdk/Embrace$AppFramework;->getValue()I +HSPLio/embrace/android/embracesdk/Embrace$AppFramework;->values()[Lio/embrace/android/embracesdk/Embrace$AppFramework; +HSPLio/embrace/android/embracesdk/Embrace;->()V +HSPLio/embrace/android/embracesdk/Embrace;->()V +HSPLio/embrace/android/embracesdk/Embrace;->enableDebugLogging()V +HSPLio/embrace/android/embracesdk/Embrace;->generateW3cTraceparent()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/Embrace;->getConfigService()Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/Embrace;->getInstance()Lio/embrace/android/embracesdk/Embrace; +HSPLio/embrace/android/embracesdk/Embrace;->getTraceIdHeader()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/Embrace;->isStarted()Z +HSPLio/embrace/android/embracesdk/Embrace;->logInfo(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/Embrace;->logMessage(Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;)V +HSPLio/embrace/android/embracesdk/Embrace;->logMessage(Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;)V +HSPLio/embrace/android/embracesdk/Embrace;->recordNetworkRequest(Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest;)V +HSPLio/embrace/android/embracesdk/Embrace;->shouldCaptureNetworkBody(Ljava/lang/String;Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/Embrace;->start(Landroid/content/Context;Z)V +HSPLio/embrace/android/embracesdk/Embrace;->start(Landroid/content/Context;ZLio/embrace/android/embracesdk/Embrace$AppFramework;)V +HSPLio/embrace/android/embracesdk/Embrace;->verifyNonNullParameters(Ljava/lang/String;[Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/EmbraceCpuInfoDelegate;->(Lio/embrace/android/embracesdk/internal/SharedObjectLoader;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type$Companion$WhenMappings;->()V +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type$Companion;->()V +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type$Companion;->fromSeverity(Lio/embrace/android/embracesdk/Severity;)Lio/embrace/android/embracesdk/EmbraceEvent$Type; +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type;->()V +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type;->getAbbreviation()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/EmbraceEvent$Type;->values()[Lio/embrace/android/embracesdk/EmbraceEvent$Type; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda0;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda10;->(Lio/embrace/android/embracesdk/EmbraceImpl;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda11;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda1;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda2;->(Lio/embrace/android/embracesdk/EmbraceImpl;JJ)V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda2;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda3;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda4;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda5;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda6;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda7;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda7;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda8;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda9;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->()V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function11;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function5;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->generateW3cTraceparent()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/EmbraceImpl;->getConfigService()Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/EmbraceImpl;->getTraceIdHeader()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/EmbraceImpl;->isStarted()Z +HSPLio/embrace/android/embracesdk/EmbraceImpl;->lambda$startImpl$2()Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/EmbraceImpl;->lambda$startImpl$3$io-embrace-android-embracesdk-EmbraceImpl(JJ)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/EmbraceImpl;->loadCrashVerifier(Lio/embrace/android/embracesdk/injection/CrashModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->logMessage(Lio/embrace/android/embracesdk/EmbraceEvent$Type;Ljava/lang/String;Ljava/util/Map;Z[Ljava/lang/StackTraceElement;Ljava/lang/String;Lio/embrace/android/embracesdk/LogExceptionType;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->logMessage(Lio/embrace/android/embracesdk/EmbraceEvent$Type;Ljava/lang/String;Ljava/util/Map;Z[Ljava/lang/StackTraceElement;Ljava/lang/String;Lio/embrace/android/embracesdk/LogExceptionType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->logMessage(Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;Z)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->logNetworkRequestImpl(Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->normalizeProperties(Ljava/util/Map;)Ljava/util/Map; +HSPLio/embrace/android/embracesdk/EmbraceImpl;->onActivityReported()V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->recordNetworkRequest(Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->shouldCaptureNetworkCall(Ljava/lang/String;Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/EmbraceImpl;->start(Landroid/content/Context;ZLio/embrace/android/embracesdk/Embrace$AppFramework;)V +HSPLio/embrace/android/embracesdk/EmbraceImpl;->startImpl(Landroid/content/Context;ZLio/embrace/android/embracesdk/Embrace$AppFramework;)V +HSPLio/embrace/android/embracesdk/EmbraceInternalInterfaceImpl;->(Lio/embrace/android/embracesdk/EmbraceImpl;)V +HSPLio/embrace/android/embracesdk/EmbraceLogger$Severity;->()V +HSPLio/embrace/android/embracesdk/EmbraceLogger$Severity;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/EmbraceLogger$Severity;->values()[Lio/embrace/android/embracesdk/EmbraceLogger$Severity; +HSPLio/embrace/android/embracesdk/FlutterInternalInterfaceImpl;->(Lio/embrace/android/embracesdk/EmbraceImpl;Lio/embrace/android/embracesdk/EmbraceInternalInterface;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$embraceInternalInterface$2;->(Lio/embrace/android/embracesdk/EmbraceImpl;)V +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$embraceInternalInterface$2;->invoke()Lio/embrace/android/embracesdk/EmbraceInternalInterfaceImpl; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$embraceInternalInterface$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$flutterInternalInterface$2;->(Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl;Lio/embrace/android/embracesdk/EmbraceImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$flutterInternalInterface$2;->invoke()Lio/embrace/android/embracesdk/FlutterInternalInterfaceImpl; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$flutterInternalInterface$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$reactNativeInternalInterface$2;->(Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl;Lio/embrace/android/embracesdk/EmbraceImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/CrashModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;)V +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$reactNativeInternalInterface$2;->invoke()Lio/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$reactNativeInternalInterface$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$unityInternalInterface$2;->(Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl;Lio/embrace/android/embracesdk/EmbraceImpl;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$unityInternalInterface$2;->invoke()Lio/embrace/android/embracesdk/UnityInternalInterfaceImpl; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl$unityInternalInterface$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl;->()V +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl;->(Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/EmbraceImpl;Lio/embrace/android/embracesdk/injection/CrashModule;)V +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl;->getEmbraceInternalInterface()Lio/embrace/android/embracesdk/EmbraceInternalInterface; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl;->getFlutterInternalInterface()Lio/embrace/android/embracesdk/FlutterInternalInterface; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl;->getReactNativeInternalInterface()Lio/embrace/android/embracesdk/ReactNativeInternalInterface; +HSPLio/embrace/android/embracesdk/InternalInterfaceModuleImpl;->getUnityInternalInterface()Lio/embrace/android/embracesdk/UnityInternalInterface; +HSPLio/embrace/android/embracesdk/LogExceptionType;->()V +HSPLio/embrace/android/embracesdk/LogExceptionType;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLio/embrace/android/embracesdk/LogExceptionType;->getValue$embrace_android_sdk_release()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl;->(Lio/embrace/android/embracesdk/EmbraceImpl;Lio/embrace/android/embracesdk/EmbraceInternalInterface;Lio/embrace/android/embracesdk/Embrace$AppFramework;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/capture/crash/CrashService;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/SessionModuleImpl$backgroundActivityService$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DataContainerModule;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/injection/CustomerLogModule;Lio/embrace/android/embracesdk/injection/SdkObservabilityModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/ndk/NativeModule;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/SessionModuleImpl$backgroundActivityService$2;->invoke()Lio/embrace/android/embracesdk/EmbraceBackgroundActivityService; +HSPLio/embrace/android/embracesdk/SessionModuleImpl$backgroundActivityService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/SessionModuleImpl$sessionHandler$2;->(Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/ndk/NativeModule;Lio/embrace/android/embracesdk/injection/DataContainerModule;Lio/embrace/android/embracesdk/injection/CustomerLogModule;Lio/embrace/android/embracesdk/injection/SdkObservabilityModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/SessionModuleImpl$sessionHandler$2;->invoke()Lio/embrace/android/embracesdk/session/SessionHandler; +HSPLio/embrace/android/embracesdk/SessionModuleImpl$sessionHandler$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/SessionModuleImpl$sessionService$2;->(Lio/embrace/android/embracesdk/SessionModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/ndk/NativeModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/SessionModuleImpl$sessionService$2;->invoke()Lio/embrace/android/embracesdk/session/EmbraceSessionService; +HSPLio/embrace/android/embracesdk/SessionModuleImpl$sessionService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/SessionModuleImpl;->()V +HSPLio/embrace/android/embracesdk/SessionModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/ndk/NativeModule;Lio/embrace/android/embracesdk/injection/DataContainerModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/injection/CustomerLogModule;Lio/embrace/android/embracesdk/injection/SdkObservabilityModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/SessionModuleImpl;->getBackgroundActivityService()Lio/embrace/android/embracesdk/session/BackgroundActivityService; +HSPLio/embrace/android/embracesdk/SessionModuleImpl;->getSessionHandler()Lio/embrace/android/embracesdk/session/SessionHandler; +HSPLio/embrace/android/embracesdk/SessionModuleImpl;->getSessionService()Lio/embrace/android/embracesdk/session/SessionService; +HSPLio/embrace/android/embracesdk/Severity;->()V +HSPLio/embrace/android/embracesdk/Severity;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/Severity;->values()[Lio/embrace/android/embracesdk/Severity; +HSPLio/embrace/android/embracesdk/UnityInternalInterfaceImpl;->(Lio/embrace/android/embracesdk/EmbraceImpl;Lio/embrace/android/embracesdk/EmbraceInternalInterface;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler$Companion;->()V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->()V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/clock/Clock;Ljava/lang/Thread;Ljava/util/concurrent/atomic/AtomicReference;Ljava/util/concurrent/ExecutorService;)V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->findIntervalsWithSamples()Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->getAnrIntervals(Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;Lio/embrace/android/embracesdk/clock/Clock;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->onThreadBlocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->onThreadBlockedInterval(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->onThreadUnblocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->reachedAnrStacktraceCaptureLimit$embrace_android_sdk_release()Z +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->setConfigService(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/anr/AnrStacktraceSampler;->size$embrace_android_sdk_release()I +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$Companion;->()V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$getCapturedData$callable$1;->(Lio/embrace/android/embracesdk/anr/EmbraceAnrService;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$getCapturedData$callable$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$getCapturedData$callable$1;->call()Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$onForeground$1;->(Lio/embrace/android/embracesdk/anr/EmbraceAnrService;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$onForeground$1;->run()V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$startAnrCapture$1;->(Lio/embrace/android/embracesdk/anr/EmbraceAnrService;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService$startAnrCapture$1;->run()V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->()V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->(Lio/embrace/android/embracesdk/config/ConfigService;Landroid/os/Looper;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;Ljava/util/concurrent/ScheduledExecutorService;Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;Lio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;Lio/embrace/android/embracesdk/clock/Clock;Ljava/util/concurrent/atomic/AtomicReference;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->access$getAnrMonitorThread$p(Lio/embrace/android/embracesdk/anr/EmbraceAnrService;)Ljava/util/concurrent/atomic/AtomicReference; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->access$getState$p(Lio/embrace/android/embracesdk/anr/EmbraceAnrService;)Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->access$getTargetThreadHeartbeatScheduler$p(Lio/embrace/android/embracesdk/anr/EmbraceAnrService;)Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->finishInitialization(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->getAnrProcessErrors(J)Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->getCapturedData()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->getCapturedData()Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->getClock()Lio/embrace/android/embracesdk/clock/Clock; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->getStacktraceSampler()Lio/embrace/android/embracesdk/anr/AnrStacktraceSampler; +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->onThreadBlocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->onThreadBlockedInterval(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->onThreadUnblocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->processAnrTick$embrace_android_sdk_release(J)V +HSPLio/embrace/android/embracesdk/anr/EmbraceAnrService;->startAnrCapture()V +HSPLio/embrace/android/embracesdk/anr/ThreadInfoCollector;->(Ljava/lang/Thread;)V +HSPLio/embrace/android/embracesdk/anr/ThreadInfoCollector;->captureSample(Lio/embrace/android/embracesdk/config/ConfigService;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/ThreadInfoCollector;->clearStacktraceCache()V +HSPLio/embrace/android/embracesdk/anr/ThreadInfoCollector;->getAllowedThreads$embrace_android_sdk_release(Lio/embrace/android/embracesdk/config/ConfigService;)Ljava/util/Set; +HSPLio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;->(Landroid/app/ActivityManager;Lio/embrace/android/embracesdk/config/ConfigService;Ljava/util/concurrent/ScheduledExecutorService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;I)V +HSPLio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;->(Landroid/app/ActivityManager;Lio/embrace/android/embracesdk/config/ConfigService;Ljava/util/concurrent/ScheduledExecutorService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;IILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;->getAnrProcessErrors(J)Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;->isFeatureEnabled()Z +HSPLio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;->onThreadBlocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;->onThreadBlockedInterval(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler;->onThreadUnblocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/anr/BlockedThreadListener;Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;Ljava/lang/Thread;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Ljava/util/concurrent/atomic/AtomicReference;)V +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/anr/BlockedThreadListener;Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;Ljava/lang/Thread;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Ljava/util/concurrent/atomic/AtomicReference;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->getConfigService()Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->isAnrDurationThresholdExceeded$embrace_android_sdk_release(J)Z +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->isDebuggerEnabled()Z +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->onTargetThreadResponse(J)V +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->setConfigService(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->setListener(Lio/embrace/android/embracesdk/anr/BlockedThreadListener;)V +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->shouldAttemptAnrSample$embrace_android_sdk_release(J)Z +HSPLio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;->updateAnrTracking(J)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$1;->(Lio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$1;->invoke(J)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$sam$java_lang_Runnable$0;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$sam$java_lang_Runnable$0;->run()V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$scheduleRegularHeartbeats$runnable$1;->(Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$scheduleRegularHeartbeats$runnable$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$scheduleRegularHeartbeats$runnable$1;->invoke()V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->(Lio/embrace/android/embracesdk/config/ConfigService;Ljava/util/concurrent/ScheduledExecutorService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;Lio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Ljava/util/concurrent/atomic/AtomicReference;)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->(Lio/embrace/android/embracesdk/config/ConfigService;Ljava/util/concurrent/ScheduledExecutorService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;Lio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Ljava/util/concurrent/atomic/AtomicReference;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->getConfigService()Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->onMonitorThreadHeartbeat$embrace_android_sdk_release()V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->scheduleRegularHeartbeats()V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->sendHeartbeatMessage()V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->setConfigService(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->setListener(Lio/embrace/android/embracesdk/anr/BlockedThreadListener;)V +HSPLio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler;->startMonitoringThread()V +HSPLio/embrace/android/embracesdk/anr/detection/LooperCompat;->getMessageQueue(Landroid/os/Looper;)Landroid/os/MessageQueue; +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler$Companion;->()V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler$onMainThreadUnblocked$1;->(Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;J)V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler$onMainThreadUnblocked$1;->run()V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->()V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->(Landroid/os/Looper;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/atomic/AtomicReference;Lio/embrace/android/embracesdk/config/ConfigService;Landroid/os/MessageQueue;Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->(Landroid/os/Looper;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/atomic/AtomicReference;Lio/embrace/android/embracesdk/config/ConfigService;Landroid/os/MessageQueue;Lio/embrace/android/embracesdk/clock/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->access$getAnrMonitorThread$p(Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;)Ljava/util/concurrent/atomic/AtomicReference; +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->getAction()Lkotlin/jvm/functions/Function1; +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->handleMessage(Landroid/os/Message;)V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->onMainThreadUnblocked()V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->setAction(Lkotlin/jvm/functions/Function1;)V +HSPLio/embrace/android/embracesdk/anr/detection/TargetThreadHandler;->start()V +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->(Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->getAnrInProgress()Z +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->getLastMonitorThreadResponseMs()J +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->getLastTargetThreadResponseMs()J +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->resetState()V +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->setAnrInProgress(Z)V +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->setLastMonitorThreadResponseMs(J)V +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->setLastSampleAttemptMs(J)V +HSPLio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState;->setLastTargetThreadResponseMs(J)V +HSPLio/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector;->(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector;->checkTimeTravel(Ljava/lang/String;J)V +HSPLio/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector;->checkUnbalancedCall(Ljava/lang/String;Z)V +HSPLio/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector;->onThreadBlocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector;->onThreadBlockedInterval(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector;->onThreadUnblocked(Ljava/lang/Thread;J)V +HSPLio/embrace/android/embracesdk/anr/sigquit/FilesDelegate;->()V +HSPLio/embrace/android/embracesdk/anr/sigquit/FilesDelegate;->getCommandFileForThread(Ljava/lang/String;)Ljava/io/File; +HSPLio/embrace/android/embracesdk/anr/sigquit/FilesDelegate;->getThreadsFileForCurrentProcess()Ljava/io/File; +HSPLio/embrace/android/embracesdk/anr/sigquit/FindGoogleThread;->(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess;Lio/embrace/android/embracesdk/anr/sigquit/GetThreadCommand;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/FindGoogleThread;->invoke()I +HSPLio/embrace/android/embracesdk/anr/sigquit/GetThreadCommand;->(Lio/embrace/android/embracesdk/anr/sigquit/FilesDelegate;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/GetThreadCommand;->invoke(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess;->(Lio/embrace/android/embracesdk/anr/sigquit/FilesDelegate;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate;->(Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate;->install(I)I +HSPLio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository;->(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository;->getGoogleAnrTimestamps(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService$initializeGoogleAnrTracking$1;->(Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService$initializeGoogleAnrTracking$1;->onConfigChange(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService$setupGoogleAnrTracking$1;->(Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService$setupGoogleAnrTracking$1;->run()V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;->(Lio/embrace/android/embracesdk/internal/SharedObjectLoader;Lio/embrace/android/embracesdk/anr/sigquit/FindGoogleThread;Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate;Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;->access$setupGoogleAnrTracking(Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;->initializeGoogleAnrTracking()V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;->installGoogleAnrHandler(I)V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;->setConfigService(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;->setupGoogleAnrHandler()V +HSPLio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService;->setupGoogleAnrTracking()V +HSPLio/embrace/android/embracesdk/capture/EmbracePerformanceInfoService;->(Lio/embrace/android/embracesdk/anr/AnrService;Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService;Lio/embrace/android/embracesdk/network/logging/NetworkLoggingService;Lio/embrace/android/embracesdk/capture/powersave/PowerSaveModeService;Lio/embrace/android/embracesdk/capture/memory/MemoryService;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository;Lio/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService;Lio/embrace/android/embracesdk/capture/strictmode/StrictModeService;Lio/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService;)V +HSPLio/embrace/android/embracesdk/capture/EmbracePerformanceInfoService;->getPerformanceInfo(JJZ)Lio/embrace/android/embracesdk/payload/PerformanceInfo; +HSPLio/embrace/android/embracesdk/capture/EmbracePerformanceInfoService;->getSessionPerformanceInfo(JJZLjava/lang/Boolean;)Lio/embrace/android/embracesdk/payload/PerformanceInfo; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService$startService$1;->(Lio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;)V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService$startService$1;->run()V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->()V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->(Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/config/ConfigService;Landroid/app/ActivityManager;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;)V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->access$processApplicationExitInfo(Lio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;)V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->buildSessionAppExitInfoData(Landroid/app/ApplicationExitInfo;Ljava/lang/String;Ljava/lang/String;)Lio/embrace/android/embracesdk/payload/AppExitInfoData; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->collectExitInfoTrace(Landroid/app/ApplicationExitInfo;)Lio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior$CollectTracesResult; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->generateUniqueHash(Landroid/app/ApplicationExitInfo;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->getCapturedData()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->getCapturedData()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->getHistoricalProcessExitReasons()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->getSessionIdValidationError(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->getUnsentExitReasons(Ljava/util/List;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->onConfigChange(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->processApplicationExitInfo()V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->processApplicationExitInfoBlobs(Ljava/util/List;)V +HSPLio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService;->startService()V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService$ipAddress$2;->(Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService$registerConnectivityActionReceiver$1;->(Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService$registerConnectivityActionReceiver$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->(Landroid/content/Context;Lio/embrace/android/embracesdk/clock/Clock;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Landroid/net/ConnectivityManager;)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->access$getContext$p(Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;)Landroid/content/Context; +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->access$getIntentFilter$p(Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;)Landroid/content/IntentFilter; +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->addNetworkConnectivityListener(Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityListener;)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->getCapturedData()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->getCapturedData()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->getCurrentNetworkStatus()Lio/embrace/android/embracesdk/comms/delivery/NetworkStatus; +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->handleNetworkStatus$default(Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;ZJILjava/lang/Object;)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->handleNetworkStatus(ZJ)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->networkStatusOnSessionStarted(J)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->onReceive(Landroid/content/Context;Landroid/content/Intent;)V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->registerConnectivityActionReceiver()V +HSPLio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService;->saveStatus(JLio/embrace/android/embracesdk/comms/delivery/NetworkStatus;)Z +HSPLio/embrace/android/embracesdk/capture/crash/EmbraceCrashService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/crash/EmbraceCrashService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/crash/EmbraceCrashService;->()V +HSPLio/embrace/android/embracesdk/capture/crash/EmbraceCrashService;->(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/session/SessionService;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/capture/user/UserService;Lio/embrace/android/embracesdk/event/EventService;Lio/embrace/android/embracesdk/anr/AnrService;Lio/embrace/android/embracesdk/ndk/NdkService;Lio/embrace/android/embracesdk/gating/GatingService;Lio/embrace/android/embracesdk/session/BackgroundActivityService;Lio/embrace/android/embracesdk/internal/crash/CrashFileMarker;Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/capture/crash/EmbraceCrashService;->registerExceptionHandler()V +HSPLio/embrace/android/embracesdk/capture/crash/EmbraceUncaughtExceptionHandler;->(Ljava/lang/Thread$UncaughtExceptionHandler;Lio/embrace/android/embracesdk/capture/crash/CrashService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$2;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$3;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$3;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$4;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$4;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$5;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$5;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$6;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$6;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$7;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$7;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getCustomBreadcrumbsForSession$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;JJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getCustomBreadcrumbsForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getCustomBreadcrumbsForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getFragmentBreadcrumbsForSession$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;JJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getFragmentBreadcrumbsForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getFragmentBreadcrumbsForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getPushNotificationsBreadcrumbsForSession$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;JJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getPushNotificationsBreadcrumbsForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getPushNotificationsBreadcrumbsForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getRnActionBreadcrumbForSession$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;JJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getRnActionBreadcrumbForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getRnActionBreadcrumbForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getTapBreadcrumbsForSession$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;JJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getTapBreadcrumbsForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getTapBreadcrumbsForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getViewBreadcrumbsForSession$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;JJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getViewBreadcrumbsForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getViewBreadcrumbsForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getWebViewBreadcrumbsForSession$1;->(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;JJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getWebViewBreadcrumbsForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getWebViewBreadcrumbsForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->()V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->(Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->access$filterBreadcrumbsForTimeWindow(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;Ljava/util/Deque;JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->access$getRnActionBreadcrumbs$p(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)Ljava/util/concurrent/LinkedBlockingDeque; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->access$getTapBreadcrumbs$p(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)Ljava/util/concurrent/LinkedBlockingDeque; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->access$getViewBreadcrumbs$p(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;)Ljava/util/concurrent/LinkedBlockingDeque; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->access$isCacheValid(Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;Ljava/util/Deque;)I +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->addToViewLogsQueue(Ljava/lang/String;JZ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->filterBreadcrumbsForTimeWindow(Ljava/util/Deque;JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getBreadcrumbs(JJ)Lio/embrace/android/embracesdk/payload/Breadcrumbs; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getCustomBreadcrumbs()Ljava/util/concurrent/LinkedBlockingDeque; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getCustomBreadcrumbsForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getFragmentBreadcrumbs()Ljava/util/concurrent/LinkedBlockingDeque; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getFragmentBreadcrumbsForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getLastViewBreadcrumbScreenName()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getPushNotifications()Ljava/util/concurrent/LinkedBlockingDeque; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getPushNotificationsBreadcrumbsForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getRnActionBreadcrumbForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getTapBreadcrumbsForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getViewBreadcrumbsForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getWebViewBreadcrumbs()Ljava/util/concurrent/LinkedBlockingDeque; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->getWebViewBreadcrumbsForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->isCacheValid(Ljava/util/Deque;)I +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->logView(Ljava/lang/String;J)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->replaceFirstSessionView(Ljava/lang/String;J)V +HSPLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->tryAddBreadcrumb(Ljava/util/concurrent/LinkedBlockingDeque;Ljava/lang/Object;I)V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService$Utils;->()V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService$Utils;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->()V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->(Lio/embrace/android/embracesdk/capture/crumbs/BreadcrumbService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->isComingFromPushNotification(Landroid/app/Activity;)Z +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService;->()V +HSPLio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService;->(Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService;->getCapturedData()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService;->getCapturedData()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$1;->(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$3;->(Lio/embrace/android/embracesdk/BuildInfo;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$deviceIdentifier$1;->(Lio/embrace/android/embracesdk/prefs/PreferencesService;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$deviceIdentifier$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isAppUpdated$1;->(Lio/embrace/android/embracesdk/prefs/PreferencesService;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isAppUpdated$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isAppUpdated$1;->invoke()Z +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isOsUpdated$1;->(Lio/embrace/android/embracesdk/prefs/PreferencesService;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isOsUpdated$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isOsUpdated$1;->invoke()Z +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion;->ofContext(Landroid/content/Context;Lio/embrace/android/embracesdk/BuildInfo;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/Embrace$AppFramework;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/session/ActivityService;Ljava/util/concurrent/ExecutorService;Landroid/app/usage/StorageStatsManager;Landroid/view/WindowManager;Landroid/app/ActivityManager;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/CpuInfoDelegate;Lio/embrace/android/embracesdk/internal/DeviceArchitecture;)Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveDiskUsage$1;->(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;Z)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveDiskUsage$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveIsJailbroken$1;->(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveIsJailbroken$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveScreenResolution$1;->(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveScreenResolution$1;->run()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$statFs$1;->()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$statFs$1;->()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$statFs$1;->invoke()Landroid/os/StatFs; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$statFs$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->(Landroid/view/WindowManager;Landroid/content/pm/PackageManager;Landroid/app/usage/StorageStatsManager;Landroid/app/ActivityManager;Lio/embrace/android/embracesdk/BuildInfo;Lio/embrace/android/embracesdk/config/ConfigService;Landroid/content/pm/ApplicationInfo;Lkotlin/Lazy;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/Embrace$AppFramework;Lkotlin/Lazy;Lkotlin/Lazy;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/session/ActivityService;Lkotlin/Lazy;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/CpuInfoDelegate;Lio/embrace/android/embracesdk/internal/DeviceArchitecture;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->(Landroid/view/WindowManager;Landroid/content/pm/PackageManager;Landroid/app/usage/StorageStatsManager;Landroid/app/ActivityManager;Lio/embrace/android/embracesdk/BuildInfo;Lio/embrace/android/embracesdk/config/ConfigService;Landroid/content/pm/ApplicationInfo;Lkotlin/Lazy;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/Embrace$AppFramework;Lkotlin/Lazy;Lkotlin/Lazy;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/session/ActivityService;Lkotlin/Lazy;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/CpuInfoDelegate;Lio/embrace/android/embracesdk/internal/DeviceArchitecture;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getConfigService$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getDiskUsage$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Lio/embrace/android/embracesdk/payload/DiskUsage; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getPackageManager$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Landroid/content/pm/PackageManager; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getPackageName$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getPreferencesService$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Lio/embrace/android/embracesdk/prefs/PreferencesService; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getScreenResolution$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getStatFs$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Lkotlin/Lazy; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getStorageStatsManager$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Landroid/app/usage/StorageStatsManager; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$getWindowManager$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Landroid/view/WindowManager; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$isJailbroken$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;)Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$setDiskUsage$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;Lio/embrace/android/embracesdk/payload/DiskUsage;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$setJailbroken$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->access$setScreenResolution$p(Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->asyncRetrieveAdditionalDeviceInfo()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->asyncRetrieveDiskUsage(Z)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->asyncRetrieveIsJailbroken()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->asyncRetrieveScreenResolution()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getActiveSessionId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getAppId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getAppInfo()Lio/embrace/android/embracesdk/payload/AppInfo; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getAppInfo(Z)Lio/embrace/android/embracesdk/payload/AppInfo; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getAppState()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getAppVersionName()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getCpuName()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getDeviceId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getDeviceInfo()Lio/embrace/android/embracesdk/payload/DeviceInfo; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getDeviceInfo(Z)Lio/embrace/android/embracesdk/payload/DeviceInfo; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getDiskUsage()Lio/embrace/android/embracesdk/payload/DiskUsage; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getEgl()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->getScreenResolution()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->isJailbroken()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->precomputeValues()V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->setActiveSessionId(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->setSessionIdToProcessStateSummary(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->()V +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->appEnvironment(Landroid/content/pm/ApplicationInfo;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getDeviceDiskAppUsage(Landroid/app/usage/StorageStatsManager;Landroid/content/pm/PackageManager;Ljava/lang/String;)Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getDeviceManufacturer()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getInternalStorageFreeCapacity(Landroid/os/StatFs;)J +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getInternalStorageTotalCapacity(Landroid/os/StatFs;)J +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getLocale()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getModel()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getNumberOfCores()I +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getOperatingSystemType()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getOperatingSystemVersion()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getOperatingSystemVersionCode()I +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getScreenResolution(Landroid/view/WindowManager;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getSystemUptime()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->getTimezoneId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->isEmulator()Z +HSPLio/embrace/android/embracesdk/capture/metadata/MetadataUtils;->isJailbroken()Z +HSPLio/embrace/android/embracesdk/capture/orientation/NoOpOrientationService;->()V +HSPLio/embrace/android/embracesdk/capture/orientation/NoOpOrientationService;->onOrientationChanged(Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService$registerPowerSaveModeReceiver$1;->(Lio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;)V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService$registerPowerSaveModeReceiver$1;->run()V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->(Landroid/content/Context;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/clock/Clock;Landroid/os/PowerManager;)V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->access$getContext$p(Lio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;)Landroid/content/Context; +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->access$getPowerSaveIntentFilter$p(Lio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;)Landroid/content/IntentFilter; +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->access$getTag$p(Lio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->getCapturedData()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->getCapturedData()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->registerPowerSaveModeReceiver()V +HSPLio/embrace/android/embracesdk/capture/screenshot/EmbraceScreenshotService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/screenshot/EmbraceScreenshotService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/screenshot/EmbraceScreenshotService;->()V +HSPLio/embrace/android/embracesdk/capture/screenshot/EmbraceScreenshotService;->(Lio/embrace/android/embracesdk/session/ActivityService;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService;->()V +HSPLio/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService;->getCapturedData()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService;->getCapturedData()Ljava/util/List; +HSPLio/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService;->start()V +HSPLio/embrace/android/embracesdk/capture/thermalstate/NoOpThermalStatusService;->()V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->()V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->(Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->getUserInfo()Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->loadUserInfoFromDisk()Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService$Companion;->()V +HSPLio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService$webVitalType$1;->()V +HSPLio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService;->()V +HSPLio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService;->(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;)V +HSPLio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService;->getCapturedData()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService;->getCapturedData()Ljava/util/List; +HSPLio/embrace/android/embracesdk/clock/NormalizedIntervalClock;->(Lio/embrace/android/embracesdk/clock/SystemClock;)V +HSPLio/embrace/android/embracesdk/clock/NormalizedIntervalClock;->now()J +HSPLio/embrace/android/embracesdk/clock/SystemClock;->()V +HSPLio/embrace/android/embracesdk/clock/SystemClock;->now()J +HSPLio/embrace/android/embracesdk/comms/api/ApiClient$Companion;->()V +HSPLio/embrace/android/embracesdk/comms/api/ApiClient$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->()V +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->(Lio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;Lkotlin/jvm/functions/Function2;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->executeHttpRequest(Lio/embrace/android/embracesdk/comms/api/EmbraceConnection;)Lio/embrace/android/embracesdk/comms/api/ApiResponse; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->getCachedConfig()Lio/embrace/android/embracesdk/comms/api/CachedConfig; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->getConfig()Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->gzip([B)[B +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->handleRemoteConfigResponse(Lio/embrace/android/embracesdk/comms/api/ApiResponse;Lio/embrace/android/embracesdk/config/remote/RemoteConfig;)Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->post(Lio/embrace/android/embracesdk/comms/api/ApiRequest;[B)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->prepareConfigRequest(Ljava/lang/String;)Lio/embrace/android/embracesdk/comms/api/ApiRequest; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->rawPost(Lio/embrace/android/embracesdk/comms/api/ApiRequest;[B)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->readHttpResponseCode(Lio/embrace/android/embracesdk/comms/api/EmbraceConnection;)I +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->readHttpResponseHeaders(Lio/embrace/android/embracesdk/comms/api/EmbraceConnection;)Ljava/util/Map; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->readResponseBodyAsString(Ljava/io/InputStream;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiClient;->setTimeouts(Lio/embrace/android/embracesdk/comms/api/EmbraceConnection;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/comms/api/EmbraceUrl;Lio/embrace/android/embracesdk/network/http/HttpMethod;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/comms/api/EmbraceUrl;Lio/embrace/android/embracesdk/network/http/HttpMethod;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->copy$default(Lio/embrace/android/embracesdk/comms/api/ApiRequest;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/comms/api/EmbraceUrl;Lio/embrace/android/embracesdk/network/http/HttpMethod;Ljava/lang/String;ILjava/lang/Object;)Lio/embrace/android/embracesdk/comms/api/ApiRequest; +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->copy(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/comms/api/EmbraceUrl;Lio/embrace/android/embracesdk/network/http/HttpMethod;Ljava/lang/String;)Lio/embrace/android/embracesdk/comms/api/ApiRequest; +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->getHeaders()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->getHttpMethod()Lio/embrace/android/embracesdk/network/http/HttpMethod; +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->getUrl()Lio/embrace/android/embracesdk/comms/api/EmbraceUrl; +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->toConnection()Lio/embrace/android/embracesdk/comms/api/EmbraceConnection; +HSPLio/embrace/android/embracesdk/comms/api/ApiRequest;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiResponse;->(Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/Object;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponse;->getBody()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/api/ApiResponse;->getStatusCode()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache$Companion;->()V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache$cacheDir$2;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache$cacheDir$2;->invoke()Ljava/io/File; +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache$cacheDir$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache;->()V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache;->(Lio/embrace/android/embracesdk/internal/EmbraceSerializer;Lkotlin/jvm/functions/Function0;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache;->(Lio/embrace/android/embracesdk/internal/EmbraceSerializer;Lkotlin/jvm/functions/Function0;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache;->getCacheDir()Ljava/io/File; +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache;->initializeIfNeeded()V +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache;->retrieveCacheResponse(Ljava/lang/String;Lio/embrace/android/embracesdk/comms/api/ApiRequest;)Ljava/net/CacheResponse; +HSPLio/embrace/android/embracesdk/comms/api/ApiResponseCache;->retrieveCachedConfig(Ljava/lang/String;Lio/embrace/android/embracesdk/comms/api/ApiRequest;)Lio/embrace/android/embracesdk/comms/api/CachedConfig; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder$Companion;->()V +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->()V +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;ZZ)V +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->buildUrl(Ljava/lang/String;ILjava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getAppId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getAppVersion()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getBaseUrls()Lio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getConfigBaseUrl()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getConfigUrl()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getCoreBaseUrl()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getEmbraceUrlWithSuffix(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->getOperatingSystemCode()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;->isDebugBuild()Z +HSPLio/embrace/android/embracesdk/comms/api/CachedConfig;->(Lio/embrace/android/embracesdk/config/remote/RemoteConfig;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/api/CachedConfig;->getConfig()Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/comms/api/CachedConfig;->isValid()Z +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->(Ljava/net/HttpURLConnection;Lio/embrace/android/embracesdk/comms/api/EmbraceUrl;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->connect()V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->getHeaderFields()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->getInputStream()Ljava/io/InputStream; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->getOutputStream()Ljava/io/OutputStream; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->getResponseCode()I +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->setConnectTimeout(Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->setDoOutput(Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->setReadTimeout(Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->setRequestMethod(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrl$Companion;->()V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrl$Companion;->getUrl(Ljava/lang/String;)Lio/embrace/android/embracesdk/comms/api/EmbraceUrl; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrl;->()V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrl;->()V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrl;->access$getEmbraceUrlFactory$cp()Lio/embrace/android/embracesdk/comms/api/EmbraceUrl$UrlFactory; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter;->()V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter;->read(Lcom/google/gson/stream/JsonReader;)Lio/embrace/android/embracesdk/comms/api/EmbraceUrl; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter;->read(Lcom/google/gson/stream/JsonReader;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrlImpl;->(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrlImpl;->openConnection()Lio/embrace/android/embracesdk/comms/api/EmbraceConnection; +HSPLio/embrace/android/embracesdk/comms/api/EmbraceUrlImpl;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$CachedSession;->(Ljava/lang/String;J)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$CachedSession;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$CachedSession;->getFilename()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$CachedSession;->getSessionId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$Companion;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$deletePayload$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$deletePayload$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$loadFailedApiCalls$cached$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$loadFailedApiCalls$cached$1;->call()Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$loadFailedApiCalls$cached$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$saveBytes$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;Ljava/lang/String;[B)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$saveBytes$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$saveFailedApiCalls$$inlined$let$lambda$1;->([BLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$saveFailedApiCalls$$inlined$let$lambda$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$sessionMessageSerializer$2;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$sessionMessageSerializer$2;->invoke()Lio/embrace/android/embracesdk/session/SessionMessageSerializer; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$sessionMessageSerializer$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->(Lio/embrace/android/embracesdk/comms/delivery/CacheService;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->access$getCacheService$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)Lio/embrace/android/embracesdk/comms/delivery/CacheService; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->access$getCachedSessions$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)Ljava/util/Map; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->access$getClock$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)Lio/embrace/android/embracesdk/clock/Clock; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->access$getLogger$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->access$getSerializer$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;)Lio/embrace/android/embracesdk/internal/EmbraceSerializer; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->deletePayload(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->getAllCachedSessionIds()Ljava/util/List; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->getSessionMessageSerializer()Lio/embrace/android/embracesdk/session/SessionMessageSerializer; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->loadCrash()Lio/embrace/android/embracesdk/payload/EventMessage; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->loadFailedApiCalls()Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->loadPayload(Ljava/lang/String;)[B +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->saveBytes(Ljava/lang/String;[B)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->saveFailedApiCalls(Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;->saveSession(Lio/embrace/android/embracesdk/payload/SessionMessage;)[B +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCall;->getApiRequest()Lio/embrace/android/embracesdk/comms/api/ApiRequest; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCall;->getCachedPayload()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls;->getSize()I +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls;->size()I +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$1;->invoke()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$createRequest$url$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$createRequest$url$1;->invoke()Lio/embrace/android/embracesdk/comms/api/EmbraceUrl; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$createRequest$url$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$postOnExecutor$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;ZLio/embrace/android/embracesdk/comms/api/ApiRequest;[BLkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$postOnExecutor$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$retryQueue$2;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$retryQueue$2;->invoke()Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$retryQueue$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$scheduleFailedApiCallsRetry$$inlined$synchronized$lambda$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;J)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$scheduleFailedApiCallsRetry$$inlined$synchronized$lambda$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendLogs$url$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendLogs$url$1;->invoke()Lio/embrace/android/embracesdk/comms/api/EmbraceUrl; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendLogs$url$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendSession$url$1;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendSession$url$1;->invoke()Lio/embrace/android/embracesdk/comms/api/EmbraceUrl; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendSession$url$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->(Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/comms/api/ApiUrlBuilder;Lio/embrace/android/embracesdk/comms/api/ApiClient;Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/config/ConfigService;Ljava/util/concurrent/ScheduledExecutorService;Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;Lio/embrace/android/embracesdk/capture/user/UserService;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->access$getApiClient$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)Lio/embrace/android/embracesdk/comms/api/ApiClient; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->access$getCacheManager$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->access$getLastNetworkStatus$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)Lio/embrace/android/embracesdk/comms/delivery/NetworkStatus; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->access$getLogger$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->access$getRetryQueue$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->access$getUrlBuilder$p(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;)Lio/embrace/android/embracesdk/comms/api/ApiUrlBuilder; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->access$retryFailedApiCall(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCall;)Z +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->createRequest(Lio/embrace/android/embracesdk/payload/EventMessage;)Lio/embrace/android/embracesdk/comms/api/ApiRequest; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->eventBuilder(Lio/embrace/android/embracesdk/comms/api/EmbraceUrl;)Lio/embrace/android/embracesdk/comms/api/ApiRequest; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->getRetryQueue()Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->isRetryTaskActive()Z +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->pendingRetriesCount()I +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->postEvent(Lio/embrace/android/embracesdk/payload/EventMessage;Lio/embrace/android/embracesdk/comms/api/ApiRequest;)Ljava/util/concurrent/Future; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->postEvent(Lio/embrace/android/embracesdk/payload/EventMessage;Lio/embrace/android/embracesdk/comms/api/ApiRequest;Lkotlin/jvm/functions/Function0;)Ljava/util/concurrent/Future; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->postOnExecutor([BLio/embrace/android/embracesdk/comms/api/ApiRequest;ZLkotlin/jvm/functions/Function0;)Ljava/util/concurrent/Future; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->retryFailedApiCall(Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCall;)Z +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->scheduleFailedApiCallsRetry$default(Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;JILjava/lang/Object;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->scheduleFailedApiCallsRetry(J)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->sendEvent(Lio/embrace/android/embracesdk/payload/EventMessage;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->sendLogs(Lio/embrace/android/embracesdk/payload/EventMessage;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->sendSession([BLkotlin/jvm/functions/Function0;)Ljava/util/concurrent/Future; +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;->shouldScheduleRetry()Z +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManagerKt$sam$java_lang_Runnable$0;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManagerKt$sam$java_lang_Runnable$0;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$1;->(Landroid/content/Context;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$1;->invoke()Ljava/io/File; +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$Companion;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$listFilenamesByPrefix$1;->(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$listFilenamesByPrefix$1;->accept(Ljava/io/File;)Z +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService;->(Landroid/content/Context;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService;->cacheBytes(Ljava/lang/String;[B)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService;->deleteFile(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService;->listFilenamesByPrefix(Ljava/lang/String;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService;->loadBytes(Ljava/lang/String;)[B +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService;->loadObject(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$Companion;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$backgroundActivities$2;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$backgroundActivities$2;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendCachedSessionsWithoutNdk$1;->(Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendCachedSessionsWithoutNdk$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendEventAsync$1;->(Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;Lio/embrace/android/embracesdk/payload/EventMessage;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendEventAsync$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendSession$1;->(Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;Lio/embrace/android/embracesdk/payload/SessionMessage;Lio/embrace/android/embracesdk/comms/delivery/SessionMessageState;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendSession$1;->run()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->(Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager;Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->access$getCacheManager$p(Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;)Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager; +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->access$getLogger$p(Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;)Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger; +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->access$getNetworkManager$p(Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;)Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager; +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->access$sendCachedSessions(Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;Ljava/util/List;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->saveSession(Lio/embrace/android/embracesdk/payload/SessionMessage;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->sendCachedCrash()V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->sendCachedSessions(Ljava/util/List;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->sendCachedSessions(ZLio/embrace/android/embracesdk/ndk/NdkService;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->sendCachedSessionsWithoutNdk(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->sendEventAsync(Lio/embrace/android/embracesdk/payload/EventMessage;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->sendLogs(Lio/embrace/android/embracesdk/payload/EventMessage;)V +HSPLio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService;->sendSession(Lio/embrace/android/embracesdk/payload/SessionMessage;Lio/embrace/android/embracesdk/comms/delivery/SessionMessageState;)V +HSPLio/embrace/android/embracesdk/comms/delivery/NetworkStatus;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/NetworkStatus;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLio/embrace/android/embracesdk/comms/delivery/NetworkStatus;->getValue()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/comms/delivery/SessionMessageState;->()V +HSPLio/embrace/android/embracesdk/comms/delivery/SessionMessageState;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$2;->(Lio/embrace/android/embracesdk/prefs/PreferencesService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$2;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$Companion;->()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$anrBehavior$1;->(Lio/embrace/android/embracesdk/config/local/SdkLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$anrBehavior$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$anrBehavior$2;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$anrBehavior$2;->invoke()Lio/embrace/android/embracesdk/config/remote/AnrRemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$anrBehavior$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$appExitInfoBehavior$1;->(Lio/embrace/android/embracesdk/config/local/SdkLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$appExitInfoBehavior$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$autoDataCaptureBehavior$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$autoDataCaptureBehavior$1;->invoke()Lio/embrace/android/embracesdk/config/local/LocalConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$autoDataCaptureBehavior$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$backgroundActivityBehavior$1;->(Lio/embrace/android/embracesdk/config/local/SdkLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$backgroundActivityBehavior$2;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$backgroundActivityBehavior$2;->invoke()Lio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$backgroundActivityBehavior$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$breadcrumbBehavior$1;->(Lio/embrace/android/embracesdk/config/local/LocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$breadcrumbBehavior$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$logMessageBehavior$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$logMessageBehavior$1;->invoke()Lio/embrace/android/embracesdk/config/remote/LogRemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$logMessageBehavior$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$networkBehavior$1;->(Lio/embrace/android/embracesdk/config/local/LocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$networkBehavior$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$networkSpanForwardingBehavior$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$networkSpanForwardingBehavior$1;->invoke()Lio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$networkSpanForwardingBehavior$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$performInitialConfigLoad$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$performInitialConfigLoad$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$refreshConfig$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;Lio/embrace/android/embracesdk/config/remote/RemoteConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$refreshConfig$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$remoteSupplier$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$remoteSupplier$1;->invoke()Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$remoteSupplier$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sdkEndpointBehavior$1;->(Lio/embrace/android/embracesdk/config/local/SdkLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sdkEndpointBehavior$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sdkModeBehavior$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sdkModeBehavior$1;->invoke()Lio/embrace/android/embracesdk/config/local/LocalConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sdkModeBehavior$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sessionBehavior$1;->(Lio/embrace/android/embracesdk/config/local/SdkLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sessionBehavior$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sessionBehavior$2;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sessionBehavior$2;->invoke()Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$sessionBehavior$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$spansBehavior$1;->(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$spansBehavior$1;->invoke()Lio/embrace/android/embracesdk/config/remote/SpansRemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$spansBehavior$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$startupBehavior$1;->(Lio/embrace/android/embracesdk/config/local/SdkLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService$startupBehavior$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->(Lio/embrace/android/embracesdk/config/local/LocalConfig;Lkotlin/jvm/functions/Function0;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Ljava/util/concurrent/ExecutorService;ZLkotlin/jvm/functions/Function0;Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->(Lio/embrace/android/embracesdk/config/local/LocalConfig;Lkotlin/jvm/functions/Function0;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Ljava/util/concurrent/ExecutorService;ZLkotlin/jvm/functions/Function0;Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$configRequiresRefresh(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)Z +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$getApiClientProvider$p(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)Lkotlin/jvm/functions/Function0; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$getClock$p(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)Lio/embrace/android/embracesdk/clock/Clock; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$getConfig(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$getConfigProp$p(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$getLocalConfig$p(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)Lio/embrace/android/embracesdk/config/local/LocalConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$getLogger$p(Lio/embrace/android/embracesdk/config/EmbraceConfigService;)Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$setConfigRetrySafeWindow$p(Lio/embrace/android/embracesdk/config/EmbraceConfigService;D)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$setLastRefreshConfigAttempt$p(Lio/embrace/android/embracesdk/config/EmbraceConfigService;J)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->access$updateConfig(Lio/embrace/android/embracesdk/config/EmbraceConfigService;Lio/embrace/android/embracesdk/config/remote/RemoteConfig;Lio/embrace/android/embracesdk/config/remote/RemoteConfig;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->addListener(Lio/embrace/android/embracesdk/config/ConfigListener;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->attemptConfigRefresh()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->configRequiresRefresh()Z +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->configRetryIsSafe()Z +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getAnrBehavior()Lio/embrace/android/embracesdk/config/behavior/AnrBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getAppExitInfoBehavior()Lio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getAutoDataCaptureBehavior()Lio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getBackgroundActivityBehavior()Lio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getBreadcrumbBehavior()Lio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getConfig()Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getDataCaptureEventBehavior()Lio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getLogMessageBehavior()Lio/embrace/android/embracesdk/config/behavior/LogMessageBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getNetworkBehavior()Lio/embrace/android/embracesdk/config/behavior/NetworkBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getNetworkSpanForwardingBehavior()Lio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getSdkEndpointBehavior()Lio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getSdkModeBehavior()Lio/embrace/android/embracesdk/config/behavior/SdkModeBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getSessionBehavior()Lio/embrace/android/embracesdk/config/behavior/SessionBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getSpansBehavior()Lio/embrace/android/embracesdk/config/behavior/SpansBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getStartupBehavior()Lio/embrace/android/embracesdk/config/behavior/StartupBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->getWebViewVitalsBehavior()Lio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior; +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->isAppExitInfoCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->isBackgroundActivityCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->isSdkDisabled()Z +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->loadConfigFromCache()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->notifyListeners()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->performInitialConfigLoad()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->persistConfig()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->refreshConfig()V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->setLastUpdated(J)V +HSPLio/embrace/android/embracesdk/config/EmbraceConfigService;->updateConfig(Lio/embrace/android/embracesdk/config/remote/RemoteConfig;Lio/embrace/android/embracesdk/config/remote/RemoteConfig;)V +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$allowPatternList$2;->(Lio/embrace/android/embracesdk/config/behavior/AnrBehavior;)V +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$allowPatternList$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$allowPatternList$2;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$blockPatternList$2;->(Lio/embrace/android/embracesdk/config/behavior/AnrBehavior;)V +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$blockPatternList$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior$blockPatternList$2;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getAllowPatternList()Ljava/util/List; +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getAnrProcessErrorsIntervalMs()J +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getBlockPatternList()Ljava/util/List; +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getMaxAnrIntervalsPerSession()I +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getMaxStacktracesPerInterval()I +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getMinDuration()I +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getMinThreadPriority()I +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getMonitorThreadPriority()I +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getSamplingIntervalMs()J +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->getStacktraceFrameLimit()I +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->isAnrCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->isAnrProcessErrorsCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->isBgAnrCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->isGoogleAnrCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->isIdleHandlerEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->isStrictModeListenerEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AnrBehavior;->shouldCaptureMainThreadOnly()Z +HSPLio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior;->appExitInfoMaxNum()I +HSPLio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior;->isEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isAnrServiceEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isComposeOnClickEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isDiskUsageReportingEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isMemoryServiceEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isNdkEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isNetworkConnectivityServiceEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isPowerSaveModeServiceEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior;->isUncaughtExceptionHandlerEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior;->isEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;->getNormalizedDeviceId()F +HSPLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;->getNormalizedDeviceId(I)F +HSPLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;->getNormalizedLargeDeviceId()F +HSPLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;->isBehaviorEnabled(F)Z +HSPLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;->isBehaviorEnabled(Ljava/lang/Float;)Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;->isBehaviorEnabled(Ljava/lang/Integer;)Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior;->getViewBreadcrumbLimit()I +HSPLio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior;->isActivityBreadcrumbCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior$2;->()V +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior$2;->()V +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior;->getEventLimits()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior;->isEventEnabled(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior;->isInternalExceptionCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior;->isLogMessageEnabled(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior;->isMessageTypeEnabled(Lio/embrace/android/embracesdk/internal/MessageType;)Z +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior;->getInfoLogLimit()I +HSPLio/embrace/android/embracesdk/config/behavior/LogMessageBehavior;->getLogMessageMaximumAllowedLength()I +HSPLio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior$2;->()V +HSPLio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior$2;->()V +HSPLio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior;->getLocal()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior;->getRemote()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior;->getThresholdCheck()Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck; +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->getNetworkCallLimitsPerDomain()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->getNetworkCaptureLimit()I +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->getNetworkCaptureRules()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->getTraceIdHeader()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->isNativeNetworkingMonitoringEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->isRequestContentLengthCaptureEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->isUrlEnabled(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/config/behavior/NetworkBehavior;->transformLocalDomainCfg()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior;->isNetworkSpanForwardingEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior;->getConfig()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior;->getData()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior$appId$2;->(Lio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;)V +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior$appId$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior$appId$2;->invoke()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->(ZLio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->getAppId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->getOffset()I +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->getThreshold()I +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->isBetaFeaturesEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->isIntegrationModeEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/SdkModeBehavior;->isSdkDisabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->getMaxSessionSecondsAllowed()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->getSessionComponents()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->isSessionErrorLogStrictModeEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->shouldGateFeature(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->shouldGateInfoLog()Z +HSPLio/embrace/android/embracesdk/config/behavior/SessionBehavior;->shouldGateStartupMoment()Z +HSPLio/embrace/android/embracesdk/config/behavior/SpansBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SpansBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SpansBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SpansBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/SpansBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/SpansBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/SpansBehavior;->isSpansEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior;->isAutomaticEndEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/StartupBehavior;->isTakingScreenshotEnabled()Z +HSPLio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior$1;->()V +HSPLio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior$Companion;->()V +HSPLio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior;->()V +HSPLio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior;->(Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior;->getMaxWebViewVitals()I +HSPLio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig;->getConfig()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig;->getData()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig;->(Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig;->(Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig;->getEnabled()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/LocalConfig$Companion;->()V +HSPLio/embrace/android/embracesdk/config/local/LocalConfig$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/local/LocalConfig$Companion;->buildConfig(Ljava/lang/String;ZLjava/lang/String;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;)Lio/embrace/android/embracesdk/config/local/LocalConfig; +HSPLio/embrace/android/embracesdk/config/local/LocalConfig$Companion;->fromResources(Lio/embrace/android/embracesdk/internal/AndroidResourcesService;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;)Lio/embrace/android/embracesdk/config/local/LocalConfig; +HSPLio/embrace/android/embracesdk/config/local/LocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/LocalConfig;->(Ljava/lang/String;ZLio/embrace/android/embracesdk/config/local/SdkLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/local/LocalConfig;->getAppId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/local/LocalConfig;->getNdkEnabled()Z +HSPLio/embrace/android/embracesdk/config/local/LocalConfig;->getSdkConfig()Lio/embrace/android/embracesdk/config/local/SdkLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->(Ljava/lang/String;Ljava/lang/Integer;Ljava/util/List;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->(Ljava/lang/String;Ljava/lang/Integer;Ljava/util/List;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->getCaptureRequestContentLength()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->getDefaultCaptureLimit()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->getDisabledUrlPatterns()Ljava/util/List; +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->getDomains()Ljava/util/List; +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->getEnableNativeMonitoring()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/NetworkLocalConfig;->getTraceIdHeader()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->(Lio/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig;Lio/embrace/android/embracesdk/config/local/TapsLocalConfig;Lio/embrace/android/embracesdk/config/local/ViewLocalConfig;Lio/embrace/android/embracesdk/config/local/WebViewLocalConfig;Ljava/lang/Boolean;Ljava/lang/Boolean;Lio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig;Lio/embrace/android/embracesdk/config/local/ComposeLocalConfig;Ljava/lang/Boolean;Lio/embrace/android/embracesdk/config/local/NetworkLocalConfig;Ljava/lang/String;Lio/embrace/android/embracesdk/config/local/AnrLocalConfig;Lio/embrace/android/embracesdk/config/local/AppLocalConfig;Lio/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfig;Lio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig;Lio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig;Lio/embrace/android/embracesdk/config/local/SessionLocalConfig;Ljava/lang/Boolean;Lio/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig;)V +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->(Lio/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig;Lio/embrace/android/embracesdk/config/local/TapsLocalConfig;Lio/embrace/android/embracesdk/config/local/ViewLocalConfig;Lio/embrace/android/embracesdk/config/local/WebViewLocalConfig;Ljava/lang/Boolean;Ljava/lang/Boolean;Lio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig;Lio/embrace/android/embracesdk/config/local/ComposeLocalConfig;Ljava/lang/Boolean;Lio/embrace/android/embracesdk/config/local/NetworkLocalConfig;Ljava/lang/String;Lio/embrace/android/embracesdk/config/local/AnrLocalConfig;Lio/embrace/android/embracesdk/config/local/AppLocalConfig;Lio/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfig;Lio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig;Lio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig;Lio/embrace/android/embracesdk/config/local/SessionLocalConfig;Ljava/lang/Boolean;Lio/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getAnr()Lio/embrace/android/embracesdk/config/local/AnrLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getApp()Lio/embrace/android/embracesdk/config/local/AppLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getAppExitInfoConfig()Lio/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getAutomaticDataCaptureConfig()Lio/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getBaseUrls()Lio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getBetaFeaturesEnabled()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getComposeConfig()Lio/embrace/android/embracesdk/config/local/ComposeLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getCrashHandler()Lio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getIntegrationModeEnabled()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getNetworking()Lio/embrace/android/embracesdk/config/local/NetworkLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getSessionConfig()Lio/embrace/android/embracesdk/config/local/SessionLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getStartupMoment()Lio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SdkLocalConfig;->getViewConfig()Lio/embrace/android/embracesdk/config/local/ViewLocalConfig; +HSPLio/embrace/android/embracesdk/config/local/SessionLocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/SessionLocalConfig;->(Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/util/Set;Ljava/util/Set;Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/config/local/SessionLocalConfig;->(Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/util/Set;Ljava/util/Set;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/local/SessionLocalConfig;->getMaxSessionSeconds()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/local/SessionLocalConfig;->getSessionComponents()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/config/local/SessionLocalConfig;->getSessionEnableErrorLogStrictMode()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig;->(Ljava/lang/Boolean;Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig;->(Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig;->getAutomaticallyEnd()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig;->getTakeScreenshot()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/local/TapsLocalConfig;->()V +HSPLio/embrace/android/embracesdk/config/local/TapsLocalConfig;->(Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/config/local/TapsLocalConfig;->(Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig$AllowedNdkSampleMethod;->(Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->()V +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Float;Ljava/lang/Boolean;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Float;Ljava/lang/Boolean;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getAnrPerSession()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getGooglePctEnabled()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getMainThreadOnly()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getMaxStacktracesPerInterval()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getMinDuration()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getMinThreadPriority()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getMonitorThreadPriority()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getPctAnrProcessErrorsEnabled()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getPctBgEnabled()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getPctEnabled()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getSampleIntervalMs()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;->getStacktraceFrameLimit()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig;->()V +HSPLio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig;->(Ljava/lang/Float;)V +HSPLio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig;->(Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig;->getThreshold()Ljava/lang/Float; +HSPLio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig;->()V +HSPLio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig;->(Ljava/lang/Float;)V +HSPLio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig;->(Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig;->getPctEnabled()Ljava/lang/Float; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->()V +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/util/Map;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Lio/embrace/android/embracesdk/config/remote/UiRemoteConfig;Lio/embrace/android/embracesdk/config/remote/NetworkRemoteConfig;Lio/embrace/android/embracesdk/config/remote/SessionRemoteConfig;Lio/embrace/android/embracesdk/config/remote/LogRemoteConfig;Lio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;Lio/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig;Ljava/lang/Boolean;Ljava/lang/Float;Lio/embrace/android/embracesdk/config/remote/AppExitInfoConfig;Lio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig;Ljava/lang/Integer;Lio/embrace/android/embracesdk/config/remote/SpansRemoteConfig;Lio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig;Lio/embrace/android/embracesdk/config/remote/WebViewVitals;)V +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/util/Map;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Lio/embrace/android/embracesdk/config/remote/UiRemoteConfig;Lio/embrace/android/embracesdk/config/remote/NetworkRemoteConfig;Lio/embrace/android/embracesdk/config/remote/SessionRemoteConfig;Lio/embrace/android/embracesdk/config/remote/LogRemoteConfig;Lio/embrace/android/embracesdk/config/remote/AnrRemoteConfig;Lio/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig;Ljava/lang/Boolean;Ljava/lang/Float;Lio/embrace/android/embracesdk/config/remote/AppExitInfoConfig;Lio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig;Ljava/lang/Integer;Lio/embrace/android/embracesdk/config/remote/SpansRemoteConfig;Lio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig;Lio/embrace/android/embracesdk/config/remote/WebViewVitals;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getAnrConfig()Lio/embrace/android/embracesdk/config/remote/AnrRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getAppExitInfoConfig()Lio/embrace/android/embracesdk/config/remote/AppExitInfoConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getBackgroundActivityConfig()Lio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getDisabledEventAndLogPatterns()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getDisabledMessageTypes()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getDisabledUrlPatterns()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getEventLimits()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getInternalExceptionCaptureEnabled()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getKillSwitchConfig()Lio/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getLogConfig()Lio/embrace/android/embracesdk/config/remote/LogRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getNetworkCaptureRules()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getNetworkConfig()Lio/embrace/android/embracesdk/config/remote/NetworkRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getNetworkSpanForwardingRemoteConfig()Lio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getOffset()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getPctBetaFeaturesEnabled()Ljava/lang/Float; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getSessionConfig()Lio/embrace/android/embracesdk/config/remote/SessionRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getSpansConfig()Lio/embrace/android/embracesdk/config/remote/SpansRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getThreshold()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getUiConfig()Lio/embrace/android/embracesdk/config/remote/UiRemoteConfig; +HSPLio/embrace/android/embracesdk/config/remote/RemoteConfig;->getWebViewVitals()Lio/embrace/android/embracesdk/config/remote/WebViewVitals; +HSPLio/embrace/android/embracesdk/config/remote/SessionRemoteConfig;->()V +HSPLio/embrace/android/embracesdk/config/remote/SessionRemoteConfig;->(Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/Set;Ljava/util/Set;)V +HSPLio/embrace/android/embracesdk/config/remote/SessionRemoteConfig;->(Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/SpansRemoteConfig;->()V +HSPLio/embrace/android/embracesdk/config/remote/SpansRemoteConfig;->(Ljava/lang/Float;)V +HSPLio/embrace/android/embracesdk/config/remote/SpansRemoteConfig;->(Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/SpansRemoteConfig;->getPctEnabled()Ljava/lang/Float; +HSPLio/embrace/android/embracesdk/config/remote/UiRemoteConfig;->()V +HSPLio/embrace/android/embracesdk/config/remote/UiRemoteConfig;->(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/config/remote/UiRemoteConfig;->(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/WebViewVitals;->()V +HSPLio/embrace/android/embracesdk/config/remote/WebViewVitals;->(Ljava/lang/Float;Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/config/remote/WebViewVitals;->(Ljava/lang/Float;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/config/remote/WebViewVitals;->getMaxVitals()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$Companion;->()V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$Companion;->getInternalEventKey$embrace_android_sdk_release(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$Companion;->isStartupEvent$embrace_android_sdk_release(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$eventIdsCache$1;->(Lio/embrace/android/embracesdk/event/EmbraceEventService;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$eventIdsCache$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$findEventIdsForSession$1;->(Lio/embrace/android/embracesdk/event/EmbraceEventService;JJ)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$findEventIdsForSession$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$findEventIdsForSession$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$logStartupSpan$1;->(Lio/embrace/android/embracesdk/event/EmbraceEventService;J)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$logStartupSpan$1;->run()V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService$startEvent$eventDescription$1;->(Lio/embrace/android/embracesdk/event/EmbraceEventService;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->()V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->(JLio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/capture/PerformanceInfoService;Lio/embrace/android/embracesdk/capture/user/UserService;Lio/embrace/android/embracesdk/capture/screenshot/ScreenshotService;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/internal/spans/SpansService;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->access$getEventIds$p(Lio/embrace/android/embracesdk/event/EmbraceEventService;)Ljava/util/NavigableMap; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->access$getSTARTUP_SPAN_NAME$cp()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->access$getSpansService$p(Lio/embrace/android/embracesdk/event/EmbraceEventService;)Lio/embrace/android/embracesdk/internal/spans/SpansService; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->access$getStartupStartTime$p(Lio/embrace/android/embracesdk/event/EmbraceEventService;)J +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->endEvent(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->endEvent(Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->findEventIdsForSession(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->getStartupMomentInfo()Lio/embrace/android/embracesdk/internal/StartupEventInfo; +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->logStartupSpan()V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->sendStartupMoment()V +HSPLio/embrace/android/embracesdk/event/EmbraceEventService;->startEvent(Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$Companion;->()V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$Companion;->getWrappedStackTrace$default(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$Companion;[Ljava/lang/StackTraceElement;ILjava/lang/Object;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$Companion;->getWrappedStackTrace([Ljava/lang/StackTraceElement;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$WhenMappings;->()V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$errorLogIdsCache$1;->(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$errorLogIdsCache$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$findLogIds$1;->(Ljava/util/NavigableMap;JJ)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$findLogIds$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$findLogIds$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$infoLogIdsCache$1;->(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$infoLogIdsCache$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$log$1;->(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceEvent$Type;JLio/embrace/android/embracesdk/Embrace$AppFramework;Lio/embrace/android/embracesdk/LogExceptionType;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/payload/UserInfo;Lio/embrace/android/embracesdk/payload/Stacktraces;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$log$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$networkLogIdsCache$1;->(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$networkLogIdsCache$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$warningLogIdsCache$1;->(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger$warningLogIdsCache$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->()V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->(Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/capture/screenshot/ScreenshotService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/capture/user/UserService;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/gating/GatingService;Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService;Ljava/util/concurrent/ExecutorService;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->(Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/capture/screenshot/ScreenshotService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/capture/user/UserService;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/clock/Clock;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/gating/GatingService;Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getClock$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Lio/embrace/android/embracesdk/clock/Clock; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getConfigService$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getDeliveryService$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Lio/embrace/android/embracesdk/comms/delivery/DeliveryService; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getErrorLogIds$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Ljava/util/NavigableMap; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getGatingService$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Lio/embrace/android/embracesdk/gating/GatingService; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getInfoLogIds$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Ljava/util/NavigableMap; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getLock$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getLogsInfoCount$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Ljava/util/concurrent/atomic/AtomicInteger; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getMetadataService$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Lio/embrace/android/embracesdk/capture/metadata/MetadataService; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getNetworkLogIds$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Ljava/util/NavigableMap; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getSessionProperties$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Lio/embrace/android/embracesdk/session/EmbraceSessionProperties; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->access$getWarningLogIds$p(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;)Ljava/util/NavigableMap; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->checkIfShouldGateLog(Lio/embrace/android/embracesdk/EmbraceEvent$Type;)Z +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->findErrorLogIds(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->findInfoLogIds(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->findLogIds(JJLio/embrace/android/embracesdk/internal/CacheableValue;Ljava/util/NavigableMap;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->findNetworkLogIds(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->findWarningLogIds(JJ)Ljava/util/List; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->getErrorLogsAttemptedToSend()I +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->getInfoLogsAttemptedToSend()I +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->getUnhandledExceptionsSent()I +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->getWarnLogsAttemptedToSend()I +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->log(Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceEvent$Type;ZLio/embrace/android/embracesdk/LogExceptionType;Ljava/util/Map;[Ljava/lang/StackTraceElement;Ljava/lang/String;Lio/embrace/android/embracesdk/Embrace$AppFramework;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->processLogMessage$default(Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;Ljava/lang/String;IILjava/lang/Object;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/event/EmbraceRemoteLogger;->processLogMessage(Ljava/lang/String;I)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/event/EventHandler;->(Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/capture/user/UserService;Lio/embrace/android/embracesdk/capture/screenshot/ScreenshotService;Lio/embrace/android/embracesdk/capture/PerformanceInfoService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/clock/Clock;Ljava/util/concurrent/ScheduledExecutorService;)V +HSPLio/embrace/android/embracesdk/event/EventHandler;->buildEndEvent(Lio/embrace/android/embracesdk/payload/Event;JJZZLio/embrace/android/embracesdk/session/EmbraceSessionProperties;Ljava/util/Map;)Lio/embrace/android/embracesdk/payload/Event; +HSPLio/embrace/android/embracesdk/event/EventHandler;->buildEndEventMessage(Lio/embrace/android/embracesdk/payload/Event;JJ)Lio/embrace/android/embracesdk/payload/EventMessage; +HSPLio/embrace/android/embracesdk/event/EventHandler;->buildStartEvent(Ljava/lang/String;Ljava/lang/String;JJLio/embrace/android/embracesdk/session/EmbraceSessionProperties;Ljava/util/Map;)Lio/embrace/android/embracesdk/payload/Event; +HSPLio/embrace/android/embracesdk/event/EventHandler;->buildStartEventMessage(Lio/embrace/android/embracesdk/payload/Event;)Lio/embrace/android/embracesdk/payload/EventMessage; +HSPLio/embrace/android/embracesdk/event/EventHandler;->buildStartupEventInfo(Lio/embrace/android/embracesdk/payload/Event;Lio/embrace/android/embracesdk/payload/Event;)Lio/embrace/android/embracesdk/internal/StartupEventInfo; +HSPLio/embrace/android/embracesdk/event/EventHandler;->calculateLateThreshold(Ljava/lang/String;)J +HSPLio/embrace/android/embracesdk/event/EventHandler;->calculateOffset(JJ)J +HSPLio/embrace/android/embracesdk/event/EventHandler;->handleScreenshot(ZLio/embrace/android/embracesdk/internal/EventDescription;)Z +HSPLio/embrace/android/embracesdk/event/EventHandler;->isAllowedToEnd()Z +HSPLio/embrace/android/embracesdk/event/EventHandler;->isAllowedToStart(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/event/EventHandler;->onEventEnded(Lio/embrace/android/embracesdk/internal/EventDescription;ZLjava/util/Map;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;)Lio/embrace/android/embracesdk/payload/EventMessage; +HSPLio/embrace/android/embracesdk/event/EventHandler;->onEventStarted(Ljava/lang/String;Ljava/lang/String;JZLio/embrace/android/embracesdk/session/EmbraceSessionProperties;Ljava/util/Map;Ljava/lang/Runnable;)Lio/embrace/android/embracesdk/internal/EventDescription; +HSPLio/embrace/android/embracesdk/event/EventHandler;->shouldSendMoment(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/event/EventHandler;->shouldTakeScreenshot(ZLio/embrace/android/embracesdk/internal/EventDescription;)Z +HSPLio/embrace/android/embracesdk/gating/EmbraceGatingService;->(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/gating/EmbraceGatingService;->gateEventMessage(Lio/embrace/android/embracesdk/payload/EventMessage;)Lio/embrace/android/embracesdk/payload/EventMessage; +HSPLio/embrace/android/embracesdk/gating/EmbraceGatingService;->gateSessionMessage(Lio/embrace/android/embracesdk/payload/SessionMessage;)Lio/embrace/android/embracesdk/payload/SessionMessage; +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2$lazyPrefs$1;->(Lio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2;)V +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2$lazyPrefs$1;->invoke()Landroid/content/SharedPreferences; +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2$lazyPrefs$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2;->(Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2;->invoke()Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService; +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl;->getPreferencesService()Lio/embrace/android/embracesdk/prefs/PreferencesService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrExecutorService$2;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrExecutorService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrExecutorService$2;->invoke()Ljava/util/concurrent/ScheduledExecutorService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrMonitorThreadFactory$1;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrMonitorThreadFactory$1;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrProcessErrorSampler$2;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrProcessErrorSampler$2;->invoke()Lio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrProcessErrorSampler$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrService$2;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrService$2;->invoke()Lio/embrace/android/embracesdk/anr/AnrService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$anrService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$blockedThreadDetector$2;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$blockedThreadDetector$2;->invoke()Lio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$blockedThreadDetector$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$googleAnrTimestampRepository$2;->(Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$googleAnrTimestampRepository$2;->invoke()Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$googleAnrTimestampRepository$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$livenessCheckScheduler$2;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$livenessCheckScheduler$2;->invoke()Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$livenessCheckScheduler$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$looper$2;->()V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$looper$2;->()V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$looper$2;->invoke()Landroid/os/Looper; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$looper$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$sigquitDetectionService$2;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$sigquitDetectionService$2;->invoke()Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$sigquitDetectionService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$state$2;->(Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$state$2;->invoke()Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$state$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$targetThreadHandler$2;->(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$targetThreadHandler$2;->invoke()Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl$targetThreadHandler$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getAnrExecutorService$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Ljava/util/concurrent/ScheduledExecutorService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getAnrMonitorThread$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Ljava/util/concurrent/atomic/AtomicReference; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getAnrMonitorThreadFactory$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Ljava/util/concurrent/ThreadFactory; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getAnrProcessErrorSampler$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Lio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getBlockedThreadDetector$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Lio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getConfigService$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getLivenessCheckScheduler$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getLooper$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Landroid/os/Looper; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getSigquitDetectionService$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getState$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->access$getTargetThreadHandler$p(Lio/embrace/android/embracesdk/injection/AnrModuleImpl;)Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getAnrExecutorService()Ljava/util/concurrent/ScheduledExecutorService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getAnrProcessErrorSampler()Lio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getAnrService()Lio/embrace/android/embracesdk/anr/AnrService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getBlockedThreadDetector()Lio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getGoogleAnrTimestampRepository()Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getLivenessCheckScheduler()Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getLooper()Landroid/os/Looper; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getSigquitDetectionService()Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getState()Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState; +HSPLio/embrace/android/embracesdk/injection/AnrModuleImpl;->getTargetThreadHandler()Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$application$2;->(Lio/embrace/android/embracesdk/injection/CoreModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$application$2;->invoke()Landroid/app/Application; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$application$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$context$2;->(Landroid/content/Context;)V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$context$2;->invoke()Landroid/content/Context; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$context$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$isDebug$2;->(Lio/embrace/android/embracesdk/injection/CoreModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$isDebug$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$isDebug$2;->invoke()Z +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$jsonSerializer$2;->()V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$jsonSerializer$2;->()V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$jsonSerializer$2;->invoke()Lio/embrace/android/embracesdk/internal/EmbraceSerializer; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$jsonSerializer$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$resources$2;->(Lio/embrace/android/embracesdk/injection/CoreModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$resources$2;->invoke()Lio/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$resources$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$serviceRegistry$2;->(Lio/embrace/android/embracesdk/injection/CoreModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$serviceRegistry$2;->invoke()Lio/embrace/android/embracesdk/registry/ServiceRegistry; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl$serviceRegistry$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->(Landroid/content/Context;Lio/embrace/android/embracesdk/Embrace$AppFramework;)V +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->getAppFramework()Lio/embrace/android/embracesdk/Embrace$AppFramework; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->getApplication()Landroid/app/Application; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->getContext()Landroid/content/Context; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->getJsonSerializer()Lio/embrace/android/embracesdk/internal/EmbraceSerializer; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->getLogger()Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->getResources()Lio/embrace/android/embracesdk/internal/AndroidResourcesService; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->getServiceRegistry()Lio/embrace/android/embracesdk/registry/ServiceRegistry; +HSPLio/embrace/android/embracesdk/injection/CoreModuleImpl;->isDebug()Z +HSPLio/embrace/android/embracesdk/injection/CoreModuleKt;->isDebug(Landroid/content/pm/ApplicationInfo;)Z +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$automaticVerificationExceptionHandler$2;->()V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$automaticVerificationExceptionHandler$2;->()V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$automaticVerificationExceptionHandler$2;->invoke()Lio/embrace/android/embracesdk/AutomaticVerificationExceptionHandler; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$automaticVerificationExceptionHandler$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2$markerFile$1;->(Lio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2;)V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2$markerFile$1;->invoke()Ljava/io/File; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2$markerFile$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2;->(Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2;->invoke()Lio/embrace/android/embracesdk/internal/crash/CrashFileMarker; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashService$2;->(Lio/embrace/android/embracesdk/injection/CrashModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/SessionModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/injection/DataContainerModule;Lio/embrace/android/embracesdk/injection/AnrModule;Lio/embrace/android/embracesdk/ndk/NativeModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashService$2;->invoke()Lio/embrace/android/embracesdk/capture/crash/EmbraceCrashService; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$crashService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$lastRunCrashVerifier$2;->(Lio/embrace/android/embracesdk/injection/CrashModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$lastRunCrashVerifier$2;->invoke()Lio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl$lastRunCrashVerifier$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/ndk/NativeModule;Lio/embrace/android/embracesdk/SessionModule;Lio/embrace/android/embracesdk/injection/AnrModule;Lio/embrace/android/embracesdk/injection/DataContainerModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl;->access$getCrashMarker$p(Lio/embrace/android/embracesdk/injection/CrashModuleImpl;)Lio/embrace/android/embracesdk/internal/crash/CrashFileMarker; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl;->getAutomaticVerificationExceptionHandler()Lio/embrace/android/embracesdk/AutomaticVerificationExceptionHandler; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl;->getCrashMarker()Lio/embrace/android/embracesdk/internal/crash/CrashFileMarker; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl;->getCrashService()Lio/embrace/android/embracesdk/capture/crash/CrashService; +HSPLio/embrace/android/embracesdk/injection/CrashModuleImpl;->getLastRunCrashVerifier()Lio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkCaptureService$2;->(Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkCaptureService$2;->invoke()Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkCaptureService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkLoggingService$2;->(Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkLoggingService$2;->invoke()Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkLoggingService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$remoteLogger$2;->(Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$remoteLogger$2;->invoke()Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$remoteLogger$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$screenshotService$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$screenshotService$2;->invoke()Lio/embrace/android/embracesdk/capture/screenshot/EmbraceScreenshotService; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$screenshotService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;->getNetworkCaptureService()Lio/embrace/android/embracesdk/network/logging/NetworkCaptureService; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;->getNetworkLoggingService()Lio/embrace/android/embracesdk/network/logging/NetworkLoggingService; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;->getRemoteLogger()Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger; +HSPLio/embrace/android/embracesdk/injection/CustomerLogModuleImpl;->getScreenshotService()Lio/embrace/android/embracesdk/capture/screenshot/ScreenshotService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$activityLifecycleBreadcrumbService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/utils/VersionChecker;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$activityLifecycleBreadcrumbService$2;->invoke()Lio/embrace/android/embracesdk/capture/crumbs/activity/EmbraceActivityLifecycleBreadcrumbService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$activityLifecycleBreadcrumbService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$breadcrumbService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$breadcrumbService$2;->invoke()Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$breadcrumbService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$memoryService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$memoryService$2;->invoke()Lio/embrace/android/embracesdk/capture/memory/MemoryService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$memoryService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$networkConnectivityService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$networkConnectivityService$2;->invoke()Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$networkConnectivityService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$powerSaveModeService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/utils/VersionChecker;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$powerSaveModeService$2;->invoke()Lio/embrace/android/embracesdk/capture/powersave/PowerSaveModeService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$powerSaveModeService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$pushNotificationService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$pushNotificationService$2;->invoke()Lio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$pushNotificationService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$strictModeService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/utils/VersionChecker;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$strictModeService$2;->invoke()Lio/embrace/android/embracesdk/capture/strictmode/StrictModeService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$strictModeService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$thermalStatusService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/utils/VersionChecker;Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$thermalStatusService$2;->invoke()Lio/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$thermalStatusService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$webviewService$2;->(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$webviewService$2;->invoke()Lio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$webviewService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/utils/VersionChecker;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/utils/VersionChecker;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->access$getBackgroundExecutorService$p(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;)Ljava/util/concurrent/ExecutorService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->access$getConfigService$p(Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;)Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getActivityLifecycleBreadcrumbService()Lio/embrace/android/embracesdk/capture/crumbs/activity/ActivityLifecycleBreadcrumbService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getActivityLifecycleBreadcrumbService()Lio/embrace/android/embracesdk/capture/crumbs/activity/EmbraceActivityLifecycleBreadcrumbService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getBreadcrumbService()Lio/embrace/android/embracesdk/capture/crumbs/BreadcrumbService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getMemoryService()Lio/embrace/android/embracesdk/capture/memory/MemoryService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getNetworkConnectivityService()Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getPowerSaveModeService()Lio/embrace/android/embracesdk/capture/powersave/PowerSaveModeService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getPushNotificationService()Lio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getStrictModeService()Lio/embrace/android/embracesdk/capture/strictmode/StrictModeService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getThermalStatusService()Lio/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService; +HSPLio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl;->getWebviewService()Lio/embrace/android/embracesdk/capture/webview/WebViewService; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$applicationExitInfoService$2;->(Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;)V +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$applicationExitInfoService$2;->invoke()Lio/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$applicationExitInfoService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$eventService$2;->(Lio/embrace/android/embracesdk/injection/DataContainerModuleImpl;JLio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/CustomerLogModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$eventService$2;->invoke()Lio/embrace/android/embracesdk/event/EmbraceEventService; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$eventService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$performanceInfoService$2;->(Lio/embrace/android/embracesdk/injection/DataContainerModuleImpl;Lio/embrace/android/embracesdk/injection/AnrModule;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/injection/CustomerLogModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/ndk/NativeModule;)V +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$performanceInfoService$2;->invoke()Lio/embrace/android/embracesdk/capture/EmbracePerformanceInfoService; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl$performanceInfoService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/injection/AnrModule;Lio/embrace/android/embracesdk/injection/CustomerLogModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/ndk/NativeModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;J)V +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl;->getApplicationExitInfoService()Lio/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl;->getEventService()Lio/embrace/android/embracesdk/event/EventService; +HSPLio/embrace/android/embracesdk/injection/DataContainerModuleImpl;->getPerformanceInfoService()Lio/embrace/android/embracesdk/capture/PerformanceInfoService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$cacheService$2;->(Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$cacheService$2;->invoke()Lio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$cacheService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryCacheManager$2;->(Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryCacheManager$2;->invoke()Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryCacheManager$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryNetworkManager$2;->(Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryNetworkManager$2;->invoke()Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryNetworkManager$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryService$2;->(Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryService$2;->invoke()Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->access$getApiRetryExecutor$p(Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl;)Ljava/util/concurrent/ScheduledExecutorService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->access$getCachedSessionsExecutorService$p(Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl;)Ljava/util/concurrent/ExecutorService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->access$getDeliveryCacheExecutorService$p(Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl;)Ljava/util/concurrent/ExecutorService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->access$getSendSessionsExecutorService$p(Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl;)Ljava/util/concurrent/ExecutorService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->getCacheService()Lio/embrace/android/embracesdk/comms/delivery/CacheService; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->getDeliveryCacheManager()Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->getDeliveryNetworkManager()Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager; +HSPLio/embrace/android/embracesdk/injection/DeliveryModuleImpl;->getDeliveryService()Lio/embrace/android/embracesdk/comms/delivery/DeliveryService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$activityService$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$activityService$2;->invoke()Lio/embrace/android/embracesdk/session/EmbraceActivityService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$activityService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2$1;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2$1;->invoke(Ljava/lang/String;Lio/embrace/android/embracesdk/comms/api/ApiRequest;)Lio/embrace/android/embracesdk/comms/api/CachedConfig; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2;->invoke()Lio/embrace/android/embracesdk/comms/api/ApiClient; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2$1;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2$1;->invoke()Ljava/io/File; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2;->(Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2;->invoke()Lio/embrace/android/embracesdk/comms/api/ApiResponseCache; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2$1;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2$1;->invoke()Lio/embrace/android/embracesdk/comms/api/ApiClient; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Ljava/lang/String;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2;->invoke()Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cpuInfoDelegate$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cpuInfoDelegate$2;->invoke()Lio/embrace/android/embracesdk/EmbraceCpuInfoDelegate; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cpuInfoDelegate$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$gatingService$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$gatingService$2;->invoke()Lio/embrace/android/embracesdk/gating/EmbraceGatingService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$gatingService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$memoryCleanerService$2;->()V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$memoryCleanerService$2;->()V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$memoryCleanerService$2;->invoke()Lio/embrace/android/embracesdk/session/EmbraceMemoryCleanerService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$memoryCleanerService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$metadataService$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/BuildInfo;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$metadataService$2;->invoke()Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$metadataService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$orientationService$2;->()V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$orientationService$2;->()V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$orientationService$2;->invoke()Lio/embrace/android/embracesdk/capture/orientation/NoOpOrientationService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$orientationService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$sharedObjectLoader$2;->()V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$sharedObjectLoader$2;->()V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$sharedObjectLoader$2;->invoke()Lio/embrace/android/embracesdk/internal/SharedObjectLoader; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$sharedObjectLoader$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$urlBuilder$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;ZLio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$urlBuilder$2;->invoke()Lio/embrace/android/embracesdk/comms/api/ApiUrlBuilder; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$urlBuilder$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$userService$2;->(Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$userService$2;->invoke()Lio/embrace/android/embracesdk/capture/user/EmbraceUserService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$userService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;Lio/embrace/android/embracesdk/injection/SystemServiceModule;Lio/embrace/android/embracesdk/injection/AndroidServicesModule;Lio/embrace/android/embracesdk/BuildInfo;Ljava/lang/String;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lio/embrace/android/embracesdk/internal/DeviceArchitecture;)V +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->access$getBackgroundExecutorService$p(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;)Ljava/util/concurrent/ExecutorService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->access$getConfigServiceProvider$p(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;)Lkotlin/jvm/functions/Function0; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->access$getConfigStopAction$p(Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;)Lkotlin/jvm/functions/Function0; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getActivityService()Lio/embrace/android/embracesdk/session/ActivityService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getApiClient()Lio/embrace/android/embracesdk/comms/api/ApiClient; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getCache()Lio/embrace/android/embracesdk/comms/api/ApiResponseCache; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getConfigService()Lio/embrace/android/embracesdk/config/ConfigService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getCpuInfoDelegate()Lio/embrace/android/embracesdk/CpuInfoDelegate; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getDeviceArchitecture()Lio/embrace/android/embracesdk/internal/DeviceArchitecture; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getGatingService()Lio/embrace/android/embracesdk/gating/GatingService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getMemoryCleanerService()Lio/embrace/android/embracesdk/session/MemoryCleanerService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getMetadataService()Lio/embrace/android/embracesdk/capture/metadata/MetadataService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getOrientationService()Lio/embrace/android/embracesdk/capture/orientation/OrientationService; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getSharedObjectLoader()Lio/embrace/android/embracesdk/internal/SharedObjectLoader; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getUrlBuilder()Lio/embrace/android/embracesdk/comms/api/ApiUrlBuilder; +HSPLio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl;->getUserService()Lio/embrace/android/embracesdk/capture/user/UserService; +HSPLio/embrace/android/embracesdk/injection/InitModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/InitModuleImpl;->(Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/internal/spans/SpansService;)V +HSPLio/embrace/android/embracesdk/injection/InitModuleImpl;->(Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/internal/spans/SpansService;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/injection/InitModuleImpl;->getClock()Lio/embrace/android/embracesdk/clock/Clock; +HSPLio/embrace/android/embracesdk/injection/InitModuleImpl;->getSpansService()Lio/embrace/android/embracesdk/internal/spans/SpansService; +HSPLio/embrace/android/embracesdk/injection/LoadType;->()V +HSPLio/embrace/android/embracesdk/injection/LoadType;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$exceptionService$2;->(Lio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/InitModule;)V +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$exceptionService$2;->invoke()Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService; +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$exceptionService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$internalErrorLogger$2;->(Lio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$internalErrorLogger$2;->invoke()Lio/embrace/android/embracesdk/logging/InternalErrorLogger; +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$internalErrorLogger$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$logStrictMode$2;->(Lio/embrace/android/embracesdk/injection/EssentialServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$logStrictMode$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$logStrictMode$2;->invoke()Z +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;->(Lio/embrace/android/embracesdk/injection/InitModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;)V +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;->access$getLogStrictMode$p(Lio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;)Z +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;->getExceptionService()Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService; +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;->getInternalErrorLogger()Lio/embrace/android/embracesdk/logging/InternalErrorLogger; +HSPLio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl;->getLogStrictMode()Z +HSPLio/embrace/android/embracesdk/injection/SingletonDelegate;->(Lio/embrace/android/embracesdk/injection/LoadType;Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/injection/SingletonDelegate;->getValue()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SingletonDelegate;->getValue(Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$activityManager$2;->(Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$activityManager$2;->invoke()Landroid/app/ActivityManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$activityManager$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$connectivityManager$2;->(Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$connectivityManager$2;->invoke()Landroid/net/ConnectivityManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$connectivityManager$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$powerManager$2;->(Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$powerManager$2;->invoke()Landroid/os/PowerManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$powerManager$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$windowManager$2;->(Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;)V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$windowManager$2;->invoke()Landroid/view/WindowManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$windowManager$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->()V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->(Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->(Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/utils/VersionChecker;)V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->(Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/utils/VersionChecker;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->access$getSystemServiceSafe(Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;Ljava/lang/String;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->getActivityManager()Landroid/app/ActivityManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->getConnectivityManager()Landroid/net/ConnectivityManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->getPowerManager()Landroid/os/PowerManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->getStorageManager()Landroid/app/usage/StorageStatsManager; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->getSystemServiceSafe(Ljava/lang/String;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/injection/SystemServiceModuleImpl;->getWindowManager()Landroid/view/WindowManager; +HSPLio/embrace/android/embracesdk/internal/ApkToolsConfig;->()V +HSPLio/embrace/android/embracesdk/internal/ApkToolsConfig;->()V +HSPLio/embrace/android/embracesdk/internal/CacheableValue;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/internal/CacheableValue;->value(Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/DeviceArchitectureImpl;->()V +HSPLio/embrace/android/embracesdk/internal/DeviceArchitectureImpl;->getArchitecture()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService;->(Landroid/content/Context;)V +HSPLio/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService;->getIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I +HSPLio/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService;->getString(I)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer$gson$1;->()V +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer$gson$1;->initialValue()Lcom/google/gson/Gson; +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer$gson$1;->initialValue()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer;->()V +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer;->bytesFromPayload(Ljava/lang/Object;Ljava/lang/Class;)[B +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer;->fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer;->loadObject(Lcom/google/gson/stream/JsonReader;Ljava/lang/Class;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/EmbraceSerializer;->toJson(Ljava/lang/Object;Ljava/lang/reflect/Type;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/EventDescription;->(Ljava/util/concurrent/Future;Lio/embrace/android/embracesdk/payload/Event;Z)V +HSPLio/embrace/android/embracesdk/internal/EventDescription;->getEvent()Lio/embrace/android/embracesdk/payload/Event; +HSPLio/embrace/android/embracesdk/internal/EventDescription;->getLateTimer()Ljava/util/concurrent/Future; +HSPLio/embrace/android/embracesdk/internal/MessageType;->()V +HSPLio/embrace/android/embracesdk/internal/MessageType;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/internal/OpenTelemetryClock;->(Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/internal/OpenTelemetryClock;->nanoTime()J +HSPLio/embrace/android/embracesdk/internal/OpenTelemetryClock;->now()J +HSPLio/embrace/android/embracesdk/internal/PatternCache;->()V +HSPLio/embrace/android/embracesdk/internal/SharedObjectLoader;->()V +HSPLio/embrace/android/embracesdk/internal/SharedObjectLoader;->loadEmbraceNative()Z +HSPLio/embrace/android/embracesdk/internal/StartupEventInfo;->(Ljava/lang/Long;Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/internal/StartupEventInfo;->getDuration()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/internal/StartupEventInfo;->getThreshold()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/internal/Systrace$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/Systrace$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/internal/Systrace$Companion;->end()V +HSPLio/embrace/android/embracesdk/internal/Systrace$Companion;->start(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/internal/Systrace;->()V +HSPLio/embrace/android/embracesdk/internal/ThreadEnforcementCheckKt;->enforceThread(Ljava/util/concurrent/atomic/AtomicReference;)V +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator$Companion;->generateW3CTraceparent()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator;->()V +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator;->(Lkotlin/random/Random;)V +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator;->(Lkotlin/random/Random;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator;->access$getINSTANCE$cp()Lio/embrace/android/embracesdk/internal/TraceparentGenerator; +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator;->generate()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator;->generateW3CTraceparent()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/TraceparentGenerator;->validRandomLong()J +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker;->()V +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker;->(Lkotlin/Lazy;)V +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker;->getAndCleanMarker()Z +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker;->isMarked()Z +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker;->markerFileExists()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/internal/crash/CrashFileMarker;->removeMark()V +HSPLio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier$readAndCleanMarkerAsync$1;->(Lio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier;)V +HSPLio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier$readAndCleanMarkerAsync$1;->call()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier$readAndCleanMarkerAsync$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier;->(Lio/embrace/android/embracesdk/internal/crash/CrashFileMarker;)V +HSPLio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier;->access$readAndCleanMarker(Lio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier;)Z +HSPLio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier;->readAndCleanMarker()Z +HSPLio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier;->readAndCleanMarkerAsync(Ljava/util/concurrent/ExecutorService;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Attribute$DefaultImpls;->keyName(Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Attribute;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;->()V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;->getCanonicalName()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;->keyName()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->embraceSpanBuilder(Lio/opentelemetry/api/trace/Tracer;Ljava/lang/String;Z)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->endSpan(Lio/opentelemetry/api/trace/Span;Lio/embrace/android/embracesdk/spans/ErrorCode;Ljava/lang/Long;)Lio/opentelemetry/api/trace/Span; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->fromMap(Lio/opentelemetry/api/common/AttributesBuilder;Ljava/util/Map;)Lio/opentelemetry/api/common/AttributesBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->makeKey(Lio/opentelemetry/api/trace/SpanBuilder;)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->makePrivate(Lio/opentelemetry/api/trace/SpanBuilder;)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->setSequenceId(Lio/opentelemetry/api/trace/Span;J)Lio/opentelemetry/api/trace/Span; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->setType(Lio/opentelemetry/api/trace/SpanBuilder;Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->toEmbraceSpanName(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt;->updateParent(Lio/opentelemetry/api/trace/SpanBuilder;Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData$Companion;->fromEventData(Ljava/util/List;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData;->()V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData;->(Lio/opentelemetry/sdk/trace/data/SpanData;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JJLio/opentelemetry/api/trace/StatusCode;Ljava/util/List;Ljava/util/Map;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanExporter;->(Lio/embrace/android/embracesdk/internal/spans/SpansService;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanExporter;->export(Ljava/util/Collection;)Lio/opentelemetry/sdk/common/CompletableResultCode; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl$Companion;->inputsValid$embrace_android_sdk_release(Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)Z +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl;->()V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor;->(Lio/opentelemetry/sdk/trace/export/SpanExporter;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor;->onEnd(Lio/opentelemetry/sdk/trace/ReadableSpan;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor;->onStart(Lio/opentelemetry/context/Context;Lio/opentelemetry/sdk/trace/ReadWriteSpan;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpansService;->(Lio/opentelemetry/sdk/common/Clock;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpansService;->completedSpans()Ljava/util/List; +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpansService;->initializeService(JJ)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpansService;->initialized()Z +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpansService;->onConfigChange(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpansService;->recordBufferedCalls()V +HSPLio/embrace/android/embracesdk/internal/spans/EmbraceSpansService;->recordCompletedSpan(Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/EmbraceSpan;Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;ZLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)Z +HSPLio/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService;->()V +HSPLio/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService;->completedSpans()Ljava/util/List; +HSPLio/embrace/android/embracesdk/internal/spans/SpansService$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/spans/SpansService$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/spans/SpansService$Companion;->getFeatureDisabledSpansService()Lio/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService; +HSPLio/embrace/android/embracesdk/internal/spans/SpansService$DefaultImpls;->recordCompletedSpan$default(Lio/embrace/android/embracesdk/internal/spans/SpansService;Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/EmbraceSpan;Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;ZLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;ILjava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/internal/spans/SpansService;->()V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$Companion;->()V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$openTelemetry$2;->(Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;)V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$openTelemetry$2;->invoke()Lio/opentelemetry/sdk/OpenTelemetrySdk; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$openTelemetry$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$sdkTracerProvider$2;->(Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;)V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$sdkTracerProvider$2;->invoke()Lio/opentelemetry/sdk/trace/SdkTracerProvider; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$sdkTracerProvider$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$tracer$2;->(Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;)V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$tracer$2;->invoke()Lio/opentelemetry/api/trace/Tracer; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$tracer$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->()V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->(JJLio/opentelemetry/sdk/common/Clock;)V +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->access$getClock$p(Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;)Lio/opentelemetry/sdk/common/Clock; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->access$getOpenTelemetry$p(Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;)Lio/opentelemetry/api/OpenTelemetry; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->access$getSdkTracerProvider$p(Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;)Lio/opentelemetry/sdk/trace/SdkTracerProvider; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->completedSpans()Ljava/util/List; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->createEmbraceSpanBuilder$default(Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;Ljava/lang/String;Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;ZILjava/lang/Object;)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->createEmbraceSpanBuilder(Ljava/lang/String;Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;Z)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->createRootSpanBuilder(Ljava/lang/String;Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;Z)Lio/opentelemetry/api/trace/SpanBuilder; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->getOpenTelemetry()Lio/opentelemetry/api/OpenTelemetry; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->getSdkTracerProvider()Lio/opentelemetry/sdk/trace/SdkTracerProvider; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->getTracer()Lio/opentelemetry/api/trace/Tracer; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->recordCompletedSpan(Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/EmbraceSpan;Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type;ZLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)Z +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->startSessionSpan(J)Lio/opentelemetry/api/trace/Span; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->storeCompletedSpans(Ljava/util/List;)Lio/opentelemetry/sdk/common/CompletableResultCode; +HSPLio/embrace/android/embracesdk/internal/spans/SpansServiceImpl;->validateAndUpdateContext(Lio/embrace/android/embracesdk/spans/EmbraceSpan;Z)Z +HSPLio/embrace/android/embracesdk/internal/utils/Uuid;->()V +HSPLio/embrace/android/embracesdk/internal/utils/Uuid;->()V +HSPLio/embrace/android/embracesdk/internal/utils/Uuid;->getEmbUuid$default(Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/internal/utils/Uuid;->getEmbUuid(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/logging/AndroidLogger$WhenMappings;->()V +HSPLio/embrace/android/embracesdk/logging/AndroidLogger;->()V +HSPLio/embrace/android/embracesdk/logging/AndroidLogger;->log(Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceLogger$Severity;Ljava/lang/Throwable;Z)V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$Companion;->()V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionClasses$2;->()V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionClasses$2;->()V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionClasses$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionClasses$2;->invoke()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionStrings$2;->(Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;)V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionStrings$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionStrings$2;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->()V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->(Lio/embrace/android/embracesdk/session/ActivityService;Lio/embrace/android/embracesdk/clock/Clock;Z)V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->access$getIgnoredExceptionClasses$p(Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;)Ljava/util/Set; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->getApplicationState()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->getCurrentExceptionError()Lio/embrace/android/embracesdk/payload/ExceptionError; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->getIgnoredExceptionClasses()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->getIgnoredExceptionStrings()Ljava/util/List; +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->handleInternalError(Ljava/lang/Throwable;)V +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->ignoreThrowableCause(Ljava/lang/Throwable;Ljava/util/HashSet;)Z +HSPLio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;->setConfigService(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->()V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->access$getLoggerActions$p(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)Ljava/util/concurrent/CopyOnWriteArrayList; +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->addLoggerAction(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger$LoggerAction;)V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->log(Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceLogger$Severity;Ljava/lang/Throwable;Z)V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->logDeveloper$default(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->logDeveloper(Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->logInfo(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->setToDefault$embrace_android_sdk_release()V +HSPLio/embrace/android/embracesdk/logging/InternalEmbraceLogger;->shouldTriggerLoggerActions(Lio/embrace/android/embracesdk/EmbraceLogger$Severity;)Z +HSPLio/embrace/android/embracesdk/logging/InternalErrorLogger;->(Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger$LoggerAction;Z)V +HSPLio/embrace/android/embracesdk/logging/InternalErrorLogger;->log(Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceLogger$Severity;Ljava/lang/Throwable;Z)V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger$Companion;->()V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger$Companion;->log(Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceLogger$Severity;Ljava/lang/Throwable;Z)V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger$Companion;->logDeveloper(Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger$Companion;->logInfo(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger;->()V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger;->logDeveloper(Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger;->logInfo(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkService$$ExternalSyntheticLambda1;->(Lio/embrace/android/embracesdk/ndk/EmbraceNdkService;Lio/embrace/android/embracesdk/internal/DeviceArchitecture;)V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkService$$ExternalSyntheticLambda2;->(Landroid/content/Context;)V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkService;->(Landroid/content/Context;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/session/ActivityService;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/capture/user/UserService;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/Embrace$AppFramework;Lio/embrace/android/embracesdk/internal/SharedObjectLoader;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository;Lio/embrace/android/embracesdk/ndk/NdkServiceDelegate$NdkDelegate;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;Lio/embrace/android/embracesdk/internal/DeviceArchitecture;)V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository;->(Landroid/content/Context;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$embraceNdkServiceRepository$2;->(Lio/embrace/android/embracesdk/injection/CoreModule;)V +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$embraceNdkServiceRepository$2;->invoke()Lio/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$embraceNdkServiceRepository$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerInstaller$2;->(Lio/embrace/android/embracesdk/ndk/NativeModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;)V +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerInstaller$2;->invoke()Lio/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerInstaller; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerInstaller$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerService$2;->(Lio/embrace/android/embracesdk/ndk/NativeModuleImpl;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerService$2;->invoke()Lio/embrace/android/embracesdk/anr/ndk/EmbraceNativeThreadSamplerService; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$ndkService$2;->(Lio/embrace/android/embracesdk/ndk/NativeModuleImpl;Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$ndkService$2;->invoke()Lio/embrace/android/embracesdk/ndk/EmbraceNdkService; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl$ndkService$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->()V +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->(Lio/embrace/android/embracesdk/injection/CoreModule;Lio/embrace/android/embracesdk/injection/EssentialServiceModule;Lio/embrace/android/embracesdk/injection/DeliveryModule;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/worker/WorkerThreadModule;)V +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->access$getEmbraceNdkServiceRepository$p(Lio/embrace/android/embracesdk/ndk/NativeModuleImpl;)Lio/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->access$nativeThreadSamplingEnabled(Lio/embrace/android/embracesdk/ndk/NativeModuleImpl;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/internal/SharedObjectLoader;)Z +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->getEmbraceNdkServiceRepository()Lio/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->getNativeThreadSamplerInstaller()Lio/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerInstaller; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->getNativeThreadSamplerService()Lio/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->getNdkService()Lio/embrace/android/embracesdk/ndk/NdkService; +HSPLio/embrace/android/embracesdk/ndk/NativeModuleImpl;->nativeThreadSamplingEnabled(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/internal/SharedObjectLoader;)Z +HSPLio/embrace/android/embracesdk/ndk/NdkDelegateImpl;->()V +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->()V +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->(Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)V +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->canSend()Z +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->fromCompletedRequest(Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJJJILjava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getBytesIn()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getBytesOut()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getEndTime()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getErrorMessage()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getErrorType()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getHttpMethod()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getNetworkCaptureData()Lio/embrace/android/embracesdk/network/http/NetworkCaptureData; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getResponseCode()Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getStartTime()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getTraceId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getUrl()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/EmbraceNetworkRequest;->getW3cTraceparent()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride;->()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride;->getURLString(Lio/embrace/android/embracesdk/HttpPathOverrideRequest;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride;->getURLString(Lio/embrace/android/embracesdk/HttpPathOverrideRequest;Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride;->(Ljava/net/HttpURLConnection;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride;->getHeaderByName(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride;->getURLString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler;->(Ljava/net/URLStreamHandler;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler;->getMethodOpenConnection(Ljava/lang/Class;)Ljava/lang/reflect/Method; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler;->getMethodOpenConnection(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/reflect/Method; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->(Ljavax/net/ssl/HttpsURLConnection;Z)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->connect()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->getHeaderFields()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->getInputStream()Ljava/io/InputStream; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->getOutputStream()Ljava/io/OutputStream; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->getResponseCode()I +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->setConnectTimeout(I)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->setDoOutput(Z)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->setReadTimeout(I)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->setRequestMethod(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler;->(Ljava/net/URLStreamHandler;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler;->getMethodOpenConnection(Ljava/lang/Class;)Ljava/lang/reflect/Method; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler;->getMethodOpenConnection(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/reflect/Method; +HSPLio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler;->newEmbraceUrlConnection(Ljava/net/URLConnection;)Ljava/net/URLConnection; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->(Ljava/net/HttpURLConnection;Z)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->(Ljava/net/HttpURLConnection;ZLio/embrace/android/embracesdk/Embrace;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->cacheResponseData()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->connect()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->getHeaderFields()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->getInputStream()Ljava/io/InputStream; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->getOutputStream()Ljava/io/OutputStream; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->getRequestMethod()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->getRequestProperty(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->getResponseCode()I +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->getWrappedInputStream(Ljava/io/InputStream;)Ljava/io/InputStream; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->hasNetworkCaptureRules()Z +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->identifyTraceId()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->internalLogNetworkCall(J)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->internalLogNetworkCall(JJZLjava/lang/Long;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->setConnectTimeout(I)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->setDoOutput(Z)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->setReadTimeout(I)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->setRequestMethod(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->shouldCaptureNetworkData()Z +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride;->shouldUncompressGzip()Z +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler;->()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler;->(Ljava/net/URLStreamHandler;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler;->(Ljava/net/URLStreamHandler;Lio/embrace/android/embracesdk/Embrace;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler;->injectTraceparent(Ljava/net/URLConnection;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler;->openConnection(Ljava/net/URL;)Ljava/net/URLConnection; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler;->setEnableRequestSizeCapture(Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory;->()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory;->()V +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory;->createURLStreamHandler(Ljava/lang/String;)Ljava/net/URLStreamHandler; +HSPLio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory;->newUrlStreamHandler(Ljava/lang/String;)Ljava/net/URLStreamHandler; +HSPLio/embrace/android/embracesdk/network/http/HttpMethod;->()V +HSPLio/embrace/android/embracesdk/network/http/HttpMethod;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/network/http/HttpMethod;->fromString(Ljava/lang/String;)Lio/embrace/android/embracesdk/network/http/HttpMethod; +HSPLio/embrace/android/embracesdk/network/http/HttpMethod;->values()[Lio/embrace/android/embracesdk/network/http/HttpMethod; +HSPLio/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker;->()V +HSPLio/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker;->()V +HSPLio/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker;->registerFactory(Z)V +HSPLio/embrace/android/embracesdk/network/http/StreamHandlerFactoryInstaller;->getFactoryField()Ljava/lang/reflect/Field; +HSPLio/embrace/android/embracesdk/network/http/StreamHandlerFactoryInstaller;->registerFactory(Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/network/logging/DomainSettings;->(ILjava/lang/String;)V +HSPLio/embrace/android/embracesdk/network/logging/DomainSettings;->getLimit()I +HSPLio/embrace/android/embracesdk/network/logging/DomainSettings;->getSuffix()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService$Companion;->()V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService$networkCaptureEncryptionManager$1;->()V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService$networkCaptureEncryptionManager$1;->()V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService;->()V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService;->(Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;)V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService;->getNetworkCaptureRules(Ljava/lang/String;Ljava/lang/String;)Ljava/util/Set; +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService$getNetworkCallsForSession$calls$1;->(Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;JJ)V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService$getNetworkCallsForSession$calls$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService$getNetworkCallsForSession$calls$1;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService$networkCallCache$1;->(Ljava/util/concurrent/ConcurrentSkipListMap;)V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService$networkCallCache$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;->(Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/network/logging/NetworkCaptureService;)V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;->access$getSessionNetworkCalls$p(Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;)Ljava/util/concurrent/ConcurrentSkipListMap; +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;->getNetworkCallsForSession(JJ)Lio/embrace/android/embracesdk/payload/NetworkSessionV2; +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;->logNetworkCall(Ljava/lang/String;Ljava/lang/String;IJJJJLjava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;->processNetworkCall(JLio/embrace/android/embracesdk/payload/NetworkCallV2;)V +HSPLio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService;->storeSettings(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/payload/ActivityLifecycleState;->()V +HSPLio/embrace/android/embracesdk/payload/ActivityLifecycleState;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/payload/AnrInterval$Companion;->()V +HSPLio/embrace/android/embracesdk/payload/AnrInterval$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/AnrInterval$Type;->()V +HSPLio/embrace/android/embracesdk/payload/AnrInterval$Type;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/payload/AnrInterval;->()V +HSPLio/embrace/android/embracesdk/payload/AnrInterval;->(JLjava/lang/Long;Ljava/lang/Long;Lio/embrace/android/embracesdk/payload/AnrInterval$Type;Lio/embrace/android/embracesdk/payload/AnrSampleList;Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/payload/AnrInterval;->(JLjava/lang/Long;Ljava/lang/Long;Lio/embrace/android/embracesdk/payload/AnrInterval$Type;Lio/embrace/android/embracesdk/payload/AnrSampleList;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/AnrInterval;->deepCopy()Lio/embrace/android/embracesdk/payload/AnrInterval; +HSPLio/embrace/android/embracesdk/payload/AnrInterval;->hasSamples()Z +HSPLio/embrace/android/embracesdk/payload/AnrInterval;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/AnrSample$Companion;->()V +HSPLio/embrace/android/embracesdk/payload/AnrSample$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/AnrSample;->()V +HSPLio/embrace/android/embracesdk/payload/AnrSample;->(JLjava/util/List;Ljava/lang/Long;Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/payload/AnrSample;->(JLjava/util/List;Ljava/lang/Long;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/AnrSample;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/AnrSampleList;->(Ljava/util/List;)V +HSPLio/embrace/android/embracesdk/payload/AnrSampleList;->copy(Ljava/util/List;)Lio/embrace/android/embracesdk/payload/AnrSampleList; +HSPLio/embrace/android/embracesdk/payload/AnrSampleList;->getSamples()Ljava/util/List; +HSPLio/embrace/android/embracesdk/payload/AnrSampleList;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/AppExitInfoData;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/payload/AppExitInfoData;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/AppInfo;->(Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/payload/AppInfo;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/AppInfo;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/Breadcrumbs;->(Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V +HSPLio/embrace/android/embracesdk/payload/Breadcrumbs;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/Breadcrumbs;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/DeviceInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/payload/DeviceInfo;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/DeviceInfo;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/DiskUsage;->(Ljava/lang/Long;Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/payload/DiskUsage;->copy$default(Lio/embrace/android/embracesdk/payload/DiskUsage;Ljava/lang/Long;Ljava/lang/Long;ILjava/lang/Object;)Lio/embrace/android/embracesdk/payload/DiskUsage; +HSPLio/embrace/android/embracesdk/payload/DiskUsage;->copy(Ljava/lang/Long;Ljava/lang/Long;)Lio/embrace/android/embracesdk/payload/DiskUsage; +HSPLio/embrace/android/embracesdk/payload/DiskUsage;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/DiskUsage;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/Event;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceEvent$Type;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V +HSPLio/embrace/android/embracesdk/payload/Event;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/EmbraceEvent$Type;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/EventMessage;->(Lio/embrace/android/embracesdk/payload/Event;Lio/embrace/android/embracesdk/payload/Crash;Lio/embrace/android/embracesdk/payload/DeviceInfo;Lio/embrace/android/embracesdk/payload/AppInfo;Lio/embrace/android/embracesdk/payload/UserInfo;Lio/embrace/android/embracesdk/payload/PerformanceInfo;Lio/embrace/android/embracesdk/payload/Stacktraces;ILio/embrace/android/embracesdk/payload/NativeCrash;)V +HSPLio/embrace/android/embracesdk/payload/EventMessage;->(Lio/embrace/android/embracesdk/payload/Event;Lio/embrace/android/embracesdk/payload/Crash;Lio/embrace/android/embracesdk/payload/DeviceInfo;Lio/embrace/android/embracesdk/payload/AppInfo;Lio/embrace/android/embracesdk/payload/UserInfo;Lio/embrace/android/embracesdk/payload/PerformanceInfo;Lio/embrace/android/embracesdk/payload/Stacktraces;ILio/embrace/android/embracesdk/payload/NativeCrash;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/EventMessage;->component1()Lio/embrace/android/embracesdk/payload/Event; +HSPLio/embrace/android/embracesdk/payload/EventMessage;->getEvent()Lio/embrace/android/embracesdk/payload/Event; +HSPLio/embrace/android/embracesdk/payload/ExceptionError;->(Z)V +HSPLio/embrace/android/embracesdk/payload/ExceptionError;->addException(Ljava/lang/Throwable;Ljava/lang/String;Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/payload/ExceptionError;->getExceptionInfo(Ljava/lang/Throwable;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/payload/ExceptionError;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/ExceptionErrorInfo;->(Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;)V +HSPLio/embrace/android/embracesdk/payload/ExceptionInfo$Companion;->()V +HSPLio/embrace/android/embracesdk/payload/ExceptionInfo$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/ExceptionInfo$Companion;->ofThrowable(Ljava/lang/Throwable;)Lio/embrace/android/embracesdk/payload/ExceptionInfo; +HSPLio/embrace/android/embracesdk/payload/ExceptionInfo;->()V +HSPLio/embrace/android/embracesdk/payload/ExceptionInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V +HSPLio/embrace/android/embracesdk/payload/Interval;->(JJLjava/lang/String;)V +HSPLio/embrace/android/embracesdk/payload/Interval;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/NetworkCallV2;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;JJJJJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/payload/NetworkCallV2;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;JJJJJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/NetworkCallV2;->getUrl()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/NetworkCallV2;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/NetworkRequests;->(Lio/embrace/android/embracesdk/payload/NetworkSessionV2;)V +HSPLio/embrace/android/embracesdk/payload/NetworkRequests;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/NetworkSessionV2$DomainCount;->(II)V +HSPLio/embrace/android/embracesdk/payload/NetworkSessionV2$DomainCount;->getCaptureLimit()I +HSPLio/embrace/android/embracesdk/payload/NetworkSessionV2$DomainCount;->getRequestCount()I +HSPLio/embrace/android/embracesdk/payload/NetworkSessionV2;->(Ljava/util/List;Ljava/util/Map;)V +HSPLio/embrace/android/embracesdk/payload/NetworkSessionV2;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/PerformanceInfo;->(Lio/embrace/android/embracesdk/payload/DiskUsage;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/embrace/android/embracesdk/payload/NetworkRequests;Ljava/util/List;)V +HSPLio/embrace/android/embracesdk/payload/PerformanceInfo;->(Lio/embrace/android/embracesdk/payload/DiskUsage;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/embrace/android/embracesdk/payload/NetworkRequests;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/PerformanceInfo;->copy$default(Lio/embrace/android/embracesdk/payload/PerformanceInfo;Lio/embrace/android/embracesdk/payload/DiskUsage;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/embrace/android/embracesdk/payload/NetworkRequests;Ljava/util/List;ILjava/lang/Object;)Lio/embrace/android/embracesdk/payload/PerformanceInfo; +HSPLio/embrace/android/embracesdk/payload/PerformanceInfo;->copy(Lio/embrace/android/embracesdk/payload/DiskUsage;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/embrace/android/embracesdk/payload/NetworkRequests;Ljava/util/List;)Lio/embrace/android/embracesdk/payload/PerformanceInfo; +HSPLio/embrace/android/embracesdk/payload/PerformanceInfo;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/PerformanceInfo;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/Session$Companion;->()V +HSPLio/embrace/android/embracesdk/payload/Session$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/Session$Companion;->buildStartSession(Ljava/lang/String;ZLio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;JILio/embrace/android/embracesdk/payload/UserInfo;Ljava/util/Map;)Lio/embrace/android/embracesdk/payload/Session; +HSPLio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;->()V +HSPLio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;->values()[Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType; +HSPLio/embrace/android/embracesdk/payload/Session;->()V +HSPLio/embrace/android/embracesdk/payload/Session;->(Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ZLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/ExceptionError;Ljava/lang/String;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Ljava/util/List;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/BetaFeatures;Ljava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/payload/UserInfo;)V +HSPLio/embrace/android/embracesdk/payload/Session;->(Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ZLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/ExceptionError;Ljava/lang/String;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Ljava/util/List;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/BetaFeatures;Ljava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/payload/UserInfo;IILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/Session;->copy$default(Lio/embrace/android/embracesdk/payload/Session;Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ZLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/ExceptionError;Ljava/lang/String;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Ljava/util/List;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/BetaFeatures;Ljava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/payload/UserInfo;IILjava/lang/Object;)Lio/embrace/android/embracesdk/payload/Session; +HSPLio/embrace/android/embracesdk/payload/Session;->copy(Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ZLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/ExceptionError;Ljava/lang/String;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Ljava/util/List;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Integer;Lio/embrace/android/embracesdk/payload/BetaFeatures;Ljava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/payload/UserInfo;)Lio/embrace/android/embracesdk/payload/Session; +HSPLio/embrace/android/embracesdk/payload/Session;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/Session;->getSessionId()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/Session;->getStartTime()J +HSPLio/embrace/android/embracesdk/payload/Session;->getUser()Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/payload/Session;->isColdStart()Z +HSPLio/embrace/android/embracesdk/payload/Session;->isReceivedTermination()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/payload/Session;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->(Lio/embrace/android/embracesdk/payload/Session;Lio/embrace/android/embracesdk/payload/UserInfo;Lio/embrace/android/embracesdk/payload/AppInfo;Lio/embrace/android/embracesdk/payload/DeviceInfo;Lio/embrace/android/embracesdk/payload/PerformanceInfo;Lio/embrace/android/embracesdk/payload/Breadcrumbs;Ljava/util/List;I)V +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->(Lio/embrace/android/embracesdk/payload/Session;Lio/embrace/android/embracesdk/payload/UserInfo;Lio/embrace/android/embracesdk/payload/AppInfo;Lio/embrace/android/embracesdk/payload/DeviceInfo;Lio/embrace/android/embracesdk/payload/PerformanceInfo;Lio/embrace/android/embracesdk/payload/Breadcrumbs;Ljava/util/List;IILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->getAppInfo()Lio/embrace/android/embracesdk/payload/AppInfo; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->getBreadcrumbs()Lio/embrace/android/embracesdk/payload/Breadcrumbs; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->getDeviceInfo()Lio/embrace/android/embracesdk/payload/DeviceInfo; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->getPerformanceInfo()Lio/embrace/android/embracesdk/payload/PerformanceInfo; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->getSession()Lio/embrace/android/embracesdk/payload/Session; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->getSpans()Ljava/util/List; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->getUserInfo()Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/payload/SessionMessage;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/Stacktraces$WhenMappings;->()V +HSPLio/embrace/android/embracesdk/payload/Stacktraces;->(Ljava/util/List;Ljava/lang/String;Lio/embrace/android/embracesdk/Embrace$AppFramework;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/payload/TapBreadcrumb$TapBreadcrumbType;->()V +HSPLio/embrace/android/embracesdk/payload/TapBreadcrumb$TapBreadcrumbType;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/payload/ThreadInfo$Companion;->()V +HSPLio/embrace/android/embracesdk/payload/ThreadInfo$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/ThreadInfo$Companion;->ofThread(Ljava/lang/Thread;[Ljava/lang/StackTraceElement;I)Lio/embrace/android/embracesdk/payload/ThreadInfo; +HSPLio/embrace/android/embracesdk/payload/ThreadInfo;->()V +HSPLio/embrace/android/embracesdk/payload/ThreadInfo;->(JLjava/lang/Thread$State;Ljava/lang/String;ILjava/util/List;)V +HSPLio/embrace/android/embracesdk/payload/ThreadInfo;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/ThreadInfo;->getThreadId()J +HSPLio/embrace/android/embracesdk/payload/ThreadInfo;->hashCode()I +HSPLio/embrace/android/embracesdk/payload/ThreadInfo;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/UserInfo$Companion;->()V +HSPLio/embrace/android/embracesdk/payload/UserInfo$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/UserInfo$Companion;->ofStored(Lio/embrace/android/embracesdk/prefs/PreferencesService;)Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/payload/UserInfo;->()V +HSPLio/embrace/android/embracesdk/payload/UserInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V +HSPLio/embrace/android/embracesdk/payload/UserInfo;->copy$default(Lio/embrace/android/embracesdk/payload/UserInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILjava/lang/Object;)Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/payload/UserInfo;->copy(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/payload/UserInfo;->equals(Ljava/lang/Object;)Z +HSPLio/embrace/android/embracesdk/payload/UserInfo;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/ViewBreadcrumb$Companion;->()V +HSPLio/embrace/android/embracesdk/payload/ViewBreadcrumb$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/ViewBreadcrumb;->()V +HSPLio/embrace/android/embracesdk/payload/ViewBreadcrumb;->(Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/payload/ViewBreadcrumb;->(Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/payload/ViewBreadcrumb;->getScreen()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/payload/ViewBreadcrumb;->getStartTime()J +HSPLio/embrace/android/embracesdk/payload/WebVitalType;->()V +HSPLio/embrace/android/embracesdk/payload/WebVitalType;->(Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$1;->(Lkotlin/Lazy;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$Companion;->()V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$alterStartupStatus$1;->(Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$alterStartupStatus$1;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$sam$java_util_concurrent_Callable$0;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService$sam$java_util_concurrent_Callable$0;->call()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->()V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->(Ljava/util/concurrent/ExecutorService;Lkotlin/Lazy;Lio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/internal/EmbraceSerializer;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->access$getPrefs$p(Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService;)Landroid/content/SharedPreferences; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->access$setStringPreference(Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService;Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->alterStartupStatus(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getAppVersion()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getApplicationExitInfoHistory()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getArrayPreference(Landroid/content/SharedPreferences;Ljava/lang/String;)Ljava/util/Set; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getBackgroundActivityEnabled()Z +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getBooleanPreference(Landroid/content/SharedPreferences;Ljava/lang/String;Z)Z +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getCustomPersonas()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getDeviceIdentifier()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getInstallDate()Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getIntegerPreference(Landroid/content/SharedPreferences;Ljava/lang/String;)Ljava/lang/Integer; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getJailbroken()Ljava/lang/Boolean; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getLongPreference(Landroid/content/SharedPreferences;Ljava/lang/String;)Ljava/lang/Long; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getMapPreference(Landroid/content/SharedPreferences;Ljava/lang/String;)Ljava/util/Map; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getOsVersion()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getPermanentSessionProperties()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getPrefs()Landroid/content/SharedPreferences; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getScreenResolution()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getSdkDisabled()Z +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getSessionNumber()I +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getStringPreference(Landroid/content/SharedPreferences;Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getUserEmailAddress()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getUserIdentifier()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getUserPayer()Z +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getUserPersonas()Ljava/util/Set; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->getUsername()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->isUsersFirstDay()Z +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setAppVersion(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setApplicationExitInfoHistory(Ljava/util/Set;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setArrayPreference(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/util/Set;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setBackgroundActivityEnabled(Z)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setBooleanPreference(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setDeviceIdentifier(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setInstallDate(Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setIntegerPreference(Landroid/content/SharedPreferences;Ljava/lang/String;I)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setJailbroken(Ljava/lang/Boolean;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setLongPreference(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/Long;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setOsVersion(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setScreenResolution(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setSdkDisabled(Z)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setSessionNumber(I)V +HSPLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->setStringPreference(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$activityListeners$2;->(Lio/embrace/android/embracesdk/registry/ServiceRegistry;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$activityListeners$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$activityListeners$2;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$closeables$2;->(Lio/embrace/android/embracesdk/registry/ServiceRegistry;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$configListeners$2;->(Lio/embrace/android/embracesdk/registry/ServiceRegistry;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$configListeners$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$configListeners$2;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$memoryCleanerListeners$2;->(Lio/embrace/android/embracesdk/registry/ServiceRegistry;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$memoryCleanerListeners$2;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$memoryCleanerListeners$2;->invoke()Ljava/util/List; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerActivityListeners$1;->(Lio/embrace/android/embracesdk/session/ActivityService;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerActivityListeners$1;->invoke(Lio/embrace/android/embracesdk/session/ActivityListener;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerActivityListeners$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerConfigListeners$1;->(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerConfigListeners$1;->invoke(Lio/embrace/android/embracesdk/config/ConfigListener;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerConfigListeners$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerMemoryCleanerListeners$1;->(Lio/embrace/android/embracesdk/session/MemoryCleanerService;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerMemoryCleanerListeners$1;->invoke(Lio/embrace/android/embracesdk/session/MemoryCleanerListener;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry$registerMemoryCleanerListeners$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->access$getRegistry$p(Lio/embrace/android/embracesdk/registry/ServiceRegistry;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->closeRegistration()V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->forEachSafe(Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->getActivityListeners()Ljava/util/List; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->getConfigListeners()Ljava/util/List; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->getMemoryCleanerListeners()Ljava/util/List; +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->registerActivityListeners(Lio/embrace/android/embracesdk/session/ActivityService;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->registerConfigListeners(Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->registerMemoryCleanerListeners(Lio/embrace/android/embracesdk/session/MemoryCleanerService;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->registerService(Ljava/lang/Object;)V +HSPLio/embrace/android/embracesdk/registry/ServiceRegistry;->registerServices([Ljava/lang/Object;)V +HSPLio/embrace/android/embracesdk/session/ActivityListener$DefaultImpls;->applicationStartupComplete(Lio/embrace/android/embracesdk/session/ActivityListener;)V +HSPLio/embrace/android/embracesdk/session/ActivityListener$DefaultImpls;->onActivityCreated(Lio/embrace/android/embracesdk/session/ActivityListener;Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/session/ActivityListener$DefaultImpls;->onForeground(Lio/embrace/android/embracesdk/session/ActivityListener;ZJJ)V +HSPLio/embrace/android/embracesdk/session/ActivityListener$DefaultImpls;->onView(Lio/embrace/android/embracesdk/session/ActivityListener;Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService$1;->(Lio/embrace/android/embracesdk/session/EmbraceActivityService;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService$1;->run()V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService$Companion;->()V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->()V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->(Landroid/app/Application;Lio/embrace/android/embracesdk/capture/orientation/OrientationService;Lio/embrace/android/embracesdk/clock/Clock;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->addListener(Lio/embrace/android/embracesdk/session/ActivityListener;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->getActivityName(Landroid/app/Activity;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->isInBackground()Z +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->onActivityResumed(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->onActivityStarted(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->onForeground()V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->setMemoryService(Lio/embrace/android/embracesdk/capture/memory/MemoryService;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->updateOrientationWithActivity(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/session/EmbraceActivityService;->updateStateWithActivity(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/session/EmbraceMemoryCleanerService;->()V +HSPLio/embrace/android/embracesdk/session/EmbraceMemoryCleanerService;->addListener(Lio/embrace/android/embracesdk/session/MemoryCleanerListener;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionProperties$Companion;->()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionProperties$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionProperties;->()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionProperties;->(Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/config/ConfigService;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionProperties;->get()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$Companion;->()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$sam$java_lang_Runnable$0;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$sam$java_lang_Runnable$0;->run()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$startSession$automaticSessionCloserCallback$1;->(Lio/embrace/android/embracesdk/session/EmbraceSessionService;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$startSession$sessionMessage$1;->(Lio/embrace/android/embracesdk/session/EmbraceSessionService;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$startSession$sessionMessage$1;->invoke()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService$startSession$sessionMessage$1;->invoke()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->(Lio/embrace/android/embracesdk/session/ActivityService;Lio/embrace/android/embracesdk/ndk/NdkService;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/session/SessionHandler;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;ZLio/embrace/android/embracesdk/clock/Clock;Lio/embrace/android/embracesdk/internal/spans/SpansService;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->applicationStartupComplete()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->onForeground(ZJJ)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->onPeriodicCacheActiveSession()V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->onView(Landroid/app/Activity;)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->setSdkStartupDuration(J)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->startSession(ZLio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;J)V +HSPLio/embrace/android/embracesdk/session/EmbraceSessionService;->startStateSession(ZJ)V +HSPLio/embrace/android/embracesdk/session/SessionHandler$WhenMappings;->()V +HSPLio/embrace/android/embracesdk/session/SessionHandler;->(Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger;Lio/embrace/android/embracesdk/config/ConfigService;Lio/embrace/android/embracesdk/prefs/PreferencesService;Lio/embrace/android/embracesdk/capture/user/UserService;Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService;Lio/embrace/android/embracesdk/capture/metadata/MetadataService;Lio/embrace/android/embracesdk/gating/GatingService;Lio/embrace/android/embracesdk/capture/crumbs/BreadcrumbService;Lio/embrace/android/embracesdk/session/ActivityService;Lio/embrace/android/embracesdk/ndk/NdkService;Lio/embrace/android/embracesdk/event/EventService;Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger;Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService;Lio/embrace/android/embracesdk/capture/PerformanceInfoService;Lio/embrace/android/embracesdk/session/MemoryCleanerService;Lio/embrace/android/embracesdk/comms/delivery/DeliveryService;Lio/embrace/android/embracesdk/capture/webview/WebViewService;Lio/embrace/android/embracesdk/capture/crumbs/activity/ActivityLifecycleBreadcrumbService;Lio/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService;Lio/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService;Lio/embrace/android/embracesdk/clock/Clock;Ljava/util/concurrent/ScheduledExecutorService;Ljava/util/concurrent/ScheduledExecutorService;Ljava/util/concurrent/ExecutorService;)V +HSPLio/embrace/android/embracesdk/session/SessionHandler;->addFirstViewBreadcrumbForSession(J)V +HSPLio/embrace/android/embracesdk/session/SessionHandler;->buildEndSessionMessage(Lio/embrace/android/embracesdk/payload/Session;ZZLjava/lang/String;Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;JJLjava/util/List;)Lio/embrace/android/embracesdk/payload/SessionMessage; +HSPLio/embrace/android/embracesdk/session/SessionHandler;->buildStartSessionMessage(Lio/embrace/android/embracesdk/payload/Session;)Lio/embrace/android/embracesdk/payload/SessionMessage; +HSPLio/embrace/android/embracesdk/session/SessionHandler;->getActiveSessionEndMessage(Lio/embrace/android/embracesdk/payload/Session;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;JLjava/util/List;)Lio/embrace/android/embracesdk/payload/SessionMessage; +HSPLio/embrace/android/embracesdk/session/SessionHandler;->handleAutomaticSessionStopper(Ljava/lang/Runnable;)V +HSPLio/embrace/android/embracesdk/session/SessionHandler;->incrementAndGetSessionNumber()I +HSPLio/embrace/android/embracesdk/session/SessionHandler;->isAllowedToEnd(Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;Lio/embrace/android/embracesdk/payload/Session;)Z +HSPLio/embrace/android/embracesdk/session/SessionHandler;->isAllowedToStart()Z +HSPLio/embrace/android/embracesdk/session/SessionHandler;->onSessionStarted(ZLio/embrace/android/embracesdk/payload/Session$SessionLifeEventType;JLio/embrace/android/embracesdk/session/EmbraceSessionProperties;Ljava/lang/Runnable;Ljava/lang/Runnable;)Lio/embrace/android/embracesdk/payload/SessionMessage; +HSPLio/embrace/android/embracesdk/session/SessionHandler;->runEndSessionForCaching(Lio/embrace/android/embracesdk/payload/Session;Lio/embrace/android/embracesdk/session/EmbraceSessionProperties;JLjava/util/List;)Lio/embrace/android/embracesdk/payload/SessionMessage; +HSPLio/embrace/android/embracesdk/session/SessionHandler;->startPeriodicCaching(Ljava/lang/Runnable;)V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$appInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$appInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$appInfo$1;->invoke(Lio/embrace/android/embracesdk/payload/SessionMessage;)Lio/embrace/android/embracesdk/payload/AppInfo; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$appInfo$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$breadcrumbs$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$breadcrumbs$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$breadcrumbs$1;->invoke(Lio/embrace/android/embracesdk/payload/SessionMessage;)Lio/embrace/android/embracesdk/payload/Breadcrumbs; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$breadcrumbs$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$deviceInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$deviceInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$deviceInfo$1;->invoke(Lio/embrace/android/embracesdk/payload/SessionMessage;)Lio/embrace/android/embracesdk/payload/DeviceInfo; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$deviceInfo$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$performanceInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$performanceInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$performanceInfo$1;->invoke(Lio/embrace/android/embracesdk/payload/SessionMessage;)Lio/embrace/android/embracesdk/payload/PerformanceInfo; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$performanceInfo$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$session$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$session$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$session$1;->invoke(Lio/embrace/android/embracesdk/payload/SessionMessage;)Lio/embrace/android/embracesdk/payload/Session; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$session$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$spans$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$spans$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$spans$1;->invoke(Lio/embrace/android/embracesdk/payload/SessionMessage;)Ljava/util/List; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$spans$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$userInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$userInfo$1;->()V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$userInfo$1;->invoke(Lio/embrace/android/embracesdk/payload/SessionMessage;)Lio/embrace/android/embracesdk/payload/UserInfo; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$userInfo$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer;->(Lio/embrace/android/embracesdk/internal/EmbraceSerializer;)V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer;->addJsonProperty(Ljava/lang/String;Ljava/lang/String;Ljava/lang/StringBuilder;)V +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer;->calculateJsonValue(Lio/embrace/android/embracesdk/payload/SessionMessage;Ljava/lang/String;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/session/SessionMessageSerializer;->serialize(Lio/embrace/android/embracesdk/payload/SessionMessage;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent$Companion;->()V +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent$Companion;->create(Ljava/lang/String;JLjava/util/Map;)Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent; +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent$Companion;->inputsValid$embrace_android_sdk_release(Ljava/lang/String;Ljava/util/Map;)Z +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent;->()V +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent;->(Ljava/lang/String;JLjava/util/Map;)V +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent;->getAttributes()Ljava/util/Map; +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent;->getName()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent;->getTimestampNanos()J +HSPLio/embrace/android/embracesdk/spans/EmbraceSpanEvent;->toString()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/utils/BuildVersionChecker;->()V +HSPLio/embrace/android/embracesdk/utils/BuildVersionChecker;->()V +HSPLio/embrace/android/embracesdk/utils/BuildVersionChecker;->isAtLeast(I)Z +HSPLio/embrace/android/embracesdk/utils/NetworkUtils;->()V +HSPLio/embrace/android/embracesdk/utils/NetworkUtils;->()V +HSPLio/embrace/android/embracesdk/utils/NetworkUtils;->getDomain(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/utils/NetworkUtils;->getValidTraceId(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/utils/NetworkUtils;->isIpAddress(Ljava/lang/String;)Z +HSPLio/embrace/android/embracesdk/utils/NetworkUtils;->isNetworkSpanForwardingEnabled(Lio/embrace/android/embracesdk/config/ConfigService;)Z +HSPLio/embrace/android/embracesdk/utils/NetworkUtils;->stripUrl(Ljava/lang/String;)Ljava/lang/String; +HSPLio/embrace/android/embracesdk/utils/ThreadUtils$runOnMainThread$wrappedRunnable$1;->(Ljava/lang/Runnable;)V +HSPLio/embrace/android/embracesdk/utils/ThreadUtils$runOnMainThread$wrappedRunnable$1;->run()V +HSPLio/embrace/android/embracesdk/utils/ThreadUtils;->()V +HSPLio/embrace/android/embracesdk/utils/ThreadUtils;->()V +HSPLio/embrace/android/embracesdk/utils/ThreadUtils;->runOnMainThread(Ljava/lang/Runnable;)V +HSPLio/embrace/android/embracesdk/utils/exceptions/Unchecked$wrap$1;->(Lkotlin/jvm/functions/Function0;)V +HSPLio/embrace/android/embracesdk/utils/exceptions/Unchecked$wrap$1;->get()Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/utils/exceptions/Unchecked;->()V +HSPLio/embrace/android/embracesdk/utils/exceptions/Unchecked;->()V +HSPLio/embrace/android/embracesdk/utils/exceptions/Unchecked;->wrap(Lio/embrace/android/embracesdk/utils/exceptions/function/CheckedSupplier;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/utils/exceptions/Unchecked;->wrap(Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +HSPLio/embrace/android/embracesdk/worker/ExecutorName;->()V +HSPLio/embrace/android/embracesdk/worker/ExecutorName;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLio/embrace/android/embracesdk/worker/ExecutorName;->getThreadName$embrace_android_sdk_release()Ljava/lang/String; +HSPLio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl$createThreadFactory$1;->(Ljava/lang/String;)V +HSPLio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl$createThreadFactory$1;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread; +HSPLio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl;->()V +HSPLio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl;->backgroundExecutor(Lio/embrace/android/embracesdk/worker/ExecutorName;)Ljava/util/concurrent/ExecutorService; +HSPLio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl;->createThreadFactory(Ljava/lang/String;)Ljava/util/concurrent/ThreadFactory; +HSPLio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl;->fetchExecutor(Lio/embrace/android/embracesdk/worker/ExecutorName;)Ljava/util/concurrent/ScheduledExecutorService; +HSPLio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl;->scheduledExecutor(Lio/embrace/android/embracesdk/worker/ExecutorName;)Ljava/util/concurrent/ScheduledExecutorService; +Lio/embrace/android/embracesdk/AutomaticVerificationExceptionHandler; +Lio/embrace/android/embracesdk/BetaApi; +Lio/embrace/android/embracesdk/BuildInfo$Companion; +Lio/embrace/android/embracesdk/BuildInfo; +Lio/embrace/android/embracesdk/CpuInfoDelegate; +Lio/embrace/android/embracesdk/Embrace$AppFramework; +Lio/embrace/android/embracesdk/Embrace; +Lio/embrace/android/embracesdk/EmbraceAndroidApi; +Lio/embrace/android/embracesdk/EmbraceApi; +Lio/embrace/android/embracesdk/EmbraceCpuInfoDelegate; +Lio/embrace/android/embracesdk/EmbraceEvent$Type$Companion$WhenMappings; +Lio/embrace/android/embracesdk/EmbraceEvent$Type$Companion; +Lio/embrace/android/embracesdk/EmbraceEvent$Type; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda0; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda10; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda11; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda1; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda2; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda3; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda4; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda5; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda6; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda7; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda8; +Lio/embrace/android/embracesdk/EmbraceImpl$$ExternalSyntheticLambda9; +Lio/embrace/android/embracesdk/EmbraceImpl; +Lio/embrace/android/embracesdk/EmbraceInternalInterface; +Lio/embrace/android/embracesdk/EmbraceInternalInterfaceImpl; +Lio/embrace/android/embracesdk/EmbraceLogger$Severity; +Lio/embrace/android/embracesdk/FlutterInternalInterface; +Lio/embrace/android/embracesdk/FlutterInternalInterfaceImpl; +Lio/embrace/android/embracesdk/HttpPathOverrideRequest; +Lio/embrace/android/embracesdk/InternalApi; +Lio/embrace/android/embracesdk/InternalInterfaceModule; +Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl$embraceInternalInterface$2; +Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl$flutterInternalInterface$2; +Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl$reactNativeInternalInterface$2; +Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl$unityInternalInterface$2; +Lio/embrace/android/embracesdk/InternalInterfaceModuleImpl; +Lio/embrace/android/embracesdk/LogExceptionType; +Lio/embrace/android/embracesdk/LogsApi; +Lio/embrace/android/embracesdk/MomentsApi; +Lio/embrace/android/embracesdk/NetworkRequestApi; +Lio/embrace/android/embracesdk/ReactNativeInternalInterface; +Lio/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl; +Lio/embrace/android/embracesdk/SessionApi; +Lio/embrace/android/embracesdk/SessionModule; +Lio/embrace/android/embracesdk/SessionModuleImpl$backgroundActivityService$2; +Lio/embrace/android/embracesdk/SessionModuleImpl$sessionHandler$2; +Lio/embrace/android/embracesdk/SessionModuleImpl$sessionService$2; +Lio/embrace/android/embracesdk/SessionModuleImpl; +Lio/embrace/android/embracesdk/Severity; +Lio/embrace/android/embracesdk/UnityInternalInterface; +Lio/embrace/android/embracesdk/UnityInternalInterfaceImpl; +Lio/embrace/android/embracesdk/UserApi; +Lio/embrace/android/embracesdk/annotation/StartupActivity; +Lio/embrace/android/embracesdk/anr/AnrService; +Lio/embrace/android/embracesdk/anr/AnrStacktraceSampler$Companion; +Lio/embrace/android/embracesdk/anr/AnrStacktraceSampler; +Lio/embrace/android/embracesdk/anr/BlockedThreadListener; +Lio/embrace/android/embracesdk/anr/EmbraceAnrService$Companion; +Lio/embrace/android/embracesdk/anr/EmbraceAnrService$getCapturedData$callable$1; +Lio/embrace/android/embracesdk/anr/EmbraceAnrService$onForeground$1; +Lio/embrace/android/embracesdk/anr/EmbraceAnrService$startAnrCapture$1; +Lio/embrace/android/embracesdk/anr/EmbraceAnrService; +Lio/embrace/android/embracesdk/anr/ThreadInfoCollector; +Lio/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler; +Lio/embrace/android/embracesdk/anr/detection/AnrProcessErrorStateInfo; +Lio/embrace/android/embracesdk/anr/detection/BlockedThreadDetector; +Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$1; +Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$sam$java_lang_Runnable$0; +Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler$scheduleRegularHeartbeats$runnable$1; +Lio/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler; +Lio/embrace/android/embracesdk/anr/detection/LooperCompat; +Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler$Companion; +Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler$onMainThreadUnblocked$1; +Lio/embrace/android/embracesdk/anr/detection/TargetThreadHandler; +Lio/embrace/android/embracesdk/anr/detection/ThreadMonitoringState; +Lio/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector; +Lio/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerInstaller; +Lio/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService; +Lio/embrace/android/embracesdk/anr/sigquit/FilesDelegate; +Lio/embrace/android/embracesdk/anr/sigquit/FindGoogleThread; +Lio/embrace/android/embracesdk/anr/sigquit/GetThreadCommand; +Lio/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess; +Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate; +Lio/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository; +Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService$initializeGoogleAnrTracking$1; +Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService$setupGoogleAnrTracking$1; +Lio/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService; +Lio/embrace/android/embracesdk/arch/DataCaptureService; +Lio/embrace/android/embracesdk/capture/EmbracePerformanceInfoService; +Lio/embrace/android/embracesdk/capture/PerformanceInfoService; +Lio/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService; +Lio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService$Companion; +Lio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService$startService$1; +Lio/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService; +Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService$ipAddress$2; +Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService$registerConnectivityActionReceiver$1; +Lio/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService; +Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityListener; +Lio/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService; +Lio/embrace/android/embracesdk/capture/crash/CrashService; +Lio/embrace/android/embracesdk/capture/crash/EmbraceCrashService$Companion; +Lio/embrace/android/embracesdk/capture/crash/EmbraceCrashService; +Lio/embrace/android/embracesdk/capture/crash/EmbraceUncaughtExceptionHandler; +Lio/embrace/android/embracesdk/capture/crumbs/Breadcrumb; +Lio/embrace/android/embracesdk/capture/crumbs/BreadcrumbService; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$2; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$3; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$4; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$5; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$6; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$7; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$Companion; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getCustomBreadcrumbsForSession$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getFragmentBreadcrumbsForSession$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getPushNotificationsBreadcrumbsForSession$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getRnActionBreadcrumbForSession$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getTapBreadcrumbsForSession$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getViewBreadcrumbsForSession$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService$getWebViewBreadcrumbsForSession$1; +Lio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService; +Lio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService$Utils; +Lio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService; +Lio/embrace/android/embracesdk/capture/crumbs/activity/ActivityLifecycleBreadcrumbService; +Lio/embrace/android/embracesdk/capture/crumbs/activity/EmbraceActivityLifecycleBreadcrumbService; +Lio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService$Companion; +Lio/embrace/android/embracesdk/capture/memory/EmbraceMemoryService; +Lio/embrace/android/embracesdk/capture/memory/MemoryService; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$3; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$deviceIdentifier$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isAppUpdated$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion$ofContext$isOsUpdated$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$Companion; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveDiskUsage$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveIsJailbroken$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$asyncRetrieveScreenResolution$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService$statFs$1; +Lio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService; +Lio/embrace/android/embracesdk/capture/metadata/MetadataService; +Lio/embrace/android/embracesdk/capture/metadata/MetadataUtils; +Lio/embrace/android/embracesdk/capture/orientation/NoOpOrientationService; +Lio/embrace/android/embracesdk/capture/orientation/OrientationService; +Lio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService$registerPowerSaveModeReceiver$1; +Lio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService; +Lio/embrace/android/embracesdk/capture/powersave/PowerSaveModeService; +Lio/embrace/android/embracesdk/capture/screenshot/EmbraceScreenshotService$Companion; +Lio/embrace/android/embracesdk/capture/screenshot/EmbraceScreenshotService; +Lio/embrace/android/embracesdk/capture/screenshot/ScreenshotService; +Lio/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService; +Lio/embrace/android/embracesdk/capture/strictmode/StrictModeService; +Lio/embrace/android/embracesdk/capture/thermalstate/NoOpThermalStatusService; +Lio/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService; +Lio/embrace/android/embracesdk/capture/user/EmbraceUserService$Companion; +Lio/embrace/android/embracesdk/capture/user/EmbraceUserService; +Lio/embrace/android/embracesdk/capture/user/UserService; +Lio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService$Companion; +Lio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService$webVitalType$1; +Lio/embrace/android/embracesdk/capture/webview/EmbraceWebViewService; +Lio/embrace/android/embracesdk/capture/webview/WebViewService; +Lio/embrace/android/embracesdk/clock/Clock; +Lio/embrace/android/embracesdk/clock/NormalizedIntervalClock; +Lio/embrace/android/embracesdk/clock/SystemClock; +Lio/embrace/android/embracesdk/comms/api/ApiClient$Companion; +Lio/embrace/android/embracesdk/comms/api/ApiClient; +Lio/embrace/android/embracesdk/comms/api/ApiRequest; +Lio/embrace/android/embracesdk/comms/api/ApiResponse; +Lio/embrace/android/embracesdk/comms/api/ApiResponseCache$Companion; +Lio/embrace/android/embracesdk/comms/api/ApiResponseCache$cacheDir$2; +Lio/embrace/android/embracesdk/comms/api/ApiResponseCache; +Lio/embrace/android/embracesdk/comms/api/ApiUrlBuilder$Companion; +Lio/embrace/android/embracesdk/comms/api/ApiUrlBuilder; +Lio/embrace/android/embracesdk/comms/api/CachedConfig; +Lio/embrace/android/embracesdk/comms/api/EmbraceConnection; +Lio/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl; +Lio/embrace/android/embracesdk/comms/api/EmbraceUrl$Companion; +Lio/embrace/android/embracesdk/comms/api/EmbraceUrl; +Lio/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter; +Lio/embrace/android/embracesdk/comms/api/EmbraceUrlImpl; +Lio/embrace/android/embracesdk/comms/delivery/CacheService; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$CachedSession; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$Companion; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$deletePayload$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$loadFailedApiCalls$cached$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$saveBytes$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$saveFailedApiCalls$$inlined$let$lambda$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager$sessionMessageSerializer$2; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCall; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$createRequest$url$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$postOnExecutor$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$retryQueue$2; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$scheduleFailedApiCallsRetry$$inlined$synchronized$lambda$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendLogs$url$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager$sendSession$url$1; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManagerKt$sam$java_lang_Runnable$0; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryService; +Lio/embrace/android/embracesdk/comms/delivery/DeliveryServiceNetwork; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$1; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$Companion; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService$listFilenamesByPrefix$1; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceCacheService; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$Companion; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$backgroundActivities$2; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendCachedSessionsWithoutNdk$1; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendEventAsync$1; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService$sendSession$1; +Lio/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService; +Lio/embrace/android/embracesdk/comms/delivery/NetworkStatus; +Lio/embrace/android/embracesdk/comms/delivery/SessionMessageState; +Lio/embrace/android/embracesdk/config/ConfigListener; +Lio/embrace/android/embracesdk/config/ConfigService; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$2; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$Companion; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$anrBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$anrBehavior$2; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$appExitInfoBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$autoDataCaptureBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$backgroundActivityBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$backgroundActivityBehavior$2; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$breadcrumbBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$logMessageBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$networkBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$networkSpanForwardingBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$performInitialConfigLoad$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$refreshConfig$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$remoteSupplier$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$sdkEndpointBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$sdkModeBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$sessionBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$sessionBehavior$2; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$spansBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService$startupBehavior$1; +Lio/embrace/android/embracesdk/config/EmbraceConfigService; +Lio/embrace/android/embracesdk/config/behavior/AnrBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/AnrBehavior$allowPatternList$2; +Lio/embrace/android/embracesdk/config/behavior/AnrBehavior$blockPatternList$2; +Lio/embrace/android/embracesdk/config/behavior/AnrBehavior; +Lio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior; +Lio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior; +Lio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior; +Lio/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck; +Lio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior; +Lio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior$2; +Lio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior; +Lio/embrace/android/embracesdk/config/behavior/LogMessageBehavior$1; +Lio/embrace/android/embracesdk/config/behavior/LogMessageBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/LogMessageBehavior; +Lio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior$2; +Lio/embrace/android/embracesdk/config/behavior/MergedConfigBehavior; +Lio/embrace/android/embracesdk/config/behavior/NetworkBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/NetworkBehavior; +Lio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior$1; +Lio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior; +Lio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior; +Lio/embrace/android/embracesdk/config/behavior/SdkModeBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/SdkModeBehavior$appId$2; +Lio/embrace/android/embracesdk/config/behavior/SdkModeBehavior; +Lio/embrace/android/embracesdk/config/behavior/SessionBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/SessionBehavior; +Lio/embrace/android/embracesdk/config/behavior/SpansBehavior$1; +Lio/embrace/android/embracesdk/config/behavior/SpansBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/SpansBehavior; +Lio/embrace/android/embracesdk/config/behavior/StartupBehavior$1; +Lio/embrace/android/embracesdk/config/behavior/StartupBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/StartupBehavior; +Lio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior$1; +Lio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior$Companion; +Lio/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior; +Lio/embrace/android/embracesdk/config/local/AnrLocalConfig; +Lio/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig; +Lio/embrace/android/embracesdk/config/local/AppLocalConfig; +Lio/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig; +Lio/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfig; +Lio/embrace/android/embracesdk/config/local/BaseUrlLocalConfig; +Lio/embrace/android/embracesdk/config/local/ComposeLocalConfig; +Lio/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig; +Lio/embrace/android/embracesdk/config/local/DomainLocalConfig; +Lio/embrace/android/embracesdk/config/local/LocalConfig$Companion; +Lio/embrace/android/embracesdk/config/local/LocalConfig; +Lio/embrace/android/embracesdk/config/local/NetworkLocalConfig; +Lio/embrace/android/embracesdk/config/local/SdkLocalConfig; +Lio/embrace/android/embracesdk/config/local/SessionLocalConfig; +Lio/embrace/android/embracesdk/config/local/StartupMomentLocalConfig; +Lio/embrace/android/embracesdk/config/local/TapsLocalConfig; +Lio/embrace/android/embracesdk/config/local/ViewLocalConfig; +Lio/embrace/android/embracesdk/config/local/WebViewLocalConfig; +Lio/embrace/android/embracesdk/config/remote/AnrRemoteConfig$AllowedNdkSampleMethod; +Lio/embrace/android/embracesdk/config/remote/AnrRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/AppExitInfoConfig; +Lio/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/LogRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/NetworkCaptureRuleRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/NetworkRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/RemoteConfig; +Lio/embrace/android/embracesdk/config/remote/SessionRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/SpansRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/UiRemoteConfig; +Lio/embrace/android/embracesdk/config/remote/WebViewVitals; +Lio/embrace/android/embracesdk/event/EmbraceEventService$Companion; +Lio/embrace/android/embracesdk/event/EmbraceEventService$eventIdsCache$1; +Lio/embrace/android/embracesdk/event/EmbraceEventService$findEventIdsForSession$1; +Lio/embrace/android/embracesdk/event/EmbraceEventService$logStartupSpan$1; +Lio/embrace/android/embracesdk/event/EmbraceEventService$startEvent$eventDescription$1; +Lio/embrace/android/embracesdk/event/EmbraceEventService; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$Companion; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$WhenMappings; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$errorLogIdsCache$1; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$findLogIds$1; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$infoLogIdsCache$1; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$log$1; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$networkLogIdsCache$1; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger$warningLogIdsCache$1; +Lio/embrace/android/embracesdk/event/EmbraceRemoteLogger; +Lio/embrace/android/embracesdk/event/EventHandler; +Lio/embrace/android/embracesdk/event/EventService; +Lio/embrace/android/embracesdk/gating/EmbraceGatingService; +Lio/embrace/android/embracesdk/gating/GatingService; +Lio/embrace/android/embracesdk/injection/AndroidServicesModule; +Lio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2$lazyPrefs$1; +Lio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl$preferencesService$2; +Lio/embrace/android/embracesdk/injection/AndroidServicesModuleImpl; +Lio/embrace/android/embracesdk/injection/AnrModule; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$anrExecutorService$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$anrMonitorThreadFactory$1; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$anrProcessErrorSampler$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$anrService$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$blockedThreadDetector$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$googleAnrTimestampRepository$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$livenessCheckScheduler$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$looper$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$sigquitDetectionService$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$state$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl$targetThreadHandler$2; +Lio/embrace/android/embracesdk/injection/AnrModuleImpl; +Lio/embrace/android/embracesdk/injection/CoreModule; +Lio/embrace/android/embracesdk/injection/CoreModuleImpl$application$2; +Lio/embrace/android/embracesdk/injection/CoreModuleImpl$context$2; +Lio/embrace/android/embracesdk/injection/CoreModuleImpl$isDebug$2; +Lio/embrace/android/embracesdk/injection/CoreModuleImpl$jsonSerializer$2; +Lio/embrace/android/embracesdk/injection/CoreModuleImpl$resources$2; +Lio/embrace/android/embracesdk/injection/CoreModuleImpl$serviceRegistry$2; +Lio/embrace/android/embracesdk/injection/CoreModuleImpl; +Lio/embrace/android/embracesdk/injection/CoreModuleKt; +Lio/embrace/android/embracesdk/injection/CrashModule; +Lio/embrace/android/embracesdk/injection/CrashModuleImpl$automaticVerificationExceptionHandler$2; +Lio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2$markerFile$1; +Lio/embrace/android/embracesdk/injection/CrashModuleImpl$crashMarker$2; +Lio/embrace/android/embracesdk/injection/CrashModuleImpl$crashService$2; +Lio/embrace/android/embracesdk/injection/CrashModuleImpl$lastRunCrashVerifier$2; +Lio/embrace/android/embracesdk/injection/CrashModuleImpl; +Lio/embrace/android/embracesdk/injection/CustomerLogModule; +Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkCaptureService$2; +Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$networkLoggingService$2; +Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$remoteLogger$2; +Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl$screenshotService$2; +Lio/embrace/android/embracesdk/injection/CustomerLogModuleImpl; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModule; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$activityLifecycleBreadcrumbService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$breadcrumbService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$memoryService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$networkConnectivityService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$powerSaveModeService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$pushNotificationService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$strictModeService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$thermalStatusService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl$webviewService$2; +Lio/embrace/android/embracesdk/injection/DataCaptureServiceModuleImpl; +Lio/embrace/android/embracesdk/injection/DataContainerModule; +Lio/embrace/android/embracesdk/injection/DataContainerModuleImpl$applicationExitInfoService$2; +Lio/embrace/android/embracesdk/injection/DataContainerModuleImpl$eventService$2; +Lio/embrace/android/embracesdk/injection/DataContainerModuleImpl$performanceInfoService$2; +Lio/embrace/android/embracesdk/injection/DataContainerModuleImpl; +Lio/embrace/android/embracesdk/injection/DeliveryModule; +Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl$cacheService$2; +Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryCacheManager$2; +Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryNetworkManager$2; +Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl$deliveryService$2; +Lio/embrace/android/embracesdk/injection/DeliveryModuleImpl; +Lio/embrace/android/embracesdk/injection/EssentialServiceModule; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$activityService$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2$1; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$apiClient$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2$1; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cache$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2$1; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$configService$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$cpuInfoDelegate$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$gatingService$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$memoryCleanerService$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$metadataService$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$orientationService$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$sharedObjectLoader$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$urlBuilder$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl$userService$2; +Lio/embrace/android/embracesdk/injection/EssentialServiceModuleImpl; +Lio/embrace/android/embracesdk/injection/InitModule; +Lio/embrace/android/embracesdk/injection/InitModuleImpl; +Lio/embrace/android/embracesdk/injection/LoadType; +Lio/embrace/android/embracesdk/injection/SdkObservabilityModule; +Lio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$exceptionService$2; +Lio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$internalErrorLogger$2; +Lio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl$logStrictMode$2; +Lio/embrace/android/embracesdk/injection/SdkObservabilityModuleImpl; +Lio/embrace/android/embracesdk/injection/SingletonDelegate; +Lio/embrace/android/embracesdk/injection/SystemServiceModule; +Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$activityManager$2; +Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$connectivityManager$2; +Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$powerManager$2; +Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl$windowManager$2; +Lio/embrace/android/embracesdk/injection/SystemServiceModuleImpl; +Lio/embrace/android/embracesdk/internal/AndroidResourcesService; +Lio/embrace/android/embracesdk/internal/ApkToolsConfig; +Lio/embrace/android/embracesdk/internal/CacheableValue; +Lio/embrace/android/embracesdk/internal/DeviceArchitecture; +Lio/embrace/android/embracesdk/internal/DeviceArchitectureImpl; +Lio/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService; +Lio/embrace/android/embracesdk/internal/EmbraceSerializer$gson$1; +Lio/embrace/android/embracesdk/internal/EmbraceSerializer; +Lio/embrace/android/embracesdk/internal/EventDescription; +Lio/embrace/android/embracesdk/internal/MessageType; +Lio/embrace/android/embracesdk/internal/OpenTelemetryClock; +Lio/embrace/android/embracesdk/internal/PatternCache; +Lio/embrace/android/embracesdk/internal/SharedObjectLoader; +Lio/embrace/android/embracesdk/internal/StartupEventInfo; +Lio/embrace/android/embracesdk/internal/Systrace$Companion; +Lio/embrace/android/embracesdk/internal/Systrace; +Lio/embrace/android/embracesdk/internal/ThreadEnforcementCheckKt; +Lio/embrace/android/embracesdk/internal/TraceparentGenerator$Companion; +Lio/embrace/android/embracesdk/internal/TraceparentGenerator; +Lio/embrace/android/embracesdk/internal/crash/CrashFileMarker$Companion; +Lio/embrace/android/embracesdk/internal/crash/CrashFileMarker; +Lio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier$readAndCleanMarkerAsync$1; +Lio/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier; +Lio/embrace/android/embracesdk/internal/spans/BufferedRecordCompletedSpan; +Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Attribute$DefaultImpls; +Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Attribute; +Lio/embrace/android/embracesdk/internal/spans/EmbraceAttributes$Type; +Lio/embrace/android/embracesdk/internal/spans/EmbraceExtensionsKt; +Lio/embrace/android/embracesdk/internal/spans/EmbraceSpanData$Companion; +Lio/embrace/android/embracesdk/internal/spans/EmbraceSpanData; +Lio/embrace/android/embracesdk/internal/spans/EmbraceSpanExporter; +Lio/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl$Companion; +Lio/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl; +Lio/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor; +Lio/embrace/android/embracesdk/internal/spans/EmbraceSpansService; +Lio/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService; +Lio/embrace/android/embracesdk/internal/spans/Initializable; +Lio/embrace/android/embracesdk/internal/spans/SpansService$Companion; +Lio/embrace/android/embracesdk/internal/spans/SpansService$DefaultImpls; +Lio/embrace/android/embracesdk/internal/spans/SpansService; +Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$Companion; +Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$openTelemetry$2; +Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$sdkTracerProvider$2; +Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl$tracer$2; +Lio/embrace/android/embracesdk/internal/spans/SpansServiceImpl; +Lio/embrace/android/embracesdk/internal/utils/Uuid; +Lio/embrace/android/embracesdk/logging/AndroidLogger$WhenMappings; +Lio/embrace/android/embracesdk/logging/AndroidLogger; +Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$Companion; +Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionClasses$2; +Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService$ignoredExceptionStrings$2; +Lio/embrace/android/embracesdk/logging/EmbraceInternalErrorService; +Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger$LoggerAction; +Lio/embrace/android/embracesdk/logging/InternalEmbraceLogger; +Lio/embrace/android/embracesdk/logging/InternalErrorLogger; +Lio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger$Companion; +Lio/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger; +Lio/embrace/android/embracesdk/ndk/EmbraceNdkService$$ExternalSyntheticLambda1; +Lio/embrace/android/embracesdk/ndk/EmbraceNdkService$$ExternalSyntheticLambda2; +Lio/embrace/android/embracesdk/ndk/EmbraceNdkService; +Lio/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository; +Lio/embrace/android/embracesdk/ndk/NativeModule; +Lio/embrace/android/embracesdk/ndk/NativeModuleImpl$embraceNdkServiceRepository$2; +Lio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerInstaller$2; +Lio/embrace/android/embracesdk/ndk/NativeModuleImpl$nativeThreadSamplerService$2; +Lio/embrace/android/embracesdk/ndk/NativeModuleImpl$ndkService$2; +Lio/embrace/android/embracesdk/ndk/NativeModuleImpl; +Lio/embrace/android/embracesdk/ndk/NdkDelegateImpl; +Lio/embrace/android/embracesdk/ndk/NdkService; +Lio/embrace/android/embracesdk/ndk/NdkServiceDelegate$NdkDelegate; +Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; +Lio/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride; +Lio/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride; +Lio/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler; +Lio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection; +Lio/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler; +Lio/embrace/android/embracesdk/network/http/EmbraceSslUrlConnectionService; +Lio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride; +Lio/embrace/android/embracesdk/network/http/EmbraceUrlConnectionService; +Lio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler; +Lio/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory; +Lio/embrace/android/embracesdk/network/http/HttpMethod; +Lio/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker; +Lio/embrace/android/embracesdk/network/http/NetworkCaptureData; +Lio/embrace/android/embracesdk/network/http/StreamHandlerFactoryInstaller; +Lio/embrace/android/embracesdk/network/logging/DomainSettings; +Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService$Companion; +Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService$networkCaptureEncryptionManager$1; +Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService; +Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService$getNetworkCallsForSession$calls$1; +Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService$networkCallCache$1; +Lio/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService; +Lio/embrace/android/embracesdk/network/logging/NetworkCaptureService; +Lio/embrace/android/embracesdk/network/logging/NetworkLoggingService; +Lio/embrace/android/embracesdk/payload/ActivityLifecycleBreadcrumb; +Lio/embrace/android/embracesdk/payload/ActivityLifecycleData; +Lio/embrace/android/embracesdk/payload/ActivityLifecycleState; +Lio/embrace/android/embracesdk/payload/AnrInterval$Companion; +Lio/embrace/android/embracesdk/payload/AnrInterval$Type; +Lio/embrace/android/embracesdk/payload/AnrInterval; +Lio/embrace/android/embracesdk/payload/AnrSample$Companion; +Lio/embrace/android/embracesdk/payload/AnrSample; +Lio/embrace/android/embracesdk/payload/AnrSampleList; +Lio/embrace/android/embracesdk/payload/AppExitInfoData; +Lio/embrace/android/embracesdk/payload/AppInfo; +Lio/embrace/android/embracesdk/payload/BetaFeatures; +Lio/embrace/android/embracesdk/payload/Breadcrumbs; +Lio/embrace/android/embracesdk/payload/Crash$Companion; +Lio/embrace/android/embracesdk/payload/Crash; +Lio/embrace/android/embracesdk/payload/CustomBreadcrumb$Companion; +Lio/embrace/android/embracesdk/payload/CustomBreadcrumb; +Lio/embrace/android/embracesdk/payload/DeviceInfo; +Lio/embrace/android/embracesdk/payload/DiskUsage; +Lio/embrace/android/embracesdk/payload/Event; +Lio/embrace/android/embracesdk/payload/EventMessage; +Lio/embrace/android/embracesdk/payload/ExceptionError; +Lio/embrace/android/embracesdk/payload/ExceptionErrorInfo; +Lio/embrace/android/embracesdk/payload/ExceptionInfo$Companion; +Lio/embrace/android/embracesdk/payload/ExceptionInfo; +Lio/embrace/android/embracesdk/payload/FragmentBreadcrumb; +Lio/embrace/android/embracesdk/payload/Interval; +Lio/embrace/android/embracesdk/payload/MemoryWarning; +Lio/embrace/android/embracesdk/payload/NativeCrash; +Lio/embrace/android/embracesdk/payload/NativeCrashDataError; +Lio/embrace/android/embracesdk/payload/NativeThreadAnrInterval; +Lio/embrace/android/embracesdk/payload/NativeThreadAnrSample; +Lio/embrace/android/embracesdk/payload/NativeThreadAnrStackframe; +Lio/embrace/android/embracesdk/payload/NetworkCallV2; +Lio/embrace/android/embracesdk/payload/NetworkRequests; +Lio/embrace/android/embracesdk/payload/NetworkSessionV2$DomainCount; +Lio/embrace/android/embracesdk/payload/NetworkSessionV2; +Lio/embrace/android/embracesdk/payload/Orientation; +Lio/embrace/android/embracesdk/payload/PerformanceInfo; +Lio/embrace/android/embracesdk/payload/PowerModeInterval; +Lio/embrace/android/embracesdk/payload/PushNotificationBreadcrumb; +Lio/embrace/android/embracesdk/payload/RnActionBreadcrumb$Companion; +Lio/embrace/android/embracesdk/payload/RnActionBreadcrumb; +Lio/embrace/android/embracesdk/payload/Session$Companion; +Lio/embrace/android/embracesdk/payload/Session$SessionLifeEventType; +Lio/embrace/android/embracesdk/payload/Session; +Lio/embrace/android/embracesdk/payload/SessionMessage; +Lio/embrace/android/embracesdk/payload/Stacktraces$WhenMappings; +Lio/embrace/android/embracesdk/payload/Stacktraces; +Lio/embrace/android/embracesdk/payload/StrictModeViolation; +Lio/embrace/android/embracesdk/payload/TapBreadcrumb$TapBreadcrumbType; +Lio/embrace/android/embracesdk/payload/TapBreadcrumb; +Lio/embrace/android/embracesdk/payload/ThermalState; +Lio/embrace/android/embracesdk/payload/ThreadInfo$Companion; +Lio/embrace/android/embracesdk/payload/ThreadInfo; +Lio/embrace/android/embracesdk/payload/UserInfo$Companion; +Lio/embrace/android/embracesdk/payload/UserInfo; +Lio/embrace/android/embracesdk/payload/ViewBreadcrumb$Companion; +Lio/embrace/android/embracesdk/payload/ViewBreadcrumb; +Lio/embrace/android/embracesdk/payload/WebViewBreadcrumb; +Lio/embrace/android/embracesdk/payload/WebViewInfo; +Lio/embrace/android/embracesdk/payload/WebVital; +Lio/embrace/android/embracesdk/payload/WebVitalType; +Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService$1; +Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService$Companion; +Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService$alterStartupStatus$1; +Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService$sam$java_util_concurrent_Callable$0; +Lio/embrace/android/embracesdk/prefs/EmbracePreferencesService; +Lio/embrace/android/embracesdk/prefs/PreferencesService; +Lio/embrace/android/embracesdk/registry/ServiceRegistry$activityListeners$2; +Lio/embrace/android/embracesdk/registry/ServiceRegistry$closeables$2; +Lio/embrace/android/embracesdk/registry/ServiceRegistry$configListeners$2; +Lio/embrace/android/embracesdk/registry/ServiceRegistry$memoryCleanerListeners$2; +Lio/embrace/android/embracesdk/registry/ServiceRegistry$registerActivityListeners$1; +Lio/embrace/android/embracesdk/registry/ServiceRegistry$registerConfigListeners$1; +Lio/embrace/android/embracesdk/registry/ServiceRegistry$registerMemoryCleanerListeners$1; +Lio/embrace/android/embracesdk/registry/ServiceRegistry; +Lio/embrace/android/embracesdk/session/ActivityListener$DefaultImpls; +Lio/embrace/android/embracesdk/session/ActivityListener; +Lio/embrace/android/embracesdk/session/ActivityService; +Lio/embrace/android/embracesdk/session/BackgroundActivityService; +Lio/embrace/android/embracesdk/session/EmbraceActivityService$1; +Lio/embrace/android/embracesdk/session/EmbraceActivityService$Companion; +Lio/embrace/android/embracesdk/session/EmbraceActivityService; +Lio/embrace/android/embracesdk/session/EmbraceMemoryCleanerService; +Lio/embrace/android/embracesdk/session/EmbraceSessionProperties$Companion; +Lio/embrace/android/embracesdk/session/EmbraceSessionProperties; +Lio/embrace/android/embracesdk/session/EmbraceSessionService$Companion; +Lio/embrace/android/embracesdk/session/EmbraceSessionService$sam$java_lang_Runnable$0; +Lio/embrace/android/embracesdk/session/EmbraceSessionService$startSession$automaticSessionCloserCallback$1; +Lio/embrace/android/embracesdk/session/EmbraceSessionService$startSession$sessionMessage$1; +Lio/embrace/android/embracesdk/session/EmbraceSessionService; +Lio/embrace/android/embracesdk/session/MemoryCleanerListener; +Lio/embrace/android/embracesdk/session/MemoryCleanerService; +Lio/embrace/android/embracesdk/session/SessionHandler$WhenMappings; +Lio/embrace/android/embracesdk/session/SessionHandler; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$appInfo$1; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$breadcrumbs$1; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$deviceInfo$1; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$performanceInfo$1; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$session$1; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$spans$1; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer$serialize$1$userInfo$1; +Lio/embrace/android/embracesdk/session/SessionMessageSerializer; +Lio/embrace/android/embracesdk/session/SessionService; +Lio/embrace/android/embracesdk/spans/EmbraceSpan; +Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent$Companion; +Lio/embrace/android/embracesdk/spans/EmbraceSpanEvent; +Lio/embrace/android/embracesdk/spans/ErrorCode; +Lio/embrace/android/embracesdk/spans/TracingApi; +Lio/embrace/android/embracesdk/utils/BuildVersionChecker; +Lio/embrace/android/embracesdk/utils/NetworkUtils; +Lio/embrace/android/embracesdk/utils/ThreadUtils$runOnMainThread$wrappedRunnable$1; +Lio/embrace/android/embracesdk/utils/ThreadUtils; +Lio/embrace/android/embracesdk/utils/VersionChecker; +Lio/embrace/android/embracesdk/utils/exceptions/Unchecked$wrap$1; +Lio/embrace/android/embracesdk/utils/exceptions/Unchecked; +Lio/embrace/android/embracesdk/utils/exceptions/function/CheckedSupplier; +Lio/embrace/android/embracesdk/worker/ExecutorName; +Lio/embrace/android/embracesdk/worker/WorkerThreadModule; +Lio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl$createThreadFactory$1; +Lio/embrace/android/embracesdk/worker/WorkerThreadModuleImpl; +PLio/embrace/android/embracesdk/anr/EmbraceAnrService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/capture/user/EmbraceUserService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/config/EmbraceConfigService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/event/EmbraceEventService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/internal/spans/EmbraceSpanData;->equals(Ljava/lang/Object;)Z +PLio/embrace/android/embracesdk/ndk/EmbraceNdkService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/payload/Interval;->equals(Ljava/lang/Object;)Z +PLio/embrace/android/embracesdk/payload/ViewBreadcrumb;->setEnd(Ljava/lang/Long;)V +PLio/embrace/android/embracesdk/prefs/EmbracePreferencesService;->onViewClose(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/session/ActivityListener$DefaultImpls;->onViewClose(Lio/embrace/android/embracesdk/session/ActivityListener;Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/session/EmbraceActivityService;->onActivityDestroyed(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/session/EmbraceActivityService;->onActivityPaused(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/session/EmbraceActivityService;->onActivityStopped(Landroid/app/Activity;)V +PLio/embrace/android/embracesdk/session/EmbraceSessionService;->onViewClose(Landroid/app/Activity;)V diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/__libunwind_config.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/__libunwind_config.h new file mode 100644 index 0000000000..bc4e696c0c --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/__libunwind_config.h @@ -0,0 +1,55 @@ +//===------------------------- __libunwind_config.h -----------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is dual licensed under the MIT and the University of Illinois Open +// Source Licenses. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#ifndef ____LIBUNWIND_CONFIG_H__ +#define ____LIBUNWIND_CONFIG_H__ +#if defined(__arm__) && !defined(__USING_SJLJ_EXCEPTIONS__) && \ + !defined(__ARM_DWARF_EH__) +#define _LIBUNWIND_ARM_EHABI 1 +#else +#define _LIBUNWIND_ARM_EHABI 0 +#endif +#if defined(_LIBUNWIND_IS_NATIVE_ONLY) +# if defined(__i386__) +# define _LIBUNWIND_TARGET_I386 1 +# define _LIBUNWIND_CONTEXT_SIZE 8 +# define _LIBUNWIND_CURSOR_SIZE 19 +# elif defined(__x86_64__) +# define _LIBUNWIND_TARGET_X86_64 1 +# define _LIBUNWIND_CONTEXT_SIZE 21 +# define _LIBUNWIND_CURSOR_SIZE 33 +# elif defined(__ppc__) +# define _LIBUNWIND_TARGET_PPC 1 +# define _LIBUNWIND_CONTEXT_SIZE 117 +# define _LIBUNWIND_CURSOR_SIZE 128 +# elif defined(__aarch64__) +# define _LIBUNWIND_TARGET_AARCH64 1 +# define _LIBUNWIND_CONTEXT_SIZE 66 +# define _LIBUNWIND_CURSOR_SIZE 78 +# elif defined(__arm__) +# define _LIBUNWIND_TARGET_ARM 1 +# define _LIBUNWIND_CONTEXT_SIZE 60 +# define _LIBUNWIND_CURSOR_SIZE 67 +# elif defined(__or1k__) +# define _LIBUNWIND_TARGET_OR1K 1 +# define _LIBUNWIND_CONTEXT_SIZE 16 +# define _LIBUNWIND_CURSOR_SIZE 28 +# else +# error "Unsupported architecture." +# endif +#else // !_LIBUNWIND_IS_NATIVE_ONLY +# define _LIBUNWIND_TARGET_I386 1 +# define _LIBUNWIND_TARGET_X86_64 1 +# define _LIBUNWIND_TARGET_PPC 1 +# define _LIBUNWIND_TARGET_AARCH64 1 +# define _LIBUNWIND_TARGET_ARM 1 +# define _LIBUNWIND_TARGET_OR1K 1 +# define _LIBUNWIND_CONTEXT_SIZE 128 +# define _LIBUNWIND_CURSOR_SIZE 140 +#endif // _LIBUNWIND_IS_NATIVE_ONLY +#endif // ____LIBUNWIND_CONFIG_H__ \ No newline at end of file diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/libunwind.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/libunwind.h new file mode 100644 index 0000000000..29917662a8 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwind/include/libunwind.h @@ -0,0 +1,508 @@ +//===---------------------------- libunwind.h -----------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is dual licensed under the MIT and the University of Illinois Open +// Source Licenses. See LICENSE.TXT for details. +// +// +// Compatible with libuwind API documented at: +// http://www.nongnu.org/libunwind/man/libunwind(3).html +// +//===----------------------------------------------------------------------===// +#ifndef __LIBUNWIND__ +#define __LIBUNWIND__ +#include "__libunwind_config.h" +#include +#include +#ifdef __APPLE__ + #include + #ifdef __arm__ + #define LIBUNWIND_AVAIL __attribute__((unavailable)) + #else + #define LIBUNWIND_AVAIL __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_5_0) + #endif +#else + #define LIBUNWIND_AVAIL +#endif +/* error codes */ +enum { + UNW_ESUCCESS = 0, /* no error */ + UNW_EUNSPEC = -6540, /* unspecified (general) error */ + UNW_ENOMEM = -6541, /* out of memory */ + UNW_EBADREG = -6542, /* bad register number */ + UNW_EREADONLYREG = -6543, /* attempt to write read-only register */ + UNW_ESTOPUNWIND = -6544, /* stop unwinding */ + UNW_EINVALIDIP = -6545, /* invalid IP */ + UNW_EBADFRAME = -6546, /* bad frame */ + UNW_EINVAL = -6547, /* unsupported operation or bad value */ + UNW_EBADVERSION = -6548, /* unwind info has unsupported version */ + UNW_ENOINFO = -6549 /* no unwind info found */ +}; +struct unw_context_t { + uint64_t data[_LIBUNWIND_CONTEXT_SIZE]; +}; +typedef struct unw_context_t unw_context_t; +struct unw_cursor_t { + uint64_t data[_LIBUNWIND_CURSOR_SIZE]; +}; +typedef struct unw_cursor_t unw_cursor_t; +typedef struct unw_addr_space *unw_addr_space_t; +typedef int unw_regnum_t; +#if _LIBUNWIND_ARM_EHABI +typedef uint32_t unw_word_t; +typedef uint64_t unw_fpreg_t; +#else +typedef uint64_t unw_word_t; +typedef double unw_fpreg_t; +#endif +struct unw_proc_info_t { + unw_word_t start_ip; /* start address of function */ + unw_word_t end_ip; /* address after end of function */ + unw_word_t lsda; /* address of language specific data area, */ + /* or zero if not used */ + unw_word_t handler; /* personality routine, or zero if not used */ + unw_word_t gp; /* not used */ + unw_word_t flags; /* not used */ + uint32_t format; /* compact unwind encoding, or zero if none */ + uint32_t unwind_info_size; /* size of dwarf unwind info, or zero if none */ + unw_word_t unwind_info; /* address of dwarf unwind info, or zero */ + unw_word_t extra; /* mach_header of mach-o image containing func */ +}; +typedef struct unw_proc_info_t unw_proc_info_t; +#ifdef __cplusplus +extern "C" { +#endif +extern int unw_getcontext(unw_context_t *) LIBUNWIND_AVAIL; +extern int unw_init_local(unw_cursor_t *, unw_context_t *) LIBUNWIND_AVAIL; +extern int unw_step(unw_cursor_t *) LIBUNWIND_AVAIL; +extern int unw_get_reg(unw_cursor_t *, unw_regnum_t, unw_word_t *) LIBUNWIND_AVAIL; +extern int unw_get_fpreg(unw_cursor_t *, unw_regnum_t, unw_fpreg_t *) LIBUNWIND_AVAIL; +extern int unw_set_reg(unw_cursor_t *, unw_regnum_t, unw_word_t) LIBUNWIND_AVAIL; +extern int unw_set_fpreg(unw_cursor_t *, unw_regnum_t, unw_fpreg_t) LIBUNWIND_AVAIL; +extern int unw_resume(unw_cursor_t *) LIBUNWIND_AVAIL; +#ifdef __arm__ +/* Save VFP registers in FSTMX format (instead of FSTMD). */ +extern void unw_save_vfp_as_X(unw_cursor_t *) LIBUNWIND_AVAIL; +#endif +extern const char *unw_regname(unw_cursor_t *, unw_regnum_t) LIBUNWIND_AVAIL; +extern int unw_get_proc_info(unw_cursor_t *, unw_proc_info_t *) LIBUNWIND_AVAIL; +extern int unw_is_fpreg(unw_cursor_t *, unw_regnum_t) LIBUNWIND_AVAIL; +extern int unw_is_signal_frame(unw_cursor_t *) LIBUNWIND_AVAIL; +extern int unw_get_proc_name(unw_cursor_t *, char *, size_t, unw_word_t *) LIBUNWIND_AVAIL; +//extern int unw_get_save_loc(unw_cursor_t*, int, unw_save_loc_t*); +extern unw_addr_space_t unw_local_addr_space; +#ifdef UNW_REMOTE +/* + * Mac OS X "remote" API for unwinding other processes on same machine + * + */ +extern unw_addr_space_t unw_create_addr_space_for_task(task_t); +extern void unw_destroy_addr_space(unw_addr_space_t); +extern int unw_init_remote_thread(unw_cursor_t *, unw_addr_space_t, thread_t *); +#endif /* UNW_REMOTE */ +/* + * traditional libuwind "remote" API + * NOT IMPLEMENTED on Mac OS X + * + * extern int unw_init_remote(unw_cursor_t*, unw_addr_space_t, + * thread_t*); + * extern unw_accessors_t unw_get_accessors(unw_addr_space_t); + * extern unw_addr_space_t unw_create_addr_space(unw_accessors_t, int); + * extern void unw_flush_cache(unw_addr_space_t, unw_word_t, + * unw_word_t); + * extern int unw_set_caching_policy(unw_addr_space_t, + * unw_caching_policy_t); + * extern void _U_dyn_register(unw_dyn_info_t*); + * extern void _U_dyn_cancel(unw_dyn_info_t*); + */ +#ifdef __cplusplus +} +#endif +// architecture independent register numbers +enum { + UNW_REG_IP = -1, // instruction pointer + UNW_REG_SP = -2, // stack pointer +}; +// 32-bit x86 registers +enum { + UNW_X86_EAX = 0, + UNW_X86_ECX = 1, + UNW_X86_EDX = 2, + UNW_X86_EBX = 3, + UNW_X86_EBP = 4, + UNW_X86_ESP = 5, + UNW_X86_ESI = 6, + UNW_X86_EDI = 7 +}; +// 64-bit x86_64 registers +enum { + UNW_X86_64_RAX = 0, + UNW_X86_64_RDX = 1, + UNW_X86_64_RCX = 2, + UNW_X86_64_RBX = 3, + UNW_X86_64_RSI = 4, + UNW_X86_64_RDI = 5, + UNW_X86_64_RBP = 6, + UNW_X86_64_RSP = 7, + UNW_X86_64_R8 = 8, + UNW_X86_64_R9 = 9, + UNW_X86_64_R10 = 10, + UNW_X86_64_R11 = 11, + UNW_X86_64_R12 = 12, + UNW_X86_64_R13 = 13, + UNW_X86_64_R14 = 14, + UNW_X86_64_R15 = 15 +}; +// 32-bit ppc register numbers +enum { + UNW_PPC_R0 = 0, + UNW_PPC_R1 = 1, + UNW_PPC_R2 = 2, + UNW_PPC_R3 = 3, + UNW_PPC_R4 = 4, + UNW_PPC_R5 = 5, + UNW_PPC_R6 = 6, + UNW_PPC_R7 = 7, + UNW_PPC_R8 = 8, + UNW_PPC_R9 = 9, + UNW_PPC_R10 = 10, + UNW_PPC_R11 = 11, + UNW_PPC_R12 = 12, + UNW_PPC_R13 = 13, + UNW_PPC_R14 = 14, + UNW_PPC_R15 = 15, + UNW_PPC_R16 = 16, + UNW_PPC_R17 = 17, + UNW_PPC_R18 = 18, + UNW_PPC_R19 = 19, + UNW_PPC_R20 = 20, + UNW_PPC_R21 = 21, + UNW_PPC_R22 = 22, + UNW_PPC_R23 = 23, + UNW_PPC_R24 = 24, + UNW_PPC_R25 = 25, + UNW_PPC_R26 = 26, + UNW_PPC_R27 = 27, + UNW_PPC_R28 = 28, + UNW_PPC_R29 = 29, + UNW_PPC_R30 = 30, + UNW_PPC_R31 = 31, + UNW_PPC_F0 = 32, + UNW_PPC_F1 = 33, + UNW_PPC_F2 = 34, + UNW_PPC_F3 = 35, + UNW_PPC_F4 = 36, + UNW_PPC_F5 = 37, + UNW_PPC_F6 = 38, + UNW_PPC_F7 = 39, + UNW_PPC_F8 = 40, + UNW_PPC_F9 = 41, + UNW_PPC_F10 = 42, + UNW_PPC_F11 = 43, + UNW_PPC_F12 = 44, + UNW_PPC_F13 = 45, + UNW_PPC_F14 = 46, + UNW_PPC_F15 = 47, + UNW_PPC_F16 = 48, + UNW_PPC_F17 = 49, + UNW_PPC_F18 = 50, + UNW_PPC_F19 = 51, + UNW_PPC_F20 = 52, + UNW_PPC_F21 = 53, + UNW_PPC_F22 = 54, + UNW_PPC_F23 = 55, + UNW_PPC_F24 = 56, + UNW_PPC_F25 = 57, + UNW_PPC_F26 = 58, + UNW_PPC_F27 = 59, + UNW_PPC_F28 = 60, + UNW_PPC_F29 = 61, + UNW_PPC_F30 = 62, + UNW_PPC_F31 = 63, + UNW_PPC_MQ = 64, + UNW_PPC_LR = 65, + UNW_PPC_CTR = 66, + UNW_PPC_AP = 67, + UNW_PPC_CR0 = 68, + UNW_PPC_CR1 = 69, + UNW_PPC_CR2 = 70, + UNW_PPC_CR3 = 71, + UNW_PPC_CR4 = 72, + UNW_PPC_CR5 = 73, + UNW_PPC_CR6 = 74, + UNW_PPC_CR7 = 75, + UNW_PPC_XER = 76, + UNW_PPC_V0 = 77, + UNW_PPC_V1 = 78, + UNW_PPC_V2 = 79, + UNW_PPC_V3 = 80, + UNW_PPC_V4 = 81, + UNW_PPC_V5 = 82, + UNW_PPC_V6 = 83, + UNW_PPC_V7 = 84, + UNW_PPC_V8 = 85, + UNW_PPC_V9 = 86, + UNW_PPC_V10 = 87, + UNW_PPC_V11 = 88, + UNW_PPC_V12 = 89, + UNW_PPC_V13 = 90, + UNW_PPC_V14 = 91, + UNW_PPC_V15 = 92, + UNW_PPC_V16 = 93, + UNW_PPC_V17 = 94, + UNW_PPC_V18 = 95, + UNW_PPC_V19 = 96, + UNW_PPC_V20 = 97, + UNW_PPC_V21 = 98, + UNW_PPC_V22 = 99, + UNW_PPC_V23 = 100, + UNW_PPC_V24 = 101, + UNW_PPC_V25 = 102, + UNW_PPC_V26 = 103, + UNW_PPC_V27 = 104, + UNW_PPC_V28 = 105, + UNW_PPC_V29 = 106, + UNW_PPC_V30 = 107, + UNW_PPC_V31 = 108, + UNW_PPC_VRSAVE = 109, + UNW_PPC_VSCR = 110, + UNW_PPC_SPE_ACC = 111, + UNW_PPC_SPEFSCR = 112 +}; +// 64-bit ARM64 registers +enum { + UNW_ARM64_X0 = 0, + UNW_ARM64_X1 = 1, + UNW_ARM64_X2 = 2, + UNW_ARM64_X3 = 3, + UNW_ARM64_X4 = 4, + UNW_ARM64_X5 = 5, + UNW_ARM64_X6 = 6, + UNW_ARM64_X7 = 7, + UNW_ARM64_X8 = 8, + UNW_ARM64_X9 = 9, + UNW_ARM64_X10 = 10, + UNW_ARM64_X11 = 11, + UNW_ARM64_X12 = 12, + UNW_ARM64_X13 = 13, + UNW_ARM64_X14 = 14, + UNW_ARM64_X15 = 15, + UNW_ARM64_X16 = 16, + UNW_ARM64_X17 = 17, + UNW_ARM64_X18 = 18, + UNW_ARM64_X19 = 19, + UNW_ARM64_X20 = 20, + UNW_ARM64_X21 = 21, + UNW_ARM64_X22 = 22, + UNW_ARM64_X23 = 23, + UNW_ARM64_X24 = 24, + UNW_ARM64_X25 = 25, + UNW_ARM64_X26 = 26, + UNW_ARM64_X27 = 27, + UNW_ARM64_X28 = 28, + UNW_ARM64_X29 = 29, + UNW_ARM64_FP = 29, + UNW_ARM64_X30 = 30, + UNW_ARM64_LR = 30, + UNW_ARM64_X31 = 31, + UNW_ARM64_SP = 31, + // reserved block + UNW_ARM64_D0 = 64, + UNW_ARM64_D1 = 65, + UNW_ARM64_D2 = 66, + UNW_ARM64_D3 = 67, + UNW_ARM64_D4 = 68, + UNW_ARM64_D5 = 69, + UNW_ARM64_D6 = 70, + UNW_ARM64_D7 = 71, + UNW_ARM64_D8 = 72, + UNW_ARM64_D9 = 73, + UNW_ARM64_D10 = 74, + UNW_ARM64_D11 = 75, + UNW_ARM64_D12 = 76, + UNW_ARM64_D13 = 77, + UNW_ARM64_D14 = 78, + UNW_ARM64_D15 = 79, + UNW_ARM64_D16 = 80, + UNW_ARM64_D17 = 81, + UNW_ARM64_D18 = 82, + UNW_ARM64_D19 = 83, + UNW_ARM64_D20 = 84, + UNW_ARM64_D21 = 85, + UNW_ARM64_D22 = 86, + UNW_ARM64_D23 = 87, + UNW_ARM64_D24 = 88, + UNW_ARM64_D25 = 89, + UNW_ARM64_D26 = 90, + UNW_ARM64_D27 = 91, + UNW_ARM64_D28 = 92, + UNW_ARM64_D29 = 93, + UNW_ARM64_D30 = 94, + UNW_ARM64_D31 = 95, +}; +// 32-bit ARM registers. Numbers match DWARF for ARM spec #3.1 Table 1. +// Naming scheme uses recommendations given in Note 4 for VFP-v2 and VFP-v3. +// In this scheme, even though the 64-bit floating point registers D0-D31 +// overlap physically with the 32-bit floating pointer registers S0-S31, +// they are given a non-overlapping range of register numbers. +// +// Commented out ranges are not preserved during unwinding. +enum { + UNW_ARM_R0 = 0, + UNW_ARM_R1 = 1, + UNW_ARM_R2 = 2, + UNW_ARM_R3 = 3, + UNW_ARM_R4 = 4, + UNW_ARM_R5 = 5, + UNW_ARM_R6 = 6, + UNW_ARM_R7 = 7, + UNW_ARM_R8 = 8, + UNW_ARM_R9 = 9, + UNW_ARM_R10 = 10, + UNW_ARM_R11 = 11, + UNW_ARM_R12 = 12, + UNW_ARM_SP = 13, // Logical alias for UNW_REG_SP + UNW_ARM_R13 = 13, + UNW_ARM_LR = 14, + UNW_ARM_R14 = 14, + UNW_ARM_IP = 15, // Logical alias for UNW_REG_IP + UNW_ARM_R15 = 15, + // 16-63 -- OBSOLETE. Used in VFP1 to represent both S0-S31 and D0-D31. + UNW_ARM_S0 = 64, + UNW_ARM_S1 = 65, + UNW_ARM_S2 = 66, + UNW_ARM_S3 = 67, + UNW_ARM_S4 = 68, + UNW_ARM_S5 = 69, + UNW_ARM_S6 = 70, + UNW_ARM_S7 = 71, + UNW_ARM_S8 = 72, + UNW_ARM_S9 = 73, + UNW_ARM_S10 = 74, + UNW_ARM_S11 = 75, + UNW_ARM_S12 = 76, + UNW_ARM_S13 = 77, + UNW_ARM_S14 = 78, + UNW_ARM_S15 = 79, + UNW_ARM_S16 = 80, + UNW_ARM_S17 = 81, + UNW_ARM_S18 = 82, + UNW_ARM_S19 = 83, + UNW_ARM_S20 = 84, + UNW_ARM_S21 = 85, + UNW_ARM_S22 = 86, + UNW_ARM_S23 = 87, + UNW_ARM_S24 = 88, + UNW_ARM_S25 = 89, + UNW_ARM_S26 = 90, + UNW_ARM_S27 = 91, + UNW_ARM_S28 = 92, + UNW_ARM_S29 = 93, + UNW_ARM_S30 = 94, + UNW_ARM_S31 = 95, + // 96-103 -- OBSOLETE. F0-F7. Used by the FPA system. Superseded by VFP. + // 104-111 -- wCGR0-wCGR7, ACC0-ACC7 (Intel wireless MMX) + UNW_ARM_WR0 = 112, + UNW_ARM_WR1 = 113, + UNW_ARM_WR2 = 114, + UNW_ARM_WR3 = 115, + UNW_ARM_WR4 = 116, + UNW_ARM_WR5 = 117, + UNW_ARM_WR6 = 118, + UNW_ARM_WR7 = 119, + UNW_ARM_WR8 = 120, + UNW_ARM_WR9 = 121, + UNW_ARM_WR10 = 122, + UNW_ARM_WR11 = 123, + UNW_ARM_WR12 = 124, + UNW_ARM_WR13 = 125, + UNW_ARM_WR14 = 126, + UNW_ARM_WR15 = 127, + // 128-133 -- SPSR, SPSR_{FIQ|IRQ|ABT|UND|SVC} + // 134-143 -- Reserved + // 144-150 -- R8_USR-R14_USR + // 151-157 -- R8_FIQ-R14_FIQ + // 158-159 -- R13_IRQ-R14_IRQ + // 160-161 -- R13_ABT-R14_ABT + // 162-163 -- R13_UND-R14_UND + // 164-165 -- R13_SVC-R14_SVC + // 166-191 -- Reserved + UNW_ARM_WC0 = 192, + UNW_ARM_WC1 = 193, + UNW_ARM_WC2 = 194, + UNW_ARM_WC3 = 195, + // 196-199 -- wC4-wC7 (Intel wireless MMX control) + // 200-255 -- Reserved + UNW_ARM_D0 = 256, + UNW_ARM_D1 = 257, + UNW_ARM_D2 = 258, + UNW_ARM_D3 = 259, + UNW_ARM_D4 = 260, + UNW_ARM_D5 = 261, + UNW_ARM_D6 = 262, + UNW_ARM_D7 = 263, + UNW_ARM_D8 = 264, + UNW_ARM_D9 = 265, + UNW_ARM_D10 = 266, + UNW_ARM_D11 = 267, + UNW_ARM_D12 = 268, + UNW_ARM_D13 = 269, + UNW_ARM_D14 = 270, + UNW_ARM_D15 = 271, + UNW_ARM_D16 = 272, + UNW_ARM_D17 = 273, + UNW_ARM_D18 = 274, + UNW_ARM_D19 = 275, + UNW_ARM_D20 = 276, + UNW_ARM_D21 = 277, + UNW_ARM_D22 = 278, + UNW_ARM_D23 = 279, + UNW_ARM_D24 = 280, + UNW_ARM_D25 = 281, + UNW_ARM_D26 = 282, + UNW_ARM_D27 = 283, + UNW_ARM_D28 = 284, + UNW_ARM_D29 = 285, + UNW_ARM_D30 = 286, + UNW_ARM_D31 = 287, + // 288-319 -- Reserved for VFP/Neon + // 320-8191 -- Reserved + // 8192-16383 -- Unspecified vendor co-processor register. +}; +// OpenRISC1000 register numbers +enum { + UNW_OR1K_R0 = 0, + UNW_OR1K_R1 = 1, + UNW_OR1K_R2 = 2, + UNW_OR1K_R3 = 3, + UNW_OR1K_R4 = 4, + UNW_OR1K_R5 = 5, + UNW_OR1K_R6 = 6, + UNW_OR1K_R7 = 7, + UNW_OR1K_R8 = 8, + UNW_OR1K_R9 = 9, + UNW_OR1K_R10 = 10, + UNW_OR1K_R11 = 11, + UNW_OR1K_R12 = 12, + UNW_OR1K_R13 = 13, + UNW_OR1K_R14 = 14, + UNW_OR1K_R15 = 15, + UNW_OR1K_R16 = 16, + UNW_OR1K_R17 = 17, + UNW_OR1K_R18 = 18, + UNW_OR1K_R19 = 19, + UNW_OR1K_R20 = 20, + UNW_OR1K_R21 = 21, + UNW_OR1K_R22 = 22, + UNW_OR1K_R23 = 23, + UNW_OR1K_R24 = 24, + UNW_OR1K_R25 = 25, + UNW_OR1K_R26 = 26, + UNW_OR1K_R27 = 27, + UNW_OR1K_R28 = 28, + UNW_OR1K_R29 = 29, + UNW_OR1K_R30 = 30, + UNW_OR1K_R31 = 31, +}; +#endif diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AndroidUnwinder.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AndroidUnwinder.cpp new file mode 100644 index 0000000000..1e6980abf3 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AndroidUnwinder.cpp @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +static int kThreadUnwindSignal = SIGRTMIN; + +// Use the demangler from libc++. +extern "C" char* __cxa_demangle(const char*, char*, size_t*, int* status); + +namespace unwindstack { + +void AndroidUnwinderData::DemangleFunctionNames() { + for (auto& frame : frames) { + char* demangled_name = __cxa_demangle(frame.function_name.c_str(), nullptr, nullptr, nullptr); + if (demangled_name != nullptr) { + frame.function_name = demangled_name; + free(demangled_name); + } + } +} + +std::string AndroidUnwinderData::GetErrorString() { + std::string error_msg(GetErrorCodeString(error.code)); + if (error.address != 0) { + error_msg += android::base::StringPrintf(" at address 0x%" PRIx64, error.address); + } + return error_msg; +} + +AndroidUnwinder* AndroidUnwinder::Create(pid_t pid) { + if (pid == getpid()) { + return new AndroidLocalUnwinder; + } else { + return new AndroidRemoteUnwinder(pid); + } +} + +bool AndroidUnwinder::Initialize(ErrorData& error) { + // Android stores the jit and dex file location only in the library + // libart.so or libartd.so. + static std::vector search_libs [[clang::no_destroy]] = {"libart.so", "libartd.so"}; + + std::call_once(initialize_, [this, &error]() { + if (!InternalInitialize(error)) { + initialize_status_ = false; + return; + } + + jit_debug_ = CreateJitDebug(arch_, process_memory_, search_libs); + +#if defined(DEXFILE_SUPPORT) + dex_files_ = CreateDexFiles(arch_, process_memory_, search_libs); +#endif + initialize_status_ = true; + }); + + return initialize_status_; +} + +std::string AndroidUnwinder::FormatFrame(const FrameData& frame) const { + if (arch_ == ARCH_UNKNOWN) { + return ""; + } + return Unwinder::FormatFrame(arch_, frame); +} + +bool AndroidLocalUnwinder::InternalInitialize(ErrorData& error) { + arch_ = Regs::CurrentArch(); + + maps_.reset(new LocalUpdatableMaps); + if (!maps_->Parse()) { + error.code = ERROR_MAPS_PARSE; + return false; + } + + if (process_memory_ == nullptr) { + process_memory_ = Memory::CreateProcessMemoryThreadCached(getpid()); + } + + return true; +} + +FrameData AndroidUnwinder::BuildFrameFromPcOnly(uint64_t pc) { + return Unwinder::BuildFrameFromPcOnly(pc, arch_, maps_.get(), jit_debug_.get(), process_memory_, + true); +} + +bool AndroidUnwinder::Unwind(AndroidUnwinderData& data) { + return Unwind(std::nullopt, data); +} + +bool AndroidUnwinder::Unwind(std::optional tid, AndroidUnwinderData& data) { + if (!Initialize(data.error)) { + return false; + } + + return InternalUnwind(tid, data); +} + +bool AndroidUnwinder::Unwind(void* ucontext, AndroidUnwinderData& data) { + if (ucontext == nullptr) { + data.error.code = ERROR_INVALID_PARAMETER; + return false; + } + + if (!Initialize(data.error)) { + return false; + } + + std::unique_ptr regs(Regs::CreateFromUcontext(arch_, ucontext)); + return Unwind(regs.get(), data); +} + +bool AndroidUnwinder::Unwind(Regs* initial_regs, AndroidUnwinderData& data) { + if (initial_regs == nullptr) { + data.error.code = ERROR_INVALID_PARAMETER; + return false; + } + + if (!Initialize(data.error)) { + return false; + } + + if (arch_ != initial_regs->Arch()) { + data.error.code = ERROR_BAD_ARCH; + return false; + } + + std::unique_ptr regs(initial_regs->Clone()); + if (data.saved_initial_regs) { + (*data.saved_initial_regs).reset(initial_regs->Clone()); + } + Unwinder unwinder(data.max_frames.value_or(max_frames_), maps_.get(), regs.get(), + process_memory_); + unwinder.SetJitDebug(jit_debug_.get()); + unwinder.SetDexFiles(dex_files_.get()); + unwinder.Unwind(data.show_all_frames ? nullptr : &initial_map_names_to_skip_, + &map_suffixes_to_ignore_); + data.frames = unwinder.ConsumeFrames(); + data.error = unwinder.LastError(); + return data.frames.size() != 0; +} + +bool AndroidLocalUnwinder::InternalUnwind(std::optional tid, AndroidUnwinderData& data) { + if (!tid) { + tid = android::base::GetThreadId(); + } + + if (static_cast(*tid) == android::base::GetThreadId()) { + // Unwind current thread. + std::unique_ptr regs(Regs::CreateFromLocal()); + RegsGetLocal(regs.get()); + return AndroidUnwinder::Unwind(regs.get(), data); + } + + ThreadUnwinder unwinder(data.max_frames.value_or(max_frames_), maps_.get(), process_memory_); + unwinder.SetJitDebug(jit_debug_.get()); + unwinder.SetDexFiles(dex_files_.get()); + std::unique_ptr* initial_regs = nullptr; + if (data.saved_initial_regs) { + initial_regs = &data.saved_initial_regs.value(); + } + unwinder.UnwindWithSignal(kThreadUnwindSignal, *tid, initial_regs, + data.show_all_frames ? nullptr : &initial_map_names_to_skip_, + &map_suffixes_to_ignore_); + data.frames = unwinder.ConsumeFrames(); + data.error = unwinder.LastError(); + return data.frames.size() != 0; +} + +bool AndroidRemoteUnwinder::InternalInitialize(ErrorData& error) { + if (arch_ == ARCH_UNKNOWN) { + arch_ = Regs::RemoteGetArch(pid_, &error.code); + } + if (arch_ == ARCH_UNKNOWN) { + return false; + } + + maps_.reset(new RemoteMaps(pid_)); + if (!maps_->Parse()) { + error.code = ERROR_MAPS_PARSE; + return false; + } + + if (process_memory_ == nullptr) { + process_memory_ = Memory::CreateProcessMemoryCached(pid_); + } + + return true; +} + +bool AndroidRemoteUnwinder::InternalUnwind(std::optional tid, AndroidUnwinderData& data) { + if (!tid) { + tid = pid_; + } + + std::unique_ptr regs(Regs::RemoteGet(*tid, &data.error.code)); + if (regs == nullptr) { + return false; + } + return AndroidUnwinder::Unwind(regs.get(), data); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.cpp new file mode 100644 index 0000000000..c737cefc1b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.cpp @@ -0,0 +1,863 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#include "ArmExidx.h" +#include "Check.h" + +namespace unwindstack { + +static constexpr uint8_t LOG_CFA_REG = 64; + +void ArmExidx::LogRawData() { + std::string log_str("Raw Data:"); + for (const uint8_t data : data_) { + log_str += android::base::StringPrintf(" 0x%02x", data); + } + Log::Info(log_indent_, "%s", log_str.c_str()); +} + +bool ArmExidx::ExtractEntryData(uint32_t entry_offset) { + data_.clear(); + status_ = ARM_STATUS_NONE; + + if (entry_offset & 1) { + // The offset needs to be at least two byte aligned. + status_ = ARM_STATUS_INVALID_ALIGNMENT; + return false; + } + + // Each entry is a 32 bit prel31 offset followed by 32 bits + // of unwind information. If bit 31 of the unwind data is zero, + // then this is a prel31 offset to the start of the unwind data. + // If the unwind data is 1, then this is a cant unwind entry. + // Otherwise, this data is the compact form of the unwind information. + uint32_t data; + if (!elf_memory_->Read32(entry_offset + 4, &data)) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = entry_offset + 4; + return false; + } + if (data == 1) { + // This is a CANT UNWIND entry. + status_ = ARM_STATUS_NO_UNWIND; + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + Log::Info(log_indent_, "Raw Data: 0x00 0x00 0x00 0x01"); + } + Log::Info(log_indent_, "[cantunwind]"); + } + return false; + } + + if (data & (1UL << 31)) { + // This is a compact table entry. + if ((data >> 24) & 0xf) { + // This is a non-zero index, this code doesn't support + // other formats. + status_ = ARM_STATUS_INVALID_PERSONALITY; + return false; + } + data_.push_back((data >> 16) & 0xff); + data_.push_back((data >> 8) & 0xff); + uint8_t last_op = data & 0xff; + data_.push_back(last_op); + if (last_op != ARM_OP_FINISH) { + // If this didn't end with a finish op, add one. + data_.push_back(ARM_OP_FINISH); + } + if (log_type_ == ARM_LOG_FULL) { + LogRawData(); + } + return true; + } + + // Get the address of the ops. + // Sign extend the data value if necessary. + int32_t signed_data = static_cast(data << 1) >> 1; + uint32_t addr = (entry_offset + 4) + signed_data; + if (!elf_memory_->Read32(addr, &data)) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = addr; + return false; + } + + size_t num_table_words; + if (data & (1UL << 31)) { + // Compact model. + switch ((data >> 24) & 0xf) { + case 0: + num_table_words = 0; + data_.push_back((data >> 16) & 0xff); + break; + case 1: + case 2: + num_table_words = (data >> 16) & 0xff; + addr += 4; + break; + default: + // Only a personality of 0, 1, 2 is valid. + status_ = ARM_STATUS_INVALID_PERSONALITY; + return false; + } + data_.push_back((data >> 8) & 0xff); + data_.push_back(data & 0xff); + } else { + // Generic model. + + // Skip the personality routine data, it doesn't contain any data + // needed to decode the unwind information. + addr += 4; + if (!elf_memory_->Read32(addr, &data)) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = addr; + return false; + } + num_table_words = (data >> 24) & 0xff; + data_.push_back((data >> 16) & 0xff); + data_.push_back((data >> 8) & 0xff); + data_.push_back(data & 0xff); + addr += 4; + } + + if (num_table_words > 5) { + status_ = ARM_STATUS_MALFORMED; + return false; + } + + for (size_t i = 0; i < num_table_words; i++) { + if (!elf_memory_->Read32(addr, &data)) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = addr; + return false; + } + data_.push_back((data >> 24) & 0xff); + data_.push_back((data >> 16) & 0xff); + data_.push_back((data >> 8) & 0xff); + data_.push_back(data & 0xff); + addr += 4; + } + + if (data_.back() != ARM_OP_FINISH) { + // If this didn't end with a finish op, add one. + data_.push_back(ARM_OP_FINISH); + } + + if (log_type_ == ARM_LOG_FULL) { + LogRawData(); + } + return true; +} + +inline bool ArmExidx::GetByte(uint8_t* byte) { + if (data_.empty()) { + status_ = ARM_STATUS_TRUNCATED; + return false; + } + *byte = data_.front(); + data_.pop_front(); + return true; +} + +inline bool ArmExidx::DecodePrefix_10_00(uint8_t byte) { + CHECK((byte >> 4) == 0x8); + + uint16_t registers = (byte & 0xf) << 8; + if (!GetByte(&byte)) { + return false; + } + + registers |= byte; + if (registers == 0) { + // 10000000 00000000: Refuse to unwind + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Refuse to unwind"); + } + status_ = ARM_STATUS_NO_UNWIND; + return false; + } + // 1000iiii iiiiiiii: Pop up to 12 integer registers under masks {r15-r12}, {r11-r4} + registers <<= 4; + + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + bool add_comma = false; + std::string msg = "pop {"; + for (size_t reg = 4; reg < 16; reg++) { + if (registers & (1 << reg)) { + if (add_comma) { + msg += ", "; + } + msg += android::base::StringPrintf("r%zu", reg); + add_comma = true; + } + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + uint32_t cfa_offset = __builtin_popcount(registers) * 4; + log_cfa_offset_ += cfa_offset; + for (size_t reg = 4; reg < 16; reg++) { + if (registers & (1 << reg)) { + log_regs_[reg] = cfa_offset; + cfa_offset -= 4; + } + } + } + + if (log_skip_execution_) { + return true; + } + } + + for (size_t reg = 4; reg < 16; reg++) { + if (registers & (1 << reg)) { + if (!process_memory_->Read32(cfa_, &(*regs_)[reg])) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = cfa_; + return false; + } + cfa_ += 4; + } + } + + // If the sp register is modified, change the cfa value. + if (registers & (1 << ARM_REG_SP)) { + cfa_ = (*regs_)[ARM_REG_SP]; + } + + // Indicate if the pc register was set. + if (registers & (1 << ARM_REG_PC)) { + pc_set_ = true; + } + return true; +} + +inline bool ArmExidx::DecodePrefix_10_01(uint8_t byte) { + CHECK((byte >> 4) == 0x9); + + uint8_t bits = byte & 0xf; + if (bits == 13 || bits == 15) { + // 10011101: Reserved as prefix for ARM register to register moves + // 10011111: Reserved as prefix for Intel Wireless MMX register to register moves + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "[Reserved]"); + } + status_ = ARM_STATUS_RESERVED; + return false; + } + // 1001nnnn: Set vsp = r[nnnn] (nnnn != 13, 15) + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + Log::Info(log_indent_, "vsp = r%d", bits); + } else { + log_regs_[LOG_CFA_REG] = bits; + } + + if (log_skip_execution_) { + return true; + } + } + // It is impossible for bits to be larger than the total number of + // arm registers, so don't bother checking if bits is a valid register. + cfa_ = (*regs_)[bits]; + return true; +} + +inline bool ArmExidx::DecodePrefix_10_10(uint8_t byte) { + CHECK((byte >> 4) == 0xa); + + // 10100nnn: Pop r4-r[4+nnn] + // 10101nnn: Pop r4-r[4+nnn], r14 + if (log_type_ != ARM_LOG_NONE) { + uint8_t end_reg = byte & 0x7; + if (log_type_ == ARM_LOG_FULL) { + std::string msg = "pop {r4"; + if (end_reg) { + msg += android::base::StringPrintf("-r%d", 4 + end_reg); + } + if (byte & 0x8) { + Log::Info(log_indent_, "%s, r14}", msg.c_str()); + } else { + Log::Info(log_indent_, "%s}", msg.c_str()); + } + } else { + end_reg += 4; + uint32_t cfa_offset = (end_reg - 3) * 4; + if (byte & 0x8) { + cfa_offset += 4; + } + log_cfa_offset_ += cfa_offset; + + for (uint8_t reg = 4; reg <= end_reg; reg++) { + log_regs_[reg] = cfa_offset; + cfa_offset -= 4; + } + + if (byte & 0x8) { + log_regs_[14] = cfa_offset; + } + } + + if (log_skip_execution_) { + return true; + } + } + + for (size_t i = 4; i <= 4 + (byte & 0x7); i++) { + if (!process_memory_->Read32(cfa_, &(*regs_)[i])) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = cfa_; + return false; + } + cfa_ += 4; + } + if (byte & 0x8) { + if (!process_memory_->Read32(cfa_, &(*regs_)[ARM_REG_R14])) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = cfa_; + return false; + } + cfa_ += 4; + } + return true; +} + +inline bool ArmExidx::DecodePrefix_10_11_0000() { + // 10110000: Finish + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + Log::Info(log_indent_, "finish"); + } + + if (log_skip_execution_) { + status_ = ARM_STATUS_FINISH; + return false; + } + } + status_ = ARM_STATUS_FINISH; + return false; +} + +inline bool ArmExidx::DecodePrefix_10_11_0001() { + uint8_t byte; + if (!GetByte(&byte)) { + return false; + } + + if (byte == 0) { + // 10110001 00000000: Spare + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Spare"); + } + status_ = ARM_STATUS_SPARE; + return false; + } + if (byte >> 4) { + // 10110001 xxxxyyyy: Spare (xxxx != 0000) + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Spare"); + } + status_ = ARM_STATUS_SPARE; + return false; + } + + // 10110001 0000iiii: Pop integer registers under mask {r3, r2, r1, r0} + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + bool add_comma = false; + std::string msg = "pop {"; + for (size_t i = 0; i < 4; i++) { + if (byte & (1 << i)) { + if (add_comma) { + msg += ", "; + } + msg += android::base::StringPrintf("r%zu", i); + add_comma = true; + } + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + byte &= 0xf; + uint32_t cfa_offset = __builtin_popcount(byte) * 4; + log_cfa_offset_ += cfa_offset; + for (size_t reg = 0; reg < 4; reg++) { + if (byte & (1 << reg)) { + log_regs_[reg] = cfa_offset; + cfa_offset -= 4; + } + } + } + + if (log_skip_execution_) { + return true; + } + } + + for (size_t reg = 0; reg < 4; reg++) { + if (byte & (1 << reg)) { + if (!process_memory_->Read32(cfa_, &(*regs_)[reg])) { + status_ = ARM_STATUS_READ_FAILED; + status_address_ = cfa_; + return false; + } + cfa_ += 4; + } + } + return true; +} + +inline void ArmExidx::AdjustRegisters(int32_t offset) { + for (auto& entry : log_regs_) { + if (entry.first >= LOG_CFA_REG) { + break; + } + entry.second += offset; + } +} + +inline bool ArmExidx::DecodePrefix_10_11_0010() { + // 10110010 uleb128: vsp = vsp + 0x204 + (uleb128 << 2) + uint32_t result = 0; + uint32_t shift = 0; + uint8_t byte; + do { + if (!GetByte(&byte)) { + return false; + } + + result |= (byte & 0x7f) << shift; + shift += 7; + } while (byte & 0x80); + result <<= 2; + if (log_type_ != ARM_LOG_NONE) { + int32_t cfa_offset = 0x204 + result; + if (log_type_ == ARM_LOG_FULL) { + Log::Info(log_indent_, "vsp = vsp + %d", cfa_offset); + } else { + log_cfa_offset_ += cfa_offset; + } + AdjustRegisters(cfa_offset); + + if (log_skip_execution_) { + return true; + } + } + cfa_ += 0x204 + result; + return true; +} + +inline bool ArmExidx::DecodePrefix_10_11_0011() { + // 10110011 sssscccc: Pop VFP double precision registers D[ssss]-D[ssss+cccc] by FSTMFDX + uint8_t byte; + if (!GetByte(&byte)) { + return false; + } + + if (log_type_ != ARM_LOG_NONE) { + uint8_t start_reg = byte >> 4; + uint8_t end_reg = start_reg + (byte & 0xf); + + if (log_type_ == ARM_LOG_FULL) { + std::string msg = android::base::StringPrintf("pop {d%d", start_reg); + if (end_reg) { + msg += android::base::StringPrintf("-d%d", end_reg); + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported DX register display"); + } + + if (log_skip_execution_) { + return true; + } + } + cfa_ += (byte & 0xf) * 8 + 12; + return true; +} + +inline bool ArmExidx::DecodePrefix_10_11_01nn() { + // 101101nn: Spare + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Spare"); + } + status_ = ARM_STATUS_SPARE; + return false; +} + +inline bool ArmExidx::DecodePrefix_10_11_1nnn(uint8_t byte) { + CHECK((byte & ~0x07) == 0xb8); + + // 10111nnn: Pop VFP double-precision registers D[8]-D[8+nnn] by FSTMFDX + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + uint8_t last_reg = (byte & 0x7); + std::string msg = "pop {d8"; + if (last_reg) { + msg += android::base::StringPrintf("-d%d", last_reg + 8); + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported DX register display"); + } + + if (log_skip_execution_) { + return true; + } + } + // Only update the cfa. + cfa_ += (byte & 0x7) * 8 + 12; + return true; +} + +inline bool ArmExidx::DecodePrefix_10(uint8_t byte) { + CHECK((byte >> 6) == 0x2); + + switch ((byte >> 4) & 0x3) { + case 0: + return DecodePrefix_10_00(byte); + case 1: + return DecodePrefix_10_01(byte); + case 2: + return DecodePrefix_10_10(byte); + default: + switch (byte & 0xf) { + case 0: + return DecodePrefix_10_11_0000(); + case 1: + return DecodePrefix_10_11_0001(); + case 2: + return DecodePrefix_10_11_0010(); + case 3: + return DecodePrefix_10_11_0011(); + default: + if (byte & 0x8) { + return DecodePrefix_10_11_1nnn(byte); + } else { + return DecodePrefix_10_11_01nn(); + } + } + } +} + +inline bool ArmExidx::DecodePrefix_11_000(uint8_t byte) { + CHECK((byte & ~0x07) == 0xc0); + + uint8_t bits = byte & 0x7; + if (bits == 6) { + if (!GetByte(&byte)) { + return false; + } + + // 11000110 sssscccc: Intel Wireless MMX pop wR[ssss]-wR[ssss+cccc] + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + uint8_t start_reg = byte >> 4; + std::string msg = android::base::StringPrintf("pop {wR%d", start_reg); + uint8_t end_reg = byte & 0xf; + if (end_reg) { + msg += android::base::StringPrintf("-wR%d", start_reg + end_reg); + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported wRX register display"); + } + + if (log_skip_execution_) { + return true; + } + } + // Only update the cfa. + cfa_ += (byte & 0xf) * 8 + 8; + } else if (bits == 7) { + if (!GetByte(&byte)) { + return false; + } + + if (byte == 0) { + // 11000111 00000000: Spare + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Spare"); + } + status_ = ARM_STATUS_SPARE; + return false; + } else if ((byte >> 4) == 0) { + // 11000111 0000iiii: Intel Wireless MMX pop wCGR registers {wCGR0,1,2,3} + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + bool add_comma = false; + std::string msg = "pop {"; + for (size_t i = 0; i < 4; i++) { + if (byte & (1 << i)) { + if (add_comma) { + msg += ", "; + } + msg += android::base::StringPrintf("wCGR%zu", i); + add_comma = true; + } + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported wCGR register display"); + } + + if (log_skip_execution_) { + return true; + } + } + // Only update the cfa. + cfa_ += __builtin_popcount(byte) * 4; + } else { + // 11000111 xxxxyyyy: Spare (xxxx != 0000) + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Spare"); + } + status_ = ARM_STATUS_SPARE; + return false; + } + } else { + // 11000nnn: Intel Wireless MMX pop wR[10]-wR[10+nnn] (nnn != 6, 7) + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + std::string msg = "pop {wR10"; + uint8_t nnn = byte & 0x7; + if (nnn) { + msg += android::base::StringPrintf("-wR%d", 10 + nnn); + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported wRX register display"); + } + + if (log_skip_execution_) { + return true; + } + } + // Only update the cfa. + cfa_ += (byte & 0x7) * 8 + 8; + } + return true; +} + +inline bool ArmExidx::DecodePrefix_11_001(uint8_t byte) { + CHECK((byte & ~0x07) == 0xc8); + + uint8_t bits = byte & 0x7; + if (bits == 0) { + // 11001000 sssscccc: Pop VFP double precision registers D[16+ssss]-D[16+ssss+cccc] by VPUSH + if (!GetByte(&byte)) { + return false; + } + + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + uint8_t start_reg = byte >> 4; + std::string msg = android::base::StringPrintf("pop {d%d", 16 + start_reg); + uint8_t end_reg = byte & 0xf; + if (end_reg) { + msg += android::base::StringPrintf("-d%d", 16 + start_reg + end_reg); + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported DX register display"); + } + + if (log_skip_execution_) { + return true; + } + } + // Only update the cfa. + cfa_ += (byte & 0xf) * 8 + 8; + } else if (bits == 1) { + // 11001001 sssscccc: Pop VFP double precision registers D[ssss]-D[ssss+cccc] by VPUSH + if (!GetByte(&byte)) { + return false; + } + + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + uint8_t start_reg = byte >> 4; + std::string msg = android::base::StringPrintf("pop {d%d", start_reg); + uint8_t end_reg = byte & 0xf; + if (end_reg) { + msg += android::base::StringPrintf("-d%d", start_reg + end_reg); + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported DX register display"); + } + + if (log_skip_execution_) { + return true; + } + } + // Only update the cfa. + cfa_ += (byte & 0xf) * 8 + 8; + } else { + // 11001yyy: Spare (yyy != 000, 001) + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Spare"); + } + status_ = ARM_STATUS_SPARE; + return false; + } + return true; +} + +inline bool ArmExidx::DecodePrefix_11_010(uint8_t byte) { + CHECK((byte & ~0x07) == 0xd0); + + // 11010nnn: Pop VFP double precision registers D[8]-D[8+nnn] by VPUSH + if (log_type_ != ARM_LOG_NONE) { + if (log_type_ == ARM_LOG_FULL) { + std::string msg = "pop {d8"; + uint8_t end_reg = byte & 0x7; + if (end_reg) { + msg += android::base::StringPrintf("-d%d", 8 + end_reg); + } + Log::Info(log_indent_, "%s}", msg.c_str()); + } else { + Log::Info(log_indent_, "Unsupported DX register display"); + } + + if (log_skip_execution_) { + return true; + } + } + cfa_ += (byte & 0x7) * 8 + 8; + return true; +} + +inline bool ArmExidx::DecodePrefix_11(uint8_t byte) { + CHECK((byte >> 6) == 0x3); + + switch ((byte >> 3) & 0x7) { + case 0: + return DecodePrefix_11_000(byte); + case 1: + return DecodePrefix_11_001(byte); + case 2: + return DecodePrefix_11_010(byte); + default: + // 11xxxyyy: Spare (xxx != 000, 001, 010) + if (log_type_ != ARM_LOG_NONE) { + Log::Info(log_indent_, "Spare"); + } + status_ = ARM_STATUS_SPARE; + return false; + } +} + +bool ArmExidx::Decode() { + status_ = ARM_STATUS_NONE; + uint8_t byte; + if (!GetByte(&byte)) { + return false; + } + + switch (byte >> 6) { + case 0: + // 00xxxxxx: vsp = vsp + (xxxxxxx << 2) + 4 + if (log_type_ != ARM_LOG_NONE) { + int32_t cfa_offset = ((byte & 0x3f) << 2) + 4; + if (log_type_ == ARM_LOG_FULL) { + Log::Info(log_indent_, "vsp = vsp + %d", cfa_offset); + } else { + log_cfa_offset_ += cfa_offset; + } + AdjustRegisters(cfa_offset); + + if (log_skip_execution_) { + break; + } + } + cfa_ += ((byte & 0x3f) << 2) + 4; + break; + case 1: + // 01xxxxxx: vsp = vsp - (xxxxxxx << 2) + 4 + if (log_type_ != ARM_LOG_NONE) { + uint32_t cfa_offset = ((byte & 0x3f) << 2) + 4; + if (log_type_ == ARM_LOG_FULL) { + Log::Info(log_indent_, "vsp = vsp - %d", cfa_offset); + } else { + log_cfa_offset_ -= cfa_offset; + } + AdjustRegisters(-cfa_offset); + + if (log_skip_execution_) { + break; + } + } + cfa_ -= ((byte & 0x3f) << 2) + 4; + break; + case 2: + return DecodePrefix_10(byte); + default: + return DecodePrefix_11(byte); + } + return true; +} + +bool ArmExidx::Eval() { + pc_set_ = false; + while (Decode()); + return status_ == ARM_STATUS_FINISH; +} + +void ArmExidx::LogByReg() { + if (log_type_ != ARM_LOG_BY_REG) { + return; + } + + uint8_t cfa_reg; + if (log_regs_.count(LOG_CFA_REG) == 0) { + cfa_reg = 13; + } else { + cfa_reg = log_regs_[LOG_CFA_REG]; + } + + if (log_cfa_offset_ != 0) { + char sign = (log_cfa_offset_ > 0) ? '+' : '-'; + Log::Info(log_indent_, "cfa = r%" PRIu8 " %c %d", cfa_reg, sign, abs(log_cfa_offset_)); + } else { + Log::Info(log_indent_, "cfa = r%" PRIu8, cfa_reg); + } + + for (const auto& entry : log_regs_) { + if (entry.first >= LOG_CFA_REG) { + break; + } + if (entry.second == 0) { + Log::Info(log_indent_, "r%" PRIu8 " = [cfa]", entry.first); + } else { + char sign = (entry.second > 0) ? '-' : '+'; + Log::Info(log_indent_, "r%" PRIu8 " = [cfa %c %d]", entry.first, sign, abs(entry.second)); + } + } +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.h new file mode 100644 index 0000000000..847613a98e --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ArmExidx.h @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; +class RegsArm; + +enum ArmStatus : size_t { + ARM_STATUS_NONE = 0, + ARM_STATUS_NO_UNWIND, + ARM_STATUS_FINISH, + ARM_STATUS_RESERVED, + ARM_STATUS_SPARE, + ARM_STATUS_TRUNCATED, + ARM_STATUS_READ_FAILED, + ARM_STATUS_MALFORMED, + ARM_STATUS_INVALID_ALIGNMENT, + ARM_STATUS_INVALID_PERSONALITY, +}; + +enum ArmOp : uint8_t { + ARM_OP_FINISH = 0xb0, +}; + +enum ArmLogType : uint8_t { + ARM_LOG_NONE, + ARM_LOG_FULL, + ARM_LOG_BY_REG, +}; + +class ArmExidx { + public: + ArmExidx(RegsArm* regs, Memory* elf_memory, Memory* process_memory) + : regs_(regs), elf_memory_(elf_memory), process_memory_(process_memory) {} + virtual ~ArmExidx() {} + + void LogRawData(); + + void LogByReg(); + + bool ExtractEntryData(uint32_t entry_offset); + + bool Eval(); + + bool Decode(); + + std::deque* data() { return &data_; } + + ArmStatus status() { return status_; } + uint64_t status_address() { return status_address_; } + + RegsArm* regs() { return regs_; } + + uint32_t cfa() { return cfa_; } + void set_cfa(uint32_t cfa) { cfa_ = cfa; } + + bool pc_set() { return pc_set_; } + void set_pc_set(bool pc_set) { pc_set_ = pc_set; } + + void set_log(ArmLogType log_type) { log_type_ = log_type; } + void set_log_skip_execution(bool skip_execution) { log_skip_execution_ = skip_execution; } + void set_log_indent(uint8_t indent) { log_indent_ = indent; } + + private: + bool GetByte(uint8_t* byte); + void AdjustRegisters(int32_t offset); + + bool DecodePrefix_10_00(uint8_t byte); + bool DecodePrefix_10_01(uint8_t byte); + bool DecodePrefix_10_10(uint8_t byte); + bool DecodePrefix_10_11_0000(); + bool DecodePrefix_10_11_0001(); + bool DecodePrefix_10_11_0010(); + bool DecodePrefix_10_11_0011(); + bool DecodePrefix_10_11_01nn(); + bool DecodePrefix_10_11_1nnn(uint8_t byte); + bool DecodePrefix_10(uint8_t byte); + + bool DecodePrefix_11_000(uint8_t byte); + bool DecodePrefix_11_001(uint8_t byte); + bool DecodePrefix_11_010(uint8_t byte); + bool DecodePrefix_11(uint8_t byte); + + RegsArm* regs_ = nullptr; + uint32_t cfa_ = 0; + std::deque data_; + ArmStatus status_ = ARM_STATUS_NONE; + uint64_t status_address_ = 0; + + Memory* elf_memory_; + Memory* process_memory_; + + ArmLogType log_type_ = ARM_LOG_NONE; + uint8_t log_indent_ = 0; + bool log_skip_execution_ = false; + bool pc_set_ = false; + int32_t log_cfa_offset_ = 0; + std::map log_regs_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86.S b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86.S new file mode 100644 index 0000000000..021e6286c0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86.S @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + + .text + .global AsmGetRegs + .balign 16 + .type AsmGetRegs, @function +AsmGetRegs: + .cfi_startproc + mov 4(%esp), %eax + movl $0, (%eax) + movl %ecx, 4(%eax) + movl %edx, 8(%eax) + movl %ebx, 12(%eax) + + /* ESP */ + leal 4(%esp), %ecx + movl %ecx, 16(%eax) + + movl %ebp, 20(%eax) + movl %esi, 24(%eax) + movl %edi, 28(%eax) + + /* EIP */ + movl (%esp), %ecx + movl %ecx, 32(%eax) + + mov %cs, 36(%eax) + mov %ss, 40(%eax) + mov %ds, 44(%eax) + mov %es, 48(%eax) + mov %fs, 52(%eax) + mov %gs, 56(%eax) + ret + + .cfi_endproc + .size AsmGetRegs, .-AsmGetRegs diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86_64.S b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86_64.S new file mode 100644 index 0000000000..4cd3b6fb63 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/AsmGetRegsX86_64.S @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + + .text + .global AsmGetRegs + .balign 16 + .type AsmGetRegs, @function +AsmGetRegs: + .cfi_startproc + movq %rax, (%rdi) + movq %rdx, 8(%rdi) + movq %rcx, 16(%rdi) + movq %rbx, 24(%rdi) + movq %rsi, 32(%rdi) + movq %rdi, 40(%rdi) + movq %rbp, 48(%rdi) + + /* RSP */ + lea 8(%rsp), %rax + movq %rax, 56(%rdi) + + movq %r8, 64(%rdi) + movq %r9, 72(%rdi) + movq %r10, 80(%rdi) + movq %r11, 88(%rdi) + movq %r12, 96(%rdi) + movq %r13, 104(%rdi) + movq %r14, 112(%rdi) + movq %r15, 120(%rdi) + + /* RIP */ + movq (%rsp), %rax + movq %rax, 128(%rdi) + ret + + .cfi_endproc + .size AsmGetRegs, .-AsmGetRegs diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Check.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Check.h new file mode 100644 index 0000000000..102cbb1746 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Check.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +#define CHECK(assertion) \ + if (__builtin_expect(!(assertion), false)) { \ + Log::Error("%s:%d: %s\n", __FILE__, __LINE__, #assertion); \ + abort(); \ + } + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.cpp new file mode 100644 index 0000000000..220ee82a0a --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include "DexFile.h" +#include "MemoryBuffer.h" + +namespace unwindstack { + +std::map> DexFile::g_mapped_dex_files; +std::mutex DexFile::g_lock; + +static bool CheckDexSupport() { + if (std::string err_msg; !art_api::dex::TryLoadLibdexfile(&err_msg)) { + Log::Error("Failed to initialize DEX file support: %s", err_msg.c_str()); + return false; + } + return true; +} + +std::shared_ptr DexFile::CreateFromDisk(uint64_t addr, uint64_t size, MapInfo* map) { + if (map == nullptr || map->name().empty()) { + return nullptr; // MapInfo not backed by file. + } + if (!(map->start() <= addr && addr < map->end())) { + return nullptr; // addr is not in MapInfo range. + } + if (size > (map->end() - addr)) { + return nullptr; // size is past the MapInfo end. + } + uint64_t offset_in_file = (addr - map->start()) + map->offset(); + + // Fast-path: Check if the dex file was already mapped from disk. + std::lock_guard guard(g_lock); + MappedFileKey cache_key(map->name(), offset_in_file, size); + std::weak_ptr& cache_entry = g_mapped_dex_files[cache_key]; + std::shared_ptr dex_api = cache_entry.lock(); + if (dex_api != nullptr) { + return std::shared_ptr(new DexFile(addr, size, std::move(dex_api))); + } + + // Load the file from disk and cache it. + std::unique_ptr memory = Memory::CreateFileMemory(map->name(), offset_in_file, size); + if (memory == nullptr) { + return nullptr; // failed to map the file. + } + std::unique_ptr dex; + art_api::dex::DexFile::Create(memory->GetPtr(), size, nullptr, map->name().c_str(), &dex); + if (dex == nullptr) { + return nullptr; // invalid DEX file. + } + dex_api.reset(new DexFileApi{std::move(dex), std::move(memory), std::mutex()}); + cache_entry = dex_api; + return std::shared_ptr(new DexFile(addr, size, std::move(dex_api))); +} + +std::shared_ptr DexFile::Create(uint64_t base_addr, uint64_t file_size, Memory* memory, + MapInfo* info) { + static bool has_dex_support = CheckDexSupport(); + if (!has_dex_support || file_size == 0) { + return nullptr; + } + + // Do not try to open the DEX file if the file name ends with "(deleted)". It does not exist. + // This happens when an app is background-optimized by ART and all of its files are replaced. + // Furthermore, do NOT try to fallback to in-memory copy. It would work, but all apps tend to + // be background-optimized at the same time, so it would lead to excessive memory use during + // system-wide profiling (essentially copying all dex files for all apps: hundreds of MBs). + // This will cause missing symbols in the backtrace, however, that outcome is inevitable + // anyway, since we can not obtain mini-debug-info for the deleted .oat files. + const std::string_view filename(info != nullptr ? info->name() : ""); + const std::string_view kDeleted("(deleted)"); + if (filename.size() >= kDeleted.size() && + filename.substr(filename.size() - kDeleted.size()) == kDeleted) { + return nullptr; + } + + std::shared_ptr dex_file = CreateFromDisk(base_addr, file_size, info); + if (dex_file != nullptr) { + return dex_file; + } + + // Fallback: make copy in local buffer. + std::unique_ptr copy(new MemoryBuffer); + if (!copy->Resize(file_size)) { + return nullptr; + } + if (!memory->ReadFully(base_addr, copy->GetPtr(0), file_size)) { + return nullptr; + } + std::unique_ptr dex; + art_api::dex::DexFile::Create(copy->GetPtr(0), file_size, nullptr, "", &dex); + if (dex == nullptr) { + return nullptr; + } + std::shared_ptr api(new DexFileApi{std::move(dex), std::move(copy), std::mutex()}); + return std::shared_ptr(new DexFile(base_addr, file_size, std::move(api))); +} + +bool DexFile::GetFunctionName(uint64_t dex_pc, SharedString* method_name, uint64_t* method_offset) { + uint64_t dex_offset = dex_pc - base_addr_; // Convert absolute PC to file-relative offset. + + // Lookup the function in the cache. + std::lock_guard guard(dex_api_->lock_); // Protect both the symbols and the C API. + auto it = symbols_.upper_bound(dex_offset); + if (it == symbols_.end() || dex_offset < it->second.offset) { + // Lookup the function in the underlying dex file. + size_t found = dex_api_->dex_->FindMethodAtOffset(dex_offset, [&](const auto& method) { + size_t code_size, name_size; + uint32_t offset = method.GetCodeOffset(&code_size); + const char* name = method.GetQualifiedName(/*with_params=*/false, &name_size); + it = symbols_.emplace(offset + code_size, Info{offset, std::string(name, name_size)}).first; + }); + if (found == 0) { + return false; + } + } + + // Return the found function. + *method_offset = dex_offset - it->second.offset; + *method_name = it->second.name; + return true; +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.h new file mode 100644 index 0000000000..3bb65daee3 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFile.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace unwindstack { + +class MapInfo; +class Memory; + +class DexFile { + struct Info { + uint32_t offset; // Symbol start offset (relative to start of dex file). + SharedString name; + }; + + public: + bool IsValidPc(uint64_t dex_pc) { + return base_addr_ <= dex_pc && (dex_pc - base_addr_) < file_size_; + } + + bool GetFunctionName(uint64_t dex_pc, SharedString* method_name, uint64_t* method_offset); + + static std::shared_ptr Create(uint64_t base_addr, uint64_t file_size, Memory* memory, + MapInfo* info); + + private: + // The underlying C API. It might be shared by multiple DexFiles (with different base_addr). + struct DexFileApi { + std::unique_ptr dex_; + std::unique_ptr memory_; // Keep alive the memory object backing the dex file data. + std::mutex lock_; // The C API is not thread-safe so we need to lock it. + }; + + static std::shared_ptr CreateFromDisk(uint64_t addr, uint64_t size, MapInfo* map); + + DexFile(uint64_t base_addr, uint64_t file_size, std::shared_ptr&& dex_api) + : base_addr_(base_addr), file_size_(file_size), dex_api_(std::move(dex_api)) {} + + uint64_t base_addr_ = 0; // Absolute address where this DEX file is in memory. + uint64_t file_size_ = 0; // Total number of bytes in the dex file. + std::shared_ptr dex_api_; // Loaded underling dex object. + + std::map symbols_; // Cache of read symbols (keyed by *end* offset). + + // The same file can be mapped many times in system-wide profiling (once per process). + // Furthermore, the ART side of the API will create expensive PC lookup table for it. + // Therefore, we maintain cache to avoid loading the same file (sub-range) many times. + // The cache is weak: It will not keep DexFiles alive (the weak_ptr will become null). + using MappedFileKey = std::tuple; // (path, offset, size). + static std::map> g_mapped_dex_files; + static std::mutex g_lock; // Guards the static cache above. +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFiles.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFiles.cpp new file mode 100644 index 0000000000..981f158351 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DexFiles.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#if defined(DEXFILE_SUPPORT) +#include "DexFile.h" +#endif + +#include "GlobalDebugImpl.h" + +namespace unwindstack { + +#if defined(DEXFILE_SUPPORT) + +template <> +bool GlobalDebugInterface::Load(Maps* maps, std::shared_ptr& memory, uint64_t addr, + uint64_t size, /*out*/ std::shared_ptr& dex) { + dex = DexFile::Create(addr, size, memory.get(), maps->Find(addr).get()); + return dex.get() != nullptr; +} + +std::unique_ptr CreateDexFiles(ArchEnum arch, std::shared_ptr& memory, + std::vector search_libs) { + return CreateGlobalDebugImpl(arch, memory, search_libs, "__dex_debug_descriptor"); +} + +#else + +template <> +bool GlobalDebugInterface::Load(Maps*, std::shared_ptr&, uint64_t, uint64_t, + std::shared_ptr&) { + return false; +} + +std::unique_ptr CreateDexFiles(ArchEnum, std::shared_ptr&, + std::vector) { + return nullptr; +} + +#endif + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.cpp new file mode 100644 index 0000000000..c167cd455e --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.cpp @@ -0,0 +1,776 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "DwarfCfa.h" +#include "DwarfEncoding.h" +#include "DwarfOp.h" + +namespace unwindstack { + +template +constexpr typename DwarfCfa::process_func DwarfCfa::kCallbackTable[64]; + +template +bool DwarfCfa::GetLocationInfo(uint64_t pc, uint64_t start_offset, uint64_t end_offset, + DwarfLocations* loc_regs) { + if (cie_loc_regs_ != nullptr) { + for (const auto& entry : *cie_loc_regs_) { + (*loc_regs)[entry.first] = entry.second; + } + } + last_error_.code = DWARF_ERROR_NONE; + last_error_.address = 0; + + memory_->set_cur_offset(start_offset); + uint64_t cfa_offset; + cur_pc_ = fde_->pc_start; + loc_regs->pc_start = cur_pc_; + while (true) { + if (cur_pc_ > pc) { + loc_regs->pc_end = cur_pc_; + return true; + } + if ((cfa_offset = memory_->cur_offset()) >= end_offset) { + loc_regs->pc_end = fde_->pc_end; + return true; + } + loc_regs->pc_start = cur_pc_; + operands_.clear(); + // Read the cfa information. + uint8_t cfa_value; + if (!memory_->ReadBytes(&cfa_value, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_->cur_offset(); + return false; + } + uint8_t cfa_low = cfa_value & 0x3f; + // Check the 2 high bits. + switch (cfa_value >> 6) { + case 1: + cur_pc_ += cfa_low * fde_->cie->code_alignment_factor; + break; + case 2: { + uint64_t offset; + if (!memory_->ReadULEB128(&offset)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_->cur_offset(); + return false; + } + SignedType signed_offset = + static_cast(offset) * fde_->cie->data_alignment_factor; + (*loc_regs)[cfa_low] = {.type = DWARF_LOCATION_OFFSET, + .values = {static_cast(signed_offset)}}; + break; + } + case 3: { + if (cie_loc_regs_ == nullptr) { + Log::Error("Invalid: restore while processing cie."); + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return false; + } + + auto reg_entry = cie_loc_regs_->find(cfa_low); + if (reg_entry == cie_loc_regs_->end()) { + loc_regs->erase(cfa_low); + } else { + (*loc_regs)[cfa_low] = reg_entry->second; + } + break; + } + case 0: { + const auto handle_func = DwarfCfa::kCallbackTable[cfa_low]; + if (handle_func == nullptr) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + const auto cfa = &DwarfCfaInfo::kTable[cfa_low]; + for (size_t i = 0; i < cfa->num_operands; i++) { + if (cfa->operands[i] == DW_EH_PE_block) { + uint64_t block_length; + if (!memory_->ReadULEB128(&block_length)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_->cur_offset(); + return false; + } + operands_.push_back(block_length); + memory_->set_cur_offset(memory_->cur_offset() + block_length); + continue; + } + uint64_t value; + if (!memory_->ReadEncodedValue(cfa->operands[i], &value)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_->cur_offset(); + return false; + } + operands_.push_back(value); + } + + if (!(this->*handle_func)(loc_regs)) { + return false; + } + break; + } + } + } +} + +template +std::string DwarfCfa::GetOperandString(uint8_t operand, uint64_t value, + uint64_t* cur_pc) { + std::string string; + switch (operand) { + case DwarfCfaInfo::DWARF_DISPLAY_REGISTER: + string = " register(" + std::to_string(value) + ")"; + break; + case DwarfCfaInfo::DWARF_DISPLAY_SIGNED_NUMBER: + string += " " + std::to_string(static_cast(value)); + break; + case DwarfCfaInfo::DWARF_DISPLAY_ADVANCE_LOC: + *cur_pc += value; + FALLTHROUGH_INTENDED; + // Fall through to log the value. + case DwarfCfaInfo::DWARF_DISPLAY_NUMBER: + string += " " + std::to_string(value); + break; + case DwarfCfaInfo::DWARF_DISPLAY_SET_LOC: + *cur_pc = value; + FALLTHROUGH_INTENDED; + // Fall through to log the value. + case DwarfCfaInfo::DWARF_DISPLAY_ADDRESS: + if (std::is_same::value) { + string += android::base::StringPrintf(" 0x%" PRIx32, static_cast(value)); + } else { + string += android::base::StringPrintf(" 0x%" PRIx64, static_cast(value)); + } + break; + default: + string = " unknown"; + } + return string; +} + +template +bool DwarfCfa::LogOffsetRegisterString(uint32_t indent, uint64_t cfa_offset, + uint8_t reg) { + uint64_t offset; + if (!memory_->ReadULEB128(&offset)) { + return false; + } + uint64_t end_offset = memory_->cur_offset(); + memory_->set_cur_offset(cfa_offset); + + std::string raw_data = "Raw Data:"; + for (uint64_t i = cfa_offset; i < end_offset; i++) { + uint8_t value; + if (!memory_->ReadBytes(&value, 1)) { + return false; + } + raw_data += android::base::StringPrintf(" 0x%02x", value); + } + Log::Info(indent, "DW_CFA_offset register(%d) %" PRId64, reg, offset); + Log::Info(indent, "%s", raw_data.c_str()); + return true; +} + +template +bool DwarfCfa::LogInstruction(uint32_t indent, uint64_t cfa_offset, uint8_t op, + uint64_t* cur_pc) { + const auto* cfa = &DwarfCfaInfo::kTable[op]; + if (cfa->name[0] == '\0' || (arch_ != ARCH_ARM64 && op == 0x2d)) { + if (op == 0x2d) { + Log::Info(indent, "Illegal (Only valid on aarch64)"); + } else { + Log::Info(indent, "Illegal"); + } + Log::Info(indent, "Raw Data: 0x%02x", op); + return true; + } + + std::string log_string(cfa->name); + std::vector expression_lines; + for (size_t i = 0; i < cfa->num_operands; i++) { + if (cfa->operands[i] == DW_EH_PE_block) { + // This is a Dwarf Expression. + uint64_t end_offset; + if (!memory_->ReadULEB128(&end_offset)) { + return false; + } + log_string += " " + std::to_string(end_offset); + end_offset += memory_->cur_offset(); + + DwarfOp op(memory_, nullptr); + op.GetLogInfo(memory_->cur_offset(), end_offset, &expression_lines); + memory_->set_cur_offset(end_offset); + } else { + uint64_t value; + if (!memory_->ReadEncodedValue(cfa->operands[i], &value)) { + return false; + } + log_string += GetOperandString(cfa->display_operands[i], value, cur_pc); + } + } + Log::Info(indent, "%s", log_string.c_str()); + + // Get the raw bytes of the data. + uint64_t end_offset = memory_->cur_offset(); + memory_->set_cur_offset(cfa_offset); + std::string raw_data("Raw Data:"); + for (uint64_t i = 0; i < end_offset - cfa_offset; i++) { + uint8_t value; + if (!memory_->ReadBytes(&value, 1)) { + return false; + } + + // Only show 10 raw bytes per line. + if ((i % 10) == 0 && i != 0) { + Log::Info(indent, "%s", raw_data.c_str()); + raw_data.clear(); + } + if (raw_data.empty()) { + raw_data = "Raw Data:"; + } + raw_data += android::base::StringPrintf(" 0x%02x", value); + } + if (!raw_data.empty()) { + Log::Info(indent, "%s", raw_data.c_str()); + } + + // Log any of the expression data. + for (const auto& line : expression_lines) { + Log::Info(indent + 1, "%s", line.c_str()); + } + return true; +} + +template +bool DwarfCfa::Log(uint32_t indent, uint64_t pc, uint64_t start_offset, + uint64_t end_offset) { + memory_->set_cur_offset(start_offset); + uint64_t cfa_offset; + uint64_t cur_pc = fde_->pc_start; + uint64_t old_pc = cur_pc; + while ((cfa_offset = memory_->cur_offset()) < end_offset && cur_pc <= pc) { + // Read the cfa information. + uint8_t cfa_value; + if (!memory_->ReadBytes(&cfa_value, 1)) { + return false; + } + + // Check the 2 high bits. + uint8_t cfa_low = cfa_value & 0x3f; + switch (cfa_value >> 6) { + case 0: + if (!LogInstruction(indent, cfa_offset, cfa_low, &cur_pc)) { + return false; + } + break; + case 1: + Log::Info(indent, "DW_CFA_advance_loc %d", cfa_low); + Log::Info(indent, "Raw Data: 0x%02x", cfa_value); + cur_pc += cfa_low * fde_->cie->code_alignment_factor; + break; + case 2: + if (!LogOffsetRegisterString(indent, cfa_offset, cfa_low)) { + return false; + } + break; + case 3: + Log::Info(indent, "DW_CFA_restore register(%d)", cfa_low); + Log::Info(indent, "Raw Data: 0x%02x", cfa_value); + break; + } + if (cur_pc != old_pc) { + // This forces a newline or empty log line. + Log::Info(""); + Log::Info(indent, "PC 0x%" PRIx64, cur_pc); + } + old_pc = cur_pc; + } + return true; +} + +// Static data. +template +bool DwarfCfa::cfa_nop(DwarfLocations*) { + return true; +} + +template <> +bool DwarfCfa::cfa_set_loc(DwarfLocations*) { + uint32_t cur_pc = cur_pc_; + uint32_t new_pc = operands_[0]; + if (new_pc < cur_pc) { + Log::Info("Warning: PC is moving backwards: old 0x%" PRIx32 " new 0x%" PRIx32, cur_pc, new_pc); + } + cur_pc_ = new_pc; + return true; +} + +template <> +bool DwarfCfa::cfa_set_loc(DwarfLocations*) { + uint64_t cur_pc = cur_pc_; + uint64_t new_pc = operands_[0]; + if (new_pc < cur_pc) { + Log::Info("Warning: PC is moving backwards: old 0x%" PRIx64 " new 0x%" PRIx64, cur_pc, new_pc); + } + cur_pc_ = new_pc; + return true; +} + +template +bool DwarfCfa::cfa_advance_loc(DwarfLocations*) { + cur_pc_ += operands_[0] * fde_->cie->code_alignment_factor; + return true; +} + +template +bool DwarfCfa::cfa_offset(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_OFFSET, .values = {operands_[1]}}; + return true; +} + +template +bool DwarfCfa::cfa_restore(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + if (cie_loc_regs_ == nullptr) { + Log::Error("Invalid: restore while processing cie."); + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return false; + } + auto reg_entry = cie_loc_regs_->find(reg); + if (reg_entry == cie_loc_regs_->end()) { + loc_regs->erase(reg); + } else { + (*loc_regs)[reg] = reg_entry->second; + } + return true; +} + +template +bool DwarfCfa::cfa_undefined(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_UNDEFINED}; + return true; +} + +template +bool DwarfCfa::cfa_same_value(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + loc_regs->erase(reg); + return true; +} + +template +bool DwarfCfa::cfa_register(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + AddressType reg_dst = operands_[1]; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_REGISTER, .values = {reg_dst}}; + return true; +} + +template +bool DwarfCfa::cfa_remember_state(DwarfLocations* loc_regs) { + loc_reg_state_.push(*loc_regs); + return true; +} + +template +bool DwarfCfa::cfa_restore_state(DwarfLocations* loc_regs) { + if (loc_reg_state_.size() == 0) { + Log::Info("Warning: Attempt to restore without remember."); + return true; + } + *loc_regs = loc_reg_state_.top(); + loc_reg_state_.pop(); + return true; +} + +template +bool DwarfCfa::cfa_def_cfa(DwarfLocations* loc_regs) { + (*loc_regs)[CFA_REG] = {.type = DWARF_LOCATION_REGISTER, .values = {operands_[0], operands_[1]}}; + return true; +} + +template +bool DwarfCfa::cfa_def_cfa_register(DwarfLocations* loc_regs) { + auto cfa_location = loc_regs->find(CFA_REG); + if (cfa_location == loc_regs->end() || cfa_location->second.type != DWARF_LOCATION_REGISTER) { + Log::Error("Attempt to set new register, but cfa is not already set to a register."); + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return false; + } + + cfa_location->second.values[0] = operands_[0]; + return true; +} + +template +bool DwarfCfa::cfa_def_cfa_offset(DwarfLocations* loc_regs) { + // Changing the offset if this is not a register is illegal. + auto cfa_location = loc_regs->find(CFA_REG); + if (cfa_location == loc_regs->end() || cfa_location->second.type != DWARF_LOCATION_REGISTER) { + Log::Error("Attempt to set offset, but cfa is not set to a register."); + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return false; + } + cfa_location->second.values[1] = operands_[0]; + return true; +} + +template +bool DwarfCfa::cfa_def_cfa_expression(DwarfLocations* loc_regs) { + // There is only one type of expression for CFA evaluation and the DWARF + // specification is unclear whether it returns the address or the + // dereferenced value. GDB expects the value, so will we. + (*loc_regs)[CFA_REG] = {.type = DWARF_LOCATION_VAL_EXPRESSION, + .values = {operands_[0], memory_->cur_offset()}}; + return true; +} + +template +bool DwarfCfa::cfa_expression(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_EXPRESSION, + .values = {operands_[1], memory_->cur_offset()}}; + return true; +} + +template +bool DwarfCfa::cfa_offset_extended_sf(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + SignedType value = static_cast(operands_[1]) * fde_->cie->data_alignment_factor; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_OFFSET, .values = {static_cast(value)}}; + return true; +} + +template +bool DwarfCfa::cfa_def_cfa_sf(DwarfLocations* loc_regs) { + SignedType offset = static_cast(operands_[1]) * fde_->cie->data_alignment_factor; + (*loc_regs)[CFA_REG] = {.type = DWARF_LOCATION_REGISTER, + .values = {operands_[0], static_cast(offset)}}; + return true; +} + +template +bool DwarfCfa::cfa_def_cfa_offset_sf(DwarfLocations* loc_regs) { + // Changing the offset if this is not a register is illegal. + auto cfa_location = loc_regs->find(CFA_REG); + if (cfa_location == loc_regs->end() || cfa_location->second.type != DWARF_LOCATION_REGISTER) { + Log::Error("Attempt to set offset, but cfa is not set to a register."); + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return false; + } + SignedType offset = static_cast(operands_[0]) * fde_->cie->data_alignment_factor; + cfa_location->second.values[1] = static_cast(offset); + return true; +} + +template +bool DwarfCfa::cfa_val_offset(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + SignedType offset = static_cast(operands_[1]) * fde_->cie->data_alignment_factor; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_VAL_OFFSET, .values = {static_cast(offset)}}; + return true; +} + +template +bool DwarfCfa::cfa_val_offset_sf(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + SignedType offset = static_cast(operands_[1]) * fde_->cie->data_alignment_factor; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_VAL_OFFSET, .values = {static_cast(offset)}}; + return true; +} + +template +bool DwarfCfa::cfa_val_expression(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + (*loc_regs)[reg] = {.type = DWARF_LOCATION_VAL_EXPRESSION, + .values = {operands_[1], memory_->cur_offset()}}; + return true; +} + +template +bool DwarfCfa::cfa_gnu_negative_offset_extended(DwarfLocations* loc_regs) { + AddressType reg = operands_[0]; + SignedType offset = -static_cast(operands_[1]); + (*loc_regs)[reg] = {.type = DWARF_LOCATION_OFFSET, .values = {static_cast(offset)}}; + return true; +} + +template +bool DwarfCfa::cfa_aarch64_negate_ra_state(DwarfLocations* loc_regs) { + // Only supported on aarch64. + if (arch_ != ARCH_ARM64) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + auto cfa_location = loc_regs->find(Arm64Reg::ARM64_PREG_RA_SIGN_STATE); + if (cfa_location == loc_regs->end()) { + (*loc_regs)[Arm64Reg::ARM64_PREG_RA_SIGN_STATE] = {.type = DWARF_LOCATION_PSEUDO_REGISTER, + .values = {1}}; + } else { + cfa_location->second.values[0] ^= 1; + } + return true; +} + +const DwarfCfaInfo::Info DwarfCfaInfo::kTable[64] = { + { + // 0x00 DW_CFA_nop + "DW_CFA_nop", + 2, + 0, + {}, + {}, + }, + { + "DW_CFA_set_loc", // 0x01 DW_CFA_set_loc + 2, + 1, + {DW_EH_PE_absptr}, + {DWARF_DISPLAY_SET_LOC}, + }, + { + "DW_CFA_advance_loc1", // 0x02 DW_CFA_advance_loc1 + 2, + 1, + {DW_EH_PE_udata1}, + {DWARF_DISPLAY_ADVANCE_LOC}, + }, + { + "DW_CFA_advance_loc2", // 0x03 DW_CFA_advance_loc2 + 2, + 1, + {DW_EH_PE_udata2}, + {DWARF_DISPLAY_ADVANCE_LOC}, + }, + { + "DW_CFA_advance_loc4", // 0x04 DW_CFA_advance_loc4 + 2, + 1, + {DW_EH_PE_udata4}, + {DWARF_DISPLAY_ADVANCE_LOC}, + }, + { + "DW_CFA_offset_extended", // 0x05 DW_CFA_offset_extended + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_NUMBER}, + }, + { + "DW_CFA_restore_extended", // 0x06 DW_CFA_restore_extended + 2, + 1, + {DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER}, + }, + { + "DW_CFA_undefined", // 0x07 DW_CFA_undefined + 2, + 1, + {DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER}, + }, + { + "DW_CFA_same_value", // 0x08 DW_CFA_same_value + 2, + 1, + {DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER}, + }, + { + "DW_CFA_register", // 0x09 DW_CFA_register + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_REGISTER}, + }, + { + "DW_CFA_remember_state", // 0x0a DW_CFA_remember_state + 2, + 0, + {}, + {}, + }, + { + "DW_CFA_restore_state", // 0x0b DW_CFA_restore_state + 2, + 0, + {}, + {}, + }, + { + "DW_CFA_def_cfa", // 0x0c DW_CFA_def_cfa + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_NUMBER}, + }, + { + "DW_CFA_def_cfa_register", // 0x0d DW_CFA_def_cfa_register + 2, + 1, + {DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER}, + }, + { + "DW_CFA_def_cfa_offset", // 0x0e DW_CFA_def_cfa_offset + 2, + 1, + {DW_EH_PE_uleb128}, + {DWARF_DISPLAY_NUMBER}, + }, + { + "DW_CFA_def_cfa_expression", // 0x0f DW_CFA_def_cfa_expression + 2, + 1, + {DW_EH_PE_block}, + {DWARF_DISPLAY_EVAL_BLOCK}, + }, + { + "DW_CFA_expression", // 0x10 DW_CFA_expression + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_block}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_EVAL_BLOCK}, + }, + { + "DW_CFA_offset_extended_sf", // 0x11 DW_CFA_offset_extend_sf + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_sleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_SIGNED_NUMBER}, + }, + { + "DW_CFA_def_cfa_sf", // 0x12 DW_CFA_def_cfa_sf + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_sleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_SIGNED_NUMBER}, + }, + { + "DW_CFA_def_cfa_offset_sf", // 0x13 DW_CFA_def_cfa_offset_sf + 2, + 1, + {DW_EH_PE_sleb128}, + {DWARF_DISPLAY_SIGNED_NUMBER}, + }, + { + "DW_CFA_val_offset", // 0x14 DW_CFA_val_offset + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_NUMBER}, + }, + { + "DW_CFA_val_offset_sf", // 0x15 DW_CFA_val_offset_sf + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_sleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_SIGNED_NUMBER}, + }, + { + "DW_CFA_val_expression", // 0x16 DW_CFA_val_expression + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_block}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_EVAL_BLOCK}, + }, + {"", 0, 0, {}, {}}, // 0x17 illegal cfa + {"", 0, 0, {}, {}}, // 0x18 illegal cfa + {"", 0, 0, {}, {}}, // 0x19 illegal cfa + {"", 0, 0, {}, {}}, // 0x1a illegal cfa + {"", 0, 0, {}, {}}, // 0x1b illegal cfa + {"", 0, 0, {}, {}}, // 0x1c DW_CFA_lo_user (Treat as illegal) + {"", 0, 0, {}, {}}, // 0x1d illegal cfa + {"", 0, 0, {}, {}}, // 0x1e illegal cfa + {"", 0, 0, {}, {}}, // 0x1f illegal cfa + {"", 0, 0, {}, {}}, // 0x20 illegal cfa + {"", 0, 0, {}, {}}, // 0x21 illegal cfa + {"", 0, 0, {}, {}}, // 0x22 illegal cfa + {"", 0, 0, {}, {}}, // 0x23 illegal cfa + {"", 0, 0, {}, {}}, // 0x24 illegal cfa + {"", 0, 0, {}, {}}, // 0x25 illegal cfa + {"", 0, 0, {}, {}}, // 0x26 illegal cfa + {"", 0, 0, {}, {}}, // 0x27 illegal cfa + {"", 0, 0, {}, {}}, // 0x28 illegal cfa + {"", 0, 0, {}, {}}, // 0x29 illegal cfa + {"", 0, 0, {}, {}}, // 0x2a illegal cfa + {"", 0, 0, {}, {}}, // 0x2b illegal cfa + {"", 0, 0, {}, {}}, // 0x2c illegal cfa + { + "DW_CFA_AARCH64_negate_ra_state", // 0x2d DW_CFA_AARCH64_negate_ra_state + 3, + 0, + {}, + {}, + }, + { + "DW_CFA_GNU_args_size", // 0x2e DW_CFA_GNU_args_size + 2, + 1, + {DW_EH_PE_uleb128}, + {DWARF_DISPLAY_NUMBER}, + }, + { + "DW_CFA_GNU_negative_offset_extended", // 0x2f DW_CFA_GNU_negative_offset_extended + 2, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_uleb128}, + {DWARF_DISPLAY_REGISTER, DWARF_DISPLAY_NUMBER}, + }, + {"", 0, 0, {}, {}}, // 0x31 illegal cfa + {"", 0, 0, {}, {}}, // 0x32 illegal cfa + {"", 0, 0, {}, {}}, // 0x33 illegal cfa + {"", 0, 0, {}, {}}, // 0x34 illegal cfa + {"", 0, 0, {}, {}}, // 0x35 illegal cfa + {"", 0, 0, {}, {}}, // 0x36 illegal cfa + {"", 0, 0, {}, {}}, // 0x37 illegal cfa + {"", 0, 0, {}, {}}, // 0x38 illegal cfa + {"", 0, 0, {}, {}}, // 0x39 illegal cfa + {"", 0, 0, {}, {}}, // 0x3a illegal cfa + {"", 0, 0, {}, {}}, // 0x3b illegal cfa + {"", 0, 0, {}, {}}, // 0x3c illegal cfa + {"", 0, 0, {}, {}}, // 0x3d illegal cfa + {"", 0, 0, {}, {}}, // 0x3e illegal cfa + {"", 0, 0, {}, {}}, // 0x3f DW_CFA_hi_user (Treat as illegal) +}; + +// Explicitly instantiate DwarfCfa. +template class DwarfCfa; +template class DwarfCfa; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.h new file mode 100644 index 0000000000..e9656e5cc3 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfCfa.h @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace unwindstack { + +// Forward declarations. +enum ArchEnum : uint8_t; + +// DWARF Standard home: http://dwarfstd.org/ +// This code is based on DWARF 4: http://http://dwarfstd.org/doc/DWARF4.pdf +// See section 6.4.2.1 for a description of the DW_CFA_xxx values. + +class DwarfCfaInfo { + public: + enum DisplayType : uint8_t { + DWARF_DISPLAY_NONE = 0, + DWARF_DISPLAY_REGISTER, + DWARF_DISPLAY_NUMBER, + DWARF_DISPLAY_SIGNED_NUMBER, + DWARF_DISPLAY_EVAL_BLOCK, + DWARF_DISPLAY_ADDRESS, + DWARF_DISPLAY_SET_LOC, + DWARF_DISPLAY_ADVANCE_LOC, + }; + + struct Info { + // It may seem cleaner to just change the type of 'name' to 'const char *'. + // However, having a pointer here would require relocation at runtime, + // causing 'kTable' to be placed in data.rel.ro section instead of rodata + // section, adding memory pressure to the system. Note that this is only + // safe because this is only used in C++ code. C++ standard, unlike C + // standard, mandates the array size to be large enough to hold the NULL + // terminator when initialized with a string literal. + const char name[36]; + uint8_t supported_version; + uint8_t num_operands; + uint8_t operands[2]; + uint8_t display_operands[2]; + }; + + const static Info kTable[64]; +}; + +template +class DwarfCfa { + // Signed version of AddressType + typedef typename std::make_signed::type SignedType; + + public: + DwarfCfa(DwarfMemory* memory, const DwarfFde* fde, ArchEnum arch) + : memory_(memory), fde_(fde), arch_(arch) {} + virtual ~DwarfCfa() = default; + + bool GetLocationInfo(uint64_t pc, uint64_t start_offset, uint64_t end_offset, + DwarfLocations* loc_regs); + + bool Log(uint32_t indent, uint64_t pc, uint64_t start_offset, uint64_t end_offset); + + const DwarfErrorData& last_error() { return last_error_; } + DwarfErrorCode LastErrorCode() { return last_error_.code; } + uint64_t LastErrorAddress() { return last_error_.address; } + + AddressType cur_pc() { return cur_pc_; } + + void set_cie_loc_regs(const DwarfLocations* cie_loc_regs) { cie_loc_regs_ = cie_loc_regs; } + + protected: + std::string GetOperandString(uint8_t operand, uint64_t value, uint64_t* cur_pc); + + bool LogOffsetRegisterString(uint32_t indent, uint64_t cfa_offset, uint8_t reg); + + bool LogInstruction(uint32_t indent, uint64_t cfa_offset, uint8_t op, uint64_t* cur_pc); + + private: + DwarfErrorData last_error_; + DwarfMemory* memory_; + const DwarfFde* fde_; + ArchEnum arch_; + + AddressType cur_pc_; + const DwarfLocations* cie_loc_regs_ = nullptr; + std::vector operands_; + std::stack loc_reg_state_; + + // CFA processing functions. + bool cfa_nop(DwarfLocations*); + bool cfa_set_loc(DwarfLocations*); + bool cfa_advance_loc(DwarfLocations*); + bool cfa_offset(DwarfLocations*); + bool cfa_restore(DwarfLocations*); + bool cfa_undefined(DwarfLocations*); + bool cfa_same_value(DwarfLocations*); + bool cfa_register(DwarfLocations*); + bool cfa_remember_state(DwarfLocations*); + bool cfa_restore_state(DwarfLocations*); + bool cfa_def_cfa(DwarfLocations*); + bool cfa_def_cfa_register(DwarfLocations*); + bool cfa_def_cfa_offset(DwarfLocations*); + bool cfa_def_cfa_expression(DwarfLocations*); + bool cfa_expression(DwarfLocations*); + bool cfa_offset_extended_sf(DwarfLocations*); + bool cfa_def_cfa_sf(DwarfLocations*); + bool cfa_def_cfa_offset_sf(DwarfLocations*); + bool cfa_val_offset(DwarfLocations*); + bool cfa_val_offset_sf(DwarfLocations*); + bool cfa_val_expression(DwarfLocations*); + bool cfa_gnu_negative_offset_extended(DwarfLocations*); + bool cfa_aarch64_negate_ra_state(DwarfLocations*); + + using process_func = bool (DwarfCfa::*)(DwarfLocations*); + constexpr static process_func kCallbackTable[64] = { + // 0x00 DW_CFA_nop + &DwarfCfa::cfa_nop, + // 0x01 DW_CFA_set_loc + &DwarfCfa::cfa_set_loc, + // 0x02 DW_CFA_advance_loc1 + &DwarfCfa::cfa_advance_loc, + // 0x03 DW_CFA_advance_loc2 + &DwarfCfa::cfa_advance_loc, + // 0x04 DW_CFA_advance_loc4 + &DwarfCfa::cfa_advance_loc, + // 0x05 DW_CFA_offset_extended + &DwarfCfa::cfa_offset, + // 0x06 DW_CFA_restore_extended + &DwarfCfa::cfa_restore, + // 0x07 DW_CFA_undefined + &DwarfCfa::cfa_undefined, + // 0x08 DW_CFA_same_value + &DwarfCfa::cfa_same_value, + // 0x09 DW_CFA_register + &DwarfCfa::cfa_register, + // 0x0a DW_CFA_remember_state + &DwarfCfa::cfa_remember_state, + // 0x0b DW_CFA_restore_state + &DwarfCfa::cfa_restore_state, + // 0x0c DW_CFA_def_cfa + &DwarfCfa::cfa_def_cfa, + // 0x0d DW_CFA_def_cfa_register + &DwarfCfa::cfa_def_cfa_register, + // 0x0e DW_CFA_def_cfa_offset + &DwarfCfa::cfa_def_cfa_offset, + // 0x0f DW_CFA_def_cfa_expression + &DwarfCfa::cfa_def_cfa_expression, + // 0x10 DW_CFA_expression + &DwarfCfa::cfa_expression, + // 0x11 DW_CFA_offset_extended_sf + &DwarfCfa::cfa_offset_extended_sf, + // 0x12 DW_CFA_def_cfa_sf + &DwarfCfa::cfa_def_cfa_sf, + // 0x13 DW_CFA_def_cfa_offset_sf + &DwarfCfa::cfa_def_cfa_offset_sf, + // 0x14 DW_CFA_val_offset + &DwarfCfa::cfa_val_offset, + // 0x15 DW_CFA_val_offset_sf + &DwarfCfa::cfa_val_offset_sf, + // 0x16 DW_CFA_val_expression + &DwarfCfa::cfa_val_expression, + // 0x17 illegal cfa + nullptr, + // 0x18 illegal cfa + nullptr, + // 0x19 illegal cfa + nullptr, + // 0x1a illegal cfa + nullptr, + // 0x1b illegal cfa + nullptr, + // 0x1c DW_CFA_lo_user (Treat this as illegal) + nullptr, + // 0x1d illegal cfa + nullptr, + // 0x1e illegal cfa + nullptr, + // 0x1f illegal cfa + nullptr, + // 0x20 illegal cfa + nullptr, + // 0x21 illegal cfa + nullptr, + // 0x22 illegal cfa + nullptr, + // 0x23 illegal cfa + nullptr, + // 0x24 illegal cfa + nullptr, + // 0x25 illegal cfa + nullptr, + // 0x26 illegal cfa + nullptr, + // 0x27 illegal cfa + nullptr, + // 0x28 illegal cfa + nullptr, + // 0x29 illegal cfa + nullptr, + // 0x2a illegal cfa + nullptr, + // 0x2b illegal cfa + nullptr, + // 0x2c illegal cfa + nullptr, + // 0x2d DW_CFA_AARCH64_negate_ra_state (aarch64 only) + // DW_CFA_GNU_window_save on other architectures. + &DwarfCfa::cfa_aarch64_negate_ra_state, + // 0x2e DW_CFA_GNU_args_size + &DwarfCfa::cfa_nop, + // 0x2f DW_CFA_GNU_negative_offset_extended + &DwarfCfa::cfa_gnu_negative_offset_extended, + // 0x30 illegal cfa + nullptr, + // 0x31 illegal cfa + nullptr, + // 0x32 illegal cfa + nullptr, + // 0x33 illegal cfa + nullptr, + // 0x34 illegal cfa + nullptr, + // 0x35 illegal cfa + nullptr, + // 0x36 illegal cfa + nullptr, + // 0x37 illegal cfa + nullptr, + // 0x38 illegal cfa + nullptr, + // 0x39 illegal cfa + nullptr, + // 0x3a illegal cfa + nullptr, + // 0x3b illegal cfa + nullptr, + // 0x3c illegal cfa + nullptr, + // 0x3d illegal cfa + nullptr, + // 0x3e illegal cfa + nullptr, + // 0x3f DW_CFA_hi_user (Treat this as illegal) + nullptr, + }; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfDebugFrame.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfDebugFrame.h new file mode 100644 index 0000000000..017568d15f --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfDebugFrame.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#include + +namespace unwindstack { + +template +class DwarfDebugFrame : public DwarfSectionImpl { + public: + DwarfDebugFrame(Memory* memory) : DwarfSectionImpl(memory) { + this->cie32_value_ = static_cast(-1); + this->cie64_value_ = static_cast(-1); + } + virtual ~DwarfDebugFrame() = default; + + uint64_t GetCieOffsetFromFde32(uint32_t pointer) override { + return this->entries_offset_ + pointer; + } + + uint64_t GetCieOffsetFromFde64(uint64_t pointer) override { + return this->entries_offset_ + pointer; + } + + uint64_t AdjustPcFromFde(uint64_t pc) override { return pc; } +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrame.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrame.h new file mode 100644 index 0000000000..93befd44e0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrame.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include + +namespace unwindstack { + +template +class DwarfEhFrame : public DwarfSectionImpl { + public: + DwarfEhFrame(Memory* memory) : DwarfSectionImpl(memory) {} + virtual ~DwarfEhFrame() = default; + + uint64_t GetCieOffsetFromFde32(uint32_t pointer) override { + return this->memory_.cur_offset() - pointer - 4; + } + + uint64_t GetCieOffsetFromFde64(uint64_t pointer) override { + return this->memory_.cur_offset() - pointer - 8; + } + + uint64_t AdjustPcFromFde(uint64_t pc) override { + // The eh_frame uses relative pcs. + return pc + this->memory_.cur_offset() - 4; + } +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.cpp new file mode 100644 index 0000000000..1358e51dea --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include "Check.h" +#include "DwarfEhFrameWithHdr.h" +#include "DwarfEncoding.h" + +namespace unwindstack { + +static inline bool IsEncodingRelative(uint8_t encoding) { + encoding >>= 4; + return encoding > 0 && encoding <= DW_EH_PE_funcrel; +} + +template +bool DwarfEhFrameWithHdr::EhFrameInit(uint64_t offset, uint64_t size, + int64_t section_bias) { + return DwarfSectionImpl::Init(offset, size, section_bias); +} + +template +bool DwarfEhFrameWithHdr::Init(uint64_t offset, uint64_t, int64_t section_bias) { + memory_.clear_func_offset(); + memory_.clear_text_offset(); + memory_.set_data_offset(offset); + memory_.set_cur_offset(offset); + + hdr_section_bias_ = section_bias; + + // Read the first four bytes all at once. + uint8_t data[4]; + if (!memory_.ReadBytes(data, 4)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + version_ = data[0]; + if (version_ != 1) { + // Unknown version. + last_error_.code = DWARF_ERROR_UNSUPPORTED_VERSION; + return false; + } + + uint8_t ptr_encoding = data[1]; + uint8_t fde_count_encoding = data[2]; + table_encoding_ = data[3]; + table_entry_size_ = memory_.template GetEncodedSize(table_encoding_); + + // If we can't perform a binary search on the entries, it's not worth + // using this object. The calling code will fall back to the DwarfEhFrame + // object in this case. + if (table_entry_size_ == 0) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + memory_.set_pc_offset(memory_.cur_offset()); + uint64_t ptr_offset; + if (!memory_.template ReadEncodedValue(ptr_encoding, &ptr_offset)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + memory_.set_pc_offset(memory_.cur_offset()); + if (!memory_.template ReadEncodedValue(fde_count_encoding, &fde_count_)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + if (fde_count_ == 0) { + last_error_.code = DWARF_ERROR_NO_FDES; + return false; + } + + hdr_entries_offset_ = memory_.cur_offset(); + hdr_entries_data_offset_ = offset; + + return true; +} + +template +const DwarfFde* DwarfEhFrameWithHdr::GetFdeFromPc(uint64_t pc) { + uint64_t fde_offset; + if (!GetFdeOffsetFromPc(pc, &fde_offset)) { + return nullptr; + } + const DwarfFde* fde = this->GetFdeFromOffset(fde_offset); + if (fde == nullptr) { + return nullptr; + } + + // There is a possibility that this entry points to a zero length FDE + // due to a bug. If this happens, try and find the non-zero length FDE + // from eh_frame directly. See b/142483624. + if (fde->pc_start == fde->pc_end) { + fde = DwarfSectionImpl::GetFdeFromPc(pc); + if (fde == nullptr) { + return nullptr; + } + } + + // Guaranteed pc >= pc_start, need to check pc in the fde range. + if (pc < fde->pc_end) { + return fde; + } + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return nullptr; +} + +template +const typename DwarfEhFrameWithHdr::FdeInfo* +DwarfEhFrameWithHdr::GetFdeInfoFromIndex(size_t index) { + auto entry = fde_info_.find(index); + if (entry != fde_info_.end()) { + return &fde_info_[index]; + } + FdeInfo* info = &fde_info_[index]; + + memory_.set_data_offset(hdr_entries_data_offset_); + memory_.set_cur_offset(hdr_entries_offset_ + 2 * index * table_entry_size_); + memory_.set_pc_offset(0); + uint64_t value; + if (!memory_.template ReadEncodedValue(table_encoding_, &value) || + !memory_.template ReadEncodedValue(table_encoding_, &info->offset)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + fde_info_.erase(index); + return nullptr; + } + + // Relative encodings require adding in the load bias. + if (IsEncodingRelative(table_encoding_)) { + value += hdr_section_bias_; + } + info->pc = value; + return info; +} + +template +bool DwarfEhFrameWithHdr::GetFdeOffsetFromPc(uint64_t pc, uint64_t* fde_offset) { + if (fde_count_ == 0) { + return false; + } + + size_t first = 0; + size_t last = fde_count_; + while (first < last) { + size_t current = (first + last) / 2; + const FdeInfo* info = GetFdeInfoFromIndex(current); + if (info == nullptr) { + return false; + } + if (pc == info->pc) { + *fde_offset = info->offset; + return true; + } + if (pc < info->pc) { + last = current; + } else { + first = current + 1; + } + } + if (last != 0) { + const FdeInfo* info = GetFdeInfoFromIndex(last - 1); + if (info == nullptr) { + return false; + } + *fde_offset = info->offset; + return true; + } + return false; +} + +template +void DwarfEhFrameWithHdr::GetFdes(std::vector* fdes) { + for (size_t i = 0; i < fde_count_; i++) { + const FdeInfo* info = GetFdeInfoFromIndex(i); + if (info == nullptr) { + break; + } + const DwarfFde* fde = this->GetFdeFromOffset(info->offset); + if (fde == nullptr) { + break; + } + + // There is a possibility that this entry points to a zero length FDE + // due to a bug. If this happens, try and find the non-zero length FDE + // from eh_frame directly. See b/142483624. + if (fde->pc_start == fde->pc_end) { + const DwarfFde* fde_real = DwarfSectionImpl::GetFdeFromPc(fde->pc_start); + if (fde_real != nullptr) { + fde = fde_real; + } + } + fdes->push_back(fde); + } +} + +// Explicitly instantiate DwarfEhFrameWithHdr +template class DwarfEhFrameWithHdr; +template class DwarfEhFrameWithHdr; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.h new file mode 100644 index 0000000000..d074b7bec2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEhFrameWithHdr.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; + +template +class DwarfEhFrameWithHdr : public DwarfSectionImpl { + public: + // Add these so that the protected members of DwarfSectionImpl + // can be accessed without needing a this->. + using DwarfSectionImpl::memory_; + using DwarfSectionImpl::last_error_; + + struct FdeInfo { + AddressType pc; + uint64_t offset; + }; + + DwarfEhFrameWithHdr(Memory* memory) : DwarfSectionImpl(memory) {} + virtual ~DwarfEhFrameWithHdr() = default; + + uint64_t GetCieOffsetFromFde32(uint32_t pointer) override { + return memory_.cur_offset() - pointer - 4; + } + + uint64_t GetCieOffsetFromFde64(uint64_t pointer) override { + return memory_.cur_offset() - pointer - 8; + } + + uint64_t AdjustPcFromFde(uint64_t pc) override { + // The eh_frame uses relative pcs. + return pc + memory_.cur_offset() - 4; + } + + bool EhFrameInit(uint64_t offset, uint64_t size, int64_t section_bias); + bool Init(uint64_t offset, uint64_t size, int64_t section_bias) override; + + const DwarfFde* GetFdeFromPc(uint64_t pc) override; + + bool GetFdeOffsetFromPc(uint64_t pc, uint64_t* fde_offset); + + const FdeInfo* GetFdeInfoFromIndex(size_t index); + + void GetFdes(std::vector* fdes) override; + + protected: + uint8_t version_ = 0; + uint8_t table_encoding_ = 0; + size_t table_entry_size_ = 0; + + uint64_t hdr_entries_offset_ = 0; + uint64_t hdr_entries_data_offset_ = 0; + uint64_t hdr_section_bias_ = 0; + + uint64_t fde_count_ = 0; + std::unordered_map fde_info_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEncoding.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEncoding.h new file mode 100644 index 0000000000..c9fbf2499d --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfEncoding.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +enum DwarfEncoding : uint8_t { + DW_EH_PE_omit = 0xff, + + DW_EH_PE_absptr = 0x00, + DW_EH_PE_uleb128 = 0x01, + DW_EH_PE_udata2 = 0x02, + DW_EH_PE_udata4 = 0x03, + DW_EH_PE_udata8 = 0x04, + DW_EH_PE_sleb128 = 0x09, + DW_EH_PE_sdata2 = 0x0a, + DW_EH_PE_sdata4 = 0x0b, + DW_EH_PE_sdata8 = 0x0c, + + DW_EH_PE_pcrel = 0x10, + DW_EH_PE_textrel = 0x20, + DW_EH_PE_datarel = 0x30, + DW_EH_PE_funcrel = 0x40, + DW_EH_PE_aligned = 0x50, + + // The following are special values used to encode CFA and OP operands. + DW_EH_PE_udata1 = 0x0d, + DW_EH_PE_sdata1 = 0x0e, + DW_EH_PE_block = 0x0f, +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfMemory.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfMemory.cpp new file mode 100644 index 0000000000..2e388c6893 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfMemory.cpp @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include + +#include "Check.h" +#include "DwarfEncoding.h" + +namespace unwindstack { + +bool DwarfMemory::ReadBytes(void* dst, size_t num_bytes) { + if (!memory_->ReadFully(cur_offset_, dst, num_bytes)) { + return false; + } + cur_offset_ += num_bytes; + return true; +} + +template +bool DwarfMemory::ReadSigned(uint64_t* value) { + SignedType signed_value; + if (!ReadBytes(&signed_value, sizeof(SignedType))) { + return false; + } + *value = static_cast(signed_value); + return true; +} + +bool DwarfMemory::ReadULEB128(uint64_t* value) { + uint64_t cur_value = 0; + uint64_t shift = 0; + uint8_t byte; + do { + if (!ReadBytes(&byte, 1)) { + return false; + } + cur_value += static_cast(byte & 0x7f) << shift; + shift += 7; + } while (byte & 0x80); + *value = cur_value; + return true; +} + +bool DwarfMemory::ReadSLEB128(int64_t* value) { + uint64_t cur_value = 0; + uint64_t shift = 0; + uint8_t byte; + do { + if (!ReadBytes(&byte, 1)) { + return false; + } + cur_value += static_cast(byte & 0x7f) << shift; + shift += 7; + } while (byte & 0x80); + if (byte & 0x40) { + // Negative value, need to sign extend. + cur_value |= static_cast(-1) << shift; + } + *value = static_cast(cur_value); + return true; +} + +template +size_t DwarfMemory::GetEncodedSize(uint8_t encoding) { + switch (encoding & 0x0f) { + case DW_EH_PE_absptr: + return sizeof(AddressType); + case DW_EH_PE_udata1: + case DW_EH_PE_sdata1: + return 1; + case DW_EH_PE_udata2: + case DW_EH_PE_sdata2: + return 2; + case DW_EH_PE_udata4: + case DW_EH_PE_sdata4: + return 4; + case DW_EH_PE_udata8: + case DW_EH_PE_sdata8: + return 8; + case DW_EH_PE_uleb128: + case DW_EH_PE_sleb128: + default: + return 0; + } +} + +bool DwarfMemory::AdjustEncodedValue(uint8_t encoding, uint64_t* value) { + CHECK((encoding & 0x0f) == 0); + + // Handle the encoding. + switch (encoding) { + case DW_EH_PE_absptr: + // Nothing to do. + break; + case DW_EH_PE_pcrel: + if (pc_offset_ == INT64_MAX) { + // Unsupported encoding. + return false; + } + *value += pc_offset_; + break; + case DW_EH_PE_textrel: + if (text_offset_ == static_cast(-1)) { + // Unsupported encoding. + return false; + } + *value += text_offset_; + break; + case DW_EH_PE_datarel: + if (data_offset_ == static_cast(-1)) { + // Unsupported encoding. + return false; + } + *value += data_offset_; + break; + case DW_EH_PE_funcrel: + if (func_offset_ == static_cast(-1)) { + // Unsupported encoding. + return false; + } + *value += func_offset_; + break; + default: + return false; + } + + return true; +} + +template +bool DwarfMemory::ReadEncodedValue(uint8_t encoding, uint64_t* value) { + if (encoding == DW_EH_PE_omit) { + *value = 0; + return true; + } else if (encoding == DW_EH_PE_aligned) { + if (__builtin_add_overflow(cur_offset_, sizeof(AddressType) - 1, &cur_offset_)) { + return false; + } + cur_offset_ &= -sizeof(AddressType); + + if (sizeof(AddressType) != sizeof(uint64_t)) { + *value = 0; + } + return ReadBytes(value, sizeof(AddressType)); + } + + // Get the data. + switch (encoding & 0x0f) { + case DW_EH_PE_absptr: + if (sizeof(AddressType) != sizeof(uint64_t)) { + *value = 0; + } + if (!ReadBytes(value, sizeof(AddressType))) { + return false; + } + break; + case DW_EH_PE_uleb128: + if (!ReadULEB128(value)) { + return false; + } + break; + case DW_EH_PE_sleb128: + int64_t signed_value; + if (!ReadSLEB128(&signed_value)) { + return false; + } + *value = static_cast(signed_value); + break; + case DW_EH_PE_udata1: { + uint8_t value8; + if (!ReadBytes(&value8, 1)) { + return false; + } + *value = value8; + } break; + case DW_EH_PE_sdata1: + if (!ReadSigned(value)) { + return false; + } + break; + case DW_EH_PE_udata2: { + uint16_t value16; + if (!ReadBytes(&value16, 2)) { + return false; + } + *value = value16; + } break; + case DW_EH_PE_sdata2: + if (!ReadSigned(value)) { + return false; + } + break; + case DW_EH_PE_udata4: { + uint32_t value32; + if (!ReadBytes(&value32, 4)) { + return false; + } + *value = value32; + } break; + case DW_EH_PE_sdata4: + if (!ReadSigned(value)) { + return false; + } + break; + case DW_EH_PE_udata8: + if (!ReadBytes(value, sizeof(uint64_t))) { + return false; + } + break; + case DW_EH_PE_sdata8: + if (!ReadSigned(value)) { + return false; + } + break; + default: + return false; + } + + return AdjustEncodedValue(encoding & 0x70, value); +} + +// Instantiate all of the needed template functions. +template bool DwarfMemory::ReadSigned(uint64_t*); +template bool DwarfMemory::ReadSigned(uint64_t*); +template bool DwarfMemory::ReadSigned(uint64_t*); +template bool DwarfMemory::ReadSigned(uint64_t*); + +template size_t DwarfMemory::GetEncodedSize(uint8_t); +template size_t DwarfMemory::GetEncodedSize(uint8_t); + +template bool DwarfMemory::ReadEncodedValue(uint8_t, uint64_t*); +template bool DwarfMemory::ReadEncodedValue(uint8_t, uint64_t*); + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.cpp new file mode 100644 index 0000000000..a5ce03f7c8 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.cpp @@ -0,0 +1,1940 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "DwarfOp.h" + +namespace unwindstack { + +enum DwarfOpHandleFunc : uint8_t { + OP_ILLEGAL = 0, + OP_DEREF, + OP_DEREF_SIZE, + OP_PUSH, + OP_DUP, + OP_DROP, + OP_OVER, + OP_PICK, + OP_SWAP, + OP_ROT, + OP_ABS, + OP_AND, + OP_DIV, + OP_MINUS, + OP_MOD, + OP_MUL, + OP_NEG, + OP_NOT, + OP_OR, + OP_PLUS, + OP_PLUS_UCONST, + OP_SHL, + OP_SHR, + OP_SHRA, + OP_XOR, + OP_BRA, + OP_EQ, + OP_GE, + OP_GT, + OP_LE, + OP_LT, + OP_NE, + OP_SKIP, + OP_LIT, + OP_REG, + OP_REGX, + OP_BREG, + OP_BREGX, + OP_NOP, + OP_NOT_IMPLEMENTED, +}; + +struct OpCallback { + // It may seem tempting to "clean this up" by replacing "const char[26]" with + // "const char*", but doing so would place the entire callback table in + // .data.rel.ro section, instead of .rodata section, and thus increase + // dirty memory usage. Libunwindstack is used by the linker and therefore + // loaded for every running process, so every bit of memory counts. + // Unlike C standard, C++ standard guarantees this array is big enough to + // store the names, or else we would get a compilation error. + const char name[26]; + + // Similarily for this field, we do NOT want to directly store function + // pointers here. Not only would that cause the callback table to be placed + // in .data.rel.ro section, but it would be duplicated for each AddressType. + // Instead, we use DwarfOpHandleFunc enum to decouple the callback table from + // the function pointers. + DwarfOpHandleFunc handle_func; + + uint8_t num_required_stack_values; + uint8_t num_operands; + uint8_t operands[2]; +}; + +constexpr static OpCallback kCallbackTable[256] = { + {"", OP_ILLEGAL, 0, 0, {}}, // 0x00 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0x01 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0x02 illegal op + { + // 0x03 DW_OP_addr + "DW_OP_addr", + OP_PUSH, + 0, + 1, + {DW_EH_PE_absptr}, + }, + {"", OP_ILLEGAL, 0, 0, {}}, // 0x04 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0x05 illegal op + { + // 0x06 DW_OP_deref + "DW_OP_deref", + OP_DEREF, + 1, + 0, + {}, + }, + {"", OP_ILLEGAL, 0, 0, {}}, // 0x07 illegal op + { + // 0x08 DW_OP_const1u + "DW_OP_const1u", + OP_PUSH, + 0, + 1, + {DW_EH_PE_udata1}, + }, + { + // 0x09 DW_OP_const1s + "DW_OP_const1s", + OP_PUSH, + 0, + 1, + {DW_EH_PE_sdata1}, + }, + { + // 0x0a DW_OP_const2u + "DW_OP_const2u", + OP_PUSH, + 0, + 1, + {DW_EH_PE_udata2}, + }, + { + // 0x0b DW_OP_const2s + "DW_OP_const2s", + OP_PUSH, + 0, + 1, + {DW_EH_PE_sdata2}, + }, + { + // 0x0c DW_OP_const4u + "DW_OP_const4u", + OP_PUSH, + 0, + 1, + {DW_EH_PE_udata4}, + }, + { + // 0x0d DW_OP_const4s + "DW_OP_const4s", + OP_PUSH, + 0, + 1, + {DW_EH_PE_sdata4}, + }, + { + // 0x0e DW_OP_const8u + "DW_OP_const8u", + OP_PUSH, + 0, + 1, + {DW_EH_PE_udata8}, + }, + { + // 0x0f DW_OP_const8s + "DW_OP_const8s", + OP_PUSH, + 0, + 1, + {DW_EH_PE_sdata8}, + }, + { + // 0x10 DW_OP_constu + "DW_OP_constu", + OP_PUSH, + 0, + 1, + {DW_EH_PE_uleb128}, + }, + { + // 0x11 DW_OP_consts + "DW_OP_consts", + OP_PUSH, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x12 DW_OP_dup + "DW_OP_dup", + OP_DUP, + 1, + 0, + {}, + }, + { + // 0x13 DW_OP_drop + "DW_OP_drop", + OP_DROP, + 1, + 0, + {}, + }, + { + // 0x14 DW_OP_over + "DW_OP_over", + OP_OVER, + 2, + 0, + {}, + }, + { + // 0x15 DW_OP_pick + "DW_OP_pick", + OP_PICK, + 0, + 1, + {DW_EH_PE_udata1}, + }, + { + // 0x16 DW_OP_swap + "DW_OP_swap", + OP_SWAP, + 2, + 0, + {}, + }, + { + // 0x17 DW_OP_rot + "DW_OP_rot", + OP_ROT, + 3, + 0, + {}, + }, + { + // 0x18 DW_OP_xderef + "DW_OP_xderef", + OP_NOT_IMPLEMENTED, + 2, + 0, + {}, + }, + { + // 0x19 DW_OP_abs + "DW_OP_abs", + OP_ABS, + 1, + 0, + {}, + }, + { + // 0x1a DW_OP_and + "DW_OP_and", + OP_AND, + 2, + 0, + {}, + }, + { + // 0x1b DW_OP_div + "DW_OP_div", + OP_DIV, + 2, + 0, + {}, + }, + { + // 0x1c DW_OP_minus + "DW_OP_minus", + OP_MINUS, + 2, + 0, + {}, + }, + { + // 0x1d DW_OP_mod + "DW_OP_mod", + OP_MOD, + 2, + 0, + {}, + }, + { + // 0x1e DW_OP_mul + "DW_OP_mul", + OP_MUL, + 2, + 0, + {}, + }, + { + // 0x1f DW_OP_neg + "DW_OP_neg", + OP_NEG, + 1, + 0, + {}, + }, + { + // 0x20 DW_OP_not + "DW_OP_not", + OP_NOT, + 1, + 0, + {}, + }, + { + // 0x21 DW_OP_or + "DW_OP_or", + OP_OR, + 2, + 0, + {}, + }, + { + // 0x22 DW_OP_plus + "DW_OP_plus", + OP_PLUS, + 2, + 0, + {}, + }, + { + // 0x23 DW_OP_plus_uconst + "DW_OP_plus_uconst", + OP_PLUS_UCONST, + 1, + 1, + {DW_EH_PE_uleb128}, + }, + { + // 0x24 DW_OP_shl + "DW_OP_shl", + OP_SHL, + 2, + 0, + {}, + }, + { + // 0x25 DW_OP_shr + "DW_OP_shr", + OP_SHR, + 2, + 0, + {}, + }, + { + // 0x26 DW_OP_shra + "DW_OP_shra", + OP_SHRA, + 2, + 0, + {}, + }, + { + // 0x27 DW_OP_xor + "DW_OP_xor", + OP_XOR, + 2, + 0, + {}, + }, + { + // 0x28 DW_OP_bra + "DW_OP_bra", + OP_BRA, + 1, + 1, + {DW_EH_PE_sdata2}, + }, + { + // 0x29 DW_OP_eq + "DW_OP_eq", + OP_EQ, + 2, + 0, + {}, + }, + { + // 0x2a DW_OP_ge + "DW_OP_ge", + OP_GE, + 2, + 0, + {}, + }, + { + // 0x2b DW_OP_gt + "DW_OP_gt", + OP_GT, + 2, + 0, + {}, + }, + { + // 0x2c DW_OP_le + "DW_OP_le", + OP_LE, + 2, + 0, + {}, + }, + { + // 0x2d DW_OP_lt + "DW_OP_lt", + OP_LT, + 2, + 0, + {}, + }, + { + // 0x2e DW_OP_ne + "DW_OP_ne", + OP_NE, + 2, + 0, + {}, + }, + { + // 0x2f DW_OP_skip + "DW_OP_skip", + OP_SKIP, + 0, + 1, + {DW_EH_PE_sdata2}, + }, + { + // 0x30 DW_OP_lit0 + "DW_OP_lit0", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x31 DW_OP_lit1 + "DW_OP_lit1", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x32 DW_OP_lit2 + "DW_OP_lit2", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x33 DW_OP_lit3 + "DW_OP_lit3", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x34 DW_OP_lit4 + "DW_OP_lit4", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x35 DW_OP_lit5 + "DW_OP_lit5", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x36 DW_OP_lit6 + "DW_OP_lit6", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x37 DW_OP_lit7 + "DW_OP_lit7", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x38 DW_OP_lit8 + "DW_OP_lit8", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x39 DW_OP_lit9 + "DW_OP_lit9", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x3a DW_OP_lit10 + "DW_OP_lit10", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x3b DW_OP_lit11 + "DW_OP_lit11", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x3c DW_OP_lit12 + "DW_OP_lit12", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x3d DW_OP_lit13 + "DW_OP_lit13", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x3e DW_OP_lit14 + "DW_OP_lit14", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x3f DW_OP_lit15 + "DW_OP_lit15", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x40 DW_OP_lit16 + "DW_OP_lit16", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x41 DW_OP_lit17 + "DW_OP_lit17", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x42 DW_OP_lit18 + "DW_OP_lit18", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x43 DW_OP_lit19 + "DW_OP_lit19", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x44 DW_OP_lit20 + "DW_OP_lit20", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x45 DW_OP_lit21 + "DW_OP_lit21", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x46 DW_OP_lit22 + "DW_OP_lit22", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x47 DW_OP_lit23 + "DW_OP_lit23", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x48 DW_OP_lit24 + "DW_OP_lit24", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x49 DW_OP_lit25 + "DW_OP_lit25", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x4a DW_OP_lit26 + "DW_OP_lit26", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x4b DW_OP_lit27 + "DW_OP_lit27", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x4c DW_OP_lit28 + "DW_OP_lit28", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x4d DW_OP_lit29 + "DW_OP_lit29", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x4e DW_OP_lit30 + "DW_OP_lit30", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x4f DW_OP_lit31 + "DW_OP_lit31", + OP_LIT, + 0, + 0, + {}, + }, + { + // 0x50 DW_OP_reg0 + "DW_OP_reg0", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x51 DW_OP_reg1 + "DW_OP_reg1", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x52 DW_OP_reg2 + "DW_OP_reg2", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x53 DW_OP_reg3 + "DW_OP_reg3", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x54 DW_OP_reg4 + "DW_OP_reg4", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x55 DW_OP_reg5 + "DW_OP_reg5", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x56 DW_OP_reg6 + "DW_OP_reg6", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x57 DW_OP_reg7 + "DW_OP_reg7", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x58 DW_OP_reg8 + "DW_OP_reg8", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x59 DW_OP_reg9 + "DW_OP_reg9", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x5a DW_OP_reg10 + "DW_OP_reg10", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x5b DW_OP_reg11 + "DW_OP_reg11", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x5c DW_OP_reg12 + "DW_OP_reg12", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x5d DW_OP_reg13 + "DW_OP_reg13", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x5e DW_OP_reg14 + "DW_OP_reg14", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x5f DW_OP_reg15 + "DW_OP_reg15", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x60 DW_OP_reg16 + "DW_OP_reg16", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x61 DW_OP_reg17 + "DW_OP_reg17", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x62 DW_OP_reg18 + "DW_OP_reg18", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x63 DW_OP_reg19 + "DW_OP_reg19", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x64 DW_OP_reg20 + "DW_OP_reg20", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x65 DW_OP_reg21 + "DW_OP_reg21", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x66 DW_OP_reg22 + "DW_OP_reg22", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x67 DW_OP_reg23 + "DW_OP_reg23", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x68 DW_OP_reg24 + "DW_OP_reg24", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x69 DW_OP_reg25 + "DW_OP_reg25", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x6a DW_OP_reg26 + "DW_OP_reg26", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x6b DW_OP_reg27 + "DW_OP_reg27", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x6c DW_OP_reg28 + "DW_OP_reg28", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x6d DW_OP_reg29 + "DW_OP_reg29", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x6e DW_OP_reg30 + "DW_OP_reg30", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x6f DW_OP_reg31 + "DW_OP_reg31", + OP_REG, + 0, + 0, + {}, + }, + { + // 0x70 DW_OP_breg0 + "DW_OP_breg0", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x71 DW_OP_breg1 + "DW_OP_breg1", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x72 DW_OP_breg2 + "DW_OP_breg2", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x73 DW_OP_breg3 + "DW_OP_breg3", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x74 DW_OP_breg4 + "DW_OP_breg4", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x75 DW_OP_breg5 + "DW_OP_breg5", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x76 DW_OP_breg6 + "DW_OP_breg6", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x77 DW_OP_breg7 + "DW_OP_breg7", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x78 DW_OP_breg8 + "DW_OP_breg8", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x79 DW_OP_breg9 + "DW_OP_breg9", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x7a DW_OP_breg10 + "DW_OP_breg10", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x7b DW_OP_breg11 + "DW_OP_breg11", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x7c DW_OP_breg12 + "DW_OP_breg12", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x7d DW_OP_breg13 + "DW_OP_breg13", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x7e DW_OP_breg14 + "DW_OP_breg14", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x7f DW_OP_breg15 + "DW_OP_breg15", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x80 DW_OP_breg16 + "DW_OP_breg16", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x81 DW_OP_breg17 + "DW_OP_breg17", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x82 DW_OP_breg18 + "DW_OP_breg18", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x83 DW_OP_breg19 + "DW_OP_breg19", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x84 DW_OP_breg20 + "DW_OP_breg20", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x85 DW_OP_breg21 + "DW_OP_breg21", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x86 DW_OP_breg22 + "DW_OP_breg22", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x87 DW_OP_breg23 + "DW_OP_breg23", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x88 DW_OP_breg24 + "DW_OP_breg24", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x89 DW_OP_breg25 + "DW_OP_breg25", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x8a DW_OP_breg26 + "DW_OP_breg26", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x8b DW_OP_breg27 + "DW_OP_breg27", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x8c DW_OP_breg28 + "DW_OP_breg28", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x8d DW_OP_breg29 + "DW_OP_breg29", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x8e DW_OP_breg30 + "DW_OP_breg30", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x8f DW_OP_breg31 + "DW_OP_breg31", + OP_BREG, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x90 DW_OP_regx + "DW_OP_regx", + OP_REGX, + 0, + 1, + {DW_EH_PE_uleb128}, + }, + { + // 0x91 DW_OP_fbreg + "DW_OP_fbreg", + OP_NOT_IMPLEMENTED, + 0, + 1, + {DW_EH_PE_sleb128}, + }, + { + // 0x92 DW_OP_bregx + "DW_OP_bregx", + OP_BREGX, + 0, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_sleb128}, + }, + { + // 0x93 DW_OP_piece + "DW_OP_piece", + OP_NOT_IMPLEMENTED, + 0, + 1, + {DW_EH_PE_uleb128}, + }, + { + // 0x94 DW_OP_deref_size + "DW_OP_deref_size", + OP_DEREF_SIZE, + 1, + 1, + {DW_EH_PE_udata1}, + }, + { + // 0x95 DW_OP_xderef_size + "DW_OP_xderef_size", + OP_NOT_IMPLEMENTED, + 0, + 1, + {DW_EH_PE_udata1}, + }, + { + // 0x96 DW_OP_nop + "DW_OP_nop", + OP_NOP, + 0, + 0, + {}, + }, + { + // 0x97 DW_OP_push_object_address + "DW_OP_push_object_address", + OP_NOT_IMPLEMENTED, + 0, + 0, + {}, + }, + { + // 0x98 DW_OP_call2 + "DW_OP_call2", + OP_NOT_IMPLEMENTED, + 0, + 1, + {DW_EH_PE_udata2}, + }, + { + // 0x99 DW_OP_call4 + "DW_OP_call4", + OP_NOT_IMPLEMENTED, + 0, + 1, + {DW_EH_PE_udata4}, + }, + { + // 0x9a DW_OP_call_ref + "DW_OP_call_ref", + OP_NOT_IMPLEMENTED, + 0, + 0, // Has a different sized operand (4 bytes or 8 bytes). + {}, + }, + { + // 0x9b DW_OP_form_tls_address + "DW_OP_form_tls_address", + OP_NOT_IMPLEMENTED, + 0, + 0, + {}, + }, + { + // 0x9c DW_OP_call_frame_cfa + "DW_OP_call_frame_cfa", + OP_NOT_IMPLEMENTED, + 0, + 0, + {}, + }, + { + // 0x9d DW_OP_bit_piece + "DW_OP_bit_piece", + OP_NOT_IMPLEMENTED, + 0, + 2, + {DW_EH_PE_uleb128, DW_EH_PE_uleb128}, + }, + { + // 0x9e DW_OP_implicit_value + "DW_OP_implicit_value", + OP_NOT_IMPLEMENTED, + 0, + 1, + {DW_EH_PE_uleb128}, + }, + { + // 0x9f DW_OP_stack_value + "DW_OP_stack_value", + OP_NOT_IMPLEMENTED, + 1, + 0, + {}, + }, + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa0 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa1 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa2 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa3 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa4 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa5 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa6 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa7 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa8 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xa9 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xaa illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xab illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xac illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xad illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xae illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xaf illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb0 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb1 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb2 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb3 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb4 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb5 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb6 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb7 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb8 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xb9 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xba illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xbb illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xbc illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xbd illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xbe illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xbf illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc0 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc1 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc2 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc3 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc4 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc5 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc6 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc7 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc8 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xc9 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xca illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xcb illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xcc illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xcd illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xce illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xcf illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd0 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd1 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd2 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd3 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd4 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd5 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd6 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd7 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd8 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xd9 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xda illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xdb illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xdc illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xdd illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xde illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xdf illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe0 DW_OP_lo_user + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe1 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe2 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe3 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe4 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe5 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe6 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe7 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe8 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xe9 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xea illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xeb illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xec illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xed illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xee illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xef illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf0 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf1 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf2 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf3 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf4 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf5 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf6 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf7 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf8 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xf9 illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xfa illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xfb illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xfc illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xfd illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xfe illegal op + {"", OP_ILLEGAL, 0, 0, {}}, // 0xff DW_OP_hi_user +}; + +template +const typename DwarfOp::OpHandleFuncPtr DwarfOp::kOpHandleFuncList[] = { + [OP_ILLEGAL] = nullptr, + [OP_DEREF] = &DwarfOp::op_deref, + [OP_DEREF_SIZE] = &DwarfOp::op_deref_size, + [OP_PUSH] = &DwarfOp::op_push, + [OP_DUP] = &DwarfOp::op_dup, + [OP_DROP] = &DwarfOp::op_drop, + [OP_OVER] = &DwarfOp::op_over, + [OP_PICK] = &DwarfOp::op_pick, + [OP_SWAP] = &DwarfOp::op_swap, + [OP_ROT] = &DwarfOp::op_rot, + [OP_ABS] = &DwarfOp::op_abs, + [OP_AND] = &DwarfOp::op_and, + [OP_DIV] = &DwarfOp::op_div, + [OP_MINUS] = &DwarfOp::op_minus, + [OP_MOD] = &DwarfOp::op_mod, + [OP_MUL] = &DwarfOp::op_mul, + [OP_NEG] = &DwarfOp::op_neg, + [OP_NOT] = &DwarfOp::op_not, + [OP_OR] = &DwarfOp::op_or, + [OP_PLUS] = &DwarfOp::op_plus, + [OP_PLUS_UCONST] = &DwarfOp::op_plus_uconst, + [OP_SHL] = &DwarfOp::op_shl, + [OP_SHR] = &DwarfOp::op_shr, + [OP_SHRA] = &DwarfOp::op_shra, + [OP_XOR] = &DwarfOp::op_xor, + [OP_BRA] = &DwarfOp::op_bra, + [OP_EQ] = &DwarfOp::op_eq, + [OP_GE] = &DwarfOp::op_ge, + [OP_GT] = &DwarfOp::op_gt, + [OP_LE] = &DwarfOp::op_le, + [OP_LT] = &DwarfOp::op_lt, + [OP_NE] = &DwarfOp::op_ne, + [OP_SKIP] = &DwarfOp::op_skip, + [OP_LIT] = &DwarfOp::op_lit, + [OP_REG] = &DwarfOp::op_reg, + [OP_REGX] = &DwarfOp::op_regx, + [OP_BREG] = &DwarfOp::op_breg, + [OP_BREGX] = &DwarfOp::op_bregx, + [OP_NOP] = &DwarfOp::op_nop, + [OP_NOT_IMPLEMENTED] = &DwarfOp::op_not_implemented, +}; + +template +bool DwarfOp::Eval(uint64_t start, uint64_t end) { + is_register_ = false; + stack_.clear(); + memory_->set_cur_offset(start); + dex_pc_set_ = false; + + // Unroll the first Decode calls to be able to check for a special + // sequence of ops and values that indicate this is the dex pc. + // The pattern is: + // OP_const4u (0x0c) 'D' 'E' 'X' '1' + // OP_drop (0x13) + if (memory_->cur_offset() < end) { + if (!Decode()) { + return false; + } + } else { + return true; + } + bool check_for_drop; + if (cur_op_ == 0x0c && operands_.back() == 0x31584544) { + check_for_drop = true; + } else { + check_for_drop = false; + } + if (memory_->cur_offset() < end) { + if (!Decode()) { + return false; + } + } else { + return true; + } + + if (check_for_drop && cur_op_ == 0x13) { + dex_pc_set_ = true; + } + + uint32_t iterations = 2; + while (memory_->cur_offset() < end) { + if (!Decode()) { + return false; + } + // To protect against a branch that creates an infinite loop, + // terminate if the number of iterations gets too high. + if (iterations++ == 1000) { + last_error_.code = DWARF_ERROR_TOO_MANY_ITERATIONS; + return false; + } + } + return true; +} + +template +bool DwarfOp::Decode() { + last_error_.code = DWARF_ERROR_NONE; + if (!memory_->ReadBytes(&cur_op_, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_->cur_offset(); + return false; + } + + const auto* op = &kCallbackTable[cur_op_]; + if (op->handle_func == OP_ILLEGAL) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + const auto handle_func = kOpHandleFuncList[op->handle_func]; + + // Make sure that the required number of stack elements is available. + if (stack_.size() < op->num_required_stack_values) { + last_error_.code = DWARF_ERROR_STACK_INDEX_NOT_VALID; + return false; + } + + operands_.clear(); + for (size_t i = 0; i < op->num_operands; i++) { + uint64_t value; + if (!memory_->ReadEncodedValue(op->operands[i], &value)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_->cur_offset(); + return false; + } + operands_.push_back(value); + } + return (this->*handle_func)(); +} + +template +void DwarfOp::GetLogInfo(uint64_t start, uint64_t end, + std::vector* lines) { + memory_->set_cur_offset(start); + while (memory_->cur_offset() < end) { + uint8_t cur_op; + if (!memory_->ReadBytes(&cur_op, 1)) { + return; + } + + std::string raw_string(android::base::StringPrintf("Raw Data: 0x%02x", cur_op)); + std::string log_string; + const auto* op = &kCallbackTable[cur_op]; + if (op->handle_func == OP_ILLEGAL) { + log_string = "Illegal"; + } else { + log_string = op->name; + uint64_t start_offset = memory_->cur_offset(); + for (size_t i = 0; i < op->num_operands; i++) { + uint64_t value; + if (!memory_->ReadEncodedValue(op->operands[i], &value)) { + return; + } + log_string += ' ' + std::to_string(value); + } + uint64_t end_offset = memory_->cur_offset(); + + memory_->set_cur_offset(start_offset); + for (size_t i = start_offset; i < end_offset; i++) { + uint8_t byte; + if (!memory_->ReadBytes(&byte, 1)) { + return; + } + raw_string += android::base::StringPrintf(" 0x%02x", byte); + } + memory_->set_cur_offset(end_offset); + } + lines->push_back(std::move(log_string)); + lines->push_back(std::move(raw_string)); + } +} + +template +bool DwarfOp::op_deref() { + // Read the address and dereference it. + AddressType addr = StackPop(); + AddressType value; + if (!regular_memory()->ReadFully(addr, &value, sizeof(value))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = addr; + return false; + } + stack_.push_front(value); + return true; +} + +template +bool DwarfOp::op_deref_size() { + AddressType bytes_to_read = OperandAt(0); + if (bytes_to_read > sizeof(AddressType) || bytes_to_read == 0) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + // Read the address and dereference it. + AddressType addr = StackPop(); + AddressType value = 0; + if (!regular_memory()->ReadFully(addr, &value, bytes_to_read)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = addr; + return false; + } + stack_.push_front(value); + return true; +} + +template +bool DwarfOp::op_push() { + // Push all of the operands. + for (auto operand : operands_) { + stack_.push_front(operand); + } + return true; +} + +template +bool DwarfOp::op_dup() { + stack_.push_front(StackAt(0)); + return true; +} + +template +bool DwarfOp::op_drop() { + StackPop(); + return true; +} + +template +bool DwarfOp::op_over() { + stack_.push_front(StackAt(1)); + return true; +} + +template +bool DwarfOp::op_pick() { + AddressType index = OperandAt(0); + if (index >= StackSize()) { + last_error_.code = DWARF_ERROR_STACK_INDEX_NOT_VALID; + return false; + } + stack_.push_front(StackAt(index)); + return true; +} + +template +bool DwarfOp::op_swap() { + AddressType old_value = stack_[0]; + stack_[0] = stack_[1]; + stack_[1] = old_value; + return true; +} + +template +bool DwarfOp::op_rot() { + AddressType top = stack_[0]; + stack_[0] = stack_[1]; + stack_[1] = stack_[2]; + stack_[2] = top; + return true; +} + +template +bool DwarfOp::op_abs() { + SignedType signed_value = static_cast(stack_[0]); + if (signed_value < 0) { + signed_value = -signed_value; + } + stack_[0] = static_cast(signed_value); + return true; +} + +template +bool DwarfOp::op_and() { + AddressType top = StackPop(); + stack_[0] &= top; + return true; +} + +template +bool DwarfOp::op_div() { + AddressType top = StackPop(); + if (top == 0) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + SignedType signed_divisor = static_cast(top); + SignedType signed_dividend = static_cast(stack_[0]); + stack_[0] = static_cast(signed_dividend / signed_divisor); + return true; +} + +template +bool DwarfOp::op_minus() { + AddressType top = StackPop(); + stack_[0] -= top; + return true; +} + +template +bool DwarfOp::op_mod() { + AddressType top = StackPop(); + if (top == 0) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + stack_[0] %= top; + return true; +} + +template +bool DwarfOp::op_mul() { + AddressType top = StackPop(); + stack_[0] *= top; + return true; +} + +template +bool DwarfOp::op_neg() { + SignedType signed_value = static_cast(stack_[0]); + stack_[0] = static_cast(-signed_value); + return true; +} + +template +bool DwarfOp::op_not() { + stack_[0] = ~stack_[0]; + return true; +} + +template +bool DwarfOp::op_or() { + AddressType top = StackPop(); + stack_[0] |= top; + return true; +} + +template +bool DwarfOp::op_plus() { + AddressType top = StackPop(); + stack_[0] += top; + return true; +} + +template +bool DwarfOp::op_plus_uconst() { + stack_[0] += OperandAt(0); + return true; +} + +template +bool DwarfOp::op_shl() { + AddressType top = StackPop(); + stack_[0] <<= top; + return true; +} + +template +bool DwarfOp::op_shr() { + AddressType top = StackPop(); + stack_[0] >>= top; + return true; +} + +template +bool DwarfOp::op_shra() { + AddressType top = StackPop(); + SignedType signed_value = static_cast(stack_[0]) >> top; + stack_[0] = static_cast(signed_value); + return true; +} + +template +bool DwarfOp::op_xor() { + AddressType top = StackPop(); + stack_[0] ^= top; + return true; +} + +template +bool DwarfOp::op_bra() { + // Requires one stack element. + AddressType top = StackPop(); + if (top == 0) { + return true; + } + + int16_t offset = static_cast(OperandAt(0)); + uint64_t cur_offset = memory_->cur_offset() + offset; + memory_->set_cur_offset(cur_offset); + return true; +} + +template +bool DwarfOp::op_eq() { + AddressType top = StackPop(); + stack_[0] = bool_to_dwarf_bool(stack_[0] == top); + return true; +} + +template +bool DwarfOp::op_ge() { + AddressType top = StackPop(); + stack_[0] = bool_to_dwarf_bool(stack_[0] >= top); + return true; +} + +template +bool DwarfOp::op_gt() { + AddressType top = StackPop(); + stack_[0] = bool_to_dwarf_bool(stack_[0] > top); + return true; +} + +template +bool DwarfOp::op_le() { + AddressType top = StackPop(); + stack_[0] = bool_to_dwarf_bool(stack_[0] <= top); + return true; +} + +template +bool DwarfOp::op_lt() { + AddressType top = StackPop(); + stack_[0] = bool_to_dwarf_bool(stack_[0] < top); + return true; +} + +template +bool DwarfOp::op_ne() { + AddressType top = StackPop(); + stack_[0] = bool_to_dwarf_bool(stack_[0] != top); + return true; +} + +template +bool DwarfOp::op_skip() { + int16_t offset = static_cast(OperandAt(0)); + uint64_t cur_offset = memory_->cur_offset() + offset; + memory_->set_cur_offset(cur_offset); + return true; +} + +template +bool DwarfOp::op_lit() { + stack_.push_front(cur_op() - 0x30); + return true; +} + +template +bool DwarfOp::op_reg() { + is_register_ = true; + stack_.push_front(cur_op() - 0x50); + return true; +} + +template +bool DwarfOp::op_regx() { + is_register_ = true; + stack_.push_front(OperandAt(0)); + return true; +} + +// It's not clear for breg/bregx, if this op should read the current +// value of the register, or where we think that register is located. +// For simplicity, the code will read the value before doing the unwind. +template +bool DwarfOp::op_breg() { + uint16_t reg = cur_op() - 0x70; + if (reg >= regs_info_->Total()) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + stack_.push_front(regs_info_->Get(reg) + OperandAt(0)); + return true; +} + +template +bool DwarfOp::op_bregx() { + AddressType reg = OperandAt(0); + if (reg >= regs_info_->Total()) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + stack_.push_front(regs_info_->Get(reg) + OperandAt(1)); + return true; +} + +template +bool DwarfOp::op_nop() { + return true; +} + +template +bool DwarfOp::op_not_implemented() { + last_error_.code = DWARF_ERROR_NOT_IMPLEMENTED; + return false; +} + +// Explicitly instantiate DwarfOp. +template class DwarfOp; +template class DwarfOp; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.h new file mode 100644 index 0000000000..2f33465749 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfOp.h @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include + +#include "DwarfEncoding.h" +#include "RegsInfo.h" + +namespace unwindstack { + +// Forward declarations. +class DwarfMemory; +class Memory; +template +class RegsImpl; + +template +class DwarfOp { + // Signed version of AddressType + typedef typename std::make_signed::type SignedType; + + public: + DwarfOp(DwarfMemory* memory, Memory* regular_memory) + : memory_(memory), regular_memory_(regular_memory) {} + virtual ~DwarfOp() = default; + + bool Decode(); + + bool Eval(uint64_t start, uint64_t end); + + void GetLogInfo(uint64_t start, uint64_t end, std::vector* lines); + + AddressType StackAt(size_t index) { return stack_[index]; } + size_t StackSize() { return stack_.size(); } + + void set_regs_info(RegsInfo* regs_info) { regs_info_ = regs_info; } + + const DwarfErrorData& last_error() { return last_error_; } + DwarfErrorCode LastErrorCode() { return last_error_.code; } + uint64_t LastErrorAddress() { return last_error_.address; } + + bool dex_pc_set() { return dex_pc_set_; } + + bool is_register() { return is_register_; } + + uint8_t cur_op() { return cur_op_; } + + Memory* regular_memory() { return regular_memory_; } + + protected: + AddressType OperandAt(size_t index) { return operands_[index]; } + size_t OperandsSize() { return operands_.size(); } + + AddressType StackPop() { + AddressType value = stack_.front(); + stack_.pop_front(); + return value; + } + + private: + DwarfMemory* memory_; + Memory* regular_memory_; + + RegsInfo* regs_info_; + bool dex_pc_set_ = false; + bool is_register_ = false; + DwarfErrorData last_error_{DWARF_ERROR_NONE, 0}; + uint8_t cur_op_; + std::vector operands_; + std::deque stack_; + + inline AddressType bool_to_dwarf_bool(bool value) { return value ? 1 : 0; } + + // Op processing functions. + bool op_deref(); + bool op_deref_size(); + bool op_push(); + bool op_dup(); + bool op_drop(); + bool op_over(); + bool op_pick(); + bool op_swap(); + bool op_rot(); + bool op_abs(); + bool op_and(); + bool op_div(); + bool op_minus(); + bool op_mod(); + bool op_mul(); + bool op_neg(); + bool op_not(); + bool op_or(); + bool op_plus(); + bool op_plus_uconst(); + bool op_shl(); + bool op_shr(); + bool op_shra(); + bool op_xor(); + bool op_bra(); + bool op_eq(); + bool op_ge(); + bool op_gt(); + bool op_le(); + bool op_lt(); + bool op_ne(); + bool op_skip(); + bool op_lit(); + bool op_reg(); + bool op_regx(); + bool op_breg(); + bool op_bregx(); + bool op_nop(); + bool op_not_implemented(); + + using OpHandleFuncPtr = bool (DwarfOp::*)(); + static const OpHandleFuncPtr kOpHandleFuncList[]; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfSection.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfSection.cpp new file mode 100644 index 0000000000..7c99bde2c2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/DwarfSection.cpp @@ -0,0 +1,827 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DwarfCfa.h" +#include "DwarfDebugFrame.h" +#include "DwarfEhFrame.h" +#include "DwarfEncoding.h" +#include "DwarfOp.h" +#include "RegsInfo.h" + +namespace unwindstack { + +DwarfSection::DwarfSection(Memory* memory) : memory_(memory) {} + +bool DwarfSection::Step(uint64_t pc, Regs* regs, Memory* process_memory, bool* finished, + bool* is_signal_frame) { + // Lookup the pc in the cache. + auto it = loc_regs_.upper_bound(pc); + if (it == loc_regs_.end() || pc < it->second.pc_start) { + last_error_.code = DWARF_ERROR_NONE; + const DwarfFde* fde = GetFdeFromPc(pc); + if (fde == nullptr || fde->cie == nullptr) { + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return false; + } + + // Now get the location information for this pc. + DwarfLocations loc_regs; + if (!GetCfaLocationInfo(pc, fde, &loc_regs, regs->Arch())) { + return false; + } + loc_regs.cie = fde->cie; + + // Store it in the cache. + it = loc_regs_.emplace(loc_regs.pc_end, std::move(loc_regs)).first; + } + + *is_signal_frame = it->second.cie->is_signal_frame; + + // Now eval the actual registers. + return Eval(it->second.cie, process_memory, it->second, regs, finished); +} + +template +const DwarfCie* DwarfSectionImpl::GetCieFromOffset(uint64_t offset) { + auto cie_entry = cie_entries_.find(offset); + if (cie_entry != cie_entries_.end()) { + return &cie_entry->second; + } + DwarfCie* cie = &cie_entries_[offset]; + memory_.set_data_offset(entries_offset_); + memory_.set_cur_offset(offset); + if (!FillInCieHeader(cie) || !FillInCie(cie)) { + // Erase the cached entry. + cie_entries_.erase(offset); + return nullptr; + } + return cie; +} + +template +bool DwarfSectionImpl::FillInCieHeader(DwarfCie* cie) { + cie->lsda_encoding = DW_EH_PE_omit; + uint32_t length32; + if (!memory_.ReadBytes(&length32, sizeof(length32))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + if (length32 == static_cast(-1)) { + // 64 bit Cie + uint64_t length64; + if (!memory_.ReadBytes(&length64, sizeof(length64))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + cie->cfa_instructions_end = memory_.cur_offset() + length64; + // TODO(b/192012848): This is wrong. We need to propagate pointer size here. + cie->fde_address_encoding = DW_EH_PE_udata8; + + uint64_t cie_id; + if (!memory_.ReadBytes(&cie_id, sizeof(cie_id))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + if (cie_id != cie64_value_) { + // This is not a Cie, something has gone horribly wrong. + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + } else { + // 32 bit Cie + cie->cfa_instructions_end = memory_.cur_offset() + length32; + // TODO(b/192012848): This is wrong. We need to propagate pointer size here. + cie->fde_address_encoding = DW_EH_PE_udata4; + + uint32_t cie_id; + if (!memory_.ReadBytes(&cie_id, sizeof(cie_id))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + if (cie_id != cie32_value_) { + // This is not a Cie, something has gone horribly wrong. + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + } + return true; +} + +template +bool DwarfSectionImpl::FillInCie(DwarfCie* cie) { + if (!memory_.ReadBytes(&cie->version, sizeof(cie->version))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + if (cie->version != 1 && cie->version != 3 && cie->version != 4 && cie->version != 5) { + // Unrecognized version. + last_error_.code = DWARF_ERROR_UNSUPPORTED_VERSION; + return false; + } + + // Read the augmentation string. + char aug_value; + do { + if (!memory_.ReadBytes(&aug_value, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + cie->augmentation_string.push_back(aug_value); + } while (aug_value != '\0'); + + if (cie->version == 4 || cie->version == 5) { + char address_size; + if (!memory_.ReadBytes(&address_size, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + cie->fde_address_encoding = address_size == 8 ? DW_EH_PE_udata8 : DW_EH_PE_udata4; + + // Segment Size + if (!memory_.ReadBytes(&cie->segment_size, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + } + + // Code Alignment Factor + if (!memory_.ReadULEB128(&cie->code_alignment_factor)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + // Data Alignment Factor + if (!memory_.ReadSLEB128(&cie->data_alignment_factor)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + if (cie->version == 1) { + // Return Address is a single byte. + uint8_t return_address_register; + if (!memory_.ReadBytes(&return_address_register, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + cie->return_address_register = return_address_register; + } else if (!memory_.ReadULEB128(&cie->return_address_register)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + if (cie->augmentation_string[0] != 'z') { + cie->cfa_instructions_offset = memory_.cur_offset(); + return true; + } + + uint64_t aug_length; + if (!memory_.ReadULEB128(&aug_length)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + cie->cfa_instructions_offset = memory_.cur_offset() + aug_length; + + for (size_t i = 1; i < cie->augmentation_string.size(); i++) { + switch (cie->augmentation_string[i]) { + case 'L': + if (!memory_.ReadBytes(&cie->lsda_encoding, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + break; + case 'P': { + uint8_t encoding; + if (!memory_.ReadBytes(&encoding, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + memory_.set_pc_offset(pc_offset_); + if (!memory_.ReadEncodedValue(encoding, &cie->personality_handler)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + } break; + case 'R': + if (!memory_.ReadBytes(&cie->fde_address_encoding, 1)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + break; + case 'S': + cie->is_signal_frame = true; + break; + } + } + return true; +} + +template +const DwarfFde* DwarfSectionImpl::GetFdeFromOffset(uint64_t offset) { + auto fde_entry = fde_entries_.find(offset); + if (fde_entry != fde_entries_.end()) { + return &fde_entry->second; + } + DwarfFde* fde = &fde_entries_[offset]; + memory_.set_data_offset(entries_offset_); + memory_.set_cur_offset(offset); + if (!FillInFdeHeader(fde) || !FillInFde(fde)) { + fde_entries_.erase(offset); + return nullptr; + } + return fde; +} + +template +bool DwarfSectionImpl::FillInFdeHeader(DwarfFde* fde) { + uint32_t length32; + if (!memory_.ReadBytes(&length32, sizeof(length32))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + if (length32 == static_cast(-1)) { + // 64 bit Fde. + uint64_t length64; + if (!memory_.ReadBytes(&length64, sizeof(length64))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + fde->cfa_instructions_end = memory_.cur_offset() + length64; + + uint64_t value64; + if (!memory_.ReadBytes(&value64, sizeof(value64))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + if (value64 == cie64_value_) { + // This is a Cie, this means something has gone wrong. + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + // Get the Cie pointer, which is necessary to properly read the rest of + // of the Fde information. + fde->cie_offset = GetCieOffsetFromFde64(value64); + } else { + // 32 bit Fde. + fde->cfa_instructions_end = memory_.cur_offset() + length32; + + uint32_t value32; + if (!memory_.ReadBytes(&value32, sizeof(value32))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + if (value32 == cie32_value_) { + // This is a Cie, this means something has gone wrong. + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + // Get the Cie pointer, which is necessary to properly read the rest of + // of the Fde information. + fde->cie_offset = GetCieOffsetFromFde32(value32); + } + return true; +} + +template +bool DwarfSectionImpl::FillInFde(DwarfFde* fde) { + uint64_t cur_offset = memory_.cur_offset(); + + const DwarfCie* cie = GetCieFromOffset(fde->cie_offset); + if (cie == nullptr) { + return false; + } + fde->cie = cie; + + if (cie->segment_size != 0) { + // Skip over the segment selector for now. + cur_offset += cie->segment_size; + } + memory_.set_cur_offset(cur_offset); + + // The load bias only applies to the start. + memory_.set_pc_offset(section_bias_); + bool valid = memory_.ReadEncodedValue(cie->fde_address_encoding, &fde->pc_start); + fde->pc_start = AdjustPcFromFde(fde->pc_start); + + memory_.set_pc_offset(0); + if (!valid || !memory_.ReadEncodedValue(cie->fde_address_encoding, &fde->pc_end)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + fde->pc_end += fde->pc_start; + + if (cie->augmentation_string.size() > 0 && cie->augmentation_string[0] == 'z') { + // Augmentation Size + uint64_t aug_length; + if (!memory_.ReadULEB128(&aug_length)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + uint64_t cur_offset = memory_.cur_offset(); + + memory_.set_pc_offset(pc_offset_); + if (!memory_.ReadEncodedValue(cie->lsda_encoding, &fde->lsda_address)) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + // Set our position to after all of the augmentation data. + memory_.set_cur_offset(cur_offset + aug_length); + } + fde->cfa_instructions_offset = memory_.cur_offset(); + + return true; +} + +template +bool DwarfSectionImpl::EvalExpression(const DwarfLocation& loc, Memory* regular_memory, + AddressType* value, + RegsInfo* regs_info, + bool* is_dex_pc) { + DwarfOp op(&memory_, regular_memory); + op.set_regs_info(regs_info); + + // Need to evaluate the op data. + uint64_t end = loc.values[1]; + uint64_t start = end - loc.values[0]; + if (!op.Eval(start, end)) { + last_error_ = op.last_error(); + return false; + } + if (op.StackSize() == 0) { + last_error_.code = DWARF_ERROR_ILLEGAL_STATE; + return false; + } + // We don't support an expression that evaluates to a register number. + if (op.is_register()) { + last_error_.code = DWARF_ERROR_NOT_IMPLEMENTED; + return false; + } + *value = op.StackAt(0); + if (is_dex_pc != nullptr && op.dex_pc_set()) { + *is_dex_pc = true; + } + return true; +} + +template +struct EvalInfo { + const DwarfLocations* loc_regs; + const DwarfCie* cie; + Memory* regular_memory; + AddressType cfa; + bool return_address_undefined = false; + RegsInfo regs_info; +}; + +template +bool DwarfSectionImpl::EvalRegister(const DwarfLocation* loc, uint32_t reg, + AddressType* reg_ptr, void* info) { + EvalInfo* eval_info = reinterpret_cast*>(info); + Memory* regular_memory = eval_info->regular_memory; + switch (loc->type) { + case DWARF_LOCATION_OFFSET: + if (!regular_memory->ReadFully(eval_info->cfa + loc->values[0], reg_ptr, sizeof(AddressType))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = eval_info->cfa + loc->values[0]; + return false; + } + break; + case DWARF_LOCATION_VAL_OFFSET: + *reg_ptr = eval_info->cfa + loc->values[0]; + break; + case DWARF_LOCATION_REGISTER: { + uint32_t cur_reg = loc->values[0]; + if (cur_reg >= eval_info->regs_info.Total()) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + *reg_ptr = eval_info->regs_info.Get(cur_reg) + loc->values[1]; + break; + } + case DWARF_LOCATION_EXPRESSION: + case DWARF_LOCATION_VAL_EXPRESSION: { + AddressType value; + bool is_dex_pc = false; + if (!EvalExpression(*loc, regular_memory, &value, &eval_info->regs_info, &is_dex_pc)) { + return false; + } + if (loc->type == DWARF_LOCATION_EXPRESSION) { + if (!regular_memory->ReadFully(value, reg_ptr, sizeof(AddressType))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = value; + return false; + } + } else { + *reg_ptr = value; + if (is_dex_pc) { + eval_info->regs_info.regs->set_dex_pc(value); + } + } + break; + } + case DWARF_LOCATION_UNDEFINED: + if (reg == eval_info->cie->return_address_register) { + eval_info->return_address_undefined = true; + } + break; + case DWARF_LOCATION_PSEUDO_REGISTER: + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + default: + break; + } + + return true; +} + +template +bool DwarfSectionImpl::Eval(const DwarfCie* cie, Memory* regular_memory, + const DwarfLocations& loc_regs, Regs* regs, + bool* finished) { + RegsImpl* cur_regs = reinterpret_cast*>(regs); + if (cie->return_address_register >= cur_regs->total_regs()) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + // Get the cfa value; + auto cfa_entry = loc_regs.find(CFA_REG); + if (cfa_entry == loc_regs.end()) { + last_error_.code = DWARF_ERROR_CFA_NOT_DEFINED; + return false; + } + + // Always set the dex pc to zero when evaluating. + cur_regs->set_dex_pc(0); + + // Reset necessary pseudo registers before evaluation. + // This is needed for ARM64, for example. + regs->ResetPseudoRegisters(); + + EvalInfo eval_info{.loc_regs = &loc_regs, + .cie = cie, + .regular_memory = regular_memory, + .regs_info = RegsInfo(cur_regs)}; + const DwarfLocation* loc = &cfa_entry->second; + // Only a few location types are valid for the cfa. + switch (loc->type) { + case DWARF_LOCATION_REGISTER: + if (loc->values[0] >= cur_regs->total_regs()) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + eval_info.cfa = (*cur_regs)[loc->values[0]]; + eval_info.cfa += loc->values[1]; + break; + case DWARF_LOCATION_VAL_EXPRESSION: { + AddressType value; + if (!EvalExpression(*loc, regular_memory, &value, &eval_info.regs_info, nullptr)) { + return false; + } + // There is only one type of valid expression for CFA evaluation. + eval_info.cfa = value; + break; + } + default: + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + + for (const auto& entry : loc_regs) { + uint32_t reg = entry.first; + // Already handled the CFA register. + if (reg == CFA_REG) continue; + + AddressType* reg_ptr; + if (reg >= cur_regs->total_regs()) { + if (entry.second.type != DWARF_LOCATION_PSEUDO_REGISTER) { + // Skip this unknown register. + continue; + } + if (!eval_info.regs_info.regs->SetPseudoRegister(reg, entry.second.values[0])) { + last_error_.code = DWARF_ERROR_ILLEGAL_VALUE; + return false; + } + } else { + reg_ptr = eval_info.regs_info.Save(reg); + if (!EvalRegister(&entry.second, reg, reg_ptr, &eval_info)) { + return false; + } + } + } + + // Find the return address location. + if (eval_info.return_address_undefined) { + cur_regs->set_pc(0); + } else { + cur_regs->set_pc((*cur_regs)[cie->return_address_register]); + } + + // If the pc was set to zero, consider this the final frame. Exception: if + // this is the sigreturn frame, then we want to try to recover the real PC + // using the return address (from LR or the stack), so keep going. + *finished = (cur_regs->pc() == 0 && !cie->is_signal_frame) ? true : false; + + cur_regs->set_sp(eval_info.cfa); + + return true; +} + +template +bool DwarfSectionImpl::GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde, + DwarfLocations* loc_regs, ArchEnum arch) { + DwarfCfa cfa(&memory_, fde, arch); + + // Look for the cached copy of the cie data. + auto reg_entry = cie_loc_regs_.find(fde->cie_offset); + if (reg_entry == cie_loc_regs_.end()) { + if (!cfa.GetLocationInfo(pc, fde->cie->cfa_instructions_offset, fde->cie->cfa_instructions_end, + loc_regs)) { + last_error_ = cfa.last_error(); + return false; + } + cie_loc_regs_[fde->cie_offset] = *loc_regs; + } + cfa.set_cie_loc_regs(&cie_loc_regs_[fde->cie_offset]); + if (!cfa.GetLocationInfo(pc, fde->cfa_instructions_offset, fde->cfa_instructions_end, loc_regs)) { + last_error_ = cfa.last_error(); + return false; + } + return true; +} + +template +bool DwarfSectionImpl::Log(uint8_t indent, uint64_t pc, const DwarfFde* fde, + ArchEnum arch) { + DwarfCfa cfa(&memory_, fde, arch); + + // Always print the cie information. + const DwarfCie* cie = fde->cie; + if (!cfa.Log(indent, pc, cie->cfa_instructions_offset, cie->cfa_instructions_end)) { + last_error_ = cfa.last_error(); + return false; + } + if (!cfa.Log(indent, pc, fde->cfa_instructions_offset, fde->cfa_instructions_end)) { + last_error_ = cfa.last_error(); + return false; + } + return true; +} + +template +bool DwarfSectionImpl::Init(uint64_t offset, uint64_t size, int64_t section_bias) { + section_bias_ = section_bias; + entries_offset_ = offset; + entries_end_ = offset + size; + + memory_.clear_func_offset(); + memory_.clear_text_offset(); + memory_.set_cur_offset(offset); + pc_offset_ = offset; + + return true; +} + +// Read CIE or FDE entry at the given offset, and set the offset to the following entry. +// The 'fde' argument is set only if we have seen an FDE entry. +template +bool DwarfSectionImpl::GetNextCieOrFde(uint64_t& next_entries_offset, + std::optional& fde_entry) { + const uint64_t start_offset = next_entries_offset; + + memory_.set_data_offset(entries_offset_); + memory_.set_cur_offset(next_entries_offset); + uint32_t value32; + if (!memory_.ReadBytes(&value32, sizeof(value32))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + uint64_t cie_offset; + uint8_t cie_fde_encoding; + bool entry_is_cie = false; + if (value32 == static_cast(-1)) { + // 64 bit entry. + uint64_t value64; + if (!memory_.ReadBytes(&value64, sizeof(value64))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + next_entries_offset = memory_.cur_offset() + value64; + // Read the Cie Id of a Cie or the pointer of the Fde. + if (!memory_.ReadBytes(&value64, sizeof(value64))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + if (value64 == cie64_value_) { + entry_is_cie = true; + cie_fde_encoding = DW_EH_PE_udata8; + } else { + cie_offset = GetCieOffsetFromFde64(value64); + } + } else { + next_entries_offset = memory_.cur_offset() + value32; + + // 32 bit Cie + if (!memory_.ReadBytes(&value32, sizeof(value32))) { + last_error_.code = DWARF_ERROR_MEMORY_INVALID; + last_error_.address = memory_.cur_offset(); + return false; + } + + if (value32 == cie32_value_) { + entry_is_cie = true; + cie_fde_encoding = DW_EH_PE_udata4; + } else { + cie_offset = GetCieOffsetFromFde32(value32); + } + } + + if (entry_is_cie) { + auto entry = cie_entries_.find(start_offset); + if (entry == cie_entries_.end()) { + DwarfCie* cie = &cie_entries_[start_offset]; + cie->lsda_encoding = DW_EH_PE_omit; + cie->cfa_instructions_end = next_entries_offset; + cie->fde_address_encoding = cie_fde_encoding; + + if (!FillInCie(cie)) { + cie_entries_.erase(start_offset); + return false; + } + } + fde_entry.reset(); + } else { + fde_entry = DwarfFde{}; + fde_entry->cfa_instructions_end = next_entries_offset; + fde_entry->cie_offset = cie_offset; + if (!FillInFde(&*fde_entry)) { + return false; + } + } + return true; +} + +template +void DwarfSectionImpl::GetFdes(std::vector* fdes) { + if (fde_index_.empty()) { + BuildFdeIndex(); + } + for (auto& it : fde_index_) { + fdes->push_back(GetFdeFromOffset(it.second)); + } +} + +template +const DwarfFde* DwarfSectionImpl::GetFdeFromPc(uint64_t pc) { + // Ensure that the binary search table is initialized. + if (fde_index_.empty()) { + BuildFdeIndex(); + } + + // Find the FDE offset in the binary search table. + auto comp = [](uint64_t pc, auto& entry) { return pc < entry.first; }; + auto it = std::upper_bound(fde_index_.begin(), fde_index_.end(), pc, comp); + if (it == fde_index_.end()) { + return nullptr; + } + + // Load the full FDE entry based on the offset. + const DwarfFde* fde = GetFdeFromOffset(/*fde_offset=*/it->second); + return fde != nullptr && fde->pc_start <= pc ? fde : nullptr; +} + +// Create binary search table to make FDE lookups fast (sorted by pc_end). +// We store only the FDE offset rather than the full entry to save memory. +// +// If there are overlapping entries, it inserts additional entries to ensure +// that one of the overlapping entries is found (it is undefined which one). +template +void DwarfSectionImpl::BuildFdeIndex() { + struct FdeInfo { + uint64_t pc_start, pc_end, fde_offset; + }; + std::vector fdes; + for (uint64_t offset = entries_offset_; offset < entries_end_;) { + const uint64_t initial_offset = offset; + std::optional fde; + if (!GetNextCieOrFde(offset, fde)) { + break; + } + if (fde.has_value() && /* defensive check */ (fde->pc_start < fde->pc_end)) { + fdes.push_back({fde->pc_start, fde->pc_end, initial_offset}); + } + if (offset <= initial_offset) { + break; // Jump back. Simply consider the processing done in this case. + } + } + std::sort(fdes.begin(), fdes.end(), [](const FdeInfo& a, const FdeInfo& b) { + return std::tie(a.pc_end, a.fde_offset) < std::tie(b.pc_end, b.fde_offset); + }); + + // If there are overlapping entries, ensure that we can always find one of them. + // For example, for entries: [300, 350) [400, 450) [100, 550) [600, 650) + // We add the following: [100, 300) [100, 400) + // Which ensures that the [100, 550) entry can be found in its whole range. + if (!fdes.empty()) { + FdeInfo filling = fdes.back(); // Entry with the minimal pc_start seen so far. + for (ssize_t i = fdes.size() - 1; i >= 0; i--) { // Iterate backwards. + uint64_t prev_pc_end = (i > 0) ? fdes[i - 1].pc_end : 0; + // If there is a gap between entries and the filling reaches the gap, fill it. + if (prev_pc_end < fdes[i].pc_start && filling.pc_start < fdes[i].pc_start) { + fdes.push_back({filling.pc_start, fdes[i].pc_start, filling.fde_offset}); + } + if (fdes[i].pc_start < filling.pc_start) { + filling = fdes[i]; + } + } + } + + // Copy data to the final binary search table (pc_end, fde_offset) and sort it. + fde_index_.reserve(fdes.size()); + for (const FdeInfo& it : fdes) { + fde_index_.emplace_back(it.pc_end, it.fde_offset); + } + if (!std::is_sorted(fde_index_.begin(), fde_index_.end())) { + std::sort(fde_index_.begin(), fde_index_.end()); + } +} + +// Explicitly instantiate DwarfSectionImpl +template class DwarfSectionImpl; +template class DwarfSectionImpl; + +// Explicitly instantiate DwarfDebugFrame +template class DwarfDebugFrame; +template class DwarfDebugFrame; + +// Explicitly instantiate DwarfEhFrame +template class DwarfEhFrame; +template class DwarfEhFrame; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Elf.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Elf.cpp new file mode 100644 index 0000000000..0df28e0f1e --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Elf.cpp @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ElfInterfaceArm.h" +#include "Symbols.h" + +namespace unwindstack { + +bool Elf::cache_enabled_; +std::unordered_map>>* Elf::cache_; +std::mutex* Elf::cache_lock_; + +bool Elf::Init() { + load_bias_ = 0; + if (!memory_) { + return false; + } + + interface_.reset(CreateInterfaceFromMemory(memory_.get())); + if (!interface_) { + return false; + } + + valid_ = interface_->Init(&load_bias_); + if (valid_) { + interface_->InitHeaders(); + InitGnuDebugdata(); + } else { + interface_.reset(nullptr); + } + return valid_; +} + +// It is expensive to initialize the .gnu_debugdata section. Provide a method +// to initialize this data separately. +void Elf::InitGnuDebugdata() { + if (!valid_ || interface_->gnu_debugdata_offset() == 0) { + return; + } + + gnu_debugdata_memory_ = interface_->CreateGnuDebugdataMemory(); + gnu_debugdata_interface_.reset(CreateInterfaceFromMemory(gnu_debugdata_memory_.get())); + ElfInterface* gnu = gnu_debugdata_interface_.get(); + if (gnu == nullptr) { + return; + } + + // Ignore the load_bias from the compressed section, the correct load bias + // is in the uncompressed data. + int64_t load_bias; + if (gnu->Init(&load_bias)) { + gnu->InitHeaders(); + interface_->SetGnuDebugdataInterface(gnu); + } else { + // Free all of the memory associated with the gnu_debugdata section. + gnu_debugdata_memory_.reset(nullptr); + gnu_debugdata_interface_.reset(nullptr); + } +} + +void Elf::Invalidate() { + interface_.reset(nullptr); + valid_ = false; +} + +std::string Elf::GetSoname() { + std::lock_guard guard(lock_); + if (!valid_) { + return ""; + } + return interface_->GetSoname(); +} + +uint64_t Elf::GetRelPc(uint64_t pc, MapInfo* map_info) { + return pc - map_info->start() + load_bias_ + map_info->elf_offset(); +} + +bool Elf::GetFunctionName(uint64_t addr, SharedString* name, uint64_t* func_offset) { + std::lock_guard guard(lock_); + return valid_ && (interface_->GetFunctionName(addr, name, func_offset) || + (gnu_debugdata_interface_ && + gnu_debugdata_interface_->GetFunctionName(addr, name, func_offset))); +} + +bool Elf::GetGlobalVariableOffset(const std::string& name, uint64_t* memory_offset) { + if (!valid_) { + return false; + } + + uint64_t vaddr; + if (!interface_->GetGlobalVariable(name, &vaddr) && + (gnu_debugdata_interface_ == nullptr || + !gnu_debugdata_interface_->GetGlobalVariable(name, &vaddr))) { + return false; + } + + if (arch() == ARCH_ARM64) { + // Tagged pointer after Android R would lead top byte to have random values + // https://source.android.com/devices/tech/debug/tagged-pointers + vaddr &= (1ULL << 56) - 1; + } + + // Check the .data section. + uint64_t vaddr_start = interface_->data_vaddr_start(); + if (vaddr >= vaddr_start && vaddr < interface_->data_vaddr_end()) { + *memory_offset = vaddr - vaddr_start + interface_->data_offset(); + return true; + } + + // Check the .dynamic section. + vaddr_start = interface_->dynamic_vaddr_start(); + if (vaddr >= vaddr_start && vaddr < interface_->dynamic_vaddr_end()) { + *memory_offset = vaddr - vaddr_start + interface_->dynamic_offset(); + return true; + } + + return false; +} + +std::string Elf::GetBuildID() { + if (!valid_) { + return ""; + } + return interface_->GetBuildID(); +} + +void Elf::GetLastError(ErrorData* data) { + if (valid_) { + *data = interface_->last_error(); + } else { + data->code = ERROR_INVALID_ELF; + data->address = 0; + } +} + +ErrorCode Elf::GetLastErrorCode() { + if (valid_) { + return interface_->LastErrorCode(); + } + return ERROR_INVALID_ELF; +} + +uint64_t Elf::GetLastErrorAddress() { + if (valid_) { + return interface_->LastErrorAddress(); + } + return 0; +} + +// The relative pc expectd by this function is relative to the start of the elf. +bool Elf::StepIfSignalHandler(uint64_t rel_pc, Regs* regs, Memory* process_memory) { + if (!valid_) { + return false; + } + + // Convert the rel_pc to an elf_offset. + if (rel_pc < static_cast(load_bias_)) { + return false; + } + return regs->StepIfSignalHandler(rel_pc - load_bias_, this, process_memory); +} + +// The relative pc is always relative to the start of the map from which it comes. +bool Elf::Step(uint64_t rel_pc, Regs* regs, Memory* process_memory, bool* finished, + bool* is_signal_frame) { + if (!valid_) { + return false; + } + + // Lock during the step which can update information in the object. + std::lock_guard guard(lock_); + return interface_->Step(rel_pc, regs, process_memory, finished, is_signal_frame); +} + +bool Elf::IsValidElf(Memory* memory) { + if (memory == nullptr) { + return false; + } + + // Verify that this is a valid elf file. + uint8_t e_ident[SELFMAG + 1]; + if (!memory->ReadFully(0, e_ident, SELFMAG)) { + return false; + } + + if (memcmp(e_ident, ELFMAG, SELFMAG) != 0) { + return false; + } + return true; +} + +bool Elf::GetInfo(Memory* memory, uint64_t* size) { + if (!IsValidElf(memory)) { + return false; + } + *size = 0; + + uint8_t class_type; + if (!memory->ReadFully(EI_CLASS, &class_type, 1)) { + return false; + } + + // Get the maximum size of the elf data from the header. + if (class_type == ELFCLASS32) { + ElfInterface32::GetMaxSize(memory, size); + } else if (class_type == ELFCLASS64) { + ElfInterface64::GetMaxSize(memory, size); + } else { + return false; + } + return true; +} + +bool Elf::IsValidPc(uint64_t pc) { + if (!valid_ || (load_bias_ > 0 && pc < static_cast(load_bias_))) { + return false; + } + + if (interface_->IsValidPc(pc)) { + return true; + } + + if (gnu_debugdata_interface_ != nullptr && gnu_debugdata_interface_->IsValidPc(pc)) { + return true; + } + + return false; +} + +bool Elf::GetTextRange(uint64_t* addr, uint64_t* size) { + if (!valid_) { + return false; + } + + if (interface_->GetTextRange(addr, size)) { + *addr += load_bias_; + return true; + } + + if (gnu_debugdata_interface_ != nullptr && gnu_debugdata_interface_->GetTextRange(addr, size)) { + *addr += load_bias_; + return true; + } + + return false; +} + +ElfInterface* Elf::CreateInterfaceFromMemory(Memory* memory) { + if (!IsValidElf(memory)) { + return nullptr; + } + + std::unique_ptr interface; + if (!memory->ReadFully(EI_CLASS, &class_type_, 1)) { + return nullptr; + } + if (class_type_ == ELFCLASS32) { + Elf32_Half e_machine; + if (!memory->ReadFully(EI_NIDENT + sizeof(Elf32_Half), &e_machine, sizeof(e_machine))) { + return nullptr; + } + + machine_type_ = e_machine; + if (e_machine == EM_ARM) { + arch_ = ARCH_ARM; + interface.reset(new ElfInterfaceArm(memory)); + } else if (e_machine == EM_386) { + arch_ = ARCH_X86; + interface.reset(new ElfInterface32(memory)); + } else { + // Unsupported. + return nullptr; + } + } else if (class_type_ == ELFCLASS64) { + Elf64_Half e_machine; + if (!memory->ReadFully(EI_NIDENT + sizeof(Elf64_Half), &e_machine, sizeof(e_machine))) { + return nullptr; + } + + machine_type_ = e_machine; + if (e_machine == EM_AARCH64) { + arch_ = ARCH_ARM64; + } else if (e_machine == EM_X86_64) { + arch_ = ARCH_X86_64; + } else { + // Unsupported. + return nullptr; + } + interface.reset(new ElfInterface64(memory)); + } + + return interface.release(); +} + +int64_t Elf::GetLoadBias(Memory* memory) { + if (!IsValidElf(memory)) { + return 0; + } + + uint8_t class_type; + if (!memory->Read(EI_CLASS, &class_type, 1)) { + return 0; + } + + if (class_type == ELFCLASS32) { + return ElfInterface::GetLoadBias(memory); + } else if (class_type == ELFCLASS64) { + return ElfInterface::GetLoadBias(memory); + } + return 0; +} + +void Elf::SetCachingEnabled(bool enable) { + if (!cache_enabled_ && enable) { + cache_enabled_ = true; + cache_ = + new std::unordered_map>>; + cache_lock_ = new std::mutex; + } else if (cache_enabled_ && !enable) { + cache_enabled_ = false; + delete cache_; + delete cache_lock_; + } +} + +void Elf::CacheLock() { + cache_lock_->lock(); +} + +void Elf::CacheUnlock() { + cache_lock_->unlock(); +} + +void Elf::CacheAdd(MapInfo* info) { + if (!info->elf()->valid()) { + return; + } + (*cache_)[std::string(info->name())].emplace(info->elf_start_offset(), info->elf()); +} + +bool Elf::CacheGet(MapInfo* info) { + auto name_entry = cache_->find(std::string(info->name())); + if (name_entry == cache_->end()) { + return false; + } + // First look to see if there is a zero offset entry, this indicates + // the whole elf is the file. + auto& offset_cache = name_entry->second; + uint64_t elf_start_offset = 0; + auto entry = offset_cache.find(elf_start_offset); + if (entry == offset_cache.end()) { + // Try and find using the current offset. + elf_start_offset = info->offset(); + entry = offset_cache.find(elf_start_offset); + if (entry == offset_cache.end()) { + // If this is an execute map, then see if the previous read-only + // map is the start of the elf. + if (!(info->flags() & PROT_EXEC)) { + return false; + } + auto prev_map = info->GetPrevRealMap(); + if (prev_map == nullptr || info->offset() <= prev_map->offset() || + (prev_map->flags() != PROT_READ)) { + return false; + } + elf_start_offset = prev_map->offset(); + entry = offset_cache.find(elf_start_offset); + if (entry == offset_cache.end()) { + return false; + } + } + } + + info->set_elf(entry->second); + info->set_elf_start_offset(elf_start_offset); + info->set_elf_offset(info->offset() - elf_start_offset); + return true; +} + +std::string Elf::GetBuildID(Memory* memory) { + if (!IsValidElf(memory)) { + return ""; + } + + uint8_t class_type; + if (!memory->Read(EI_CLASS, &class_type, 1)) { + return ""; + } + + if (class_type == ELFCLASS32) { + return ElfInterface::ReadBuildIDFromMemory(memory); + } else if (class_type == ELFCLASS64) { + return ElfInterface::ReadBuildIDFromMemory(memory); + } + return ""; +} + +std::string Elf::GetPrintableBuildID(std::string& build_id) { + if (build_id.empty()) { + return ""; + } + std::string printable_build_id; + for (const char& c : build_id) { + // Use %hhx to avoid sign extension on abis that have signed chars. + printable_build_id += android::base::StringPrintf("%02hhx", c); + } + return printable_build_id; +} + +std::string Elf::GetPrintableBuildID() { + std::string build_id = GetBuildID(); + return Elf::GetPrintableBuildID(build_id); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterface.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterface.cpp new file mode 100644 index 0000000000..8fe0ecf601 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterface.cpp @@ -0,0 +1,629 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "DwarfDebugFrame.h" +#include "DwarfEhFrame.h" +#include "DwarfEhFrameWithHdr.h" +#include "MemoryBuffer.h" +#include "Symbols.h" + +namespace unwindstack { + +ElfInterface::~ElfInterface() { + for (auto symbol : symbols_) { + delete symbol; + } +} + +bool ElfInterface::IsValidPc(uint64_t pc) { + if (!pt_loads_.empty()) { + for (auto& entry : pt_loads_) { + uint64_t start = entry.second.table_offset; + uint64_t end = start + entry.second.table_size; + if (pc >= start && pc < end) { + return true; + } + } + return false; + } + + // No PT_LOAD data, look for a fde for this pc in the section data. + if (debug_frame_ != nullptr && debug_frame_->GetFdeFromPc(pc) != nullptr) { + return true; + } + + if (eh_frame_ != nullptr && eh_frame_->GetFdeFromPc(pc) != nullptr) { + return true; + } + + return false; +} + +bool ElfInterface::GetTextRange(uint64_t* addr, uint64_t* size) { + if (text_size_ != 0) { + *addr = text_addr_; + *size = text_size_; + return true; + } + return false; +} + +std::unique_ptr ElfInterface::CreateGnuDebugdataMemory() { + return nullptr; +} + +template +void ElfInterfaceImpl::InitHeaders() { + if (eh_frame_hdr_offset_ != 0) { + DwarfEhFrameWithHdr* eh_frame_hdr = new DwarfEhFrameWithHdr(memory_); + eh_frame_.reset(eh_frame_hdr); + if (!eh_frame_hdr->EhFrameInit(eh_frame_offset_, eh_frame_size_, eh_frame_section_bias_) || + !eh_frame_->Init(eh_frame_hdr_offset_, eh_frame_hdr_size_, eh_frame_hdr_section_bias_)) { + eh_frame_.reset(nullptr); + } + } + + if (eh_frame_.get() == nullptr && eh_frame_offset_ != 0) { + // If there is an eh_frame section without an eh_frame_hdr section, + // or using the frame hdr object failed to init. + eh_frame_.reset(new DwarfEhFrame(memory_)); + if (!eh_frame_->Init(eh_frame_offset_, eh_frame_size_, eh_frame_section_bias_)) { + eh_frame_.reset(nullptr); + } + } + + if (eh_frame_.get() == nullptr) { + eh_frame_hdr_offset_ = 0; + eh_frame_hdr_section_bias_ = 0; + eh_frame_hdr_size_ = static_cast(-1); + eh_frame_offset_ = 0; + eh_frame_section_bias_ = 0; + eh_frame_size_ = static_cast(-1); + } + + if (debug_frame_offset_ != 0) { + debug_frame_.reset(new DwarfDebugFrame(memory_)); + if (!debug_frame_->Init(debug_frame_offset_, debug_frame_size_, debug_frame_section_bias_)) { + debug_frame_.reset(nullptr); + debug_frame_offset_ = 0; + debug_frame_size_ = static_cast(-1); + } + } +} + +template +bool ElfInterfaceImpl::ReadAllHeaders(int64_t* load_bias) { + EhdrType ehdr; + if (!memory_->ReadFully(0, &ehdr, sizeof(ehdr))) { + last_error_.code = ERROR_MEMORY_INVALID; + last_error_.address = 0; + return false; + } + + // If we have enough information that this is an elf file, then allow + // malformed program and section headers. + ReadProgramHeaders(ehdr, load_bias); + ReadSectionHeaders(ehdr); + return true; +} + +template +int64_t ElfInterface::GetLoadBias(Memory* memory) { + EhdrType ehdr; + if (!memory->ReadFully(0, &ehdr, sizeof(ehdr))) { + return false; + } + + uint64_t offset = ehdr.e_phoff; + for (size_t i = 0; i < ehdr.e_phnum; i++, offset += ehdr.e_phentsize) { + PhdrType phdr; + if (!memory->ReadFully(offset, &phdr, sizeof(phdr))) { + return 0; + } + + // Find the first executable load when looking for the load bias. + if (phdr.p_type == PT_LOAD && (phdr.p_flags & PF_X)) { + return static_cast(phdr.p_vaddr) - phdr.p_offset; + } + } + return 0; +} + +template +void ElfInterfaceImpl::ReadProgramHeaders(const EhdrType& ehdr, int64_t* load_bias) { + uint64_t offset = ehdr.e_phoff; + bool first_exec_load_header = true; + for (size_t i = 0; i < ehdr.e_phnum; i++, offset += ehdr.e_phentsize) { + PhdrType phdr; + if (!memory_->ReadFully(offset, &phdr, sizeof(phdr))) { + return; + } + + switch (phdr.p_type) { + case PT_LOAD: + { + if ((phdr.p_flags & PF_X) == 0) { + continue; + } + + pt_loads_[phdr.p_offset] = LoadInfo{phdr.p_offset, phdr.p_vaddr, + static_cast(phdr.p_memsz)}; + // Only set the load bias from the first executable load header. + if (first_exec_load_header) { + *load_bias = static_cast(phdr.p_vaddr) - phdr.p_offset; + } + first_exec_load_header = false; + break; + } + + case PT_GNU_EH_FRAME: + // This is really the pointer to the .eh_frame_hdr section. + eh_frame_hdr_offset_ = phdr.p_offset; + eh_frame_hdr_section_bias_ = static_cast(phdr.p_vaddr) - phdr.p_offset; + eh_frame_hdr_size_ = phdr.p_memsz; + break; + + case PT_DYNAMIC: + dynamic_offset_ = phdr.p_offset; + dynamic_vaddr_start_ = phdr.p_vaddr; + if (__builtin_add_overflow(dynamic_vaddr_start_, phdr.p_memsz, &dynamic_vaddr_end_)) { + dynamic_offset_ = 0; + dynamic_vaddr_start_ = 0; + dynamic_vaddr_end_ = 0; + } + break; + + default: + HandleUnknownType(phdr.p_type, phdr.p_offset, phdr.p_filesz); + break; + } + } +} + +template +std::string ElfInterfaceImpl::ReadBuildID() { + // Ensure there is no overflow in any of the calulations below. + uint64_t tmp; + if (__builtin_add_overflow(gnu_build_id_offset_, gnu_build_id_size_, &tmp)) { + return ""; + } + + uint64_t offset = 0; + while (offset < gnu_build_id_size_) { + if (gnu_build_id_size_ - offset < sizeof(NhdrType)) { + return ""; + } + NhdrType hdr; + if (!memory_->ReadFully(gnu_build_id_offset_ + offset, &hdr, sizeof(hdr))) { + return ""; + } + offset += sizeof(hdr); + + if (gnu_build_id_size_ - offset < hdr.n_namesz) { + return ""; + } + if (hdr.n_namesz > 0) { + std::string name(hdr.n_namesz, '\0'); + if (!memory_->ReadFully(gnu_build_id_offset_ + offset, &(name[0]), hdr.n_namesz)) { + return ""; + } + + // Trim trailing \0 as GNU is stored as a C string in the ELF file. + if (name.back() == '\0') + name.resize(name.size() - 1); + + // Align hdr.n_namesz to next power multiple of 4. See man 5 elf. + offset += (hdr.n_namesz + 3) & ~3; + + if (name == "GNU" && hdr.n_type == NT_GNU_BUILD_ID) { + if (gnu_build_id_size_ - offset < hdr.n_descsz || hdr.n_descsz == 0) { + return ""; + } + std::string build_id(hdr.n_descsz, '\0'); + if (memory_->ReadFully(gnu_build_id_offset_ + offset, &build_id[0], hdr.n_descsz)) { + return build_id; + } + return ""; + } + } + // Align hdr.n_descsz to next power multiple of 4. See man 5 elf. + offset += (hdr.n_descsz + 3) & ~3; + } + return ""; +} +template +void ElfInterfaceImpl::ReadSectionHeaders(const EhdrType& ehdr) { + uint64_t offset = ehdr.e_shoff; + uint64_t sec_offset = 0; + uint64_t sec_size = 0; + + // Get the location of the section header names. + // If something is malformed in the header table data, we aren't going + // to terminate, we'll simply ignore this part. + ShdrType shdr; + if (ehdr.e_shstrndx < ehdr.e_shnum) { + uint64_t sh_offset = offset + ehdr.e_shstrndx * ehdr.e_shentsize; + if (memory_->ReadFully(sh_offset, &shdr, sizeof(shdr))) { + sec_offset = shdr.sh_offset; + sec_size = shdr.sh_size; + } + } + + // Skip the first header, it's always going to be NULL. + offset += ehdr.e_shentsize; + for (size_t i = 1; i < ehdr.e_shnum; i++, offset += ehdr.e_shentsize) { + if (!memory_->ReadFully(offset, &shdr, sizeof(shdr))) { + return; + } + + if (shdr.sh_type == SHT_SYMTAB || shdr.sh_type == SHT_DYNSYM) { + // Need to go get the information about the section that contains + // the string terminated names. + ShdrType str_shdr; + if (shdr.sh_link >= ehdr.e_shnum) { + continue; + } + uint64_t str_offset = ehdr.e_shoff + shdr.sh_link * ehdr.e_shentsize; + if (!memory_->ReadFully(str_offset, &str_shdr, sizeof(str_shdr))) { + continue; + } + if (str_shdr.sh_type != SHT_STRTAB) { + continue; + } + symbols_.push_back(new Symbols(shdr.sh_offset, shdr.sh_size, shdr.sh_entsize, + str_shdr.sh_offset, str_shdr.sh_size)); + } else if ((shdr.sh_type == SHT_PROGBITS || shdr.sh_type == SHT_NOBITS) && sec_size != 0) { + // Look for the .debug_frame and .gnu_debugdata. + if (shdr.sh_name < sec_size) { + std::string name; + if (memory_->ReadString(sec_offset + shdr.sh_name, &name, sec_size - shdr.sh_name)) { + if (name == ".debug_frame") { + debug_frame_offset_ = shdr.sh_offset; + debug_frame_size_ = shdr.sh_size; + debug_frame_section_bias_ = static_cast(shdr.sh_addr) - shdr.sh_offset; + } else if (name == ".gnu_debugdata") { + gnu_debugdata_offset_ = shdr.sh_offset; + gnu_debugdata_size_ = shdr.sh_size; + } else if (name == ".eh_frame") { + eh_frame_offset_ = shdr.sh_offset; + eh_frame_section_bias_ = static_cast(shdr.sh_addr) - shdr.sh_offset; + eh_frame_size_ = shdr.sh_size; + } else if (eh_frame_hdr_offset_ == 0 && name == ".eh_frame_hdr") { + eh_frame_hdr_offset_ = shdr.sh_offset; + eh_frame_hdr_section_bias_ = static_cast(shdr.sh_addr) - shdr.sh_offset; + eh_frame_hdr_size_ = shdr.sh_size; + } else if (name == ".data") { + data_offset_ = shdr.sh_offset; + data_vaddr_start_ = shdr.sh_addr; + if (__builtin_add_overflow(data_vaddr_start_, shdr.sh_size, &data_vaddr_end_)) { + data_offset_ = 0; + data_vaddr_start_ = 0; + data_vaddr_end_ = 0; + } + } else if (name == ".text") { + text_addr_ = shdr.sh_addr; + text_size_ = shdr.sh_size; + } + } + } + } else if (shdr.sh_type == SHT_STRTAB) { + // In order to read soname, keep track of address to offset mapping. + strtabs_.push_back(std::make_pair(static_cast(shdr.sh_addr), + static_cast(shdr.sh_offset))); + } else if (shdr.sh_type == SHT_NOTE) { + if (shdr.sh_name < sec_size) { + std::string name; + if (memory_->ReadString(sec_offset + shdr.sh_name, &name, sec_size - shdr.sh_name) && + name == ".note.gnu.build-id") { + gnu_build_id_offset_ = shdr.sh_offset; + gnu_build_id_size_ = shdr.sh_size; + } + } + } + } +} + +template +std::string ElfInterfaceImpl::GetSoname() { + if (soname_type_ == SONAME_INVALID) { + return ""; + } + if (soname_type_ == SONAME_VALID) { + return soname_; + } + + soname_type_ = SONAME_INVALID; + + uint64_t soname_offset = 0; + uint64_t strtab_addr = 0; + uint64_t strtab_size = 0; + + // Find the soname location from the dynamic headers section. + DynType dyn; + uint64_t offset = dynamic_offset_; + uint64_t max_offset = offset + dynamic_vaddr_end_ - dynamic_vaddr_start_; + for (uint64_t offset = dynamic_offset_; offset < max_offset; offset += sizeof(DynType)) { + if (!memory_->ReadFully(offset, &dyn, sizeof(dyn))) { + last_error_.code = ERROR_MEMORY_INVALID; + last_error_.address = offset; + return ""; + } + + if (dyn.d_tag == DT_STRTAB) { + strtab_addr = dyn.d_un.d_ptr; + } else if (dyn.d_tag == DT_STRSZ) { + strtab_size = dyn.d_un.d_val; + } else if (dyn.d_tag == DT_SONAME) { + soname_offset = dyn.d_un.d_val; + } else if (dyn.d_tag == DT_NULL) { + break; + } + } + + // Need to map the strtab address to the real offset. + for (const auto& entry : strtabs_) { + if (entry.first == strtab_addr) { + soname_offset = entry.second + soname_offset; + uint64_t soname_max = entry.second + strtab_size; + if (soname_offset >= soname_max) { + return ""; + } + if (!memory_->ReadString(soname_offset, &soname_, soname_max - soname_offset)) { + return ""; + } + soname_type_ = SONAME_VALID; + return soname_; + } + } + return ""; +} + +template +bool ElfInterfaceImpl::GetFunctionName(uint64_t addr, SharedString* name, + uint64_t* func_offset) { + if (symbols_.empty()) { + return false; + } + + for (const auto symbol : symbols_) { + if (symbol->template GetName(addr, memory_, name, func_offset)) { + return true; + } + } + return false; +} + +template +bool ElfInterfaceImpl::GetGlobalVariable(const std::string& name, + uint64_t* memory_address) { + if (symbols_.empty()) { + return false; + } + + for (const auto symbol : symbols_) { + if (symbol->template GetGlobal(memory_, name, memory_address)) { + return true; + } + } + return false; +} + +bool ElfInterface::Step(uint64_t pc, Regs* regs, Memory* process_memory, bool* finished, + bool* is_signal_frame) { + last_error_.code = ERROR_NONE; + last_error_.address = 0; + + // Try the debug_frame first since it contains the most specific unwind + // information. + DwarfSection* debug_frame = debug_frame_.get(); + if (debug_frame != nullptr && + debug_frame->Step(pc, regs, process_memory, finished, is_signal_frame)) { + return true; + } + + // Try the eh_frame next. + DwarfSection* eh_frame = eh_frame_.get(); + if (eh_frame != nullptr && eh_frame->Step(pc, regs, process_memory, finished, is_signal_frame)) { + return true; + } + + if (gnu_debugdata_interface_ != nullptr && + gnu_debugdata_interface_->Step(pc, regs, process_memory, finished, is_signal_frame)) { + return true; + } + + // Set the error code based on the first error encountered. + DwarfSection* section = nullptr; + if (debug_frame_ != nullptr) { + section = debug_frame_.get(); + } else if (eh_frame_ != nullptr) { + section = eh_frame_.get(); + } else if (gnu_debugdata_interface_ != nullptr) { + last_error_ = gnu_debugdata_interface_->last_error(); + return false; + } else { + return false; + } + + // Convert the DWARF ERROR to an external error. + DwarfErrorCode code = section->LastErrorCode(); + switch (code) { + case DWARF_ERROR_NONE: + last_error_.code = ERROR_NONE; + break; + + case DWARF_ERROR_MEMORY_INVALID: + last_error_.code = ERROR_MEMORY_INVALID; + last_error_.address = section->LastErrorAddress(); + break; + + case DWARF_ERROR_ILLEGAL_VALUE: + case DWARF_ERROR_ILLEGAL_STATE: + case DWARF_ERROR_STACK_INDEX_NOT_VALID: + case DWARF_ERROR_TOO_MANY_ITERATIONS: + case DWARF_ERROR_CFA_NOT_DEFINED: + case DWARF_ERROR_NO_FDES: + last_error_.code = ERROR_UNWIND_INFO; + break; + + case DWARF_ERROR_NOT_IMPLEMENTED: + case DWARF_ERROR_UNSUPPORTED_VERSION: + last_error_.code = ERROR_UNSUPPORTED; + break; + } + return false; +} + +// This is an estimation of the size of the elf file using the location +// of the section headers and size. This assumes that the section headers +// are at the end of the elf file. If the elf has a load bias, the size +// will be too large, but this is acceptable. +template +void ElfInterfaceImpl::GetMaxSize(Memory* memory, uint64_t* size) { + EhdrType ehdr; + if (!memory->ReadFully(0, &ehdr, sizeof(ehdr))) { + return; + } + if (ehdr.e_shnum == 0) { + return; + } + *size = ehdr.e_shoff + ehdr.e_shentsize * ehdr.e_shnum; +} + +template +bool GetBuildIDInfo(Memory* memory, uint64_t* build_id_offset, uint64_t* build_id_size) { + EhdrType ehdr; + if (!memory->ReadFully(0, &ehdr, sizeof(ehdr))) { + return false; + } + + uint64_t offset = ehdr.e_shoff; + uint64_t sec_offset; + uint64_t sec_size; + ShdrType shdr; + if (ehdr.e_shstrndx >= ehdr.e_shnum) { + return false; + } + + uint64_t sh_offset = offset + ehdr.e_shstrndx * ehdr.e_shentsize; + if (!memory->ReadFully(sh_offset, &shdr, sizeof(shdr))) { + return false; + } + sec_offset = shdr.sh_offset; + sec_size = shdr.sh_size; + + // Skip the first header, it's always going to be NULL. + offset += ehdr.e_shentsize; + for (size_t i = 1; i < ehdr.e_shnum; i++, offset += ehdr.e_shentsize) { + if (!memory->ReadFully(offset, &shdr, sizeof(shdr))) { + return false; + } + std::string name; + if (shdr.sh_type == SHT_NOTE && shdr.sh_name < sec_size && + memory->ReadString(sec_offset + shdr.sh_name, &name, sec_size - shdr.sh_name) && + name == ".note.gnu.build-id") { + *build_id_offset = shdr.sh_offset; + *build_id_size = shdr.sh_size; + return true; + } + } + + return false; +} + +template +std::string ElfInterface::ReadBuildIDFromMemory(Memory* memory) { + uint64_t note_offset; + uint64_t note_size; + if (!GetBuildIDInfo(memory, ¬e_offset, ¬e_size)) { + return ""; + } + + // Ensure there is no overflow in any of the calculations below. + uint64_t tmp; + if (__builtin_add_overflow(note_offset, note_size, &tmp)) { + return ""; + } + + uint64_t offset = 0; + while (offset < note_size) { + if (note_size - offset < sizeof(NhdrType)) { + return ""; + } + NhdrType hdr; + if (!memory->ReadFully(note_offset + offset, &hdr, sizeof(hdr))) { + return ""; + } + offset += sizeof(hdr); + + if (note_size - offset < hdr.n_namesz) { + return ""; + } + if (hdr.n_namesz > 0) { + std::string name(hdr.n_namesz, '\0'); + if (!memory->ReadFully(note_offset + offset, &(name[0]), hdr.n_namesz)) { + return ""; + } + + // Trim trailing \0 as GNU is stored as a C string in the ELF file. + if (name.back() == '\0') name.resize(name.size() - 1); + + // Align hdr.n_namesz to next power multiple of 4. See man 5 elf. + offset += (hdr.n_namesz + 3) & ~3; + + if (name == "GNU" && hdr.n_type == NT_GNU_BUILD_ID) { + if (note_size - offset < hdr.n_descsz || hdr.n_descsz == 0) { + return ""; + } + std::string build_id(hdr.n_descsz, '\0'); + if (memory->ReadFully(note_offset + offset, &build_id[0], hdr.n_descsz)) { + return build_id; + } + return ""; + } + } + // Align hdr.n_descsz to next power multiple of 4. See man 5 elf. + offset += (hdr.n_descsz + 3) & ~3; + } + return ""; +} + +// Instantiate all of the needed template functions. +template class ElfInterfaceImpl; +template class ElfInterfaceImpl; + +template int64_t ElfInterface::GetLoadBias(Memory*); +template int64_t ElfInterface::GetLoadBias(Memory*); + +template std::string ElfInterface::ReadBuildIDFromMemory( + Memory*); +template std::string ElfInterface::ReadBuildIDFromMemory( + Memory*); + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.cpp new file mode 100644 index 0000000000..de6a5d7b36 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include "ArmExidx.h" +#include "ElfInterfaceArm.h" + +namespace unwindstack { + +bool ElfInterfaceArm::Init(int64_t* load_bias) { + if (!ElfInterface32::Init(load_bias)) { + return false; + } + load_bias_ = *load_bias; + return true; +} + +bool ElfInterfaceArm::FindEntry(uint32_t pc, uint64_t* entry_offset) { + if (start_offset_ == 0 || total_entries_ == 0) { + last_error_.code = ERROR_UNWIND_INFO; + return false; + } + + size_t first = 0; + size_t last = total_entries_; + while (first < last) { + size_t current = (first + last) / 2; + uint32_t addr = addrs_[current]; + if (addr == 0) { + if (!GetPrel31Addr(start_offset_ + current * 8, &addr)) { + return false; + } + addrs_[current] = addr; + } + if (pc == addr) { + *entry_offset = start_offset_ + current * 8; + return true; + } + if (pc < addr) { + last = current; + } else { + first = current + 1; + } + } + if (last != 0) { + *entry_offset = start_offset_ + (last - 1) * 8; + return true; + } + last_error_.code = ERROR_UNWIND_INFO; + return false; +} + +bool ElfInterfaceArm::GetPrel31Addr(uint32_t offset, uint32_t* addr) { + uint32_t data; + if (!memory_->Read32(offset, &data)) { + last_error_.code = ERROR_MEMORY_INVALID; + last_error_.address = offset; + return false; + } + + // Sign extend the value if necessary. + int32_t value = (static_cast(data) << 1) >> 1; + *addr = offset + value; + return true; +} + +#if !defined(PT_ARM_EXIDX) +#define PT_ARM_EXIDX 0x70000001 +#endif + +void ElfInterfaceArm::HandleUnknownType(uint32_t type, uint64_t ph_offset, uint64_t ph_filesz) { + if (type != PT_ARM_EXIDX) { + return; + } + + // The offset already takes into account the load bias. + start_offset_ = ph_offset; + + // Always use filesz instead of memsz. In most cases they are the same, + // but some shared libraries wind up setting one correctly and not the other. + total_entries_ = ph_filesz / 8; +} + +bool ElfInterfaceArm::Step(uint64_t pc, Regs* regs, Memory* process_memory, bool* finished, + bool* is_signal_frame) { + // Dwarf unwind information is precise about whether a pc is covered or not, + // but arm unwind information only has ranges of pc. In order to avoid + // incorrectly doing a bad unwind using arm unwind information for a + // different function, always try and unwind with the dwarf information first. + return ElfInterface32::Step(pc, regs, process_memory, finished, is_signal_frame) || + StepExidx(pc, regs, process_memory, finished); +} + +bool ElfInterfaceArm::StepExidx(uint64_t pc, Regs* regs, Memory* process_memory, bool* finished) { + // Adjust the load bias to get the real relative pc. + if (pc < load_bias_) { + last_error_.code = ERROR_UNWIND_INFO; + return false; + } + pc -= load_bias_; + + RegsArm* regs_arm = reinterpret_cast(regs); + uint64_t entry_offset; + if (!FindEntry(pc, &entry_offset)) { + return false; + } + + ArmExidx arm(regs_arm, memory_, process_memory); + arm.set_cfa(regs_arm->sp()); + bool return_value = false; + if (arm.ExtractEntryData(entry_offset) && arm.Eval()) { + // If the pc was not set, then use the LR registers for the PC. + if (!arm.pc_set()) { + (*regs_arm)[ARM_REG_PC] = (*regs_arm)[ARM_REG_LR]; + } + (*regs_arm)[ARM_REG_SP] = arm.cfa(); + return_value = true; + + // If the pc was set to zero, consider this the final frame. + *finished = (regs_arm->pc() == 0) ? true : false; + } + + if (arm.status() == ARM_STATUS_NO_UNWIND) { + *finished = true; + return true; + } + + if (!return_value) { + switch (arm.status()) { + case ARM_STATUS_NONE: + case ARM_STATUS_NO_UNWIND: + case ARM_STATUS_FINISH: + last_error_.code = ERROR_NONE; + break; + + case ARM_STATUS_RESERVED: + case ARM_STATUS_SPARE: + case ARM_STATUS_TRUNCATED: + case ARM_STATUS_MALFORMED: + case ARM_STATUS_INVALID_ALIGNMENT: + case ARM_STATUS_INVALID_PERSONALITY: + last_error_.code = ERROR_UNWIND_INFO; + break; + + case ARM_STATUS_READ_FAILED: + last_error_.code = ERROR_MEMORY_INVALID; + last_error_.address = arm.status_address(); + break; + } + } + return return_value; +} + +bool ElfInterfaceArm::GetFunctionName(uint64_t addr, SharedString* name, uint64_t* offset) { + // For ARM, thumb function symbols have bit 0 set, but the address passed + // in here might not have this bit set and result in a failure to find + // the thumb function names. Adjust the address and offset to account + // for this possible case. + if (ElfInterface32::GetFunctionName(addr | 1, name, offset)) { + *offset &= ~1; + return true; + } + return false; +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.h new file mode 100644 index 0000000000..6ee6dc984d --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ElfInterfaceArm.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include + +#include +#include + +namespace unwindstack { + +class ElfInterfaceArm : public ElfInterface32 { + public: + ElfInterfaceArm(Memory* memory) : ElfInterface32(memory) {} + virtual ~ElfInterfaceArm() = default; + + class iterator : public std::iterator { + public: + iterator(ElfInterfaceArm* interface, size_t index) : interface_(interface), index_(index) { } + + iterator& operator++() { index_++; return *this; } + iterator& operator++(int increment) { index_ += increment; return *this; } + iterator& operator--() { index_--; return *this; } + iterator& operator--(int decrement) { index_ -= decrement; return *this; } + + bool operator==(const iterator& rhs) { return this->index_ == rhs.index_; } + bool operator!=(const iterator& rhs) { return this->index_ != rhs.index_; } + + uint32_t operator*() { + uint32_t addr = interface_->addrs_[index_]; + if (addr == 0) { + if (!interface_->GetPrel31Addr(interface_->start_offset_ + index_ * 8, &addr)) { + return 0; + } + interface_->addrs_[index_] = addr; + } + return addr; + } + + private: + ElfInterfaceArm* interface_ = nullptr; + size_t index_ = 0; + }; + + iterator begin() { return iterator(this, 0); } + iterator end() { return iterator(this, total_entries_); } + + bool Init(int64_t* section_bias) override; + + bool GetPrel31Addr(uint32_t offset, uint32_t* addr); + + bool FindEntry(uint32_t pc, uint64_t* entry_offset); + + void HandleUnknownType(uint32_t type, uint64_t ph_offset, uint64_t ph_filesz) override; + + bool Step(uint64_t pc, Regs* regs, Memory* process_memory, bool* finished, + bool* is_signal_frame) override; + + bool StepExidx(uint64_t pc, Regs* regs, Memory* process_memory, bool* finished); + + bool GetFunctionName(uint64_t addr, SharedString* name, uint64_t* offset) override; + + uint64_t start_offset() { return start_offset_; } + + size_t total_entries() { return total_entries_; } + + void set_load_bias(uint64_t load_bias) { load_bias_ = load_bias; } + + protected: + uint64_t start_offset_ = 0; + size_t total_entries_ = 0; + uint64_t load_bias_ = 0; + + std::unordered_map addrs_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Global.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Global.cpp new file mode 100644 index 0000000000..0183bd3182 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Global.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +namespace unwindstack { + +Global::Global(std::shared_ptr& memory) : memory_(memory) {} +Global::Global(std::shared_ptr& memory, std::vector& search_libs) + : memory_(memory), search_libs_(search_libs) {} + +void Global::SetArch(ArchEnum arch) { + if (arch_ == ARCH_UNKNOWN) { + arch_ = arch; + ProcessArch(); + } +} + +bool Global::Searchable(const std::string& name) { + if (search_libs_.empty()) { + return true; + } + + if (name.empty()) { + return false; + } + + std::string base_name = android::base::Basename(name); + for (const std::string& lib : search_libs_) { + if (base_name == lib) { + return true; + } + } + return false; +} + +void Global::FindAndReadVariable(Maps* maps, const char* var_str) { + std::string variable(var_str); + // When looking for global variables, do not arbitrarily search every + // readable map. Instead look for a specific pattern that must exist. + // The pattern should be a readable map, followed by a read-write + // map with a non-zero offset. + // For example: + // f0000-f1000 0 r-- /system/lib/libc.so + // f1000-f2000 1000 r-x /system/lib/libc.so + // f2000-f3000 2000 rw- /system/lib/libc.so + // This also works: + // f0000-f2000 0 r-- /system/lib/libc.so + // f2000-f3000 2000 rw- /system/lib/libc.so + // It is also possible to see empty maps after the read-only like so: + // f0000-f1000 0 r-- /system/lib/libc.so + // f1000-f2000 0 --- + // f2000-f3000 1000 r-x /system/lib/libc.so + // f3000-f4000 2000 rw- /system/lib/libc.so + MapInfo* map_zero = nullptr; + for (const auto& info : *maps) { + if ((info->flags() & (PROT_READ | PROT_WRITE)) == (PROT_READ | PROT_WRITE) && + map_zero != nullptr && Searchable(info->name()) && info->name() == map_zero->name()) { + Elf* elf = map_zero->GetElf(memory_, arch()); + uint64_t ptr; + if (elf->GetGlobalVariableOffset(variable, &ptr) && ptr != 0) { + uint64_t offset_end = info->offset() + info->end() - info->start(); + if (ptr >= info->offset() && ptr < offset_end) { + ptr = info->start() + ptr - info->offset(); + if (ReadVariableData(ptr)) { + break; + } + } + } + } else if (info->offset() == 0 && !info->name().empty()) { + map_zero = info.get(); + } + } +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/GlobalDebugImpl.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/GlobalDebugImpl.h new file mode 100644 index 0000000000..f83a2c6610 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/GlobalDebugImpl.h @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include + +#include "Check.h" +#include "GlobalDebugInterface.h" +#include "MemoryCache.h" +#include "MemoryRange.h" + +// This implements the JIT Compilation Interface. +// See https://sourceware.org/gdb/onlinedocs/gdb/JIT-Interface.html +// +// We use it to get in-memory ELF files created by the ART compiler, +// but we also use it to get list of DEX files used by the runtime. + +namespace unwindstack { + +// Implementation templated for ELF/DEX and for different architectures. +template +class GlobalDebugImpl : public GlobalDebugInterface, public Global { + public: + static constexpr int kMaxRaceRetries = 16; + static constexpr int kMaxHeadRetries = 16; + static constexpr uint8_t kMagic[8] = {'A', 'n', 'd', 'r', 'o', 'i', 'd', '2'}; + + struct JITCodeEntry { + Uintptr_T next; + Uintptr_T prev; + Uintptr_T symfile_addr; + Uint64_T symfile_size; + // Android-specific fields: + Uint64_T timestamp; + uint32_t seqlock; + }; + + static constexpr size_t kSizeOfCodeEntryV1 = offsetof(JITCodeEntry, timestamp); + static constexpr size_t kSizeOfCodeEntryV2 = sizeof(JITCodeEntry); + + struct JITDescriptor { + uint32_t version; + uint32_t action_flag; + Uintptr_T relevant_entry; + Uintptr_T first_entry; + // Android-specific fields: + uint8_t magic[8]; + uint32_t flags; + uint32_t sizeof_descriptor; + uint32_t sizeof_entry; + uint32_t seqlock; + Uint64_T timestamp; + }; + + static constexpr size_t kSizeOfDescriptorV1 = offsetof(JITDescriptor, magic); + static constexpr size_t kSizeOfDescriptorV2 = sizeof(JITDescriptor); + + // This uniquely identifies entry in presence of concurrent modifications. + // Each (address,seqlock) pair is unique for each newly created JIT entry. + struct UID { + uint64_t address; // Address of JITCodeEntry in memory. + uint32_t seqlock; // This servers as "version" for the given address. + + bool operator<(const UID& other) const { + return std::tie(address, seqlock) < std::tie(other.address, other.seqlock); + } + }; + + GlobalDebugImpl(ArchEnum arch, std::shared_ptr& memory, + std::vector& search_libs, const char* global_variable_name) + : Global(memory, search_libs), global_variable_name_(global_variable_name) { + SetArch(arch); + } + + bool ReadDescriptor(uint64_t addr) { + JITDescriptor desc{}; + // Try to read the full descriptor including Android-specific fields. + if (!this->memory_->ReadFully(addr, &desc, kSizeOfDescriptorV2)) { + // Fallback to just the minimal descriptor. + // This will make the magic check below fail. + if (!this->memory_->ReadFully(addr, &desc, kSizeOfDescriptorV1)) { + return false; + } + } + + if (desc.version != 1 || desc.first_entry == 0) { + // Either unknown version, or no jit entries. + return false; + } + + // Check if there are extra Android-specific fields. + if (memcmp(desc.magic, kMagic, sizeof(kMagic)) == 0) { + jit_entry_size_ = kSizeOfCodeEntryV2; + seqlock_offset_ = offsetof(JITCodeEntry, seqlock); + } else { + jit_entry_size_ = kSizeOfCodeEntryV1; + seqlock_offset_ = 0; + } + descriptor_addr_ = addr; + return true; + } + + void ProcessArch() {} + + bool ReadVariableData(uint64_t ptr) { return ReadDescriptor(ptr); } + + // Invoke callback for all symfiles that contain the given PC. + // Returns true if any callback returns true (which also aborts the iteration). + template bool */> + bool ForEachSymfile(Maps* maps, uint64_t pc, Callback callback) { + // Use a single lock, this object should be used so infrequently that + // a fine grain lock is unnecessary. + std::lock_guard guard(lock_); + if (descriptor_addr_ == 0) { + FindAndReadVariable(maps, global_variable_name_); + if (descriptor_addr_ == 0) { + return false; + } + } + + // Try to find the entry in already loaded symbol files. + for (auto& it : entries_) { + Symfile* symfile = it.second.get(); + // Check seqlock to make sure that entry is still valid (it may be very old). + if (symfile->IsValidPc(pc) && CheckSeqlock(it.first) && callback(symfile)) { + return true; + } + } + + // Update all entries and retry. + ReadAllEntries(maps); + for (auto& it : entries_) { + Symfile* symfile = it.second.get(); + // Note that the entry could become invalid since the ReadAllEntries above, + // but that is ok. We don't want to fail or refresh the entries yet again. + // This is as if we found the entry in time and it became invalid after return. + // This is relevant when ART moves/packs JIT entries. That is, the entry is + // technically deleted, but only because it was copied into merged uber-entry. + // So the JIT method is still alive and the deleted data is still correct. + if (symfile->IsValidPc(pc) && callback(symfile)) { + return true; + } + } + + return false; + } + + bool GetFunctionName(Maps* maps, uint64_t pc, SharedString* name, uint64_t* offset) { + // NB: If symfiles overlap in PC ranges, this will check all of them. + return ForEachSymfile(maps, pc, [pc, name, offset](Symfile* file) { + return file->GetFunctionName(pc, name, offset); + }); + } + + Symfile* Find(Maps* maps, uint64_t pc) { + // NB: If symfiles overlap in PC ranges (which can happen for both ELF and DEX), + // this will check all of them and return one that also has a matching function. + Symfile* result = nullptr; + bool found = ForEachSymfile(maps, pc, [pc, &result](Symfile* file) { + result = file; + SharedString name; + uint64_t offset; + return file->GetFunctionName(pc, &name, &offset); + }); + if (found) { + return result; // Found symfile with symbol that also matches the PC. + } + // There is no matching symbol, so return any symfile for which the PC is valid. + // This is a useful fallback for tests, which often have symfiles with no functions. + return result; + } + + // Read all entries from the process and cache them locally. + // The linked list might be concurrently modified. We detect races and retry. + bool ReadAllEntries(Maps* maps) { + for (int i = 0; i < kMaxRaceRetries; i++) { + bool race = false; + if (!ReadAllEntries(maps, &race)) { + if (race) { + continue; // Retry due to concurrent modification of the linked list. + } + return false; // Failed to read entries. + } + return true; // Success. + } + return false; // Too many retries. + } + + // Read all JIT entries while assuming there might be concurrent modifications. + // If there is a race, the method will fail and the caller should retry the call. + bool ReadAllEntries(Maps* maps, bool* race) { + // New entries might be added while we iterate over the linked list. + // In particular, an entry could be effectively moved from end to start due to + // the ART repacking algorithm, which groups smaller entries into a big one. + // Therefore keep reading the most recent entries until we reach a fixed point. + std::map> entries; + for (size_t i = 0; i < kMaxHeadRetries; i++) { + size_t old_size = entries.size(); + if (!ReadNewEntries(maps, &entries, race)) { + return false; + } + if (entries.size() == old_size) { + entries_.swap(entries); + return true; + } + } + return false; // Too many retries. + } + + // Read new JIT entries (head of linked list) until we find one that we have seen before. + // This method uses seqlocks extensively to ensure safety in case of concurrent modifications. + bool ReadNewEntries(Maps* maps, std::map>* entries, bool* race) { + // Read the address of the head entry in the linked list. + UID uid; + if (!ReadNextField(descriptor_addr_ + offsetof(JITDescriptor, first_entry), &uid, race)) { + return false; + } + + // Follow the linked list. + while (uid.address != 0) { + // Check if we have reached an already cached entry (we restart from head repeatedly). + if (entries->count(uid) != 0) { + return true; + } + + // Read the entry. + JITCodeEntry data{}; + if (!memory_->ReadFully(uid.address, &data, jit_entry_size_)) { + return false; + } + data.symfile_addr = StripAddressTag(data.symfile_addr); + + // Check the seqlock to verify the symfile_addr and symfile_size. + if (!CheckSeqlock(uid, race)) { + return false; + } + + // Copy and load the symfile. + auto it = entries_.find(uid); + if (it != entries_.end()) { + // The symfile was already loaded - just copy the reference. + entries->emplace(uid, it->second); + } else if (data.symfile_addr != 0) { + std::shared_ptr symfile; + bool ok = this->Load(maps, memory_, data.symfile_addr, data.symfile_size.value, symfile); + // Check seqlock first because load can fail due to race (so we want to trigger retry). + // TODO: Extract the memory copy code before the load, so that it is immune to races. + if (!CheckSeqlock(uid, race)) { + return false; // The ELF/DEX data was removed before we loaded it. + } + // Exclude symbol files that fail to load (but continue loading other files). + if (ok) { + entries->emplace(uid, symfile); + } + } + + // Go to next entry. + UID next_uid; + if (!ReadNextField(uid.address + offsetof(JITCodeEntry, next), &next_uid, race)) { + return false; // The next pointer was modified while we were reading it. + } + if (!CheckSeqlock(uid, race)) { + return false; // This entry was deleted before we moved to the next one. + } + uid = next_uid; + } + + return true; + } + + // Read the address and seqlock of entry from the next field of linked list. + // This is non-trivial since they need to be consistent (as if we read both atomically). + // + // We're reading pointers, which can point at heap-allocated structures (the + // case for the __dex_debug_descriptor pointers at the time of writing). + // On 64 bit systems, the target process might have top-byte heap pointer + // tagging enabled, so we need to mask out the tag. We also know that the + // address must point to userspace, so the top byte of the address must be + // zero on both x64 and aarch64 without tagging. Therefore the masking can be + // done unconditionally. + bool ReadNextField(uint64_t next_field_addr, UID* uid, bool* race) { + Uintptr_T address[2]{0, 0}; + uint32_t seqlock[2]{0, 0}; + // Read all data twice: address[0], seqlock[0], address[1], seqlock[1]. + for (int i = 0; i < 2; i++) { + std::atomic_thread_fence(std::memory_order_acquire); + if (!(memory_->ReadFully(next_field_addr, &address[i], sizeof(address[i])))) { + return false; + } + address[i] = StripAddressTag(address[i]); + if (seqlock_offset_ == 0) { + // There is no seqlock field. + *uid = UID{.address = address[0], .seqlock = 0}; + return true; + } + if (address[i] != 0) { + std::atomic_thread_fence(std::memory_order_acquire); + if (!memory_->ReadFully(address[i] + seqlock_offset_, &seqlock[i], sizeof(seqlock[i]))) { + return false; + } + } + } + // Check that both reads returned identical values, and that the entry is live. + if (address[0] != address[1] || seqlock[0] != seqlock[1] || (seqlock[0] & 1) == 1) { + *race = true; + return false; + } + // Since address[1] is sandwiched between two seqlock reads, we know that + // at the time of address[1] read, the entry had the given seqlock value. + *uid = UID{.address = address[1], .seqlock = seqlock[1]}; + return true; + } + + // Check that the given entry has not been deleted (or replaced by new entry at same address). + bool CheckSeqlock(UID uid, bool* race = nullptr) { + if (seqlock_offset_ == 0) { + // There is no seqlock field. + return true; + } + // This is required for memory synchronization if the we are working with local memory. + // For other types of memory (e.g. remote) this is no-op and has no significant effect. + std::atomic_thread_fence(std::memory_order_acquire); + uint32_t seen_seqlock; + if (!memory_->Read32(uid.address + seqlock_offset_, &seen_seqlock)) { + return false; + } + if (seen_seqlock != uid.seqlock) { + if (race != nullptr) { + *race = true; + } + return false; + } + return true; + } + + // AArch64 has Address tagging (aka Top Byte Ignore) feature, which is used by + // HWASAN and MTE to store metadata in the address. We need to remove the tag. + Uintptr_T StripAddressTag(Uintptr_T addr) { + if (arch() == ARCH_ARM64) { + // Make the value signed so it will be sign extended if necessary. + return static_cast((static_cast(addr) << 8) >> 8); + } + return addr; + } + + private: + const char* global_variable_name_ = nullptr; + uint64_t descriptor_addr_ = 0; // Non-zero if we have found (non-empty) descriptor. + uint32_t jit_entry_size_ = 0; + uint32_t seqlock_offset_ = 0; + std::map> entries_; // Cached loaded entries. + + std::mutex lock_; +}; + +// uint64_t values on x86 are not naturally aligned, +// but uint64_t values on ARM are naturally aligned. +struct Uint64_P { + uint64_t value; +} __attribute__((packed)); +struct Uint64_A { + uint64_t value; +} __attribute__((aligned(8))); + +template +std::unique_ptr> CreateGlobalDebugImpl( + ArchEnum arch, std::shared_ptr& memory, std::vector search_libs, + const char* global_variable_name) { + CHECK(arch != ARCH_UNKNOWN); + + // The interface needs to see real-time changes in memory for synchronization with the + // concurrently running ART JIT compiler. Skip caching and read the memory directly. + std::shared_ptr jit_memory; + MemoryCacheBase* cached_memory = memory->AsMemoryCacheBase(); + if (cached_memory != nullptr) { + jit_memory = cached_memory->UnderlyingMemory(); + } else { + jit_memory = memory; + } + + switch (arch) { + case ARCH_X86: { + using Impl = GlobalDebugImpl; + static_assert(offsetof(typename Impl::JITCodeEntry, symfile_size) == 12, "layout"); + static_assert(offsetof(typename Impl::JITCodeEntry, seqlock) == 28, "layout"); + static_assert(sizeof(typename Impl::JITCodeEntry) == 32, "layout"); + static_assert(sizeof(typename Impl::JITDescriptor) == 48, "layout"); + return std::make_unique(arch, jit_memory, search_libs, global_variable_name); + } + case ARCH_ARM: { + using Impl = GlobalDebugImpl; + static_assert(offsetof(typename Impl::JITCodeEntry, symfile_size) == 16, "layout"); + static_assert(offsetof(typename Impl::JITCodeEntry, seqlock) == 32, "layout"); + static_assert(sizeof(typename Impl::JITCodeEntry) == 40, "layout"); + static_assert(sizeof(typename Impl::JITDescriptor) == 48, "layout"); + return std::make_unique(arch, jit_memory, search_libs, global_variable_name); + } + case ARCH_ARM64: + case ARCH_X86_64: { + using Impl = GlobalDebugImpl; + static_assert(offsetof(typename Impl::JITCodeEntry, symfile_size) == 24, "layout"); + static_assert(offsetof(typename Impl::JITCodeEntry, seqlock) == 40, "layout"); + static_assert(sizeof(typename Impl::JITCodeEntry) == 48, "layout"); + static_assert(sizeof(typename Impl::JITDescriptor) == 56, "layout"); + return std::make_unique(arch, jit_memory, search_libs, global_variable_name); + } + default: + abort(); + } +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/JitDebug.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/JitDebug.cpp new file mode 100644 index 0000000000..1aebcf5f04 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/JitDebug.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include "GlobalDebugImpl.h" +#include "MemoryBuffer.h" + +namespace unwindstack { + +template <> +bool GlobalDebugInterface::Load(Maps*, std::shared_ptr& memory, uint64_t addr, + uint64_t size, /*out*/ std::shared_ptr& elf) { + std::unique_ptr copy(new MemoryBuffer()); + if (!copy->Resize(size) || !memory->ReadFully(addr, copy->GetPtr(0), size)) { + return false; + } + elf.reset(new Elf(copy.release())); + return elf->Init() && elf->valid(); +} + +std::unique_ptr CreateJitDebug(ArchEnum arch, std::shared_ptr& memory, + std::vector search_libs) { + return CreateGlobalDebugImpl(arch, memory, search_libs, "__jit_debug_descriptor"); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LICENSE b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LICENSE new file mode 100644 index 0000000000..23b0dd3b8c --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LICENSE @@ -0,0 +1,232 @@ +Android Code +Copyright 2005-2019 The Android Open Source Project +Copyright 2019 Functional Software Inc. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + + +UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE + +Unicode Data Files include all data files under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +and http://www.unicode.org/cldr/data/ . Unicode Software includes any +source code published in the Unicode Standard or under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, and +http://www.unicode.org/cldr/data/. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA +FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY +ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF +THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, +DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2008 Unicode, Inc. All rights reserved. Distributed +under the Terms of Use in http://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation (the +"Data Files") or Unicode software and any associated documentation (the +"Software") to deal in the Data Files or Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, and/or sell copies of the Data Files or Software, +and to permit persons to whom the Data Files or Software are furnished to +do so, provided that (a) the above copyright notice(s) and this permission +notice appear with all copies of the Data Files or Software, (b) both the +above copyright notice(s) and this permission notice appear in associated +documentation, and (c) there is clear notice in each modified Data File +or in the Software as well as in the documentation associated with the +Data File(s) or Software that the data or software has been modified. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS +INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT +OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE +OR PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, use +or other dealings in these Data Files or Software without prior written +authorization of the copyright holder. diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Log.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Log.cpp new file mode 100644 index 0000000000..e36c7349eb --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Log.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#define LOG_TAG "unwind" +#include "android-base/log_main.h" + +#include "android-base/stringprintf.h" + +#include + +namespace unwindstack { + +static bool g_print_to_stdout = false; + +void log_to_stdout(bool enable) { + g_print_to_stdout = enable; +} + +// Send the data to the log. +void log(uint8_t indent, const char* format, ...) { + std::string real_format; + if (indent > 0) { + real_format = android::base::StringPrintf("%*s%s", 2 * indent, " ", format); + } else { + real_format = format; + } + va_list args; + va_start(args, format); + if (g_print_to_stdout) { + real_format += '\n'; + vprintf(real_format.c_str(), args); + } else { + LOG_PRI_VA(ANDROID_LOG_INFO, LOG_TAG, real_format.c_str(), args); + } + va_end(args); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LogAndroid.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LogAndroid.cpp new file mode 100644 index 0000000000..bee6c3ec62 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/LogAndroid.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#define LOG_TAG "unwind" +#include + +#include + +#include +#include "android-base/log_main.h" + +namespace unwindstack { + +namespace Log { + +// Send the data to the log. +static void LogWithPriority(int priority, uint8_t indent, const char* format, va_list args) { + std::string real_format; + if (indent > 0) { + real_format = android::base::StringPrintf("%*s%s", 2 * indent, " ", format); + } else { + real_format = format; + } + LOG_PRI_VA(priority, LOG_TAG, real_format.c_str(), args); +} + +void Info(const char* format, ...) { + va_list args; + va_start(args, format); + LogWithPriority(ANDROID_LOG_INFO, 0, format, args); + va_end(args); +} + +void Info(uint8_t indent, const char* format, ...) { + va_list args; + va_start(args, format); + LogWithPriority(ANDROID_LOG_INFO, indent, format, args); + va_end(args); +} + +void Error(const char* format, ...) { + va_list args; + va_start(args, format); + LogWithPriority(ANDROID_LOG_ERROR, 0, format, args); + va_end(args); +} + +void AsyncSafe(const char* format, ...) { + va_list args; + va_start(args, format); + vprintf(format, args); + printf("\n"); + va_end(args); +} + +} // namespace Log + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MapInfo.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MapInfo.cpp new file mode 100644 index 0000000000..6db118389b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MapInfo.cpp @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#include "MemoryFileAtOffset.h" +#include "MemoryRange.h" + +namespace unwindstack { + +bool MapInfo::ElfFileNotReadable() { + const std::string& map_name = name(); + return memory_backed_elf() && !map_name.empty() && map_name[0] != '[' && + !android::base::StartsWith(map_name, "/memfd:"); +} + +std::shared_ptr MapInfo::GetPrevRealMap() { + if (name().empty()) { + return nullptr; + } + + for (auto prev = prev_map(); prev != nullptr; prev = prev->prev_map()) { + if (!prev->IsBlank()) { + if (prev->name() == name()) { + return prev; + } + return nullptr; + } + } + return nullptr; +} + +std::shared_ptr MapInfo::GetNextRealMap() { + if (name().empty()) { + return nullptr; + } + + for (auto next = next_map(); next != nullptr; next = next->next_map()) { + if (!next->IsBlank()) { + if (next->name() == name()) { + return next; + } + return nullptr; + } + } + return nullptr; +} + +bool MapInfo::InitFileMemoryFromPreviousReadOnlyMap(MemoryFileAtOffset* memory) { + // One last attempt, see if the previous map is read-only with the + // same name and stretches across this map. + auto prev_real_map = GetPrevRealMap(); + if (prev_real_map == nullptr || prev_real_map->flags() != PROT_READ || + prev_real_map->offset() >= offset()) { + return false; + } + + uint64_t map_size = end() - prev_real_map->end(); + if (!memory->Init(name(), prev_real_map->offset(), map_size)) { + return false; + } + + uint64_t max_size; + if (!Elf::GetInfo(memory, &max_size) || max_size < map_size) { + return false; + } + + if (!memory->Init(name(), prev_real_map->offset(), max_size)) { + return false; + } + + set_elf_offset(offset() - prev_real_map->offset()); + set_elf_start_offset(prev_real_map->offset()); + return true; +} + +Memory* MapInfo::GetFileMemory() { + // Fail on device maps. + if (flags() & MAPS_FLAGS_DEVICE_MAP) { + return nullptr; + } + + std::unique_ptr memory(new MemoryFileAtOffset); + if (offset() == 0) { + if (memory->Init(name(), 0)) { + return memory.release(); + } + return nullptr; + } + + // These are the possibilities when the offset is non-zero. + // - There is an elf file embedded in a file, and the offset is the + // the start of the elf in the file. + // - There is an elf file embedded in a file, and the offset is the + // the start of the executable part of the file. The actual start + // of the elf is in the read-only segment preceeding this map. + // - The whole file is an elf file, and the offset needs to be saved. + // + // Map in just the part of the file for the map. If this is not + // a valid elf, then reinit as if the whole file is an elf file. + // If the offset is a valid elf, then determine the size of the map + // and reinit to that size. This is needed because the dynamic linker + // only maps in a portion of the original elf, and never the symbol + // file data. + // + // For maps with MAPS_FLAGS_JIT_SYMFILE_MAP, the map range is for a JIT function, + // which can be smaller than elf header size. So make sure map_size is large enough + // to read elf header. + uint64_t map_size = std::max(end() - start(), sizeof(ElfTypes64::Ehdr)); + if (!memory->Init(name(), offset(), map_size)) { + return nullptr; + } + + // Check if the start of this map is an embedded elf. + uint64_t max_size = 0; + if (Elf::GetInfo(memory.get(), &max_size)) { + set_elf_start_offset(offset()); + if (max_size > map_size) { + if (memory->Init(name(), offset(), max_size)) { + return memory.release(); + } + // Try to reinit using the default map_size. + if (memory->Init(name(), offset(), map_size)) { + return memory.release(); + } + set_elf_start_offset(0); + return nullptr; + } + return memory.release(); + } + + // No elf at offset, try to init as if the whole file is an elf. + if (memory->Init(name(), 0) && Elf::IsValidElf(memory.get())) { + set_elf_offset(offset()); + return memory.release(); + } + + // See if the map previous to this one contains a read-only map + // that represents the real start of the elf data. + if (InitFileMemoryFromPreviousReadOnlyMap(memory.get())) { + return memory.release(); + } + + // Failed to find elf at start of file or at read-only map, return + // file object from the current map. + if (memory->Init(name(), offset(), map_size)) { + return memory.release(); + } + return nullptr; +} + +Memory* MapInfo::CreateMemory(const std::shared_ptr& process_memory) { + if (end() <= start()) { + return nullptr; + } + + set_elf_offset(0); + + // Fail on device maps. + if (flags() & MAPS_FLAGS_DEVICE_MAP) { + return nullptr; + } + + // First try and use the file associated with the info. + if (!name().empty()) { + Memory* memory = GetFileMemory(); + if (memory != nullptr) { + return memory; + } + } + + if (process_memory == nullptr) { + return nullptr; + } + + set_memory_backed_elf(true); + + // Need to verify that this elf is valid. It's possible that + // only part of the elf file to be mapped into memory is in the executable + // map. In this case, there will be another read-only map that includes the + // first part of the elf file. This is done if the linker rosegment + // option is used. + std::unique_ptr memory(new MemoryRange(process_memory, start(), end() - start(), 0)); + if (Elf::IsValidElf(memory.get())) { + set_elf_start_offset(offset()); + + auto next_real_map = GetNextRealMap(); + + // Might need to peek at the next map to create a memory object that + // includes that map too. + if (offset() != 0 || next_real_map == nullptr || offset() >= next_real_map->offset()) { + return memory.release(); + } + + // There is a possibility that the elf object has already been created + // in the next map. Since this should be a very uncommon path, just + // redo the work. If this happens, the elf for this map will eventually + // be discarded. + MemoryRanges* ranges = new MemoryRanges; + ranges->Insert(new MemoryRange(process_memory, start(), end() - start(), 0)); + ranges->Insert(new MemoryRange(process_memory, next_real_map->start(), + next_real_map->end() - next_real_map->start(), + next_real_map->offset() - offset())); + + return ranges; + } + + auto prev_real_map = GetPrevRealMap(); + + // Find the read-only map by looking at the previous map. The linker + // doesn't guarantee that this invariant will always be true. However, + // if that changes, there is likely something else that will change and + // break something. + if (offset() == 0 || prev_real_map == nullptr || prev_real_map->offset() >= offset()) { + set_memory_backed_elf(false); + return nullptr; + } + + // Make sure that relative pc values are corrected properly. + set_elf_offset(offset() - prev_real_map->offset()); + // Use this as the elf start offset, otherwise, you always get offsets into + // the r-x section, which is not quite the right information. + set_elf_start_offset(prev_real_map->offset()); + + std::unique_ptr ranges(new MemoryRanges); + if (!ranges->Insert(new MemoryRange(process_memory, prev_real_map->start(), + prev_real_map->end() - prev_real_map->start(), 0))) { + return nullptr; + } + if (!ranges->Insert(new MemoryRange(process_memory, start(), end() - start(), elf_offset()))) { + return nullptr; + } + return ranges.release(); +} + +class ScopedElfCacheLock { + public: + ScopedElfCacheLock() { + if (Elf::CachingEnabled()) Elf::CacheLock(); + } + ~ScopedElfCacheLock() { + if (Elf::CachingEnabled()) Elf::CacheUnlock(); + } +}; + +Elf* MapInfo::GetElf(const std::shared_ptr& process_memory, ArchEnum expected_arch) { + // Make sure no other thread is trying to add the elf to this map. + std::lock_guard guard(elf_mutex()); + + if (elf().get() != nullptr) { + return elf().get(); + } + + ScopedElfCacheLock elf_cache_lock; + if (Elf::CachingEnabled() && !name().empty()) { + if (Elf::CacheGet(this)) { + return elf().get(); + } + } + + elf().reset(new Elf(CreateMemory(process_memory))); + // If the init fails, keep the elf around as an invalid object so we + // don't try to reinit the object. + elf()->Init(); + if (elf()->valid() && expected_arch != elf()->arch()) { + // Make the elf invalid, mismatch between arch and expected arch. + elf()->Invalidate(); + } + + if (!elf()->valid()) { + set_elf_start_offset(offset()); + } else if (auto prev_real_map = GetPrevRealMap(); prev_real_map != nullptr && + prev_real_map->flags() == PROT_READ && + prev_real_map->offset() < offset()) { + // If there is a read-only map then a read-execute map that represents the + // same elf object, make sure the previous map is using the same elf + // object if it hasn't already been set. Locking this should not result + // in a deadlock as long as the invariant that the code only ever tries + // to lock the previous real map holds true. + std::lock_guard guard(prev_real_map->elf_mutex()); + if (prev_real_map->elf() == nullptr) { + // Need to verify if the map is the previous read-only map. + prev_real_map->set_elf(elf()); + prev_real_map->set_memory_backed_elf(memory_backed_elf()); + prev_real_map->set_elf_start_offset(elf_start_offset()); + prev_real_map->set_elf_offset(prev_real_map->offset() - elf_start_offset()); + } else if (prev_real_map->elf_start_offset() == elf_start_offset()) { + // Discard this elf, and use the elf from the previous map instead. + set_elf(prev_real_map->elf()); + } + } + + // Cache the elf only after all of the above checks since we might + // discard the original elf we created. + if (Elf::CachingEnabled()) { + Elf::CacheAdd(this); + } + return elf().get(); +} + +bool MapInfo::GetFunctionName(uint64_t addr, SharedString* name, uint64_t* func_offset) { + { + // Make sure no other thread is trying to update this elf object. + std::lock_guard guard(elf_mutex()); + if (elf() == nullptr) { + return false; + } + } + // No longer need the lock, once the elf object is created, it is not deleted + // until this object is deleted. + return elf()->GetFunctionName(addr, name, func_offset); +} + +uint64_t MapInfo::GetLoadBias() { + uint64_t cur_load_bias = load_bias().load(); + if (cur_load_bias != UINT64_MAX) { + return cur_load_bias; + } + + Elf* elf_obj = GetElfObj(); + if (elf_obj == nullptr) { + return UINT64_MAX; + } + + if (elf_obj->valid()) { + cur_load_bias = elf_obj->GetLoadBias(); + set_load_bias(cur_load_bias); + return cur_load_bias; + } + + set_load_bias(0); + return 0; +} + +uint64_t MapInfo::GetLoadBias(const std::shared_ptr& process_memory) { + uint64_t cur_load_bias = GetLoadBias(); + if (cur_load_bias != UINT64_MAX) { + return cur_load_bias; + } + + // Call lightweight static function that will only read enough of the + // elf data to get the load bias. + std::unique_ptr memory(CreateMemory(process_memory)); + cur_load_bias = Elf::GetLoadBias(memory.get()); + set_load_bias(cur_load_bias); + return cur_load_bias; +} + +MapInfo::~MapInfo() { + ElfFields* elf_fields = elf_fields_.load(); + if (elf_fields != nullptr) { + delete elf_fields->build_id_.load(); + delete elf_fields; + } +} + +std::string MapInfo::GetFullName() { + Elf* elf_obj = GetElfObj(); + if (elf_obj == nullptr || elf_start_offset() == 0 || name().empty()) { + return name(); + } + + std::string soname = elf_obj->GetSoname(); + if (soname.empty()) { + return name(); + } + + std::string full_name(name()); + full_name += '!'; + full_name += soname; + return full_name; +} + +SharedString MapInfo::GetBuildID() { + SharedString* id = build_id().load(); + if (id != nullptr) { + return *id; + } + + // No need to lock, at worst if multiple threads do this at the same + // time it should be detected and only one thread should win and + // save the data. + + std::string result; + Elf* elf_obj = GetElfObj(); + if (elf_obj != nullptr) { + result = elf_obj->GetBuildID(); + } else { + // This will only work if we can get the file associated with this memory. + // If this is only available in memory, then the section name information + // is not present and we will not be able to find the build id info. + std::unique_ptr memory(GetFileMemory()); + if (memory != nullptr) { + result = Elf::GetBuildID(memory.get()); + } + } + return SetBuildID(std::move(result)); +} + +SharedString MapInfo::SetBuildID(std::string&& new_build_id) { + std::unique_ptr new_build_id_ptr(new SharedString(std::move(new_build_id))); + SharedString* expected_id = nullptr; + // Strong version since we need to reliably return the stored pointer. + if (build_id().compare_exchange_strong(expected_id, new_build_id_ptr.get())) { + // Value saved, so make sure the memory is not freed. + return *new_build_id_ptr.release(); + } else { + // The expected value is set to the stored value on failure. + return *expected_id; + } +} + +MapInfo::ElfFields& MapInfo::GetElfFields() { + ElfFields* elf_fields = elf_fields_.load(std::memory_order_acquire); + if (elf_fields != nullptr) { + return *elf_fields; + } + // Allocate and initialize the field in thread-safe way. + std::unique_ptr desired(new ElfFields()); + ElfFields* expected = nullptr; + // Strong version is reliable. Weak version might randomly return false. + if (elf_fields_.compare_exchange_strong(expected, desired.get())) { + return *desired.release(); // Success: we transferred the pointer ownership to the field. + } else { + return *expected; // Failure: 'expected' is updated to the value set by the other thread. + } +} + +std::string MapInfo::GetPrintableBuildID() { + std::string raw_build_id = GetBuildID(); + return Elf::GetPrintableBuildID(raw_build_id); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Maps.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Maps.cpp new file mode 100644 index 0000000000..36f9d4c0ad --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Maps.cpp @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace unwindstack { + +std::shared_ptr Maps::Find(uint64_t pc) { + if (maps_.empty()) { + return nullptr; + } + size_t first = 0; + size_t last = maps_.size(); + while (first < last) { + size_t index = (first + last) / 2; + const auto& cur = maps_[index]; + if (pc >= cur->start() && pc < cur->end()) { + return cur; + } else if (pc < cur->start()) { + last = index; + } else { + first = index + 1; + } + } + return nullptr; +} + +bool Maps::Parse() { + std::shared_ptr prev_map; + return android::procinfo::ReadMapFile(GetMapsFile(), + [&](const android::procinfo::MapInfo& mapinfo) { + // Mark a device map in /dev/ and not in /dev/ashmem/ specially. + auto flags = mapinfo.flags; + if (strncmp(mapinfo.name.c_str(), "/dev/", 5) == 0 && + strncmp(mapinfo.name.c_str() + 5, "ashmem/", 7) != 0) { + flags |= unwindstack::MAPS_FLAGS_DEVICE_MAP; + } + maps_.emplace_back( + MapInfo::Create(prev_map, mapinfo.start, mapinfo.end, mapinfo.pgoff, flags, mapinfo.name)); + prev_map = maps_.back(); + }); +} + +void Maps::Add(uint64_t start, uint64_t end, uint64_t offset, uint64_t flags, + const std::string& name) { + std::shared_ptr prev_map(maps_.empty() ? nullptr : maps_.back()); + auto map_info = MapInfo::Create(prev_map, start, end, offset, flags, name); + maps_.emplace_back(std::move(map_info)); +} + +void Maps::Add(uint64_t start, uint64_t end, uint64_t offset, uint64_t flags, + const std::string& name, uint64_t load_bias) { + std::shared_ptr prev_map(maps_.empty() ? nullptr : maps_.back()); + auto map_info = MapInfo::Create(prev_map, start, end, offset, flags, name); + map_info->set_load_bias(load_bias); + maps_.emplace_back(std::move(map_info)); +} + +void Maps::Sort() { + if (maps_.empty()) { + return; + } + + std::sort(maps_.begin(), maps_.end(), + [](const std::shared_ptr& a, const std::shared_ptr& b) { + return a->start() < b->start(); + }); + + // Set prev_map and next_map on the info objects. + std::shared_ptr prev_map; + // Set the last next_map to nullptr. + maps_.back()->set_next_map(prev_map); + for (auto& map_info : maps_) { + map_info->set_prev_map(prev_map); + if (prev_map) { + prev_map->set_next_map(map_info); + } + prev_map = map_info; + } +} + +bool BufferMaps::Parse() { + std::string content(buffer_); + std::shared_ptr prev_map; + return android::procinfo::ReadMapFileContent( + &content[0], [&](const android::procinfo::MapInfo& mapinfo) { + // Mark a device map in /dev/ and not in /dev/ashmem/ specially. + auto flags = mapinfo.flags; + if (strncmp(mapinfo.name.c_str(), "/dev/", 5) == 0 && + strncmp(mapinfo.name.c_str() + 5, "ashmem/", 7) != 0) { + flags |= unwindstack::MAPS_FLAGS_DEVICE_MAP; + } + maps_.emplace_back(MapInfo::Create(prev_map, mapinfo.start, mapinfo.end, mapinfo.pgoff, + flags, mapinfo.name)); + prev_map = maps_.back(); + }); +} + +const std::string RemoteMaps::GetMapsFile() const { + return "/proc/" + std::to_string(pid_) + "/maps"; +} + +const std::string LocalUpdatableMaps::GetMapsFile() const { + return "/proc/self/maps"; +} + +LocalUpdatableMaps::LocalUpdatableMaps() : Maps() { + pthread_rwlock_init(&maps_rwlock_, nullptr); +} + +std::shared_ptr LocalUpdatableMaps::Find(uint64_t pc) { + pthread_rwlock_rdlock(&maps_rwlock_); + std::shared_ptr map_info = Maps::Find(pc); + pthread_rwlock_unlock(&maps_rwlock_); + + if (map_info == nullptr) { + pthread_rwlock_wrlock(&maps_rwlock_); + // This is guaranteed not to invalidate any previous MapInfo objects so + // we don't need to worry about any MapInfo* values already in use. + if (Reparse()) { + map_info = Maps::Find(pc); + } + pthread_rwlock_unlock(&maps_rwlock_); + } + + return map_info; +} + +bool LocalUpdatableMaps::Parse() { + pthread_rwlock_wrlock(&maps_rwlock_); + bool parsed = Maps::Parse(); + pthread_rwlock_unlock(&maps_rwlock_); + return parsed; +} + +bool LocalUpdatableMaps::Reparse(/*out*/ bool* any_changed) { + // New maps will be added at the end without deleting the old ones. + size_t last_map_idx = maps_.size(); + if (!Maps::Parse()) { + maps_.resize(last_map_idx); + return false; + } + + size_t search_map_idx = 0; + size_t num_deleted_old_entries = 0; + size_t num_deleted_new_entries = 0; + for (size_t new_map_idx = last_map_idx; new_map_idx < maps_.size(); new_map_idx++) { + auto& new_map_info = maps_[new_map_idx]; + uint64_t start = new_map_info->start(); + uint64_t end = new_map_info->end(); + uint64_t flags = new_map_info->flags(); + const SharedString& name = new_map_info->name(); + for (size_t old_map_idx = search_map_idx; old_map_idx < last_map_idx; old_map_idx++) { + auto& info = maps_[old_map_idx]; + if (start == info->start() && end == info->end() && flags == info->flags() && + name == info->name()) { + search_map_idx = old_map_idx + 1; + // Since we are throwing away a map from the new list, need to + // adjust the next/prev pointers in the old map entry. + auto prev = new_map_info->prev_map(); + auto next = new_map_info->next_map(); + info->set_prev_map(prev); + info->set_next_map(next); + + // Fix up the pointers in the prev and next entries. + if (prev != nullptr) { + prev->set_next_map(info); + } + if (next != nullptr) { + next->set_prev_map(info); + } + + maps_[new_map_idx] = nullptr; + num_deleted_new_entries++; + break; + } else if (info->start() > start) { + // Stop, there isn't going to be a match. + search_map_idx = old_map_idx; + break; + } + + // Never delete these maps, they may be in use. The assumption is + // that there will only every be a handful of these so waiting + // to destroy them is not too expensive. + // Since these are all shared_ptrs, we can just remove the references. + // Any code still holding on to the pointer, will still have a + // valid pointer after this. + search_map_idx = old_map_idx + 1; + maps_[old_map_idx] = nullptr; + num_deleted_old_entries++; + } + if (search_map_idx >= last_map_idx) { + break; + } + } + + for (size_t i = search_map_idx; i < last_map_idx; i++) { + maps_[i] = nullptr; + num_deleted_old_entries++; + } + + // Sort all of the values such that the nullptrs wind up at the end, then + // resize them away. + std::sort(maps_.begin(), maps_.end(), [](const auto& a, const auto& b) { + if (a == nullptr) { + return false; + } else if (b == nullptr) { + return true; + } + return a->start() < b->start(); + }); + maps_.resize(maps_.size() - num_deleted_old_entries - num_deleted_new_entries); + + if (any_changed != nullptr) { + *any_changed = num_deleted_old_entries != 0 || maps_.size() != last_map_idx; + } + + return true; +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Memory.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Memory.cpp new file mode 100644 index 0000000000..ac398ed0a1 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Memory.cpp @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "MemoryBuffer.h" +#include "MemoryCache.h" +#include "MemoryFileAtOffset.h" +#include "MemoryLocal.h" +#include "MemoryOffline.h" +#include "MemoryOfflineBuffer.h" +#include "MemoryRange.h" +#include "MemoryRemote.h" + +namespace unwindstack { + +static size_t ProcessVmRead(pid_t pid, uint64_t remote_src, void* dst, size_t len) { + + // Split up the remote read across page boundaries. + // From the manpage: + // A partial read/write may result if one of the remote_iov elements points to an invalid + // memory region in the remote process. + // + // Partial transfers apply at the granularity of iovec elements. These system calls won't + // perform a partial transfer that splits a single iovec element. + constexpr size_t kMaxIovecs = 64; + struct iovec src_iovs[kMaxIovecs]; + + uint64_t cur = remote_src; + size_t total_read = 0; + while (len > 0) { + struct iovec dst_iov = { + .iov_base = &reinterpret_cast(dst)[total_read], .iov_len = len, + }; + + size_t iovecs_used = 0; + while (len > 0) { + if (iovecs_used == kMaxIovecs) { + break; + } + + // struct iovec uses void* for iov_base. + if (cur >= UINTPTR_MAX) { + errno = EFAULT; + return total_read; + } + + src_iovs[iovecs_used].iov_base = reinterpret_cast(cur); + + uintptr_t misalignment = cur & (getpagesize() - 1); + size_t iov_len = getpagesize() - misalignment; + iov_len = std::min(iov_len, len); + + len -= iov_len; + if (__builtin_add_overflow(cur, iov_len, &cur)) { + errno = EFAULT; + return total_read; + } + + src_iovs[iovecs_used].iov_len = iov_len; + ++iovecs_used; + } + + ssize_t rc = syscall(SYS_process_vm_readv, pid, &dst_iov, 1, src_iovs, iovecs_used, 0); + if (rc == -1) { + return total_read; + } + total_read += rc; + } + return total_read; +} + +static bool PtraceReadLong(pid_t pid, uint64_t addr, long* value) { + // ptrace() returns -1 and sets errno when the operation fails. + // To disambiguate -1 from a valid result, we clear errno beforehand. + errno = 0; + *value = ptrace(PTRACE_PEEKTEXT, pid, reinterpret_cast(addr), nullptr); + if (*value == -1 && errno) { + return false; + } + return true; +} + +static size_t PtraceRead(pid_t pid, uint64_t addr, void* dst, size_t bytes) { + // Make sure that there is no overflow. + uint64_t max_size; + if (__builtin_add_overflow(addr, bytes, &max_size)) { + return 0; + } + + size_t bytes_read = 0; + long data; + size_t align_bytes = addr & (sizeof(long) - 1); + if (align_bytes != 0) { + if (!PtraceReadLong(pid, addr & ~(sizeof(long) - 1), &data)) { + return 0; + } + size_t copy_bytes = std::min(sizeof(long) - align_bytes, bytes); + memcpy(dst, reinterpret_cast(&data) + align_bytes, copy_bytes); + addr += copy_bytes; + dst = reinterpret_cast(reinterpret_cast(dst) + copy_bytes); + bytes -= copy_bytes; + bytes_read += copy_bytes; + } + + for (size_t i = 0; i < bytes / sizeof(long); i++) { + if (!PtraceReadLong(pid, addr, &data)) { + return bytes_read; + } + memcpy(dst, &data, sizeof(long)); + dst = reinterpret_cast(reinterpret_cast(dst) + sizeof(long)); + addr += sizeof(long); + bytes_read += sizeof(long); + } + + size_t left_over = bytes & (sizeof(long) - 1); + if (left_over) { + if (!PtraceReadLong(pid, addr, &data)) { + return bytes_read; + } + memcpy(dst, &data, left_over); + bytes_read += left_over; + } + return bytes_read; +} + +bool Memory::ReadFully(uint64_t addr, void* dst, size_t size) { + size_t rc = Read(addr, dst, size); + return rc == size; +} + +bool Memory::ReadString(uint64_t addr, std::string* dst, size_t max_read) { + char buffer[256]; // Large enough for 99% of symbol names. + size_t size = 0; // Number of bytes which were read into the buffer. + for (size_t offset = 0; offset < max_read; offset += size) { + // Look for null-terminator first, so we can allocate string of exact size. + // If we know the end of valid memory range, do the reads in larger blocks. + size_t read = std::min(sizeof(buffer), max_read - offset); + size = Read(addr + offset, buffer, read); + if (size == 0) { + return false; // We have not found end of string yet and we can not read more data. + } + size_t length = strnlen(buffer, size); // Index of the null-terminator. + if (length < size) { + // We found the null-terminator. Allocate the string and set its content. + if (offset == 0) { + // We did just single read, so the buffer already contains the whole string. + dst->assign(buffer, length); + return true; + } else { + // The buffer contains only the last block. Read the whole string again. + dst->assign(offset + length, '\0'); + return ReadFully(addr, dst->data(), dst->size()); + } + } + } + return false; +} + +std::unique_ptr Memory::CreateFileMemory(const std::string& path, uint64_t offset, + uint64_t size) { + auto memory = std::make_unique(); + + if (memory->Init(path, offset, size)) { + return memory; + } + + return nullptr; +} + +std::shared_ptr Memory::CreateProcessMemory(pid_t pid) { + if (pid == getpid()) { + return std::shared_ptr(new MemoryLocal()); + } + return std::shared_ptr(new MemoryRemote(pid)); +} + +std::shared_ptr Memory::CreateProcessMemoryCached(pid_t pid) { + if (pid == getpid()) { + return std::shared_ptr(new MemoryCache(new MemoryLocal())); + } + return std::shared_ptr(new MemoryCache(new MemoryRemote(pid))); +} + +std::shared_ptr Memory::CreateProcessMemoryThreadCached(pid_t pid) { + if (pid == getpid()) { + return std::shared_ptr(new MemoryThreadCache(new MemoryLocal())); + } + return std::shared_ptr(new MemoryThreadCache(new MemoryRemote(pid))); +} + +std::shared_ptr Memory::CreateOfflineMemory(const uint8_t* data, uint64_t start, + uint64_t end) { + return std::shared_ptr(new MemoryOfflineBuffer(data, start, end)); +} + +size_t MemoryBuffer::Read(uint64_t addr, void* dst, size_t size) { + if (addr >= size_) { + return 0; + } + + size_t bytes_left = size_ - static_cast(addr); + const unsigned char* actual_base = static_cast(raw_) + addr; + size_t actual_len = std::min(bytes_left, size); + + memcpy(dst, actual_base, actual_len); + return actual_len; +} + +uint8_t* MemoryBuffer::GetPtr(size_t offset) { + if (offset < size_) { + return &raw_[offset]; + } + return nullptr; +} + +MemoryFileAtOffset::~MemoryFileAtOffset() { + Clear(); +} + +void MemoryFileAtOffset::Clear() { + if (data_) { + munmap(&data_[-offset_], size_ + offset_); + data_ = nullptr; + } +} + +bool MemoryFileAtOffset::Init(const std::string& file, uint64_t offset, uint64_t size) { + // Clear out any previous data if it exists. + Clear(); + + android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(file.c_str(), O_RDONLY | O_CLOEXEC))); + if (fd == -1) { + return false; + } + struct stat buf; + if (fstat(fd, &buf) == -1) { + return false; + } + if (offset >= static_cast(buf.st_size)) { + return false; + } + + offset_ = offset & (getpagesize() - 1); + uint64_t aligned_offset = offset & ~(getpagesize() - 1); + if (aligned_offset > static_cast(buf.st_size) || + offset > static_cast(buf.st_size)) { + return false; + } + + size_ = buf.st_size - aligned_offset; + uint64_t max_size; + if (!__builtin_add_overflow(size, offset_, &max_size) && max_size < size_) { + // Truncate the mapped size. + size_ = max_size; + } + void* map = mmap(nullptr, size_, PROT_READ, MAP_PRIVATE, fd, aligned_offset); + if (map == MAP_FAILED) { + return false; + } + + data_ = &reinterpret_cast(map)[offset_]; + size_ -= offset_; + + return true; +} + +size_t MemoryFileAtOffset::Read(uint64_t addr, void* dst, size_t size) { + if (addr >= size_) { + return 0; + } + + size_t bytes_left = size_ - static_cast(addr); + const unsigned char* actual_base = static_cast(data_) + addr; + size_t actual_len = std::min(bytes_left, size); + + memcpy(dst, actual_base, actual_len); + return actual_len; +} + +size_t MemoryRemote::Read(uint64_t addr, void* dst, size_t size) { +#if !defined(__LP64__) + // Cannot read an address greater than 32 bits in a 32 bit context. + if (addr > UINT32_MAX) { + return 0; + } +#endif + + size_t (*read_func)(pid_t, uint64_t, void*, size_t) = + reinterpret_cast(read_redirect_func_.load()); + if (read_func != nullptr) { + return read_func(pid_, addr, dst, size); + } else { + // Prefer process_vm_read, try it first. If it doesn't work, use the + // ptrace function. If at least one of them returns at least some data, + // set that as the permanent function to use. + // This assumes that if process_vm_read works once, it will continue + // to work. + size_t bytes = ProcessVmRead(pid_, addr, dst, size); + if (bytes > 0) { + read_redirect_func_ = reinterpret_cast(ProcessVmRead); + return bytes; + } + bytes = PtraceRead(pid_, addr, dst, size); + if (bytes > 0) { + read_redirect_func_ = reinterpret_cast(PtraceRead); + } + return bytes; + } +} + +size_t MemoryLocal::Read(uint64_t addr, void* dst, size_t size) { + return ProcessVmRead(getpid(), addr, dst, size); +} + +MemoryRange::MemoryRange(const std::shared_ptr& memory, uint64_t begin, uint64_t length, + uint64_t offset) + : memory_(memory), begin_(begin), length_(length), offset_(offset) {} + +size_t MemoryRange::Read(uint64_t addr, void* dst, size_t size) { + if (addr < offset_) { + return 0; + } + + uint64_t read_offset = addr - offset_; + if (read_offset >= length_) { + return 0; + } + + uint64_t read_length = std::min(static_cast(size), length_ - read_offset); + uint64_t read_addr; + if (__builtin_add_overflow(read_offset, begin_, &read_addr)) { + return 0; + } + + return memory_->Read(read_addr, dst, read_length); +} + +bool MemoryRanges::Insert(MemoryRange* memory) { + uint64_t last_addr; + if (__builtin_add_overflow(memory->offset(), memory->length(), &last_addr)) { + // This should never happen in the real world. However, it is possible + // that an offset in a mapped in segment could be crafted such that + // this value overflows. In that case, clamp the value to the max uint64 + // value. + last_addr = UINT64_MAX; + } + auto entry = maps_.try_emplace(last_addr, memory); + if (entry.second) { + return true; + } + delete memory; + return false; +} + +size_t MemoryRanges::Read(uint64_t addr, void* dst, size_t size) { + auto entry = maps_.upper_bound(addr); + if (entry != maps_.end()) { + return entry->second->Read(addr, dst, size); + } + return 0; +} + +bool MemoryOffline::Init(const std::string& file, uint64_t offset) { + auto memory_file = std::make_shared(); + if (!memory_file->Init(file, offset)) { + return false; + } + + // The first uint64_t value is the start of memory. + uint64_t start; + if (!memory_file->ReadFully(0, &start, sizeof(start))) { + return false; + } + + uint64_t size = memory_file->Size(); + if (__builtin_sub_overflow(size, sizeof(start), &size)) { + return false; + } + + memory_ = std::make_unique(memory_file, sizeof(start), size, start); + return true; +} + +bool MemoryOffline::Init(const std::string& file, uint64_t offset, uint64_t start, uint64_t size) { + auto memory_file = std::make_shared(); + if (!memory_file->Init(file, offset)) { + return false; + } + + memory_ = std::make_unique(memory_file, 0, size, start); + return true; +} + +size_t MemoryOffline::Read(uint64_t addr, void* dst, size_t size) { + if (!memory_) { + return 0; + } + + return memory_->Read(addr, dst, size); +} + +MemoryOfflineBuffer::MemoryOfflineBuffer(const uint8_t* data, uint64_t start, uint64_t end) + : data_(data), start_(start), end_(end) {} + +void MemoryOfflineBuffer::Reset(const uint8_t* data, uint64_t start, uint64_t end) { + data_ = data; + start_ = start; + end_ = end; +} + +size_t MemoryOfflineBuffer::Read(uint64_t addr, void* dst, size_t size) { + if (addr < start_ || addr >= end_) { + return 0; + } + + size_t read_length = std::min(size, static_cast(end_ - addr)); + memcpy(dst, &data_[addr - start_], read_length); + return read_length; +} + +MemoryOfflineParts::~MemoryOfflineParts() { + for (auto memory : memories_) { + delete memory; + } +} + +size_t MemoryOfflineParts::Read(uint64_t addr, void* dst, size_t size) { + if (memories_.empty()) { + return 0; + } + + // Do a read on each memory object, no support for reading across the + // different memory objects. + for (MemoryOffline* memory : memories_) { + size_t bytes = memory->Read(addr, dst, size); + if (bytes != 0) { + return bytes; + } + } + return 0; +} + +size_t MemoryCacheBase::InternalCachedRead(uint64_t addr, void* dst, size_t size, + CacheDataType* cache) { + uint64_t addr_page = addr >> kCacheBits; + auto entry = cache->find(addr_page); + uint8_t* cache_dst; + if (entry != cache->end()) { + cache_dst = entry->second; + } else { + cache_dst = (*cache)[addr_page]; + if (!impl_->ReadFully(addr_page << kCacheBits, cache_dst, kCacheSize)) { + // Erase the entry. + cache->erase(addr_page); + return impl_->Read(addr, dst, size); + } + } + size_t max_read = ((addr_page + 1) << kCacheBits) - addr; + if (size <= max_read) { + memcpy(dst, &cache_dst[addr & kCacheMask], size); + return size; + } + + // The read crossed into another cached entry, since a read can only cross + // into one extra cached page, duplicate the code rather than looping. + memcpy(dst, &cache_dst[addr & kCacheMask], max_read); + dst = &reinterpret_cast(dst)[max_read]; + addr_page++; + + entry = cache->find(addr_page); + if (entry != cache->end()) { + cache_dst = entry->second; + } else { + cache_dst = (*cache)[addr_page]; + if (!impl_->ReadFully(addr_page << kCacheBits, cache_dst, kCacheSize)) { + // Erase the entry. + cache->erase(addr_page); + return impl_->Read(addr_page << kCacheBits, dst, size - max_read) + max_read; + } + } + memcpy(dst, cache_dst, size - max_read); + return size; +} + +void MemoryCache::Clear() { + std::lock_guard lock(cache_lock_); + cache_.clear(); +} + +size_t MemoryCache::CachedRead(uint64_t addr, void* dst, size_t size) { + // Use a single lock since this object is not designed to be performant + // for multiple object reading from multiple threads. + std::lock_guard lock(cache_lock_); + + return InternalCachedRead(addr, dst, size, &cache_); +} + +MemoryThreadCache::MemoryThreadCache(Memory* memory) : MemoryCacheBase(memory) { + thread_cache_ = std::make_optional(); + if (pthread_key_create(&*thread_cache_, [](void* memory) { + CacheDataType* cache = reinterpret_cast(memory); + delete cache; + }) != 0) { + Log::AsyncSafe("Failed to create pthread key."); + thread_cache_.reset(); + } +} + +MemoryThreadCache::~MemoryThreadCache() { + if (thread_cache_) { + CacheDataType* cache = reinterpret_cast(pthread_getspecific(*thread_cache_)); + delete cache; + pthread_key_delete(*thread_cache_); + } +} + +size_t MemoryThreadCache::CachedRead(uint64_t addr, void* dst, size_t size) { + if (!thread_cache_) { + return impl_->Read(addr, dst, size); + } + + CacheDataType* cache = reinterpret_cast(pthread_getspecific(*thread_cache_)); + if (cache == nullptr) { + cache = new CacheDataType; + pthread_setspecific(*thread_cache_, cache); + } + + return InternalCachedRead(addr, dst, size, cache); +} + +void MemoryThreadCache::Clear() { + if (!thread_cache_) { + return; + } + + CacheDataType* cache = reinterpret_cast(pthread_getspecific(*thread_cache_)); + if (cache != nullptr) { + delete cache; + pthread_setspecific(*thread_cache_, nullptr); + } +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryBuffer.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryBuffer.h new file mode 100644 index 0000000000..a5b57433ca --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryBuffer.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include + +#include + +namespace unwindstack { + +class MemoryBuffer : public Memory { + public: + MemoryBuffer() = default; + virtual ~MemoryBuffer() { free(raw_); } + + size_t Read(uint64_t addr, void* dst, size_t size) override; + + uint8_t* GetPtr(size_t offset) override; + + bool Resize(size_t size) { + void* new_raw = realloc(raw_, size); + if (new_raw == nullptr) { + free(raw_); + raw_ = nullptr; + size_ = 0; + return false; + } + raw_ = reinterpret_cast(new_raw); + size_ = size; + return true; + } + + uint64_t Size() { return size_; } + + private: + uint8_t* raw_ = nullptr; + size_t size_ = 0; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryCache.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryCache.h new file mode 100644 index 0000000000..de5e9a0a9e --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryCache.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + +namespace unwindstack { + +class MemoryCacheBase : public Memory { + public: + MemoryCacheBase(Memory* memory) : impl_(memory) {} + virtual ~MemoryCacheBase() = default; + + MemoryCacheBase* AsMemoryCacheBase() override { return this; } + + const std::shared_ptr& UnderlyingMemory() { return impl_; } + + size_t Read(uint64_t addr, void* dst, size_t size) override { + // Only look at the cache for small reads. + if (size > 64) { + return impl_->Read(addr, dst, size); + } + return CachedRead(addr, dst, size); + } + + long ReadTag(uint64_t addr) override { return impl_->ReadTag(addr); } + + protected: + constexpr static size_t kCacheBits = 12; + constexpr static size_t kCacheMask = (1 << kCacheBits) - 1; + constexpr static size_t kCacheSize = 1 << kCacheBits; + + using CacheDataType = std::unordered_map; + + virtual size_t CachedRead(uint64_t addr, void* dst, size_t size) = 0; + + size_t InternalCachedRead(uint64_t addr, void* dst, size_t size, CacheDataType* cache); + + std::shared_ptr impl_; +}; + +class MemoryCache : public MemoryCacheBase { + public: + MemoryCache(Memory* memory) : MemoryCacheBase(memory) {} + virtual ~MemoryCache() = default; + + size_t CachedRead(uint64_t addr, void* dst, size_t size) override; + + void Clear() override; + + protected: + CacheDataType cache_; + + std::mutex cache_lock_; +}; + +class MemoryThreadCache : public MemoryCacheBase { + public: + MemoryThreadCache(Memory* memory); + virtual ~MemoryThreadCache(); + + size_t CachedRead(uint64_t addr, void* dst, size_t size) override; + + void Clear() override; + + protected: + std::optional thread_cache_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryFileAtOffset.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryFileAtOffset.h new file mode 100644 index 0000000000..90cf00b752 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryFileAtOffset.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +class MemoryFileAtOffset : public Memory { + public: + MemoryFileAtOffset() = default; + virtual ~MemoryFileAtOffset(); + + bool Init(const std::string& file, uint64_t offset, uint64_t size = UINT64_MAX); + + uint8_t* GetPtr(size_t addr = 0) override { return addr < size_ ? data_ + addr : nullptr; } + + size_t Read(uint64_t addr, void* dst, size_t size) override; + + size_t Size() { return size_; } + + void Clear() override; + + protected: + size_t size_ = 0; + size_t offset_ = 0; + uint8_t* data_ = nullptr; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryLocal.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryLocal.h new file mode 100644 index 0000000000..f98c9cbce4 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryLocal.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +class MemoryLocal : public Memory { + public: + MemoryLocal() = default; + virtual ~MemoryLocal() = default; + + size_t Read(uint64_t addr, void* dst, size_t size) override; + long ReadTag(uint64_t addr) override; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryMte.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryMte.cpp new file mode 100644 index 0000000000..74555da417 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryMte.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "MemoryLocal.h" +#include "MemoryRemote.h" + +namespace unwindstack { + +long MemoryRemote::ReadTag(uint64_t addr) { +#if defined(PTRACE_PEEKMTETAGS) || defined(PT_PEEKMTETAGS) + char tag; + iovec iov = {&tag, 1}; + if (ptrace(PTRACE_PEEKMTETAGS, pid_, reinterpret_cast(addr), &iov) != 0 || + iov.iov_len != 1) { + return -1; + } + return tag; +#else + (void)addr; + return -1; +#endif +} + +long MemoryLocal::ReadTag(uint64_t addr) { + return -1; +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOffline.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOffline.h new file mode 100644 index 0000000000..024e111cf5 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOffline.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +#include "MemoryRange.h" + +namespace unwindstack { + +class MemoryOffline : public Memory { + public: + MemoryOffline() = default; + virtual ~MemoryOffline() = default; + + bool Init(const std::string& file, uint64_t offset); + + bool Init(const std::string& file, uint64_t offset, uint64_t start, uint64_t size); + + size_t Read(uint64_t addr, void* dst, size_t size) override; + + private: + std::unique_ptr memory_; +}; + +class MemoryOfflineParts : public Memory { + public: + MemoryOfflineParts() = default; + virtual ~MemoryOfflineParts(); + + void Add(MemoryOffline* memory) { memories_.push_back(memory); } + + size_t Read(uint64_t addr, void* dst, size_t size) override; + + private: + std::vector memories_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOfflineBuffer.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOfflineBuffer.h new file mode 100644 index 0000000000..d77008e0d5 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryOfflineBuffer.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +class MemoryOfflineBuffer : public Memory { + public: + MemoryOfflineBuffer(const uint8_t* data, uint64_t start, uint64_t end); + virtual ~MemoryOfflineBuffer() = default; + + void Reset(const uint8_t* data, uint64_t start, uint64_t end); + + size_t Read(uint64_t addr, void* dst, size_t size) override; + + private: + const uint8_t* data_; + uint64_t start_; + uint64_t end_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRange.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRange.h new file mode 100644 index 0000000000..b789ee3e1d --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRange.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace unwindstack { + +// MemoryRange maps one address range onto another. +// The range [src_begin, src_begin + length) in the underlying Memory is mapped onto offset, +// such that range.read(offset) is equivalent to underlying.read(src_begin). +class MemoryRange : public Memory { + public: + MemoryRange(const std::shared_ptr& memory, uint64_t begin, uint64_t length, + uint64_t offset); + virtual ~MemoryRange() = default; + + size_t Read(uint64_t addr, void* dst, size_t size) override; + + uint64_t offset() { return offset_; } + uint64_t length() { return length_; } + + private: + std::shared_ptr memory_; + uint64_t begin_; + uint64_t length_; + uint64_t offset_; +}; + +class MemoryRanges : public Memory { + public: + MemoryRanges() = default; + virtual ~MemoryRanges() = default; + + bool Insert(MemoryRange* memory); + + size_t Read(uint64_t addr, void* dst, size_t size) override; + + private: + std::map> maps_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRemote.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRemote.h new file mode 100644 index 0000000000..563e5b74b0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/MemoryRemote.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +#include + +namespace unwindstack { + +class MemoryRemote : public Memory { + public: + MemoryRemote(pid_t pid) : pid_(pid), read_redirect_func_(0) {} + virtual ~MemoryRemote() = default; + + size_t Read(uint64_t addr, void* dst, size_t size) override; + long ReadTag(uint64_t addr) override; + + pid_t pid() { return pid_; } + + private: + pid_t pid_; + std::atomic_uintptr_t read_redirect_func_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/OWNERS b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/OWNERS new file mode 100644 index 0000000000..6f7e4a3c46 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/OWNERS @@ -0,0 +1 @@ +cferris@google.com diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/README.md b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/README.md new file mode 100644 index 0000000000..07f2f941a0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/README.md @@ -0,0 +1,14 @@ +# libunwindstack-ndk + +This repository contains a patched version of libunwindstack to build for NDK. + +We removed some parts of the code that we won't use, like MIPS, bionic and windows support, but there's still room for improvements and unused code removal. + +We also had to add headers from libraries like art_api, android-base and procinfo. + +We got the libunwindstack library from here: https://android.googlesource.com/platform/system/core/+/refs/heads/master + +The commit was c1f66b44fbf8c115f008d625f409449f111cdfa0: +https://android.googlesource.com/platform/system/unwinding/+/c1f66b44fbf8c115f008d625f409449f111cdfa0 + +If we wanted to update again, we could grab the latest commit, overwrite the whole library, and remove the things we don't use. \ No newline at end of file diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Regs.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Regs.cpp new file mode 100644 index 0000000000..937a404419 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Regs.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace unwindstack { + +// The largest user structure. +// constexpr size_t MAX_USER_REGS_SIZE = sizeof(mips64_user_regs) + 10; +static constexpr size_t kMaxUserRegsSize = std::max( + sizeof(arm_user_regs), + std::max(sizeof(arm64_user_regs), std::max(sizeof(x86_user_regs), sizeof(x86_64_user_regs)))); + +// This function assumes that reg_data is already aligned to a 64 bit value. +// If not this could crash with an unaligned access. +Regs* Regs::RemoteGet(pid_t pid, ErrorCode* error_code) { + // Make the buffer large enough to contain the largest registers type. + std::vector buffer(kMaxUserRegsSize / sizeof(uint64_t)); + struct iovec io; + io.iov_base = buffer.data(); + io.iov_len = buffer.size() * sizeof(uint64_t); + + if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, reinterpret_cast(&io)) == -1) { + Log::Error("PTRACE_GETREGSET failed for pid %d: %s", pid, strerror(errno)); + if (error_code != nullptr) { + *error_code = ERROR_PTRACE_CALL; + } + return nullptr; + } + + // Infer the process architecture from the size of its register structure. + switch (io.iov_len) { + case sizeof(x86_user_regs): + return RegsX86::Read(buffer.data()); + case sizeof(x86_64_user_regs): + return RegsX86_64::Read(buffer.data()); + case sizeof(arm_user_regs): + return RegsArm::Read(buffer.data()); + case sizeof(arm64_user_regs): + return RegsArm64::Read(buffer.data()); + } + + Log::Error("No matching size of user regs structure for pid %d: size %zu", pid, io.iov_len); + if (error_code != nullptr) { + *error_code = ERROR_UNSUPPORTED; + } + return nullptr; +} + +ArchEnum Regs::RemoteGetArch(pid_t pid, ErrorCode* error_code) { + // Make the buffer large enough to contain the largest registers type. + std::vector buffer(kMaxUserRegsSize / sizeof(uint64_t)); + struct iovec io; + io.iov_base = buffer.data(); + io.iov_len = buffer.size() * sizeof(uint64_t); + + if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, reinterpret_cast(&io)) == -1) { + Log::Error("PTRACE_GETREGSET failed for pid %d: %s", pid, strerror(errno)); + if (error_code != nullptr) { + *error_code = ERROR_PTRACE_CALL; + } + return ARCH_UNKNOWN; + } + + // Infer the process architecture from the size of its register structure. + switch (io.iov_len) { + case sizeof(x86_user_regs): + return ARCH_X86; + case sizeof(x86_64_user_regs): + return ARCH_X86_64; + case sizeof(arm_user_regs): + return ARCH_ARM; + case sizeof(arm64_user_regs): + return ARCH_ARM64; + } + + Log::Error("No matching size of user regs structure for pid %d: size %zu", pid, io.iov_len); + if (error_code != nullptr) { + *error_code = ERROR_UNSUPPORTED; + } + return ARCH_UNKNOWN; +} + +Regs* Regs::CreateFromUcontext(ArchEnum arch, void* ucontext) { + switch (arch) { + case ARCH_X86: + return RegsX86::CreateFromUcontext(ucontext); + case ARCH_X86_64: + return RegsX86_64::CreateFromUcontext(ucontext); + case ARCH_ARM: + return RegsArm::CreateFromUcontext(ucontext); + case ARCH_ARM64: + return RegsArm64::CreateFromUcontext(ucontext); + case ARCH_UNKNOWN: + default: + return nullptr; + } +} + +ArchEnum Regs::CurrentArch() { +#if defined(__arm__) + return ARCH_ARM; +#elif defined(__aarch64__) + return ARCH_ARM64; +#elif defined(__i386__) + return ARCH_X86; +#elif defined(__x86_64__) + return ARCH_X86_64; +#elif defined(__riscv) + return ARCH_RISCV64; +#else + abort(); +#endif +} + +Regs* Regs::CreateFromLocal() { + Regs* regs; +#if defined(__arm__) + regs = new RegsArm(); +#elif defined(__aarch64__) + regs = new RegsArm64(); +#elif defined(__i386__) + regs = new RegsX86(); +#elif defined(__x86_64__) + regs = new RegsX86_64(); +#elif defined(__riscv) + regs = new RegsRiscv64(); +#else + abort(); +#endif + return regs; +} + +uint64_t get_pc_adjustment_common(uint64_t rel_pc, Elf* elf, ArchEnum arch) { + switch (arch) { + case ARCH_ARM: { + if (!elf->valid()) { + return 2; + } + + uint64_t load_bias = elf->GetLoadBias(); + if (rel_pc < load_bias) { + if (rel_pc < 2) { + return 0; + } + return 2; + } + uint64_t adjusted_rel_pc = rel_pc - load_bias; + if (adjusted_rel_pc < 5) { + if (adjusted_rel_pc < 2) { + return 0; + } + return 2; + } + + if (adjusted_rel_pc & 1) { + // This is a thumb instruction, it could be 2 or 4 bytes. + uint32_t value; + if (!elf->memory()->ReadFully(adjusted_rel_pc - 5, &value, sizeof(value)) || + (value & 0xe000f000) != 0xe000f000) { + return 2; + } + } + return 4; + } + case ARCH_ARM64: { + if (rel_pc < 4) { + return 0; + } + return 4; + } + case ARCH_X86: + case ARCH_X86_64: { + if (rel_pc == 0) { + return 0; + } + return 1; + } + case ARCH_UNKNOWN: + return 0; + } +} + + uint64_t Regs::GetPcAdjustment(uint64_t rel_pc, Elf* elf, ArchEnum arch) { + return get_pc_adjustment_common(rel_pc, elf, arch); + } + + uint64_t GetPcAdjustment(uint64_t rel_pc, Elf* elf, ArchEnum arch) { + return get_pc_adjustment_common(rel_pc, elf, arch); + } + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm.cpp new file mode 100644 index 0000000000..1aaa08f569 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace unwindstack { + +RegsArm::RegsArm() : RegsImpl(ARM_REG_LAST, Location(LOCATION_REGISTER, ARM_REG_LR)) {} + +ArchEnum RegsArm::Arch() { + return ARCH_ARM; +} + +uint64_t RegsArm::pc() { + return regs_[ARM_REG_PC]; +} + +uint64_t RegsArm::sp() { + return regs_[ARM_REG_SP]; +} + +void RegsArm::set_pc(uint64_t pc) { + regs_[ARM_REG_PC] = pc; +} + +void RegsArm::set_sp(uint64_t sp) { + regs_[ARM_REG_SP] = sp; +} + +bool RegsArm::SetPcFromReturnAddress(Memory*) { + uint32_t lr = regs_[ARM_REG_LR]; + if (regs_[ARM_REG_PC] == lr) { + return false; + } + + regs_[ARM_REG_PC] = lr; + return true; +} + +void RegsArm::IterateRegisters(std::function fn) { + fn("r0", regs_[ARM_REG_R0]); + fn("r1", regs_[ARM_REG_R1]); + fn("r2", regs_[ARM_REG_R2]); + fn("r3", regs_[ARM_REG_R3]); + fn("r4", regs_[ARM_REG_R4]); + fn("r5", regs_[ARM_REG_R5]); + fn("r6", regs_[ARM_REG_R6]); + fn("r7", regs_[ARM_REG_R7]); + fn("r8", regs_[ARM_REG_R8]); + fn("r9", regs_[ARM_REG_R9]); + fn("r10", regs_[ARM_REG_R10]); + fn("r11", regs_[ARM_REG_R11]); + fn("ip", regs_[ARM_REG_R12]); + fn("sp", regs_[ARM_REG_SP]); + fn("lr", regs_[ARM_REG_LR]); + fn("pc", regs_[ARM_REG_PC]); +} + +Regs* RegsArm::Read(void* remote_data) { + arm_user_regs* user = reinterpret_cast(remote_data); + + RegsArm* regs = new RegsArm(); + memcpy(regs->RawData(), &user->regs[0], ARM_REG_LAST * sizeof(uint32_t)); + return regs; +} + +Regs* RegsArm::CreateFromUcontext(void* ucontext) { + arm_ucontext_t* arm_ucontext = reinterpret_cast(ucontext); + + RegsArm* regs = new RegsArm(); + memcpy(regs->RawData(), &arm_ucontext->uc_mcontext.regs[0], ARM_REG_LAST * sizeof(uint32_t)); + return regs; +} + +bool RegsArm::StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) { + uint32_t data; + Memory* elf_memory = elf->memory(); + // Read from elf memory since it is usually more expensive to read from + // process memory. + if (!elf_memory->ReadFully(elf_offset, &data, sizeof(data))) { + return false; + } + + uint64_t offset = 0; + if (data == 0xe3a07077 || data == 0xef900077 || data == 0xdf002777) { + uint64_t sp = regs_[ARM_REG_SP]; + // non-RT sigreturn call. + // __restore: + // + // Form 1 (arm): + // 0x77 0x70 mov r7, #0x77 + // 0xa0 0xe3 svc 0x00000000 + // + // Form 2 (arm): + // 0x77 0x00 0x90 0xef svc 0x00900077 + // + // Form 3 (thumb): + // 0x77 0x27 movs r7, #77 + // 0x00 0xdf svc 0 + if (!process_memory->ReadFully(sp, &data, sizeof(data))) { + return false; + } + if (data == 0x5ac3c35a) { + // SP + uc_mcontext offset + r0 offset. + offset = sp + 0x14 + 0xc; + } else { + // SP + r0 offset + offset = sp + 0xc; + } + } else if (data == 0xe3a070ad || data == 0xef9000ad || data == 0xdf0027ad) { + uint64_t sp = regs_[ARM_REG_SP]; + // RT sigreturn call. + // __restore_rt: + // + // Form 1 (arm): + // 0xad 0x70 mov r7, #0xad + // 0xa0 0xe3 svc 0x00000000 + // + // Form 2 (arm): + // 0xad 0x00 0x90 0xef svc 0x009000ad + // + // Form 3 (thumb): + // 0xad 0x27 movs r7, #ad + // 0x00 0xdf svc 0 + if (!process_memory->ReadFully(sp, &data, sizeof(data))) { + return false; + } + if (data == sp + 8) { + // SP + 8 + sizeof(siginfo_t) + uc_mcontext_offset + r0 offset + offset = sp + 8 + 0x80 + 0x14 + 0xc; + } else { + // SP + sizeof(siginfo_t) + uc_mcontext_offset + r0 offset + offset = sp + 0x80 + 0x14 + 0xc; + } + } + if (offset == 0) { + return false; + } + + if (!process_memory->ReadFully(offset, regs_.data(), sizeof(uint32_t) * ARM_REG_LAST)) { + return false; + } + return true; +} + +Regs* RegsArm::Clone() { + return new RegsArm(*this); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm64.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm64.cpp new file mode 100644 index 0000000000..0af9a1e392 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsArm64.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace unwindstack { + +RegsArm64::RegsArm64() + : RegsImpl(ARM64_REG_LAST, Location(LOCATION_REGISTER, ARM64_REG_LR)) { + ResetPseudoRegisters(); + pac_mask_ = 0; +} + +ArchEnum RegsArm64::Arch() { + return ARCH_ARM64; +} + +uint64_t RegsArm64::pc() { + return regs_[ARM64_REG_PC]; +} + +uint64_t RegsArm64::sp() { + return regs_[ARM64_REG_SP]; +} + +static uint64_t strip_pac(uint64_t pc, uint64_t mask) { + // If the target is aarch64 then the return address may have been + // signed using the Armv8.3-A Pointer Authentication extension. The + // original return address can be restored by stripping out the + // authentication code using a mask or xpaclri. xpaclri is a NOP on + // pre-Armv8.3-A architectures. + if (mask) { + pc &= ~mask; + } + return pc; +} + +void RegsArm64::set_pc(uint64_t pc) { + if ((0 != pc) && IsRASigned()) { + pc = strip_pac(pc, pac_mask_); + } + regs_[ARM64_REG_PC] = pc; +} + +void RegsArm64::set_sp(uint64_t sp) { + regs_[ARM64_REG_SP] = sp; +} + +void RegsArm64::fallback_pc() { + // As a last resort, try stripping the PC of the pointer + // authentication code. + regs_[ARM64_REG_PC] = strip_pac(regs_[ARM64_REG_PC], pac_mask_); +} + +bool RegsArm64::SetPcFromReturnAddress(Memory*) { + uint64_t lr = regs_[ARM64_REG_LR]; + if (regs_[ARM64_REG_PC] == lr) { + return false; + } + + regs_[ARM64_REG_PC] = lr; + return true; +} + +void RegsArm64::IterateRegisters(std::function fn) { + fn("x0", regs_[ARM64_REG_R0]); + fn("x1", regs_[ARM64_REG_R1]); + fn("x2", regs_[ARM64_REG_R2]); + fn("x3", regs_[ARM64_REG_R3]); + fn("x4", regs_[ARM64_REG_R4]); + fn("x5", regs_[ARM64_REG_R5]); + fn("x6", regs_[ARM64_REG_R6]); + fn("x7", regs_[ARM64_REG_R7]); + fn("x8", regs_[ARM64_REG_R8]); + fn("x9", regs_[ARM64_REG_R9]); + fn("x10", regs_[ARM64_REG_R10]); + fn("x11", regs_[ARM64_REG_R11]); + fn("x12", regs_[ARM64_REG_R12]); + fn("x13", regs_[ARM64_REG_R13]); + fn("x14", regs_[ARM64_REG_R14]); + fn("x15", regs_[ARM64_REG_R15]); + fn("x16", regs_[ARM64_REG_R16]); + fn("x17", regs_[ARM64_REG_R17]); + fn("x18", regs_[ARM64_REG_R18]); + fn("x19", regs_[ARM64_REG_R19]); + fn("x20", regs_[ARM64_REG_R20]); + fn("x21", regs_[ARM64_REG_R21]); + fn("x22", regs_[ARM64_REG_R22]); + fn("x23", regs_[ARM64_REG_R23]); + fn("x24", regs_[ARM64_REG_R24]); + fn("x25", regs_[ARM64_REG_R25]); + fn("x26", regs_[ARM64_REG_R26]); + fn("x27", regs_[ARM64_REG_R27]); + fn("x28", regs_[ARM64_REG_R28]); + fn("x29", regs_[ARM64_REG_R29]); + fn("lr", regs_[ARM64_REG_LR]); + fn("sp", regs_[ARM64_REG_SP]); + fn("pc", regs_[ARM64_REG_PC]); + fn("pst", regs_[ARM64_REG_PSTATE]); +} + +Regs* RegsArm64::Read(void* remote_data) { + arm64_user_regs* user = reinterpret_cast(remote_data); + + RegsArm64* regs = new RegsArm64(); + memcpy(regs->RawData(), &user->regs[0], (ARM64_REG_R30 + 1) * sizeof(uint64_t)); + uint64_t* reg_data = reinterpret_cast(regs->RawData()); + reg_data[ARM64_REG_SP] = user->sp; + reg_data[ARM64_REG_PC] = user->pc; + reg_data[ARM64_REG_PSTATE] = user->pstate; + return regs; +} + +Regs* RegsArm64::CreateFromUcontext(void* ucontext) { + arm64_ucontext_t* arm64_ucontext = reinterpret_cast(ucontext); + + RegsArm64* regs = new RegsArm64(); + memcpy(regs->RawData(), &arm64_ucontext->uc_mcontext.regs[0], ARM64_REG_LAST * sizeof(uint64_t)); + return regs; +} + +bool RegsArm64::StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) { + uint64_t data; + Memory* elf_memory = elf->memory(); + // Read from elf memory since it is usually more expensive to read from + // process memory. + if (!elf_memory->ReadFully(elf_offset, &data, sizeof(data))) { + return false; + } + + // Look for the kernel sigreturn function. + // __kernel_rt_sigreturn: + // 0xd2801168 mov x8, #0x8b + // 0xd4000001 svc #0x0 + if (data != 0xd4000001d2801168ULL) { + return false; + } + + // SP + sizeof(siginfo_t) + uc_mcontext offset + X0 offset. + if (!process_memory->ReadFully(regs_[ARM64_REG_SP] + 0x80 + 0xb0 + 0x08, regs_.data(), + sizeof(uint64_t) * ARM64_REG_LAST)) { + return false; + } + return true; +} + +void RegsArm64::ResetPseudoRegisters(void) { + // DWARF for AArch64 says RA_SIGN_STATE should be initialized to 0. + this->SetPseudoRegister(Arm64Reg::ARM64_PREG_RA_SIGN_STATE, 0); +} + +bool RegsArm64::SetPseudoRegister(uint16_t id, uint64_t value) { + if ((id >= Arm64Reg::ARM64_PREG_FIRST) && (id < Arm64Reg::ARM64_PREG_LAST)) { + pseudo_regs_[id - Arm64Reg::ARM64_PREG_FIRST] = value; + return true; + } + return false; +} + +bool RegsArm64::GetPseudoRegister(uint16_t id, uint64_t* value) { + if ((id >= Arm64Reg::ARM64_PREG_FIRST) && (id < Arm64Reg::ARM64_PREG_LAST)) { + *value = pseudo_regs_[id - Arm64Reg::ARM64_PREG_FIRST]; + return true; + } + return false; +} + +bool RegsArm64::IsRASigned() { + uint64_t value; + auto result = this->GetPseudoRegister(Arm64Reg::ARM64_PREG_RA_SIGN_STATE, &value); + return (result && (value != 0)); +} + +void RegsArm64::SetPACMask(uint64_t mask) { + pac_mask_ = mask; +} + +Regs* RegsArm64::Clone() { + return new RegsArm64(*this); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsInfo.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsInfo.h new file mode 100644 index 0000000000..28b297e707 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsInfo.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +template +struct RegsInfo { + static constexpr size_t MAX_REGISTERS = 64; + + RegsInfo(RegsImpl* regs) : regs(regs) {} + + RegsImpl* regs = nullptr; + uint64_t saved_reg_map = 0; + AddressType saved_regs[MAX_REGISTERS]; + + inline AddressType Get(uint32_t reg) { + if (IsSaved(reg)) { + return saved_regs[reg]; + } + return (*regs)[reg]; + } + + inline AddressType* Save(uint32_t reg) { + if (reg >= MAX_REGISTERS) { + // This should never happen since all currently supported + // architectures have < 64 total registers. + abort(); + } + saved_reg_map |= 1ULL << reg; + saved_regs[reg] = (*regs)[reg]; + return &(*regs)[reg]; + } + + inline bool IsSaved(uint32_t reg) { + if (reg > MAX_REGISTERS) { + // This should never happen since all currently supported + // architectures have < 64 total registers. + abort(); + } + return saved_reg_map & (1ULL << reg); + } + + inline uint16_t Total() { return regs->total_regs(); } +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86.cpp new file mode 100644 index 0000000000..4d3c246a41 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace unwindstack { + +RegsX86::RegsX86() : RegsImpl(X86_REG_LAST, Location(LOCATION_SP_OFFSET, -4)) {} + +ArchEnum RegsX86::Arch() { + return ARCH_X86; +} + +uint64_t RegsX86::pc() { + return regs_[X86_REG_PC]; +} + +uint64_t RegsX86::sp() { + return regs_[X86_REG_SP]; +} + +void RegsX86::set_pc(uint64_t pc) { + regs_[X86_REG_PC] = static_cast(pc); +} + +void RegsX86::set_sp(uint64_t sp) { + regs_[X86_REG_SP] = static_cast(sp); +} + +bool RegsX86::SetPcFromReturnAddress(Memory* process_memory) { + // Attempt to get the return address from the top of the stack. + uint32_t new_pc; + if (!process_memory->ReadFully(regs_[X86_REG_SP], &new_pc, sizeof(new_pc)) || + new_pc == regs_[X86_REG_PC]) { + return false; + } + + regs_[X86_REG_PC] = new_pc; + return true; +} + +void RegsX86::IterateRegisters(std::function fn) { + fn("eax", regs_[X86_REG_EAX]); + fn("ebx", regs_[X86_REG_EBX]); + fn("ecx", regs_[X86_REG_ECX]); + fn("edx", regs_[X86_REG_EDX]); + fn("ebp", regs_[X86_REG_EBP]); + fn("edi", regs_[X86_REG_EDI]); + fn("esi", regs_[X86_REG_ESI]); + fn("esp", regs_[X86_REG_ESP]); + fn("eip", regs_[X86_REG_EIP]); +} + +Regs* RegsX86::Read(void* user_data) { + x86_user_regs* user = reinterpret_cast(user_data); + + RegsX86* regs = new RegsX86(); + (*regs)[X86_REG_EAX] = user->eax; + (*regs)[X86_REG_EBX] = user->ebx; + (*regs)[X86_REG_ECX] = user->ecx; + (*regs)[X86_REG_EDX] = user->edx; + (*regs)[X86_REG_EBP] = user->ebp; + (*regs)[X86_REG_EDI] = user->edi; + (*regs)[X86_REG_ESI] = user->esi; + (*regs)[X86_REG_ESP] = user->esp; + (*regs)[X86_REG_EIP] = user->eip; + + return regs; +} + +void RegsX86::SetFromUcontext(x86_ucontext_t* ucontext) { + // Put the registers in the expected order. + regs_[X86_REG_EDI] = ucontext->uc_mcontext.edi; + regs_[X86_REG_ESI] = ucontext->uc_mcontext.esi; + regs_[X86_REG_EBP] = ucontext->uc_mcontext.ebp; + regs_[X86_REG_ESP] = ucontext->uc_mcontext.esp; + regs_[X86_REG_EBX] = ucontext->uc_mcontext.ebx; + regs_[X86_REG_EDX] = ucontext->uc_mcontext.edx; + regs_[X86_REG_ECX] = ucontext->uc_mcontext.ecx; + regs_[X86_REG_EAX] = ucontext->uc_mcontext.eax; + regs_[X86_REG_EIP] = ucontext->uc_mcontext.eip; +} + +Regs* RegsX86::CreateFromUcontext(void* ucontext) { + x86_ucontext_t* x86_ucontext = reinterpret_cast(ucontext); + + RegsX86* regs = new RegsX86(); + regs->SetFromUcontext(x86_ucontext); + return regs; +} + +bool RegsX86::StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) { + uint64_t data; + Memory* elf_memory = elf->memory(); + // Read from elf memory since it is usually more expensive to read from + // process memory. + if (!elf_memory->ReadFully(elf_offset, &data, sizeof(data))) { + return false; + } + + if (data == 0x80cd00000077b858ULL) { + // Without SA_SIGINFO set, the return sequence is: + // + // __restore: + // 0x58 pop %eax + // 0xb8 0x77 0x00 0x00 0x00 movl 0x77,%eax + // 0xcd 0x80 int 0x80 + // + // SP points at arguments: + // int signum + // struct sigcontext (same format as mcontext) + struct x86_mcontext_t context; + if (!process_memory->ReadFully(regs_[X86_REG_SP] + 4, &context, sizeof(context))) { + return false; + } + regs_[X86_REG_EBP] = context.ebp; + regs_[X86_REG_ESP] = context.esp; + regs_[X86_REG_EBX] = context.ebx; + regs_[X86_REG_EDX] = context.edx; + regs_[X86_REG_ECX] = context.ecx; + regs_[X86_REG_EAX] = context.eax; + regs_[X86_REG_EIP] = context.eip; + return true; + } else if ((data & 0x00ffffffffffffffULL) == 0x0080cd000000adb8ULL) { + // With SA_SIGINFO set, the return sequence is: + // + // __restore_rt: + // 0xb8 0xad 0x00 0x00 0x00 movl 0xad,%eax + // 0xcd 0x80 int 0x80 + // + // SP points at arguments: + // int signum + // siginfo* + // ucontext* + + // Get the location of the sigcontext data. + uint32_t ptr; + if (!process_memory->ReadFully(regs_[X86_REG_SP] + 8, &ptr, sizeof(ptr))) { + return false; + } + // Only read the portion of the data structure we care about. + x86_ucontext_t x86_ucontext; + if (!process_memory->ReadFully(ptr + 0x14, &x86_ucontext.uc_mcontext, sizeof(x86_mcontext_t))) { + return false; + } + SetFromUcontext(&x86_ucontext); + return true; + } + return false; +} + +Regs* RegsX86::Clone() { + return new RegsX86(*this); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86_64.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86_64.cpp new file mode 100644 index 0000000000..26d9f6578a --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/RegsX86_64.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace unwindstack { + +RegsX86_64::RegsX86_64() : RegsImpl(X86_64_REG_LAST, Location(LOCATION_SP_OFFSET, -8)) {} + +ArchEnum RegsX86_64::Arch() { + return ARCH_X86_64; +} + +uint64_t RegsX86_64::pc() { + return regs_[X86_64_REG_PC]; +} + +uint64_t RegsX86_64::sp() { + return regs_[X86_64_REG_SP]; +} + +void RegsX86_64::set_pc(uint64_t pc) { + regs_[X86_64_REG_PC] = pc; +} + +void RegsX86_64::set_sp(uint64_t sp) { + regs_[X86_64_REG_SP] = sp; +} + +bool RegsX86_64::SetPcFromReturnAddress(Memory* process_memory) { + // Attempt to get the return address from the top of the stack. + uint64_t new_pc; + if (!process_memory->ReadFully(regs_[X86_64_REG_SP], &new_pc, sizeof(new_pc)) || + new_pc == regs_[X86_64_REG_PC]) { + return false; + } + + regs_[X86_64_REG_PC] = new_pc; + return true; +} + +void RegsX86_64::IterateRegisters(std::function fn) { + fn("rax", regs_[X86_64_REG_RAX]); + fn("rbx", regs_[X86_64_REG_RBX]); + fn("rcx", regs_[X86_64_REG_RCX]); + fn("rdx", regs_[X86_64_REG_RDX]); + fn("r8", regs_[X86_64_REG_R8]); + fn("r9", regs_[X86_64_REG_R9]); + fn("r10", regs_[X86_64_REG_R10]); + fn("r11", regs_[X86_64_REG_R11]); + fn("r12", regs_[X86_64_REG_R12]); + fn("r13", regs_[X86_64_REG_R13]); + fn("r14", regs_[X86_64_REG_R14]); + fn("r15", regs_[X86_64_REG_R15]); + fn("rdi", regs_[X86_64_REG_RDI]); + fn("rsi", regs_[X86_64_REG_RSI]); + fn("rbp", regs_[X86_64_REG_RBP]); + fn("rsp", regs_[X86_64_REG_RSP]); + fn("rip", regs_[X86_64_REG_RIP]); +} + +Regs* RegsX86_64::Read(void* remote_data) { + x86_64_user_regs* user = reinterpret_cast(remote_data); + + RegsX86_64* regs = new RegsX86_64(); + (*regs)[X86_64_REG_RAX] = user->rax; + (*regs)[X86_64_REG_RBX] = user->rbx; + (*regs)[X86_64_REG_RCX] = user->rcx; + (*regs)[X86_64_REG_RDX] = user->rdx; + (*regs)[X86_64_REG_R8] = user->r8; + (*regs)[X86_64_REG_R9] = user->r9; + (*regs)[X86_64_REG_R10] = user->r10; + (*regs)[X86_64_REG_R11] = user->r11; + (*regs)[X86_64_REG_R12] = user->r12; + (*regs)[X86_64_REG_R13] = user->r13; + (*regs)[X86_64_REG_R14] = user->r14; + (*regs)[X86_64_REG_R15] = user->r15; + (*regs)[X86_64_REG_RDI] = user->rdi; + (*regs)[X86_64_REG_RSI] = user->rsi; + (*regs)[X86_64_REG_RBP] = user->rbp; + (*regs)[X86_64_REG_RSP] = user->rsp; + (*regs)[X86_64_REG_RIP] = user->rip; + + return regs; +} + +void RegsX86_64::SetFromUcontext(x86_64_ucontext_t* ucontext) { + // R8-R15 + memcpy(®s_[X86_64_REG_R8], &ucontext->uc_mcontext.r8, 8 * sizeof(uint64_t)); + + // Rest of the registers. + regs_[X86_64_REG_RDI] = ucontext->uc_mcontext.rdi; + regs_[X86_64_REG_RSI] = ucontext->uc_mcontext.rsi; + regs_[X86_64_REG_RBP] = ucontext->uc_mcontext.rbp; + regs_[X86_64_REG_RBX] = ucontext->uc_mcontext.rbx; + regs_[X86_64_REG_RDX] = ucontext->uc_mcontext.rdx; + regs_[X86_64_REG_RAX] = ucontext->uc_mcontext.rax; + regs_[X86_64_REG_RCX] = ucontext->uc_mcontext.rcx; + regs_[X86_64_REG_RSP] = ucontext->uc_mcontext.rsp; + regs_[X86_64_REG_RIP] = ucontext->uc_mcontext.rip; +} + +Regs* RegsX86_64::CreateFromUcontext(void* ucontext) { + x86_64_ucontext_t* x86_64_ucontext = reinterpret_cast(ucontext); + + RegsX86_64* regs = new RegsX86_64(); + regs->SetFromUcontext(x86_64_ucontext); + return regs; +} + +bool RegsX86_64::StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) { + uint64_t data; + Memory* elf_memory = elf->memory(); + // Read from elf memory since it is usually more expensive to read from + // process memory. + if (!elf_memory->ReadFully(elf_offset, &data, sizeof(data)) || data != 0x0f0000000fc0c748) { + return false; + } + + uint8_t data2; + if (!elf_memory->ReadFully(elf_offset + 8, &data2, sizeof(data2)) || data2 != 0x05) { + return false; + } + + // __restore_rt: + // 0x48 0xc7 0xc0 0x0f 0x00 0x00 0x00 mov $0xf,%rax + // 0x0f 0x05 syscall + + // Read the mcontext data from the stack. + // sp points to the ucontext data structure, read only the mcontext part. + x86_64_ucontext_t x86_64_ucontext; + if (!process_memory->ReadFully(regs_[X86_64_REG_SP] + 0x28, &x86_64_ucontext.uc_mcontext, + sizeof(x86_64_mcontext_t))) { + return false; + } + SetFromUcontext(&x86_64_ucontext); + return true; +} + +Regs* RegsX86_64::Clone() { + return new RegsX86_64(*this); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.cpp new file mode 100644 index 0000000000..67c9d0b0bd --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include + +#include + +#include "Check.h" +#include "Symbols.h" + +namespace unwindstack { + +Symbols::Symbols(uint64_t offset, uint64_t size, uint64_t entry_size, uint64_t str_offset, + uint64_t str_size) + : offset_(offset), + count_(entry_size != 0 ? ((size / entry_size > kMaxSymbols) ? kMaxSymbols : size / entry_size) + : 0), + entry_size_(entry_size), + str_offset_(str_offset) { + if (__builtin_add_overflow(str_offset_, str_size, &str_end_)) { + // Set to the max so that the code will still try to get symbol names. + // Any reads that might be invalid will simply return no data, so + // this will not result in crashes. + // The assumption is that this value might have been corrupted, but + // enough of the elf data is valid such that the code can still + // get symbol information. + str_end_ = UINT64_MAX; + } +} + +template +static bool IsFunc(const SymType* entry) { + return entry->st_shndx != SHN_UNDEF && ELF32_ST_TYPE(entry->st_info) == STT_FUNC; +} + +// Binary search the symbol table to find function containing the given address. +// Without remap, the symbol table is assumed to be sorted and accessed directly. +// If the symbol table is not sorted this method might fail but should not crash. +// When the indices are remapped, they are guaranteed to be sorted by address. +template +Symbols::Info* Symbols::BinarySearch(uint64_t addr, Memory* elf_memory, uint64_t* func_offset) { + // Fast-path: Check if the symbol has been already read from memory. + // Otherwise use the cache iterator to constrain the binary search range. + // (the symbol must be in the gap between this and the previous iterator) + auto it = symbols_.upper_bound(addr); + if (it != symbols_.end()) { + uint64_t sym_value = (it->first - it->second.size); // Function address. + if (sym_value <= addr) { + *func_offset = addr - sym_value; + return &it->second; + } + } + uint32_t count = RemapIndices ? remap_->size() : count_; + uint32_t last = (it != symbols_.end()) ? it->second.index : count; + uint32_t first = (it != symbols_.begin()) ? std::prev(it)->second.index + 1 : 0; + + while (first < last) { + uint32_t current = first + (last - first) / 2; + uint32_t symbol_index = RemapIndices ? remap_.value()[current] : current; + uint64_t offset = symbol_index * entry_size_; + if (__builtin_add_overflow(offset, offset_, &offset)) { + // The elf data might be malformed. + return nullptr; + } + SymType sym; + if (!elf_memory->ReadFully(offset, &sym, sizeof(sym))) { + return nullptr; + } + // There shouldn't be multiple symbols with same end address, but in case there are, + // overwrite the cache with the last entry, so that 'sym' and 'info' are consistent. + Info& info = symbols_[sym.st_value + sym.st_size]; + info = {.size = static_cast(sym.st_size), .index = current}; + if (addr < sym.st_value) { + last = current; + } else if (addr < sym.st_value + sym.st_size) { + *func_offset = addr - sym.st_value; + return &info; + } else { + first = current + 1; + } + } + return nullptr; +} + +// Create remapping table which allows us to access symbols as if they were sorted by address. +template +void Symbols::BuildRemapTable(Memory* elf_memory) { + std::vector addrs; // Addresses of all symbols (addrs[i] == symbols[i].st_value). + addrs.reserve(count_); + remap_.emplace(); // Construct the optional remap table. + remap_->reserve(count_); + for (size_t symbol_idx = 0; symbol_idx < count_;) { + // Read symbols from memory. We intentionally bypass the cache to save memory. + // Do the reads in batches so that we minimize the number of memory read calls. + uint64_t read_bytes = (count_ - symbol_idx) * entry_size_; + uint8_t buffer[1024]; + read_bytes = std::min(sizeof(buffer), read_bytes); + uint64_t offset = symbol_idx * entry_size_; + if (__builtin_add_overflow(offset, offset_, &offset)) { + // The elf data might be malformed. + break; + } + read_bytes = elf_memory->Read(offset, buffer, read_bytes); + if (read_bytes < sizeof(SymType)) { + // The elf data might be malformed. + break; + } + for (uint64_t offset = 0; offset <= read_bytes - sizeof(SymType); + offset += entry_size_, symbol_idx++) { + SymType sym; + memcpy(&sym, &buffer[offset], sizeof(SymType)); // Copy to ensure alignment. + addrs.push_back(sym.st_value); // Always insert so it is indexable by symbol index. + // NB: It is important to filter our zero-sized symbols since otherwise we can get + // duplicate end addresses in the table (e.g. if there is custom "end" symbol marker). + if (IsFunc(&sym) && sym.st_size != 0) { + remap_->push_back(symbol_idx); // Indices of function symbols only. + } + } + } + // Sort by address to make the remap list binary searchable (stable due to the abegin(), remap_->end(), comp); + // Remove duplicate entries (methods de-duplicated by the linker). + auto pred = [&addrs](auto a, auto b) { return addrs[a] == addrs[b]; }; + remap_->erase(std::unique(remap_->begin(), remap_->end(), pred), remap_->end()); + remap_->shrink_to_fit(); +} + +template +bool Symbols::GetName(uint64_t addr, Memory* elf_memory, SharedString* name, + uint64_t* func_offset) { + Info* info; + if (!remap_.has_value()) { + // Assume the symbol table is sorted. If it is not, this will gracefully fail. + info = BinarySearch(addr, elf_memory, func_offset); + if (info == nullptr) { + // Create the remapping table and retry the search. + BuildRemapTable(elf_memory); + symbols_.clear(); // Remove cached symbols since the access pattern will be different. + info = BinarySearch(addr, elf_memory, func_offset); + } + } else { + // Fast search using the previously created remap table. + info = BinarySearch(addr, elf_memory, func_offset); + } + if (info == nullptr) { + return false; + } + // Read and cache the symbol name. + if (info->name.is_null()) { + SymType sym; + uint32_t symbol_index = remap_.has_value() ? remap_.value()[info->index] : info->index; + uint64_t offset = symbol_index * entry_size_; + if (__builtin_add_overflow(offset, offset_, &offset)) { + // The elf data might be malformed. + return false; + } + if (!elf_memory->ReadFully(offset, &sym, sizeof(sym))) { + return false; + } + std::string symbol_name; + uint64_t str; + if (__builtin_add_overflow(str_offset_, sym.st_name, &str) || str >= str_end_) { + return false; + } + if (!IsFunc(&sym) || !elf_memory->ReadString(str, &symbol_name, str_end_ - str)) { + return false; + } + info->name = SharedString(std::move(symbol_name)); + } + *name = info->name; + return true; +} + +template +bool Symbols::GetGlobal(Memory* elf_memory, const std::string& name, uint64_t* memory_address) { + // Lookup from cache. + auto it = global_variables_.find(name); + if (it != global_variables_.end()) { + if (it->second.has_value()) { + *memory_address = it->second.value(); + return true; + } + return false; + } + + // Linear scan of all symbols. + for (uint32_t i = 0; i < count_; i++) { + uint64_t offset = i * entry_size_; + if (__builtin_add_overflow(offset_, offset, &offset)) { + // The elf data might be malformed. + return false; + } + SymType entry; + if (!elf_memory->ReadFully(offset, &entry, sizeof(entry))) { + return false; + } + + if (entry.st_shndx != SHN_UNDEF && ELF32_ST_TYPE(entry.st_info) == STT_OBJECT && + ELF32_ST_BIND(entry.st_info) == STB_GLOBAL) { + uint64_t str_offset = str_offset_ + entry.st_name; + if (__builtin_add_overflow(str_offset_, entry.st_name, &str_offset)) { + // The elf data might be malformed. + return false; + } + if (str_offset < str_end_) { + std::string symbol; + if (elf_memory->ReadString(str_offset, &symbol, str_end_ - str_offset) && symbol == name) { + global_variables_.emplace(name, entry.st_value); + *memory_address = entry.st_value; + return true; + } + } + } + } + global_variables_.emplace(name, std::optional()); // Remember "not found" outcome. + return false; +} + +// Instantiate all of the needed template functions. +template bool Symbols::GetName(uint64_t, Memory*, SharedString*, uint64_t*); +template bool Symbols::GetName(uint64_t, Memory*, SharedString*, uint64_t*); + +template bool Symbols::GetGlobal(Memory*, const std::string&, uint64_t*); +template bool Symbols::GetGlobal(Memory*, const std::string&, uint64_t*); +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.h new file mode 100644 index 0000000000..999c710f5b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Symbols.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include + +namespace unwindstack { + +// Forward declaration. +class Memory; + +class Symbols { + struct Info { + uint32_t size; // Symbol size in bytes. + uint32_t index; // Index into *sorted* symbol table. + SharedString name; + }; + + public: + Symbols(uint64_t offset, uint64_t size, uint64_t entry_size, uint64_t str_offset, + uint64_t str_size); + virtual ~Symbols() = default; + + template + bool GetName(uint64_t addr, Memory* elf_memory, SharedString* name, uint64_t* func_offset); + + template + bool GetGlobal(Memory* elf_memory, const std::string& name, uint64_t* memory_address); + + void ClearCache() { + symbols_.clear(); + remap_.reset(); + } + + private: + template + Info* BinarySearch(uint64_t addr, Memory* elf_memory, uint64_t* func_offset); + + template + void BuildRemapTable(Memory* elf_memory); + + const uint64_t offset_; + const uint64_t count_; + const uint64_t entry_size_; + const uint64_t str_offset_; + uint64_t str_end_; + + std::map symbols_; // Cache of read symbols (keyed by function *end* address). + std::optional> remap_; // Indices of function symbols sorted by address. + + // Cache of global data (non-function) symbols. + std::unordered_map> global_variables_; + + // Do not allow the total number of symbols to go above this. + constexpr static size_t kMaxSymbols = 1000000; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/TEST_MAPPING b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/TEST_MAPPING new file mode 100644 index 0000000000..d06ad37a57 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/TEST_MAPPING @@ -0,0 +1,22 @@ +{ + "presubmit": [ + { + "name": "libunwindstack_unit_test" + }, + { + "name": "CtsSimpleperfTestCases" + }, + { + "name": "debuggerd_test" + }, + { + "name": "CtsPerfettoTestCases" + } + ], + + "hwasan-presubmit": [ + { + "name": "libunwindstack_unit_test" + } + ] +} diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.cpp new file mode 100644 index 0000000000..0e62f09d15 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "ThreadEntry.h" + +namespace unwindstack { + +std::mutex ThreadEntry::entries_mutex_; +std::map ThreadEntry::entries_; + +// Assumes that ThreadEntry::entries_mutex_ has already been locked before +// creating a ThreadEntry object. +ThreadEntry::ThreadEntry(pid_t tid) : tid_(tid), ref_count_(1), wait_value_(0) { + // Add ourselves to the global list. + entries_[tid_] = this; +} + +ThreadEntry* ThreadEntry::Get(pid_t tid, bool create) { + ThreadEntry* entry = nullptr; + + std::lock_guard guard(entries_mutex_); + auto iter = entries_.find(tid); + if (iter == entries_.end()) { + if (create) { + entry = new ThreadEntry(tid); + } + } else { + entry = iter->second; + entry->ref_count_++; + } + + return entry; +} + +void ThreadEntry::Remove(ThreadEntry* entry) { + entry->Unlock(); + + std::lock_guard guard(entries_mutex_); + if (--entry->ref_count_ == 0) { + delete entry; + } +} + +// Assumes that ThreadEntry::entries_mutex_ has already been locked before +// deleting a ThreadEntry object. +ThreadEntry::~ThreadEntry() { + auto iter = entries_.find(tid_); + if (iter != entries_.end()) { + entries_.erase(iter); + } +} + +const char* ThreadEntry::GetWaitTypeName(WaitType type) { + switch (type) { + case WAIT_FOR_UCONTEXT: + return "ucontext"; + case WAIT_FOR_UNWIND_TO_COMPLETE: + return "unwind to complete"; + case WAIT_FOR_THREAD_TO_RESTART: + return "thread to restart"; + } +} + +bool ThreadEntry::Wait(WaitType type) { + static const std::chrono::duration wait_time(std::chrono::seconds(10)); + std::unique_lock lock(wait_mutex_); + if (wait_cond_.wait_for(lock, wait_time, [this, type] { return wait_value_ == type; })) { + return true; + } else { + Log::AsyncSafe("Timeout waiting for %s", GetWaitTypeName(type)); + return false; + } +} + +void ThreadEntry::Wake() { + wait_mutex_.lock(); + wait_value_++; + wait_mutex_.unlock(); + + wait_cond_.notify_one(); +} + +void ThreadEntry::CopyUcontextFromSigcontext(void* sigcontext) { + ucontext_t* ucontext = reinterpret_cast(sigcontext); + // The only thing the unwinder cares about is the mcontext data. + memcpy(&ucontext_.uc_mcontext, &ucontext->uc_mcontext, sizeof(ucontext->uc_mcontext)); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.h new file mode 100644 index 0000000000..32501e2b52 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadEntry.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace unwindstack { + +enum WaitType : int { + WAIT_FOR_UCONTEXT = 1, + WAIT_FOR_UNWIND_TO_COMPLETE, + WAIT_FOR_THREAD_TO_RESTART, +}; + +class ThreadEntry { + public: + static ThreadEntry* Get(pid_t tid, bool create = true); + + static void Remove(ThreadEntry* entry); + + void Wake(); + + bool Wait(WaitType type); + + void CopyUcontextFromSigcontext(void* sigcontext); + + inline void Lock() { + mutex_.lock(); + + // Always reset the wait value since this could be the first or nth + // time this entry is locked. + wait_value_ = 0; + } + + inline void Unlock() { mutex_.unlock(); } + + inline ucontext_t* GetUcontext() { return &ucontext_; } + + private: + ThreadEntry(pid_t tid); + ~ThreadEntry(); + + pid_t tid_; + int ref_count_; + std::mutex mutex_; + std::mutex wait_mutex_; + std::condition_variable wait_cond_; + int wait_value_; + ucontext_t ucontext_; + + static std::mutex entries_mutex_; + static std::map entries_; + + static const char* GetWaitTypeName(WaitType type); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadUnwinder.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadUnwinder.cpp new file mode 100644 index 0000000000..71835db6ca --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/ThreadUnwinder.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "ThreadEntry.h" + +namespace unwindstack { + +static void SignalLogOnly(int, siginfo_t*, void*) { + android::base::ErrnoRestorer restore; + + Log::AsyncSafe("pid %d, tid %d: Received a spurious thread signal\n", getpid(), + static_cast(android::base::GetThreadId())); +} + +static void SignalHandler(int, siginfo_t*, void* sigcontext) { + android::base::ErrnoRestorer restore; + + ThreadEntry* entry = ThreadEntry::Get(android::base::GetThreadId(), false); + if (!entry) { + return; + } + + entry->CopyUcontextFromSigcontext(sigcontext); + + // Indicate the ucontext is now valid. + entry->Wake(); + // Pause the thread until the unwind is complete. This avoids having + // the thread run ahead causing problems. + // The number indicates that we are waiting for the second Wake() call + // overall which is made by the thread requesting an unwind. + if (entry->Wait(WAIT_FOR_UNWIND_TO_COMPLETE)) { + // Do not remove the entry here because that can result in a deadlock + // if the code cannot properly send a signal to the thread under test. + entry->Wake(); + } + // If the wait fails, the entry might have been freed, so only exit. +} + +ThreadUnwinder::ThreadUnwinder(size_t max_frames, Maps* maps) + : UnwinderFromPid(max_frames, getpid(), Regs::CurrentArch(), maps) {} + +ThreadUnwinder::ThreadUnwinder(size_t max_frames, Maps* maps, + std::shared_ptr& process_memory) + : UnwinderFromPid(max_frames, getpid(), Regs::CurrentArch(), maps, process_memory) {} + +ThreadUnwinder::ThreadUnwinder(size_t max_frames, const ThreadUnwinder* unwinder) + : UnwinderFromPid(max_frames, getpid(), Regs::CurrentArch()) { + process_memory_ = unwinder->process_memory_; + maps_ = unwinder->maps_; + jit_debug_ = unwinder->jit_debug_; + dex_files_ = unwinder->dex_files_; + initted_ = unwinder->initted_; +} + +ThreadEntry* ThreadUnwinder::SendSignalToThread(int signal, pid_t tid) { + static std::mutex action_mutex; + std::lock_guard guard(action_mutex); + + ThreadEntry* entry = ThreadEntry::Get(tid); + entry->Lock(); + struct sigaction new_action = {.sa_sigaction = SignalHandler, + .sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK}; + struct sigaction old_action = {}; + sigemptyset(&new_action.sa_mask); + if (sigaction(signal, &new_action, &old_action) != 0) { + Log::AsyncSafe("sigaction failed: %s", strerror(errno)); + ThreadEntry::Remove(entry); + last_error_.code = ERROR_SYSTEM_CALL; + return nullptr; + } + + if (tgkill(getpid(), tid, signal) != 0) { + // Do not emit an error message, this might be expected. Set the + // error and let the caller decide. + if (errno == ESRCH) { + last_error_.code = ERROR_THREAD_DOES_NOT_EXIST; + } else { + last_error_.code = ERROR_SYSTEM_CALL; + } + + sigaction(signal, &old_action, nullptr); + ThreadEntry::Remove(entry); + return nullptr; + } + + // Wait for the thread to get the ucontext. The number indicates + // that we are waiting for the first Wake() call made by the thread. + bool wait_completed = entry->Wait(WAIT_FOR_UCONTEXT); + if (wait_completed) { + return entry; + } + + if (old_action.sa_sigaction == nullptr) { + // If the wait failed, it could be that the signal could not be delivered + // within the timeout. Add a signal handler that's simply going to log + // something so that we don't crash if the signal eventually gets + // delivered. Only do this if there isn't already an action set up. + struct sigaction log_action = {.sa_sigaction = SignalLogOnly, + .sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK}; + sigemptyset(&log_action.sa_mask); + sigaction(signal, &log_action, nullptr); + } else { + sigaction(signal, &old_action, nullptr); + } + + // Check to see if the thread has disappeared. + if (tgkill(getpid(), tid, 0) == -1 && errno == ESRCH) { + last_error_.code = ERROR_THREAD_DOES_NOT_EXIST; + } else { + last_error_.code = ERROR_THREAD_TIMEOUT; + } + + ThreadEntry::Remove(entry); + + return nullptr; +} + +void ThreadUnwinder::UnwindWithSignal(int signal, pid_t tid, std::unique_ptr* initial_regs, + const std::vector* initial_map_names_to_skip, + const std::vector* map_suffixes_to_ignore) { + ClearErrors(); + if (tid == static_cast(android::base::GetThreadId())) { + last_error_.code = ERROR_UNSUPPORTED; + return; + } + + if (!Init()) { + return; + } + + ThreadEntry* entry = SendSignalToThread(signal, tid); + if (entry == nullptr) { + return; + } + + std::unique_ptr regs(Regs::CreateFromUcontext(Regs::CurrentArch(), entry->GetUcontext())); + if (initial_regs != nullptr) { + initial_regs->reset(regs->Clone()); + } + SetRegs(regs.get()); + UnwinderFromPid::Unwind(initial_map_names_to_skip, map_suffixes_to_ignore); + + // Tell the signal handler to exit and release the entry. + entry->Wake(); + + // Wait for the thread to indicate it is done with the ThreadEntry. + // If this fails, the Wait command will log an error message. + entry->Wait(WAIT_FOR_THREAD_TO_RESTART); + + ThreadEntry::Remove(entry); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Unwinder.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Unwinder.cpp new file mode 100644 index 0000000000..d19888c339 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/Unwinder.cpp @@ -0,0 +1,468 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _GNU_SOURCE 1 +#define _IGNORE_DEX_PC true // this can give us more detailed stacktraces, but the backend should have dex info. +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "Check.h" + +// Use the demangler from libc++. +extern "C" char* __cxa_demangle(const char*, char*, size_t*, int* status); + +namespace unwindstack { + +// Inject extra 'virtual' frame that represents the dex pc data. +// The dex pc is a magic register defined in the Mterp interpreter, +// and thus it will be restored/observed in the frame after it. +// Adding the dex frame first here will create something like: +// #7 pc 0015fa20 core.vdex java.util.Arrays.binarySearch+8 +// #8 pc 006b1ba1 libartd.so ExecuteMterpImpl+14625 +// #9 pc 0039a1ef libartd.so art::interpreter::Execute+719 +void Unwinder::FillInDexFrame() { + size_t frame_num = frames_.size(); + frames_.resize(frame_num + 1); + FrameData* frame = &frames_.at(frame_num); + frame->num = frame_num; + + uint64_t dex_pc = regs_->dex_pc(); + frame->pc = dex_pc; + frame->sp = regs_->sp(); + + frame->map_info = maps_->Find(dex_pc); + if (frame->map_info != nullptr) { + frame->rel_pc = dex_pc - frame->map_info->start(); + // Initialize the load bias for this map so subsequent calls + // to GetLoadBias() will always return data. + frame->map_info->set_load_bias(0); + } else { + frame->rel_pc = dex_pc; + warnings_ |= WARNING_DEX_PC_NOT_IN_MAP; + return; + } + + if (!resolve_names_) { + return; + } + +#if defined(DEXFILE_SUPPORT) + if (dex_files_ == nullptr) { + return; + } + + dex_files_->GetFunctionName(maps_, dex_pc, &frame->function_name, &frame->function_offset); +#endif +} + +FrameData* Unwinder::FillInFrame(std::shared_ptr& map_info, Elf* /*elf*/, uint64_t rel_pc, + uint64_t pc_adjustment) { + size_t frame_num = frames_.size(); + frames_.resize(frame_num + 1); + FrameData* frame = &frames_.at(frame_num); + frame->num = frame_num; + frame->sp = regs_->sp(); + frame->rel_pc = rel_pc - pc_adjustment; + frame->pc = regs_->pc() - pc_adjustment; + + if (map_info == nullptr) { + // Nothing else to update. + return nullptr; + } + + frame->map_info = map_info; + + return frame; +} + +static bool ShouldStop(const std::vector* map_suffixes_to_ignore, + const std::string& map_name) { + if (map_suffixes_to_ignore == nullptr) { + return false; + } + auto pos = map_name.find_last_of('.'); + if (pos == std::string::npos) { + return false; + } + + return std::find(map_suffixes_to_ignore->begin(), map_suffixes_to_ignore->end(), + map_name.substr(pos + 1)) != map_suffixes_to_ignore->end(); +} + +void Unwinder::Unwind(const std::vector* initial_map_names_to_skip, + const std::vector* map_suffixes_to_ignore) { + CHECK(arch_ != ARCH_UNKNOWN); + ClearErrors(); + + frames_.clear(); + + // Clear any cached data from previous unwinds. + process_memory_->Clear(); + + if (maps_->Find(regs_->pc()) == nullptr) { + regs_->fallback_pc(); + } + + bool return_address_attempt = false; + bool adjust_pc = false; + for (; frames_.size() < max_frames_;) { + uint64_t cur_pc = regs_->pc(); + uint64_t cur_sp = regs_->sp(); + + std::shared_ptr map_info = maps_->Find(regs_->pc()); + uint64_t pc_adjustment = 0; + uint64_t step_pc; + uint64_t rel_pc; + Elf* elf; + bool ignore_frame = false; + if (map_info == nullptr) { + step_pc = regs_->pc(); + rel_pc = step_pc; + // If we get invalid map via return_address_attempt, don't hide error for the previous frame. + if (!return_address_attempt || last_error_.code == ERROR_NONE) { + last_error_.code = ERROR_INVALID_MAP; + last_error_.address = step_pc; + } + elf = nullptr; + } else { + ignore_frame = + initial_map_names_to_skip != nullptr && + std::find(initial_map_names_to_skip->begin(), initial_map_names_to_skip->end(), + android::base::Basename(map_info->name())) != initial_map_names_to_skip->end(); + if (!ignore_frame && ShouldStop(map_suffixes_to_ignore, map_info->name())) { + break; + } + elf = map_info->GetElf(process_memory_, arch_); + step_pc = regs_->pc(); + rel_pc = elf->GetRelPc(step_pc, map_info.get()); + // Everyone except elf data in gdb jit debug maps uses the relative pc. + if (!(map_info->flags() & MAPS_FLAGS_JIT_SYMFILE_MAP)) { + step_pc = rel_pc; + } + if (adjust_pc) { + pc_adjustment = GetPcAdjustment(rel_pc, elf, arch_); + } else { + pc_adjustment = 0; + } + step_pc -= pc_adjustment; + + // If the pc is in an invalid elf file, try and get an Elf object + // using the jit debug information. + if (!elf->valid() && jit_debug_ != nullptr && (map_info->flags() & PROT_EXEC)) { + uint64_t adjusted_jit_pc = regs_->pc() - pc_adjustment; + Elf* jit_elf = jit_debug_->Find(maps_, adjusted_jit_pc); + if (jit_elf != nullptr) { + // The jit debug information requires a non relative adjusted pc. + step_pc = adjusted_jit_pc; + elf = jit_elf; + } + } + } + + FrameData* frame = nullptr; + if (!ignore_frame) { + if (!_IGNORE_DEX_PC && regs_->dex_pc() != 0) { + // Add a frame to represent the dex file. + FillInDexFrame(); + // Clear the dex pc so that we don't repeat this frame later. + regs_->set_dex_pc(0); + + // Make sure there is enough room for the real frame. + if (frames_.size() == max_frames_) { + last_error_.code = ERROR_MAX_FRAMES_EXCEEDED; + break; + } + } + + frame = FillInFrame(map_info, elf, rel_pc, pc_adjustment); + + // Once a frame is added, stop skipping frames. + initial_map_names_to_skip = nullptr; + } + adjust_pc = true; + + bool stepped = false; + bool in_device_map = false; + bool finished = false; + if (map_info != nullptr) { + if (map_info->flags() & MAPS_FLAGS_DEVICE_MAP) { + // Do not stop here, fall through in case we are + // in the speculative unwind path and need to remove + // some of the speculative frames. + in_device_map = true; + } else { + auto sp_info = maps_->Find(regs_->sp()); + if (sp_info != nullptr && sp_info->flags() & MAPS_FLAGS_DEVICE_MAP) { + // Do not stop here, fall through in case we are + // in the speculative unwind path and need to remove + // some of the speculative frames. + in_device_map = true; + } else { + bool is_signal_frame = false; + if (elf->StepIfSignalHandler(rel_pc, regs_, process_memory_.get())) { + stepped = true; + is_signal_frame = true; + } else if (elf->Step(step_pc, regs_, process_memory_.get(), &finished, + &is_signal_frame)) { + stepped = true; + } + if (is_signal_frame && frame != nullptr) { + // Need to adjust the relative pc because the signal handler + // pc should not be adjusted. + frame->rel_pc = rel_pc; + frame->pc += pc_adjustment; + step_pc = rel_pc; + } + elf->GetLastError(&last_error_); + } + } + } + + if (frame != nullptr) { + if (!resolve_names_ || + !elf->GetFunctionName(step_pc, &frame->function_name, &frame->function_offset)) { + frame->function_name = ""; + frame->function_offset = 0; + } + } + + if (finished) { + break; + } + + if (!stepped) { + if (return_address_attempt) { + // Only remove the speculative frame if there are more than two frames + // or the pc in the first frame is in a valid map. + // This allows for a case where the code jumps into the middle of + // nowhere, but there is no other unwind information after that. + if (frames_.size() > 2 || (frames_.size() > 0 && maps_->Find(frames_[0].pc) != nullptr)) { + // Remove the speculative frame. + frames_.pop_back(); + } + break; + } else if (in_device_map) { + // Do not attempt any other unwinding, pc or sp is in a device + // map. + break; + } else { + // Steping didn't work, try this secondary method. + if (!regs_->SetPcFromReturnAddress(process_memory_.get())) { + break; + } + return_address_attempt = true; + } + } else { + return_address_attempt = false; + if (max_frames_ == frames_.size()) { + last_error_.code = ERROR_MAX_FRAMES_EXCEEDED; + } + } + + // If the pc and sp didn't change, then consider everything stopped. + if (cur_pc == regs_->pc() && cur_sp == regs_->sp()) { + last_error_.code = ERROR_REPEATED_FRAME; + break; + } + } +} + +std::string Unwinder::FormatFrame(const FrameData& frame) const { + return FormatFrame(arch_, frame, display_build_id_); +} + +std::string Unwinder::FormatFrame(ArchEnum arch, const FrameData& frame, bool display_build_id) { + std::string data; + if (ArchIs32Bit(arch)) { + data += android::base::StringPrintf(" #%02zu pc %08" PRIx64, frame.num, frame.rel_pc); + } else { + data += android::base::StringPrintf(" #%02zu pc %016" PRIx64, frame.num, frame.rel_pc); + } + + auto map_info = frame.map_info; + if (map_info == nullptr) { + // No valid map associated with this frame. + data += " "; + } else if (!map_info->name().empty()) { + data += " "; + data += map_info->GetFullName(); + } else { + data += android::base::StringPrintf(" ", map_info->start()); + } + + if (map_info != nullptr && map_info->elf_start_offset() != 0) { + data += android::base::StringPrintf(" (offset 0x%" PRIx64 ")", map_info->elf_start_offset()); + } + + if (!frame.function_name.empty()) { + char* demangled_name = __cxa_demangle(frame.function_name.c_str(), nullptr, nullptr, nullptr); + if (demangled_name == nullptr) { + data += " ("; + data += frame.function_name; + } else { + data += " ("; + data += demangled_name; + free(demangled_name); + } + if (frame.function_offset != 0) { + data += android::base::StringPrintf("+%" PRId64, frame.function_offset); + } + data += ')'; + } + + if (map_info != nullptr && display_build_id) { + std::string build_id = map_info->GetPrintableBuildID(); + if (!build_id.empty()) { + data += " (BuildId: " + build_id + ')'; + } + } + return data; +} + +std::string Unwinder::FormatFrame(size_t frame_num) const { + if (frame_num >= frames_.size()) { + return ""; + } + return FormatFrame(arch_, frames_[frame_num], display_build_id_); +} + +void Unwinder::SetJitDebug(JitDebug* jit_debug) { + jit_debug_ = jit_debug; +} + +void Unwinder::SetDexFiles(DexFiles* dex_files) { + dex_files_ = dex_files; +} + +bool UnwinderFromPid::Init() { + CHECK(arch_ != ARCH_UNKNOWN); + if (initted_) { + return true; + } + initted_ = true; + + if (maps_ == nullptr) { + if (pid_ == getpid()) { + maps_ptr_.reset(new LocalMaps()); + } else { + maps_ptr_.reset(new RemoteMaps(pid_)); + } + if (!maps_ptr_->Parse()) { + ClearErrors(); + last_error_.code = ERROR_INVALID_MAP; + return false; + } + maps_ = maps_ptr_.get(); + } + + if (process_memory_ == nullptr) { + if (pid_ == getpid()) { + // Local unwind, so use thread cache to allow multiple threads + // to cache data even when multiple threads access the same object. + process_memory_ = Memory::CreateProcessMemoryThreadCached(pid_); + } else { + // Remote unwind should be safe to cache since the unwind will + // be occurring on a stopped process. + process_memory_ = Memory::CreateProcessMemoryCached(pid_); + } + } + + jit_debug_ptr_ = CreateJitDebug(arch_, process_memory_); + jit_debug_ = jit_debug_ptr_.get(); + SetJitDebug(jit_debug_); +#if defined(DEXFILE_SUPPORT) + dex_files_ptr_ = CreateDexFiles(arch_, process_memory_); + dex_files_ = dex_files_ptr_.get(); + SetDexFiles(dex_files_); +#endif + + return true; +} + +void UnwinderFromPid::Unwind(const std::vector* initial_map_names_to_skip, + const std::vector* map_suffixes_to_ignore) { + if (!Init()) { + return; + } + Unwinder::Unwind(initial_map_names_to_skip, map_suffixes_to_ignore); +} + +FrameData Unwinder::BuildFrameFromPcOnly(uint64_t pc, ArchEnum arch, Maps* maps, + JitDebug* jit_debug, + std::shared_ptr process_memory, + bool resolve_names) { + FrameData frame; + + std::shared_ptr map_info = maps->Find(pc); + if (map_info == nullptr || arch == ARCH_UNKNOWN) { + frame.pc = pc; + frame.rel_pc = pc; + return frame; + } + + Elf* elf = map_info->GetElf(process_memory, arch); + + uint64_t relative_pc = elf->GetRelPc(pc, map_info.get()); + + uint64_t pc_adjustment = GetPcAdjustment(relative_pc, elf, arch); + relative_pc -= pc_adjustment; + // The debug PC may be different if the PC comes from the JIT. + uint64_t debug_pc = relative_pc; + + // If we don't have a valid ELF file, check the JIT. + if (!elf->valid() && jit_debug != nullptr) { + uint64_t jit_pc = pc - pc_adjustment; + Elf* jit_elf = jit_debug->Find(maps, jit_pc); + if (jit_elf != nullptr) { + debug_pc = jit_pc; + elf = jit_elf; + } + } + + // Copy all the things we need into the frame for symbolization. + frame.rel_pc = relative_pc; + frame.pc = pc - pc_adjustment; + frame.map_info = map_info; + + if (!resolve_names || + !elf->GetFunctionName(debug_pc, &frame.function_name, &frame.function_offset)) { + frame.function_name = ""; + frame.function_offset = 0; + } + return frame; +} + + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/file.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/file.cpp new file mode 100644 index 0000000000..17c2dbf99b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/file.cpp @@ -0,0 +1,569 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "android-base/file.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#endif +#if defined(_WIN32) +#include +#include +#define O_NOFOLLOW 0 +#define OS_PATH_SEPARATOR '\\' +#else +#define OS_PATH_SEPARATOR '/' +#endif + +#include "android-base/macros.h" // For TEMP_FAILURE_RETRY on Darwin. +#include "android-base/unique_fd.h" +#include "android-base/utf8.h" + +namespace { + +#ifdef _WIN32 +static int mkstemp(char* name_template, size_t size_in_chars) { + std::wstring path; + CHECK(android::base::UTF8ToWide(name_template, &path)) + << "path can't be converted to wchar: " << name_template; + if (_wmktemp_s(path.data(), path.size() + 1) != 0) { + return -1; + } + + // Use open() to match the close() that TemporaryFile's destructor does. + // Use O_BINARY to match base file APIs. + int fd = _wopen(path.c_str(), O_CREAT | O_EXCL | O_RDWR | O_BINARY, S_IRUSR | S_IWUSR); + if (fd < 0) { + return -1; + } + + std::string path_utf8; + CHECK(android::base::WideToUTF8(path, &path_utf8)) << "path can't be converted to utf8"; + CHECK(strcpy_s(name_template, size_in_chars, path_utf8.c_str()) == 0) + << "utf8 path can't be assigned back to name_template"; + + return fd; +} + +static char* mkdtemp(char* name_template, size_t size_in_chars) { + std::wstring path; + CHECK(android::base::UTF8ToWide(name_template, &path)) + << "path can't be converted to wchar: " << name_template; + + if (_wmktemp_s(path.data(), path.size() + 1) != 0) { + return nullptr; + } + + if (_wmkdir(path.c_str()) != 0) { + return nullptr; + } + + std::string path_utf8; + CHECK(android::base::WideToUTF8(path, &path_utf8)) << "path can't be converted to utf8"; + CHECK(strcpy_s(name_template, size_in_chars, path_utf8.c_str()) == 0) + << "utf8 path can't be assigned back to name_template"; + + return name_template; +} +#endif + +} // namespace + +namespace android { +namespace base { + +// Versions of standard library APIs that support UTF-8 strings. +using namespace android::base::utf8; + +bool ReadFdToString(borrowed_fd fd, std::string* content) { + content->clear(); + + // Although original we had small files in mind, this code gets used for + // very large files too, where the std::string growth heuristics might not + // be suitable. https://code.google.com/p/android/issues/detail?id=258500. + struct stat sb; + if (fstat(fd.get(), &sb) != -1 && sb.st_size > 0) { + content->reserve(sb.st_size); + } + + char buf[4096] __attribute__((__uninitialized__)); + ssize_t n; + while ((n = TEMP_FAILURE_RETRY(read(fd.get(), &buf[0], sizeof(buf)))) > 0) { + content->append(buf, n); + } + return (n == 0) ? true : false; +} + +bool ReadFileToString(const std::string& path, std::string* content, bool follow_symlinks) { + content->clear(); + + int flags = O_RDONLY | O_CLOEXEC | O_BINARY | (follow_symlinks ? 0 : O_NOFOLLOW); + android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(path.c_str(), flags))); + if (fd == -1) { + return false; + } + return ReadFdToString(fd, content); +} + +bool WriteStringToFd(std::string_view content, borrowed_fd fd) { + const char* p = content.data(); + size_t left = content.size(); + while (left > 0) { + ssize_t n = TEMP_FAILURE_RETRY(write(fd.get(), p, left)); + if (n == -1) { + return false; + } + p += n; + left -= n; + } + return true; +} + +static bool CleanUpAfterFailedWrite(const std::string& path) { + // Something went wrong. Let's not leave a corrupt file lying around. + int saved_errno = errno; + unlink(path.c_str()); + errno = saved_errno; + return false; +} + +#if !defined(_WIN32) +bool WriteStringToFile(const std::string& content, const std::string& path, + mode_t mode, uid_t owner, gid_t group, + bool follow_symlinks) { + int flags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_BINARY | + (follow_symlinks ? 0 : O_NOFOLLOW); + android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode))); + if (fd == -1) { + unwindstack::Log::Error("android::WriteStringToFile open failed"); + return false; + } + + // We do an explicit fchmod here because we assume that the caller really + // meant what they said and doesn't want the umask-influenced mode. + if (fchmod(fd, mode) == -1) { + unwindstack::Log::Error("android::WriteStringToFile fchmod failed"); + return CleanUpAfterFailedWrite(path); + } + if (fchown(fd, owner, group) == -1) { + unwindstack::Log::Error("android::WriteStringToFile fchown failed"); + return CleanUpAfterFailedWrite(path); + } + if (!WriteStringToFd(content, fd)) { + unwindstack::Log::Error("android::WriteStringToFile write failed"); + return CleanUpAfterFailedWrite(path); + } + return true; +} +#endif + +bool WriteStringToFile(const std::string& content, const std::string& path, + bool follow_symlinks) { + int flags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_BINARY | + (follow_symlinks ? 0 : O_NOFOLLOW); + android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(path.c_str(), flags, 0666))); + if (fd == -1) { + return false; + } + return WriteStringToFd(content, fd) || CleanUpAfterFailedWrite(path); +} + +bool ReadFully(borrowed_fd fd, void* data, size_t byte_count) { + uint8_t* p = reinterpret_cast(data); + size_t remaining = byte_count; + while (remaining > 0) { + ssize_t n = TEMP_FAILURE_RETRY(read(fd.get(), p, remaining)); + if (n <= 0) return false; + p += n; + remaining -= n; + } + return true; +} + +#if defined(_WIN32) +// Windows implementation of pread. Note that this DOES move the file descriptors read position, +// but it does so atomically. +static ssize_t pread(borrowed_fd fd, void* data, size_t byte_count, off64_t offset) { + DWORD bytes_read; + OVERLAPPED overlapped; + memset(&overlapped, 0, sizeof(OVERLAPPED)); + overlapped.Offset = static_cast(offset); + overlapped.OffsetHigh = static_cast(offset >> 32); + if (!ReadFile(reinterpret_cast(_get_osfhandle(fd.get())), data, + static_cast(byte_count), &bytes_read, &overlapped)) { + // In case someone tries to read errno (since this is masquerading as a POSIX call) + errno = EIO; + return -1; + } + return static_cast(bytes_read); +} + +static ssize_t pwrite(borrowed_fd fd, const void* data, size_t byte_count, off64_t offset) { + DWORD bytes_written; + OVERLAPPED overlapped; + memset(&overlapped, 0, sizeof(OVERLAPPED)); + overlapped.Offset = static_cast(offset); + overlapped.OffsetHigh = static_cast(offset >> 32); + if (!WriteFile(reinterpret_cast(_get_osfhandle(fd.get())), data, + static_cast(byte_count), &bytes_written, &overlapped)) { + // In case someone tries to read errno (since this is masquerading as a POSIX call) + errno = EIO; + return -1; + } + return static_cast(bytes_written); +} +#endif + +bool ReadFullyAtOffset(borrowed_fd fd, void* data, size_t byte_count, off64_t offset) { + uint8_t* p = reinterpret_cast(data); + while (byte_count > 0) { + ssize_t n = TEMP_FAILURE_RETRY(pread(fd.get(), p, byte_count, offset)); + if (n <= 0) return false; + p += n; + byte_count -= n; + offset += n; + } + return true; +} + +bool WriteFullyAtOffset(borrowed_fd fd, const void* data, size_t byte_count, off64_t offset) { + const uint8_t* p = reinterpret_cast(data); + size_t remaining = byte_count; + while (remaining > 0) { + ssize_t n = TEMP_FAILURE_RETRY(pwrite(fd.get(), p, remaining, offset)); + if (n == -1) return false; + p += n; + remaining -= n; + offset += n; + } + return true; +} + +bool WriteFully(borrowed_fd fd, const void* data, size_t byte_count) { + const uint8_t* p = reinterpret_cast(data); + size_t remaining = byte_count; + while (remaining > 0) { + ssize_t n = TEMP_FAILURE_RETRY(write(fd.get(), p, remaining)); + if (n == -1) return false; + p += n; + remaining -= n; + } + return true; +} + +bool RemoveFileIfExists(const std::string& path, std::string* err) { + struct stat st; +#if defined(_WIN32) + // TODO: Windows version can't handle symbolic links correctly. + int result = stat(path.c_str(), &st); + bool file_type_removable = (result == 0 && S_ISREG(st.st_mode)); +#else + int result = lstat(path.c_str(), &st); + bool file_type_removable = (result == 0 && (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode))); +#endif + if (result == -1) { + if (errno == ENOENT || errno == ENOTDIR) return true; + if (err != nullptr) *err = strerror(errno); + return false; + } + + if (result == 0) { + if (!file_type_removable) { + if (err != nullptr) { + *err = "is not a regular file or symbolic link"; + } + return false; + } + if (unlink(path.c_str()) == -1) { + if (err != nullptr) { + *err = strerror(errno); + } + return false; + } + } + return true; +} + +#if !defined(_WIN32) +bool Readlink(const std::string& path, std::string* result) { + result->clear(); + + // Most Linux file systems (ext2 and ext4, say) limit symbolic links to + // 4095 bytes. Since we'll copy out into the string anyway, it doesn't + // waste memory to just start there. We add 1 so that we can recognize + // whether it actually fit (rather than being truncated to 4095). + std::vector buf(4095 + 1); + while (true) { + ssize_t size = readlink(path.c_str(), &buf[0], buf.size()); + // Unrecoverable error? + if (size == -1) return false; + // It fit! (If size == buf.size(), it may have been truncated.) + if (static_cast(size) < buf.size()) { + result->assign(&buf[0], size); + return true; + } + // Double our buffer and try again. + buf.resize(buf.size() * 2); + } +} +#endif + +#if !defined(_WIN32) +bool Realpath(const std::string& path, std::string* result) { + result->clear(); + + // realpath may exit with EINTR. Retry if so. + char* realpath_buf = nullptr; + do { + realpath_buf = realpath(path.c_str(), nullptr); + } while (realpath_buf == nullptr && errno == EINTR); + + if (realpath_buf == nullptr) { + return false; + } + result->assign(realpath_buf); + free(realpath_buf); + return true; +} +#endif + +std::string GetExecutablePath() { +#if defined(__linux__) + std::string path; + android::base::Readlink("/proc/self/exe", &path); + return path; +#elif defined(__APPLE__) + char path[PATH_MAX + 1]; + uint32_t path_len = sizeof(path); + int rc = _NSGetExecutablePath(path, &path_len); + if (rc < 0) { + std::unique_ptr path_buf(new char[path_len]); + _NSGetExecutablePath(path_buf.get(), &path_len); + return path_buf.get(); + } + return path; +#elif defined(_WIN32) + char path[PATH_MAX + 1]; + DWORD result = GetModuleFileName(NULL, path, sizeof(path) - 1); + if (result == 0 || result == sizeof(path) - 1) return ""; + path[PATH_MAX - 1] = 0; + return path; +#else +#error unknown OS +#endif +} + +std::string GetExecutableDirectory() { + return Dirname(GetExecutablePath()); +} + +#if defined(_WIN32) +std::string Basename(std::string_view path) { + // TODO: how much of this is actually necessary for mingw? + + // Copy path because basename may modify the string passed in. + std::string result(path); + + // Use lock because basename() may write to a process global and return a + // pointer to that. Note that this locking strategy only works if all other + // callers to basename in the process also grab this same lock, but its + // better than nothing. Bionic's basename returns a thread-local buffer. + static std::mutex& basename_lock = *new std::mutex(); + std::lock_guard lock(basename_lock); + + // Note that if std::string uses copy-on-write strings, &str[0] will cause + // the copy to be made, so there is no chance of us accidentally writing to + // the storage for 'path'. + char* name = basename(&result[0]); + + // In case basename returned a pointer to a process global, copy that string + // before leaving the lock. + result.assign(name); + + return result; +} +#else +// Copied from bionic so that Basename() below can be portable and thread-safe. +static int _basename_r(const char* path, size_t path_size, char* buffer, size_t buffer_size) { + const char* startp = nullptr; + const char* endp = nullptr; + int len; + int result; + + // Empty or NULL string gets treated as ".". + if (path == nullptr || path_size == 0) { + startp = "."; + len = 1; + goto Exit; + } + + // Strip trailing slashes. + endp = path + path_size - 1; + while (endp > path && *endp == '/') { + endp--; + } + + // All slashes becomes "/". + if (endp == path && *endp == '/') { + startp = "/"; + len = 1; + goto Exit; + } + + // Find the start of the base. + startp = endp; + while (startp > path && *(startp - 1) != '/') { + startp--; + } + + len = endp - startp +1; + + Exit: + result = len; + if (buffer == nullptr) { + return result; + } + if (len > static_cast(buffer_size) - 1) { + len = buffer_size - 1; + result = -1; + errno = ERANGE; + } + + if (len >= 0) { + memcpy(buffer, startp, len); + buffer[len] = 0; + } + return result; +} +std::string Basename(std::string_view path) { + char buf[PATH_MAX] __attribute__((__uninitialized__)); + const auto size = _basename_r(path.data(), path.size(), buf, sizeof(buf)); + return size > 0 ? std::string(buf, size) : std::string(); +} +#endif + +#if defined(_WIN32) +std::string Dirname(std::string_view path) { + // TODO: how much of this is actually necessary for mingw? + + // Copy path because dirname may modify the string passed in. + std::string result(path); + + // Use lock because dirname() may write to a process global and return a + // pointer to that. Note that this locking strategy only works if all other + // callers to dirname in the process also grab this same lock, but its + // better than nothing. Bionic's dirname returns a thread-local buffer. + static std::mutex& dirname_lock = *new std::mutex(); + std::lock_guard lock(dirname_lock); + + // Note that if std::string uses copy-on-write strings, &str[0] will cause + // the copy to be made, so there is no chance of us accidentally writing to + // the storage for 'path'. + char* parent = dirname(&result[0]); + + // In case dirname returned a pointer to a process global, copy that string + // before leaving the lock. + result.assign(parent); + + return result; +} +#else +// Copied from bionic so that Dirname() below can be portable and thread-safe. +static int _dirname_r(const char* path, size_t path_size, char* buffer, size_t buffer_size) { + const char* endp = nullptr; + int len; + int result; + + // Empty or NULL string gets treated as ".". + if (path == nullptr || path_size == 0) { + path = "."; + len = 1; + goto Exit; + } + + // Strip trailing slashes. + endp = path + path_size - 1; + while (endp > path && *endp == '/') { + endp--; + } + + // Find the start of the dir. + while (endp > path && *endp != '/') { + endp--; + } + + // Either the dir is "/" or there are no slashes. + if (endp == path) { + path = (*endp == '/') ? "/" : "."; + len = 1; + goto Exit; + } + + do { + endp--; + } while (endp > path && *endp == '/'); + + len = endp - path + 1; + + Exit: + result = len; + if (len + 1 > MAXPATHLEN) { + errno = ENAMETOOLONG; + return -1; + } + if (buffer == nullptr) { + return result; + } + + if (len > static_cast(buffer_size) - 1) { + len = buffer_size - 1; + result = -1; + errno = ERANGE; + } + + if (len >= 0) { + memcpy(buffer, path, len); + buffer[len] = 0; + } + return result; +} +std::string Dirname(std::string_view path) { + char buf[PATH_MAX] __attribute__((__uninitialized__)); + const auto size = _dirname_r(path.data(), path.size(), buf, sizeof(buf)); + return size > 0 ? std::string(buf, size) : std::string(); +} +#endif + +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/log_main.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/log_main.h new file mode 100644 index 0000000000..3caf683db4 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/log_main.h @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2005-2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _LIBS_LOG_LOG_MAIN_H +#define _LIBS_LOG_LOG_MAIN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Normally we strip the effects of ALOGV (VERBOSE messages), + * LOG_FATAL and LOG_FATAL_IF (FATAL assert messages) from the + * release builds be defining NDEBUG. You can modify this (for + * example with "#define LOG_NDEBUG 0" at the top of your source + * file) to change that behavior. + */ + +#ifndef LOG_NDEBUG +#ifdef NDEBUG +#define LOG_NDEBUG 1 +#else +#define LOG_NDEBUG 0 +#endif +#endif + +/* --------------------------------------------------------------------- */ + +/* + * This file uses ", ## __VA_ARGS__" zero-argument token pasting to + * work around issues with debug-only syntax errors in assertions + * that are missing format strings. See commit + * 19299904343daf191267564fe32e6cd5c165cd42 + */ +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif + +#ifndef __predict_false +#define __predict_false(exp) __builtin_expect((exp) != 0, 0) +#endif + +#define android_writeLog(prio, tag, text) __android_log_write(prio, tag, text) + +#define android_printLog(prio, tag, ...) \ + __android_log_print(prio, tag, __VA_ARGS__) + +#define android_vprintLog(prio, cond, tag, ...) \ + __android_log_vprint(prio, tag, __VA_ARGS__) + +/* + * Log macro that allows you to specify a number for the priority. + */ +#ifndef LOG_PRI +#define LOG_PRI(priority, tag, ...) android_printLog(priority, tag, __VA_ARGS__) +#endif + +/* + * Log macro that allows you to pass in a varargs ("args" is a va_list). + */ +#ifndef LOG_PRI_VA +#define LOG_PRI_VA(priority, tag, fmt, args) \ + android_vprintLog(priority, NULL, tag, fmt, args) +#endif + +/* --------------------------------------------------------------------- */ + +/* XXX Macros to work around syntax errors in places where format string + * arg is not passed to ALOG_ASSERT, LOG_ALWAYS_FATAL or LOG_ALWAYS_FATAL_IF + * (happens only in debug builds). + */ + +/* Returns 2nd arg. Used to substitute default value if caller's vararg list + * is empty. + */ +#define __android_second(dummy, second, ...) second + +/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise + * returns nothing. + */ +#define __android_rest(first, ...) , ##__VA_ARGS__ + +#define android_printAssert(cond, tag, ...) \ + __android_log_assert(cond, tag, \ + __android_second(0, ##__VA_ARGS__, NULL) \ + __android_rest(__VA_ARGS__)) + +/* + * Log a fatal error. If the given condition fails, this stops program + * execution like a normal assertion, but also generating the given message. + * It is NOT stripped from release builds. Note that the condition test + * is -inverted- from the normal assert() semantics. + */ +#ifndef LOG_ALWAYS_FATAL_IF +#define LOG_ALWAYS_FATAL_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \ + : (void)0) +#endif + +#ifndef LOG_ALWAYS_FATAL +#define LOG_ALWAYS_FATAL(...) \ + (((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__))) +#endif + +/* + * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that + * are stripped out of release builds. + */ + +#if LOG_NDEBUG + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) ((void)0) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) ((void)0) +#endif + +#else + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__) +#endif + +#endif + +/* + * Assertion that generates a log message when the assertion fails. + * Stripped out of release builds. Uses the current LOG_TAG. + */ +#ifndef ALOG_ASSERT +#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * C/C++ logging functions. See the logging documentation for API details. + * + * We'd like these to be available from C code (in case we import some from + * somewhere), so this has a C interface. + * + * The output will be correct when the log file is shared between multiple + * threads and/or multiple processes so long as the operating system + * supports O_APPEND. These calls have mutex-protected data structures + * and so are NOT reentrant. Do not use LOG in a signal handler. + */ + +/* --------------------------------------------------------------------- */ + +/* + * Simplified macro to send a verbose log message using the current LOG_TAG. + */ +#ifndef ALOGV +#define __ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) +#if LOG_NDEBUG +#define ALOGV(...) \ + do { \ + if (0) { \ + __ALOGV(__VA_ARGS__); \ + } \ + } while (0) +#else +#define ALOGV(...) __ALOGV(__VA_ARGS__) +#endif +#endif + +#ifndef ALOGV_IF +#if LOG_NDEBUG +#define ALOGV_IF(cond, ...) ((void)0) +#else +#define ALOGV_IF(cond, ...) \ + ((__predict_false(cond)) ? ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) \ + : (void)0) +#endif +#endif + +/* + * Simplified macro to send a debug log message using the current LOG_TAG. + */ +#ifndef ALOGD +#define ALOGD(...) ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGD_IF +#define ALOGD_IF(cond, ...) \ + ((__predict_false(cond)) ? ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) \ + : (void)0) +#endif + +/* + * Simplified macro to send an info log message using the current LOG_TAG. + */ +#ifndef ALOGI +#define ALOGI(...) ((void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGI_IF +#define ALOGI_IF(cond, ...) \ + ((__predict_false(cond)) ? ((void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) \ + : (void)0) +#endif + +/* + * Simplified macro to send a warning log message using the current LOG_TAG. + */ +#ifndef ALOGW +#define ALOGW(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGW_IF +#define ALOGW_IF(cond, ...) \ + ((__predict_false(cond)) ? ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) \ + : (void)0) +#endif + +/* + * Simplified macro to send an error log message using the current LOG_TAG. + */ +#ifndef ALOGE +#define ALOGE(...) ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGE_IF +#define ALOGE_IF(cond, ...) \ + ((__predict_false(cond)) ? ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) \ + : (void)0) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * verbose priority. + */ +#ifndef IF_ALOGV +#if LOG_NDEBUG +#define IF_ALOGV() if (false) +#else +#define IF_ALOGV() IF_ALOG(LOG_VERBOSE, LOG_TAG) +#endif +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * debug priority. + */ +#ifndef IF_ALOGD +#define IF_ALOGD() IF_ALOG(LOG_DEBUG, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * info priority. + */ +#ifndef IF_ALOGI +#define IF_ALOGI() IF_ALOG(LOG_INFO, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * warn priority. + */ +#ifndef IF_ALOGW +#define IF_ALOGW() IF_ALOG(LOG_WARN, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * error priority. + */ +#ifndef IF_ALOGE +#define IF_ALOGE() IF_ALOG(LOG_ERROR, LOG_TAG) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * Basic log message macro. + * + * Example: + * ALOG(LOG_WARN, NULL, "Failed with error %d", errno); + * + * The second argument may be NULL or "" to indicate the "global" tag. + */ +#ifndef ALOG +#define ALOG(priority, tag, ...) LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__) +#endif + +/* + * Conditional given a desired logging priority and tag. + */ +#ifndef IF_ALOG +#define IF_ALOG(priority, tag) if (android_testLog(ANDROID_##priority, tag)) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * IF_ALOG uses android_testLog, but IF_ALOG can be overridden. + * android_testLog will remain constant in its purpose as a wrapper + * for Android logging filter policy, and can be subject to + * change. It can be reused by the developers that override + * IF_ALOG as a convenient means to reimplement their policy + * over Android. + */ + +#ifndef __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE +#ifndef __ANDROID_API__ +#define __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE 2 +#elif __ANDROID_API__ > 24 /* > Nougat */ +#define __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE 2 +#elif __ANDROID_API__ > 22 /* > Lollipop */ +#define __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE 1 +#else +#define __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE 0 +#endif +#endif + +#if __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE + +/* + * Use the per-tag properties "log.tag." to generate a runtime + * result of non-zero to expose a log. prio is ANDROID_LOG_VERBOSE to + * ANDROID_LOG_FATAL. default_prio if no property. Undefined behavior if + * any other value. + */ +int __android_log_is_loggable(int prio, const char* tag, int default_prio); + +#if __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE > 1 +#include + +int __android_log_is_loggable_len(int prio, const char* tag, size_t len, + int default_prio); + +#if LOG_NDEBUG /* Production */ +#define android_testLog(prio, tag) \ + (__android_log_is_loggable_len(prio, tag, (tag && *tag) ? strlen(tag) : 0, \ + ANDROID_LOG_DEBUG) != 0) +#else +#define android_testLog(prio, tag) \ + (__android_log_is_loggable_len(prio, tag, (tag && *tag) ? strlen(tag) : 0, \ + ANDROID_LOG_VERBOSE) != 0) +#endif + +#else + +#if LOG_NDEBUG /* Production */ +#define android_testLog(prio, tag) \ + (__android_log_is_loggable(prio, tag, ANDROID_LOG_DEBUG) != 0) +#else +#define android_testLog(prio, tag) \ + (__android_log_is_loggable(prio, tag, ANDROID_LOG_VERBOSE) != 0) +#endif + +#endif + +#else /* __ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE */ + +#define android_testLog(prio, tag) (1) + +#endif /* !__ANDROID_USE_LIBLOG_LOGGABLE_INTERFACE */ + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBS_LOG_LOG_MAIN_H */ diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.cpp new file mode 100644 index 0000000000..e83ab1316f --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "android-base/stringprintf.h" + +#include + +#include + +namespace android { +namespace base { + +void StringAppendV(std::string* dst, const char* format, va_list ap) { + // First try with a small fixed size buffer + char space[1024] __attribute__((__uninitialized__)); + + // It's possible for methods that use a va_list to invalidate + // the data in it upon use. The fix is to make a copy + // of the structure before using it and use that copy instead. + va_list backup_ap; + va_copy(backup_ap, ap); + int result = vsnprintf(space, sizeof(space), format, backup_ap); + va_end(backup_ap); + + if (result < static_cast(sizeof(space))) { + if (result >= 0) { + // Normal case -- everything fit. + dst->append(space, result); + return; + } + + if (result < 0) { + // Just an error. + return; + } + } + + // Increase the buffer size to the size requested by vsnprintf, + // plus one for the closing \0. + int length = result + 1; + char* buf = new char[length]; + + // Restore the va_list before we use it again + va_copy(backup_ap, ap); + result = vsnprintf(buf, length, format, backup_ap); + va_end(backup_ap); + + if (result >= 0 && result < length) { + // It fit + dst->append(buf, result); + } + delete[] buf; +} + +std::string StringPrintf(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + std::string result; + StringAppendV(&result, fmt, ap); + va_end(ap); + return result; +} + +void StringAppendF(std::string* dst, const char* format, ...) { + va_list ap; + va_start(ap, format); + StringAppendV(dst, format, ap); + va_end(ap); +} + +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.h new file mode 100644 index 0000000000..cf666abe0f --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/stringprintf.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_BASE_STRINGPRINTF_H +#define ANDROID_BASE_STRINGPRINTF_H + +#include +#include + +namespace android { +namespace base { + +// These printf-like functions are implemented in terms of vsnprintf, so they +// use the same attribute for compile-time format string checking. On Windows, +// if the mingw version of vsnprintf is used, use `gnu_printf' which allows z +// in %zd and PRIu64 (and related) to be recognized by the compile-time +// checking. +#define FORMAT_ARCHETYPE __printf__ +#ifdef __USE_MINGW_ANSI_STDIO +#if __USE_MINGW_ANSI_STDIO +#undef FORMAT_ARCHETYPE +#define FORMAT_ARCHETYPE gnu_printf +#endif +#endif + +// Returns a string corresponding to printf-like formatting of the arguments. +std::string StringPrintf(const char* fmt, ...) + __attribute__((__format__(FORMAT_ARCHETYPE, 1, 2))); + +// Appends a printf-like formatting of the arguments to 'dst'. +void StringAppendF(std::string* dst, const char* fmt, ...) + __attribute__((__format__(FORMAT_ARCHETYPE, 2, 3))); + +// Appends a printf-like formatting of the arguments to 'dst'. +void StringAppendV(std::string* dst, const char* format, va_list ap) + __attribute__((__format__(FORMAT_ARCHETYPE, 2, 0))); + +#undef FORMAT_ARCHETYPE + +} // namespace base +} // namespace android + +#endif // ANDROID_BASE_STRINGPRINTF_H diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/strings.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/strings.cpp new file mode 100644 index 0000000000..2d4dc9d692 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/strings.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "android-base/strings.h" + +#include "android-base/stringprintf.h" + +#include +#include + +#include +#include + +namespace android { +namespace base { + +#define CHECK_NE(a, b) \ + if ((a) == (b)) abort(); + +std::vector Split(const std::string& s, + const std::string& delimiters) { + CHECK_NE(delimiters.size(), 0U); + + std::vector result; + + size_t base = 0; + size_t found; + while (true) { + found = s.find_first_of(delimiters, base); + result.push_back(s.substr(base, found - base)); + if (found == s.npos) break; + base = found + 1; + } + + return result; +} + +std::vector Tokenize(const std::string& s, const std::string& delimiters) { + CHECK_NE(delimiters.size(), 0U); + + std::vector result; + size_t end = 0; + + while (true) { + size_t base = s.find_first_not_of(delimiters, end); + if (base == s.npos) { + break; + } + end = s.find_first_of(delimiters, base); + result.push_back(s.substr(base, end - base)); + } + return result; +} + +[[deprecated("Retained only for binary compatibility (symbol name)")]] +std::string Trim(const std::string& s) { + return Trim(std::string_view(s)); +} + +template std::string Trim(const char*&); +template std::string Trim(const char*&&); +template std::string Trim(const std::string&); +template std::string Trim(const std::string&&); +template std::string Trim(std::string_view&); +template std::string Trim(std::string_view&&); + +// These cases are probably the norm, so we mark them extern in the header to +// aid compile time and binary size. +template std::string Join(const std::vector&, char); +template std::string Join(const std::vector&, char); +template std::string Join(const std::vector&, const std::string&); +template std::string Join(const std::vector&, const std::string&); + +bool StartsWith(std::string_view s, std::string_view prefix) { + return s.substr(0, prefix.size()) == prefix; +} + +bool StartsWith(std::string_view s, char prefix) { + return !s.empty() && s.front() == prefix; +} + +bool StartsWithIgnoreCase(std::string_view s, std::string_view prefix) { + return s.size() >= prefix.size() && strncasecmp(s.data(), prefix.data(), prefix.size()) == 0; +} + +bool EndsWith(std::string_view s, std::string_view suffix) { + return s.size() >= suffix.size() && s.substr(s.size() - suffix.size(), suffix.size()) == suffix; +} + +bool EndsWith(std::string_view s, char suffix) { + return !s.empty() && s.back() == suffix; +} + +bool EndsWithIgnoreCase(std::string_view s, std::string_view suffix) { + return s.size() >= suffix.size() && + strncasecmp(s.data() + (s.size() - suffix.size()), suffix.data(), suffix.size()) == 0; +} + +bool EqualsIgnoreCase(std::string_view lhs, std::string_view rhs) { + return lhs.size() == rhs.size() && strncasecmp(lhs.data(), rhs.data(), lhs.size()) == 0; +} + +std::string StringReplace(std::string_view s, std::string_view from, std::string_view to, + bool all) { + if (from.empty()) return std::string(s); + + std::string result; + std::string_view::size_type start_pos = 0; + do { + std::string_view::size_type pos = s.find(from, start_pos); + if (pos == std::string_view::npos) break; + + result.append(s.data() + start_pos, pos - start_pos); + result.append(to.data(), to.size()); + + start_pos = pos + from.size(); + } while (all); + result.append(s.data() + start_pos, s.size() - start_pos); + return result; +} + +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/threads.cpp b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/threads.cpp new file mode 100644 index 0000000000..6d7d7c6ccc --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/threads.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#if defined(__APPLE__) +#include +#elif defined(__linux__) && !defined(__ANDROID__) +#include +#elif defined(_WIN32) +#include +#endif + +namespace android { +namespace base { + +uint64_t GetThreadId() { +#if defined(__BIONIC__) + return gettid(); +#elif defined(__APPLE__) + uint64_t tid; + pthread_threadid_np(NULL, &tid); + return tid; +#elif defined(__linux__) + return syscall(__NR_gettid); +#elif defined(_WIN32) + return GetCurrentThreadId(); +#endif +} + +} // namespace base +} // namespace android + +#if defined(__GLIBC__) || defined(ANDROID_HOST_MUSL) +int tgkill(int tgid, int tid, int sig) { + return syscall(__NR_tgkill, tgid, tid, sig); +} +#endif diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/unique_fd.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/unique_fd.h new file mode 100644 index 0000000000..6cfcfcd379 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/android-base/unique_fd.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_BASE_UNIQUE_FD_H +#define ANDROID_BASE_UNIQUE_FD_H + +#include + +// DO NOT INCLUDE OTHER LIBBASE HEADERS! +// This file gets used in libbinder, and libbinder is used everywhere. +// Including other headers from libbase frequently results in inclusion of +// android-base/macros.h, which causes macro collisions. + +// Container for a file descriptor that automatically closes the descriptor as +// it goes out of scope. +// +// unique_fd ufd(open("/some/path", "r")); +// if (ufd.get() == -1) return error; +// +// // Do something useful, possibly including 'return'. +// +// return 0; // Descriptor is closed for you. +// +// unique_fd is also known as ScopedFd/ScopedFD/scoped_fd; mentioned here to help +// you find this class if you're searching for one of those names. +namespace android { +namespace base { + +struct DefaultCloser { + static void Close(int fd) { + // Even if close(2) fails with EINTR, the fd will have been closed. + // Using TEMP_FAILURE_RETRY will either lead to EBADF or closing someone + // else's fd. + // http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html + ::close(fd); + } +}; + +template +class unique_fd_impl final { + public: + unique_fd_impl() : value_(-1) {} + + explicit unique_fd_impl(int value) : value_(value) {} + ~unique_fd_impl() { reset(); } + + unique_fd_impl(unique_fd_impl&& other) : value_(other.release()) {} + unique_fd_impl& operator=(unique_fd_impl&& s) { + reset(s.release()); + return *this; + } + + void reset(int new_value = -1) { + if (value_ != -1) { + Closer::Close(value_); + } + value_ = new_value; + } + + int get() const { return value_; } + operator int() const { return get(); } + + int release() __attribute__((warn_unused_result)) { + int ret = value_; + value_ = -1; + return ret; + } + + private: + int value_; + + unique_fd_impl(const unique_fd_impl&); + void operator=(const unique_fd_impl&); +}; + +using unique_fd = unique_fd_impl; + +} // namespace base +} // namespace android + +template +int close(const android::base::unique_fd_impl&) +#if defined(__clang__) + __attribute__((__unavailable__( +#else + __attribute__((__error__( +#endif + "close called on unique_fd" + ))); + +#endif // ANDROID_BASE_UNIQUE_FD_H diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/cmake/CMakeLists.txt b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/cmake/CMakeLists.txt new file mode 100644 index 0000000000..31a578ee38 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/cmake/CMakeLists.txt @@ -0,0 +1,61 @@ +set(UNWINDSTACK_ROOT ..) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +include_directories(${UNWINDSTACK_ROOT} ${UNWINDSTACK_ROOT}/include) + +enable_language(ASM) + +file(GLOB UNWINDSTACK_SOURCES + ${UNWINDSTACK_ROOT}/AndroidUnwinder.cpp + ${UNWINDSTACK_ROOT}/ArmExidx.cpp + ${UNWINDSTACK_ROOT}/DexFile.cpp + ${UNWINDSTACK_ROOT}/DexFiles.cpp + ${UNWINDSTACK_ROOT}/DwarfCfa.cpp + ${UNWINDSTACK_ROOT}/DwarfEhFrameWithHdr.cpp + ${UNWINDSTACK_ROOT}/DwarfMemory.cpp + ${UNWINDSTACK_ROOT}/DwarfOp.cpp + ${UNWINDSTACK_ROOT}/DwarfSection.cpp + ${UNWINDSTACK_ROOT}/Elf.cpp + ${UNWINDSTACK_ROOT}/ElfInterface.cpp + ${UNWINDSTACK_ROOT}/ElfInterfaceArm.cpp + ${UNWINDSTACK_ROOT}/Global.cpp + ${UNWINDSTACK_ROOT}/JitDebug.cpp + ${UNWINDSTACK_ROOT}/Log.cpp + ${UNWINDSTACK_ROOT}/LogAndroid.cpp + ${UNWINDSTACK_ROOT}/MapInfo.cpp + ${UNWINDSTACK_ROOT}/Maps.cpp + ${UNWINDSTACK_ROOT}/Memory.cpp + ${UNWINDSTACK_ROOT}/MemoryMte.cpp + ${UNWINDSTACK_ROOT}/Regs.cpp + ${UNWINDSTACK_ROOT}/RegsArm.cpp + ${UNWINDSTACK_ROOT}/RegsArm64.cpp + ${UNWINDSTACK_ROOT}/RegsX86.cpp + ${UNWINDSTACK_ROOT}/RegsX86_64.cpp + ${UNWINDSTACK_ROOT}/Symbols.cpp + ${UNWINDSTACK_ROOT}/ThreadEntry.cpp + ${UNWINDSTACK_ROOT}/ThreadUnwinder.cpp + ${UNWINDSTACK_ROOT}/Unwinder.cpp + ${UNWINDSTACK_ROOT}/android-base/file.cpp + ${UNWINDSTACK_ROOT}/android-base/stringprintf.cpp + ${UNWINDSTACK_ROOT}/android-base/strings.cpp + ${UNWINDSTACK_ROOT}/android-base/threads.cpp +) + +if(${CMAKE_SYSTEM_PROCESSOR} MATCHES arm) +elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "amd64.*|x86_64.*|AMD64.*") + set(UNWINDSTACK_SOURCES_GETREG + ${UNWINDSTACK_ROOT}/AsmGetRegsX86_64.S + ) +elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "i686.*|i386.*|x86.*") + set(UNWINDSTACK_SOURCES_GETREG + ${UNWINDSTACK_ROOT}/AsmGetRegsX86.S + ) +else() + add_definitions(-DEM_ARM=40) +endif() + +add_library(unwindstack STATIC + ${UNWINDSTACK_SOURCES} + ${UNWINDSTACK_SOURCES_GETREG} +) +target_link_libraries(unwindstack log) diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/GlobalDebugInterface.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/GlobalDebugInterface.h new file mode 100644 index 0000000000..b1e91069b0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/GlobalDebugInterface.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace unwindstack { + +// Base class for architecture specific implementations (see "GlobalDebugImpl.h"). +// It provides access to JITed ELF files, and loaded DEX files in the ART runtime. +template +class GlobalDebugInterface { + public: + virtual ~GlobalDebugInterface() {} + + virtual bool GetFunctionName(Maps* maps, uint64_t pc, SharedString* name, uint64_t* offset) = 0; + + virtual Symfile* Find(Maps* maps, uint64_t pc) = 0; + + protected: + bool Load(Maps* maps, std::shared_ptr& memory, uint64_t addr, uint64_t size, + /*out*/ std::shared_ptr& dex); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/errno_restorer.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/errno_restorer.h new file mode 100644 index 0000000000..2689505aff --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/errno_restorer.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "errno.h" + +#include "android-base/macros.h" + +namespace android { +namespace base { + +class ErrnoRestorer { + public: + ErrnoRestorer() : saved_errno_(errno) {} + + ~ErrnoRestorer() { errno = saved_errno_; } + + // Allow this object to be used as part of && operation. + explicit operator bool() const { return true; } + + private: + const int saved_errno_; + + DISALLOW_COPY_AND_ASSIGN(ErrnoRestorer); +}; + +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/file.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/file.h new file mode 100644 index 0000000000..67078a2642 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/file.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +#include "android-base/macros.h" +#include "android-base/off64_t.h" +#include "android-base/unique_fd.h" + +#if !defined(_WIN32) && !defined(O_BINARY) +/** Windows needs O_BINARY, but Unix never mangles line endings. */ +#define O_BINARY 0 +#endif + +#if defined(_WIN32) && !defined(O_CLOEXEC) +/** Windows has O_CLOEXEC but calls it O_NOINHERIT for some reason. */ +#define O_CLOEXEC O_NOINHERIT +#endif + +namespace android { +namespace base { + +bool ReadFdToString(borrowed_fd fd, std::string* content); +bool ReadFileToString(const std::string& path, std::string* content, + bool follow_symlinks = false); + +bool WriteStringToFile(const std::string& content, const std::string& path, + bool follow_symlinks = false); +bool WriteStringToFd(std::string_view content, borrowed_fd fd); + +#if !defined(_WIN32) +bool WriteStringToFile(const std::string& content, const std::string& path, + mode_t mode, uid_t owner, gid_t group, + bool follow_symlinks = false); +#endif + +bool ReadFully(borrowed_fd fd, void* data, size_t byte_count); + +// Reads `byte_count` bytes from the file descriptor at the specified offset. +// Returns false if there was an IO error or EOF was reached before reading `byte_count` bytes. +// +// NOTE: On Linux/Mac, this function wraps pread, which provides atomic read support without +// modifying the read pointer of the file descriptor. On Windows, however, the read pointer does +// get modified. This means that ReadFullyAtOffset can be used concurrently with other calls to the +// same function, but concurrently seeking or reading incrementally can lead to unexpected +// behavior. +bool ReadFullyAtOffset(borrowed_fd fd, void* data, size_t byte_count, off64_t offset); + +bool WriteFully(borrowed_fd fd, const void* data, size_t byte_count); +bool WriteFullyAtOffset(borrowed_fd fd, const void* data, size_t byte_count, off64_t offset); + +bool RemoveFileIfExists(const std::string& path, std::string* err = nullptr); + +#if !defined(_WIN32) +bool Realpath(const std::string& path, std::string* result); +bool Readlink(const std::string& path, std::string* result); +#endif + +std::string GetExecutablePath(); +std::string GetExecutableDirectory(); + +// Like the regular basename and dirname, but thread-safe on all +// platforms and capable of correctly handling exotic Windows paths. +std::string Basename(std::string_view path); +std::string Dirname(std::string_view path); + +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/macros.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/macros.h new file mode 100644 index 0000000000..f141f34e6c --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/macros.h @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include // for size_t +#include // for TEMP_FAILURE_RETRY + +#include + +// bionic and glibc both have TEMP_FAILURE_RETRY, but eg Mac OS' libc doesn't. +#ifndef TEMP_FAILURE_RETRY +#define TEMP_FAILURE_RETRY(exp) \ + ({ \ + decltype(exp) _rc; \ + do { \ + _rc = (exp); \ + } while (_rc == -1 && errno == EINTR); \ + _rc; \ + }) +#endif + +// A macro to disallow the copy constructor and operator= functions +// This must be placed in the private: declarations for a class. +// +// For disallowing only assign or copy, delete the relevant operator or +// constructor, for example: +// void operator=(const TypeName&) = delete; +// Note, that most uses of DISALLOW_ASSIGN and DISALLOW_COPY are broken +// semantically, one should either use disallow both or neither. Try to +// avoid these in new code. +#define DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&) = delete; \ + void operator=(const TypeName&) = delete + +// A macro to disallow all the implicit constructors, namely the +// default constructor, copy constructor and operator= functions. +// +// This should be used in the private: declarations for a class +// that wants to prevent anyone from instantiating it. This is +// especially useful for classes containing only static methods. +#define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \ + TypeName() = delete; \ + DISALLOW_COPY_AND_ASSIGN(TypeName) + +// The arraysize(arr) macro returns the # of elements in an array arr. +// The expression is a compile-time constant, and therefore can be +// used in defining new arrays, for example. If you use arraysize on +// a pointer by mistake, you will get a compile-time error. +// +// One caveat is that arraysize() doesn't accept any array of an +// anonymous type or a type defined inside a function. In these rare +// cases, you have to use the unsafe ARRAYSIZE_UNSAFE() macro below. This is +// due to a limitation in C++'s template system. The limitation might +// eventually be removed, but it hasn't happened yet. + +// This template function declaration is used in defining arraysize. +// Note that the function doesn't need an implementation, as we only +// use its type. +template +char(&ArraySizeHelper(T(&array)[N]))[N]; // NOLINT(readability/casting) + +#define arraysize(array) (sizeof(ArraySizeHelper(array))) + +#define SIZEOF_MEMBER(t, f) sizeof(std::declval().f) + +// Changing this definition will cause you a lot of pain. A majority of +// vendor code defines LIKELY and UNLIKELY this way, and includes +// this header through an indirect path. +#define LIKELY( exp ) (__builtin_expect( (exp) != 0, true )) +#define UNLIKELY( exp ) (__builtin_expect( (exp) != 0, false )) + +#define WARN_UNUSED __attribute__((warn_unused_result)) + +// A deprecated function to call to create a false use of the parameter, for +// example: +// int foo(int x) { UNUSED(x); return 10; } +// to avoid compiler warnings. Going forward we prefer ATTRIBUTE_UNUSED. +template +void UNUSED(const T&...) { +} + +// An attribute to place on a parameter to a function, for example: +// int foo(int x ATTRIBUTE_UNUSED) { return 10; } +// to avoid compiler warnings. +#define ATTRIBUTE_UNUSED __attribute__((__unused__)) + +// The FALLTHROUGH_INTENDED macro can be used to annotate implicit fall-through +// between switch labels: +// switch (x) { +// case 40: +// case 41: +// if (truth_is_out_there) { +// ++x; +// FALLTHROUGH_INTENDED; // Use instead of/along with annotations in +// // comments. +// } else { +// return x; +// } +// case 42: +// ... +// +// As shown in the example above, the FALLTHROUGH_INTENDED macro should be +// followed by a semicolon. It is designed to mimic control-flow statements +// like 'break;', so it can be placed in most places where 'break;' can, but +// only if there are no statements on the execution path between it and the +// next switch label. +// +// When compiled with clang, the FALLTHROUGH_INTENDED macro is expanded to +// [[clang::fallthrough]] attribute, which is analysed when performing switch +// labels fall-through diagnostic ('-Wimplicit-fallthrough'). See clang +// documentation on language extensions for details: +// http://clang.llvm.org/docs/LanguageExtensions.html#clang__fallthrough +// +// When used with unsupported compilers, the FALLTHROUGH_INTENDED macro has no +// effect on diagnostics. +// +// In either case this macro has no effect on runtime behavior and performance +// of code. +#ifndef FALLTHROUGH_INTENDED +#define FALLTHROUGH_INTENDED [[clang::fallthrough]] // NOLINT +#endif + +// Current ABI string +#if defined(__arm__) +#define ABI_STRING "arm" +#elif defined(__aarch64__) +#define ABI_STRING "arm64" +#elif defined(__i386__) +#define ABI_STRING "x86" +#elif defined(__riscv) +#define ABI_STRING "riscv64" +#elif defined(__x86_64__) +#define ABI_STRING "x86_64" +#endif diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/off64_t.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/off64_t.h new file mode 100644 index 0000000000..e6b71b81e1 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/off64_t.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#if defined(__APPLE__) +/** Mac OS has always had a 64-bit off_t, so it doesn't have off64_t. */ +typedef off_t off64_t; +#endif diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/stringprintf.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/stringprintf.h new file mode 100644 index 0000000000..93c56afd74 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/stringprintf.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android { +namespace base { + +// These printf-like functions are implemented in terms of vsnprintf, so they +// use the same attribute for compile-time format string checking. + +// Returns a string corresponding to printf-like formatting of the arguments. +std::string StringPrintf(const char* fmt, ...) __attribute__((__format__(__printf__, 1, 2))); + +// Appends a printf-like formatting of the arguments to 'dst'. +void StringAppendF(std::string* dst, const char* fmt, ...) + __attribute__((__format__(__printf__, 2, 3))); + +// Appends a printf-like formatting of the arguments to 'dst'. +void StringAppendV(std::string* dst, const char* format, va_list ap) + __attribute__((__format__(__printf__, 2, 0))); + +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/strings.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/strings.h new file mode 100644 index 0000000000..a28792cfcb --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/strings.h @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace android { +namespace base { + +// Splits a string into a vector of strings. +// +// The string is split at each occurrence of a character in delimiters. +// +// The empty string is not a valid delimiter list. +std::vector Split(const std::string& s, + const std::string& delimiters); + +// Splits a string into a vector of string tokens. +// +// The string is split at each occurrence of a character in delimiters. +// Coalesce runs of delimiter bytes and ignore delimiter bytes at the start or +// end of string. In other words, return only nonempty string tokens. +// Use when you don't care about recovering the original string with Join(). +// +// Example: +// Tokenize(" foo bar ", " ") => {"foo", "bar"} +// Join(Tokenize(" foo bar", " "), " ") => "foo bar" +// +// The empty string is not a valid delimiter list. +std::vector Tokenize(const std::string& s, const std::string& delimiters); + +namespace internal { +template +constexpr bool always_false_v = false; +} + +template +std::string Trim(T&& t) { + std::string_view sv; + std::string s; + if constexpr (std::is_convertible_v) { + sv = std::forward(t); + } else if constexpr (std::is_convertible_v) { + // The previous version of this function allowed for types which are implicitly convertible + // to std::string but not to std::string_view. For these types we go through std::string first + // here in order to retain source compatibility. + s = t; + sv = s; + } else { + static_assert(internal::always_false_v, + "Implicit conversion to std::string or std::string_view not possible"); + } + + // Skip initial whitespace. + while (!sv.empty() && isspace(sv.front())) { + sv.remove_prefix(1); + } + + // Skip terminating whitespace. + while (!sv.empty() && isspace(sv.back())) { + sv.remove_suffix(1); + } + + return std::string(sv); +} + +// We instantiate the common cases in strings.cpp. +extern template std::string Trim(const char*&); +extern template std::string Trim(const char*&&); +extern template std::string Trim(const std::string&); +extern template std::string Trim(const std::string&&); +extern template std::string Trim(std::string_view&); +extern template std::string Trim(std::string_view&&); + +// Joins a container of things into a single string, using the given separator. +template +std::string Join(const ContainerT& things, SeparatorT separator) { + if (things.empty()) { + return ""; + } + + std::ostringstream result; + result << *things.begin(); + for (auto it = std::next(things.begin()); it != things.end(); ++it) { + result << separator << *it; + } + return result.str(); +} + +// We instantiate the common cases in strings.cpp. +extern template std::string Join(const std::vector&, char); +extern template std::string Join(const std::vector&, char); +extern template std::string Join(const std::vector&, const std::string&); +extern template std::string Join(const std::vector&, const std::string&); + +// Tests whether 's' starts with 'prefix'. +bool StartsWith(std::string_view s, std::string_view prefix); +bool StartsWith(std::string_view s, char prefix); +bool StartsWithIgnoreCase(std::string_view s, std::string_view prefix); + +// Tests whether 's' ends with 'suffix'. +bool EndsWith(std::string_view s, std::string_view suffix); +bool EndsWith(std::string_view s, char suffix); +bool EndsWithIgnoreCase(std::string_view s, std::string_view suffix); + +// Tests whether 'lhs' equals 'rhs', ignoring case. +bool EqualsIgnoreCase(std::string_view lhs, std::string_view rhs); + +// Removes `prefix` from the start of the given string and returns true (if +// it was present), false otherwise. +inline bool ConsumePrefix(std::string_view* s, std::string_view prefix) { + if (!StartsWith(*s, prefix)) return false; + s->remove_prefix(prefix.size()); + return true; +} + +// Removes `suffix` from the end of the given string and returns true (if +// it was present), false otherwise. +inline bool ConsumeSuffix(std::string_view* s, std::string_view suffix) { + if (!EndsWith(*s, suffix)) return false; + s->remove_suffix(suffix.size()); + return true; +} + +// Replaces `from` with `to` in `s`, once if `all == false`, or as many times as +// there are matches if `all == true`. +[[nodiscard]] std::string StringReplace(std::string_view s, std::string_view from, + std::string_view to, bool all); + + +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/threads.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/threads.h new file mode 100644 index 0000000000..dbf1b4704b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/threads.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android { +namespace base { +uint64_t GetThreadId(); +} +} // namespace android + +#if defined(__GLIBC__) || defined(ANDROID_HOST_MUSL) +// bionic has this Linux-specifix call, but glibc and musl don't. +extern "C" int tgkill(int tgid, int tid, int sig); +#endif diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/unique_fd.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/unique_fd.h new file mode 100644 index 0000000000..ea3712af5f --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/unique_fd.h @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +// DO NOT INCLUDE OTHER LIBBASE HEADERS HERE! +// This file gets used in libbinder, and libbinder is used everywhere. +// Including other headers from libbase frequently results in inclusion of +// android-base/macros.h, which causes macro collisions. + +#if defined(__BIONIC__) +#include +#endif +#if !defined(_WIN32) && !defined(__TRUSTY__) +#include +#endif + +namespace android { +namespace base { + +// Container for a file descriptor that automatically closes the descriptor as +// it goes out of scope. +// +// unique_fd ufd(open("/some/path", "r")); +// if (ufd.get() == -1) return error; +// +// // Do something useful, possibly including 'return'. +// +// return 0; // Descriptor is closed for you. +// +// See also the Pipe()/Socketpair()/Fdopen()/Fdopendir() functions in this file +// that provide interoperability with the libc functions with the same (but +// lowercase) names. +// +// unique_fd is also known as ScopedFd/ScopedFD/scoped_fd; mentioned here to help +// you find this class if you're searching for one of those names. +// +// unique_fd itself is a specialization of unique_fd_impl with a default closer. +template +class unique_fd_impl final { + public: + unique_fd_impl() {} + + explicit unique_fd_impl(int fd) { reset(fd); } + ~unique_fd_impl() { reset(); } + + unique_fd_impl(const unique_fd_impl&) = delete; + void operator=(const unique_fd_impl&) = delete; + unique_fd_impl(unique_fd_impl&& other) noexcept { reset(other.release()); } + unique_fd_impl& operator=(unique_fd_impl&& s) noexcept { + int fd = s.fd_; + s.fd_ = -1; + reset(fd, &s); + return *this; + } + + [[clang::reinitializes]] void reset(int new_value = -1) { reset(new_value, nullptr); } + + int get() const { return fd_; } + +#if !defined(ANDROID_BASE_UNIQUE_FD_DISABLE_IMPLICIT_CONVERSION) + // unique_fd's operator int is dangerous, but we have way too much code that + // depends on it, so make this opt-in at first. + operator int() const { return get(); } // NOLINT +#endif + + bool operator>=(int rhs) const { return get() >= rhs; } + bool operator<(int rhs) const { return get() < rhs; } + bool operator==(int rhs) const { return get() == rhs; } + bool operator!=(int rhs) const { return get() != rhs; } + bool operator==(const unique_fd_impl& rhs) const { return get() == rhs.get(); } + bool operator!=(const unique_fd_impl& rhs) const { return get() != rhs.get(); } + + // Catch bogus error checks (i.e.: "!fd" instead of "fd != -1"). + bool operator!() const = delete; + + bool ok() const { return get() >= 0; } + + int release() __attribute__((warn_unused_result)) { + tag(fd_, this, nullptr); + int ret = fd_; + fd_ = -1; + return ret; + } + + private: + void reset(int new_value, void* previous_tag) { + int previous_errno = errno; + + if (fd_ != -1) { + close(fd_, this); + } + + fd_ = new_value; + if (new_value != -1) { + tag(new_value, previous_tag, this); + } + + errno = previous_errno; + } + + int fd_ = -1; + + // Template magic to use Closer::Tag if available, and do nothing if not. + // If Closer::Tag exists, this implementation is preferred, because int is a better match. + // If not, this implementation is SFINAEd away, and the no-op below is the only one that exists. + template + static auto tag(int fd, void* old_tag, void* new_tag) + -> decltype(T::Tag(fd, old_tag, new_tag), void()) { + T::Tag(fd, old_tag, new_tag); + } + + template + static void tag(long, void*, void*) { + // No-op. + } + + // Same as above, to select between Closer::Close(int) and Closer::Close(int, void*). + template + static auto close(int fd, void* tag_value) -> decltype(T::Close(fd, tag_value), void()) { + T::Close(fd, tag_value); + } + + template + static auto close(int fd, void*) -> decltype(T::Close(fd), void()) { + T::Close(fd); + } +}; + +// The actual details of closing are factored out to support unusual cases. +// Almost everyone will want this DefaultCloser, which handles fdsan on bionic. +struct DefaultCloser { + static void Close(int fd) { + // Even if close(2) fails with EINTR, the fd will have been closed. + // Using TEMP_FAILURE_RETRY will either lead to EBADF or closing someone + // else's fd. + // http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html + ::close(fd); + } +}; + +using unique_fd = unique_fd_impl; + +#if !defined(_WIN32) && !defined(__TRUSTY__) + +// Inline functions, so that they can be used header-only. + +// See pipe(2). +// This helper hides the details of converting to unique_fd, and also hides the +// fact that macOS doesn't support O_CLOEXEC or O_NONBLOCK directly. +template +inline bool Pipe(unique_fd_impl* read, unique_fd_impl* write, + int flags = O_CLOEXEC) { + int pipefd[2]; + +#if defined(__linux__) + if (pipe2(pipefd, flags) != 0) { + return false; + } +#else // defined(__APPLE__) + if (flags & ~(O_CLOEXEC | O_NONBLOCK)) { + return false; + } + if (pipe(pipefd) != 0) { + return false; + } + + if (flags & O_CLOEXEC) { + if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) != 0 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) != 0) { + close(pipefd[0]); + close(pipefd[1]); + return false; + } + } + if (flags & O_NONBLOCK) { + if (fcntl(pipefd[0], F_SETFL, O_NONBLOCK) != 0 || fcntl(pipefd[1], F_SETFL, O_NONBLOCK) != 0) { + close(pipefd[0]); + close(pipefd[1]); + return false; + } + } +#endif + + read->reset(pipefd[0]); + write->reset(pipefd[1]); + return true; +} + +// See socketpair(2). +// This helper hides the details of converting to unique_fd. +template +inline bool Socketpair(int domain, int type, int protocol, unique_fd_impl* left, + unique_fd_impl* right) { + int sockfd[2]; + if (socketpair(domain, type, protocol, sockfd) != 0) { + return false; + } + left->reset(sockfd[0]); + right->reset(sockfd[1]); + return true; +} + +// See socketpair(2). +// This helper hides the details of converting to unique_fd. +template +inline bool Socketpair(int type, unique_fd_impl* left, unique_fd_impl* right) { + return Socketpair(AF_UNIX, type, 0, left, right); +} + +// See fdopen(3). +// Using fdopen with unique_fd correctly is more annoying than it should be, +// because fdopen doesn't close the file descriptor received upon failure. +inline FILE* Fdopen(unique_fd&& ufd, const char* mode) { + int fd = ufd.release(); + FILE* file = fdopen(fd, mode); + if (!file) { + close(fd); + } + return file; +} + +// See fdopendir(3). +// Using fdopendir with unique_fd correctly is more annoying than it should be, +// because fdopen doesn't close the file descriptor received upon failure. +inline DIR* Fdopendir(unique_fd&& ufd) { + int fd = ufd.release(); + DIR* dir = fdopendir(fd); + if (dir == nullptr) { + close(fd); + } + return dir; +} + +#endif // !defined(_WIN32) && !defined(__TRUSTY__) + +// A wrapper type that can be implicitly constructed from either int or +// unique_fd. This supports cases where you don't actually own the file +// descriptor, and can't take ownership, but are temporarily acting as if +// you're the owner. +// +// One example would be a function that needs to also allow +// STDERR_FILENO, not just a newly-opened fd. Another example would be JNI code +// that's using a file descriptor that's actually owned by a +// ParcelFileDescriptor or whatever on the Java side, but where the JNI code +// would like to enforce this weaker sense of "temporary ownership". +// +// If you think of unique_fd as being like std::string in that represents +// ownership, borrowed_fd is like std::string_view (and int is like const +// char*). +struct borrowed_fd { + /* implicit */ borrowed_fd(int fd) : fd_(fd) {} // NOLINT + template + /* implicit */ borrowed_fd(const unique_fd_impl& ufd) : fd_(ufd.get()) {} // NOLINT + + int get() const { return fd_; } + + bool operator>=(int rhs) const { return get() >= rhs; } + bool operator<(int rhs) const { return get() < rhs; } + bool operator==(int rhs) const { return get() == rhs; } + bool operator!=(int rhs) const { return get() != rhs; } + + private: + int fd_ = -1; +}; +} // namespace base +} // namespace android + +template +int close(const android::base::unique_fd_impl&) + __attribute__((__unavailable__("close called on unique_fd"))); + +template +FILE* fdopen(const android::base::unique_fd_impl&, const char* mode) + __attribute__((__unavailable__("fdopen takes ownership of the fd passed in; either dup the " + "unique_fd, or use android::base::Fdopen to pass ownership"))); + +template +DIR* fdopendir(const android::base::unique_fd_impl&) __attribute__(( + __unavailable__("fdopendir takes ownership of the fd passed in; either dup the " + "unique_fd, or use android::base::Fdopendir to pass ownership"))); diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/utf8.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/utf8.h new file mode 100644 index 0000000000..1a414ec795 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/android-base/utf8.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#ifdef _WIN32 +#include +#include +#else +// Bring in prototypes for standard APIs so that we can import them into the utf8 namespace. +#include // open +#include // fopen +#include // mkdir +#include // unlink +#endif + +namespace android { +namespace base { + +// Only available on Windows because this is only needed on Windows. +#ifdef _WIN32 +// Convert size number of UTF-16 wchar_t's to UTF-8. Returns whether the +// conversion was done successfully. +bool WideToUTF8(const wchar_t* utf16, const size_t size, std::string* utf8); + +// Convert a NULL-terminated string of UTF-16 characters to UTF-8. Returns +// whether the conversion was done successfully. +bool WideToUTF8(const wchar_t* utf16, std::string* utf8); + +// Convert a UTF-16 std::wstring (including any embedded NULL characters) to +// UTF-8. Returns whether the conversion was done successfully. +bool WideToUTF8(const std::wstring& utf16, std::string* utf8); + +// Convert size number of UTF-8 char's to UTF-16. Returns whether the conversion +// was done successfully. +bool UTF8ToWide(const char* utf8, const size_t size, std::wstring* utf16); + +// Convert a NULL-terminated string of UTF-8 characters to UTF-16. Returns +// whether the conversion was done successfully. +bool UTF8ToWide(const char* utf8, std::wstring* utf16); + +// Convert a UTF-8 std::string (including any embedded NULL characters) to +// UTF-16. Returns whether the conversion was done successfully. +bool UTF8ToWide(const std::string& utf8, std::wstring* utf16); + +// Convert a file system path, represented as a NULL-terminated string of +// UTF-8 characters, to a UTF-16 string representing the same file system +// path using the Windows extended-lengh path representation. +// +// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#MAXPATH: +// ```The Windows API has many functions that also have Unicode versions to +// permit an extended-length path for a maximum total path length of 32,767 +// characters. To specify an extended-length path, use the "\\?\" prefix. +// For example, "\\?\D:\very long path".``` +// +// Returns whether the conversion was done successfully. +bool UTF8PathToWindowsLongPath(const char* utf8, std::wstring* utf16); +#endif + +// The functions in the utf8 namespace take UTF-8 strings. For Windows, these +// are wrappers, for non-Windows these just expose existing APIs. To call these +// functions, use: +// +// // anonymous namespace to avoid conflict with existing open(), unlink(), etc. +// namespace { +// // Import functions into anonymous namespace. +// using namespace android::base::utf8; +// +// void SomeFunction(const char* name) { +// int fd = open(name, ...); // Calls android::base::utf8::open(). +// ... +// unlink(name); // Calls android::base::utf8::unlink(). +// } +// } +namespace utf8 { + +#ifdef _WIN32 +FILE* fopen(const char* name, const char* mode); +int mkdir(const char* name, mode_t mode); +int open(const char* name, int flags, ...); +int unlink(const char* name); +#else +using ::fopen; +using ::mkdir; +using ::open; +using ::unlink; +#endif + +} // namespace utf8 +} // namespace base +} // namespace android diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_external.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_external.h new file mode 100644 index 0000000000..d9db200fdc --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_external.h @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ART_LIBDEXFILE_EXTERNAL_INCLUDE_ART_API_DEX_FILE_EXTERNAL_H_ +#define ART_LIBDEXFILE_EXTERNAL_INCLUDE_ART_API_DEX_FILE_EXTERNAL_H_ + +// Dex file external API +#include +#include +#include + +__BEGIN_DECLS + +// This is the stable C ABI that backs art_api::dex below. Structs and functions +// may only be added here. C++ users should use dex_file_support.h instead. + +struct ADexFile; +typedef struct ADexFile ADexFile; // NOLINT + +struct ADexFile_Method; +typedef struct ADexFile_Method ADexFile_Method; // NOLINT + +enum ADexFile_Error : uint32_t { + ADEXFILE_ERROR_OK = 0, + ADEXFILE_ERROR_INVALID_DEX = 1, + ADEXFILE_ERROR_INVALID_HEADER = 2, + ADEXFILE_ERROR_NOT_ENOUGH_DATA = 3, +}; +typedef enum ADexFile_Error ADexFile_Error; // NOLINT + +// Callback used to return information about a dex method. +// The method information is valid only during the callback. +// NOLINTNEXTLINE +typedef void ADexFile_MethodCallback(void* _Nullable callback_data, + const ADexFile_Method* _Nonnull method); + +// Interprets a chunk of memory as a dex file. +// +// @param address Pointer to the start of dex file data. +// The caller must retain the memory until the object is destroyed. +// @param size Size of the memory range. If the size is too small, the method returns +// ADEXFILE_ERROR_NOT_ENOUGH_DATA and sets new_size to some larger size +// (which still might large enough, so several retries might be needed). +// @param new_size On successful load, this contains exact dex file size from header. +// @param location A string that describes the dex file. Preferably its path. +// It is mostly used just for log messages and may be "". +// @param dex_file The created dex file object, or nullptr on error. +// It must be later freed with ADexFile_Destroy. +// +// @return ADEXFILE_ERROR_OK if successful. +// @return ADEXFILE_ERROR_NOT_ENOUGH_DATA if the provided dex file is too short (truncated). +// @return ADEXFILE_ERROR_INVALID_HEADER if the memory does not seem to represent DEX file. +// @return ADEXFILE_ERROR_INVALID_DEX if any other non-specific error occurs. +// +// Thread-safe (creates new object). +ADexFile_Error ADexFile_create(const void* _Nonnull address, + size_t size, + size_t* _Nullable new_size, + const char* _Nonnull location, + /*out*/ ADexFile* _Nullable * _Nonnull out_dex_file); + +// Find method at given offset and call callback with information about the method. +// +// @param dex_offset Offset relative to the start of the dex file header. +// @param callback The callback to call when method is found. Any data that needs to +// outlive the execution of the callback must be copied by the user. +// @param callback_data Extra user-specified argument for the callback. +// +// @return Number of methods found (0 or 1). +// +// Not thread-safe for calls on the same ADexFile instance. +size_t ADexFile_findMethodAtOffset(ADexFile* _Nonnull self, + size_t dex_offset, + ADexFile_MethodCallback* _Nonnull callback, + void* _Nullable callback_data); + +// Call callback for all methods in the DEX file. +// +// @param flags Specifies which information should be obtained. +// @param callback The callback to call for all methods. Any data that needs to +// outlive the execution of the callback must be copied by the user. +// @param callback_data Extra user-specified argument for the callback. +// +// @return Number of methods found. +// +// Not thread-safe for calls on the same ADexFile instance. +size_t ADexFile_forEachMethod(ADexFile* _Nonnull self, + ADexFile_MethodCallback* _Nonnull callback, + void* _Nullable callback_data); + +// Free the given object. +// +// Thread-safe, can be called only once for given instance. +void ADexFile_destroy(ADexFile* _Nullable self); + +// @return Offset of byte-code of the method relative to start of the dex file. +// @param out_size Optionally return size of byte-code in bytes. +// Not thread-safe for calls on the same ADexFile instance. +size_t ADexFile_Method_getCodeOffset(const ADexFile_Method* _Nonnull self, + size_t* _Nullable out_size); + +// @return Method name only (without class). +// The encoding is slightly modified UTF8 (see Dex specification). +// @param out_size Optionally return string size (excluding null-terminator). +// +// Returned data may be short lived: it must be copied before calling +// this method again within the same ADexFile. +// (it is currently long lived, but this is not guaranteed in the future). +// +// Not thread-safe for calls on the same ADexFile instance. +const char* _Nonnull ADexFile_Method_getName(const ADexFile_Method* _Nonnull self, + size_t* _Nullable out_size); + +// @return Method name (with class name). +// The encoding is slightly modified UTF8 (see Dex specification). +// @param out_size Optionally return string size (excluding null-terminator). +// @param with_params Whether to include method parameters and return type. +// +// Returned data may be short lived: it must be copied before calling +// this method again within the same ADexFile. +// (it points to pretty printing buffer within the ADexFile instance) +// +// Not thread-safe for calls on the same ADexFile instance. +const char* _Nonnull ADexFile_Method_getQualifiedName(const ADexFile_Method* _Nonnull self, + int with_params, + size_t* _Nullable out_size); + +// @return Class descriptor (mangled class name). +// The encoding is slightly modified UTF8 (see Dex specification). +// @param out_size Optionally return string size (excluding null-terminator). +// +// Returned data may be short lived: it must be copied before calling +// this method again within the same ADexFile. +// (it is currently long lived, but this is not guaranteed in the future). +// +// Not thread-safe for calls on the same ADexFile instance. +const char* _Nonnull ADexFile_Method_getClassDescriptor(const ADexFile_Method* _Nonnull self, + size_t* _Nullable out_size); + +// @return Compile-time literal or nullptr on error. +const char* _Nullable ADexFile_Error_toString(ADexFile_Error self); + +__END_DECLS + +#endif // ART_LIBDEXFILE_EXTERNAL_INCLUDE_ART_API_DEX_FILE_EXTERNAL_H_ diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_support.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_support.h new file mode 100644 index 0000000000..2361bf9211 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/art_api/dex_file_support.h @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ART_LIBDEXFILE_EXTERNAL_INCLUDE_ART_API_DEX_FILE_SUPPORT_H_ +#define ART_LIBDEXFILE_EXTERNAL_INCLUDE_ART_API_DEX_FILE_SUPPORT_H_ + +// C++ wrapper for the dex file external API. + +#include +#include + +#include + +#include "art_api/dex_file_external.h" + +namespace art_api { +namespace dex { + +#define FOR_EACH_ADEX_FILE_SYMBOL(MACRO) \ + MACRO(ADexFile_Error_toString) \ + MACRO(ADexFile_Method_getClassDescriptor) \ + MACRO(ADexFile_Method_getCodeOffset) \ + MACRO(ADexFile_Method_getName) \ + MACRO(ADexFile_Method_getQualifiedName) \ + MACRO(ADexFile_create) \ + MACRO(ADexFile_destroy) \ + MACRO(ADexFile_findMethodAtOffset) \ + MACRO(ADexFile_forEachMethod) \ + +#define DEFINE_ADEX_FILE_SYMBOL(DLFUNC) extern decltype(DLFUNC)* g_##DLFUNC; +FOR_EACH_ADEX_FILE_SYMBOL(DEFINE_ADEX_FILE_SYMBOL) +#undef DEFINE_ADEX_FILE_SYMBOL + +// Returns true if libdexfile.so is already loaded. Otherwise tries to +// load it and returns true if successful. Otherwise returns false and sets +// *error_msg. Thread safe. +bool TryLoadLibdexfile(std::string* error_msg); + +// TryLoadLibdexfile and fatally abort process if unsuccessful. +void LoadLibdexfile(); + +// API for reading ordinary dex files and CompactDex files. +// It is minimal 1:1 C++ wrapper around the C ABI. +// See documentation in dex_file_external.h +class DexFile { + public: + struct Method { + size_t GetCodeOffset(size_t* out_size = nullptr) const { + return g_ADexFile_Method_getCodeOffset(self, out_size); + } + + const char* GetName(size_t* out_size = nullptr) const { + return g_ADexFile_Method_getName(self, out_size); + } + + const char* GetQualifiedName(bool with_params = false, size_t* out_size = nullptr) const { + return g_ADexFile_Method_getQualifiedName(self, with_params, out_size); + } + + const char* GetClassDescriptor(size_t* out_size = nullptr) const { + return g_ADexFile_Method_getClassDescriptor(self, out_size); + } + + const ADexFile_Method* const self; + }; + + struct Error { + const char* ToString() const { + return g_ADexFile_Error_toString(self); + } + + bool Ok() const { + return self == ADEXFILE_ERROR_OK; + } + + ADexFile_Error Code() { + return self; + } + + ADexFile_Error const self; + }; + + static Error Create(const void* address, + size_t size, + size_t* new_size, + const char* location, + /*out*/ std::unique_ptr* out_dex_file) { + LoadLibdexfile(); + ADexFile* adex = nullptr; + ADexFile_Error error = g_ADexFile_create(address, size, new_size, location, &adex); + if (adex != nullptr) { + *out_dex_file = std::unique_ptr(new DexFile{adex}); + } + return Error{error}; + } + + virtual ~DexFile() { + g_ADexFile_destroy(self_); + } + + template + inline size_t FindMethodAtOffset(uint32_t dex_offset, T callback) { + auto cb = [](void* ctx, const ADexFile_Method* m) { (*reinterpret_cast(ctx))(Method{m}); }; + return g_ADexFile_findMethodAtOffset(self_, dex_offset, cb, &callback); + } + + template + inline size_t ForEachMethod(T callback) { + auto cb = [](void* ctx, const ADexFile_Method* m) { (*reinterpret_cast(ctx))(Method{m}); }; + return g_ADexFile_forEachMethod(self_, cb, &callback); + } + + protected: + explicit DexFile(ADexFile* self) : self_(self) {} + + ADexFile* const self_; + + DISALLOW_COPY_AND_ASSIGN(DexFile); +}; + +} // namespace dex +} // namespace art_api + +#endif // ART_LIBDEXFILE_EXTERNAL_INCLUDE_ART_API_DEX_FILE_SUPPORT_H_ diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/log/android/log.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/log/android/log.h new file mode 100644 index 0000000000..5dc365a4dd --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/log/android/log.h @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +/** + * @addtogroup Logging + * @{ + */ + +/** + * \file + * + * Support routines to send messages to the Android log buffer, + * which can later be accessed through the `logcat` utility. + * + * Each log message must have + * - a priority + * - a log tag + * - some text + * + * The tag normally corresponds to the component that emits the log message, + * and should be reasonably small. + * + * Log message text may be truncated to less than an implementation-specific + * limit (1023 bytes). + * + * Note that a newline character ("\n") will be appended automatically to your + * log message, if not already there. It is not possible to send several + * messages and have them appear on a single line in logcat. + * + * Please use logging in moderation: + * + * - Sending log messages eats CPU and slow down your application and the + * system. + * + * - The circular log buffer is pretty small, so sending many messages + * will hide other important log messages. + * + * - In release builds, only send log messages to account for exceptional + * conditions. + */ + +#include +#include +#include +#include + +#if !defined(__BIONIC__) && !defined(__INTRODUCED_IN) +#define __INTRODUCED_IN(x) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Android log priority values, in increasing order of priority. + */ +typedef enum android_LogPriority { + /** For internal use only. */ + ANDROID_LOG_UNKNOWN = 0, + /** The default priority, for internal use only. */ + ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ + /** Verbose logging. Should typically be disabled for a release apk. */ + ANDROID_LOG_VERBOSE, + /** Debug logging. Should typically be disabled for a release apk. */ + ANDROID_LOG_DEBUG, + /** Informational logging. Should typically be disabled for a release apk. */ + ANDROID_LOG_INFO, + /** Warning logging. For use with recoverable failures. */ + ANDROID_LOG_WARN, + /** Error logging. For use with unrecoverable failures. */ + ANDROID_LOG_ERROR, + /** Fatal logging. For use when aborting. */ + ANDROID_LOG_FATAL, + /** For internal use only. */ + ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */ +} android_LogPriority; + +/** + * Writes the constant string `text` to the log, with priority `prio` and tag + * `tag`. + */ +int __android_log_write(int prio, const char* tag, const char* text); + +/** + * Writes a formatted string to the log, with priority `prio` and tag `tag`. + * The details of formatting are the same as for + * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). + */ +int __android_log_print(int prio, const char* tag, const char* fmt, ...) + __attribute__((__format__(printf, 3, 4))); + +/** + * Equivalent to `__android_log_print`, but taking a `va_list`. + * (If `__android_log_print` is like `printf`, this is like `vprintf`.) + */ +int __android_log_vprint(int prio, const char* tag, const char* fmt, va_list ap) + __attribute__((__format__(printf, 3, 0))); + +/** + * Writes an assertion failure to the log (as `ANDROID_LOG_FATAL`) and to + * stderr, before calling + * [abort(3)](http://man7.org/linux/man-pages/man3/abort.3.html). + * + * If `fmt` is non-null, `cond` is unused. If `fmt` is null, the string + * `Assertion failed: %s` is used with `cond` as the string argument. + * If both `fmt` and `cond` are null, a default string is provided. + * + * Most callers should use + * [assert(3)](http://man7.org/linux/man-pages/man3/assert.3.html) from + * `<assert.h>` instead, or the `__assert` and `__assert2` functions + * provided by bionic if more control is needed. They support automatically + * including the source filename and line number more conveniently than this + * function. + */ +void __android_log_assert(const char* cond, const char* tag, const char* fmt, ...) + __attribute__((__noreturn__)) __attribute__((__format__(printf, 3, 4))); + +/** + * Identifies a specific log buffer for __android_log_buf_write() + * and __android_log_buf_print(). + */ +typedef enum log_id { + LOG_ID_MIN = 0, + + /** The main log buffer. This is the only log buffer available to apps. */ + LOG_ID_MAIN = 0, + /** The radio log buffer. */ + LOG_ID_RADIO = 1, + /** The event log buffer. */ + LOG_ID_EVENTS = 2, + /** The system log buffer. */ + LOG_ID_SYSTEM = 3, + /** The crash log buffer. */ + LOG_ID_CRASH = 4, + /** The statistics log buffer. */ + LOG_ID_STATS = 5, + /** The security log buffer. */ + LOG_ID_SECURITY = 6, + /** The kernel log buffer. */ + LOG_ID_KERNEL = 7, + + LOG_ID_MAX, + + /** Let the logging function choose the best log target. */ + LOG_ID_DEFAULT = 0x7FFFFFFF +} log_id_t; + +/** + * Writes the constant string `text` to the log buffer `id`, + * with priority `prio` and tag `tag`. + * + * Apps should use __android_log_write() instead. + */ +int __android_log_buf_write(int bufID, int prio, const char* tag, const char* text); + +/** + * Writes a formatted string to log buffer `id`, + * with priority `prio` and tag `tag`. + * The details of formatting are the same as for + * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). + * + * Apps should use __android_log_print() instead. + */ +int __android_log_buf_print(int bufID, int prio, const char* tag, const char* fmt, ...) + __attribute__((__format__(printf, 4, 5))); + +/** + * Logger data struct used for writing log messages to liblog via __android_log_write_logger_data() + * and sending log messages to user defined loggers specified in __android_log_set_logger(). + */ +struct __android_log_message { + /** Must be set to sizeof(__android_log_message) and is used for versioning. */ + size_t struct_size; + + /** {@link log_id_t} values. */ + int32_t buffer_id; + + /** {@link android_LogPriority} values. */ + int32_t priority; + + /** The tag for the log message. */ + const char* tag; + + /** Optional file name, may be set to nullptr. */ + const char* file; + + /** Optional line number, ignore if file is nullptr. */ + uint32_t line; + + /** The log message itself. */ + const char* message; +}; + +/** + * Prototype for the 'logger' function that is called for every log message. + */ +typedef void (*__android_logger_function)(const struct __android_log_message* log_message); +/** + * Prototype for the 'abort' function that is called when liblog will abort due to + * __android_log_assert() failures. + */ +typedef void (*__android_aborter_function)(const char* abort_message); + +/** + * Writes the log message specified by log_message. log_message includes additional file name and + * line number information that a logger may use. log_message is versioned for backwards + * compatibility. + * This assumes that loggability has already been checked through __android_log_is_loggable(). + * Higher level logging libraries, such as libbase, first check loggability, then format their + * buffers, then pass the message to liblog via this function, and therefore we do not want to + * duplicate the loggability check here. + * + * @param log_message the log message itself, see __android_log_message. + * + * Available since API level 30. + */ +void __android_log_write_log_message(struct __android_log_message* log_message) __INTRODUCED_IN(30); + +/** + * Sets a user defined logger function. All log messages sent to liblog will be set to the + * function pointer specified by logger for processing. It is not expected that log messages are + * already terminated with a new line. This function should add new lines if required for line + * separation. + * + * @param logger the new function that will handle log messages. + * + * Available since API level 30. + */ +void __android_log_set_logger(__android_logger_function logger) __INTRODUCED_IN(30); + +/** + * Writes the log message to logd. This is an __android_logger_function and can be provided to + * __android_log_set_logger(). It is the default logger when running liblog on a device. + * + * @param log_message the log message to write, see __android_log_message. + * + * Available since API level 30. + */ +void __android_log_logd_logger(const struct __android_log_message* log_message) __INTRODUCED_IN(30); + +/** + * Writes the log message to stderr. This is an __android_logger_function and can be provided to + * __android_log_set_logger(). It is the default logger when running liblog on host. + * + * @param log_message the log message to write, see __android_log_message. + * + * Available since API level 30. + */ +void __android_log_stderr_logger(const struct __android_log_message* log_message) + __INTRODUCED_IN(30); + +/** + * Sets a user defined aborter function that is called for __android_log_assert() failures. This + * user defined aborter function is highly recommended to abort and be noreturn, but is not strictly + * required to. + * + * @param aborter the new aborter function, see __android_aborter_function. + * + * Available since API level 30. + */ +void __android_log_set_aborter(__android_aborter_function aborter) __INTRODUCED_IN(30); + +/** + * Calls the stored aborter function. This allows for other logging libraries to use the same + * aborter function by calling this function in liblog. + * + * @param abort_message an additional message supplied when aborting, for example this is used to + * call android_set_abort_message() in __android_log_default_aborter(). + * + * Available since API level 30. + */ +void __android_log_call_aborter(const char* abort_message) __INTRODUCED_IN(30); + +/** + * Sets android_set_abort_message() on device then aborts(). This is the default aborter. + * + * @param abort_message an additional message supplied when aborting. This functions calls + * android_set_abort_message() with its contents. + * + * Available since API level 30. + */ +void __android_log_default_aborter(const char* abort_message) __attribute__((noreturn)) +__INTRODUCED_IN(30); + +/** + * Use the per-tag properties "log.tag." along with the minimum priority from + * __android_log_set_minimum_priority() to determine if a log message with a given prio and tag will + * be printed. A non-zero result indicates yes, zero indicates false. + * + * If both a priority for a tag and a minimum priority are set by + * __android_log_set_minimum_priority(), then the lowest of the two values are to determine the + * minimum priority needed to log. If only one is set, then that value is used to determine the + * minimum priority needed. If none are set, then default_priority is used. + * + * @param prio the priority to test, takes android_LogPriority values. + * @param tag the tag to test. + * @param default_prio the default priority to use if no properties or minimum priority are set. + * @return an integer where 1 indicates that the message is loggable and 0 indicates that it is not. + * + * Available since API level 30. + */ +int __android_log_is_loggable(int prio, const char* tag, int default_prio) __INTRODUCED_IN(30); + +/** + * Use the per-tag properties "log.tag." along with the minimum priority from + * __android_log_set_minimum_priority() to determine if a log message with a given prio and tag will + * be printed. A non-zero result indicates yes, zero indicates false. + * + * If both a priority for a tag and a minimum priority are set by + * __android_log_set_minimum_priority(), then the lowest of the two values are to determine the + * minimum priority needed to log. If only one is set, then that value is used to determine the + * minimum priority needed. If none are set, then default_priority is used. + * + * @param prio the priority to test, takes android_LogPriority values. + * @param tag the tag to test. + * @param len the length of the tag. + * @param default_prio the default priority to use if no properties or minimum priority are set. + * @return an integer where 1 indicates that the message is loggable and 0 indicates that it is not. + * + * Available since API level 30. + */ +int __android_log_is_loggable_len(int prio, const char* tag, size_t len, int default_prio) + __INTRODUCED_IN(30); + +/** + * Sets the minimum priority that will be logged for this process. + * + * @param priority the new minimum priority to set, takes android_LogPriority values. + * @return the previous set minimum priority as android_LogPriority values, or + * ANDROID_LOG_DEFAULT if none was set. + * + * Available since API level 30. + */ +int32_t __android_log_set_minimum_priority(int32_t priority) __INTRODUCED_IN(30); + +/** + * Gets the minimum priority that will be logged for this process. If none has been set by a + * previous __android_log_set_minimum_priority() call, this returns ANDROID_LOG_DEFAULT. + * + * @return the current minimum priority as android_LogPriority values, or + * ANDROID_LOG_DEFAULT if none is set. + * + * Available since API level 30. + */ +int32_t __android_log_get_minimum_priority(void) __INTRODUCED_IN(30); + +/** + * Sets the default tag if no tag is provided when writing a log message. Defaults to + * getprogname(). This truncates tag to the maximum log message size, though appropriate tags + * should be much smaller. + * + * @param tag the new log tag. + * + * Available since API level 30. + */ +void __android_log_set_default_tag(const char* tag) __INTRODUCED_IN(30); + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process.h new file mode 100644 index 0000000000..a0c7d7d2f0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace android { +namespace procinfo { + +#if defined(__linux__) + +enum ProcessState { + kProcessStateUnknown, + kProcessStateRunning, + kProcessStateSleeping, + kProcessStateUninterruptibleWait, + kProcessStateStopped, + kProcessStateZombie, +}; + +struct ProcessInfo { + std::string name; + ProcessState state; + pid_t tid; + pid_t pid; + pid_t ppid; + pid_t tracer; + uid_t uid; + uid_t gid; + + // Start time of the process since boot, measured in clock ticks. + uint64_t starttime; +}; + +// Fetch the list of threads from a given process's /proc/ directory. +// |fd| should be an fd pointing at a /proc/ directory. +template +auto GetProcessTidsFromProcPidFd(int fd, Collection* out, std::string* error = nullptr) -> + typename std::enable_if= sizeof(pid_t), bool>::type { + out->clear(); + + int task_fd = openat(fd, "task", O_DIRECTORY | O_RDONLY | O_CLOEXEC); + std::unique_ptr dir(fdopendir(task_fd), closedir); + if (!dir) { + if (error != nullptr) { + *error = "failed to open task directory"; + } + return false; + } + + struct dirent* dent; + while ((dent = readdir(dir.get()))) { + if (strcmp(dent->d_name, ".") != 0 && strcmp(dent->d_name, "..") != 0) { + pid_t tid; + if (!android::base::ParseInt(dent->d_name, &tid, 1, std::numeric_limits::max())) { + if (error != nullptr) { + *error = std::string("failed to parse task id: ") + dent->d_name; + } + return false; + } + + out->insert(out->end(), tid); + } + } + + return true; +} + +template +auto GetProcessTids(pid_t pid, Collection* out, std::string* error = nullptr) -> + typename std::enable_if= sizeof(pid_t), bool>::type { + char task_path[PATH_MAX]; + if (snprintf(task_path, PATH_MAX, "/proc/%d", pid) >= PATH_MAX) { + if (error != nullptr) { + *error = "task path overflow (pid = " + std::to_string(pid) + ")"; + } + return false; + } + + android::base::unique_fd fd(open(task_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC)); + if (fd == -1) { + if (error != nullptr) { + *error = std::string("failed to open ") + task_path; + } + return false; + } + + return GetProcessTidsFromProcPidFd(fd.get(), out, error); +} + +#endif + +} /* namespace procinfo */ +} /* namespace android */ diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process_map.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process_map.h new file mode 100644 index 0000000000..3c9d144f13 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/procinfo/process_map.h @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace android { +namespace procinfo { + +struct MapInfo { + uint64_t start; + uint64_t end; + uint16_t flags; + uint64_t pgoff; + ino_t inode; + std::string name; + bool shared; + + MapInfo(uint64_t start, uint64_t end, uint16_t flags, uint64_t pgoff, ino_t inode, + const char* name, bool shared) + : start(start), + end(end), + flags(flags), + pgoff(pgoff), + inode(inode), + name(name), + shared(shared) {} + + MapInfo(const MapInfo& params) + : start(params.start), + end(params.end), + flags(params.flags), + pgoff(params.pgoff), + inode(params.inode), + name(params.name), + shared(params.shared) {} +}; + +typedef std::function MapInfoCallback; +typedef std::function MapInfoParamsCallback; + +static inline bool PassSpace(char** p) { + if (**p != ' ') { + return false; + } + while (**p == ' ') { + (*p)++; + } + return true; +} + +static inline bool PassXdigit(char** p) { + if (!isxdigit(**p)) { + return false; + } + do { + (*p)++; + } while (isxdigit(**p)); + return true; +} + +// Parses the given line p pointing at proc//maps content buffer and returns true on success +// and false on failure parsing. The first new line character of line will be replaced by the +// null character and *next_line will point to the character after the null. +// +// Example of how a parsed line look line: +// 00400000-00409000 r-xp 00000000 fc:00 426998 /usr/lib/gvfs/gvfsd-http +static inline bool ParseMapsFileLine(char* p, uint64_t& start_addr, uint64_t& end_addr, uint16_t& flags, + uint64_t& pgoff, ino_t& inode, char** name, bool& shared, char** next_line) { + // Make the first new line character null. + *next_line = strchr(p, '\n'); + if (*next_line != nullptr) { + **next_line = '\0'; + (*next_line)++; + } + + char* end; + // start_addr + start_addr = strtoull(p, &end, 16); + if (end == p || *end != '-') { + return false; + } + p = end + 1; + // end_addr + end_addr = strtoull(p, &end, 16); + if (end == p) { + return false; + } + p = end; + if (!PassSpace(&p)) { + return false; + } + // flags + flags = 0; + if (*p == 'r') { + flags |= PROT_READ; + } else if (*p != '-') { + return false; + } + p++; + if (*p == 'w') { + flags |= PROT_WRITE; + } else if (*p != '-') { + return false; + } + p++; + if (*p == 'x') { + flags |= PROT_EXEC; + } else if (*p != '-') { + return false; + } + p++; + if (*p != 'p' && *p != 's') { + return false; + } + shared = *p == 's'; + + p++; + if (!PassSpace(&p)) { + return false; + } + // pgoff + pgoff = strtoull(p, &end, 16); + if (end == p) { + return false; + } + p = end; + if (!PassSpace(&p)) { + return false; + } + // major:minor + if (!PassXdigit(&p) || *p++ != ':' || !PassXdigit(&p) || !PassSpace(&p)) { + return false; + } + // inode + inode = strtoull(p, &end, 10); + if (end == p) { + return false; + } + p = end; + + if (*p != '\0' && !PassSpace(&p)) { + return false; + } + + // Assumes that the first new character was replaced with null. + *name = p; + + return true; +} + +inline bool ReadMapFileContent(char* content, const MapInfoParamsCallback& callback) { + uint64_t start_addr; + uint64_t end_addr; + uint16_t flags; + uint64_t pgoff; + ino_t inode; + char* line_start = content; + char* next_line; + char* name; + bool shared; + + while (line_start != nullptr && *line_start != '\0') { + bool parsed = ParseMapsFileLine(line_start, start_addr, end_addr, flags, pgoff, + inode, &name, shared, &next_line); + if (!parsed) { + return false; + } + + line_start = next_line; + callback(start_addr, end_addr, flags, pgoff, inode, name, shared); + } + return true; +} + +inline bool ReadMapFileContent(char* content, const MapInfoCallback& callback) { + uint64_t start_addr; + uint64_t end_addr; + uint16_t flags; + uint64_t pgoff; + ino_t inode; + char* line_start = content; + char* next_line; + char* name; + bool shared; + + while (line_start != nullptr && *line_start != '\0') { + bool parsed = ParseMapsFileLine(line_start, start_addr, end_addr, flags, pgoff, + inode, &name, shared, &next_line); + if (!parsed) { + return false; + } + + line_start = next_line; + callback(MapInfo(start_addr, end_addr, flags, pgoff, inode, name, shared)); + } + return true; +} + +inline bool ReadMapFile(const std::string& map_file, + const MapInfoCallback& callback) { + std::string content; + if (!android::base::ReadFileToString(map_file, &content)) { + return false; + } + return ReadMapFileContent(&content[0], callback); +} + + +inline bool ReadMapFile(const std::string& map_file, const MapInfoParamsCallback& callback, + std::string& mapsBuffer) { + if (!android::base::ReadFileToString(map_file, &mapsBuffer)) { + return false; + } + return ReadMapFileContent(&mapsBuffer[0], callback); +} + +inline bool ReadMapFile(const std::string& map_file, + const MapInfoParamsCallback& callback) { + std::string content; + return ReadMapFile(map_file, callback, content); +} + +inline bool ReadProcessMaps(pid_t pid, const MapInfoCallback& callback) { + return ReadMapFile("/proc/" + std::to_string(pid) + "/maps", callback); +} + +inline bool ReadProcessMaps(pid_t pid, const MapInfoParamsCallback& callback, + std::string& mapsBuffer) { + return ReadMapFile("/proc/" + std::to_string(pid) + "/maps", callback, mapsBuffer); +} + +inline bool ReadProcessMaps(pid_t pid, const MapInfoParamsCallback& callback) { + std::string content; + return ReadProcessMaps(pid, callback, content); +} + +inline bool ReadProcessMaps(pid_t pid, std::vector* maps) { + return ReadProcessMaps(pid, [&](const MapInfo& mapinfo) { maps->emplace_back(mapinfo); }); +} + +// Reads maps file and executes given callback for each mapping +// Warning: buffer should not be modified asynchronously while this function executes +template +inline bool ReadMapFileAsyncSafe(const char* map_file, void* buffer, size_t buffer_size, + const CallbackType& callback) { + if (buffer == nullptr || buffer_size == 0) { + return false; + } + + int fd = open(map_file, O_RDONLY | O_CLOEXEC); + if (fd == -1) { + return false; + } + + char* char_buffer = reinterpret_cast(buffer); + size_t start = 0; + size_t read_bytes = 0; + char* line = nullptr; + bool read_complete = false; + while (true) { + ssize_t bytes = + TEMP_FAILURE_RETRY(read(fd, char_buffer + read_bytes, buffer_size - read_bytes - 1)); + if (bytes <= 0) { + if (read_bytes == 0) { + close(fd); + return bytes == 0; + } + // Treat the last piece of data as the last line. + char_buffer[start + read_bytes] = '\n'; + bytes = 1; + read_complete = true; + } + read_bytes += bytes; + + while (read_bytes > 0) { + char* newline = reinterpret_cast(memchr(&char_buffer[start], '\n', read_bytes)); + if (newline == nullptr) { + break; + } + *newline = '\0'; + line = &char_buffer[start]; + start = newline - char_buffer + 1; + read_bytes -= newline - line + 1; + + // Ignore the return code, errors are okay. + ReadMapFileContent(line, callback); + } + + if (read_complete) { + close(fd); + return true; + } + + if (start == 0 && read_bytes == buffer_size - 1) { + // The buffer provided is too small to contain this line, give up + // and indicate failure. + close(fd); + return false; + } + + // Copy any leftover data to the front of the buffer. + if (start > 0) { + if (read_bytes > 0) { + memmove(char_buffer, &char_buffer[start], read_bytes); + } + start = 0; + } + } +} + +} /* namespace procinfo */ +} /* namespace android */ diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/AndroidUnwinder.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/AndroidUnwinder.h new file mode 100644 index 0000000000..1d8ff339f1 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/AndroidUnwinder.h @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace unwindstack { + +struct AndroidUnwinderData { + AndroidUnwinderData() = default; + explicit AndroidUnwinderData(const size_t max_frames) : max_frames(max_frames) {} + explicit AndroidUnwinderData(const bool show_all_frames) : show_all_frames(show_all_frames) {} + + void DemangleFunctionNames(); + + std::string GetErrorString(); + + std::vector frames; + ErrorData error; + std::optional> saved_initial_regs; + const std::optional max_frames; + const bool show_all_frames = false; +}; + +class AndroidUnwinder { + public: + AndroidUnwinder(pid_t pid) : pid_(pid) {} + AndroidUnwinder(pid_t pid, std::shared_ptr& memory) + : pid_(pid), process_memory_(memory) {} + AndroidUnwinder(pid_t pid, ArchEnum arch) : pid_(pid), arch_(arch) {} + AndroidUnwinder(pid_t pid, const std::vector initial_map_names_to_skip) + : pid_(pid), initial_map_names_to_skip_(std::move(initial_map_names_to_skip)) {} + AndroidUnwinder(pid_t pid, const std::vector initial_map_names_to_skip, + const std::vector map_suffixes_to_ignore) + : pid_(pid), + initial_map_names_to_skip_(std::move(initial_map_names_to_skip)), + map_suffixes_to_ignore_(std::move(map_suffixes_to_ignore)) {} + virtual ~AndroidUnwinder() = default; + + bool Initialize(ErrorData& error); + + std::shared_ptr& GetProcessMemory() { return process_memory_; } + unwindstack::Maps* GetMaps() { return maps_.get(); } + + const JitDebug& GetJitDebug() { return *jit_debug_.get(); } + const DexFiles& GetDexFiles() { return *dex_files_.get(); } + + std::string FormatFrame(const FrameData& frame) const; + + bool Unwind(AndroidUnwinderData& data); + bool Unwind(std::optional tid, AndroidUnwinderData& data); + bool Unwind(void* ucontext, AndroidUnwinderData& data); + bool Unwind(Regs* initial_regs, AndroidUnwinderData& data); + + FrameData BuildFrameFromPcOnly(uint64_t pc); + + static AndroidUnwinder* Create(pid_t pid); + + protected: + virtual bool InternalInitialize(ErrorData& error) = 0; + + virtual bool InternalUnwind(std::optional tid, AndroidUnwinderData& data) = 0; + + pid_t pid_; + + size_t max_frames_ = kMaxNumFrames; + std::vector initial_map_names_to_skip_; + std::vector map_suffixes_to_ignore_; + std::once_flag initialize_; + bool initialize_status_ = false; + + ArchEnum arch_ = ARCH_UNKNOWN; + + std::shared_ptr maps_; + std::shared_ptr process_memory_; + std::unique_ptr jit_debug_; + std::unique_ptr dex_files_; + + static constexpr size_t kMaxNumFrames = 512; +}; + +class AndroidLocalUnwinder : public AndroidUnwinder { + public: + AndroidLocalUnwinder() : AndroidUnwinder(getpid()) { + initial_map_names_to_skip_.emplace_back(kUnwindstackLib); + } + AndroidLocalUnwinder(std::shared_ptr& process_memory) + : AndroidUnwinder(getpid(), process_memory) { + initial_map_names_to_skip_.emplace_back(kUnwindstackLib); + } + AndroidLocalUnwinder(const std::vector& initial_map_names_to_skip) + : AndroidUnwinder(getpid(), initial_map_names_to_skip) { + initial_map_names_to_skip_.emplace_back(kUnwindstackLib); + } + AndroidLocalUnwinder(const std::vector& initial_map_names_to_skip, + const std::vector& map_suffixes_to_ignore) + : AndroidUnwinder(getpid(), initial_map_names_to_skip, map_suffixes_to_ignore) { + initial_map_names_to_skip_.emplace_back(kUnwindstackLib); + } + virtual ~AndroidLocalUnwinder() = default; + + protected: + static constexpr const char* kUnwindstackLib = "libunwindstack.so"; + + bool InternalInitialize(ErrorData& error) override; + + bool InternalUnwind(std::optional tid, AndroidUnwinderData& data) override; +}; + +class AndroidRemoteUnwinder : public AndroidUnwinder { + public: + AndroidRemoteUnwinder(pid_t pid) : AndroidUnwinder(pid) {} + AndroidRemoteUnwinder(pid_t pid, std::shared_ptr& process_memory) + : AndroidUnwinder(pid, process_memory) {} + AndroidRemoteUnwinder(pid_t pid, ArchEnum arch) : AndroidUnwinder(pid, arch) {} + AndroidRemoteUnwinder(pid_t pid, const std::vector initial_map_names_to_skip) + : AndroidUnwinder(pid, initial_map_names_to_skip) {} + AndroidRemoteUnwinder(pid_t pid, const std::vector initial_map_names_to_skip, + const std::vector map_suffixes_to_ignore) + : AndroidUnwinder(pid, initial_map_names_to_skip, map_suffixes_to_ignore) {} + virtual ~AndroidRemoteUnwinder() = default; + + protected: + bool InternalInitialize(ErrorData& error) override; + + bool InternalUnwind(std::optional tid, AndroidUnwinderData& data) override; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Arch.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Arch.h new file mode 100644 index 0000000000..0432ab0f11 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Arch.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +enum ArchEnum : uint8_t { + ARCH_UNKNOWN = 0, + ARCH_ARM, + ARCH_ARM64, + ARCH_X86, + ARCH_X86_64 +}; + +static inline bool ArchIs32Bit(ArchEnum arch) { + switch (arch) { + case ARCH_ARM: + case ARCH_X86: + return true; + default: + return false; + } +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DexFiles.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DexFiles.h new file mode 100644 index 0000000000..101d772920 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DexFiles.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include + +#include + +namespace unwindstack { + +enum ArchEnum : uint8_t; +class DexFile; +class Memory; + +using DexFiles = GlobalDebugInterface; + +std::unique_ptr CreateDexFiles(ArchEnum arch, std::shared_ptr& memory, + std::vector search_libs = {}); + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfError.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfError.h new file mode 100644 index 0000000000..6143523ee5 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfError.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +enum DwarfErrorCode : uint8_t { + DWARF_ERROR_NONE, + DWARF_ERROR_MEMORY_INVALID, + DWARF_ERROR_ILLEGAL_VALUE, + DWARF_ERROR_ILLEGAL_STATE, + DWARF_ERROR_STACK_INDEX_NOT_VALID, + DWARF_ERROR_NOT_IMPLEMENTED, + DWARF_ERROR_TOO_MANY_ITERATIONS, + DWARF_ERROR_CFA_NOT_DEFINED, + DWARF_ERROR_UNSUPPORTED_VERSION, + DWARF_ERROR_NO_FDES, +}; + +struct DwarfErrorData { + DwarfErrorCode code; + uint64_t address; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfLocation.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfLocation.h new file mode 100644 index 0000000000..9726f15ed0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfLocation.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +struct DwarfCie; + +enum DwarfLocationEnum : uint8_t { + DWARF_LOCATION_INVALID = 0, + DWARF_LOCATION_UNDEFINED, + DWARF_LOCATION_OFFSET, + DWARF_LOCATION_VAL_OFFSET, + DWARF_LOCATION_REGISTER, + DWARF_LOCATION_EXPRESSION, + DWARF_LOCATION_VAL_EXPRESSION, + DWARF_LOCATION_PSEUDO_REGISTER, +}; + +struct DwarfLocation { + DwarfLocationEnum type; + uint64_t values[2]; +}; + +struct DwarfLocations : public std::unordered_map { + const DwarfCie* cie; + // The range of PCs where the locations are valid (end is exclusive). + uint64_t pc_start = 0; + uint64_t pc_end = 0; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfMemory.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfMemory.h new file mode 100644 index 0000000000..2ef3b30f13 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfMemory.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; + +class DwarfMemory { + public: + DwarfMemory(Memory* memory) : memory_(memory) {} + virtual ~DwarfMemory() = default; + + bool ReadBytes(void* dst, size_t num_bytes); + + template + bool ReadSigned(uint64_t* value); + + bool ReadULEB128(uint64_t* value); + + bool ReadSLEB128(int64_t* value); + + template + size_t GetEncodedSize(uint8_t encoding); + + bool AdjustEncodedValue(uint8_t encoding, uint64_t* value); + + template + bool ReadEncodedValue(uint8_t encoding, uint64_t* value); + + uint64_t cur_offset() { return cur_offset_; } + void set_cur_offset(uint64_t cur_offset) { cur_offset_ = cur_offset; } + + void set_pc_offset(int64_t offset) { pc_offset_ = offset; } + void clear_pc_offset() { pc_offset_ = INT64_MAX; } + + void set_data_offset(uint64_t offset) { data_offset_ = offset; } + void clear_data_offset() { data_offset_ = static_cast(-1); } + + void set_func_offset(uint64_t offset) { func_offset_ = offset; } + void clear_func_offset() { func_offset_ = static_cast(-1); } + + void set_text_offset(uint64_t offset) { text_offset_ = offset; } + void clear_text_offset() { text_offset_ = static_cast(-1); } + + private: + Memory* memory_; + uint64_t cur_offset_ = 0; + + int64_t pc_offset_ = INT64_MAX; + uint64_t data_offset_ = static_cast(-1); + uint64_t func_offset_ = static_cast(-1); + uint64_t text_offset_ = static_cast(-1); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfSection.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfSection.h new file mode 100644 index 0000000000..33435b22a5 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfSection.h @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace unwindstack { + +// Forward declarations. +enum ArchEnum : uint8_t; +class Memory; +class Regs; +template +struct RegsInfo; + +class DwarfSection { + public: + DwarfSection(Memory* memory); + virtual ~DwarfSection() = default; + + class iterator : public std::iterator { + public: + iterator(DwarfSection* section, size_t index) : index_(index) { + section->GetFdes(&fdes_); + if (index_ == static_cast(-1)) { + index_ = fdes_.size(); + } + } + + iterator& operator++() { + index_++; + return *this; + } + iterator& operator++(int increment) { + index_ += increment; + return *this; + } + iterator& operator--() { + index_--; + return *this; + } + iterator& operator--(int decrement) { + index_ -= decrement; + return *this; + } + + bool operator==(const iterator& rhs) { return this->index_ == rhs.index_; } + bool operator!=(const iterator& rhs) { return this->index_ != rhs.index_; } + + const DwarfFde* operator*() { + if (index_ > fdes_.size()) return nullptr; + return fdes_[index_]; + } + + private: + std::vector fdes_; + size_t index_ = 0; + }; + + iterator begin() { return iterator(this, 0); } + iterator end() { return iterator(this, static_cast(-1)); } + + DwarfErrorCode LastErrorCode() { return last_error_.code; } + uint64_t LastErrorAddress() { return last_error_.address; } + + virtual bool Init(uint64_t offset, uint64_t size, int64_t section_bias) = 0; + + virtual bool Eval(const DwarfCie*, Memory*, const DwarfLocations&, Regs*, bool*) = 0; + + virtual bool Log(uint8_t indent, uint64_t pc, const DwarfFde* fde, ArchEnum arch) = 0; + + virtual void GetFdes(std::vector* fdes) = 0; + + virtual const DwarfFde* GetFdeFromPc(uint64_t pc) = 0; + + virtual bool GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde, DwarfLocations* loc_regs, + ArchEnum arch) = 0; + + virtual uint64_t GetCieOffsetFromFde32(uint32_t pointer) = 0; + + virtual uint64_t GetCieOffsetFromFde64(uint64_t pointer) = 0; + + virtual uint64_t AdjustPcFromFde(uint64_t pc) = 0; + + bool Step(uint64_t pc, Regs* regs, Memory* process_memory, bool* finished, bool* is_signal_frame); + + protected: + DwarfMemory memory_; + DwarfErrorData last_error_{DWARF_ERROR_NONE, 0}; + + uint32_t cie32_value_ = 0; + uint64_t cie64_value_ = 0; + + std::unordered_map fde_entries_; + std::unordered_map cie_entries_; + std::unordered_map cie_loc_regs_; + std::map loc_regs_; // Single row indexed by pc_end. +}; + +template +class DwarfSectionImpl : public DwarfSection { + public: + DwarfSectionImpl(Memory* memory) : DwarfSection(memory) {} + virtual ~DwarfSectionImpl() = default; + + bool Init(uint64_t offset, uint64_t size, int64_t section_bias) override; + + const DwarfCie* GetCieFromOffset(uint64_t offset); + + const DwarfFde* GetFdeFromOffset(uint64_t offset); + + const DwarfFde* GetFdeFromPc(uint64_t pc) override; + + void GetFdes(std::vector* fdes) override; + + bool EvalRegister(const DwarfLocation* loc, uint32_t reg, AddressType* reg_ptr, void* info); + + bool Eval(const DwarfCie* cie, Memory* regular_memory, const DwarfLocations& loc_regs, Regs* regs, + bool* finished) override; + + bool GetCfaLocationInfo(uint64_t pc, const DwarfFde* fde, DwarfLocations* loc_regs, + ArchEnum arch) override; + + bool Log(uint8_t indent, uint64_t pc, const DwarfFde* fde, ArchEnum arch) override; + + protected: + using DwarfFdeMap = + std::map>; + + bool GetNextCieOrFde(/*inout*/ uint64_t& offset, /*out*/ std::optional& fde); + + bool FillInCieHeader(DwarfCie* cie); + + bool FillInCie(DwarfCie* cie); + + bool FillInFdeHeader(DwarfFde* fde); + + bool FillInFde(DwarfFde* fde); + + bool EvalExpression(const DwarfLocation& loc, Memory* regular_memory, AddressType* value, + RegsInfo* regs_info, bool* is_dex_pc); + + static void InsertFde(uint64_t fde_offset, const DwarfFde* fde, /*out*/ DwarfFdeMap& fdes); + + void BuildFdeIndex(); + + int64_t section_bias_ = 0; + uint64_t entries_offset_ = 0; + uint64_t entries_end_ = 0; + uint64_t pc_offset_ = 0; + + // Binary search table (similar to .eh_frame_hdr). Contains only FDE offsets to save memory. + std::vector> fde_index_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfStructs.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfStructs.h new file mode 100644 index 0000000000..4c053600d6 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/DwarfStructs.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +struct DwarfCie { + uint8_t version = 0; + uint8_t fde_address_encoding = 0; + uint8_t lsda_encoding = 0; + uint8_t segment_size = 0; + std::vector augmentation_string; + uint64_t personality_handler = 0; + uint64_t cfa_instructions_offset = 0; + uint64_t cfa_instructions_end = 0; + uint64_t code_alignment_factor = 0; + int64_t data_alignment_factor = 0; + uint64_t return_address_register = 0; + bool is_signal_frame = false; +}; + +struct DwarfFde { + uint64_t cie_offset = 0; + uint64_t cfa_instructions_offset = 0; + uint64_t cfa_instructions_end = 0; + uint64_t pc_start = 0; + uint64_t pc_end = 0; + uint64_t lsda_address = 0; + const DwarfCie* cie = nullptr; +}; + +constexpr uint16_t CFA_REG = static_cast(-1); + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Elf.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Elf.h new file mode 100644 index 0000000000..6fff262834 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Elf.h @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#if !defined(EM_AARCH64) +#define EM_AARCH64 183 +#endif + +#if !defined(EM_RISCV) +#define EM_RISCV 243 +#endif + +namespace unwindstack { + +// Forward declaration. +class MapInfo; +class Regs; + +class Elf { + public: + Elf(Memory* memory) : memory_(memory) {} + virtual ~Elf() = default; + + bool Init(); + + void InitGnuDebugdata(); + + void Invalidate(); + + std::string GetSoname(); + + bool GetFunctionName(uint64_t addr, SharedString* name, uint64_t* func_offset); + + bool GetGlobalVariableOffset(const std::string& name, uint64_t* memory_offset); + + uint64_t GetRelPc(uint64_t pc, MapInfo* map_info); + + bool StepIfSignalHandler(uint64_t rel_pc, Regs* regs, Memory* process_memory); + + bool Step(uint64_t rel_pc, Regs* regs, Memory* process_memory, bool* finished, + bool* is_signal_frame); + + ElfInterface* CreateInterfaceFromMemory(Memory* memory); + + std::string GetBuildID(); + + std::string GetPrintableBuildID(); + + int64_t GetLoadBias() { return load_bias_; } + + bool IsValidPc(uint64_t pc); + + bool GetTextRange(uint64_t* addr, uint64_t* size); + + void GetLastError(ErrorData* data); + ErrorCode GetLastErrorCode(); + uint64_t GetLastErrorAddress(); + + bool valid() { return valid_; } + + uint32_t machine_type() { return machine_type_; } + + uint8_t class_type() { return class_type_; } + + ArchEnum arch() { return arch_; } + + Memory* memory() { return memory_.get(); } + + ElfInterface* interface() { return interface_.get(); } + + ElfInterface* gnu_debugdata_interface() { return gnu_debugdata_interface_.get(); } + + static bool IsValidElf(Memory* memory); + + static bool GetInfo(Memory* memory, uint64_t* size); + + static int64_t GetLoadBias(Memory* memory); + + static std::string GetBuildID(Memory* memory); + + // Caching cannot be enabled/disabled while unwinding. It is assumed + // that once enabled, it remains enabled while all unwinds are running. + // If the state of the caching changes while unwinding is occurring, + // it could cause crashes. + static void SetCachingEnabled(bool enable); + + static bool CachingEnabled() { return cache_enabled_; } + + static void CacheLock(); + static void CacheUnlock(); + static void CacheAdd(MapInfo* info); + static bool CacheGet(MapInfo* info); + + static std::string GetPrintableBuildID(std::string& build_id); + + protected: + bool valid_ = false; + int64_t load_bias_ = 0; + std::unique_ptr interface_; + std::unique_ptr memory_; + uint32_t machine_type_; + uint8_t class_type_; + ArchEnum arch_; + // Protect calls that can modify internal state of the interface object. + std::mutex lock_; + + std::unique_ptr gnu_debugdata_memory_; + std::unique_ptr gnu_debugdata_interface_; + + static bool cache_enabled_; + static std::unordered_map>>* + cache_; + static std::mutex* cache_lock_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/ElfInterface.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/ElfInterface.h new file mode 100644 index 0000000000..a192450f21 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/ElfInterface.h @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; +class Regs; +class Symbols; + +struct LoadInfo { + uint64_t offset; + uint64_t table_offset; + size_t table_size; +}; + +enum : uint8_t { + SONAME_UNKNOWN = 0, + SONAME_VALID, + SONAME_INVALID, +}; + +struct ElfTypes32 { + using AddressType = uint32_t; + using Dyn = Elf32_Dyn; + using Ehdr = Elf32_Ehdr; + using Nhdr = Elf32_Nhdr; + using Phdr = Elf32_Phdr; + using Shdr = Elf32_Shdr; + using Sym = Elf32_Sym; +}; + +struct ElfTypes64 { + using AddressType = uint64_t; + using Dyn = Elf64_Dyn; + using Ehdr = Elf64_Ehdr; + using Nhdr = Elf64_Nhdr; + using Phdr = Elf64_Phdr; + using Shdr = Elf64_Shdr; + using Sym = Elf64_Sym; +}; + +class ElfInterface { + public: + ElfInterface(Memory* memory) : memory_(memory) {} + virtual ~ElfInterface(); + + virtual bool Init(int64_t* load_bias) = 0; + + virtual void InitHeaders() = 0; + + virtual std::string GetSoname() = 0; + + virtual bool GetFunctionName(uint64_t addr, SharedString* name, uint64_t* offset) = 0; + + virtual bool GetGlobalVariable(const std::string& name, uint64_t* memory_address) = 0; + + virtual std::string GetBuildID() = 0; + + virtual bool Step(uint64_t rel_pc, Regs* regs, Memory* process_memory, bool* finished, + bool* is_signal_frame); + + virtual bool IsValidPc(uint64_t pc); + + bool GetTextRange(uint64_t* addr, uint64_t* size); + + std::unique_ptr CreateGnuDebugdataMemory(); + + Memory* memory() { return memory_; } + + const std::unordered_map& pt_loads() { return pt_loads_; } + + void SetGnuDebugdataInterface(ElfInterface* interface) { gnu_debugdata_interface_ = interface; } + + uint64_t dynamic_offset() { return dynamic_offset_; } + uint64_t dynamic_vaddr_start() { return dynamic_vaddr_start_; } + uint64_t dynamic_vaddr_end() { return dynamic_vaddr_end_; } + uint64_t data_offset() { return data_offset_; } + uint64_t data_vaddr_start() { return data_vaddr_start_; } + uint64_t data_vaddr_end() { return data_vaddr_end_; } + uint64_t eh_frame_hdr_offset() { return eh_frame_hdr_offset_; } + int64_t eh_frame_hdr_section_bias() { return eh_frame_hdr_section_bias_; } + uint64_t eh_frame_hdr_size() { return eh_frame_hdr_size_; } + uint64_t eh_frame_offset() { return eh_frame_offset_; } + int64_t eh_frame_section_bias() { return eh_frame_section_bias_; } + uint64_t eh_frame_size() { return eh_frame_size_; } + uint64_t debug_frame_offset() { return debug_frame_offset_; } + int64_t debug_frame_section_bias() { return debug_frame_section_bias_; } + uint64_t debug_frame_size() { return debug_frame_size_; } + uint64_t gnu_debugdata_offset() { return gnu_debugdata_offset_; } + uint64_t gnu_debugdata_size() { return gnu_debugdata_size_; } + uint64_t gnu_build_id_offset() { return gnu_build_id_offset_; } + uint64_t gnu_build_id_size() { return gnu_build_id_size_; } + + DwarfSection* eh_frame() { return eh_frame_.get(); } + DwarfSection* debug_frame() { return debug_frame_.get(); } + + const ErrorData& last_error() { return last_error_; } + ErrorCode LastErrorCode() { return last_error_.code; } + uint64_t LastErrorAddress() { return last_error_.address; } + + template + static int64_t GetLoadBias(Memory* memory); + + template + static std::string ReadBuildIDFromMemory(Memory* memory); + + protected: + virtual void HandleUnknownType(uint32_t, uint64_t, uint64_t) {} + + Memory* memory_; + std::unordered_map pt_loads_; + + // Stored elf data. + uint64_t dynamic_offset_ = 0; + uint64_t dynamic_vaddr_start_ = 0; + uint64_t dynamic_vaddr_end_ = 0; + + uint64_t data_offset_ = 0; + uint64_t data_vaddr_start_ = 0; + uint64_t data_vaddr_end_ = 0; + + uint64_t eh_frame_hdr_offset_ = 0; + int64_t eh_frame_hdr_section_bias_ = 0; + uint64_t eh_frame_hdr_size_ = 0; + + uint64_t eh_frame_offset_ = 0; + int64_t eh_frame_section_bias_ = 0; + uint64_t eh_frame_size_ = 0; + + uint64_t debug_frame_offset_ = 0; + int64_t debug_frame_section_bias_ = 0; + uint64_t debug_frame_size_ = 0; + + uint64_t gnu_debugdata_offset_ = 0; + uint64_t gnu_debugdata_size_ = 0; + + uint64_t gnu_build_id_offset_ = 0; + uint64_t gnu_build_id_size_ = 0; + + uint64_t text_addr_ = 0; + uint64_t text_size_ = 0; + + uint8_t soname_type_ = SONAME_UNKNOWN; + std::string soname_; + + ErrorData last_error_{ERROR_NONE, 0}; + + std::unique_ptr eh_frame_; + std::unique_ptr debug_frame_; + // The Elf object owns the gnu_debugdata interface object. + ElfInterface* gnu_debugdata_interface_ = nullptr; + + std::vector symbols_; + std::vector> strtabs_; +}; + +template +class ElfInterfaceImpl : public ElfInterface { + public: + using AddressType = typename ElfTypes::AddressType; + using DynType = typename ElfTypes::Dyn; + using EhdrType = typename ElfTypes::Ehdr; + using NhdrType = typename ElfTypes::Nhdr; + using PhdrType = typename ElfTypes::Phdr; + using ShdrType = typename ElfTypes::Shdr; + using SymType = typename ElfTypes::Sym; + + ElfInterfaceImpl(Memory* memory) : ElfInterface(memory) {} + virtual ~ElfInterfaceImpl() = default; + + bool Init(int64_t* load_bias) override { return ReadAllHeaders(load_bias); } + + void InitHeaders() override; + + std::string GetSoname() override; + + bool GetFunctionName(uint64_t addr, SharedString* name, uint64_t* func_offset) override; + + bool GetGlobalVariable(const std::string& name, uint64_t* memory_address) override; + + std::string GetBuildID() override { return ReadBuildID(); } + + static void GetMaxSize(Memory* memory, uint64_t* size); + + protected: + bool ReadAllHeaders(int64_t* load_bias); + + void ReadProgramHeaders(const EhdrType& ehdr, int64_t* load_bias); + + void ReadSectionHeaders(const EhdrType& ehdr); + + std::string ReadBuildID(); +}; + +using ElfInterface32 = ElfInterfaceImpl; +using ElfInterface64 = ElfInterfaceImpl; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Error.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Error.h new file mode 100644 index 0000000000..878cff4d9c --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Error.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +// A bit map of warnings, multiple warnings can be set at the same time. +enum WarningCode : uint64_t { + WARNING_NONE = 0, + WARNING_DEX_PC_NOT_IN_MAP = 0x1, // A dex pc was found, but it doesn't exist + // in any valid map. +}; + +enum ErrorCode : uint8_t { + ERROR_NONE, // No error. + ERROR_MEMORY_INVALID, // Memory read failed. + ERROR_UNWIND_INFO, // Unable to use unwind information to unwind. + ERROR_UNSUPPORTED, // Encountered unsupported feature. + ERROR_INVALID_MAP, // Unwind in an invalid map. + ERROR_MAX_FRAMES_EXCEEDED, // The number of frames exceed the total allowed. + ERROR_REPEATED_FRAME, // The last frame has the same pc/sp as the next. + ERROR_INVALID_ELF, // Unwind in an invalid elf. + ERROR_THREAD_DOES_NOT_EXIST, // Attempt to unwind a local thread that does + // not exist. + ERROR_THREAD_TIMEOUT, // Timeout trying to unwind a local thread. + ERROR_SYSTEM_CALL, // System call failed while unwinding. + ERROR_BAD_ARCH, // Arch invalid (none, or mismatched). + ERROR_MAPS_PARSE, // Failed to parse maps data. + ERROR_INVALID_PARAMETER, // Invalid parameter passed to function. + ERROR_PTRACE_CALL, // Ptrace call failed while unwinding. + ERROR_MAX = ERROR_INVALID_PARAMETER, +}; + +static inline const char* GetErrorCodeString(ErrorCode error) { + switch (error) { + case ERROR_NONE: + return "None"; + case ERROR_MEMORY_INVALID: + return "Memory Invalid"; + case ERROR_UNWIND_INFO: + return "Unwind Info"; + case ERROR_UNSUPPORTED: + return "Unsupported"; + case ERROR_INVALID_MAP: + return "Invalid Map"; + case ERROR_MAX_FRAMES_EXCEEDED: + return "Maximum Frames Exceeded"; + case ERROR_REPEATED_FRAME: + return "Repeated Frame"; + case ERROR_INVALID_ELF: + return "Invalid Elf"; + case ERROR_THREAD_DOES_NOT_EXIST: + return "Thread Does Not Exist"; + case ERROR_THREAD_TIMEOUT: + return "Thread Timeout"; + case ERROR_SYSTEM_CALL: + return "System Call Failed"; + case ERROR_BAD_ARCH: + return "Invalid arch detected"; + case ERROR_MAPS_PARSE: + return "Failed to parse maps"; + case ERROR_INVALID_PARAMETER: + return "Invalid parameter"; + case ERROR_PTRACE_CALL: + return "Ptrace Call Failed"; + } +} + +struct ErrorData { + ErrorCode code; + uint64_t address; // Only valid when code is ERROR_MEMORY_INVALID. + // Indicates the failing address. +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Global.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Global.h new file mode 100644 index 0000000000..d12ecb187b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Global.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Maps; +class MapInfo; + +class Global { + public: + explicit Global(std::shared_ptr& memory); + Global(std::shared_ptr& memory, std::vector& search_libs); + virtual ~Global() = default; + + void SetArch(ArchEnum arch); + + ArchEnum arch() { return arch_; } + + protected: + bool Searchable(const std::string& name); + void FindAndReadVariable(Maps* maps, const char* variable); + + virtual bool ReadVariableData(uint64_t offset) = 0; + + virtual void ProcessArch() = 0; + + ArchEnum arch_ = ARCH_UNKNOWN; + + std::shared_ptr memory_; + std::vector search_libs_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/JitDebug.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/JitDebug.h new file mode 100644 index 0000000000..8653f94ad5 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/JitDebug.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include + +#include + +namespace unwindstack { + +enum ArchEnum : uint8_t; +class Elf; +class Memory; + +using JitDebug = GlobalDebugInterface; + +std::unique_ptr CreateJitDebug(ArchEnum arch, std::shared_ptr& memory, + std::vector search_libs = {}); + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Log.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Log.h new file mode 100644 index 0000000000..34eb218da2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Log.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#if !defined(__printflike) +#define __printflike(x, y) __attribute__((__format__(printf, x, y))) +#endif + +namespace unwindstack { + +namespace Log { + +void Error(const char* format, ...) __printflike(1, 2); +void Info(const char* format, ...) __printflike(1, 2); +void Info(uint8_t indent, const char* format, ...) __printflike(2, 3); +void AsyncSafe(const char* format, ...) __printflike(1, 2); + +} // namespace Log + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm.h new file mode 100644 index 0000000000..6b8198e5e2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +enum ArmReg : uint16_t { + ARM_REG_R0 = 0, + ARM_REG_R1, + ARM_REG_R2, + ARM_REG_R3, + ARM_REG_R4, + ARM_REG_R5, + ARM_REG_R6, + ARM_REG_R7, + ARM_REG_R8, + ARM_REG_R9, + ARM_REG_R10, + ARM_REG_R11, + ARM_REG_R12, + ARM_REG_R13, + ARM_REG_R14, + ARM_REG_R15, + ARM_REG_LAST, + + ARM_REG_SP = ARM_REG_R13, + ARM_REG_LR = ARM_REG_R14, + ARM_REG_PC = ARM_REG_R15, +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm64.h new file mode 100644 index 0000000000..f1b7c1dcc6 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineArm64.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +enum Arm64Reg : uint16_t { + ARM64_REG_R0 = 0, + ARM64_REG_R1, + ARM64_REG_R2, + ARM64_REG_R3, + ARM64_REG_R4, + ARM64_REG_R5, + ARM64_REG_R6, + ARM64_REG_R7, + ARM64_REG_R8, + ARM64_REG_R9, + ARM64_REG_R10, + ARM64_REG_R11, + ARM64_REG_R12, + ARM64_REG_R13, + ARM64_REG_R14, + ARM64_REG_R15, + ARM64_REG_R16, + ARM64_REG_R17, + ARM64_REG_R18, + ARM64_REG_R19, + ARM64_REG_R20, + ARM64_REG_R21, + ARM64_REG_R22, + ARM64_REG_R23, + ARM64_REG_R24, + ARM64_REG_R25, + ARM64_REG_R26, + ARM64_REG_R27, + ARM64_REG_R28, + ARM64_REG_R29, + ARM64_REG_R30, + ARM64_REG_R31, + ARM64_REG_PC, + ARM64_REG_PSTATE, + ARM64_REG_LAST, + + ARM64_REG_SP = ARM64_REG_R31, + ARM64_REG_LR = ARM64_REG_R30, + + // Pseudo registers. These are not machine registers. + + // AARCH64 Return address signed state pseudo-register + ARM64_PREG_RA_SIGN_STATE = 34, + ARM64_PREG_FIRST = ARM64_PREG_RA_SIGN_STATE, + ARM64_PREG_LAST, +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86.h new file mode 100644 index 0000000000..ff4fd4b9bf --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +// Matches the numbers for the registers as generated by compilers. +// If this is changed, then unwinding will fail. +enum X86Reg : uint16_t { + X86_REG_EAX = 0, + X86_REG_ECX = 1, + X86_REG_EDX = 2, + X86_REG_EBX = 3, + X86_REG_ESP = 4, + X86_REG_EBP = 5, + X86_REG_ESI = 6, + X86_REG_EDI = 7, + X86_REG_EIP = 8, + X86_REG_EFL = 9, + X86_REG_CS = 10, + X86_REG_SS = 11, + X86_REG_DS = 12, + X86_REG_ES = 13, + X86_REG_FS = 14, + X86_REG_GS = 15, + X86_REG_LAST, + + X86_REG_SP = X86_REG_ESP, + X86_REG_PC = X86_REG_EIP, +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86_64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86_64.h new file mode 100644 index 0000000000..66670e39ec --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MachineX86_64.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace unwindstack { + +// Matches the numbers for the registers as generated by compilers. +// If this is changed, then unwinding will fail. +enum X86_64Reg : uint16_t { + X86_64_REG_RAX = 0, + X86_64_REG_RDX = 1, + X86_64_REG_RCX = 2, + X86_64_REG_RBX = 3, + X86_64_REG_RSI = 4, + X86_64_REG_RDI = 5, + X86_64_REG_RBP = 6, + X86_64_REG_RSP = 7, + X86_64_REG_R8 = 8, + X86_64_REG_R9 = 9, + X86_64_REG_R10 = 10, + X86_64_REG_R11 = 11, + X86_64_REG_R12 = 12, + X86_64_REG_R13 = 13, + X86_64_REG_R14 = 14, + X86_64_REG_R15 = 15, + X86_64_REG_RIP = 16, + X86_64_REG_LAST, + + X86_64_REG_SP = X86_64_REG_RSP, + X86_64_REG_PC = X86_64_REG_RIP, +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MapInfo.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MapInfo.h new file mode 100644 index 0000000000..12711f59d2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/MapInfo.h @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include + +namespace unwindstack { + +class MemoryFileAtOffset; + +// Represents virtual memory map (as obtained from /proc/*/maps). +// +// Note that we have to be surprisingly careful with memory usage here, +// since in system-wide profiling this data can take considerable space. +// (for example, 400 process * 400 maps * 128 bytes = 20 MB + string data). +class MapInfo { + public: + MapInfo(std::shared_ptr& prev_map, uint64_t start, uint64_t end, uint64_t offset, + uint64_t flags, SharedString name) + : start_(start), + end_(end), + offset_(offset), + flags_(flags), + name_(name), + elf_fields_(nullptr), + prev_map_(prev_map) {} + MapInfo(uint64_t start, uint64_t end, uint64_t offset, uint64_t flags, SharedString name) + : start_(start), + end_(end), + offset_(offset), + flags_(flags), + name_(name), + elf_fields_(nullptr) {} + + static inline std::shared_ptr Create(std::shared_ptr& prev_map, + uint64_t start, uint64_t end, uint64_t offset, + uint64_t flags, SharedString name) { + auto map_info = std::make_shared(prev_map, start, end, offset, flags, name); + if (prev_map) { + prev_map->next_map_ = map_info; + } + return map_info; + } + static inline std::shared_ptr Create(uint64_t start, uint64_t end, uint64_t offset, + uint64_t flags, SharedString name) { + return std::make_shared(start, end, offset, flags, name); + } + + ~MapInfo(); + + // Cached data for mapped ELF files. + // We allocate this structure lazily since there are much fewer ELFs than maps. + struct ElfFields { + ElfFields() : load_bias_(UINT64_MAX), build_id_(0) {} + + std::shared_ptr elf_; + // The offset of the beginning of this mapping to the beginning of the + // ELF file. + // elf_offset == offset - elf_start_offset. + // This value is only non-zero if the offset is non-zero but there is + // no elf signature found at that offset. + uint64_t elf_offset_ = 0; + // This value is the offset into the file of the map in memory that is the + // start of the elf. This is not equal to offset when the linker splits + // shared libraries into a read-only and read-execute map. + uint64_t elf_start_offset_ = 0; + + std::atomic_uint64_t load_bias_; + + // This is a pointer to a new'd std::string. + // Using an atomic value means that we don't need to lock and will + // make it easier to move to a fine grained lock in the future. + std::atomic build_id_; + + // Set to true if the elf file data is coming from memory. + bool memory_backed_elf_ = false; + + // Protect the creation of the elf object. + std::mutex elf_mutex_; + }; + + // True if the file named by this map is not actually readable and the + // elf is using the data in memory. + bool ElfFileNotReadable(); + + // This is the previous map with the same name that is not empty and with + // a 0 offset. For example, this set of maps: + // 1000-2000 r--p 000000 00:00 0 libc.so + // 2000-3000 ---p 000000 00:00 0 + // 3000-4000 r-xp 003000 00:00 0 libc.so + // The last map's prev_map would point to the 2000-3000 map, while + // GetPrevRealMap() would point to the 1000-2000 map. + // NOTE: If a map is encountered that has a non-zero offset, or has a + // a name different from the current map, then GetPrevRealMap() + // returns nullptr. + std::shared_ptr GetPrevRealMap(); + // This is the next map with the same name that is not empty and with + // a 0 offset. For the example above, the first map's GetNextRealMap() + // would be the 3000-4000 map. + // NOTE: If a map is encountered that has a non-zero offset, or has a + // a name different from the current map, then GetNextRealMap() + // returns nullptr. + std::shared_ptr GetNextRealMap(); + + // This is guaranteed to give out the Elf object associated with the + // object. The invariant is that once the Elf object is set under the + // lock in a MapInfo object it never changes and is not freed until + // the MapInfo object is destructed. + inline Elf* GetElfObj() { + std::lock_guard guard(elf_mutex()); + return elf().get(); + } + + inline uint64_t start() const { return start_; } + inline void set_start(uint64_t value) { start_ = value; } + + inline uint64_t end() const { return end_; } + inline void set_end(uint64_t value) { end_ = value; } + + inline uint64_t offset() const { return offset_; } + inline void set_offset(uint64_t value) { offset_ = value; } + + inline uint16_t flags() const { return flags_; } + inline void set_flags(uint16_t value) { flags_ = value; } + + inline SharedString& name() { return name_; } + inline void set_name(SharedString& value) { name_ = value; } + inline void set_name(const char* value) { name_ = value; } + + inline std::shared_ptr& elf() { return GetElfFields().elf_; } + inline void set_elf(std::shared_ptr& value) { GetElfFields().elf_ = value; } + inline void set_elf(Elf* value) { GetElfFields().elf_.reset(value); } + + inline uint64_t elf_offset() { return GetElfFields().elf_offset_; } + inline void set_elf_offset(uint64_t value) { GetElfFields().elf_offset_ = value; } + + inline uint64_t elf_start_offset() { return GetElfFields().elf_start_offset_; } + inline void set_elf_start_offset(uint64_t value) { GetElfFields().elf_start_offset_ = value; } + + inline std::atomic_uint64_t& load_bias() { return GetElfFields().load_bias_; } + inline void set_load_bias(uint64_t value) { GetElfFields().load_bias_ = value; } + + inline std::atomic& build_id() { return GetElfFields().build_id_; } + inline void set_build_id(SharedString* value) { GetElfFields().build_id_ = value; } + + inline bool memory_backed_elf() { return GetElfFields().memory_backed_elf_; } + inline void set_memory_backed_elf(bool value) { GetElfFields().memory_backed_elf_ = value; } + + inline std::shared_ptr prev_map() const { return prev_map_.lock(); } + inline void set_prev_map(std::shared_ptr& value) { prev_map_ = value; } + + inline std::shared_ptr next_map() const { return next_map_.lock(); } + inline void set_next_map(std::shared_ptr& value) { next_map_ = value; } + + // This function guarantees it will never return nullptr. + Elf* GetElf(const std::shared_ptr& process_memory, ArchEnum expected_arch); + + // Guaranteed to return the proper value if GetElf() has been called. + uint64_t GetLoadBias(); + + // Will get the proper value even if GetElf() hasn't been called. + uint64_t GetLoadBias(const std::shared_ptr& process_memory); + + // This returns the name of the map plus the soname if this particular + // map represents an elf file that is contained inside of another file. + // The format of this soname embedded name is: + // file.apk!libutils.so + // Otherwise, this function only returns the name of the map. + std::string GetFullName(); + + Memory* CreateMemory(const std::shared_ptr& process_memory); + + bool GetFunctionName(uint64_t addr, SharedString* name, uint64_t* func_offset); + + // Returns the raw build id read from the elf data. + SharedString GetBuildID(); + + // Used internally, and by tests. It sets the value only if it was not already set. + SharedString SetBuildID(std::string&& new_build_id); + + // Returns the printable version of the build id (hex dump of raw data). + std::string GetPrintableBuildID(); + + inline bool IsBlank() { return offset() == 0 && flags() == 0 && name().empty(); } + + // Returns elf_fields_. It will create the object if it is null. + ElfFields& GetElfFields(); + + private: + MapInfo(const MapInfo&) = delete; + void operator=(const MapInfo&) = delete; + + Memory* GetFileMemory(); + bool InitFileMemoryFromPreviousReadOnlyMap(MemoryFileAtOffset* memory); + + // Protect the creation of the elf object. + std::mutex& elf_mutex() { return GetElfFields().elf_mutex_; } + + uint64_t start_ = 0; + uint64_t end_ = 0; + uint64_t offset_ = 0; + uint16_t flags_ = 0; + SharedString name_; + + std::atomic elf_fields_; + + std::weak_ptr prev_map_; + std::weak_ptr next_map_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Maps.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Maps.h new file mode 100644 index 0000000000..d9e138391a --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Maps.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include + +namespace unwindstack { + +// Special flag to indicate a map is in /dev/. However, a map in +// /dev/ashmem/... does not set this flag. +static constexpr int MAPS_FLAGS_DEVICE_MAP = 0x8000; +// Special flag to indicate that this map represents an elf file +// created by ART for use with the gdb jit debug interface. +// This should only ever appear in offline maps data. +static constexpr int MAPS_FLAGS_JIT_SYMFILE_MAP = 0x4000; + +class Maps { + public: + virtual ~Maps() = default; + + Maps() = default; + + // Maps are not copyable but movable, because they own pointers to MapInfo + // objects. + Maps(const Maps&) = delete; + Maps& operator=(const Maps&) = delete; + Maps(Maps&&) = default; + Maps& operator=(Maps&&) = default; + + virtual std::shared_ptr Find(uint64_t pc); + + virtual bool Parse(); + + virtual const std::string GetMapsFile() const { return ""; } + + void Add(uint64_t start, uint64_t end, uint64_t offset, uint64_t flags, const std::string& name); + void Add(uint64_t start, uint64_t end, uint64_t offset, uint64_t flags, const std::string& name, + uint64_t load_bias); + + void Sort(); + + typedef std::vector>::iterator iterator; + iterator begin() { return maps_.begin(); } + iterator end() { return maps_.end(); } + + typedef std::vector>::const_iterator const_iterator; + const_iterator begin() const { return maps_.begin(); } + const_iterator end() const { return maps_.end(); } + + size_t Total() { return maps_.size(); } + + std::shared_ptr Get(size_t index) { + if (index >= maps_.size()) return nullptr; + return maps_[index]; + } + + protected: + std::vector> maps_; +}; + +class RemoteMaps : public Maps { + public: + RemoteMaps(pid_t pid) : pid_(pid) {} + virtual ~RemoteMaps() = default; + + virtual const std::string GetMapsFile() const override; + + private: + pid_t pid_; +}; + +class LocalMaps : public RemoteMaps { + public: + LocalMaps() : RemoteMaps(getpid()) {} + virtual ~LocalMaps() = default; +}; + +class LocalUpdatableMaps : public Maps { + public: + LocalUpdatableMaps(); + virtual ~LocalUpdatableMaps() = default; + + std::shared_ptr Find(uint64_t pc) override; + + bool Parse() override; + + const std::string GetMapsFile() const override; + + bool Reparse(/*out*/ bool* any_changed = nullptr); + + private: + pthread_rwlock_t maps_rwlock_; +}; + +class BufferMaps : public Maps { + public: + BufferMaps(const char* buffer) : buffer_(buffer) {} + virtual ~BufferMaps() = default; + + bool Parse() override; + + private: + const char* buffer_; +}; + +class FileMaps : public Maps { + public: + FileMaps(const std::string& file) : file_(file) {} + virtual ~FileMaps() = default; + + const std::string GetMapsFile() const override { return file_; } + + protected: + const std::string file_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Memory.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Memory.h new file mode 100644 index 0000000000..d6ca29ecc6 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Memory.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace unwindstack { + +class MemoryCacheBase; + +class Memory { + public: + Memory() = default; + virtual ~Memory() = default; + + static std::shared_ptr CreateProcessMemory(pid_t pid); + static std::shared_ptr CreateProcessMemoryCached(pid_t pid); + static std::shared_ptr CreateProcessMemoryThreadCached(pid_t pid); + static std::shared_ptr CreateOfflineMemory(const uint8_t* data, uint64_t start, + uint64_t end); + static std::unique_ptr CreateFileMemory(const std::string& path, uint64_t offset, + uint64_t size = UINT64_MAX); + + virtual MemoryCacheBase* AsMemoryCacheBase() { return nullptr; } + + virtual bool ReadString(uint64_t addr, std::string* dst, size_t max_read); + + virtual void Clear() {} + + // Get pointer to directly access the data for buffers that support it. + virtual uint8_t* GetPtr(size_t /*addr*/ = 0) { return nullptr; } + + virtual size_t Read(uint64_t addr, void* dst, size_t size) = 0; + virtual long ReadTag(uint64_t) { return -1; } + + bool ReadFully(uint64_t addr, void* dst, size_t size); + + inline bool Read32(uint64_t addr, uint32_t* dst) { + return ReadFully(addr, dst, sizeof(uint32_t)); + } + + inline bool Read64(uint64_t addr, uint64_t* dst) { + return ReadFully(addr, dst, sizeof(uint64_t)); + } +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Regs.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Regs.h new file mode 100644 index 0000000000..bc7dab57f4 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Regs.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Elf; +class Memory; + +class Regs { + public: + enum LocationEnum : uint8_t { + LOCATION_UNKNOWN = 0, + LOCATION_REGISTER, + LOCATION_SP_OFFSET, + }; + + struct Location { + Location(LocationEnum type, int16_t value) : type(type), value(value) {} + + LocationEnum type; + int16_t value; + }; + + Regs(uint16_t total_regs, const Location& return_loc) + : total_regs_(total_regs), return_loc_(return_loc) {} + virtual ~Regs() = default; + + virtual ArchEnum Arch() = 0; + + bool Is32Bit() { return ArchIs32Bit(Arch()); } + + virtual void* RawData() = 0; + virtual uint64_t pc() = 0; + virtual uint64_t sp() = 0; + + virtual void set_pc(uint64_t pc) = 0; + virtual void set_sp(uint64_t sp) = 0; + + uint64_t dex_pc() { return dex_pc_; } + void set_dex_pc(uint64_t dex_pc) { dex_pc_ = dex_pc; } + + virtual void fallback_pc() {} + + virtual uint64_t GetPcAdjustment(uint64_t rel_pc, Elf* elf, ArchEnum arch); + + virtual void ResetPseudoRegisters() {} + virtual bool SetPseudoRegister(uint16_t, uint64_t) { return false; } + virtual bool GetPseudoRegister(uint16_t, uint64_t*) { return false; } + + virtual bool StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) = 0; + + virtual bool SetPcFromReturnAddress(Memory* process_memory) = 0; + + virtual void IterateRegisters(std::function) = 0; + + uint16_t total_regs() { return total_regs_; } + + virtual Regs* Clone() = 0; + + static ArchEnum CurrentArch(); + static ArchEnum RemoteGetArch(pid_t pid, ErrorCode* error_code = nullptr); + static Regs* RemoteGet(pid_t pid, ErrorCode* error_code = nullptr); + static Regs* CreateFromUcontext(ArchEnum arch, void* ucontext); + static Regs* CreateFromLocal(); + + protected: + uint16_t total_regs_; + Location return_loc_; + uint64_t dex_pc_ = 0; +}; + +template +class RegsImpl : public Regs { + public: + RegsImpl(uint16_t total_regs, Location return_loc) + : Regs(total_regs, return_loc), regs_(total_regs) {} + virtual ~RegsImpl() = default; + + inline AddressType& operator[](size_t reg) { return regs_[reg]; } + + void* RawData() override { return regs_.data(); } + + virtual void IterateRegisters(std::function fn) override { + for (size_t i = 0; i < regs_.size(); ++i) { + fn(std::to_string(i).c_str(), regs_[i]); + } + } + + protected: + std::vector regs_; +}; + +uint64_t GetPcAdjustment(uint64_t rel_pc, Elf* elf, ArchEnum arch); + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm.h new file mode 100644 index 0000000000..5596605d04 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; + +class RegsArm : public RegsImpl { + public: + RegsArm(); + virtual ~RegsArm() = default; + + ArchEnum Arch() override final; + + bool SetPcFromReturnAddress(Memory* process_memory) override; + + bool StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) override; + + void IterateRegisters(std::function) override final; + + uint64_t pc() override; + uint64_t sp() override; + + void set_pc(uint64_t pc) override; + void set_sp(uint64_t sp) override; + + Regs* Clone() override final; + + static Regs* Read(void* data); + + static Regs* CreateFromUcontext(void* ucontext); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm64.h new file mode 100644 index 0000000000..d12a0437ae --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsArm64.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#include +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; + +class RegsArm64 : public RegsImpl { + public: + RegsArm64(); + virtual ~RegsArm64() = default; + + ArchEnum Arch() override final; + + bool SetPcFromReturnAddress(Memory* process_memory) override; + + bool StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) override; + + void IterateRegisters(std::function) override final; + + uint64_t pc() override; + uint64_t sp() override; + + void set_pc(uint64_t pc) override; + void set_sp(uint64_t sp) override; + + void fallback_pc() override; + + void ResetPseudoRegisters() override; + + bool SetPseudoRegister(uint16_t id, uint64_t value) override; + + bool GetPseudoRegister(uint16_t id, uint64_t* value) override; + + bool IsRASigned(); + + void SetPACMask(uint64_t mask); + + Regs* Clone() override final; + + static Regs* Read(void* data); + + static Regs* CreateFromUcontext(void* ucontext); + + protected: + uint64_t pseudo_regs_[Arm64Reg::ARM64_PREG_LAST - Arm64Reg::ARM64_PREG_FIRST]; + uint64_t pac_mask_; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsGetLocal.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsGetLocal.h new file mode 100644 index 0000000000..0e1d1af3dc --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsGetLocal.h @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +namespace unwindstack { + +#if defined(__arm__) + +inline __attribute__((__always_inline__)) void AsmGetRegs(void* reg_data) { + asm volatile( + ".align 2\n" + "bx pc\n" + "nop\n" + ".code 32\n" + "stmia %[base], {r0-r12}\n" + "add r2, %[base], #52\n" + "mov r3, r13\n" + "mov r4, r14\n" + "mov r5, r15\n" + "stmia r2, {r3-r5}\n" + "orr %[base], pc, #1\n" + "bx %[base]\n" + : [ base ] "+r"(reg_data) + : + : "r2", "r3", "r4", "r5", "memory"); +} + +#elif defined(__aarch64__) + +inline __attribute__((__always_inline__)) void AsmGetRegs(void* reg_data) { + asm volatile( + "1:\n" + "stp x0, x1, [%[base], #0]\n" + "stp x2, x3, [%[base], #16]\n" + "stp x4, x5, [%[base], #32]\n" + "stp x6, x7, [%[base], #48]\n" + "stp x8, x9, [%[base], #64]\n" + "stp x10, x11, [%[base], #80]\n" + "stp x12, x13, [%[base], #96]\n" + "stp x14, x15, [%[base], #112]\n" + "stp x16, x17, [%[base], #128]\n" + "stp x18, x19, [%[base], #144]\n" + "stp x20, x21, [%[base], #160]\n" + "stp x22, x23, [%[base], #176]\n" + "stp x24, x25, [%[base], #192]\n" + "stp x26, x27, [%[base], #208]\n" + "stp x28, x29, [%[base], #224]\n" + "str x30, [%[base], #240]\n" + "mov x12, sp\n" + "adr x13, 1b\n" + "stp x12, x13, [%[base], #248]\n" + : [base] "+r"(reg_data) + : + : "x12", "x13", "memory"); +} + +#elif defined(__riscv) + +inline __attribute__((__always_inline__)) void AsmGetRegs(void* reg_data) { + asm volatile( + "1:\n" + "sd ra, 8(%[base])\n" + "sd sp, 16(%[base])\n" + "sd gp, 24(%[base])\n" + "sd tp, 32(%[base])\n" + "sd t0, 40(%[base])\n" + "sd t1, 48(%[base])\n" + "sd t2, 56(%[base])\n" + "sd s0, 64(%[base])\n" + "sd s1, 72(%[base])\n" + "sd a0, 80(%[base])\n" + "sd a1, 88(%[base])\n" + "sd a2, 96(%[base])\n" + "sd a3, 104(%[base])\n" + "sd a4, 112(%[base])\n" + "sd a5, 120(%[base])\n" + "sd a6, 128(%[base])\n" + "sd a7, 136(%[base])\n" + "sd s2, 144(%[base])\n" + "sd s3, 152(%[base])\n" + "sd s4, 160(%[base])\n" + "sd s5, 168(%[base])\n" + "sd s6, 176(%[base])\n" + "sd s7, 184(%[base])\n" + "sd s8, 192(%[base])\n" + "sd s9, 200(%[base])\n" + "sd s10, 208(%[base])\n" + "sd s11, 216(%[base])\n" + "sd t3, 224(%[base])\n" + "sd t4, 232(%[base])\n" + "sd t5, 240(%[base])\n" + "sd t6, 248(%[base])\n" + "la t1, 1b\n" + "sd t1, 0(%[base])\n" + : [base] "+r"(reg_data) + : + : "t1", "memory"); +} + +#elif defined(__i386__) || defined(__x86_64__) + +extern "C" void AsmGetRegs(void* regs); + +#endif + +inline __attribute__((__always_inline__)) void RegsGetLocal(Regs* regs) { + AsmGetRegs(regs->RawData()); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86.h new file mode 100644 index 0000000000..d8245eef8f --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; +struct x86_ucontext_t; + +class RegsX86 : public RegsImpl { + public: + RegsX86(); + virtual ~RegsX86() = default; + + ArchEnum Arch() override final; + + bool SetPcFromReturnAddress(Memory* process_memory) override; + + bool StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) override; + + void SetFromUcontext(x86_ucontext_t* ucontext); + + void IterateRegisters(std::function) override final; + + uint64_t pc() override; + uint64_t sp() override; + + void set_pc(uint64_t pc) override; + void set_sp(uint64_t sp) override; + + Regs* Clone() override final; + + static Regs* Read(void* data); + + static Regs* CreateFromUcontext(void* ucontext); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86_64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86_64.h new file mode 100644 index 0000000000..90fee939c5 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/RegsX86_64.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Memory; +struct x86_64_ucontext_t; + +class RegsX86_64 : public RegsImpl { + public: + RegsX86_64(); + virtual ~RegsX86_64() = default; + + ArchEnum Arch() override final; + + bool SetPcFromReturnAddress(Memory* process_memory) override; + + bool StepIfSignalHandler(uint64_t elf_offset, Elf* elf, Memory* process_memory) override; + + void SetFromUcontext(x86_64_ucontext_t* ucontext); + + void IterateRegisters(std::function) override final; + + uint64_t pc() override; + uint64_t sp() override; + + void set_pc(uint64_t pc) override; + void set_sp(uint64_t sp) override; + + Regs* Clone() override final; + + static Regs* Read(void* data); + + static Regs* CreateFromUcontext(void* ucontext); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/SharedString.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/SharedString.h new file mode 100644 index 0000000000..bdf709ed83 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/SharedString.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace unwindstack { + +// Ref-counted read-only string. Used to avoid string allocations/copies. +// It is intended to be transparent std::string replacement in most cases. +class SharedString { + public: + SharedString() : data_() {} + SharedString(std::string&& s) : data_(std::make_shared(std::move(s))) {} + SharedString(const std::string& s) : SharedString(std::string(s)) {} + SharedString(const char* s) : SharedString(std::string(s)) {} + + void clear() { data_.reset(); } + bool is_null() const { return data_.get() == nullptr; } + bool empty() const { return is_null() ? true : data_->empty(); } + const char* c_str() const { return is_null() ? "" : data_->c_str(); } + + operator const std::string&() const { + [[clang::no_destroy]] static const std::string empty; + return data_ ? *data_.get() : empty; + } + + operator std::string_view() const { return static_cast(*this); } + + private: + std::shared_ptr data_; +}; + +template >> +static inline bool operator==(const T& a, const T& b) { + return static_cast(a) == static_cast(b); +} +static inline bool operator==(const SharedString& a, std::string_view b) { + return static_cast(a) == b; +} +static inline bool operator==(std::string_view a, const SharedString& b) { + return a == static_cast(b); +} +template >> +static inline bool operator!=(const T& a, const T& b) { + return !(a == b); +} +static inline bool operator!=(const SharedString& a, std::string_view b) { + return !(a == b); +} +static inline bool operator!=(std::string_view a, const SharedString& b) { + return !(a == b); +} +static inline std::string operator+(const SharedString& a, const char* b) { + return static_cast(a) + b; +} +static inline std::string operator+(const char* a, const SharedString& b) { + return a + static_cast(b); +} + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm.h new file mode 100644 index 0000000000..0d7f90d37d --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +struct arm_stack_t { + uint32_t ss_sp; // void __user* + int32_t ss_flags; // int + uint32_t ss_size; // size_t +}; + +struct arm_mcontext_t { + uint32_t trap_no; // unsigned long + uint32_t error_code; // unsigned long + uint32_t oldmask; // unsigned long + uint32_t regs[ARM_REG_LAST]; // unsigned long + uint32_t cpsr; // unsigned long + uint32_t fault_address; // unsigned long +}; + +struct arm_ucontext_t { + uint32_t uc_flags; // unsigned long + uint32_t uc_link; // struct ucontext* + arm_stack_t uc_stack; + arm_mcontext_t uc_mcontext; + // Nothing else is used, so don't define it. +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm64.h new file mode 100644 index 0000000000..49278b3754 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextArm64.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +struct arm64_stack_t { + uint64_t ss_sp; // void __user* + int32_t ss_flags; // int + uint64_t ss_size; // size_t +}; + +struct arm64_sigset_t { + uint64_t sig; // unsigned long +}; + +struct arm64_mcontext_t { + uint64_t fault_address; // __u64 + uint64_t regs[ARM64_REG_LAST]; // __u64 + uint64_t pstate; // __u64 + // Nothing else is used, so don't define it. +}; + +struct arm64_ucontext_t { + uint64_t uc_flags; // unsigned long + uint64_t uc_link; // struct ucontext* + arm64_stack_t uc_stack; + arm64_sigset_t uc_sigmask; + // The kernel adds extra padding after uc_sigmask to match glibc sigset_t on ARM64. + char __padding[128 - sizeof(arm64_sigset_t)]; + // The full structure requires 16 byte alignment, but our partial structure + // doesn't, so force the alignment. + arm64_mcontext_t uc_mcontext __attribute__((aligned(16))); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86.h new file mode 100644 index 0000000000..9dfdf2a1a8 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +struct x86_stack_t { + uint32_t ss_sp; // void __user* + int32_t ss_flags; // int + uint32_t ss_size; // size_t +}; + +struct x86_mcontext_t { + uint32_t gs; + uint32_t fs; + uint32_t es; + uint32_t ds; + uint32_t edi; + uint32_t esi; + uint32_t ebp; + uint32_t esp; + uint32_t ebx; + uint32_t edx; + uint32_t ecx; + uint32_t eax; + uint32_t trapno; + uint32_t err; + uint32_t eip; + uint32_t cs; + uint32_t efl; + uint32_t uesp; + uint32_t ss; + // Only care about the registers, skip everything else. +}; + +struct x86_ucontext_t { + uint32_t uc_flags; // unsigned long + uint32_t uc_link; // struct ucontext* + x86_stack_t uc_stack; + x86_mcontext_t uc_mcontext; + // Nothing else is used, so don't define it. +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86_64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86_64.h new file mode 100644 index 0000000000..2801217866 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UcontextX86_64.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +#include + +#include + +namespace unwindstack { + +struct x86_64_stack_t { + uint64_t ss_sp; // void __user* + int32_t ss_flags; // int + int32_t pad; + uint64_t ss_size; // size_t +}; + +struct x86_64_mcontext_t { + uint64_t r8; + uint64_t r9; + uint64_t r10; + uint64_t r11; + uint64_t r12; + uint64_t r13; + uint64_t r14; + uint64_t r15; + uint64_t rdi; + uint64_t rsi; + uint64_t rbp; + uint64_t rbx; + uint64_t rdx; + uint64_t rax; + uint64_t rcx; + uint64_t rsp; + uint64_t rip; + uint64_t efl; + uint64_t csgsfs; + uint64_t err; + uint64_t trapno; + uint64_t oldmask; + uint64_t cr2; + // Only care about the registers, skip everything else. +}; + +struct x86_64_ucontext_t { + uint64_t uc_flags; // unsigned long + uint64_t uc_link; // struct ucontext* + x86_64_stack_t uc_stack; + x86_64_mcontext_t uc_mcontext; + // Nothing else is used, so don't define it. +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Unwinder.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Unwinder.h new file mode 100644 index 0000000000..0795a514af --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/Unwinder.h @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace unwindstack { + +// Forward declarations. +class Elf; +class ThreadEntry; + +struct FrameData { + size_t num; + + uint64_t rel_pc; + uint64_t pc; + uint64_t sp; + + SharedString function_name; + uint64_t function_offset = 0; + + std::shared_ptr map_info; +}; + +class Unwinder { + public: + Unwinder(size_t max_frames, Maps* maps, Regs* regs, std::shared_ptr process_memory) + : max_frames_(max_frames), + maps_(maps), + regs_(regs), + process_memory_(process_memory), + arch_(regs->Arch()) {} + Unwinder(size_t max_frames, Maps* maps, std::shared_ptr process_memory) + : max_frames_(max_frames), maps_(maps), process_memory_(process_memory) {} + + virtual ~Unwinder() = default; + + virtual void Unwind(const std::vector* initial_map_names_to_skip = nullptr, + const std::vector* map_suffixes_to_ignore = nullptr); + + size_t NumFrames() const { return frames_.size(); } + + // Returns frames after unwinding. + // Intentionally mutable (which can be used to swap in reserved memory before unwinding). + std::vector& frames() { return frames_; } + + std::vector ConsumeFrames() { + std::vector frames = std::move(frames_); + frames_.clear(); + return frames; + } + + std::string FormatFrame(size_t frame_num) const; + std::string FormatFrame(const FrameData& frame) const; + + static std::string FormatFrame(ArchEnum arch, const FrameData& frame, + bool display_build_id = true); + + void SetArch(ArchEnum arch) { arch_ = arch; }; + + void SetJitDebug(JitDebug* jit_debug); + + void SetRegs(Regs* regs) { + regs_ = regs; + arch_ = regs_ != nullptr ? regs->Arch() : ARCH_UNKNOWN; + } + Maps* GetMaps() { return maps_; } + std::shared_ptr& GetProcessMemory() { return process_memory_; } + + // Disabling the resolving of names results in the function name being + // set to an empty string and the function offset being set to zero. + void SetResolveNames(bool resolve) { resolve_names_ = resolve; } + + void SetDisplayBuildID(bool display_build_id) { display_build_id_ = display_build_id; } + + void SetDexFiles(DexFiles* dex_files); + + const ErrorData& LastError() { return last_error_; } + ErrorCode LastErrorCode() { return last_error_.code; } + const char* LastErrorCodeString() { return GetErrorCodeString(last_error_.code); } + uint64_t LastErrorAddress() { return last_error_.address; } + uint64_t warnings() { return warnings_; } + + // Builds a frame for symbolization using the maps from this unwinder. The + // constructed frame contains just enough information to be used to symbolize + // frames collected by frame-pointer unwinding that's done outside of + // libunwindstack. This is used by tombstoned to symbolize frame pointer-based + // stack traces that are collected by tools such as GWP-ASan and MTE. + static FrameData BuildFrameFromPcOnly(uint64_t pc, ArchEnum arch, Maps* maps, JitDebug* jit_debug, + std::shared_ptr process_memory, bool resolve_names); + + protected: + Unwinder(size_t max_frames, Maps* maps = nullptr) : max_frames_(max_frames), maps_(maps) {} + Unwinder(size_t max_frames, ArchEnum arch, Maps* maps = nullptr) + : max_frames_(max_frames), maps_(maps), arch_(arch) {} + Unwinder(size_t max_frames, ArchEnum arch, Maps* maps, std::shared_ptr& process_memory) + : max_frames_(max_frames), maps_(maps), process_memory_(process_memory), arch_(arch) {} + + void ClearErrors() { + warnings_ = WARNING_NONE; + last_error_.code = ERROR_NONE; + last_error_.address = 0; + } + + void FillInDexFrame(); + FrameData* FillInFrame(std::shared_ptr& map_info, Elf* elf, uint64_t rel_pc, + uint64_t pc_adjustment); + + size_t max_frames_; + Maps* maps_ = nullptr; + Regs* regs_; + std::vector frames_; + std::shared_ptr process_memory_; + JitDebug* jit_debug_ = nullptr; + DexFiles* dex_files_ = nullptr; + bool resolve_names_ = true; + bool display_build_id_ = false; + ErrorData last_error_; + uint64_t warnings_; + ArchEnum arch_ = ARCH_UNKNOWN; +}; + +class UnwinderFromPid : public Unwinder { + public: + UnwinderFromPid(size_t max_frames, pid_t pid, Maps* maps = nullptr) + : Unwinder(max_frames, maps), pid_(pid) {} + UnwinderFromPid(size_t max_frames, pid_t pid, std::shared_ptr& process_memory) + : Unwinder(max_frames, nullptr, process_memory), pid_(pid) {} + UnwinderFromPid(size_t max_frames, pid_t pid, ArchEnum arch, Maps* maps = nullptr) + : Unwinder(max_frames, arch, maps), pid_(pid) {} + UnwinderFromPid(size_t max_frames, pid_t pid, ArchEnum arch, Maps* maps, + std::shared_ptr& process_memory) + : Unwinder(max_frames, arch, maps, process_memory), pid_(pid) {} + virtual ~UnwinderFromPid() = default; + + bool Init(); + + void Unwind(const std::vector* initial_map_names_to_skip = nullptr, + const std::vector* map_suffixes_to_ignore = nullptr) override; + + protected: + pid_t pid_; + std::unique_ptr maps_ptr_; + std::unique_ptr jit_debug_ptr_; + std::unique_ptr dex_files_ptr_; + bool initted_ = false; +}; + +class ThreadUnwinder : public UnwinderFromPid { + public: + ThreadUnwinder(size_t max_frames, Maps* maps = nullptr); + ThreadUnwinder(size_t max_frames, Maps* maps, std::shared_ptr& process_memory); + ThreadUnwinder(size_t max_frames, const ThreadUnwinder* unwinder); + virtual ~ThreadUnwinder() = default; + + void SetObjects(ThreadUnwinder* unwinder); + + void Unwind(const std::vector*, const std::vector*) override {} + + void UnwindWithSignal(int signal, pid_t tid, std::unique_ptr* initial_regs = nullptr, + const std::vector* initial_map_names_to_skip = nullptr, + const std::vector* map_suffixes_to_ignore = nullptr); + + protected: + ThreadEntry* SendSignalToThread(int signal, pid_t tid); +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm.h new file mode 100644 index 0000000000..725a35bf8a --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +namespace unwindstack { + +struct arm_user_regs { + uint32_t regs[18]; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm64.h new file mode 100644 index 0000000000..0e16cd6141 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserArm64.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +namespace unwindstack { + +struct arm64_user_regs { + uint64_t regs[31]; + uint64_t sp; + uint64_t pc; + uint64_t pstate; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86.h new file mode 100644 index 0000000000..9508010d99 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +namespace unwindstack { + +struct x86_user_regs { + uint32_t ebx; + uint32_t ecx; + uint32_t edx; + uint32_t esi; + uint32_t edi; + uint32_t ebp; + uint32_t eax; + uint32_t xds; + uint32_t xes; + uint32_t xfs; + uint32_t xgs; + uint32_t orig_eax; + uint32_t eip; + uint32_t xcs; + uint32_t eflags; + uint32_t esp; + uint32_t xss; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86_64.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86_64.h new file mode 100644 index 0000000000..d7ff2e2514 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/include/unwindstack/UserX86_64.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#pragma once + +namespace unwindstack { + +struct x86_64_user_regs { + uint64_t r15; + uint64_t r14; + uint64_t r13; + uint64_t r12; + uint64_t rbp; + uint64_t rbx; + uint64_t r11; + uint64_t r10; + uint64_t r9; + uint64_t r8; + uint64_t rax; + uint64_t rcx; + uint64_t rdx; + uint64_t rsi; + uint64_t rdi; + uint64_t orig_rax; + uint64_t rip; + uint64_t cs; + uint64_t eflags; + uint64_t rsp; + uint64_t ss; + uint64_t fs_base; + uint64_t gs_base; + uint64_t ds; + uint64_t es; + uint64_t fs; + uint64_t gs; +}; + +} // namespace unwindstack diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/unistdfix.h b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/unistdfix.h new file mode 100644 index 0000000000..ae1886f355 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/libunwindstack-ndk/unistdfix.h @@ -0,0 +1,7 @@ +#undef TEMP_FAILURE_RETRY +#define TEMP_FAILURE_RETRY(exp) ({ \ + __typeof__(exp) _rc; \ + do { \ + _rc = (exp); \ + } while (_rc == -1 && errno == EINTR); \ + _rc; }) diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.c b/embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.c new file mode 100644 index 0000000000..29bf74e523 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.c @@ -0,0 +1,2424 @@ +/* + SPDX-License-Identifier: MIT + + Parson 1.2.1 ( http://kgabis.github.com/parson/ ) + Copyright (c) 2012 - 2021 Krzysztof Gabis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#ifdef _MSC_VER +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif /* _CRT_SECURE_NO_WARNINGS */ +#endif /* _MSC_VER */ + +#include "parson.h" + +#define PARSON_IMPL_VERSION_MAJOR 1 +#define PARSON_IMPL_VERSION_MINOR 2 +#define PARSON_IMPL_VERSION_PATCH 1 + +#if (PARSON_VERSION_MAJOR != PARSON_IMPL_VERSION_MAJOR)\ +|| (PARSON_VERSION_MINOR != PARSON_IMPL_VERSION_MINOR)\ +|| (PARSON_VERSION_PATCH != PARSON_IMPL_VERSION_PATCH) +#error "parson version mismatch between parson.c and parson.h" +#endif + +#include +#include +#include +#include +#include +#include + +/* Apparently sscanf is not implemented in some "standard" libraries, so don't use it, if you + * don't have to. */ +#ifdef sscanf +#undef sscanf +#define sscanf THINK_TWICE_ABOUT_USING_SSCANF +#endif + +/* strcpy is unsafe */ +#ifdef strcpy +#undef strcpy +#endif +#define strcpy USE_MEMCPY_INSTEAD_OF_STRCPY + +#define STARTING_CAPACITY 16 +#define MAX_NESTING 2048 + +#define FLOAT_FORMAT "%1.17g" /* do not increase precision without incresing NUM_BUF_SIZE */ +#define NUM_BUF_SIZE 64 /* double printed with "%1.17g" shouldn't be longer than 25 bytes so let's be paranoid and use 64 */ + +#define SIZEOF_TOKEN(a) (sizeof(a) - 1) +#define SKIP_CHAR(str) ((*str)++) +#define SKIP_WHITESPACES(str) while (isspace((unsigned char)(**str))) { SKIP_CHAR(str); } +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +#undef malloc +#undef free + +#if defined(isnan) && defined(isinf) +#define IS_NUMBER_INVALID(x) (isnan((x)) || isinf((x))) +#else +#define IS_NUMBER_INVALID(x) (((x) * 0.0) != 0.0) +#endif + +#define OBJECT_INVALID_IX ((size_t)-1) + +static JSON_Malloc_Function parson_malloc = malloc; +static JSON_Free_Function parson_free = free; + +static int parson_escape_slashes = 1; + +#define IS_CONT(b) (((unsigned char)(b) & 0xC0) == 0x80) /* is utf-8 continuation byte */ + +typedef int parson_bool_t; + +#define PARSON_TRUE 1 +#define PARSON_FALSE 0 + +typedef struct json_string { + char *chars; + size_t length; +} JSON_String; + +/* Type definitions */ +typedef union json_value_value { + JSON_String string; + double number; + JSON_Object *object; + JSON_Array *array; + int boolean; + int null; +} JSON_Value_Value; + +struct json_value_t { + JSON_Value *parent; + JSON_Value_Type type; + JSON_Value_Value value; +}; + +struct json_object_t { + JSON_Value *wrapping_value; + size_t *cells; + unsigned long *hashes; + char **names; + JSON_Value **values; + size_t *cell_ixs; + size_t count; + size_t item_capacity; + size_t cell_capacity; +}; + +struct json_array_t { + JSON_Value *wrapping_value; + JSON_Value **items; + size_t count; + size_t capacity; +}; + +/* Various */ +static char * read_file(const char *filename); +static void remove_comments(char *string, const char *start_token, const char *end_token); +static char * parson_strndup(const char *string, size_t n); +static char * parson_strdup(const char *string); +static int hex_char_to_int(char c); +static JSON_Status parse_utf16_hex(const char *string, unsigned int *result); +static int num_bytes_in_utf8_sequence(unsigned char c); +static JSON_Status verify_utf8_sequence(const unsigned char *string, int *len); +static parson_bool_t is_valid_utf8(const char *string, size_t string_len); +static parson_bool_t is_decimal(const char *string, size_t length); +static unsigned long hash_string(const char *string, size_t n); + +/* JSON Object */ +static JSON_Object * json_object_make(JSON_Value *wrapping_value); +static JSON_Status json_object_init(JSON_Object *object, size_t capacity); +static void json_object_deinit(JSON_Object *object, parson_bool_t free_keys, parson_bool_t free_values); +static JSON_Status json_object_grow_and_rehash(JSON_Object *object); +static size_t json_object_get_cell_ix(const JSON_Object *object, const char *key, size_t key_len, unsigned long hash, parson_bool_t *out_found); +static JSON_Status json_object_add(JSON_Object *object, char *name, JSON_Value *value); +static JSON_Value * json_object_getn_value(const JSON_Object *object, const char *name, size_t name_len); +static JSON_Status json_object_remove_internal(JSON_Object *object, const char *name, parson_bool_t free_value); +static JSON_Status json_object_dotremove_internal(JSON_Object *object, const char *name, parson_bool_t free_value); +static void json_object_free(JSON_Object *object); + +/* JSON Array */ +static JSON_Array * json_array_make(JSON_Value *wrapping_value); +static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value); +static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity); +static void json_array_free(JSON_Array *array); + +/* JSON Value */ +static JSON_Value * json_value_init_string_no_copy(char *string, size_t length); +static const JSON_String * json_value_get_string_desc(const JSON_Value *value); + +/* Parser */ +static JSON_Status skip_quotes(const char **string); +static JSON_Status parse_utf16(const char **unprocessed, char **processed); +static char * process_string(const char *input, size_t input_len, size_t *output_len); +static char * get_quoted_string(const char **string, size_t *output_string_len); +static JSON_Value * parse_object_value(const char **string, size_t nesting); +static JSON_Value * parse_array_value(const char **string, size_t nesting); +static JSON_Value * parse_string_value(const char **string); +static JSON_Value * parse_boolean_value(const char **string); +static JSON_Value * parse_number_value(const char **string); +static JSON_Value * parse_null_value(const char **string); +static JSON_Value * parse_value(const char **string, size_t nesting); + +/* Serialization */ +static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, parson_bool_t is_pretty, char *num_buf); +static int json_serialize_string(const char *string, size_t len, char *buf); +static int append_indent(char *buf, int level); +static int append_string(char *buf, const char *string); + +/* Various */ +static char * read_file(const char * filename) { + FILE *fp = fopen(filename, "r"); + size_t size_to_read = 0; + size_t size_read = 0; + long pos; + char *file_contents; + if (!fp) { + return NULL; + } + fseek(fp, 0L, SEEK_END); + pos = ftell(fp); + if (pos < 0) { + fclose(fp); + return NULL; + } + size_to_read = pos; + rewind(fp); + file_contents = (char*)parson_malloc(sizeof(char) * (size_to_read + 1)); + if (!file_contents) { + fclose(fp); + return NULL; + } + size_read = fread(file_contents, 1, size_to_read, fp); + if (size_read == 0 || ferror(fp)) { + fclose(fp); + parson_free(file_contents); + return NULL; + } + fclose(fp); + file_contents[size_read] = '\0'; + return file_contents; +} + +static void remove_comments(char *string, const char *start_token, const char *end_token) { + parson_bool_t in_string = PARSON_FALSE, escaped = PARSON_FALSE; + size_t i; + char *ptr = NULL, current_char; + size_t start_token_len = strlen(start_token); + size_t end_token_len = strlen(end_token); + if (start_token_len == 0 || end_token_len == 0) { + return; + } + while ((current_char = *string) != '\0') { + if (current_char == '\\' && !escaped) { + escaped = PARSON_TRUE; + string++; + continue; + } else if (current_char == '\"' && !escaped) { + in_string = !in_string; + } else if (!in_string && strncmp(string, start_token, start_token_len) == 0) { + for(i = 0; i < start_token_len; i++) { + string[i] = ' '; + } + string = string + start_token_len; + ptr = strstr(string, end_token); + if (!ptr) { + return; + } + for (i = 0; i < (ptr - string) + end_token_len; i++) { + string[i] = ' '; + } + string = ptr + end_token_len - 1; + } + escaped = PARSON_FALSE; + string++; + } +} + +static char * parson_strndup(const char *string, size_t n) { + /* We expect the caller has validated that 'n' fits within the input buffer. */ + char *output_string = (char*)parson_malloc(n + 1); + if (!output_string) { + return NULL; + } + output_string[n] = '\0'; + memcpy(output_string, string, n); + return output_string; +} + +static char * parson_strdup(const char *string) { + return parson_strndup(string, strlen(string)); +} + +static int hex_char_to_int(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } else if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; +} + +static JSON_Status parse_utf16_hex(const char *s, unsigned int *result) { + int x1, x2, x3, x4; + if (s[0] == '\0' || s[1] == '\0' || s[2] == '\0' || s[3] == '\0') { + return JSONFailure; + } + x1 = hex_char_to_int(s[0]); + x2 = hex_char_to_int(s[1]); + x3 = hex_char_to_int(s[2]); + x4 = hex_char_to_int(s[3]); + if (x1 == -1 || x2 == -1 || x3 == -1 || x4 == -1) { + return JSONFailure; + } + *result = (unsigned int)((x1 << 12) | (x2 << 8) | (x3 << 4) | x4); + return JSONSuccess; +} + +static int num_bytes_in_utf8_sequence(unsigned char c) { + if (c == 0xC0 || c == 0xC1 || c > 0xF4 || IS_CONT(c)) { + return 0; + } else if ((c & 0x80) == 0) { /* 0xxxxxxx */ + return 1; + } else if ((c & 0xE0) == 0xC0) { /* 110xxxxx */ + return 2; + } else if ((c & 0xF0) == 0xE0) { /* 1110xxxx */ + return 3; + } else if ((c & 0xF8) == 0xF0) { /* 11110xxx */ + return 4; + } + return 0; /* won't happen */ +} + +static JSON_Status verify_utf8_sequence(const unsigned char *string, int *len) { + unsigned int cp = 0; + *len = num_bytes_in_utf8_sequence(string[0]); + + if (*len == 1) { + cp = string[0]; + } else if (*len == 2 && IS_CONT(string[1])) { + cp = string[0] & 0x1F; + cp = (cp << 6) | (string[1] & 0x3F); + } else if (*len == 3 && IS_CONT(string[1]) && IS_CONT(string[2])) { + cp = ((unsigned char)string[0]) & 0xF; + cp = (cp << 6) | (string[1] & 0x3F); + cp = (cp << 6) | (string[2] & 0x3F); + } else if (*len == 4 && IS_CONT(string[1]) && IS_CONT(string[2]) && IS_CONT(string[3])) { + cp = string[0] & 0x7; + cp = (cp << 6) | (string[1] & 0x3F); + cp = (cp << 6) | (string[2] & 0x3F); + cp = (cp << 6) | (string[3] & 0x3F); + } else { + return JSONFailure; + } + + /* overlong encodings */ + if ((cp < 0x80 && *len > 1) || + (cp < 0x800 && *len > 2) || + (cp < 0x10000 && *len > 3)) { + return JSONFailure; + } + + /* invalid unicode */ + if (cp > 0x10FFFF) { + return JSONFailure; + } + + /* surrogate halves */ + if (cp >= 0xD800 && cp <= 0xDFFF) { + return JSONFailure; + } + + return JSONSuccess; +} + +static int is_valid_utf8(const char *string, size_t string_len) { + int len = 0; + const char *string_end = string + string_len; + while (string < string_end) { + if (verify_utf8_sequence((const unsigned char*)string, &len) != JSONSuccess) { + return PARSON_FALSE; + } + string += len; + } + return PARSON_TRUE; +} + +static parson_bool_t is_decimal(const char *string, size_t length) { + if (length > 1 && string[0] == '0' && string[1] != '.') { + return PARSON_FALSE; + } + if (length > 2 && !strncmp(string, "-0", 2) && string[2] != '.') { + return PARSON_FALSE; + } + while (length--) { + if (strchr("xX", string[length])) { + return PARSON_FALSE; + } + } + return PARSON_TRUE; +} + +static unsigned long hash_string(const char *string, size_t n) { +#ifdef PARSON_FORCE_HASH_COLLISIONS + (void)string; + (void)n; + return 0; +#else + unsigned long hash = 5381; + unsigned char c; + size_t i = 0; + for (i = 0; i < n; i++) { + c = string[i]; + if (c == '\0') { + break; + } + hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ + } + return hash; +#endif +} + +/* JSON Object */ +static JSON_Object * json_object_make(JSON_Value *wrapping_value) { + JSON_Status res = JSONFailure; + JSON_Object *new_obj = (JSON_Object*)parson_malloc(sizeof(JSON_Object)); + if (new_obj == NULL) { + return NULL; + } + new_obj->wrapping_value = wrapping_value; + res = json_object_init(new_obj, 0); + if (res != JSONSuccess) { + parson_free(new_obj); + return NULL; + } + return new_obj; +} + +static JSON_Status json_object_init(JSON_Object *object, size_t capacity) { + unsigned int i = 0; + + object->cells = NULL; + object->names = NULL; + object->values = NULL; + object->cell_ixs = NULL; + object->hashes = NULL; + + object->count = 0; + object->cell_capacity = capacity; + object->item_capacity = (unsigned int)(capacity * 0.7f); + + if (capacity == 0) { + return JSONSuccess; + } + + object->cells = (size_t*)parson_malloc(object->cell_capacity * sizeof(*object->cells)); + object->names = (char**)parson_malloc(object->item_capacity * sizeof(*object->names)); + object->values = (JSON_Value**)parson_malloc(object->item_capacity * sizeof(*object->values)); + object->cell_ixs = (size_t*)parson_malloc(object->item_capacity * sizeof(*object->cell_ixs)); + object->hashes = (unsigned long*)parson_malloc(object->item_capacity * sizeof(*object->hashes)); + if (object->cells == NULL + || object->names == NULL + || object->values == NULL + || object->cell_ixs == NULL + || object->hashes == NULL) { + goto error; + } + for (i = 0; i < object->cell_capacity; i++) { + object->cells[i] = OBJECT_INVALID_IX; + } + return JSONSuccess; + error: + parson_free(object->cells); + parson_free(object->names); + parson_free(object->values); + parson_free(object->cell_ixs); + parson_free(object->hashes); + return JSONFailure; +} + +static void json_object_deinit(JSON_Object *object, parson_bool_t free_keys, parson_bool_t free_values) { + unsigned int i = 0; + for (i = 0; i < object->count; i++) { + if (free_keys) { + parson_free(object->names[i]); + } + if (free_values) { + json_value_free(object->values[i]); + } + } + + object->count = 0; + object->item_capacity = 0; + object->cell_capacity = 0; + + parson_free(object->cells); + parson_free(object->names); + parson_free(object->values); + parson_free(object->cell_ixs); + parson_free(object->hashes); + + object->cells = NULL; + object->names = NULL; + object->values = NULL; + object->cell_ixs = NULL; + object->hashes = NULL; +} + +static JSON_Status json_object_grow_and_rehash(JSON_Object *object) { + JSON_Value *wrapping_value = NULL; + JSON_Object new_object; + char *key = NULL; + JSON_Value *value = NULL; + unsigned int i = 0; + size_t new_capacity = MAX(object->cell_capacity * 2, STARTING_CAPACITY); + JSON_Status res = json_object_init(&new_object, new_capacity); + if (res != JSONSuccess) { + return JSONFailure; + } + + wrapping_value = json_object_get_wrapping_value(object); + new_object.wrapping_value = wrapping_value; + + for (i = 0; i < object->count; i++) { + key = object->names[i]; + value = object->values[i]; + res = json_object_add(&new_object, key, value); + if (res != JSONSuccess) { + json_object_deinit(&new_object, PARSON_FALSE, PARSON_FALSE); + return JSONFailure; + } + value->parent = wrapping_value; + } + json_object_deinit(object, PARSON_FALSE, PARSON_FALSE); + *object = new_object; + return JSONSuccess; +} + +static size_t json_object_get_cell_ix(const JSON_Object *object, const char *key, size_t key_len, unsigned long hash, parson_bool_t *out_found) { + size_t cell_ix = hash & (object->cell_capacity - 1); + size_t cell = 0; + size_t ix = 0; + unsigned int i = 0; + unsigned long hash_to_check = 0; + const char *key_to_check = NULL; + size_t key_to_check_len = 0; + + *out_found = PARSON_FALSE; + + for (i = 0; i < object->cell_capacity; i++) { + ix = (cell_ix + i) & (object->cell_capacity - 1); + cell = object->cells[ix]; + if (cell == OBJECT_INVALID_IX) { + return ix; + } + hash_to_check = object->hashes[cell]; + if (hash != hash_to_check) { + continue; + } + key_to_check = object->names[cell]; + key_to_check_len = strlen(key_to_check); + if (key_to_check_len == key_len && strncmp(key, key_to_check, key_len) == 0) { + *out_found = PARSON_TRUE; + return ix; + } + } + return OBJECT_INVALID_IX; +} + +static JSON_Status json_object_add(JSON_Object *object, char *name, JSON_Value *value) { + unsigned long hash = 0; + parson_bool_t found = PARSON_FALSE; + size_t cell_ix = 0; + JSON_Status res = JSONFailure; + + if (!object || !name || !value) { + return JSONFailure; + } + + hash = hash_string(name, strlen(name)); + found = PARSON_FALSE; + cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); + if (found) { + return JSONFailure; + } + + if (object->count >= object->item_capacity) { + res = json_object_grow_and_rehash(object); + if (res != JSONSuccess) { + return JSONFailure; + } + cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); + } + + object->names[object->count] = name; + object->cells[cell_ix] = object->count; + object->values[object->count] = value; + object->cell_ixs[object->count] = cell_ix; + object->hashes[object->count] = hash; + object->count++; + value->parent = json_object_get_wrapping_value(object); + + return JSONSuccess; +} + +static JSON_Value * json_object_getn_value(const JSON_Object *object, const char *name, size_t name_len) { + unsigned long hash = 0; + parson_bool_t found = PARSON_FALSE; + unsigned long cell_ix = 0; + size_t item_ix = 0; + if (!object || !name) { + return NULL; + } + hash = hash_string(name, name_len); + found = PARSON_FALSE; + cell_ix = json_object_get_cell_ix(object, name, name_len, hash, &found); + if (!found) { + return NULL; + } + item_ix = object->cells[cell_ix]; + return object->values[item_ix]; +} + +static JSON_Status json_object_remove_internal(JSON_Object *object, const char *name, parson_bool_t free_value) { + unsigned long hash = 0; + parson_bool_t found = PARSON_FALSE; + size_t cell = 0; + size_t item_ix = 0; + size_t last_item_ix = 0; + size_t i = 0; + size_t j = 0; + size_t x = 0; + size_t k = 0; + JSON_Value *val = NULL; + + if (object == NULL) { + return JSONFailure; + } + + hash = hash_string(name, strlen(name)); + found = PARSON_FALSE; + cell = json_object_get_cell_ix(object, name, strlen(name), hash, &found); + if (!found) { + return JSONFailure; + } + + item_ix = object->cells[cell]; + if (free_value) { + val = object->values[item_ix]; + json_value_free(val); + val = NULL; + } + + parson_free(object->names[item_ix]); + last_item_ix = object->count - 1; + if (item_ix < last_item_ix) { + object->names[item_ix] = object->names[last_item_ix]; + object->values[item_ix] = object->values[last_item_ix]; + object->cell_ixs[item_ix] = object->cell_ixs[last_item_ix]; + object->hashes[item_ix] = object->hashes[last_item_ix]; + object->cells[object->cell_ixs[item_ix]] = item_ix; + } + object->count--; + + i = cell; + j = i; + for (x = 0; x < (object->cell_capacity - 1); x++) { + j = (j + 1) & (object->cell_capacity - 1); + if (object->cells[j] == OBJECT_INVALID_IX) { + break; + } + k = object->hashes[object->cells[j]] & (object->cell_capacity - 1); + if ((j > i && (k <= i || k > j)) + || (j < i && (k <= i && k > j))) { + object->cell_ixs[object->cells[j]] = i; + object->cells[i] = object->cells[j]; + i = j; + } + } + object->cells[i] = OBJECT_INVALID_IX; + return JSONSuccess; +} + +static JSON_Status json_object_dotremove_internal(JSON_Object *object, const char *name, parson_bool_t free_value) { + JSON_Value *temp_value = NULL; + JSON_Object *temp_object = NULL; + const char *dot_pos = strchr(name, '.'); + if (!dot_pos) { + return json_object_remove_internal(object, name, free_value); + } + temp_value = json_object_getn_value(object, name, dot_pos - name); + if (json_value_get_type(temp_value) != JSONObject) { + return JSONFailure; + } + temp_object = json_value_get_object(temp_value); + return json_object_dotremove_internal(temp_object, dot_pos + 1, free_value); +} + +static void json_object_free(JSON_Object *object) { + json_object_deinit(object, PARSON_TRUE, PARSON_TRUE); + parson_free(object); +} + +/* JSON Array */ +static JSON_Array * json_array_make(JSON_Value *wrapping_value) { + JSON_Array *new_array = (JSON_Array*)parson_malloc(sizeof(JSON_Array)); + if (new_array == NULL) { + return NULL; + } + new_array->wrapping_value = wrapping_value; + new_array->items = (JSON_Value**)NULL; + new_array->capacity = 0; + new_array->count = 0; + return new_array; +} + +static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value) { + if (array->count >= array->capacity) { + size_t new_capacity = MAX(array->capacity * 2, STARTING_CAPACITY); + if (json_array_resize(array, new_capacity) != JSONSuccess) { + return JSONFailure; + } + } + value->parent = json_array_get_wrapping_value(array); + array->items[array->count] = value; + array->count++; + return JSONSuccess; +} + +static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity) { + JSON_Value **new_items = NULL; + if (new_capacity == 0) { + return JSONFailure; + } + new_items = (JSON_Value**)parson_malloc(new_capacity * sizeof(JSON_Value*)); + if (new_items == NULL) { + return JSONFailure; + } + if (array->items != NULL && array->count > 0) { + memcpy(new_items, array->items, array->count * sizeof(JSON_Value*)); + } + parson_free(array->items); + array->items = new_items; + array->capacity = new_capacity; + return JSONSuccess; +} + +static void json_array_free(JSON_Array *array) { + size_t i; + for (i = 0; i < array->count; i++) { + json_value_free(array->items[i]); + } + parson_free(array->items); + parson_free(array); +} + +/* JSON Value */ +static JSON_Value * json_value_init_string_no_copy(char *string, size_t length) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { + return NULL; + } + new_value->parent = NULL; + new_value->type = JSONString; + new_value->value.string.chars = string; + new_value->value.string.length = length; + return new_value; +} + +/* Parser */ +static JSON_Status skip_quotes(const char **string) { + if (**string != '\"') { + return JSONFailure; + } + SKIP_CHAR(string); + while (**string != '\"') { + if (**string == '\0') { + return JSONFailure; + } else if (**string == '\\') { + SKIP_CHAR(string); + if (**string == '\0') { + return JSONFailure; + } + } + SKIP_CHAR(string); + } + SKIP_CHAR(string); + return JSONSuccess; +} + +static JSON_Status parse_utf16(const char **unprocessed, char **processed) { + unsigned int cp, lead, trail; + char *processed_ptr = *processed; + const char *unprocessed_ptr = *unprocessed; + JSON_Status status = JSONFailure; + unprocessed_ptr++; /* skips u */ + status = parse_utf16_hex(unprocessed_ptr, &cp); + if (status != JSONSuccess) { + return JSONFailure; + } + if (cp < 0x80) { + processed_ptr[0] = (char)cp; /* 0xxxxxxx */ + } else if (cp < 0x800) { + processed_ptr[0] = ((cp >> 6) & 0x1F) | 0xC0; /* 110xxxxx */ + processed_ptr[1] = ((cp) & 0x3F) | 0x80; /* 10xxxxxx */ + processed_ptr += 1; + } else if (cp < 0xD800 || cp > 0xDFFF) { + processed_ptr[0] = ((cp >> 12) & 0x0F) | 0xE0; /* 1110xxxx */ + processed_ptr[1] = ((cp >> 6) & 0x3F) | 0x80; /* 10xxxxxx */ + processed_ptr[2] = ((cp) & 0x3F) | 0x80; /* 10xxxxxx */ + processed_ptr += 2; + } else if (cp >= 0xD800 && cp <= 0xDBFF) { /* lead surrogate (0xD800..0xDBFF) */ + lead = cp; + unprocessed_ptr += 4; /* should always be within the buffer, otherwise previous sscanf would fail */ + if (*unprocessed_ptr++ != '\\' || *unprocessed_ptr++ != 'u') { + return JSONFailure; + } + status = parse_utf16_hex(unprocessed_ptr, &trail); + if (status != JSONSuccess || trail < 0xDC00 || trail > 0xDFFF) { /* valid trail surrogate? (0xDC00..0xDFFF) */ + return JSONFailure; + } + cp = ((((lead - 0xD800) & 0x3FF) << 10) | ((trail - 0xDC00) & 0x3FF)) + 0x010000; + processed_ptr[0] = (((cp >> 18) & 0x07) | 0xF0); /* 11110xxx */ + processed_ptr[1] = (((cp >> 12) & 0x3F) | 0x80); /* 10xxxxxx */ + processed_ptr[2] = (((cp >> 6) & 0x3F) | 0x80); /* 10xxxxxx */ + processed_ptr[3] = (((cp) & 0x3F) | 0x80); /* 10xxxxxx */ + processed_ptr += 3; + } else { /* trail surrogate before lead surrogate */ + return JSONFailure; + } + unprocessed_ptr += 3; + *processed = processed_ptr; + *unprocessed = unprocessed_ptr; + return JSONSuccess; +} + + +/* Copies and processes passed string up to supplied length. +Example: "\u006Corem ipsum" -> lorem ipsum */ +static char* process_string(const char *input, size_t input_len, size_t *output_len) { + const char *input_ptr = input; + size_t initial_size = (input_len + 1) * sizeof(char); + size_t final_size = 0; + char *output = NULL, *output_ptr = NULL, *resized_output = NULL; + output = (char*)parson_malloc(initial_size); + if (output == NULL) { + goto error; + } + output_ptr = output; + while ((*input_ptr != '\0') && (size_t)(input_ptr - input) < input_len) { + if (*input_ptr == '\\') { + input_ptr++; + switch (*input_ptr) { + case '\"': *output_ptr = '\"'; break; + case '\\': *output_ptr = '\\'; break; + case '/': *output_ptr = '/'; break; + case 'b': *output_ptr = '\b'; break; + case 'f': *output_ptr = '\f'; break; + case 'n': *output_ptr = '\n'; break; + case 'r': *output_ptr = '\r'; break; + case 't': *output_ptr = '\t'; break; + case 'u': + if (parse_utf16(&input_ptr, &output_ptr) != JSONSuccess) { + goto error; + } + break; + default: + goto error; + } + } else if ((unsigned char)*input_ptr < 0x20) { + goto error; /* 0x00-0x19 are invalid characters for json string (http://www.ietf.org/rfc/rfc4627.txt) */ + } else { + *output_ptr = *input_ptr; + } + output_ptr++; + input_ptr++; + } + *output_ptr = '\0'; + /* resize to new length */ + final_size = (size_t)(output_ptr-output) + 1; + /* todo: don't resize if final_size == initial_size */ + resized_output = (char*)parson_malloc(final_size); + if (resized_output == NULL) { + goto error; + } + memcpy(resized_output, output, final_size); + *output_len = final_size - 1; + parson_free(output); + return resized_output; + error: + parson_free(output); + return NULL; +} + +/* Return processed contents of a string between quotes and + skips passed argument to a matching quote. */ +static char * get_quoted_string(const char **string, size_t *output_string_len) { + const char *string_start = *string; + size_t input_string_len = 0; + JSON_Status status = skip_quotes(string); + if (status != JSONSuccess) { + return NULL; + } + input_string_len = *string - string_start - 2; /* length without quotes */ + return process_string(string_start + 1, input_string_len, output_string_len); +} + +static JSON_Value * parse_value(const char **string, size_t nesting) { + if (nesting > MAX_NESTING) { + return NULL; + } + SKIP_WHITESPACES(string); + switch (**string) { + case '{': + return parse_object_value(string, nesting + 1); + case '[': + return parse_array_value(string, nesting + 1); + case '\"': + return parse_string_value(string); + case 'f': case 't': + return parse_boolean_value(string); + case '-': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return parse_number_value(string); + case 'n': + return parse_null_value(string); + default: + return NULL; + } +} + +static JSON_Value * parse_object_value(const char **string, size_t nesting) { + JSON_Status status = JSONFailure; + JSON_Value *output_value = NULL, *new_value = NULL; + JSON_Object *output_object = NULL; + char *new_key = NULL; + + output_value = json_value_init_object(); + if (output_value == NULL) { + return NULL; + } + if (**string != '{') { + json_value_free(output_value); + return NULL; + } + output_object = json_value_get_object(output_value); + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + if (**string == '}') { /* empty object */ + SKIP_CHAR(string); + return output_value; + } + while (**string != '\0') { + size_t key_len = 0; + new_key = get_quoted_string(string, &key_len); + /* We do not support key names with embedded \0 chars */ + if (!new_key) { + json_value_free(output_value); + return NULL; + } + if (key_len != strlen(new_key)) { + parson_free(new_key); + json_value_free(output_value); + return NULL; + } + SKIP_WHITESPACES(string); + if (**string != ':') { + parson_free(new_key); + json_value_free(output_value); + return NULL; + } + SKIP_CHAR(string); + new_value = parse_value(string, nesting); + if (new_value == NULL) { + parson_free(new_key); + json_value_free(output_value); + return NULL; + } + status = json_object_add(output_object, new_key, new_value); + if (status != JSONSuccess) { + parson_free(new_key); + json_value_free(new_value); + json_value_free(output_value); + return NULL; + } + SKIP_WHITESPACES(string); + if (**string != ',') { + break; + } + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + } + SKIP_WHITESPACES(string); + if (**string != '}') { + json_value_free(output_value); + return NULL; + } + SKIP_CHAR(string); + return output_value; +} + +static JSON_Value * parse_array_value(const char **string, size_t nesting) { + JSON_Value *output_value = NULL, *new_array_value = NULL; + JSON_Array *output_array = NULL; + output_value = json_value_init_array(); + if (output_value == NULL) { + return NULL; + } + if (**string != '[') { + json_value_free(output_value); + return NULL; + } + output_array = json_value_get_array(output_value); + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + if (**string == ']') { /* empty array */ + SKIP_CHAR(string); + return output_value; + } + while (**string != '\0') { + new_array_value = parse_value(string, nesting); + if (new_array_value == NULL) { + json_value_free(output_value); + return NULL; + } + if (json_array_add(output_array, new_array_value) != JSONSuccess) { + json_value_free(new_array_value); + json_value_free(output_value); + return NULL; + } + SKIP_WHITESPACES(string); + if (**string != ',') { + break; + } + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + } + SKIP_WHITESPACES(string); + if (**string != ']' || /* Trim array after parsing is over */ + json_array_resize(output_array, json_array_get_count(output_array)) != JSONSuccess) { + json_value_free(output_value); + return NULL; + } + SKIP_CHAR(string); + return output_value; +} + +static JSON_Value * parse_string_value(const char **string) { + JSON_Value *value = NULL; + size_t new_string_len = 0; + char *new_string = get_quoted_string(string, &new_string_len); + if (new_string == NULL) { + return NULL; + } + value = json_value_init_string_no_copy(new_string, new_string_len); + if (value == NULL) { + parson_free(new_string); + return NULL; + } + return value; +} + +static JSON_Value * parse_boolean_value(const char **string) { + size_t true_token_size = SIZEOF_TOKEN("true"); + size_t false_token_size = SIZEOF_TOKEN("false"); + if (strncmp("true", *string, true_token_size) == 0) { + *string += true_token_size; + return json_value_init_boolean(1); + } else if (strncmp("false", *string, false_token_size) == 0) { + *string += false_token_size; + return json_value_init_boolean(0); + } + return NULL; +} + +static JSON_Value * parse_number_value(const char **string) { + char *end; + double number = 0; + errno = 0; + number = strtod(*string, &end); + if (errno == ERANGE && (number <= -HUGE_VAL || number >= HUGE_VAL)) { + return NULL; + } + if ((errno && errno != ERANGE) || !is_decimal(*string, end - *string)) { + return NULL; + } + *string = end; + return json_value_init_number(number); +} + +static JSON_Value * parse_null_value(const char **string) { + size_t token_size = SIZEOF_TOKEN("null"); + if (strncmp("null", *string, token_size) == 0) { + *string += token_size; + return json_value_init_null(); + } + return NULL; +} + +/* Serialization */ +#define APPEND_STRING(str) do { written = append_string(buf, (str));\ + if (written < 0) { return -1; }\ + if (buf != NULL) { buf += written; }\ + written_total += written; } while(0) + +#define APPEND_INDENT(level) do { written = append_indent(buf, (level));\ + if (written < 0) { return -1; }\ + if (buf != NULL) { buf += written; }\ + written_total += written; } while(0) + +static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, parson_bool_t is_pretty, char *num_buf) +{ + const char *key = NULL, *string = NULL; + JSON_Value *temp_value = NULL; + JSON_Array *array = NULL; + JSON_Object *object = NULL; + size_t i = 0, count = 0; + double num = 0.0; + int written = -1, written_total = 0; + size_t len = 0; + + switch (json_value_get_type(value)) { + case JSONArray: + array = json_value_get_array(value); + count = json_array_get_count(array); + APPEND_STRING("["); + if (count > 0 && is_pretty) { + APPEND_STRING("\n"); + } + for (i = 0; i < count; i++) { + if (is_pretty) { + APPEND_INDENT(level+1); + } + temp_value = json_array_get_value(array, i); + written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf); + if (written < 0) { + return -1; + } + if (buf != NULL) { + buf += written; + } + written_total += written; + if (i < (count - 1)) { + APPEND_STRING(","); + } + if (is_pretty) { + APPEND_STRING("\n"); + } + } + if (count > 0 && is_pretty) { + APPEND_INDENT(level); + } + APPEND_STRING("]"); + return written_total; + case JSONObject: + object = json_value_get_object(value); + count = json_object_get_count(object); + APPEND_STRING("{"); + if (count > 0 && is_pretty) { + APPEND_STRING("\n"); + } + for (i = 0; i < count; i++) { + key = json_object_get_name(object, i); + if (key == NULL) { + return -1; + } + if (is_pretty) { + APPEND_INDENT(level+1); + } + /* We do not support key names with embedded \0 chars */ + written = json_serialize_string(key, strlen(key), buf); + if (written < 0) { + return -1; + } + if (buf != NULL) { + buf += written; + } + written_total += written; + APPEND_STRING(":"); + if (is_pretty) { + APPEND_STRING(" "); + } + temp_value = json_object_get_value_at(object, i); + written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf); + if (written < 0) { + return -1; + } + if (buf != NULL) { + buf += written; + } + written_total += written; + if (i < (count - 1)) { + APPEND_STRING(","); + } + if (is_pretty) { + APPEND_STRING("\n"); + } + } + if (count > 0 && is_pretty) { + APPEND_INDENT(level); + } + APPEND_STRING("}"); + return written_total; + case JSONString: + string = json_value_get_string(value); + if (string == NULL) { + return -1; + } + len = json_value_get_string_len(value); + written = json_serialize_string(string, len, buf); + if (written < 0) { + return -1; + } + if (buf != NULL) { + buf += written; + } + written_total += written; + return written_total; + case JSONBoolean: + if (json_value_get_boolean(value)) { + APPEND_STRING("true"); + } else { + APPEND_STRING("false"); + } + return written_total; + case JSONNumber: + num = json_value_get_number(value); + if (buf != NULL) { + num_buf = buf; + } + written = sprintf(num_buf, FLOAT_FORMAT, num); + if (written < 0) { + return -1; + } + if (buf != NULL) { + buf += written; + } + written_total += written; + return written_total; + case JSONNull: + APPEND_STRING("null"); + return written_total; + case JSONError: + return -1; + default: + return -1; + } +} + +static int json_serialize_string(const char *string, size_t len, char *buf) { + size_t i = 0; + char c = '\0'; + int written = -1, written_total = 0; + APPEND_STRING("\""); + for (i = 0; i < len; i++) { + c = string[i]; + switch (c) { + case '\"': APPEND_STRING("\\\""); break; + case '\\': APPEND_STRING("\\\\"); break; + case '\b': APPEND_STRING("\\b"); break; + case '\f': APPEND_STRING("\\f"); break; + case '\n': APPEND_STRING("\\n"); break; + case '\r': APPEND_STRING("\\r"); break; + case '\t': APPEND_STRING("\\t"); break; + case '\x00': APPEND_STRING("\\u0000"); break; + case '\x01': APPEND_STRING("\\u0001"); break; + case '\x02': APPEND_STRING("\\u0002"); break; + case '\x03': APPEND_STRING("\\u0003"); break; + case '\x04': APPEND_STRING("\\u0004"); break; + case '\x05': APPEND_STRING("\\u0005"); break; + case '\x06': APPEND_STRING("\\u0006"); break; + case '\x07': APPEND_STRING("\\u0007"); break; + /* '\x08' duplicate: '\b' */ + /* '\x09' duplicate: '\t' */ + /* '\x0a' duplicate: '\n' */ + case '\x0b': APPEND_STRING("\\u000b"); break; + /* '\x0c' duplicate: '\f' */ + /* '\x0d' duplicate: '\r' */ + case '\x0e': APPEND_STRING("\\u000e"); break; + case '\x0f': APPEND_STRING("\\u000f"); break; + case '\x10': APPEND_STRING("\\u0010"); break; + case '\x11': APPEND_STRING("\\u0011"); break; + case '\x12': APPEND_STRING("\\u0012"); break; + case '\x13': APPEND_STRING("\\u0013"); break; + case '\x14': APPEND_STRING("\\u0014"); break; + case '\x15': APPEND_STRING("\\u0015"); break; + case '\x16': APPEND_STRING("\\u0016"); break; + case '\x17': APPEND_STRING("\\u0017"); break; + case '\x18': APPEND_STRING("\\u0018"); break; + case '\x19': APPEND_STRING("\\u0019"); break; + case '\x1a': APPEND_STRING("\\u001a"); break; + case '\x1b': APPEND_STRING("\\u001b"); break; + case '\x1c': APPEND_STRING("\\u001c"); break; + case '\x1d': APPEND_STRING("\\u001d"); break; + case '\x1e': APPEND_STRING("\\u001e"); break; + case '\x1f': APPEND_STRING("\\u001f"); break; + case '/': + if (parson_escape_slashes) { + APPEND_STRING("\\/"); /* to make json embeddable in xml\/html */ + } else { + APPEND_STRING("/"); + } + break; + default: + if (buf != NULL) { + buf[0] = c; + buf += 1; + } + written_total += 1; + break; + } + } + APPEND_STRING("\""); + return written_total; +} + +static int append_indent(char *buf, int level) { + int i; + int written = -1, written_total = 0; + for (i = 0; i < level; i++) { + APPEND_STRING(" "); + } + return written_total; +} + +static int append_string(char *buf, const char *string) { + if (buf == NULL) { + return (int)strlen(string); + } + return sprintf(buf, "%s", string); +} + +#undef APPEND_STRING +#undef APPEND_INDENT + +/* Parser API */ +JSON_Value * json_parse_file(const char *filename) { + char *file_contents = read_file(filename); + JSON_Value *output_value = NULL; + if (file_contents == NULL) { + return NULL; + } + output_value = json_parse_string(file_contents); + parson_free(file_contents); + return output_value; +} + +JSON_Value * json_parse_file_with_comments(const char *filename) { + char *file_contents = read_file(filename); + JSON_Value *output_value = NULL; + if (file_contents == NULL) { + return NULL; + } + output_value = json_parse_string_with_comments(file_contents); + parson_free(file_contents); + return output_value; +} + +JSON_Value * json_parse_string(const char *string) { + if (string == NULL) { + return NULL; + } + if (string[0] == '\xEF' && string[1] == '\xBB' && string[2] == '\xBF') { + string = string + 3; /* Support for UTF-8 BOM */ + } + return parse_value((const char**)&string, 0); +} + +JSON_Value * json_parse_string_with_comments(const char *string) { + JSON_Value *result = NULL; + char *string_mutable_copy = NULL, *string_mutable_copy_ptr = NULL; + string_mutable_copy = parson_strdup(string); + if (string_mutable_copy == NULL) { + return NULL; + } + remove_comments(string_mutable_copy, "/*", "*/"); + remove_comments(string_mutable_copy, "//", "\n"); + string_mutable_copy_ptr = string_mutable_copy; + result = parse_value((const char**)&string_mutable_copy_ptr, 0); + parson_free(string_mutable_copy); + return result; +} + +/* JSON Object API */ + +JSON_Value * json_object_get_value(const JSON_Object *object, const char *name) { + if (object == NULL || name == NULL) { + return NULL; + } + return json_object_getn_value(object, name, strlen(name)); +} + +const char * json_object_get_string(const JSON_Object *object, const char *name) { + return json_value_get_string(json_object_get_value(object, name)); +} + +size_t json_object_get_string_len(const JSON_Object *object, const char *name) { + return json_value_get_string_len(json_object_get_value(object, name)); +} + +double json_object_get_number(const JSON_Object *object, const char *name) { + return json_value_get_number(json_object_get_value(object, name)); +} + +JSON_Object * json_object_get_object(const JSON_Object *object, const char *name) { + return json_value_get_object(json_object_get_value(object, name)); +} + +JSON_Array * json_object_get_array(const JSON_Object *object, const char *name) { + return json_value_get_array(json_object_get_value(object, name)); +} + +int json_object_get_boolean(const JSON_Object *object, const char *name) { + return json_value_get_boolean(json_object_get_value(object, name)); +} + +JSON_Value * json_object_dotget_value(const JSON_Object *object, const char *name) { + const char *dot_position = strchr(name, '.'); + if (!dot_position) { + return json_object_get_value(object, name); + } + object = json_value_get_object(json_object_getn_value(object, name, dot_position - name)); + return json_object_dotget_value(object, dot_position + 1); +} + +const char * json_object_dotget_string(const JSON_Object *object, const char *name) { + return json_value_get_string(json_object_dotget_value(object, name)); +} + +size_t json_object_dotget_string_len(const JSON_Object *object, const char *name) { + return json_value_get_string_len(json_object_dotget_value(object, name)); +} + +double json_object_dotget_number(const JSON_Object *object, const char *name) { + return json_value_get_number(json_object_dotget_value(object, name)); +} + +JSON_Object * json_object_dotget_object(const JSON_Object *object, const char *name) { + return json_value_get_object(json_object_dotget_value(object, name)); +} + +JSON_Array * json_object_dotget_array(const JSON_Object *object, const char *name) { + return json_value_get_array(json_object_dotget_value(object, name)); +} + +int json_object_dotget_boolean(const JSON_Object *object, const char *name) { + return json_value_get_boolean(json_object_dotget_value(object, name)); +} + +size_t json_object_get_count(const JSON_Object *object) { + return object ? object->count : 0; +} + +const char * json_object_get_name(const JSON_Object *object, size_t index) { + if (object == NULL || index >= json_object_get_count(object)) { + return NULL; + } + return object->names[index]; +} + +JSON_Value * json_object_get_value_at(const JSON_Object *object, size_t index) { + if (object == NULL || index >= json_object_get_count(object)) { + return NULL; + } + return object->values[index]; +} + +JSON_Value *json_object_get_wrapping_value(const JSON_Object *object) { + if (!object) { + return NULL; + } + return object->wrapping_value; +} + +int json_object_has_value (const JSON_Object *object, const char *name) { + return json_object_get_value(object, name) != NULL; +} + +int json_object_has_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type) { + JSON_Value *val = json_object_get_value(object, name); + return val != NULL && json_value_get_type(val) == type; +} + +int json_object_dothas_value (const JSON_Object *object, const char *name) { + return json_object_dotget_value(object, name) != NULL; +} + +int json_object_dothas_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type) { + JSON_Value *val = json_object_dotget_value(object, name); + return val != NULL && json_value_get_type(val) == type; +} + +/* JSON Array API */ +JSON_Value * json_array_get_value(const JSON_Array *array, size_t index) { + if (array == NULL || index >= json_array_get_count(array)) { + return NULL; + } + return array->items[index]; +} + +const char * json_array_get_string(const JSON_Array *array, size_t index) { + return json_value_get_string(json_array_get_value(array, index)); +} + +size_t json_array_get_string_len(const JSON_Array *array, size_t index) { + return json_value_get_string_len(json_array_get_value(array, index)); +} + +double json_array_get_number(const JSON_Array *array, size_t index) { + return json_value_get_number(json_array_get_value(array, index)); +} + +JSON_Object * json_array_get_object(const JSON_Array *array, size_t index) { + return json_value_get_object(json_array_get_value(array, index)); +} + +JSON_Array * json_array_get_array(const JSON_Array *array, size_t index) { + return json_value_get_array(json_array_get_value(array, index)); +} + +int json_array_get_boolean(const JSON_Array *array, size_t index) { + return json_value_get_boolean(json_array_get_value(array, index)); +} + +size_t json_array_get_count(const JSON_Array *array) { + return array ? array->count : 0; +} + +JSON_Value * json_array_get_wrapping_value(const JSON_Array *array) { + if (!array) { + return NULL; + } + return array->wrapping_value; +} + +/* JSON Value API */ +JSON_Value_Type json_value_get_type(const JSON_Value *value) { + return value ? value->type : JSONError; +} + +JSON_Object * json_value_get_object(const JSON_Value *value) { + return json_value_get_type(value) == JSONObject ? value->value.object : NULL; +} + +JSON_Array * json_value_get_array(const JSON_Value *value) { + return json_value_get_type(value) == JSONArray ? value->value.array : NULL; +} + +static const JSON_String * json_value_get_string_desc(const JSON_Value *value) { + return json_value_get_type(value) == JSONString ? &value->value.string : NULL; +} + +const char * json_value_get_string(const JSON_Value *value) { + const JSON_String *str = json_value_get_string_desc(value); + return str ? str->chars : NULL; +} + +size_t json_value_get_string_len(const JSON_Value *value) { + const JSON_String *str = json_value_get_string_desc(value); + return str ? str->length : 0; +} + +double json_value_get_number(const JSON_Value *value) { + return json_value_get_type(value) == JSONNumber ? value->value.number : 0; +} + +int json_value_get_boolean(const JSON_Value *value) { + return json_value_get_type(value) == JSONBoolean ? value->value.boolean : -1; +} + +JSON_Value * json_value_get_parent (const JSON_Value *value) { + return value ? value->parent : NULL; +} + +void json_value_free(JSON_Value *value) { + switch (json_value_get_type(value)) { + case JSONObject: + json_object_free(value->value.object); + break; + case JSONString: + parson_free(value->value.string.chars); + break; + case JSONArray: + json_array_free(value->value.array); + break; + default: + break; + } + parson_free(value); +} + +JSON_Value * json_value_init_object(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { + return NULL; + } + new_value->parent = NULL; + new_value->type = JSONObject; + new_value->value.object = json_object_make(new_value); + if (!new_value->value.object) { + parson_free(new_value); + return NULL; + } + return new_value; +} + +JSON_Value * json_value_init_array(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { + return NULL; + } + new_value->parent = NULL; + new_value->type = JSONArray; + new_value->value.array = json_array_make(new_value); + if (!new_value->value.array) { + parson_free(new_value); + return NULL; + } + return new_value; +} + +JSON_Value * json_value_init_string(const char *string) { + if (string == NULL) { + return NULL; + } + return json_value_init_string_with_len(string, strlen(string)); +} + +JSON_Value * json_value_init_string_with_len(const char *string, size_t length) { + char *copy = NULL; + JSON_Value *value; + if (string == NULL) { + return NULL; + } + if (!is_valid_utf8(string, length)) { + return NULL; + } + copy = parson_strndup(string, length); + if (copy == NULL) { + return NULL; + } + value = json_value_init_string_no_copy(copy, length); + if (value == NULL) { + parson_free(copy); + } + return value; +} + +JSON_Value * json_value_init_number(double number) { + JSON_Value *new_value = NULL; + if (IS_NUMBER_INVALID(number)) { + return NULL; + } + new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (new_value == NULL) { + return NULL; + } + new_value->parent = NULL; + new_value->type = JSONNumber; + new_value->value.number = number; + return new_value; +} + +JSON_Value * json_value_init_boolean(int boolean) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { + return NULL; + } + new_value->parent = NULL; + new_value->type = JSONBoolean; + new_value->value.boolean = boolean ? 1 : 0; + return new_value; +} + +JSON_Value * json_value_init_null(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) { + return NULL; + } + new_value->parent = NULL; + new_value->type = JSONNull; + return new_value; +} + +JSON_Value * json_value_deep_copy(const JSON_Value *value) { + size_t i = 0; + JSON_Value *return_value = NULL, *temp_value_copy = NULL, *temp_value = NULL; + const JSON_String *temp_string = NULL; + const char *temp_key = NULL; + char *temp_string_copy = NULL; + JSON_Array *temp_array = NULL, *temp_array_copy = NULL; + JSON_Object *temp_object = NULL, *temp_object_copy = NULL; + JSON_Status res = JSONFailure; + char *key_copy = NULL; + + switch (json_value_get_type(value)) { + case JSONArray: + temp_array = json_value_get_array(value); + return_value = json_value_init_array(); + if (return_value == NULL) { + return NULL; + } + temp_array_copy = json_value_get_array(return_value); + for (i = 0; i < json_array_get_count(temp_array); i++) { + temp_value = json_array_get_value(temp_array, i); + temp_value_copy = json_value_deep_copy(temp_value); + if (temp_value_copy == NULL) { + json_value_free(return_value); + return NULL; + } + if (json_array_add(temp_array_copy, temp_value_copy) != JSONSuccess) { + json_value_free(return_value); + json_value_free(temp_value_copy); + return NULL; + } + } + return return_value; + case JSONObject: + temp_object = json_value_get_object(value); + return_value = json_value_init_object(); + if (!return_value) { + return NULL; + } + temp_object_copy = json_value_get_object(return_value); + for (i = 0; i < json_object_get_count(temp_object); i++) { + temp_key = json_object_get_name(temp_object, i); + temp_value = json_object_get_value(temp_object, temp_key); + temp_value_copy = json_value_deep_copy(temp_value); + if (!temp_value_copy) { + json_value_free(return_value); + return NULL; + } + key_copy = parson_strdup(temp_key); + if (!key_copy) { + json_value_free(temp_value_copy); + json_value_free(return_value); + return NULL; + } + res = json_object_add(temp_object_copy, key_copy, temp_value_copy); + if (res != JSONSuccess) { + parson_free(key_copy); + json_value_free(temp_value_copy); + json_value_free(return_value); + return NULL; + } + } + return return_value; + case JSONBoolean: + return json_value_init_boolean(json_value_get_boolean(value)); + case JSONNumber: + return json_value_init_number(json_value_get_number(value)); + case JSONString: + temp_string = json_value_get_string_desc(value); + if (temp_string == NULL) { + return NULL; + } + temp_string_copy = parson_strndup(temp_string->chars, temp_string->length); + if (temp_string_copy == NULL) { + return NULL; + } + return_value = json_value_init_string_no_copy(temp_string_copy, temp_string->length); + if (return_value == NULL) { + parson_free(temp_string_copy); + } + return return_value; + case JSONNull: + return json_value_init_null(); + case JSONError: + return NULL; + default: + return NULL; + } +} + +size_t json_serialization_size(const JSON_Value *value) { + char num_buf[NUM_BUF_SIZE]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */ + int res = json_serialize_to_buffer_r(value, NULL, 0, PARSON_FALSE, num_buf); + return res < 0 ? 0 : (size_t)(res) + 1; +} + +JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) { + int written = -1; + size_t needed_size_in_bytes = json_serialization_size(value); + if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) { + return JSONFailure; + } + written = json_serialize_to_buffer_r(value, buf, 0, PARSON_FALSE, NULL); + if (written < 0) { + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename) { + JSON_Status return_code = JSONSuccess; + FILE *fp = NULL; + char *serialized_string = json_serialize_to_string(value); + if (serialized_string == NULL) { + return JSONFailure; + } + fp = fopen(filename, "w"); + if (fp == NULL) { + json_free_serialized_string(serialized_string); + return JSONFailure; + } + if (fputs(serialized_string, fp) == EOF) { + return_code = JSONFailure; + } + if (fclose(fp) == EOF) { + return_code = JSONFailure; + } + json_free_serialized_string(serialized_string); + return return_code; +} + +char * json_serialize_to_string(const JSON_Value *value) { + JSON_Status serialization_result = JSONFailure; + size_t buf_size_bytes = json_serialization_size(value); + char *buf = NULL; + if (buf_size_bytes == 0) { + return NULL; + } + buf = (char*)parson_malloc(buf_size_bytes); + if (buf == NULL) { + return NULL; + } + serialization_result = json_serialize_to_buffer(value, buf, buf_size_bytes); + if (serialization_result != JSONSuccess) { + json_free_serialized_string(buf); + return NULL; + } + return buf; +} + +size_t json_serialization_size_pretty(const JSON_Value *value) { + char num_buf[NUM_BUF_SIZE]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */ + int res = json_serialize_to_buffer_r(value, NULL, 0, PARSON_TRUE, num_buf); + return res < 0 ? 0 : (size_t)(res) + 1; +} + +JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) { + int written = -1; + size_t needed_size_in_bytes = json_serialization_size_pretty(value); + if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) { + return JSONFailure; + } + written = json_serialize_to_buffer_r(value, buf, 0, PARSON_TRUE, NULL); + if (written < 0) { + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename) { + JSON_Status return_code = JSONSuccess; + FILE *fp = NULL; + char *serialized_string = json_serialize_to_string_pretty(value); + if (serialized_string == NULL) { + return JSONFailure; + } + fp = fopen(filename, "w"); + if (fp == NULL) { + json_free_serialized_string(serialized_string); + return JSONFailure; + } + if (fputs(serialized_string, fp) == EOF) { + return_code = JSONFailure; + } + if (fclose(fp) == EOF) { + return_code = JSONFailure; + } + json_free_serialized_string(serialized_string); + return return_code; +} + +char * json_serialize_to_string_pretty(const JSON_Value *value) { + JSON_Status serialization_result = JSONFailure; + size_t buf_size_bytes = json_serialization_size_pretty(value); + char *buf = NULL; + if (buf_size_bytes == 0) { + return NULL; + } + buf = (char*)parson_malloc(buf_size_bytes); + if (buf == NULL) { + return NULL; + } + serialization_result = json_serialize_to_buffer_pretty(value, buf, buf_size_bytes); + if (serialization_result != JSONSuccess) { + json_free_serialized_string(buf); + return NULL; + } + return buf; +} + +void json_free_serialized_string(char *string) { + parson_free(string); +} + +JSON_Status json_array_remove(JSON_Array *array, size_t ix) { + size_t to_move_bytes = 0; + if (array == NULL || ix >= json_array_get_count(array)) { + return JSONFailure; + } + json_value_free(json_array_get_value(array, ix)); + to_move_bytes = (json_array_get_count(array) - 1 - ix) * sizeof(JSON_Value*); + memmove(array->items + ix, array->items + ix + 1, to_move_bytes); + array->count -= 1; + return JSONSuccess; +} + +JSON_Status json_array_replace_value(JSON_Array *array, size_t ix, JSON_Value *value) { + if (array == NULL || value == NULL || value->parent != NULL || ix >= json_array_get_count(array)) { + return JSONFailure; + } + json_value_free(json_array_get_value(array, ix)); + value->parent = json_array_get_wrapping_value(array); + array->items[ix] = value; + return JSONSuccess; +} + +JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string) { + JSON_Value *value = json_value_init_string(string); + if (value == NULL) { + return JSONFailure; + } + if (json_array_replace_value(array, i, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_replace_string_with_len(JSON_Array *array, size_t i, const char *string, size_t len) { + JSON_Value *value = json_value_init_string_with_len(string, len); + if (value == NULL) { + return JSONFailure; + } + if (json_array_replace_value(array, i, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number) { + JSON_Value *value = json_value_init_number(number); + if (value == NULL) { + return JSONFailure; + } + if (json_array_replace_value(array, i, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean) { + JSON_Value *value = json_value_init_boolean(boolean); + if (value == NULL) { + return JSONFailure; + } + if (json_array_replace_value(array, i, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_replace_null(JSON_Array *array, size_t i) { + JSON_Value *value = json_value_init_null(); + if (value == NULL) { + return JSONFailure; + } + if (json_array_replace_value(array, i, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_clear(JSON_Array *array) { + size_t i = 0; + if (array == NULL) { + return JSONFailure; + } + for (i = 0; i < json_array_get_count(array); i++) { + json_value_free(json_array_get_value(array, i)); + } + array->count = 0; + return JSONSuccess; +} + +JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value) { + if (array == NULL || value == NULL || value->parent != NULL) { + return JSONFailure; + } + return json_array_add(array, value); +} + +JSON_Status json_array_append_string(JSON_Array *array, const char *string) { + JSON_Value *value = json_value_init_string(string); + if (value == NULL) { + return JSONFailure; + } + if (json_array_append_value(array, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_append_string_with_len(JSON_Array *array, const char *string, size_t len) { + JSON_Value *value = json_value_init_string_with_len(string, len); + if (value == NULL) { + return JSONFailure; + } + if (json_array_append_value(array, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_append_number(JSON_Array *array, double number) { + JSON_Value *value = json_value_init_number(number); + if (value == NULL) { + return JSONFailure; + } + if (json_array_append_value(array, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_append_boolean(JSON_Array *array, int boolean) { + JSON_Value *value = json_value_init_boolean(boolean); + if (value == NULL) { + return JSONFailure; + } + if (json_array_append_value(array, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_append_null(JSON_Array *array) { + JSON_Value *value = json_value_init_null(); + if (value == NULL) { + return JSONFailure; + } + if (json_array_append_value(array, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value) { + unsigned long hash = 0; + parson_bool_t found = PARSON_FALSE; + size_t cell_ix = 0; + size_t item_ix = 0; + JSON_Value *old_value = NULL; + char *key_copy = NULL; + + if (!object || !name || !value || value->parent) { + return JSONFailure; + } + hash = hash_string(name, strlen(name)); + found = PARSON_FALSE; + cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); + if (found) { + item_ix = object->cells[cell_ix]; + old_value = object->values[item_ix]; + json_value_free(old_value); + object->values[item_ix] = value; + value->parent = json_object_get_wrapping_value(object); + return JSONSuccess; + } + if (object->count >= object->item_capacity) { + JSON_Status res = json_object_grow_and_rehash(object); + if (res != JSONSuccess) { + return JSONFailure; + } + cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); + } + key_copy = parson_strdup(name); + if (!key_copy) { + return JSONFailure; + } + object->names[object->count] = key_copy; + object->cells[cell_ix] = object->count; + object->values[object->count] = value; + object->cell_ixs[object->count] = cell_ix; + object->hashes[object->count] = hash; + object->count++; + value->parent = json_object_get_wrapping_value(object); + return JSONSuccess; +} + +JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string) { + JSON_Value *value = json_value_init_string(string); + JSON_Status status = json_object_set_value(object, name, value); + if (status != JSONSuccess) { + json_value_free(value); + } + return status; +} + +JSON_Status json_object_set_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len) { + JSON_Value *value = json_value_init_string_with_len(string, len); + JSON_Status status = json_object_set_value(object, name, value); + if (status != JSONSuccess) { + json_value_free(value); + } + return status; +} + +JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number) { + JSON_Value *value = json_value_init_number(number); + JSON_Status status = json_object_set_value(object, name, value); + if (status != JSONSuccess) { + json_value_free(value); + } + return status; +} + +JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean) { + JSON_Value *value = json_value_init_boolean(boolean); + JSON_Status status = json_object_set_value(object, name, value); + if (status != JSONSuccess) { + json_value_free(value); + } + return status; +} + +JSON_Status json_object_set_null(JSON_Object *object, const char *name) { + JSON_Value *value = json_value_init_null(); + JSON_Status status = json_object_set_value(object, name, value); + if (status != JSONSuccess) { + json_value_free(value); + } + return status; +} + +JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value) { + const char *dot_pos = NULL; + JSON_Value *temp_value = NULL, *new_value = NULL; + JSON_Object *temp_object = NULL, *new_object = NULL; + JSON_Status status = JSONFailure; + size_t name_len = 0; + char *name_copy = NULL; + + if (object == NULL || name == NULL || value == NULL) { + return JSONFailure; + } + dot_pos = strchr(name, '.'); + if (dot_pos == NULL) { + return json_object_set_value(object, name, value); + } + name_len = dot_pos - name; + temp_value = json_object_getn_value(object, name, name_len); + if (temp_value) { + /* Don't overwrite existing non-object (unlike json_object_set_value, but it shouldn't be changed at this point) */ + if (json_value_get_type(temp_value) != JSONObject) { + return JSONFailure; + } + temp_object = json_value_get_object(temp_value); + return json_object_dotset_value(temp_object, dot_pos + 1, value); + } + new_value = json_value_init_object(); + if (new_value == NULL) { + return JSONFailure; + } + new_object = json_value_get_object(new_value); + status = json_object_dotset_value(new_object, dot_pos + 1, value); + if (status != JSONSuccess) { + json_value_free(new_value); + return JSONFailure; + } + name_copy = parson_strndup(name, name_len); + if (!name_copy) { + json_object_dotremove_internal(new_object, dot_pos + 1, 0); + json_value_free(new_value); + return JSONFailure; + } + status = json_object_add(object, name_copy, new_value); + if (status != JSONSuccess) { + parson_free(name_copy); + json_object_dotremove_internal(new_object, dot_pos + 1, 0); + json_value_free(new_value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string) { + JSON_Value *value = json_value_init_string(string); + if (value == NULL) { + return JSONFailure; + } + if (json_object_dotset_value(object, name, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len) { + JSON_Value *value = json_value_init_string_with_len(string, len); + if (value == NULL) { + return JSONFailure; + } + if (json_object_dotset_value(object, name, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number) { + JSON_Value *value = json_value_init_number(number); + if (value == NULL) { + return JSONFailure; + } + if (json_object_dotset_value(object, name, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean) { + JSON_Value *value = json_value_init_boolean(boolean); + if (value == NULL) { + return JSONFailure; + } + if (json_object_dotset_value(object, name, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_null(JSON_Object *object, const char *name) { + JSON_Value *value = json_value_init_null(); + if (value == NULL) { + return JSONFailure; + } + if (json_object_dotset_value(object, name, value) != JSONSuccess) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_remove(JSON_Object *object, const char *name) { + return json_object_remove_internal(object, name, PARSON_TRUE); +} + +JSON_Status json_object_dotremove(JSON_Object *object, const char *name) { + return json_object_dotremove_internal(object, name, PARSON_TRUE); +} + +JSON_Status json_object_clear(JSON_Object *object) { + size_t i = 0; + if (object == NULL) { + return JSONFailure; + } + for (i = 0; i < json_object_get_count(object); i++) { + parson_free(object->names[i]); + json_value_free(object->values[i]); + } + object->count = 0; + return JSONSuccess; +} + +JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value) { + JSON_Value *temp_schema_value = NULL, *temp_value = NULL; + JSON_Array *schema_array = NULL, *value_array = NULL; + JSON_Object *schema_object = NULL, *value_object = NULL; + JSON_Value_Type schema_type = JSONError, value_type = JSONError; + const char *key = NULL; + size_t i = 0, count = 0; + if (schema == NULL || value == NULL) { + return JSONFailure; + } + schema_type = json_value_get_type(schema); + value_type = json_value_get_type(value); + if (schema_type != value_type && schema_type != JSONNull) { /* null represents all values */ + return JSONFailure; + } + switch (schema_type) { + case JSONArray: + schema_array = json_value_get_array(schema); + value_array = json_value_get_array(value); + count = json_array_get_count(schema_array); + if (count == 0) { + return JSONSuccess; /* Empty array allows all types */ + } + /* Get first value from array, rest is ignored */ + temp_schema_value = json_array_get_value(schema_array, 0); + for (i = 0; i < json_array_get_count(value_array); i++) { + temp_value = json_array_get_value(value_array, i); + if (json_validate(temp_schema_value, temp_value) != JSONSuccess) { + return JSONFailure; + } + } + return JSONSuccess; + case JSONObject: + schema_object = json_value_get_object(schema); + value_object = json_value_get_object(value); + count = json_object_get_count(schema_object); + if (count == 0) { + return JSONSuccess; /* Empty object allows all objects */ + } else if (json_object_get_count(value_object) < count) { + return JSONFailure; /* Tested object mustn't have less name-value pairs than schema */ + } + for (i = 0; i < count; i++) { + key = json_object_get_name(schema_object, i); + temp_schema_value = json_object_get_value(schema_object, key); + temp_value = json_object_get_value(value_object, key); + if (temp_value == NULL) { + return JSONFailure; + } + if (json_validate(temp_schema_value, temp_value) != JSONSuccess) { + return JSONFailure; + } + } + return JSONSuccess; + case JSONString: case JSONNumber: case JSONBoolean: case JSONNull: + return JSONSuccess; /* equality already tested before switch */ + case JSONError: default: + return JSONFailure; + } +} + +int json_value_equals(const JSON_Value *a, const JSON_Value *b) { + JSON_Object *a_object = NULL, *b_object = NULL; + JSON_Array *a_array = NULL, *b_array = NULL; + const JSON_String *a_string = NULL, *b_string = NULL; + const char *key = NULL; + size_t a_count = 0, b_count = 0, i = 0; + JSON_Value_Type a_type, b_type; + a_type = json_value_get_type(a); + b_type = json_value_get_type(b); + if (a_type != b_type) { + return PARSON_FALSE; + } + switch (a_type) { + case JSONArray: + a_array = json_value_get_array(a); + b_array = json_value_get_array(b); + a_count = json_array_get_count(a_array); + b_count = json_array_get_count(b_array); + if (a_count != b_count) { + return PARSON_FALSE; + } + for (i = 0; i < a_count; i++) { + if (!json_value_equals(json_array_get_value(a_array, i), + json_array_get_value(b_array, i))) { + return PARSON_FALSE; + } + } + return PARSON_TRUE; + case JSONObject: + a_object = json_value_get_object(a); + b_object = json_value_get_object(b); + a_count = json_object_get_count(a_object); + b_count = json_object_get_count(b_object); + if (a_count != b_count) { + return PARSON_FALSE; + } + for (i = 0; i < a_count; i++) { + key = json_object_get_name(a_object, i); + if (!json_value_equals(json_object_get_value(a_object, key), + json_object_get_value(b_object, key))) { + return PARSON_FALSE; + } + } + return PARSON_TRUE; + case JSONString: + a_string = json_value_get_string_desc(a); + b_string = json_value_get_string_desc(b); + if (a_string == NULL || b_string == NULL) { + return PARSON_FALSE; /* shouldn't happen */ + } + return a_string->length == b_string->length && + memcmp(a_string->chars, b_string->chars, a_string->length) == 0; + case JSONBoolean: + return json_value_get_boolean(a) == json_value_get_boolean(b); + case JSONNumber: + return fabs(json_value_get_number(a) - json_value_get_number(b)) < 0.000001; /* EPSILON */ + case JSONError: + return PARSON_TRUE; + case JSONNull: + return PARSON_TRUE; + default: + return PARSON_TRUE; + } +} + +JSON_Value_Type json_type(const JSON_Value *value) { + return json_value_get_type(value); +} + +JSON_Object * json_object (const JSON_Value *value) { + return json_value_get_object(value); +} + +JSON_Array * json_array(const JSON_Value *value) { + return json_value_get_array(value); +} + +const char * json_string(const JSON_Value *value) { + return json_value_get_string(value); +} + +size_t json_string_len(const JSON_Value *value) { + return json_value_get_string_len(value); +} + +double json_number(const JSON_Value *value) { + return json_value_get_number(value); +} + +int json_boolean(const JSON_Value *value) { + return json_value_get_boolean(value); +} + +void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun) { + parson_malloc = malloc_fun; + parson_free = free_fun; +} + +void json_set_escape_slashes(int escape_slashes) { + parson_escape_slashes = escape_slashes; +} diff --git a/embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.h b/embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.h new file mode 100644 index 0000000000..beeca4cb49 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/3rdparty/parson/parson.h @@ -0,0 +1,256 @@ +/* + SPDX-License-Identifier: MIT + + Parson 1.2.1 ( http://kgabis.github.com/parson/ ) + Copyright (c) 2012 - 2021 Krzysztof Gabis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef parson_parson_h +#define parson_parson_h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define PARSON_VERSION_MAJOR 1 +#define PARSON_VERSION_MINOR 2 +#define PARSON_VERSION_PATCH 1 + +#define PARSON_VERSION_STRING "1.2.1" + +#include /* size_t */ + +/* Types and enums */ +typedef struct json_object_t JSON_Object; +typedef struct json_array_t JSON_Array; +typedef struct json_value_t JSON_Value; + +enum json_value_type { + JSONError = -1, + JSONNull = 1, + JSONString = 2, + JSONNumber = 3, + JSONObject = 4, + JSONArray = 5, + JSONBoolean = 6 +}; +typedef int JSON_Value_Type; + +enum json_result_t { + JSONSuccess = 0, + JSONFailure = -1 +}; +typedef int JSON_Status; + +typedef void * (*JSON_Malloc_Function)(size_t); +typedef void (*JSON_Free_Function)(void *); + +/* Call only once, before calling any other function from parson API. If not called, malloc and free + from stdlib will be used for all allocations */ +void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun); + +/* Sets if slashes should be escaped or not when serializing JSON. By default slashes are escaped. + This function sets a global setting and is not thread safe. */ +void json_set_escape_slashes(int escape_slashes); + +/* Parses first JSON value in a file, returns NULL in case of error */ +JSON_Value * json_parse_file(const char *filename); + +/* Parses first JSON value in a file and ignores comments (/ * * / and //), + returns NULL in case of error */ +JSON_Value * json_parse_file_with_comments(const char *filename); + +/* Parses first JSON value in a string, returns NULL in case of error */ +JSON_Value * json_parse_string(const char *string); + +/* Parses first JSON value in a string and ignores comments (/ * * / and //), + returns NULL in case of error */ +JSON_Value * json_parse_string_with_comments(const char *string); + +/* Serialization */ +size_t json_serialization_size(const JSON_Value *value); /* returns 0 on fail */ +JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes); +JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename); +char * json_serialize_to_string(const JSON_Value *value); + +/* Pretty serialization */ +size_t json_serialization_size_pretty(const JSON_Value *value); /* returns 0 on fail */ +JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes); +JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename); +char * json_serialize_to_string_pretty(const JSON_Value *value); + +void json_free_serialized_string(char *string); /* frees string from json_serialize_to_string and json_serialize_to_string_pretty */ + +/* Comparing */ +int json_value_equals(const JSON_Value *a, const JSON_Value *b); + +/* Validation + This is *NOT* JSON Schema. It validates json by checking if object have identically + named fields with matching types. + For example schema {"name":"", "age":0} will validate + {"name":"Joe", "age":25} and {"name":"Joe", "age":25, "gender":"m"}, + but not {"name":"Joe"} or {"name":"Joe", "age":"Cucumber"}. + In case of arrays, only first value in schema is checked against all values in tested array. + Empty objects ({}) validate all objects, empty arrays ([]) validate all arrays, + null validates values of every type. + */ +JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value); + +/* + * JSON Object + */ +JSON_Value * json_object_get_value (const JSON_Object *object, const char *name); +const char * json_object_get_string (const JSON_Object *object, const char *name); +size_t json_object_get_string_len(const JSON_Object *object, const char *name); /* doesn't account for last null character */ +JSON_Object * json_object_get_object (const JSON_Object *object, const char *name); +JSON_Array * json_object_get_array (const JSON_Object *object, const char *name); +double json_object_get_number (const JSON_Object *object, const char *name); /* returns 0 on fail */ +int json_object_get_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */ + +/* dotget functions enable addressing values with dot notation in nested objects, + just like in structs or c++/java/c# objects (e.g. objectA.objectB.value). + Because valid names in JSON can contain dots, some values may be inaccessible + this way. */ +JSON_Value * json_object_dotget_value (const JSON_Object *object, const char *name); +const char * json_object_dotget_string (const JSON_Object *object, const char *name); +size_t json_object_dotget_string_len(const JSON_Object *object, const char *name); /* doesn't account for last null character */ +JSON_Object * json_object_dotget_object (const JSON_Object *object, const char *name); +JSON_Array * json_object_dotget_array (const JSON_Object *object, const char *name); +double json_object_dotget_number (const JSON_Object *object, const char *name); /* returns 0 on fail */ +int json_object_dotget_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */ + +/* Functions to get available names */ +size_t json_object_get_count (const JSON_Object *object); +const char * json_object_get_name (const JSON_Object *object, size_t index); +JSON_Value * json_object_get_value_at(const JSON_Object *object, size_t index); +JSON_Value * json_object_get_wrapping_value(const JSON_Object *object); + +/* Functions to check if object has a value with a specific name. Returned value is 1 if object has + * a value and 0 if it doesn't. dothas functions behave exactly like dotget functions. */ +int json_object_has_value (const JSON_Object *object, const char *name); +int json_object_has_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type); + +int json_object_dothas_value (const JSON_Object *object, const char *name); +int json_object_dothas_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type); + +/* Creates new name-value pair or frees and replaces old value with a new one. + * json_object_set_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value); +JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string); +JSON_Status json_object_set_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len); /* length shouldn't include last null character */ +JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number); +JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean); +JSON_Status json_object_set_null(JSON_Object *object, const char *name); + +/* Works like dotget functions, but creates whole hierarchy if necessary. + * json_object_dotset_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value); +JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string); +JSON_Status json_object_dotset_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len); /* length shouldn't include last null character */ +JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number); +JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean); +JSON_Status json_object_dotset_null(JSON_Object *object, const char *name); + +/* Frees and removes name-value pair */ +JSON_Status json_object_remove(JSON_Object *object, const char *name); + +/* Works like dotget function, but removes name-value pair only on exact match. */ +JSON_Status json_object_dotremove(JSON_Object *object, const char *key); + +/* Removes all name-value pairs in object */ +JSON_Status json_object_clear(JSON_Object *object); + +/* + *JSON Array + */ +JSON_Value * json_array_get_value (const JSON_Array *array, size_t index); +const char * json_array_get_string (const JSON_Array *array, size_t index); +size_t json_array_get_string_len(const JSON_Array *array, size_t index); /* doesn't account for last null character */ +JSON_Object * json_array_get_object (const JSON_Array *array, size_t index); +JSON_Array * json_array_get_array (const JSON_Array *array, size_t index); +double json_array_get_number (const JSON_Array *array, size_t index); /* returns 0 on fail */ +int json_array_get_boolean(const JSON_Array *array, size_t index); /* returns -1 on fail */ +size_t json_array_get_count (const JSON_Array *array); +JSON_Value * json_array_get_wrapping_value(const JSON_Array *array); + +/* Frees and removes value at given index, does nothing and returns JSONFailure if index doesn't exist. + * Order of values in array may change during execution. */ +JSON_Status json_array_remove(JSON_Array *array, size_t i); + +/* Frees and removes from array value at given index and replaces it with given one. + * Does nothing and returns JSONFailure if index doesn't exist. + * json_array_replace_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_array_replace_value(JSON_Array *array, size_t i, JSON_Value *value); +JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string); +JSON_Status json_array_replace_string_with_len(JSON_Array *array, size_t i, const char *string, size_t len); /* length shouldn't include last null character */ +JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number); +JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean); +JSON_Status json_array_replace_null(JSON_Array *array, size_t i); + +/* Frees and removes all values from array */ +JSON_Status json_array_clear(JSON_Array *array); + +/* Appends new value at the end of array. + * json_array_append_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value); +JSON_Status json_array_append_string(JSON_Array *array, const char *string); +JSON_Status json_array_append_string_with_len(JSON_Array *array, const char *string, size_t len); /* length shouldn't include last null character */ +JSON_Status json_array_append_number(JSON_Array *array, double number); +JSON_Status json_array_append_boolean(JSON_Array *array, int boolean); +JSON_Status json_array_append_null(JSON_Array *array); + +/* + *JSON Value + */ +JSON_Value * json_value_init_object (void); +JSON_Value * json_value_init_array (void); +JSON_Value * json_value_init_string (const char *string); /* copies passed string */ +JSON_Value * json_value_init_string_with_len(const char *string, size_t length); /* copies passed string, length shouldn't include last null character */ +JSON_Value * json_value_init_number (double number); +JSON_Value * json_value_init_boolean(int boolean); +JSON_Value * json_value_init_null (void); +JSON_Value * json_value_deep_copy (const JSON_Value *value); +void json_value_free (JSON_Value *value); + +JSON_Value_Type json_value_get_type (const JSON_Value *value); +JSON_Object * json_value_get_object (const JSON_Value *value); +JSON_Array * json_value_get_array (const JSON_Value *value); +const char * json_value_get_string (const JSON_Value *value); +size_t json_value_get_string_len(const JSON_Value *value); /* doesn't account for last null character */ +double json_value_get_number (const JSON_Value *value); +int json_value_get_boolean(const JSON_Value *value); +JSON_Value * json_value_get_parent (const JSON_Value *value); + +/* Same as above, but shorter */ +JSON_Value_Type json_type (const JSON_Value *value); +JSON_Object * json_object (const JSON_Value *value); +JSON_Array * json_array (const JSON_Value *value); +const char * json_string (const JSON_Value *value); +size_t json_string_len(const JSON_Value *value); /* doesn't account for last null character */ +double json_number (const JSON_Value *value); +int json_boolean(const JSON_Value *value); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/embrace-android-sdk/src/main/cpp/CMakeLists.txt b/embrace-android-sdk/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..132491f4b2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/CMakeLists.txt @@ -0,0 +1,91 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.4.1) + +set (CMAKE_CXX_STANDARD 17) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +add_library(# Sets the name of the library. + embrace-native + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + emb_ndk_crash_samples.cpp + emb_anr_manager.c + anr.c + emb_log.c + emb_ndk_manager.c + jni_util.c + signals/signal_utils.c + signals/signals_c.c + signals/signals_cpp.cpp + sampler/sampler_unwinder_unwind.c + sampler/sampler_unwinder_stack.cpp + sampler/stacktrace_sampler.c + sampler/stacktrace_sampler_jni.c + sampler/unwinder_dlinfo.c + sampler/emb_timer.c + unwinders/unwinder.c + unwinders/unwinder_stack.cpp + safejni/safe_jni.c + utils/system_clock.c + utilities.c + file_marker.c + file_writer.c + base_64_encoder.c + 3rdparty/parson/parson.c + cpuinfo.c + ) + +include_directories( + 3rdparty/libunwind/include + 3rdparty/libunwindstack-ndk/include + 3rdparty/parson +) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +target_link_libraries( # Specifies the target library. + embrace-native + + # Links the target library to the log library + # included in the NDK. + ${log-lib}) + +set_target_properties(embrace-native + PROPERTIES + COMPILE_OPTIONS + -Werror -pedantic -Wall) + +add_subdirectory(3rdparty/libunwindstack-ndk/cmake) +target_link_libraries(embrace-native unwindstack) +if (${ANDROID_ABI} STREQUAL "armeabi" OR ${ANDROID_ABI} STREQUAL "armeabi-v7a") + add_library(libunwind STATIC IMPORTED) + set_target_properties(libunwind PROPERTIES IMPORTED_LOCATION + ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/${ANDROID_ABI}/libunwind.a) + target_link_libraries(embrace-native libunwind) +endif () \ No newline at end of file diff --git a/embrace-android-sdk/src/main/cpp/CrashSampleClass.cpp b/embrace-android-sdk/src/main/cpp/CrashSampleClass.cpp new file mode 100644 index 0000000000..6a24a403c4 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/CrashSampleClass.cpp @@ -0,0 +1,44 @@ +// +// Created by Raul Striglio on 01/08/2022. +// + +/* Wrapper Class to add extra stack frame to each error */ +#include "CrashSamplesImplClass.cpp" + +class CrashSampleClass { +public: + void sigill(); + + void sigfpe(); + + void sigsegv(); + + void sigabort(); + + void throwException(); +}; + +void CrashSampleClass::sigill(){ + CrashSampleImplClass crashSampleImplClass; + crashSampleImplClass.sigill(); +} + +void CrashSampleClass::sigfpe(){ + CrashSampleImplClass crashSampleImplClass; + crashSampleImplClass.sigfpe(); +} + +void CrashSampleClass::sigsegv(){ + CrashSampleImplClass crashSampleImplClass; + crashSampleImplClass.sigsegv(); +} + +void CrashSampleClass::sigabort(){ + CrashSampleImplClass crashSampleImplClass; + crashSampleImplClass.sigabort(); +} + +void CrashSampleClass::throwException(){ + CrashSampleImplClass crashSampleImplClass; + crashSampleImplClass.throwException(); +} diff --git a/embrace-android-sdk/src/main/cpp/CrashSamplesImplClass.cpp b/embrace-android-sdk/src/main/cpp/CrashSamplesImplClass.cpp new file mode 100644 index 0000000000..17680e813c --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/CrashSamplesImplClass.cpp @@ -0,0 +1,60 @@ +// +// Created by Raul Striglio on 01/08/2022. +// + +/* Wrapper Class to add extra stack frame to each error */ +#include +#include +#include + +using namespace std; + +class CrashSampleImplClass { +public: + void sigill(); + + void sigfpe(); + + void sigsegv(); + + void sigabort(); + + void throwException(); +}; + +void CrashSampleImplClass::sigill() { + asm(".byte 0x0f, 0x0b"); +} + +int do_div_by_0() { + int i; + std::string a = "0"; + int j = 0; + for (i = 20; i >= 0; i--) { + j = 10 / j; + } + + return j; +} + +void CrashSampleImplClass::sigfpe() { + printf("%d", do_div_by_0()); +} + +void CrashSampleImplClass::sigsegv() { + *((char *) 0xdeadbaad) = 39; +} + +void CrashSampleImplClass::sigabort() { + abort(); +} + +void myterminate() { + auto const ep = std::current_exception(); + throw ep; +} + +void CrashSampleImplClass::throwException() { + std::set_terminate(myterminate); + throw std::runtime_error("Embrace Ndk Crash"); +} diff --git a/embrace-android-sdk/src/main/cpp/anr.c b/embrace-android-sdk/src/main/cpp/anr.c new file mode 100644 index 0000000000..c6140e2a2e --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/anr.c @@ -0,0 +1,253 @@ +// +// Created by Fredric Newberg on 10/14/21. +// + +#include "anr.h" +#include +#include +#include +#include +#include +#include +#include +#include "safejni/safe_jni.h" +#include "emb_log.h" + +static bool enabled = false; +static bool installed = false; + +static jmethodID anr_mid = NULL; +static jobject anr_service_obj = NULL; +static JavaVM *emb_jvm = NULL; + +static volatile bool watchdog_thread_triggered = false; +static int64_t last_ts_ms = 0; + +static pid_t pid = -1; +static pid_t google_thread_id = GOOGLE_THREAD_ID_DEFAULT; + +static pthread_mutex_t emb_anr_install_lock = PTHREAD_MUTEX_INITIALIZER; + +static pthread_t watchdog_thread; +static sem_t watchdog_semaphore; +static bool have_semaphore = false; + +static const useconds_t polling_delay_ms = 100000; + +/* + * time helper + */ +static inline int64_t get_timestamp_millis() { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) { + return 0; + } + + return ((int64_t) ts.tv_sec * 1000) + ((int64_t) ts.tv_nsec / 1000000); +} + + +/* + * Block/unblock SIGQUIT helpers + */ + +static inline void manage_sigquit(int how) { + sigset_t quit_set; + sigemptyset(&quit_set); + sigaddset(&quit_set, SIGQUIT); + if (pthread_sigmask(how, &quit_set, NULL) != 0) { + // TODO: add log + } +} + +static inline void block_sigquit() { + manage_sigquit(SIG_BLOCK); +} + +static inline void unblock_sigquit() { + manage_sigquit(SIG_UNBLOCK); +} + + +static inline void watchdog_wait_for_trigger() { + // Use sem_wait() if possible, fall back to polling if not available. + watchdog_thread_triggered = false; + if (!have_semaphore || sem_wait(&watchdog_semaphore) != 0) { + EMB_LOGINFO("Waiting for watchdog to trigger."); + while (!watchdog_thread_triggered) { + usleep(polling_delay_ms); + } + EMB_LOGINFO("Watchdog has triggered."); + } +} + +static inline void kick_google() { + if (google_thread_id <= 0) { + EMB_LOGINFO("No Google ANR thread to kick..."); + return; + } + EMB_LOGINFO("Kicking Google ANR reporting."); + syscall(SYS_tgkill, pid, google_thread_id, SIGQUIT); +} + +static void process_anr() { + bool did_attach = false; + int attach_res; + JNIEnv *env; + int res = (*emb_jvm)->GetEnv(emb_jvm, (void **) &env, JNI_VERSION_1_4); + switch (res) { + case JNI_OK: + break; + case JNI_EDETACHED: + attach_res = (*emb_jvm)->AttachCurrentThread(emb_jvm, &env, NULL); + if (attach_res != 0) { + EMB_LOGERROR("Failed to call attach current thread: %d", attach_res); + return; + } + did_attach = true; + EMB_LOGINFO("Had to attach current thread to report ANR"); + break; + default: + EMB_LOGERROR("Failed to get JNI environment: %d", res); + return; + } + + if (anr_service_obj != NULL && anr_mid != NULL) { + // last_ts_ms was set in the signal handler to ensure the most accurate time possible + if (emb_jni_call_void_method(env, anr_service_obj, anr_mid, last_ts_ms)) { + EMB_LOGERROR("Failed to report ANR through JNI."); + } else { + EMB_LOGINFO("Reported ANR through JNI."); + } + } else { + EMB_LOGERROR("Failed to capture ANR - null JNI methods."); + } + + if (did_attach) { + (*emb_jvm)->DetachCurrentThread(emb_jvm); + } +} + + +_Noreturn static void *watchdog_thread_main(void *_) { + for (;;) { + watchdog_wait_for_trigger(); + + // Trigger Google ANR processing (occurs on a different thread). + kick_google(); + + if (enabled) { + process_anr(); + } + + // Unblock SIGQUIT again so that handle_sigquit() will run again. + unblock_sigquit(); + } +} + + +static inline void trigger_sigquit_watchdog_thread() { + // Set the trigger flag for the fallback spin-lock in + // sigquit_watchdog_thread_main() + watchdog_thread_triggered = true; + + if (have_semaphore) { + sem_post(&watchdog_semaphore); + } +} + +static void handle_sigquit(__unused int signum, __unused siginfo_t *info, __unused void *user_context) { + // Re-block SIGQUIT so that the Google handler can trigger. + // Do it in this handler so that the signal pending flags flip on the next + // context switch and will be off when the next sigquit_watchdog_thread_main() + // loop runs. + block_sigquit(); + + last_ts_ms = get_timestamp_millis(); + + // TODO: capture stacktrace? + + trigger_sigquit_watchdog_thread(); +} + + +static int64_t install_signal_handler() { + EMB_LOGDEV("Native - Installing Google ANR signal handler."); + int64_t result = 0; + if (google_thread_id == GOOGLE_THREAD_ID_DEFAULT) { + EMB_LOGWARN("Cannot configure Google ANR reporting since we do not have the watcher thread ID"); + } + + if (sem_init(&watchdog_semaphore, 0, 0) == 0) { + EMB_LOGDEV("We are on a modern platform and we can use a semaphore for alerting. Yay!"); + have_semaphore = true; + } else { + result |= EMB_ANR_INSTALL_NO_SEMAPHORE; + EMB_LOGDEV("We are on an old platform and we have to fall back on polling... bummer..."); + } + + // Start the watchdog thread + if (pthread_create(&watchdog_thread, NULL, watchdog_thread_main, NULL) != 0) { + result |= EMB_ANR_INSTALL_WATCHDOG_THREAD_CREATE_FAIL; + // TODO: do cleanup to enable Google ANR reporting? + EMB_LOGINFO("We failed to start the watchdog thread. We will not be able to capture Google ANRs"); + return result; + } + + struct sigaction handler; + sigemptyset(&handler.sa_mask); + handler.sa_sigaction = handle_sigquit; + handler.sa_flags = SA_SIGINFO; + if (sigaction(SIGQUIT, &handler, NULL) != 0) { + EMB_LOGERROR("failed to install sigquit handler: %s", strerror(errno)); + + result |= EMB_ANR_INSTALL_HANDLER_FAIL; + return result; + } + EMB_LOGDEV("installed sigquit handler"); + + unblock_sigquit(); + return result; +} + +static bool configure_reporting(JNIEnv *env) { + EMB_LOGDEV("Configuring Google ANR reporting"); + if (env == NULL) { + return false; + } + int result = (*env)->GetJavaVM(env, &emb_jvm); + if (result != 0) { + EMB_LOGERROR("Reporting config failed, could not get Java VM"); + return false; + } + + jclass anr_class = emb_jni_find_class(env, "io/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate"); + if (anr_class == NULL) { + EMB_LOGERROR("Reporting config failed, could not find GoogleAnrHandlerNativeDelegate class"); + return false; + } + EMB_LOGDEV("got ANR class id %p", anr_class); + anr_mid = emb_jni_get_method_id(env, anr_class, "saveGoogleAnr", "(J)V"); + return true; +} + +int emb_install_google_anr_handler(JNIEnv *env, jobject anr_service, jint _google_thread_id) { + pthread_mutex_lock(&emb_anr_install_lock); + int res = 0; + EMB_LOGDEV("anr_service %p", anr_service); + + if (!installed) { + pid = getpid(); + google_thread_id = _google_thread_id; + + enabled = true; + if (configure_reporting(env) && anr_service != NULL) { + anr_service_obj = (*env)->NewGlobalRef(env, anr_service); + res = install_signal_handler(); + installed = true; + } + } + pthread_mutex_unlock(&emb_anr_install_lock); + + return res; +} diff --git a/embrace-android-sdk/src/main/cpp/anr.h b/embrace-android-sdk/src/main/cpp/anr.h new file mode 100644 index 0000000000..50821406e8 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/anr.h @@ -0,0 +1,19 @@ +// +// Created by Fredric Newberg on 10/14/21. +// + +#ifndef EMBRACE_ANDROID_SDK3_ANR_H +#define EMBRACE_ANDROID_SDK3_ANR_H + +#include +#include + +#define GOOGLE_THREAD_ID_DEFAULT -1 + +#define EMB_ANR_INSTALL_NO_SEMAPHORE 1 << 0 +#define EMB_ANR_INSTALL_WATCHDOG_THREAD_CREATE_FAIL 1 << 1 +#define EMB_ANR_INSTALL_HANDLER_FAIL 1 << 2 + +int emb_install_google_anr_handler(JNIEnv *env, jobject anr_service, jint _google_thread_id); + +#endif //EMBRACE_ANDROID_SDK3_ANR_H diff --git a/embrace-android-sdk/src/main/cpp/base_64_encoder.c b/embrace-android-sdk/src/main/cpp/base_64_encoder.c new file mode 100644 index 0000000000..60cab4bcc3 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/base_64_encoder.c @@ -0,0 +1,60 @@ +// +// Created by Eric Lanz on 5/18/20. +// + +#include "base_64_encoder.h" +#include + +// Taken from: https://nachtimwald.com/2017/11/18/base64-encode-and-decode-in-c/ by Eric Lanz May 18th, 2020 + +const char b64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +size_t b64_encoded_size(size_t inlen) +{ + size_t ret; + + ret = inlen; + if (inlen % 3 != 0) + ret += 3 - (inlen % 3); + ret /= 3; + ret *= 4; + + return ret; +} + +char *b64_encode(const char *in, size_t len) +{ + char *out; + size_t elen; + size_t i; + size_t j; + size_t v; + + if (in == NULL || len == 0) + return NULL; + + elen = b64_encoded_size(len); + out = malloc(elen+1); + out[elen] = '\0'; + + for (i=0, j=0; i> 18) & 0x3F]; + out[j+1] = b64chars[(v >> 12) & 0x3F]; + if (i+1 < len) { + out[j+2] = b64chars[(v >> 6) & 0x3F]; + } else { + out[j+2] = '='; + } + if (i+2 < len) { + out[j+3] = b64chars[v & 0x3F]; + } else { + out[j+3] = '='; + } + } + + return out; +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/cpp/base_64_encoder.h b/embrace-android-sdk/src/main/cpp/base_64_encoder.h new file mode 100644 index 0000000000..2cea17938f --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/base_64_encoder.h @@ -0,0 +1,12 @@ +// +// Created by Eric Lanz on 5/18/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_BASE_64_ENCODER_H +#define EMBRACE_NATIVE_CRASHES_BASE_64_ENCODER_H + +#include + +char *b64_encode(const char *in, size_t len); + +#endif //EMBRACE_NATIVE_CRASHES_BASE_64_ENCODER_H diff --git a/embrace-android-sdk/src/main/cpp/cpuinfo.c b/embrace-android-sdk/src/main/cpp/cpuinfo.c new file mode 100644 index 0000000000..4a246181b6 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/cpuinfo.c @@ -0,0 +1,36 @@ +// +// Created by ignacio saslavsky on 14/12/2022. +// + +#include +#include +#include "emb_log.h" +#include "safejni/safe_jni.h" + +// defined PROP_VALUE_MAX 92 in system_properties.h +// https://android.googlesource.com/platform/bionic/+/466dbe4/libc/include/sys/system_properties.h + +#define CPUINFO_BUILD_PROP_VALUE_MAX 92 +#define CPUINFO_CPU_NAME_KEY "ro.board.platform" +#define CPUINFO_EGL_KEY "ro.hardware.egl" + + +void emb_cpuinfo_android_property_get(const char* key, char* value) { + __system_property_get(key, value); +} + +jstring emb_get_property(JNIEnv *env, const char* key) { + char property_value[CPUINFO_BUILD_PROP_VALUE_MAX]; + emb_cpuinfo_android_property_get(key, property_value); + return emb_jni_new_string_utf(env, property_value); +} + +JNIEXPORT jstring JNICALL +Java_io_embrace_android_embracesdk_capture_cpu_EmbraceCpuInfoDelegate_getNativeCpuName(JNIEnv* env, jobject thiz) { + return emb_get_property(env, CPUINFO_CPU_NAME_KEY); +} + +JNIEXPORT jstring JNICALL +Java_io_embrace_android_embracesdk_capture_cpu_EmbraceCpuInfoDelegate_getNativeEgl(JNIEnv* env, jobject thiz) { + return emb_get_property(env, CPUINFO_EGL_KEY); +} diff --git a/embrace-android-sdk/src/main/cpp/emb_anr_manager.c b/embrace-android-sdk/src/main/cpp/emb_anr_manager.c new file mode 100644 index 0000000000..4a0fd541f0 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/emb_anr_manager.c @@ -0,0 +1,24 @@ +// +// Created by Fredric Newberg on 10/14/21. +// + +#include "anr.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jint JNICALL +Java_io_embrace_android_embracesdk_anr_sigquit_GoogleAnrHandlerNativeDelegate_installGoogleAnrHandler( + JNIEnv *env, jobject thiz, + jint google_thread_id) { + return emb_install_google_anr_handler(env, thiz, google_thread_id); +} + +#ifdef __cplusplus +} +#endif + + diff --git a/embrace-android-sdk/src/main/cpp/emb_log.c b/embrace-android-sdk/src/main/cpp/emb_log.c new file mode 100644 index 0000000000..0cb1d6773c --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/emb_log.c @@ -0,0 +1,11 @@ +#include + +static volatile bool g_emb_dev_logging = false; + +void emb_enable_dev_logging() { + g_emb_dev_logging = true; +} + +bool emb_dev_logging_enabled() { + return g_emb_dev_logging; +} diff --git a/embrace-android-sdk/src/main/cpp/emb_log.h b/embrace-android-sdk/src/main/cpp/emb_log.h new file mode 100644 index 0000000000..8bcec40637 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/emb_log.h @@ -0,0 +1,36 @@ +#ifndef EMBRACE_LOG_H +#define EMBRACE_LOG_H + +#include + +#define EMB_LOG_TAG "emb_ndk" +#define EMB_DEV_LOG_TAG "emb_ndk_dev" + +void emb_enable_dev_logging(); +bool emb_dev_logging_enabled(); + +#ifndef EMB_LOGERROR +#define EMB_LOGERROR(fmt, ...) \ + __android_log_print(ANDROID_LOG_ERROR, EMB_LOG_TAG, fmt, ##__VA_ARGS__) +#endif + +#ifndef EMB_LOGWARN +#define EMB_LOGWARN(fmt, ...) \ + __android_log_print(ANDROID_LOG_WARN, EMB_LOG_TAG, fmt, ##__VA_ARGS__) +#endif + +#ifndef EMB_LOGINFO +#define EMB_LOGINFO(fmt, ...) \ + __android_log_print(ANDROID_LOG_INFO, EMB_LOG_TAG, fmt, ##__VA_ARGS__) +#endif + +#ifndef EMB_LOGDEV +#define EMB_LOGDEV(fmt, ...) \ + do { \ + if (emb_dev_logging_enabled()) { \ + __android_log_print(ANDROID_LOG_ERROR, EMB_DEV_LOG_TAG, fmt, ##__VA_ARGS__); \ + } \ + } while (0) +#endif + +#endif //EMBRACE_LOG_H diff --git a/embrace-android-sdk/src/main/cpp/emb_ndk_crash_samples.cpp b/embrace-android-sdk/src/main/cpp/emb_ndk_crash_samples.cpp new file mode 100644 index 0000000000..48ed3c4d1b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/emb_ndk_crash_samples.cpp @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include "CrashSampleClass.cpp" + +using namespace std; + +/* Wrapper Class to add extra stack frame to each error */ +class EmbCrashSampleClass { +public: + static void sigill(); + + static void sigfpe(); + + static void sigsegv(); + + static void sigabort(); + + static void throwException(); +}; + +void EmbCrashSampleClass::throwException() { + CrashSampleClass crashSampleImplClass; + crashSampleImplClass.throwException(); +} + +void EmbCrashSampleClass::sigabort() { + CrashSampleClass crashSampleImplClass; + crashSampleImplClass.sigabort(); +} + +void EmbCrashSampleClass::sigsegv() { + CrashSampleClass crashSampleImplClass; + crashSampleImplClass.sigsegv(); +} + +void EmbCrashSampleClass::sigill() { + CrashSampleClass crashSampleImplClass; + crashSampleImplClass.sigill(); +} + +void EmbCrashSampleClass::sigfpe() { + CrashSampleClass crashSampleImplClass; + crashSampleImplClass.sigfpe(); +} + +extern "C" +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_samples_EmbraceCrashSamplesNdkDelegateImpl_sigfpe(JNIEnv *env, + jobject thiz) { + EmbCrashSampleClass embCrashSampleClass; + embCrashSampleClass.sigfpe(); +} + +extern "C" +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_samples_EmbraceCrashSamplesNdkDelegateImpl_sigsegv(JNIEnv *env, + jobject thiz) { + EmbCrashSampleClass embCrashSampleClass; + embCrashSampleClass.sigsegv(); +} + +extern "C" +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_samples_EmbraceCrashSamplesNdkDelegateImpl_sigAbort(JNIEnv *env, + jobject thiz) { + EmbCrashSampleClass embCrashSampleClass; + embCrashSampleClass.sigabort(); +} + +extern "C" +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_samples_EmbraceCrashSamplesNdkDelegateImpl_sigIllegalInstruction( + JNIEnv *env, jobject thiz) { + EmbCrashSampleClass embCrashSampleClass; + embCrashSampleClass.sigill(); +} +extern "C" +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_samples_EmbraceCrashSamplesNdkDelegateImpl_throwException( + JNIEnv *env, + jobject thiz) { + EmbCrashSampleClass embCrashSampleClass; + embCrashSampleClass.throwException(); +} diff --git a/embrace-android-sdk/src/main/cpp/emb_ndk_manager.c b/embrace-android-sdk/src/main/cpp/emb_ndk_manager.c new file mode 100644 index 0000000000..693a0cdf54 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/emb_ndk_manager.c @@ -0,0 +1,340 @@ +#include +#include +#include +#include +#include +#include "emb_ndk_manager.h" +#include "file_writer.h" +#include "signals/signals_c.h" +#include "signals/signals_cpp.h" +#include "unwinders/unwinder.h" +#include "utilities.h" +#include "inttypes.h" +#include "jni_util.h" +#include "sampler/stacktrace_sampler.h" +#include "emb_log.h" +#include "safejni/safe_jni.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define WARNING_LOG_BUFFER_SIZE 1024 + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection" +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnusedParameter" + +static JNIEnv *__emb_jni_env = NULL; +static emb_env __impl_emb_env = {0}; +static emb_env *__emb_env = &__impl_emb_env; + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1installSignalHandlers(JNIEnv *env, + jobject thiz, + jstring _base_path, + jstring _crash_marker_path, + jstring _device_meta_data, + jstring _session_id, + jstring _app_state, + jstring _report_id, + jint api_level, + jboolean is_32bit, + jboolean dev_logging) { + if (dev_logging) { + emb_enable_dev_logging(); + } + EMB_LOGINFO("Installing Signal Handlers"); + if (__emb_jni_env) { + EMB_LOGINFO("handler already installed."); + return; + } + __emb_jni_env = env; + + EMB_LOGDEV("unwinder args: apiLevel=%d, 32bit=%d", api_level, is_32bit); + + EMB_LOGDEV("Setting up initial state."); + const char *device_meta_data = (*env)->GetStringUTFChars(env, _device_meta_data, 0); + snprintf(__emb_env->crash.meta_data, EMB_DEVICE_META_DATA_SIZE, "%s", device_meta_data); + const char *session_id = (*env)->GetStringUTFChars(env, _session_id, 0); + snprintf(__emb_env->crash.session_id, EMB_SESSION_ID_SIZE, "%s", session_id); + const char *report_id = (*env)->GetStringUTFChars(env, _report_id, 0); + snprintf(__emb_env->crash.report_id, EMB_REPORT_ID_SIZE, "%s", report_id); + const char *app_state = (*env)->GetStringUTFChars(env, _app_state, 0); + snprintf(__emb_env->crash.app_state, EMB_APP_DATA_SIZE, "%s", app_state); + + EMB_LOGDEV("Setting up base path."); + const char *base_path = (*env)->GetStringUTFChars(env, _base_path, 0); + snprintf(__emb_env->base_path, EMB_PATH_SIZE, "%s", base_path); + EMB_LOGINFO("base path: %s", base_path); + + EMB_LOGDEV("Setting up crash marker path."); + const char *crash_marker_path = (*env)->GetStringUTFChars(env, _crash_marker_path, 0); + snprintf(__emb_env->crash_marker_path, EMB_PATH_SIZE, "%s", crash_marker_path); + EMB_LOGINFO("crash marker path: %s", crash_marker_path); + + EMB_LOGDEV("Recording start timestamp."); + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + // get timestamp in millis + __emb_env->crash.start_ts = ((int64_t) ts.tv_sec * 1000) + ((int64_t) ts.tv_nsec / 1000000); + + // must set start_ts before calling this + emb_set_report_paths(__emb_env, session_id); + + // install signal handlers + if (!emb_setup_c_signal_handlers(__emb_env)) { + EMB_LOGWARN("failed to install c handlers."); + } else { + EMB_LOGINFO("c handlers installed."); + } + if (!emb_setup_cpp_sig_handler(__emb_env)) { + EMB_LOGWARN("failed to install cpp handlers."); + } else { + EMB_LOGINFO("cpp handlers installed."); + } + EMB_LOGDEV("Completed signal handler install."); +} + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1updateMetaData(JNIEnv *env, + jobject thiz, + jstring _device_meta_data) { + if (!__emb_env) { + EMB_LOGWARN("can't update device meta data until install is called."); + return; + } + const char *device_meta_data = (*env)->GetStringUTFChars(env, _device_meta_data, 0); + + if (strlen(device_meta_data) >= EMB_DEVICE_META_DATA_SIZE) { + EMB_LOGWARN("Failed to update metadata: too large"); + return; + } + + snprintf(__emb_env->crash.meta_data, EMB_DEVICE_META_DATA_SIZE, "%s", device_meta_data); +} + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1updateSessionId(JNIEnv *env, + jobject thiz, + jstring _session_id) { + if (!__emb_env) { + EMB_LOGWARN("can't update session ID until install is called."); + return; + } + const char *session_id = (*env)->GetStringUTFChars(env, _session_id, 0); + snprintf(__emb_env->crash.session_id, EMB_SESSION_ID_SIZE, "%s", session_id); + emb_set_report_paths(__emb_env, session_id); +} + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1updateAppState(JNIEnv *env, + jobject thiz, + jstring _app_state) { + if (!__emb_env) { + EMB_LOGWARN("can't update app state until install is called."); + return; + } + const char *app_state = (*env)->GetStringUTFChars(env, _app_state, 0); + snprintf(__emb_env->crash.app_state, EMB_APP_DATA_SIZE, "%s", app_state); +} + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1uninstallSignals(JNIEnv *env, + jobject thiz) { + if (!__emb_env) { + EMB_LOGINFO("can't uninstall, not installed."); + return; + } + EMB_LOGINFO("Uninstalling C++ signal handler."); + emb_remove_cpp_sig_handler(); + + EMB_LOGINFO("Uninstalling C signal handlers."); + emb_remove_c_sig_handlers(); + __emb_env = NULL; +} + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1testNativeCrash_1C(JNIEnv *env, + jobject thiz) { + abort(); +} + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1testNativeCrash_1CPP(JNIEnv *env, + jobject thiz) { + emb_fake_crash(); +} + +JNIEXPORT jstring JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1getCrashReport( + JNIEnv *env, jobject _this, jstring _report_path) { + EMB_LOGDEV("Called getCrashReport()."); + static pthread_mutex_t crash_reader_mutex = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&crash_reader_mutex); + const char *crash_path = NULL; + emb_crash *crash = NULL; + char *payload = NULL; + jstring payload_str = NULL; + + crash_path = (*env)->GetStringUTFChars(env, _report_path, NULL); + if (crash_path == NULL) { + EMB_LOGERROR("Failed to allocate crash path."); + goto cleanup; + } else { + EMB_LOGDEV("Loading crash from %s", crash_path); + } + + crash = emb_read_crash_from_file(crash_path); + if (crash != NULL) { + EMB_LOGDEV("Successfully read emb_crash struct into memory."); + + payload = emb_crash_to_json(crash); + if (payload == NULL) { + EMB_LOGERROR("failed to convert crash report to JSON at %s", crash_path); + } else { + EMB_LOGDEV("Serialized emb_crash into JSON payload."); + } + } else { + EMB_LOGERROR("failed to read crash report at %s", crash_path); + } + + payload_str = (*env)->NewStringUTF(env, payload); + + if (payload_str != NULL) { + EMB_LOGDEV("Creating UTF string for payload."); + } else { + EMB_LOGDEV("Failed to create UTF string for payload."); + } + + cleanup: + pthread_mutex_unlock(&crash_reader_mutex); + if (crash != NULL) { + free(crash); + } + if (payload != NULL) { + free(payload); + } + emb_jni_release_string_utf_chars(env, _report_path, crash_path); + + return payload_str; +} + + +JNIEXPORT jstring JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1getErrors( + JNIEnv *env, jobject _this, jstring _report_path) { + EMB_LOGDEV("Called getErrors()."); + static pthread_mutex_t error_reader_mutex = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&error_reader_mutex); + const char *error_path = NULL; + emb_error *errors = NULL; + char *payload = NULL; + jstring payload_str = NULL; + + error_path = (*env)->GetStringUTFChars(env, _report_path, NULL); + if (error_path == NULL) { + EMB_LOGERROR("Failed to allocate error path."); + goto cleanup; + } else { + EMB_LOGDEV("Loading error from %s", error_path); + } + errors = emb_read_errors_from_file(error_path); + if (errors != NULL) { + EMB_LOGDEV("Successfully read emb_error struct into memory."); + + payload = emb_errors_to_json(errors); + if (payload == NULL) { + EMB_LOGERROR("failed to convert errors to JSON at %s", error_path); + } else { + EMB_LOGDEV("Serialized emb_error into JSON payload."); + } + } else { + EMB_LOGERROR("failed to read errors at %s", error_path); + } + + payload_str = (*env)->NewStringUTF(env, payload); + + if (payload_str != NULL) { + EMB_LOGDEV("Creating UTF string for payload."); + } else { + EMB_LOGDEV("Failed to create UTF string for payload."); + } + + cleanup: + pthread_mutex_unlock(&error_reader_mutex); + if (errors != NULL) { + free(errors); + } + if (payload != NULL) { + free(payload); + } + emb_jni_release_string_utf_chars(env, _report_path, error_path); + + return payload_str; +} + +JNIEXPORT jboolean JNICALL +Java_io_embrace_android_embracesdk_anr_ndk_NativeThreadSamplerNdkDelegate_setupNativeThreadSampler( + JNIEnv *env, + jobject thiz, + jboolean is32bit) { + return emb_setup_native_thread_sampler(__emb_env, is32bit); +} + +JNIEXPORT jboolean JNICALL +Java_io_embrace_android_embracesdk_anr_ndk_NativeThreadSamplerNdkDelegate_monitorCurrentThread( + JNIEnv *env, + jobject thiz) { + return emb_monitor_current_thread(); +} + +JNIEXPORT void JNICALL +Java_io_embrace_android_embracesdk_anr_ndk_NativeThreadSamplerNdkDelegate_startSampling( + JNIEnv *env, + jobject thiz, + jint unwinder, + jlong interval_ms) { + emb_set_unwinder(unwinder); + emb_start_thread_sampler((long) interval_ms); +} + +JNIEXPORT jstring JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1checkForOverwrittenHandlers(JNIEnv *env, + jobject thiz) { + char buffer[WARNING_LOG_BUFFER_SIZE]; + EMB_LOGINFO("Checking for Overwritten handlers"); + if (emb_check_for_overwritten_handlers(buffer, WARNING_LOG_BUFFER_SIZE)) { + return emb_jni_new_string_utf(env, buffer); + } else { + return NULL; + } +} + +JNIEXPORT jboolean JNICALL +Java_io_embrace_android_embracesdk_ndk_NdkDelegateImpl__1reinstallSignalHandlers(JNIEnv *env, + jobject thiz) { + EMB_LOGINFO("About to reinstall 3rd party handlers"); + + // install signal handlers + if (!emb_setup_c_signal_handlers(__emb_env)) { + EMB_LOGWARN("failed to reinstall c handlers."); + } else { + EMB_LOGINFO("c handlers reinstalled."); + } + if (!emb_setup_cpp_sig_handler(__emb_env)) { + EMB_LOGWARN("failed to reinstall cpp handlers."); + } else { + EMB_LOGINFO("cpp handlers reinstalled."); + } + EMB_LOGDEV("Completed signal handler reinstall."); + return false; +} + +#pragma clang diagnostic pop +#pragma clang diagnostic pop + +#ifdef __cplusplus +} +#endif diff --git a/embrace-android-sdk/src/main/cpp/emb_ndk_manager.h b/embrace-android-sdk/src/main/cpp/emb_ndk_manager.h new file mode 100644 index 0000000000..d028415e17 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/emb_ndk_manager.h @@ -0,0 +1,44 @@ +// +// Created by Eric Lanz on 5/5/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_EMB_NDK_MANAGER_H +#define EMBRACE_NATIVE_CRASHES_EMB_NDK_MANAGER_H + +#include "stack_frames.h" +#include + +#ifdef CLANG_ANALYZE_ASYNCSAFE +#define __asyncsafe __attribute__((asyncsafe)); +#else +#define __asyncsafe +#endif + +#define CRASH_REPORT_VERSION1 "v1" +#define CRASH_REPORT_CURRENT_VERSION CRASH_REPORT_VERSION1 + +typedef struct { + int num; + int context; +} emb_error; + + +// "/proc//maps" + \0 is 22 bytes long +#define MAP_SRC_PATH_SIZE 22 + +typedef struct { + char base_path[EMB_PATH_SIZE]; + char crash_marker_path[EMB_PATH_SIZE]; + char report_path[EMB_PATH_SIZE]; + char map_path[EMB_PATH_SIZE]; + char error_path[EMB_PATH_SIZE]; + char map_src_path[MAP_SRC_PATH_SIZE]; + int err_fd; + bool currently_handling; + bool already_handled_crash; + emb_crash crash; + emb_error last_error; + int errors_captured; +} emb_env; + +#endif //EMBRACE_NATIVE_CRASHES_EMB_NDK_MANAGER_H diff --git a/embrace-android-sdk/src/main/cpp/file_marker.c b/embrace-android-sdk/src/main/cpp/file_marker.c new file mode 100644 index 0000000000..e02b5d6ee2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/file_marker.c @@ -0,0 +1,22 @@ +// +// Created by Fredric Newberg on 6/2/23. +// + +#include +#include +#include "file_marker.h" + +/* + * Write a file to the crash marker location to indicate that a crash has occurred. This is the + * same path that we write to from the JVM to indicate that a JVM crash has occurred. + * + * This function is safe to call from a signal handler. + */ +void emb_write_crash_marker_file(emb_env *env, const char *source) { + // Open the file for writing, truncating it if it already exists. + int fd = open(env->crash_marker_path, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd > 0) { + write(fd, source, 1); + close(fd); + } +} diff --git a/embrace-android-sdk/src/main/cpp/file_marker.h b/embrace-android-sdk/src/main/cpp/file_marker.h new file mode 100644 index 0000000000..869bfcb9a2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/file_marker.h @@ -0,0 +1,24 @@ +// +// Created by Fredric Newberg on 6/2/23. +// + +#ifndef EMBRACE_NATIVE_CRASHES_FILE_MARKER_H +#define EMBRACE_NATIVE_CRASHES_FILE_MARKER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "emb_ndk_manager.h" + +// reserve "1" for JVM crashes +#define CRASH_MARKER_SOURCE_SIGNAL "2" +#define CRASH_MARKER_SOURCE_CPP_EXCEPTION "3" + +void emb_write_crash_marker_file(emb_env *env, const char *source); + +#ifdef __cplusplus +} +#endif + +#endif //EMBRACE_NATIVE_CRASHES_FILE_MARKER_H diff --git a/embrace-android-sdk/src/main/cpp/file_writer.c b/embrace-android-sdk/src/main/cpp/file_writer.c new file mode 100644 index 0000000000..a447217b1c --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/file_writer.c @@ -0,0 +1,264 @@ +// +// Created by Eric Lanz on 5/17/20. +// + +#include "file_writer.h" +#include "inttypes.h" +#include "base_64_encoder.h" +#include "3rdparty/parson/parson.h" +#include "utilities.h" +#include "emb_log.h" +#include +#include +#include +#include +#include +#include + +// crash keys +static const char *kDeviceMetaKey = "meta"; +static const char *kReportIDKey = "report_id"; +static const char *kSessionIDKey = "sid"; +static const char *kCrashTSKey = "ts"; +static const char *kAppStateKey = "state"; +static const char *kUnwinderErrorCode = "ue"; +static const char *kExceptionNameKey = "en"; +static const char *kExceptionMsgKey = "em"; +static const char *kExceptionCodeKey = "ec"; +static const char *kExceptionErrnoKey = "ee"; +static const char *kExceptionSignoKey = "es"; +static const char *kExceptionFaultAddr = "fa"; +static const char *kFramesKey = "fr"; +static const char *kFilenameKey = "mo"; +static const char *kMethodKey = "md"; +static const char *kFrameAddrKey = "fa"; +static const char *kOffsetAddrKey = "oa"; +static const char *kModuleAddrKey = "ma"; +static const char *kLineNumKey = "ln"; +static const char *kCrashKey = "crash"; +static const char *kVersionKey = "v"; + +// error keys +static const char *kErrNum = "n"; +static const char *kErrContext = "c"; + +// when values are "" in our tracking struct this string will be used instead so the server knows it was intentional +// currently sticking with "" +static const char *kDefaultNULLFallbackString = ""; +static const char *kCurrentPayloadVersion = "1"; + + +bool emb_write_crash_to_file(emb_env *env) { + int fd = open(env->report_path, O_WRONLY | O_CREAT, 0644); + if (fd == -1) { + emb_log_last_error(env, EMB_ERROR_FAILED_TO_OPEN_CRASH_FILE, 0); + return false; + } + + ssize_t len = write(fd, &env->crash, sizeof(emb_crash)); + close(fd); + return len == sizeof(emb_crash); +} + +emb_crash *emb_read_crash_from_file(const char *path) { + int fd = open(path, O_RDONLY); + if (fd == -1) { + EMB_LOGERROR("failed to open native crash file at %s", path); + return NULL; + } + + size_t crash_size = sizeof(emb_crash); + emb_crash *crash = calloc(1, crash_size); + + ssize_t len = read(fd, crash, crash_size); + + if (len == -1) { // log the error code for more debug info. + EMB_LOGERROR("Encountered error reading emb_crash struct. %d: %s", errno, strerror(errno)); + } + + close(fd); + if (len != crash_size) { + EMB_LOGERROR("Exiting native crash file read because we read %d instead of %d", + (int) len, (int) crash_size); + free(crash); + return NULL; + } + return crash; +} + +emb_error *emb_read_errors_from_file(const char *path) { + int fd = open(path, O_RDONLY); + if (fd == -1) { + EMB_LOGERROR("failed to open native crash error file at %s", path); + return NULL; + } + + size_t error_size = sizeof(emb_error); + emb_error *errors = calloc(EMB_MAX_ERRORS, error_size); + emb_error *wp = errors; + int count = 0; + + while (count < EMB_MAX_ERRORS) { + ssize_t len = read(fd, wp++, error_size); + + if (len == -1) { // log the error code for more debug info. + EMB_LOGERROR("Encountered error reading emb_error struct. %d: %s", errno, strerror(errno)); + } + if (len == 0) { + break; + } + if (len != error_size) { + EMB_LOGERROR("exiting native crash error file read because we read %d instead of %d after %d errors", + (int) len, (int) error_size, count); + free(errors); + close(fd); + return NULL; + } + count++; + } + + close(fd); + + return errors; +} + +char *emb_crash_to_json(emb_crash *crash) { + EMB_LOGDEV("Starting serialization of emb_crash struct to JSON string."); + JSON_Value *root_value = json_value_init_object(); + JSON_Object *root_object = json_value_get_object(root_value); + char *serialized_string = NULL; + + JSON_Value *meta_value = json_parse_string(crash->meta_data); + + if (meta_value != NULL) { + EMB_LOGDEV("Successfully parsed crash JSON metadata"); + json_object_set_value(root_object, kDeviceMetaKey, meta_value); + } else { + EMB_LOGERROR("Could not JSON decode metadata: %s", crash->meta_data); + } + + EMB_LOGDEV("Serializing IDs + payload version."); + json_object_set_string(root_object, kReportIDKey, crash->report_id); + json_object_set_string(root_object, kVersionKey, kCurrentPayloadVersion); + json_object_set_number(root_object, kCrashTSKey, crash->crash_ts); + json_object_set_string(root_object, kSessionIDKey, crash->session_id); + json_object_set_string(root_object, kAppStateKey, crash->app_state); + + // crash data + EMB_LOGDEV("Serializing crash data."); + JSON_Value *crash_value = json_value_init_object(); + JSON_Object *crash_object = json_value_get_object(crash_value); + + json_object_set_number(root_object, kUnwinderErrorCode, crash->unwinder_error); + + emb_exception *exception = &crash->capture; + // exception name + if (strlen(exception->name) == 0) { + EMB_LOGDEV("Defaulting to NULL exception name."); + json_object_set_string(crash_object, kExceptionNameKey, kDefaultNULLFallbackString); + } else { + EMB_LOGDEV("Serializing exception name %s", exception->name); + json_object_set_string(crash_object, kExceptionNameKey, exception->name); + } + // exception message + if (strlen(exception->message) == 0) { + EMB_LOGDEV("Defaulting to NULL exception message."); + json_object_set_string(crash_object, kExceptionMsgKey, kDefaultNULLFallbackString); + } else { + EMB_LOGDEV("Serializing exception message %s", exception->message); + json_object_set_string(crash_object, kExceptionMsgKey, exception->message); + } + + EMB_LOGDEV("Serializing signal information. sig_code=%d, sig_errno=%d, sig_no=%d", + crash->sig_code, crash->sig_errno, crash->sig_no); + json_object_set_number(crash_object, kExceptionCodeKey, crash->sig_code); + json_object_set_number(crash_object, kExceptionErrnoKey, crash->sig_errno); + json_object_set_number(crash_object, kExceptionSignoKey, crash->sig_no); + json_object_set_number(crash_object, kExceptionFaultAddr, crash->fault_addr); + + JSON_Value *frames_value = json_value_init_array(); + JSON_Array *frames_object = json_value_get_array(frames_value); + EMB_LOGDEV("About to serialize %d stack frames.", (int) exception->num_sframes); + + for (int i = 0; i < exception->num_sframes; ++i) { + JSON_Value *frame_value = json_value_init_object(); + JSON_Object *frame_object = json_value_get_object(frame_value); + + emb_sframe frame = exception->stacktrace[i]; + + // module name + if (strlen(frame.filename) == 0) { + json_object_set_string(frame_object, kFilenameKey, kDefaultNULLFallbackString); + } else { + json_object_set_string(frame_object, kFilenameKey, frame.filename); + } + // symbol name + if (strlen(frame.method) == 0) { + json_object_set_string(frame_object, kMethodKey, kDefaultNULLFallbackString); + } else { + json_object_set_string(frame_object, kMethodKey, frame.method); + } + // TODO: lu vs u? + json_object_set_number(frame_object, kFrameAddrKey, frame.frame_addr); + json_object_set_number(frame_object, kOffsetAddrKey, frame.offset_addr); + json_object_set_number(frame_object, kModuleAddrKey, frame.module_addr); + json_object_set_number(frame_object, kLineNumKey, frame.line_num); + + json_array_append_value(frames_object, frame_value); + } + EMB_LOGDEV("Finished serializing stackframes."); + + json_object_set_value(crash_object, kFramesKey, frames_value); + + EMB_LOGDEV("Converting tree to JSON string."); + char *serialized_crash = json_serialize_to_string_pretty(crash_value); + + EMB_LOGDEV("Starting Base64 encoding."); + char *base64_crash = b64_encode(serialized_crash, strlen(serialized_crash)); + json_free_serialized_string(serialized_crash); + + EMB_LOGDEV("Altering JSON tree root."); + json_object_set_string(root_object, kCrashKey, base64_crash); + free(base64_crash); + + // final result + EMB_LOGDEV("Serializing final JSON string"); + serialized_string = json_serialize_to_string_pretty(root_value); +// json_free_serialized_string(serialized_string); + json_value_free(root_value); + json_value_free(crash_value); + return serialized_string; +} + +char *emb_errors_to_json(emb_error *errors) { + EMB_LOGDEV("Starting serialization of emb_error struct to JSON string."); + char *serialized_string = NULL; + emb_error *cur_error = errors; + int count = 0; + + JSON_Value *errors_value = json_value_init_array(); + JSON_Array *errors_object = json_value_get_array(errors_value); + + while (count < EMB_MAX_ERRORS) { + // errors is calloc'd so we know that once we hit a value with zero, we are done. + if (cur_error->num == 0) { + break; + } + + JSON_Value *error_value = json_value_init_object(); + JSON_Object *error_object = json_value_get_object(error_value); + + json_object_set_number(error_object, kErrNum, cur_error->num); + json_object_set_number(error_object, kErrContext, cur_error->context); + + json_array_append_value(errors_object, error_value); + + cur_error++; + count++; + } + EMB_LOGDEV("Converted %d errors.", count); + EMB_LOGDEV("Serializing final JSON string."); + serialized_string = json_serialize_to_string_pretty(errors_value); + json_value_free(errors_value); + return serialized_string; +} diff --git a/embrace-android-sdk/src/main/cpp/file_writer.h b/embrace-android-sdk/src/main/cpp/file_writer.h new file mode 100644 index 0000000000..89773c0102 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/file_writer.h @@ -0,0 +1,25 @@ +// +// Created by Eric Lanz on 5/17/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_FILE_WRITER_H +#define EMBRACE_NATIVE_CRASHES_FILE_WRITER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "emb_ndk_manager.h" + +bool emb_write_crash_to_file(emb_env *stack); +emb_crash *emb_read_crash_from_file(const char *path); +emb_error *emb_read_errors_from_file(const char *path); +char *emb_crash_to_json(emb_crash *crash); +char *emb_errors_to_json(emb_error *errors); + +#ifdef __cplusplus +} +#endif + +#endif //EMBRACE_NATIVE_CRASHES_FILE_WRITER_H diff --git a/embrace-android-sdk/src/main/cpp/jni_util.c b/embrace-android-sdk/src/main/cpp/jni_util.c new file mode 100644 index 0000000000..75dc097ce1 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/jni_util.c @@ -0,0 +1,29 @@ +#include "jni_util.h" +#include + +JavaVM *emb_JVM; + +jint JNI_OnLoad(JavaVM *vm, void *reserved) { + emb_JVM = vm; + return JNI_VERSION_1_6; +} + +#ifdef __cplusplus +extern "C" { +#endif +bool emb_jniIsAttached() { + JNIEnv *env; + int status = (*emb_JVM)->GetEnv(emb_JVM, (void **)&env, JNI_VERSION_1_6); + + return status == JNI_OK; +} + +void emb_jni_release_string_utf_chars(JNIEnv *env, jstring string, const char *utf) { + if (env != NULL && string != NULL && utf != NULL) { + (*env)->ReleaseStringUTFChars(env, string, utf); + } +} + +#ifdef __cplusplus +} +#endif diff --git a/embrace-android-sdk/src/main/cpp/jni_util.h b/embrace-android-sdk/src/main/cpp/jni_util.h new file mode 100644 index 0000000000..cfbeced5e4 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/jni_util.h @@ -0,0 +1,12 @@ +// +// Created by Fredric Newberg on 8/18/21. +// + +#ifndef EMBRACE_ANDROID_SDK3_JNI_UTIL_H +#define EMBRACE_ANDROID_SDK3_JNI_UTIL_H + +#include + +void emb_jni_release_string_utf_chars(JNIEnv *env, jstring string, const char *utf); + +#endif //EMBRACE_ANDROID_SDK3_JNI_UTIL_H diff --git a/embrace-android-sdk/src/main/cpp/safejni/safe_jni.c b/embrace-android-sdk/src/main/cpp/safejni/safe_jni.c new file mode 100644 index 0000000000..73708569dd --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/safejni/safe_jni.c @@ -0,0 +1,112 @@ +#include +#include +#include "safe_jni.h" + +/** + * Clears any pending exceptions, and returns true if the JNI has a pending exception. + * This usually indicates a programming error or an OutOfMemoryError. Embrace should + * usually deal with this by gracefully no-oping from whatever JNI call we were + * attempting to perform, although the recovery code depends on the exact scenario. + * + * @param env the JNI env + * @return true if there is a pending exception. + */ +static bool jni_has_pending_exception(_Nonnull JNIEnv *_Nonnull env) { + bool result = (*env)->ExceptionCheck(env); + if (result) { + (*env)->ExceptionClear(env); + } + return result; +} + +_Nullable jclass emb_jni_find_class(_Nonnull JNIEnv *_Nonnull env, + const char *_Nonnull clz) { + jclass obj = (*env)->FindClass(env, clz); + + if (jni_has_pending_exception(env)) { + return NULL; + } + return obj; +} + +_Nullable jstring emb_jni_new_string_utf(_Nonnull JNIEnv *_Nonnull env, + const char *_Nonnull src) { + jstring obj = (*env)->NewStringUTF(env, src); + + if (jni_has_pending_exception(env)) { + return NULL; + } + return obj; +} + +_Nullable jmethodID emb_jni_get_method_id(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const char *_Nonnull name, + const char *_Nonnull sig) { + jmethodID id = (*env)->GetMethodID(env, clz, name, sig); + + if (jni_has_pending_exception(env)) { + return NULL; + } + return id; +} + +_Nullable jclass emb_jni_find_class_global_ref(_Nonnull JNIEnv *_Nonnull env, + const char *_Nonnull clz) { + jclass obj = emb_jni_find_class(env, clz); + + if (obj == NULL) { + return NULL; + } + return (*env)->NewGlobalRef(env, obj); +} + +_Nullable jobject emb_jni_new_object(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const _Nonnull jmethodID mthd, + ...) { + va_list vargs; + va_start(vargs, mthd); + jobject obj = (*env)->NewObjectV(env, clz, mthd, vargs); + va_end(vargs); + + if (jni_has_pending_exception(env)) { + return NULL; + } else { + return obj; + } +} + +jboolean emb_jni_call_boolean_method(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const _Nonnull jmethodID mthd, + ...) { + va_list vargs; + va_start(vargs, mthd); + jboolean result = (*env)->CallBooleanMethodV(env, clz, mthd, vargs); + va_end(vargs); + + if (jni_has_pending_exception(env)) { + return false; + } else { + return result; + } +} + + +jboolean emb_jni_call_void_method(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const _Nonnull jmethodID mthd, + ...) { + va_list vargs; + va_start(vargs, mthd); + (*env)->CallVoidMethodV(env, clz, mthd, vargs); + va_end(vargs); + + return jni_has_pending_exception(env); +} + +void emb_jni_delete_local_ref(_Nonnull JNIEnv *_Nonnull env, + _Nonnull jobject obj) { + (*env)->DeleteLocalRef(env, obj); +} diff --git a/embrace-android-sdk/src/main/cpp/safejni/safe_jni.h b/embrace-android-sdk/src/main/cpp/safejni/safe_jni.h new file mode 100644 index 0000000000..1b13b233fd --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/safejni/safe_jni.h @@ -0,0 +1,68 @@ +#ifndef EMBRACE_SAFE_JNI_H +#define EMBRACE_SAFE_JNI_H + +/** + * Calls (*env)->FindClass and clears any pending exceptions, returning + * null if an exception occurred. + */ +_Nullable jclass emb_jni_find_class(_Nonnull JNIEnv *_Nonnull env, + const char *_Nonnull clz); + +/** + * Calls (*env)->NewStringUtf and clears any pending exceptions, returning + * null if an exception occurred. + */ +_Nullable jstring emb_jni_new_string_utf(_Nonnull JNIEnv *_Nonnull env, + const char *_Nonnull src); + +/** + * Calls (*env)->GetMethodId and clears any pending exceptions, returning + * null if an exception occurred. + */ +_Nullable jmethodID emb_jni_get_method_id(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const char *_Nonnull name, + const char *_Nonnull sig); + +/** + * Calls (*env)->FindClass and creates a global ref. This method also clears any + * pending exceptions, and returns null if an exception occurred. + */ +_Nullable jclass emb_jni_find_class_global_ref(_Nonnull JNIEnv *_Nonnull env, + const char *_Nonnull clz); + +/** + * Calls (*env)->NewObject and clears any pending exceptions, returning + * null if an exception occurred. + */ +_Nullable jobject emb_jni_new_object(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const _Nonnull jmethodID mthd, + ...); + +/** + * Calls (*env)->CallBooleanMethod and clears any pending exceptions, returning + * false if an exception occurred. + */ +jboolean emb_jni_call_boolean_method(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const _Nonnull jmethodID mthd, + ...); + +/** + * Calls (*env)->CallVoidMethod and clears any pending exceptions, returning + * false if an exception occurred. + */ +jboolean emb_jni_call_void_method(_Nonnull JNIEnv *_Nonnull env, + const _Nonnull jclass clz, + const _Nonnull jmethodID mthd, + ...); + +/** + * Calls (*env)->DeleteLocalRef. + */ +void emb_jni_delete_local_ref(_Nonnull JNIEnv *_Nonnull env, + _Nonnull jobject obj); + + +#endif //EMBRACE_SAFE_JNI_H diff --git a/embrace-android-sdk/src/main/cpp/sampler/emb_timer.c b/embrace-android-sdk/src/main/cpp/sampler/emb_timer.c new file mode 100644 index 0000000000..ddb3ac4890 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/emb_timer.c @@ -0,0 +1,58 @@ +#include +#include +#include "emb_timer.h" +#include "../emb_log.h" + +// see https://man7.org/linux/man-pages/man2/timer_create.2.html + +static long millis_to_nanos(long ms) { + return ms * 1000000; +} + +static void millis_to_timespec(struct timespec *timespec, long ms) { + if (timespec == NULL) { + return; + } + // reset fields + timespec->tv_sec = 0; + timespec->tv_nsec = 0; + + // populate seconds + timespec->tv_sec = ms / 1000; + ms -= timespec->tv_sec * 1000; + + // populate nanoseconds + timespec->tv_nsec = millis_to_nanos(ms); +} + +int emb_create_timer(timer_t *timer, + struct sigevent *sigevent, + void (*function)(union sigval)) { + if (timer == NULL || sigevent == NULL || function == NULL) { + return -1; + } + sigevent->sigev_notify = SIGEV_THREAD; + sigevent->sigev_signo = SIGRTMIN; + sigevent->sigev_notify_function = function; + return timer_create(CLOCK_MONOTONIC, sigevent, timer); +} + +int emb_start_timer(timer_t timer, + struct itimerspec *timerspec, + long initial_delay_ms, + long interval_ms) { + if (timer == NULL || timerspec == NULL) { + return -1; + } + millis_to_timespec(&timerspec->it_value, initial_delay_ms); + millis_to_timespec(&timerspec->it_interval, interval_ms); + return timer_settime(timer, 0, timerspec, NULL); +} + +int emb_stop_timer(timer_t timer, struct itimerspec *timerspec) { + if (timer == NULL || timerspec == NULL) { + return -1; + } + // setting delay + interval to 0 stops the timer + return emb_start_timer(timer, timerspec, 0, 0); +} diff --git a/embrace-android-sdk/src/main/cpp/sampler/emb_timer.h b/embrace-android-sdk/src/main/cpp/sampler/emb_timer.h new file mode 100644 index 0000000000..6fc6ebf55a --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/emb_timer.h @@ -0,0 +1,30 @@ +#ifndef EMBRACE_EMB_TIMER_H +#define EMBRACE_EMB_TIMER_H + +#include + +/** + * Creates a pthread timer that notifies on the given function pointer as though a new + * thread had been started. + */ +int emb_create_timer(timer_t *timer, + struct sigevent *sigevent, + void (*function)(union sigval)); + +/** + * Starts a pthread timer with the configured timer & intervals. + * + * IMPORTANT: initial_delay_ms MUST be greater than zero, otherwise the timer won't start. + */ +int emb_start_timer(timer_t timer, + struct itimerspec *timerspec, + long initial_delay_ms, + long interval_ms); + +/** + * Stops (but does not delete) a pthread timer. + */ +int emb_stop_timer(timer_t timer, + struct itimerspec *timerspec); + +#endif //EMBRACE_EMB_TIMER_H diff --git a/embrace-android-sdk/src/main/cpp/sampler/sampler_structs.h b/embrace-android-sdk/src/main/cpp/sampler/sampler_structs.h new file mode 100644 index 0000000000..142b6fb3a6 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/sampler_structs.h @@ -0,0 +1,92 @@ +#ifndef EMBRACE_SAMPLER_STRUCTS_H +#define EMBRACE_SAMPLER_STRUCTS_H + +#include "../stack_frames.h" + +// Android blocks SIGUSR1 in the same way as SIGQUIT as it uses the signal +// to force garbage collection. We therefore use SIGUSR2 instead. +// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:art/runtime/signal_catcher.cc;l=187 +#define EMB_TARGET_THREAD_SIGNUM SIGUSR2 + +enum sample_unwinder_type { + LIBUNWIND, + LIBUNWINDSTACK, +}; + +/** + * Holds info on a stackframe captured during a native sample. + */ +typedef struct { + /** + * The program counter + */ + uint64_t pc; + + /** + * The load address of shared object. This information may not be available + * in which case the value will be 0x0. + */ + uint64_t so_load_addr; + + /** + * The absolute path of the shared object. This information may not be available + * in which case the string will be empty. + */ + char so_path[kEMBSamplePathMaxLen]; + + /** + * The result for unwinding this particular stackframe. Non-zero values indicate an error. + */ + int result; +} volatile emb_sample_stackframe; + +/** + * Holds info on a stacktrace captured during a native sample. + */ +typedef struct { + + /** + * The number of stackframes captured. + */ + size_t num_sframes; + + /** + * All the stackframes which have been captured during the current sample. + */ + emb_sample_stackframe stack[kEMBMaxSampleSFrames]; + + /** + * A zero value indicates the sample was successful. A non-zero value indicates + * that something went wrong with the sample. Error codes match those defined in utilities.h. + */ + uint8_t result; + + /** + * The timestamp at which the sample started + */ + int64_t timestamp_ms; + + /** + * The duration of the overall sample + */ + int64_t duration_ms; +} volatile emb_sample; + +/** + * Holds all the samples captured during an interval. + */ +typedef struct { + + /** + * The number of samples made. + */ + size_t num_samples; + + /** + * All the samples that have been captured during this interval. + */ + emb_sample samples[kEMBMaxSamples]; + +} volatile emb_interval; + +#endif //EMBRACE_SAMPLER_STRUCTS_H diff --git a/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.cpp b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.cpp new file mode 100644 index 0000000000..ed4facf92a --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.cpp @@ -0,0 +1,50 @@ +#include "string.h" +#include "sampler_unwinder_stack.h" +#include +#include +#include +#include +#include "../utilities.h" +#include "unwinder_dlinfo.h" +#include "unwindstack/AndroidUnwinder.h" + +static ssize_t emb_unwindstack_impl(emb_env *env, emb_unwind_state *sample, void *user_context) { + if (env != nullptr && env->currently_handling) { + sample->result = EMB_ERROR_ENV_TERMINATING; + return 0; + } + + unwindstack::AndroidUnwinder *unwinder = unwindstack::AndroidUnwinder::Create(getpid()); + unwindstack::AndroidUnwinderData android_unwinder_data = unwindstack::AndroidUnwinderData(); + + if (unwinder->Unwind(user_context, android_unwinder_data)) { + int i = 0; + for (const auto &frame: android_unwinder_data.frames) { + sample->stack[i++] = frame.pc; + } + } else { + sample->result = EMB_ERROR_UNWIND_STACK_FAILURE; + sample->num_sframes = 0; + return 0; + } + + int frame_count = static_cast(android_unwinder_data.frames.size()); + + sample->num_sframes = frame_count; + + return frame_count; +} + +size_t emb_unwind_with_libunwindstack(emb_env *env, emb_sample *sample, + void *user_context) { + + emb_unwind_state _unwind_state = {0}; + emb_unwind_state *unwind_state = &_unwind_state; + ssize_t frame_count = emb_unwindstack_impl(env, unwind_state, user_context); + + // copy frames from temporary unwind_state struct to more permament emb_sample + // (allows truncating the stack) + emb_copy_frames(sample, unwind_state); + emb_symbolicate_stacktrace(sample); + return frame_count; +} diff --git a/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.h b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.h new file mode 100644 index 0000000000..4a2d065d7d --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_stack.h @@ -0,0 +1,25 @@ +#ifndef EMBRACE_SAMPLER_UNWINDER_STACK_H +#define EMBRACE_SAMPLER_UNWINDER_STACK_H + +#include +#include +#include "sampler_structs.h" +#include "../emb_ndk_manager.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Unwind the stack using the libunwindstack unwinder. + * + * unwinder.h documents that this is supported back to API 15 with no restrictions on + * CPU architecture, so no conditional checks will be put in place. + */ +size_t emb_unwind_with_libunwindstack(emb_env *_env, emb_sample *sample, + void *user_context); + +#ifdef __cplusplus +} +#endif +#endif //EMBRACE_SAMPLER_UNWINDER_STACK_H diff --git a/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.c b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.c new file mode 100644 index 0000000000..055cb13b01 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.c @@ -0,0 +1,113 @@ +#include +#include +#include "sampler_unwinder_unwind.h" +#include "unwinder_dlinfo.h" +#include "../utilities.h" + +#if defined(__arm__) +#include +#endif + +static emb_env *env = NULL; + +static _Unwind_Reason_Code emb_libunwind_sampling_callback(struct _Unwind_Context *context, + void *arg) { + if (env != NULL && env->currently_handling) { + return _URC_NO_REASON; + } + emb_unwind_state *state = (emb_unwind_state *) arg; + uint64_t ip = _Unwind_GetIP(context); + + size_t index = state->num_sframes; + if (index >= kEMBSampleUnwindLimit) { + return _URC_END_OF_STACK; + } else if (index > 0 && (void *) ip == NULL) { + return _URC_NO_REASON; + } + + state->stack[index] = ip; + state->num_sframes++; + return _URC_NO_REASON; +} + +#if defined(__arm__) +static ssize_t emb_unwind_32bit_stack(emb_unwind_state *state, + siginfo_t *info, void *user_context) __asyncsafe { + unw_cursor_t cursor; + unw_context_t uc; + int index = 0; + + if (unw_getcontext(&uc) != 0) { + state->result = EMB_ERROR_UNW_CONTEXT_FAILED; + return 0; + } + int local = unw_init_local(&cursor, &uc); + if (local != 0) { + state->result = EMB_ERROR_UNW_INIT_LOCAL_FAILED; + return 0; + } + if (user_context != NULL) { + const ucontext_t *signal_ucontext = (const ucontext_t *)user_context; + const struct sigcontext *signal_mcontext = &(signal_ucontext->uc_mcontext); + unw_set_reg(&cursor, UNW_ARM_R0, signal_mcontext->arm_r0); + unw_set_reg(&cursor, UNW_ARM_R1, signal_mcontext->arm_r1); + unw_set_reg(&cursor, UNW_ARM_R2, signal_mcontext->arm_r2); + unw_set_reg(&cursor, UNW_ARM_R3, signal_mcontext->arm_r3); + unw_set_reg(&cursor, UNW_ARM_R4, signal_mcontext->arm_r4); + unw_set_reg(&cursor, UNW_ARM_R5, signal_mcontext->arm_r5); + unw_set_reg(&cursor, UNW_ARM_R6, signal_mcontext->arm_r6); + unw_set_reg(&cursor, UNW_ARM_R7, signal_mcontext->arm_r7); + unw_set_reg(&cursor, UNW_ARM_R8, signal_mcontext->arm_r8); + unw_set_reg(&cursor, UNW_ARM_R9, signal_mcontext->arm_r9); + unw_set_reg(&cursor, UNW_ARM_R10, signal_mcontext->arm_r10); + unw_set_reg(&cursor, UNW_ARM_R11, signal_mcontext->arm_fp); + unw_set_reg(&cursor, UNW_ARM_R12, signal_mcontext->arm_ip); + unw_set_reg(&cursor, UNW_ARM_R13, signal_mcontext->arm_sp); + unw_set_reg(&cursor, UNW_ARM_R14, signal_mcontext->arm_lr); + unw_set_reg(&cursor, UNW_ARM_R15, signal_mcontext->arm_pc); + unw_set_reg(&cursor, UNW_REG_IP, signal_mcontext->arm_pc); + unw_set_reg(&cursor, UNW_REG_SP, signal_mcontext->arm_sp); + state->stack[index++] = signal_mcontext->arm_pc; + state->num_sframes++; + } + + while (unw_step(&cursor) > 0 && index < kEMBSampleUnwindLimit) { + unw_word_t ip = 0; + unw_get_reg(&cursor, UNW_REG_IP, &ip); + state->stack[index++] = ip; + state->num_sframes++; + } + return index; +} +#endif + +static _Unwind_Reason_Code calculate_unwind_result(_Unwind_Reason_Code code) { + return code == _URC_END_OF_STACK || code == _URC_NO_REASON ? 0 : code; +} + +size_t emb_unwind_with_libunwind(emb_env *_env, emb_sample *sample, bool is32bit, + siginfo_t *info, void *user_context) { + env = _env; + emb_unwind_state _unwind_state = {0}; + emb_unwind_state *unwind_state = &_unwind_state; + bool unwound = false; + +#if defined(__arm__) + if (is32bit) { + emb_unwind_32bit_stack(unwind_state, info, user_context); + sample->result = unwind_state->result; + unwound = true; + } +#endif + if (!unwound) { + _Unwind_Reason_Code code = _Unwind_Backtrace(emb_libunwind_sampling_callback, + (void *) unwind_state); + sample->result = calculate_unwind_result(code); + } + + // copy frames from temporary unwind_state struct to more permament emb_sample + // (allows truncating the stack) + emb_copy_frames(sample, unwind_state); + emb_symbolicate_stacktrace(sample); + return sample->num_sframes; +} diff --git a/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.h b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.h new file mode 100644 index 0000000000..66e95dde50 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/sampler_unwinder_unwind.h @@ -0,0 +1,14 @@ +#ifndef EMBRACE_SAMPLER_UNWINDER_UNWIND_H +#define EMBRACE_SAMPLER_UNWINDER_UNWIND_H + +#include +#include +#include "sampler_structs.h" +#include "../emb_ndk_manager.h" + +/** + * Unwind the stack using the libunwind unwinder. + */ +size_t emb_unwind_with_libunwind(emb_env *env, emb_sample *sample, bool is32bit, siginfo_t *info, void *user_context); + +#endif //EMBRACE_SAMPLER_UNWINDER_UNWIND_H diff --git a/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.c b/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.c new file mode 100644 index 0000000000..0a230c47ab --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.c @@ -0,0 +1,306 @@ +#include +#include +#include +#include +#include +#include +#include +#include "../unwinders/unwinder_stack.h" +#include "sampler_unwinder_unwind.h" +#include "sampler_unwinder_stack.h" +#include "../signals/signal_utils.h" +#include "../utilities.h" +#include "stacktrace_sampler.h" +#include "../emb_log.h" +#include "../utils/system_clock.h" +#include "emb_timer.h" + +/* The unwinder - by default libunwind is used */ +static volatile enum sample_unwinder_type unwind_type = LIBUNWIND; + +/* Whether the architecture is 32 bit or not */ +static volatile bool is32bit; + +/* Struct which holds the samples for the interval. */ +static emb_interval _interval = {0}; +static volatile emb_interval *interval = &_interval; +static volatile bool is_thread_sampler_started = false; + +/* Handle to the thread which we want to sample. */ +static volatile pthread_t target_thread = -1; + +/* The global embrace environment. This may be null if NDK error reporting hasn't been enabled. */ +static emb_env *env = NULL; + +/* The current signal handler for EMB_TARGET_THREAD_SIGNUM */ +static struct sigaction _handler = {0}; +static struct sigaction *handler = &_handler; + +/* The previous signal handler for EMB_TARGET_THREAD_SIGNUM, if any was set. */ +static struct sigaction _prev_handler = {0}; +static struct sigaction *prev_handler = &_prev_handler; + +/* Holds information about the timer action */ +static struct sigevent timer_action = {0}; + +/* Holds the ID of the current timer */ +static timer_t timer_id = NULL; + +/* Holds information about the timer interval + delay */ +static struct itimerspec timerspec = {0}; + +emb_interval *emb_current_interval() { + return interval; +} + +emb_sample *emb_current_sample() { + return &interval->samples[interval->num_samples]; +} + +static void emb_remove_stackframes(int result, emb_sample *sample) { + // serialize the first stackframe so we can see where things went wrong. + sample->num_sframes = 1; + sample->result = result; +} + +static void emb_process_captured_sample(emb_sample *sample) { + if (sample->num_sframes <= 1) { + return; + } + + // avoid serializing useless frames reported by libunwindstack + if (sample->result == EMB_ERROR_UNWIND_STACK_FAILURE) { + emb_remove_stackframes(EMB_ERROR_UNWIND_STACK_FAILURE, sample); + return; + } + + // avoid serializing useless frames from unwinders stuck in an infinite loop + for (int k = 1; k < sample->num_sframes; k++) { + volatile emb_sample_stackframe *prev_frame = &sample->stack[k - 1]; + volatile emb_sample_stackframe *frame = &sample->stack[k]; + + if (frame->pc != prev_frame->pc) { + return; + } + } + emb_remove_stackframes(EMB_UNWIND_INFINITE_LOOP, sample); +} + +/** + * Samples the target thread by attempting to gather a sample. + */ +static void emb_sample_target_thread(siginfo_t *info, void *user_context, emb_sample *sample) { + switch (unwind_type) { + case LIBUNWIND: + emb_unwind_with_libunwind(env, sample, is32bit, info, user_context); + break; + case LIBUNWINDSTACK: + emb_unwind_with_libunwindstack(env, sample, user_context); + break; + default: + sample->result = EMB_UNKNOWN_UNWIND_TYPE; + emb_log_last_error(env, EMB_UNKNOWN_UNWIND_TYPE, unwind_type); + } + + // check we captured something sensible. + emb_process_captured_sample(sample); +} + +static void emb_start_sample(emb_sample *sample) { + int64_t timestamp = sample->timestamp_ms; + memset((void *) sample, 0, sizeof(emb_sample)); + sample->timestamp_ms = timestamp; + + // This shouldn't happen, but let's be extra paranoid and set an error code that will show up + // if there is a data race between emb_fetch_sample() and other C code. + // This could explains some (but not all) of the unusual stacktraces. + sample->result = EMB_SAMPLE_DATA_RACE; +} + +static void emb_end_sample(emb_sample *sample) { + // Reset an error code that will show up if there is a data race between emb_fetch_sample() + // and other C code, that was set in emb_start_sample(). + if (sample->result == EMB_SAMPLE_DATA_RACE) { + sample->result = 0; + } + + // record timestamp + int64_t now = emb_get_time_ms(); + sample->duration_ms = now - sample->timestamp_ms; + interval->num_samples++; +} + +static bool is_installed() { + return env != NULL; +} + +static bool has_reached_sample_limit() { + return !is_installed() || interval->num_samples >= kEMBMaxSamples; +} + +/** + * Handles EMB_TARGET_THREAD_SIGNUM signals sent on the target thread. + */ +static void emb_handle_target_signal(int signum, siginfo_t *info, void *user_context) __asyncsafe { + if (has_reached_sample_limit()) { + return; + } + + // make a best effort to avoid sampling in the case of a fatal signal. + emb_sample *sample = emb_current_sample(); + if (env != NULL && !env->currently_handling) { + emb_start_sample(sample); + emb_sample_target_thread(info, user_context, sample); + } + emb_end_sample(sample); + + // ignore the previous handler. We don't want to end in an infinite loop if that + // handler raises SIGUSR2. +} + +/** + * Installs a signal handler for EMB_TARGET_THREAD_SIGNUM. This should be called on the + * target thread - the caller is responsible for enforcing this. + * + * @return true if the handler was installed, otherwise false. + */ +static bool emb_install_signal_handler() { + EMB_LOGDEV("Setting up signal handler for EMB_TARGET_THREAD_SIGNUM."); + + bool result = true; + EMB_LOGDEV("Populating handler with information."); + handler->sa_sigaction = emb_handle_target_signal; + handler->sa_flags = SA_SIGINFO; + + // block all other signals when the handler is already executing. + // https://www.gnu.org/software/libc/manual/html_node/Blocking-for-Handler.html + sigfillset(&handler->sa_mask); + + const int signal = EMB_TARGET_THREAD_SIGNUM; + int success = sigaction(signal, handler, prev_handler); + if (success != 0) { + EMB_LOGERROR("Sig install failed: %s", strerror(errno)); + result = false; + } else { + EMB_LOGDEV("Successfully installed handler for EMB_TARGET_THREAD_SIGNUM."); + } + return result; +} + +void emb_set_unwinder(int unwinder) { + EMB_LOGDEV("Called emb_set_unwinder(), unwinder=%d", unwinder); + unwind_type = unwinder; + EMB_LOGDEV("Preparing to sample native thread."); +} + +/** + * Initiates sampling of the target thread. + */ +static int raise_signal_on_target_thread() { + int status = 0; + + if (target_thread != -1) { + int result = pthread_kill(target_thread, EMB_TARGET_THREAD_SIGNUM); + if (result != 0) { + EMB_LOGWARN("Failed to send signal to target thread: %d", result); + status = EMB_ERROR_SIGUSR2_FAILED; + } else { + EMB_LOGINFO("Sent signal to target thread with ID %ld, result=%d", (long) target_thread, result); + } + } else { + EMB_LOGWARN("target_thread not set, skipping sending signal to target thread."); + status = EMB_ERROR_TARGET_THREAD_NULL; + } + return status; +} + +void emb_sigev_notify_function(union sigval sigval) { + if (has_reached_sample_limit()) { + emb_stop_timer(timer_id, &timerspec); + return; + } else { + emb_current_sample()->timestamp_ms = emb_get_time_ms(); + raise_signal_on_target_thread(); + } +} + +/** + * Raises SIGUSR2 on the target thread. + */ +int emb_start_thread_sampler(long interval_ms) { + EMB_LOGDEV("Called emb_start_thread_sampler()."); + if (is_thread_sampler_started) { + return -1; + } + is_thread_sampler_started = true; + + if (!is_installed()) { + return EMB_ERROR_NOT_INSTALLED; + } + interval->num_samples = 0; + + EMB_LOGDEV("Starting timer for sampling."); + if (emb_start_timer(timer_id, &timerspec, 1, interval_ms) != 0) { + EMB_LOGERROR("Failure starting timer, errno=%d", errno); + return EMB_ERROR_TIMER_FAILED; + } + return 0; +} + +/** + * Raises SIGUSR2 on the target thread. + */ +int emb_stop_thread_sampler() { + EMB_LOGDEV("Called emb_stop_thread_sampler()."); + if (!is_thread_sampler_started) { + return -1; + } + is_thread_sampler_started = false; + + if (!is_installed()) { + return EMB_ERROR_NOT_INSTALLED; + } + + EMB_LOGDEV("Stopping timer."); + if (emb_stop_timer(timer_id, &timerspec) != 0) { + EMB_LOGERROR("Failure stopping timer, errno=%d", errno); + } + return 0; +} + +bool emb_monitor_current_thread() { + EMB_LOGDEV("Called emb_monitor_current_thread()."); + bool result = true; + static pthread_mutex_t _emb_signal_mutex = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&_emb_signal_mutex); + + EMB_LOGINFO("Installing SIGUSR2 handler."); + target_thread = pthread_self(); + EMB_LOGDEV("Target thread ID=%ld", target_thread); + result = emb_install_signal_handler(); + + pthread_mutex_unlock(&_emb_signal_mutex); + return result; +} + +bool emb_setup_native_thread_sampler(emb_env *emb_env, bool _is32bit) { + EMB_LOGDEV("Called emb_setup_native_thread_sampler()."); + + static pthread_mutex_t _emb_timer_mutex = PTHREAD_MUTEX_INITIALIZER; + bool result = true; + is32bit = _is32bit; + + pthread_mutex_lock(&_emb_timer_mutex); + if (!is_installed()) { + EMB_LOGINFO("Installing SIGUSR2 handler."); + env = emb_env; + + EMB_LOGDEV("Creating timer for sampling."); + if (emb_create_timer(&timer_id, &timer_action, emb_sigev_notify_function) != 0) { + EMB_LOGERROR("Failure creating timer, errno=%d", errno); + result = false; + } + } + pthread_mutex_unlock(&_emb_timer_mutex); + return result; +} diff --git a/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.h b/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.h new file mode 100644 index 0000000000..6661a6a3a6 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler.h @@ -0,0 +1,39 @@ +#ifndef EMBRACE_NATIVE_STACKTRACE_SAMPLER_H +#define EMBRACE_NATIVE_STACKTRACE_SAMPLER_H + +#include "sampler_structs.h" +#include "../emb_ndk_manager.h" + +/** + * Performs one-time initialization required to sample native threads + */ +bool emb_setup_native_thread_sampler(emb_env *emb_env, bool _is32bit); + +/** + * Initializes a signal handler for SIGUSR2 on the current thread. Signals can then be sent from + * other threads when a stacktrace is required. + */ +bool emb_monitor_current_thread(); + +/** + * Raises a user-defined signal (SIGUSR2) on the target thread at regular intervals. + * This allows us to sample the thread that was monitored in emb_setup_native_thread_sampler(). + */ +int emb_start_thread_sampler(long interval_ms); + +/** + * Stops raised SIGUSR2 on the target thread at regular intervals. + */ +int emb_stop_thread_sampler(); + +/** + * Prepares the native layer for imminent stacktrace sampling. + */ +void emb_set_unwinder(int unwinder); + +/** + * Fetches the samples for the current interval. + */ +emb_interval *emb_current_interval(); + +#endif //EMBRACE_NATIVE_STACKTRACE_SAMPLER_H diff --git a/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler_jni.c b/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler_jni.c new file mode 100644 index 0000000000..425b3f24af --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/stacktrace_sampler_jni.c @@ -0,0 +1,351 @@ +#include +#include +#include "stacktrace_sampler.h" +#include "../utilities.h" +#include "../safejni/safe_jni.h" +#include "../emb_log.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + bool initialized; + jclass clz_native_thread_anr_sample; + jclass clz_native_thread_anr_stackframe; + jclass clz_integer; + jclass clz_long; + jclass clz_arraylist; + + jmethodID ctor_native_thread_anr_sample; + jmethodID ctor_native_thread_anr_stackframe; + jmethodID ctor_integer; + jmethodID ctor_long; + jmethodID ctor_arraylist; + jmethodID mthd_arraylist_add; +} volatile emb_sampler_jni_cache; + +/* Caches global JNI refs to avoid expensive lookups */ +static emb_sampler_jni_cache impl_cache = {0}; +static emb_sampler_jni_cache *cache = &impl_cache; + +/** + * Populates the JNI cache, storing the classes as global refs in the struct. + */ +static bool populate_jni_cache(JNIEnv *env) { + EMB_LOGDEV("Populating the JNI cache."); + + // load class ref for NativeThreadAnrSample + cache->clz_native_thread_anr_sample = emb_jni_find_class_global_ref(env, + "io/embrace/android/embracesdk/payload/NativeThreadAnrSample"); + if (cache->clz_native_thread_anr_sample == NULL) { + EMB_LOGDEV("Failed to initialize clz_native_thread_anr_sample"); + return false; + } + + // load class ref for NativeThreadAnrStackframe + cache->clz_native_thread_anr_stackframe = emb_jni_find_class_global_ref(env, + "io/embrace/android/embracesdk/payload/NativeThreadAnrStackframe"); + if (cache->clz_native_thread_anr_stackframe == NULL) { + EMB_LOGDEV("Failed to initialize clz_native_thread_anr_stackframe"); + return false; + } + + // load class ref for Integer + cache->clz_integer = emb_jni_find_class_global_ref(env, "java/lang/Integer"); + if (cache->clz_integer == NULL) { + EMB_LOGDEV("Failed to initialize clz_integer"); + return false; + } + + // load class ref for Long + cache->clz_long = emb_jni_find_class_global_ref(env, "java/lang/Long"); + if (cache->clz_long == NULL) { + EMB_LOGDEV("Failed to initialize clz_long"); + return false; + } + + // load class ref for ArrayList + cache->clz_arraylist = emb_jni_find_class_global_ref(env, "java/util/ArrayList"); + if (cache->clz_arraylist == NULL) { + EMB_LOGDEV("Failed to initialize clz_arraylist"); + return false; + } + + // load method ref for NativeThreadAnrSample ctor + cache->ctor_native_thread_anr_sample = emb_jni_get_method_id(env, + cache->clz_native_thread_anr_sample, + "", + "(Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;)V"); + if (cache->ctor_native_thread_anr_sample == NULL) { + EMB_LOGDEV("Failed to initialize ctor_native_thread_anr_sample"); + return false; + } + + // load method ref for NativeThreadAnrStackframe ctor + cache->ctor_native_thread_anr_stackframe = emb_jni_get_method_id(env, + cache->clz_native_thread_anr_stackframe, + "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V"); + if (cache->ctor_native_thread_anr_stackframe == NULL) { + EMB_LOGDEV("Failed to initialize ctor_native_thread_anr_stackframe"); + return false; + } + + // load method ref for Integer ctor + cache->ctor_integer = emb_jni_get_method_id(env, cache->clz_integer, "", "(I)V"); + if (cache->ctor_integer == NULL) { + EMB_LOGDEV("Failed to initialize ctor_integer"); + return false; + } + + // load method ref for Long ctor + cache->ctor_long = emb_jni_get_method_id(env, cache->clz_long, "", "(J)V"); + if (cache->ctor_long == NULL) { + EMB_LOGDEV("Failed to initialize ctor_long"); + return false; + } + + // load method ref for ArrayList ctor + cache->ctor_arraylist = emb_jni_get_method_id(env, cache->clz_arraylist, "", "(I)V"); + if (cache->ctor_arraylist == NULL) { + EMB_LOGDEV("Failed to initialize ctor_arraylist"); + return false; + } + + // load method ref for ArrayList#add + cache->mthd_arraylist_add = emb_jni_get_method_id(env, cache->clz_arraylist, "add", + "(Ljava/lang/Object;)Z"); + if (cache->mthd_arraylist_add == NULL) { + EMB_LOGDEV("Failed to initialize mthd_arraylist_add"); + return false; + } + + // everything initialized fine. + EMB_LOGDEV("Populated JNI cache."); + return true; +} + +/** + * Inits a cache of JNI references. This MUST only be called from Embrace's ANR monitor + * thread - it's not safe to share JNI refs across threads. + * + * The cache may fail to initialize in rare cases - e.g. if there isn't enough memory to + * find the class/methods. We attempt to handle failure by returning early in this case, + * and on the next call we make another attempt to initialize. + */ +static bool init_jni_cache(JNIEnv *env) { + if (!cache->initialized) { + cache->initialized = populate_jni_cache(env); + } + return cache->initialized; +} + +void convert_to_hex_addr(uint64_t addr, char *buffer) { + snprintf(buffer, kEMBSampleAddrLen, "0x%lx", (unsigned long) addr); +} + +static bool add_element_to_sample_list(JNIEnv *env, emb_sample *sample, + jobject frames, int index) { + bool success = false; + emb_sample_stackframe *frame = &(sample->stack[index]); + + // convert pointers to hex addresses. + char pc_buf[kEMBSampleAddrLen] = {0}; + char load_buf[kEMBSampleAddrLen] = {0}; + convert_to_hex_addr(frame->pc, pc_buf); + convert_to_hex_addr(frame->so_load_addr, load_buf); + + jstring pc = NULL; + jstring so_load_addr = NULL; + jstring so_path = NULL; + jobject result = NULL; + jobject obj = NULL; + + // create string for NativeThreadAnrStackframe#pc + pc = emb_jni_new_string_utf(env, pc_buf); + if (pc == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrStackframe#pc"); + goto exit; + } + + // create string for load addr + so_load_addr = emb_jni_new_string_utf(env, load_buf); + if (so_load_addr == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrStackframe#soLoadAddr"); + goto exit; + } + + // create string for SO path + so_path = emb_jni_new_string_utf(env, (char *) frame->so_path); + if (so_path == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrStackframe#soPath"); + goto exit; + } + + // create Integer for NativeThreadAnrStackframe#result + result = emb_jni_new_object(env, cache->clz_integer, cache->ctor_integer, (jint) frame->result); + if (result == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrStackframe#result"); + goto exit; + } + + // create NativeThreadAnrStackframe object + obj = emb_jni_new_object(env, cache->clz_native_thread_anr_stackframe, + cache->ctor_native_thread_anr_stackframe, + pc, so_load_addr, so_path, result); + + // add NativeStackframe to List + success = emb_jni_call_boolean_method(env, frames, cache->mthd_arraylist_add, obj); + + // perform explicit cleanup of JNI refs. + exit: + if (pc != NULL) { + emb_jni_delete_local_ref(env, pc); + } + if (so_load_addr != NULL) { + emb_jni_delete_local_ref(env, so_load_addr); + } + if (so_path != NULL) { + emb_jni_delete_local_ref(env, so_path); + } + if (result != NULL) { + emb_jni_delete_local_ref(env, result); + } + if (obj != NULL) { + emb_jni_delete_local_ref(env, obj); + } + return success; +} + +static jobject construct_sample_list(JNIEnv *env, emb_sample *sample) { + jint frame_count = (jint) sample->num_sframes; + + // create ArrayList instance with exact capacity + jobject frames = emb_jni_new_object(env, cache->clz_arraylist, cache->ctor_arraylist, + frame_count); + + if (frames == NULL) { + EMB_LOGDEV("Failed to instantiate ArrayList"); + return NULL; + } + + for (int k = 0; k < frame_count; k++) { // add NativeThreadAnrStackframe element to list + if (!add_element_to_sample_list(env, sample, frames, k)) { + EMB_LOGDEV("Failed to instantiate sample list"); + return NULL; + } + } + return frames; +} + +static jobject emb_serialize_sample(JNIEnv *env, emb_sample *sample) { + jobject result = NULL; + jobject timestamp = NULL; + jobject duration = NULL; + jobject frames = NULL; + jobject response = NULL; + + // create Integer for NativeThreadAnrSample#result + result = emb_jni_new_object(env, cache->clz_integer, cache->ctor_integer, + (jint) sample->result); + if (result == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrSample#result"); + goto exit; + } + + // create Long for NativeThreadAnrSample#sampleTimestamp + timestamp = emb_jni_new_object(env, cache->clz_long, cache->ctor_long, + (jlong) sample->timestamp_ms); + if (timestamp == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrSample#sampleTimestamp"); + goto exit; + } + + // create Long for NativeThreadAnrSample#sampleDurationMs + duration = emb_jni_new_object(env, cache->clz_long, cache->ctor_long, (jlong) sample->duration_ms); + if (duration == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrSample#sampleDurationMs"); + goto exit; + } + + // create List for NativeThreadAnrSample#stack + frames = construct_sample_list(env, sample); + if (frames == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrSample#stackframes"); + goto exit; + } + + // create NativeThreadAnrSample instance + response = emb_jni_new_object(env, cache->clz_native_thread_anr_sample, + cache->ctor_native_thread_anr_sample, result, timestamp, + duration, frames); + if (response == NULL) { + EMB_LOGDEV("Failed to instantiate NativeThreadAnrSample"); + goto exit; + } + + // perform explicit cleanup of JNI refs. + exit: + if (result != NULL) { + emb_jni_delete_local_ref(env, result); + } + if (timestamp != NULL) { + emb_jni_delete_local_ref(env, timestamp); + } + if (duration != NULL) { + emb_jni_delete_local_ref(env, duration); + } + if (frames != NULL) { + emb_jni_delete_local_ref(env, frames); + } + return response; +} + +static jobject construct_interval_list(JNIEnv *env, emb_interval *interval) { + jint sample_count = (jint) interval->num_samples; + EMB_LOGDEV("Serializing %d samples", sample_count); + + // create ArrayList instance with exact capacity + jobject samples = emb_jni_new_object(env, cache->clz_arraylist, cache->ctor_arraylist, + sample_count); + + if (samples == NULL) { + EMB_LOGDEV("Failed to instantiate ArrayList"); + return NULL; + } + + for (int k = 0; k < sample_count; k++) { // add NativeThreadAnrSample elements to list + jobject sample = emb_serialize_sample(env, &interval->samples[k]); + bool success = emb_jni_call_boolean_method(env, samples, cache->mthd_arraylist_add, sample); + + if (!success) { + EMB_LOGDEV("Failed to instantiate sample list"); + return NULL; + } + } + return samples; +} + +JNIEXPORT jobject JNICALL +Java_io_embrace_android_embracesdk_anr_ndk_NativeThreadSamplerNdkDelegate_finishSampling(JNIEnv *env, + jobject thiz) { + + // cancel any pending samples here. + emb_stop_thread_sampler(); + + // fetch the sample struct + emb_interval *interval = emb_current_interval(); + if (interval == NULL) { // nothing collected - return. + return NULL; + } + if (!init_jni_cache(env)) { // initialize cache of JNI refs if needed + EMB_LOGDEV("JNI cache failed to initialize."); + return NULL; + } + return construct_interval_list(env, interval); +} + +#ifdef __cplusplus +} +#endif diff --git a/embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.c b/embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.c new file mode 100644 index 0000000000..333637e1c6 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.c @@ -0,0 +1,118 @@ +#include +#include +#include "sampler_unwinder_unwind.h" +#include "../utilities.h" +#include "../emb_log.h" +#include "unwinder_dlinfo.h" + +// controls whether extra debug info is logged for development purposes +#define EMB_LOG_DLADDR_INFO false + +void convert_to_hex_addr(uint64_t addr, char *buffer); + +static void emb_log_debug_info(uint64_t ip, size_t index, const emb_sample_stackframe *frame, + const Dl_info *info, int result) { + char frame_buf[kEMBSampleAddrLen] = {0}; + char symbol_buf[kEMBSampleAddrLen] = {0}; + char load_buf[kEMBSampleAddrLen] = {0}; + + convert_to_hex_addr(frame->pc, frame_buf); + convert_to_hex_addr(frame->so_load_addr, load_buf); + + // now get the symbol address (if it was set) + if (info->dli_saddr != NULL && info->dli_sname != NULL) { + convert_to_hex_addr((uint64_t) info->dli_saddr, symbol_buf); + } + + if (result == 0) { + EMB_LOGINFO("Frame %d: the address %s could not be matched to a shared object.", + (int) index, frame_buf); + } else { + if (info->dli_saddr == NULL && info->dli_sname == NULL) { + uint64_t base_addr = ip - (uint64_t) info->dli_fbase; + convert_to_hex_addr(base_addr, symbol_buf); + + EMB_LOGINFO( + "Frame %d: the address was matched to a shared object, " + "but not a symbol within the shared object. so_path=%s, pc=%s, so_load_addr=%s, base_addr=%s", + (int) index, + frame->so_path, + frame_buf, + load_buf, + symbol_buf + ); + } else { + EMB_LOGINFO("Frame %d: %s %s, pc=%s, so_load_addr=%s, so_symbol_addr=%s", + (int) index, + frame->so_path, + info->dli_sname, + frame_buf, + load_buf, + symbol_buf + ); + } + } +} + +int emb_get_dlinfo_for_ip(uint64_t ip, size_t index, emb_sample_stackframe *frame) { + size_t size = sizeof(Dl_info); + Dl_info info = {0}; + memset(&info, 0, size); + + int result = dladdr((const void *) ip, &info); + + if (result != 0) { + // get the shared object load address + frame->so_load_addr = (uint64_t) (&info)->dli_fbase; + const char *path = (&info)->dli_fname; + + if (path != NULL) { + emb_strncpy((char *) frame->so_path, path, sizeof frame->so_path); + } + } + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "Simplify" + if (EMB_LOG_DLADDR_INFO) { + emb_log_debug_info(ip, index, frame, &info, result); + } +#pragma clang diagnostic pop + + return result; +} + +void emb_symbolicate_stacktrace(emb_sample *sample) { + for (int k = 0; k < sample->num_sframes; k++) { + emb_sample_stackframe *frame = &sample->stack[k]; + emb_get_dlinfo_for_ip(frame->pc, k, frame); + } +} + +static size_t calculate_frame_start_pos(const emb_unwind_state *unwind_state) { + size_t start = 0; + if (unwind_state->num_sframes > kEMBMaxSampleSFrames) { + start = unwind_state->num_sframes - kEMBMaxSampleSFrames; + } + return start; +} + +static size_t calculate_stacktrace_size(const emb_unwind_state *unwind_state) { + size_t size = unwind_state->num_sframes; + return size > kEMBMaxSampleSFrames ? kEMBMaxSampleSFrames : size; +} + +void emb_copy_frames(emb_sample *sample, const emb_unwind_state *unwind_state) { + sample->result = unwind_state->result; + size_t start = calculate_frame_start_pos(unwind_state); + sample->num_sframes = calculate_stacktrace_size(unwind_state); + + bool truncated = sample->num_sframes != unwind_state->num_sframes; + + if (truncated) { + sample->result = EMB_ERROR_TRUNCATED_STACKTRACE; + } + + for (size_t k = 0; k < sample->num_sframes; k++) { + sample->stack[k].pc = unwind_state->stack[start + k]; + } +} diff --git a/embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.h b/embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.h new file mode 100644 index 0000000000..8d06c3f621 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/sampler/unwinder_dlinfo.h @@ -0,0 +1,44 @@ +#ifndef EMBRACE_UNWINDER_DLINFO_H +#define EMBRACE_UNWINDER_DLINFO_H + +#include "sampler_unwinder_unwind.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint64_t stack[kEMBSampleUnwindLimit]; // unwind up to 256, then copy everything to other struct. + uint16_t num_sframes; + uint8_t result; +} emb_unwind_state; + +/** + * Uses dladdr to get information on the shared object load address, symbol address, and path. + * This information may not always be available. + * + * Returns the result from dladdr. + * See: https://man7.org/linux/man-pages/man3/dladdr.3.html + */ +int emb_get_dlinfo_for_ip(uint64_t ip, size_t index, emb_sample_stackframe *frame); + +/** + * Uses dladdr to get information on the shared object load address, symbol address, and path + * for the entire stacktrace. + * This information may not always be available. + * + * See: https://man7.org/linux/man-pages/man3/dladdr.3.html + */ +void emb_symbolicate_stacktrace(emb_sample *sample); + +/** + * Copies stackframes from the unwind_state to the emb_sample struct, applying limits + * on stacktrace size as necessary. The bottom-most frames will always be preferred + * (this leads to better flamegraph grouping for traces that exceed the max frame limit). + */ +void emb_copy_frames(emb_sample *sample, const emb_unwind_state *unwind_state); + +#ifdef __cplusplus +} +#endif +#endif //EMBRACE_UNWINDER_DLINFO_H diff --git a/embrace-android-sdk/src/main/cpp/signals/signal_utils.c b/embrace-android-sdk/src/main/cpp/signals/signal_utils.c new file mode 100644 index 0000000000..f38dc315dc --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/signals/signal_utils.c @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include +#include "signal_utils.h" +#include "../emb_log.h" + +#define EMB_ALT_STACK_SIZE SIGSTKSZ * 2 + +static char emb_alt_stack[EMB_ALT_STACK_SIZE] = {0}; + +bool emb_sig_stk_setup(stack_t stack) { + stack.ss_sp = &emb_alt_stack; + stack.ss_size = EMB_ALT_STACK_SIZE; + stack.ss_flags = 0; + if (sigaltstack(&stack, 0) < 0) { + EMB_LOGWARN("Sig Stack set failed: %s", strerror(errno)); + return false; + } + return true; +} + +void emb_trigger_prev_handler(int signum, siginfo_t *info, void *user_context, struct sigaction prev_handler) { + if (prev_handler.sa_flags & SA_SIGINFO) { + prev_handler.sa_sigaction(signum, info, user_context); + } else if (prev_handler.sa_handler == SIG_DFL) { + raise(signum); + } else if (prev_handler.sa_handler != SIG_IGN) { + void (*prev_func)(int) = prev_handler.sa_handler; + prev_func(signum); + } +} diff --git a/embrace-android-sdk/src/main/cpp/signals/signal_utils.h b/embrace-android-sdk/src/main/cpp/signals/signal_utils.h new file mode 100644 index 0000000000..bc22552b2e --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/signals/signal_utils.h @@ -0,0 +1,28 @@ +#ifndef EMBRACE_SIGNAL_UTILS_H +#define EMBRACE_SIGNAL_UTILS_H + +#include +#include + +/** + * Creates an alternate stack for use by a signal handler. + * https://man7.org/linux/man-pages/man2/sigaltstack.2.html + * + * @param stack a struct where the alternate stack will be stored + * @return true if stack setup was successful. + */ +bool emb_sig_stk_setup(stack_t stack); + +/** + * Invokes the previous signal handler if it was set. This respects SA_SIGINFO, SIG_DFL, + * and SIG_IGN. + * + * @param signum the signal number + * @param info the signal handler info + * @param user_context the signal handler context + * @param prev_handler the previous handler + */ +void emb_trigger_prev_handler(int signum, siginfo_t *info, void *user_context, + struct sigaction prev_handler); + +#endif //EMBRACE_SIGNAL_UTILS_H diff --git a/embrace-android-sdk/src/main/cpp/signals/signals_c.c b/embrace-android-sdk/src/main/cpp/signals/signals_c.c new file mode 100644 index 0000000000..f6cfb2ae72 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/signals/signals_c.c @@ -0,0 +1,234 @@ +// +// Created by Eric Lanz on 5/5/20. +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include "signals_c.h" +#include "../unwinders/unwinder.h" +#include "../utilities.h" +#include "../file_marker.h" +#include "../file_writer.h" +#include "signal_utils.h" +#include "../emb_log.h" + +#define EMB_SIG_HANDLER_COUNT 6 +#define EMB_TMP_BUF_SIZE 1024 + +struct emb_sig_handler_entry { + int signum; + char *sig_name; + char *sig_msg; + struct sigaction action; + struct sigaction prev_action; +}; + +struct emb_sig_handler_entry handler_entries[EMB_SIG_HANDLER_COUNT] = { + {SIGILL, "SIGILL", "Illegal instruction", {0}, {0}}, + {SIGTRAP, "SIGTRAP", "Trace/breakpoint trap", {0}, {0}}, + {SIGABRT, "SIGABRT", "Abort program", {0}, {0}}, + {SIGBUS, "SIGBUS", "Memory error", {0}, {0}}, + {SIGFPE, "SIGFPE", "FP exception", {0}, {0}}, + {SIGSEGV, "SIGSEGV", "Segmentation fault", {0}, {0}}, +}; + +// ref to global state +static volatile emb_env *_emb_env = NULL; + +// unwind stack +static stack_t emb_sig_stack = {0}; + +static struct emb_sig_handler_entry *find_handler_by_signum(const int signum) { + for (int k = 0; k < EMB_SIG_HANDLER_COUNT; ++k) { + if (signum == handler_entries[k].signum) { + return &handler_entries[k]; + } + } + return NULL; +} + +void emb_remove_c_sig_handlers() { + if (!_emb_env) { + return; + } + for (int k = 0; k < EMB_SIG_HANDLER_COUNT; k++) { + struct emb_sig_handler_entry *entry = &handler_entries[k]; + sigaction(entry->signum, &entry->prev_action, NULL); + } +} + +static inline void invoke_prev_sigaction(int signum, siginfo_t *info, + void *user_context) __asyncsafe { + emb_remove_c_sig_handlers(); + struct emb_sig_handler_entry *entry = find_handler_by_signum(signum); + if (entry != NULL && _emb_env != NULL) { + _emb_env = NULL; + emb_trigger_prev_handler(signum, info, user_context, entry->prev_action); + } + _emb_env = NULL; +} + +void emb_handle_signal(int signum, siginfo_t *info, void *user_context) __asyncsafe { + emb_env *env = (emb_env *) _emb_env; + if (env == NULL) { + emb_log_last_error(env, EMB_ERROR_C_SIGNAL_HANDLER_NOT_INSTALLED, 0); + return; + } + if (env->currently_handling) { + if (env->already_handled_crash) { + invoke_prev_sigaction(signum, info, user_context); + } + return; + } + env->currently_handling = true; + + emb_set_crash_time(env); + + env->crash.unhandled = true; + env->crash.sig_code = info->si_code; + env->crash.sig_errno = info->si_errno; + env->crash.sig_no = info->si_signo; + env->crash.fault_addr = (uintptr_t) info->si_addr; + env->crash.unhandled_count++; + env->crash.capture.num_sframes = emb_process_capture(env, info, user_context); + + struct emb_sig_handler_entry *entry = find_handler_by_signum(signum); + + if (entry != NULL) { + emb_strncpy(env->crash.capture.name, + entry->sig_name, + sizeof(env->crash.capture.name)); + emb_strncpy(env->crash.capture.message, + entry->sig_msg, + sizeof(env->crash.capture.message)); + } + + emb_write_crash_to_file(env); + + // Used to determine during the next launch if we crashed on the previous launch. + emb_write_crash_marker_file(env, CRASH_MARKER_SOURCE_SIGNAL); + + if (env->err_fd > 0) { + close(env->err_fd); + } + invoke_prev_sigaction(signum, info, user_context); +} + +static void retrieve_symbol_info(char *buf, Dl_info *info, int result) { + if (result != 0) { + if ((info)->dli_sname != NULL) { + snprintf(buf, EMB_TMP_BUF_SIZE, "%s (%s)", (info)->dli_sname, (info)->dli_fname); + } else { + snprintf(buf, EMB_TMP_BUF_SIZE, "%s", (info)->dli_fname); + } + } else { + snprintf(buf, EMB_TMP_BUF_SIZE, "%s", "Unknown"); + } +} + +static void gen_sig_handler_override_msg(char *buffer, const size_t buffer_size, const unsigned long ptr, + bool overrides[EMB_SIG_HANDLER_COUNT]) { + // try and get the symbol symbol_info of the external handler via dladdr + Dl_info info = {0}; + int result = dladdr((const void *) ptr, &info); + char buf[EMB_TMP_BUF_SIZE]; + retrieve_symbol_info(buf, &info, result); + + snprintf(buffer, buffer_size, "%s - SIGILL=%d, SIGTRAP=%d, SIGABRT=%d, SIGBUS=%d, " + "SIGFPE=%d, SIGSEGV=%d", buf, overrides[0], overrides[1], + overrides[2], overrides[3], overrides[4], overrides[5]); +} + +bool emb_check_for_overwritten_handlers(char *buffer, const size_t buffer_size) { + if (!_emb_env) { + return false; + } + + void *ptr = NULL; + bool result = false; + struct sigaction _handler = {0}; + struct sigaction *handler = &_handler; + bool overrides[EMB_SIG_HANDLER_COUNT] = {0}; + + for (int k = 0; k < EMB_SIG_HANDLER_COUNT; k++) { + // get the current handler without altering it + struct emb_sig_handler_entry *entry = &handler_entries[k]; + const int signal = entry->signum; + int code = sigaction(signal, NULL, handler); + + if (code != 0) { // something went wrong, bomb out. + EMB_LOGWARN("Failed to check for overwritten handler for signal %d, code=%d", signal, code); + result = false; + break; + } + + // get pointer to function supplied to either sigaction() or signal() + if (handler->sa_flags & SA_SIGINFO) { + ptr = handler->sa_sigaction; + } else { + ptr = handler->sa_handler; + } + + // Someone overwrote us for this signal. Log a warning that shows the culprit + if (ptr != NULL && ptr != &emb_handle_signal) { + overrides[k] = true; + result = true; + } + } + + if (result) { // generate a message. for now, assume that the handler is the same for all. + gen_sig_handler_override_msg(buffer, buffer_size, (unsigned long) ptr, overrides); + } + return result; +} + +bool emb_install_signal_handlers(bool reinstall) { + if (!emb_sig_stk_setup(emb_sig_stack)) { + return false; + } + for (int k = 0; k < EMB_SIG_HANDLER_COUNT; k++) { + struct emb_sig_handler_entry *entry = &handler_entries[k]; + struct sigaction *action = &entry->action; + + // prepare for sigaction + sigemptyset(&action->sa_mask); + action->sa_sigaction = emb_handle_signal; + action->sa_flags = SA_SIGINFO | SA_ONSTACK; + + // only store the original handler from when we were first installed. + // this avoids the possibility of the overwritten handler calling us back + // and triggering a hang + struct sigaction *old_action = NULL; + if (!reinstall) { + old_action = &entry->prev_action; + } + int success = sigaction(entry->signum, action, old_action); + if (success != 0) { + EMB_LOGWARN("Sig install failed: %s", strerror(errno)); + return false; + } + } + return true; +} + +bool emb_setup_c_signal_handlers(emb_env *env) { + bool result = true; + static pthread_mutex_t _emb_signal_mutex = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&_emb_signal_mutex); + + if (_emb_env) { + EMB_LOGINFO("c handler already installed."); + } else { + bool reinstall = _emb_env != NULL; + _emb_env = env; + result = emb_install_signal_handlers(reinstall); + } + pthread_mutex_unlock(&_emb_signal_mutex); + return result; +} diff --git a/embrace-android-sdk/src/main/cpp/signals/signals_c.h b/embrace-android-sdk/src/main/cpp/signals/signals_c.h new file mode 100644 index 0000000000..e0ecced0c2 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/signals/signals_c.h @@ -0,0 +1,29 @@ +// +// Created by Eric Lanz on 5/5/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_SIGNALS_C_H +#define EMBRACE_NATIVE_CRASHES_SIGNALS_C_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "../emb_ndk_manager.h" + +bool emb_setup_c_signal_handlers(emb_env* env); +void emb_remove_c_sig_handlers(); + +/** + * This function should only be called _after_ signal handlers have been installed by Embrace. + * It copies to the string buffer if any signal handler does not match the embrace signal handler + * (i.e it has been overwritten by some other SDK). It will also return true in this case. + */ +bool emb_check_for_overwritten_handlers(char *buffer, const size_t buffer_size); + +#ifdef __cplusplus +} +#endif + +#endif //EMBRACE_NATIVE_CRASHES_SIGNALS_C_H diff --git a/embrace-android-sdk/src/main/cpp/signals/signals_cpp.cpp b/embrace-android-sdk/src/main/cpp/signals/signals_cpp.cpp new file mode 100644 index 0000000000..c7bfb7bfac --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/signals/signals_cpp.cpp @@ -0,0 +1,141 @@ +// +// Created by Eric Lanz on 5/5/20. +// + +#include "signals_cpp.h" +#include +#include +#include +#include +#include +#include +#include "../utilities.h" +#include "../unwinders/unwinder.h" +#include "../file_marker.h" +#include "../file_writer.h" +#include "../emb_log.h" + +void emb_termination_handler(); + +std::terminate_handler emb_prev_handler = nullptr; +static emb_env *_emb_env = nullptr; + +void emb_fake_crash() { + throw std::overflow_error("fake Embrace crash"); +} + +void install_cpp_signal_handler() { + static pthread_mutex_t _emb_signal_mutex = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&_emb_signal_mutex); + + // avoid setting the same termination handler twice in a row + if (std::get_terminate() != emb_termination_handler) { + std::terminate_handler old_handler = std::set_terminate(emb_termination_handler); + + // only store the original handler from when we were first installed. + // this avoids the possibility of the overwritten handler calling us back + // and triggering a hang + if (emb_prev_handler == nullptr) { + emb_prev_handler = old_handler; + } + } + + pthread_mutex_unlock(&_emb_signal_mutex); +} + +bool emb_setup_cpp_sig_handler(emb_env *env) { + _emb_env = env; + install_cpp_signal_handler(); + return true; +} + +void emb_remove_cpp_sig_handler() { + if (_emb_env == nullptr) { + return; + } + std::set_terminate(emb_prev_handler); + _emb_env = nullptr; +} + +// This is a way to pre-populate actual exception messages from the runtime +void emb_parse_exception_message(char *exc_msg, size_t length) { + try { + throw; + } catch (std::exception &exc) { + emb_strncpy(exc_msg, (char *) exc.what(), length); + } catch (std::exception *exc) { + emb_strncpy(exc_msg, (char *) exc->what(), length); + } catch (std::string obj) { + emb_strncpy(exc_msg, (char *) obj.c_str(), length); + } catch (char *obj) { + snprintf(exc_msg, length, "%s", obj); + } catch (char obj) { + snprintf(exc_msg, length, "%c", obj); + } catch (short obj) { + snprintf(exc_msg, length, "%d", obj); + } catch (int obj) { + snprintf(exc_msg, length, "%d", obj); + } catch (long obj) { + snprintf(exc_msg, length, "%ld", obj); + } catch (long long obj) { + snprintf(exc_msg, length, "%lld", obj); + } catch (long double obj) { + snprintf(exc_msg, length, "%Lf", obj); + } catch (double obj) { + snprintf(exc_msg, length, "%f", obj); + } catch (float obj) { + snprintf(exc_msg, length, "%f", obj); + } catch (unsigned char obj) { + snprintf(exc_msg, length, "%u", obj); + } catch (unsigned short obj) { + snprintf(exc_msg, length, "%u", obj); + } catch (unsigned int obj) { + snprintf(exc_msg, length, "%u", obj); + } catch (unsigned long obj) { + snprintf(exc_msg, length, "%lu", obj); + } catch (unsigned long long obj) { + snprintf(exc_msg, length, "%llu", obj); + } catch (...) { + // unknown + } +} + +void emb_termination_handler() { + if (_emb_env == nullptr || _emb_env->currently_handling) { + return; + } + + emb_set_crash_time(_emb_env); + + _emb_env->currently_handling = true; + _emb_env->crash.unhandled = true; + _emb_env->crash.unhandled_count++; + _emb_env->crash.capture.num_sframes = emb_process_capture(_emb_env, nullptr, nullptr); + + std::type_info *type_info = __cxxabiv1::__cxa_current_exception_type(); + if (type_info != nullptr) { + emb_strncpy(_emb_env->crash.capture.name, + (char *) type_info->name(), + sizeof(_emb_env->crash.capture.name)); + } + size_t msg_len = sizeof(_emb_env->crash.capture.message); + char msg[msg_len]; + emb_parse_exception_message(msg, msg_len); + emb_strncpy(_emb_env->crash.capture.message, (char *) msg, + sizeof(_emb_env->crash.capture.message)); + + emb_write_crash_to_file(_emb_env); + _emb_env->already_handled_crash = true; + + // Used to determine during the next launch if we crashed on the previous launch. + emb_write_crash_marker_file(_emb_env, CRASH_MARKER_SOURCE_CPP_EXCEPTION); + + if (_emb_env->err_fd > 0) { + close(_emb_env->err_fd); + } + + emb_remove_cpp_sig_handler(); + if (emb_prev_handler != nullptr) { + emb_prev_handler(); + } +} diff --git a/embrace-android-sdk/src/main/cpp/signals/signals_cpp.h b/embrace-android-sdk/src/main/cpp/signals/signals_cpp.h new file mode 100644 index 0000000000..1974dc8328 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/signals/signals_cpp.h @@ -0,0 +1,23 @@ +// +// Created by Eric Lanz on 5/5/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_SIGNALS_CPP_H +#define EMBRACE_NATIVE_CRASHES_SIGNALS_CPP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "../emb_ndk_manager.h" + +bool emb_setup_cpp_sig_handler(emb_env *env); +void emb_remove_cpp_sig_handler(void); + +void emb_fake_crash(void); + +#ifdef __cplusplus +} +#endif + +#endif //EMBRACE_NATIVE_CRASHES_SIGNALS_CPP_H diff --git a/embrace-android-sdk/src/main/cpp/stack_frames.h b/embrace-android-sdk/src/main/cpp/stack_frames.h new file mode 100644 index 0000000000..f5d7707b06 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/stack_frames.h @@ -0,0 +1,104 @@ +// +// Created by Eric Lanz on 5/12/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_STACK_FRAMES_H +#define EMBRACE_NATIVE_CRASHES_STACK_FRAMES_H + +#include +#include + +#ifndef kEMBSamplePathMaxLen +#define kEMBSamplePathMaxLen 256 +#endif + +#ifndef kEMBSampleAddrLen +#define kEMBSampleAddrLen 32 +#endif + +#ifndef kEMBMaxSFrames +#define kEMBMaxSFrames 100 +#endif + +#ifndef kEMBSampleUnwindLimit +/** + * The number of frames that can be unwound in a sample. + */ +#define kEMBSampleUnwindLimit 256 +#endif + +#ifndef kEMBMaxSampleSFrames +/** + * The number of frames that can be serialized in a sample. + */ +#define kEMBMaxSampleSFrames 100 +#endif + +#ifndef kEMBMaxSamples +#define kEMBMaxSamples 10 +#endif + +#ifndef kEMBMaxExceptionNameSize +#define kEMBMaxExceptionNameSize 64 +#endif + +#ifndef kEMBMaxExceptionMessageSize +#define kEMBMaxExceptionMessageSize 256 +#endif + +#ifndef EMB_APP_DATA_SIZE +#define EMB_APP_DATA_SIZE 128 +#endif + +#ifndef EMB_DEVICE_META_DATA_SIZE +#define EMB_DEVICE_META_DATA_SIZE 2048 +#endif + +#ifndef EMB_REPORT_ID_SIZE +#define EMB_REPORT_ID_SIZE 256 +#endif + +#ifndef EMB_SESSION_ID_SIZE +#define EMB_SESSION_ID_SIZE 256 +#endif + +#ifndef EMB_PATH_SIZE +#define EMB_PATH_SIZE 512 +#endif + +typedef struct { + char filename[256]; + char method[256]; + + uintptr_t frame_addr; + uintptr_t offset_addr; + uintptr_t module_addr; + uintptr_t line_num; +} emb_sframe; + +typedef struct { + char name[kEMBMaxExceptionNameSize]; + char message[kEMBMaxExceptionMessageSize]; + + ssize_t num_sframes; + emb_sframe stacktrace[kEMBMaxSFrames]; +} emb_exception; + +typedef struct { + emb_exception capture; + bool unhandled; + int unhandled_count; + char session_id[EMB_SESSION_ID_SIZE]; + char report_id[EMB_REPORT_ID_SIZE]; + char meta_data[EMB_DEVICE_META_DATA_SIZE]; + char app_state[EMB_APP_DATA_SIZE]; + int64_t crash_ts; + int64_t start_ts; + int sig_code; + int sig_no; + int sig_errno; + uintptr_t fault_addr; + uint8_t unwinder_error; +} emb_crash; + +#endif //EMBRACE_NATIVE_CRASHES_STACK_FRAMES_H diff --git a/embrace-android-sdk/src/main/cpp/unwinders/unwinder.c b/embrace-android-sdk/src/main/cpp/unwinders/unwinder.c new file mode 100644 index 0000000000..e4feb1e133 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/unwinders/unwinder.c @@ -0,0 +1,38 @@ +// +// Created by Eric Lanz on 5/5/20. +// + +#include "unwinder.h" +#include "unwinder_stack.h" +#include "../utilities.h" +#include "../emb_log.h" +#include + +void emb_fix_fileinfo(ssize_t frame_count, + emb_sframe stacktrace[kEMBMaxSFrames]) { + static Dl_info info; + for (int i = 0; i < frame_count; ++i) { + if (dladdr((void *)stacktrace[i].frame_addr, &info) != 0) { + stacktrace[i].module_addr = (uintptr_t)info.dli_fbase; + stacktrace[i].offset_addr = (uintptr_t)info.dli_saddr; + stacktrace[i].line_num = + stacktrace[i].frame_addr - stacktrace[i].module_addr; + if (info.dli_fname != NULL) { + emb_strncpy(stacktrace[i].filename, (char *)info.dli_fname, sizeof(stacktrace[i].filename)); + } + if (info.dli_sname != NULL) { + emb_strncpy(stacktrace[i].method, (char *)info.dli_sname, sizeof(stacktrace[i].method)); + } + } + } +} + +ssize_t emb_process_capture(emb_env *env, siginfo_t *info, void *user_context) { + ssize_t frame_count; + + frame_count = emb_process_stack(env, info, user_context); + + emb_fix_fileinfo(frame_count, env->crash.capture.stacktrace); + + return frame_count; +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/cpp/unwinders/unwinder.h b/embrace-android-sdk/src/main/cpp/unwinders/unwinder.h new file mode 100644 index 0000000000..50f3704840 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/unwinders/unwinder.h @@ -0,0 +1,24 @@ +// +// Created by Eric Lanz on 5/5/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_UNWINDER_H +#define EMBRACE_NATIVE_CRASHES_UNWINDER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include "../emb_ndk_manager.h" +#include "../stack_frames.h" + +ssize_t emb_process_capture(emb_env *env, siginfo_t *info, void *user_context); + +#ifdef __cplusplus +} +#endif + +#endif //EMBRACE_NATIVE_CRASHES_UNWINDER_H diff --git a/embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.cpp b/embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.cpp new file mode 100644 index 0000000000..0be7de6907 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.cpp @@ -0,0 +1,38 @@ +// +// Created by Eric Lanz on 5/12/20. +// + +#include "string.h" +#include "unwinder_stack.h" +#include +#include +#include "../utilities.h" +#include "../3rdparty/libunwindstack-ndk/MemoryLocal.h" +#include "unwindstack/AndroidUnwinder.h" + +ssize_t +emb_process_stack(emb_env *env, siginfo_t *info, void *user_context) { + if (user_context == NULL) { + emb_log_last_error(env, EMB_ERROR_NO_USER_CONTEXT, 0); + return 0; + } + + unwindstack::AndroidUnwinder *unwinder = unwindstack::AndroidUnwinder::Create(getpid()); + unwindstack::AndroidUnwinderData android_unwinder_data = unwindstack::AndroidUnwinderData(); + emb_sframe *stacktrace = env->crash.capture.stacktrace; + + bool unwindSuccessful = unwinder->Unwind(user_context, android_unwinder_data); + + env->crash.unwinder_error = android_unwinder_data.error.code; + + if (unwindSuccessful) { + int i = 0; + for (const auto &frame: android_unwinder_data.frames) { + stacktrace[i++].frame_addr = frame.pc; + } + } else { + return 0; + } + return static_cast(android_unwinder_data.frames.size()); + +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.h b/embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.h new file mode 100644 index 0000000000..798c824f9b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/unwinders/unwinder_stack.h @@ -0,0 +1,16 @@ +// +// Created by Eric Lanz on 5/12/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_UNWINDER_STACK_H +#define EMBRACE_NATIVE_CRASHES_UNWINDER_STACK_H + +#include "../emb_ndk_manager.h" +#include +#ifdef __cplusplus +extern "C" +#endif +ssize_t +emb_process_stack(emb_env *env, siginfo_t *info, void *user_context); + +#endif //EMBRACE_NATIVE_CRASHES_UNWINDER_STACK_H diff --git a/embrace-android-sdk/src/main/cpp/utilities.c b/embrace-android-sdk/src/main/cpp/utilities.c new file mode 100644 index 0000000000..1ac5f78732 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/utilities.c @@ -0,0 +1,100 @@ +// +// Created by Eric Lanz on 5/12/20. +// + +#include +#include +#include +#include +#include +#include "inttypes.h" +#include "utilities.h" +#include "emb_log.h" + +void emb_strncpy(char *dst, const char *src, size_t len) { + int i = 0; + while (i <= len) { + char current = src[i]; + dst[i] = current; + if (current == '\0') { + break; + } + i++; + } +} + +void emb_set_report_paths(emb_env *env, const char *session_id) { + snprintf(env->report_path, EMB_PATH_SIZE, "%s/emb_ndk.%s.%s.%" PRId64 ".crash", env->base_path, + CRASH_REPORT_CURRENT_VERSION, session_id, env->crash.start_ts); + EMB_LOGINFO("report path: %s", env->report_path); + snprintf(env->error_path, EMB_PATH_SIZE, "%s/emb_ndk.%s.%s.%" PRId64 ".error", env->base_path, + CRASH_REPORT_CURRENT_VERSION, session_id, env->crash.start_ts); + EMB_LOGINFO("error path: %s", env->error_path); + snprintf(env->map_path, EMB_PATH_SIZE, "%s/emb_ndk.%s.%s.%" PRId64 ".map", env->base_path, + CRASH_REPORT_CURRENT_VERSION, session_id, env->crash.start_ts); + EMB_LOGINFO("map path: %s", env->map_path); + snprintf(env->map_src_path, MAP_SRC_PATH_SIZE, "/proc/%d/maps", getpid()); +} + +int emb_dump_map(emb_env *env) { + int fd_in = open(env->map_src_path, O_RDONLY | O_CLOEXEC); + if (fd_in == -1) { + return -1; + } + + int fd_out = open(env->map_path, O_WRONLY | O_CREAT, 0644); + if (fd_out == -1) { + close(fd_in); + return -2; + } + + char buf[1024]; + int size; + int rv = 0; + + while (true) { + size = read(fd_in, &buf, sizeof(buf)); + if (size == 0) { + break; + } + if (size < 0) { + rv = -3; + goto cleanup; + } + write(fd_out, &buf, size); + } + + cleanup: + + close(fd_in); + close(fd_out); + + return rv; +} + + +void emb_set_crash_time(emb_env *env) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + // get timestamp in millis + env->crash.crash_ts = ((int64_t) ts.tv_sec * 1000) + ((int64_t) ts.tv_nsec / 1000000); +} + +void emb_log_last_error(emb_env *env, int num, int context) { + if (env == NULL) { + return; + } + if (env->errors_captured >= EMB_MAX_ERRORS) { + return; + } + + if (!env->err_fd) { + env->err_fd = open(env->error_path, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (env->err_fd <= 0) { + return; + } + } + env->last_error.context = context; + env->last_error.num = num; + write(env->err_fd, &env->last_error, sizeof(emb_error)); +} diff --git a/embrace-android-sdk/src/main/cpp/utilities.h b/embrace-android-sdk/src/main/cpp/utilities.h new file mode 100644 index 0000000000..1849d1042a --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/utilities.h @@ -0,0 +1,53 @@ +// +// Created by Eric Lanz on 5/12/20. +// + +#ifndef EMBRACE_NATIVE_CRASHES_UTILITIES_H +#define EMBRACE_NATIVE_CRASHES_UTILITIES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "emb_ndk_manager.h" + +#define EMB_ERROR_C_SIGNAL_HANDLER_NOT_INSTALLED 1 +#define EMB_ERROR_FAILED_TO_OPEN_CRASH_FILE 2 +#define EMB_ERROR_NO_USER_CONTEXT 3 +#define EMB_ERROR_MAP_PARSE_FAILED 4 +#define EMB_ERROR_NO_UNWIND_STATE_DEFINED 5 +#define EMB_ERROR_MAP_NOT_FOUND 6 +#define EMB_ERROR_ELF_NOT_FOUND 7 +#define EMB_ERROR_UNWIND_STACK_FAILURE 8 +#define EMB_UNKNOWN_UNWIND_TYPE 9 +#define EMB_UNSUPPORTED_UNWIND_ARCH 10 +#define EMB_UNWIND_INFINITE_LOOP 11 +#define EMB_SAMPLE_DATA_RACE 12 +#define EMB_ERROR_ELF_STEP_FAILED 13 +#define EMB_ERROR_ENV_TERMINATING 14 +#define EMB_ERROR_SAMPLE_IN_PROGRESS 15 +#define EMB_ERROR_TARGET_THREAD_NULL 16 +#define EMB_ERROR_SIGUSR2_FAILED 17 +#define EMB_ERROR_UNW_CONTEXT_FAILED 18 +#define EMB_ERROR_UNW_INIT_LOCAL_FAILED 19 +#define EMB_ERROR_NOT_INSTALLED 20 +#define EMB_ERROR_TIMER_FAILED 21 +#define EMB_ERROR_TRUNCATED_STACKTRACE 22 + +// the backend can handle error codes between 0-255. + +#define EMB_MAX_ERRORS 10 + +int emb_dump_map(emb_env *env); +void emb_log_last_error(emb_env *env, int num, int context); +void emb_strncpy(char *dst, const char *src, size_t len); +void emb_set_crash_time(emb_env *env); +void emb_set_report_paths(emb_env *env, const char *session_id); + + +#ifdef __cplusplus +} +#endif + +#endif //EMBRACE_NATIVE_CRASHES_UTILITIES_H diff --git a/embrace-android-sdk/src/main/cpp/utils/system_clock.c b/embrace-android-sdk/src/main/cpp/utils/system_clock.c new file mode 100644 index 0000000000..cb0c4ec8c4 --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/utils/system_clock.c @@ -0,0 +1,45 @@ +// +// Created by Jamie Lynch on 14/10/2022. +// + +#include +#include +#include "system_clock.h" + +#define SECONDS_TO_MS 1000 +#define NANOS_TO_MS 1000000 + +static volatile int64_t baseline_ms = -1; + +static int64_t get_time_impl(clockid_t clock) { + struct timespec time = {0}; + if (clock_gettime(clock, &time) != 0) { + return -1; + } + int64_t seconds = time.tv_sec; + int64_t nano_seconds = time.tv_nsec; + return (seconds * SECONDS_TO_MS) + (nano_seconds / NANOS_TO_MS); +} + +/** + * We want to report the time in milliseconds but using the monotonic clock as we are + * timing intervals. This creates a baseline ms that is added to any future monotonic + * time - effectively converting it to realtime but without the downsides. + */ +static void initialize_baseline_ms() { + int64_t realtime_ms = get_time_impl(CLOCK_REALTIME); + int64_t monotonic_ms = get_time_impl(CLOCK_MONOTONIC); + baseline_ms = realtime_ms - monotonic_ms; +} + +int64_t emb_get_time_ms() { + if (baseline_ms == -1) { + initialize_baseline_ms(); + } + int64_t now = get_time_impl(CLOCK_MONOTONIC); + + if (now == -1 || baseline_ms == -1) { + return -1; + } + return baseline_ms + now; +} diff --git a/embrace-android-sdk/src/main/cpp/utils/system_clock.h b/embrace-android-sdk/src/main/cpp/utils/system_clock.h new file mode 100644 index 0000000000..eed09b483b --- /dev/null +++ b/embrace-android-sdk/src/main/cpp/utils/system_clock.h @@ -0,0 +1,9 @@ +#ifndef EMBRACE_SYSTEM_CLOCK_H +#define EMBRACE_SYSTEM_CLOCK_H + +/** + * Gets the current monotonic time in milliseconds. + */ +int64_t emb_get_time_ms(); + +#endif //EMBRACE_SYSTEM_CLOCK_H diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/BetaApi.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/BetaApi.kt new file mode 100644 index 0000000000..3343f81a65 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/BetaApi.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk + +/** + * Classes and methods marked with this annotation are part of Embrace's Beta API that will only function if the app using it is + * enrolled in the related feature's Beta program. Even then, they are not guaranteed to be supported going forward, and any changes to + * them that break source compatibility can be made in future SDK versions without prior warning. + * + * If you are interested in trying out any Beta APIs, please contact your Embrace representative or reach out directly through the website. + */ +public annotation class BetaApi diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java new file mode 100644 index 0000000000..0cb59d5f59 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java @@ -0,0 +1,787 @@ +package io.embrace.android.embracesdk; + +import static io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.logger; + +import android.content.Context; +import android.util.Pair; +import android.webkit.ConsoleMessage; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +import io.embrace.android.embracesdk.config.ConfigService; +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb; +import io.embrace.android.embracesdk.spans.EmbraceSpan; +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent; +import io.embrace.android.embracesdk.spans.ErrorCode; +import kotlin.jvm.functions.Function0; + +/** + * Entry point for the SDK. This class is part of the Embrace Public API. + *

+ * Contains a singleton instance of itself, and is used for initializing the SDK. + */ +@SuppressWarnings("unused") +public final class Embrace implements EmbraceAndroidApi { + + /** + * Singleton instance of the Embrace SDK. + */ + private static final Embrace embrace = new Embrace(); + private static EmbraceImpl impl = new EmbraceImpl(); + + @NonNull + private final InternalEmbraceLogger internalEmbraceLogger = InternalStaticEmbraceLogger.logger; + + static final String NULL_PARAMETER_ERROR_MESSAGE_TEMPLATE = " cannot be invoked because it contains null parameters"; + + Embrace() { + } + + /** + * Gets the singleton instance of the Embrace SDK. + * + * @return the instance of the Embrace SDK + */ + @NonNull + public static Embrace getInstance() { + return embrace; + } + + /** + * Gets the EmbraceImpl instance. This provides access to internally visible functions + * intended for use in the Android SDK only + */ + @NonNull + static EmbraceImpl getImpl() { + return impl; + } + + static void setImpl(@Nullable EmbraceImpl instance) { + impl = instance; + } + + @Override + public void start(@NonNull Context context) { + if (verifyNonNullParameters("start", context)) { + start(context, true, AppFramework.NATIVE); + } + } + + @Override + public void start(@NonNull Context context, boolean enableIntegrationTesting) { + if (verifyNonNullParameters("start", context)) { + start(context, enableIntegrationTesting, AppFramework.NATIVE); + } + } + + @Override + public void start(@NonNull Context context, boolean enableIntegrationTesting, @NonNull AppFramework appFramework) { + if (verifyNonNullParameters("start", context, appFramework)) { + impl.start(context, enableIntegrationTesting, appFramework); + } + } + + @Override + public boolean isStarted() { + return impl.isStarted(); + } + + @Override + public boolean setAppId(@NonNull String appId) { + if (verifyNonNullParameters("setAppId", appId)) { + return impl.setAppId(appId); + } else { + return false; + } + } + + @Override + public void setUserIdentifier(@Nullable String userId) { + impl.setUserIdentifier(userId); + } + + @Override + public void clearUserIdentifier() { + impl.clearUserIdentifier(); + } + + @Override + public void setUserEmail(@Nullable String email) { + impl.setUserEmail(email); + } + + @Override + public void clearUserEmail() { + impl.clearUserEmail(); + } + + @Override + public void setUserAsPayer() { + impl.setUserAsPayer(); + } + + @Override + public void clearUserAsPayer() { + impl.clearUserAsPayer(); + } + + @Override + public void addUserPersona(@NonNull String persona) { + if (verifyNonNullParameters("addUserPersona", persona)) { + impl.addUserPersona(persona); + } + } + + @Override + public void clearUserPersona(@NonNull String persona) { + if (verifyNonNullParameters("clearUserPersona", persona)) { + impl.clearUserPersona(persona); + } + } + + @Override + public void clearAllUserPersonas() { + impl.clearAllUserPersonas(); + } + + @Override + public boolean addSessionProperty(@NonNull String key, @NonNull String value, boolean permanent) { + if (verifyNonNullParameters("addSessionProperty", key, value)) { + return impl.addSessionProperty(key, value, permanent); + } + + return false; + } + + @Override + public boolean removeSessionProperty(@NonNull String key) { + if (verifyNonNullParameters("removeSessionProperty", key)) { + return impl.removeSessionProperty(key); + } + + return false; + } + + @Override + @Nullable + public Map getSessionProperties() { + return impl.getSessionProperties(); + } + + @Override + public void setUsername(@Nullable String username) { + impl.setUsername(username); + } + + @Override + public void clearUsername() { + impl.clearUsername(); + } + + @Override + public void startMoment(@NonNull String name) { + if (verifyNonNullParameters("startMoment", name)) { + startMoment(name, null); + } + } + + @Override + public void startMoment(@NonNull String name, @Nullable String identifier) { + if (verifyNonNullParameters("startMoment", name)) { + startMoment(name, identifier, null); + } + } + + @Override + public void startMoment(@NonNull String name, + @Nullable String identifier, + @Nullable Map properties) { + if (verifyNonNullParameters("startMoment", name)) { + impl.startMoment(name, identifier, properties); + } + } + + @Override + public void endMoment(@NonNull String name) { + if (verifyNonNullParameters("endMoment", name)) { + endMoment(name, null, null); + } + } + + @Override + public void endMoment(@NonNull String name, @Nullable String identifier) { + if (verifyNonNullParameters("endMoment", name)) { + endMoment(name, identifier, null); + } + } + + @Override + public void endMoment(@NonNull String name, @Nullable Map properties) { + if (verifyNonNullParameters("endMoment", name)) { + endMoment(name, null, properties); + } + } + + @Override + public void endMoment(@NonNull String name, @Nullable String identifier, @Nullable Map properties) { + if (verifyNonNullParameters("endMoment", name)) { + impl.endMoment(name, identifier, properties); + } + } + + @Override + public void endAppStartup() { + impl.endAppStartup(null); + } + + @Override + public void endAppStartup(@NonNull Map properties) { + if (verifyNonNullParameters("endAppStartup", properties)) { + impl.endAppStartup(properties); + } + } + + @Override + @NonNull + public String getTraceIdHeader() { + return impl.getTraceIdHeader(); + } + + @NonNull + @Override + public String generateW3cTraceparent() { + return impl.generateW3cTraceparent(); + } + + @Override + public void recordNetworkRequest(@NonNull EmbraceNetworkRequest networkRequest) { + if (verifyNonNullParameters("recordNetworkRequest", networkRequest)) { + impl.recordNetworkRequest(networkRequest); + } + } + + @Override + public void logInfo(@NonNull String message) { + if (verifyNonNullParameters("logInfo", message)) { + logMessage(message, Severity.INFO); + } + } + + @Override + public void logWarning(@NonNull String message) { + if (verifyNonNullParameters("logWarning", message)) { + logMessage(message, Severity.WARNING); + } + } + + @Override + public void logError(@NonNull String message) { + if (verifyNonNullParameters("logError", message)) { + logMessage(message, Severity.ERROR); + } + } + + /** + * Logs a React Native Redux Action. + */ + public void logRnAction(@NonNull String name, long startTime, long endTime, + @NonNull Map properties, int bytesSent, @NonNull String output) { + if (verifyNonNullParameters("logRnAction", name, properties, output)) { + impl.logRnAction(name, startTime, endTime, properties, bytesSent, output); + } + } + + @Override + public void addBreadcrumb(@NonNull String message) { + if (verifyNonNullParameters("addBreadcrumb", message)) { + impl.addBreadcrumb(message); + } + } + + @Override + public void logMessage(@NonNull String message, @NonNull Severity severity) { + if (verifyNonNullParameters("logMessage", message, severity)) { + logMessage(message, severity, null); + } + } + + @Override + public void logMessage(@NonNull String message, + @NonNull Severity severity, + @Nullable Map properties) { + if (verifyNonNullParameters("logMessage", message, severity)) { + impl.logMessage(message, severity, properties); + } + } + + @Override + public void logException(@NonNull Throwable throwable) { + if (verifyNonNullParameters("logException", throwable)) { + logException(throwable, Severity.ERROR); + } + } + + @Override + public void logException(@NonNull Throwable throwable, @NonNull Severity severity) { + if (verifyNonNullParameters("logException", throwable, severity)) { + logException(throwable, severity, null); + } + } + + @Override + public void logException(@NonNull Throwable throwable, + @NonNull Severity severity, + @Nullable Map properties) { + if (verifyNonNullParameters("logException", throwable, severity)) { + logException(throwable, severity, properties, null); + } + } + + @Override + public void logException(@NonNull Throwable throwable, + @NonNull Severity severity, + @Nullable Map properties, + @Nullable String message) { + if (verifyNonNullParameters("logException", throwable, severity)) { + impl.logException(throwable, severity, properties, message); + } + } + + @Override + public void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements) { + if (verifyNonNullParameters("logCustomStacktrace", (Object) stacktraceElements)) { + logCustomStacktrace(stacktraceElements, Severity.ERROR); + } + } + + @Override + public void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, @NonNull Severity severity) { + if (verifyNonNullParameters("logCustomStacktrace", (Object) stacktraceElements, severity)) { + logCustomStacktrace(stacktraceElements, severity, null); + } + } + + @Override + public void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, + @NonNull Severity severity, + @Nullable Map properties) { + if (verifyNonNullParameters("logCustomStacktrace", (Object) stacktraceElements, severity)) { + logCustomStacktrace(stacktraceElements, severity, properties, null); + } + } + + @Override + public void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, + @NonNull Severity severity, + @Nullable Map properties, + @Nullable String message) { + if (verifyNonNullParameters("logCustomStacktrace", (Object) stacktraceElements, severity)) { + impl.logCustomStacktrace(stacktraceElements, severity, properties, message); + } + } + + /** + * Logs an internal error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logInternalError(@Nullable String message, @Nullable String details) { + impl.logInternalError(message, details); + } + + /** + * Logs an internal error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logInternalError(@NonNull Throwable error) { + impl.logInternalError(error); + } + + @Override + public synchronized void endSession() { + endSession(false); + } + + @Override + public synchronized void endSession(boolean clearUserInfo) { + impl.endSession(clearUserInfo); + } + + @Override + @NonNull + public String getDeviceId() { + return impl.getDeviceId(); + } + + @Override + public boolean startView(@NonNull String name) { + if (verifyNonNullParameters("startView", name)) { + return impl.startView(name); + } + return false; + } + + @Override + public boolean endView(@NonNull String name) { + if (verifyNonNullParameters("endView", name)) { + return impl.endView(name); + } + return false; + } + + /** + * Logs the fact that a particular view was entered. + *

+ * If the previously logged view has the same name, a duplicate view breadcrumb will not be + * logged. + * + * @param screen the name of the view to log + */ + @InternalApi + public void logRnView(@NonNull String screen) { + impl.logRnView(screen); + } + + @Nullable + @InternalApi + public ConfigService getConfigService() { + return impl.getConfigService(); + } + + @InternalApi + void installUnityThreadSampler() { + getImpl().installUnityThreadSampler(); + } + + @Override + public boolean isTracingAvailable() { + return impl.tracer.getValue().isTracingAvailable(); + } + + @Nullable + @Override + public EmbraceSpan createSpan(@NonNull String name) { + if (verifyNonNullParameters("createSpan", name)) { + return impl.tracer.getValue().createSpan(name); + } + + return null; + } + + @Nullable + @Override + public EmbraceSpan createSpan(@NonNull String name, @Nullable EmbraceSpan parent) { + if (verifyNonNullParameters("createSpan", name)) { + return impl.tracer.getValue().createSpan(name, parent); + } + + return null; + } + + @Override + public T recordSpan(@NonNull String name, @NonNull Function0 code) { + if (verifyNonNullParameters("recordSpan", name, code)) { + return impl.tracer.getValue().recordSpan(name, code); + } + + return code != null ? code.invoke() : null; + } + + @Override + public T recordSpan(@NonNull String name, @Nullable EmbraceSpan parent, @NonNull Function0 code) { + if (verifyNonNullParameters("recordSpan", name, code)) { + return impl.tracer.getValue().recordSpan(name, parent, code); + } + + return code != null ? code.invoke() : null; + } + + @Override + public boolean recordCompletedSpan(@NonNull String name, long startTimeNanos, long endTimeNanos, @Nullable ErrorCode errorCode, + @Nullable EmbraceSpan parent, @Nullable Map attributes, + @Nullable List events) { + if (verifyNonNullParameters("recordCompletedSpan", name)) { + return impl.tracer.getValue().recordCompletedSpan(name, startTimeNanos, endTimeNanos, errorCode, parent, attributes, events); + } + + return false; + } + + @Override + public boolean recordCompletedSpan(@NonNull String name, long startTimeNanos, long endTimeNanos) { + if (verifyNonNullParameters("recordCompletedSpan", name)) { + return impl.tracer.getValue().recordCompletedSpan(name, startTimeNanos, endTimeNanos); + } + + return false; + } + + @Override + public boolean recordCompletedSpan(@NonNull String name, long startTimeNanos, long endTimeNanos, @Nullable ErrorCode errorCode) { + if (verifyNonNullParameters("recordCompletedSpan", name)) { + return impl.tracer.getValue().recordCompletedSpan(name, startTimeNanos, endTimeNanos, errorCode); + } + + return false; + } + + @Override + public boolean recordCompletedSpan(@NonNull String name, long startTimeNanos, long endTimeNanos, @Nullable EmbraceSpan parent) { + if (verifyNonNullParameters("recordCompletedSpan", name)) { + return impl.tracer.getValue().recordCompletedSpan(name, startTimeNanos, endTimeNanos, parent); + } + + return false; + } + + @Override + public boolean recordCompletedSpan(@NonNull String name, long startTimeNanos, long endTimeNanos, @Nullable ErrorCode errorCode, + @Nullable EmbraceSpan parent) { + if (verifyNonNullParameters("recordCompletedSpan", name)) { + return impl.tracer.getValue().recordCompletedSpan(name, startTimeNanos, endTimeNanos, errorCode, parent); + } + + return false; + } + + @Override + public boolean recordCompletedSpan(@NonNull String name, long startTimeNanos, long endTimeNanos, + @Nullable Map attributes, @Nullable List events) { + if (verifyNonNullParameters("recordCompletedSpan", name)) { + return impl.tracer.getValue().recordCompletedSpan(name, startTimeNanos, endTimeNanos, attributes, events); + } + + return false; + } + + /** + * The AppFramework that is in use. + */ + public enum AppFramework { + NATIVE(1), + REACT_NATIVE(2), + UNITY(3), + FLUTTER(4); + + private final int value; + + AppFramework(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + /** + * Gets the {@link ReactNativeInternalInterface} that should be used as the sole source of + * communication with the Android SDK for React Native. + */ + @Nullable + @InternalApi + public ReactNativeInternalInterface getReactNativeInternalInterface() { + return impl.getReactNativeInternalInterface(); + } + + /** + * Gets the {@link UnityInternalInterface} that should be used as the sole source of + * communication with the Android SDK for Unity. + */ + @Nullable + @InternalApi + public UnityInternalInterface getUnityInternalInterface() { + return impl.getUnityInternalInterface(); + } + + /** + * Gets the {@link FlutterInternalInterface} that should be used as the sole source of + * communication with the Android SDK for Flutter. + */ + @Nullable + @InternalApi + public FlutterInternalInterface getFlutterInternalInterface() { + return impl.getFlutterInternalInterface(); + } + + /** + * Logs a handled Dart error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logHandledDartException( + @Nullable String stack, + @Nullable String name, + @Nullable String message, + @Nullable String context, + @Nullable String library + ) { + impl.logDartException(stack, name, message, context, library, LogExceptionType.HANDLED); + } + + /** + * Logs an unhandled Dart error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logUnhandledDartException( + @Nullable String stack, + @Nullable String name, + @Nullable String message, + @Nullable String context, + @Nullable String library + ) { + impl.logDartException(stack, name, message, context, library, LogExceptionType.UNHANDLED); + } + + @InternalApi + public void sampleCurrentThreadDuringAnrs() { + impl.sampleCurrentThreadDuringAnrs(); + } + + /** + * Logs taps from Compose views + * @param point Position of the captured clicked + * @param elementName Name of the clicked element + */ + @InternalApi + public void logComposeTap(@NonNull Pair point, @NonNull String elementName) { + impl.getEmbraceInternalInterface().logComposeTap(point, elementName); + } + + /** + * Allows Unity customers to verify their integration. + */ + void verifyUnityIntegration() { + EmbraceSamples.verifyIntegration(); + } + + @Override + public void logPushNotification(@Nullable String title, + @Nullable String body, + @Nullable String topic, + @Nullable String id, + @Nullable Integer notificationPriority, + @NonNull Integer messageDeliveredPriority, + @NonNull Boolean isNotification, + @NonNull Boolean hasData) { + if (verifyNonNullParameters("logPushNotification", messageDeliveredPriority, isNotification, hasData)) { + impl.logPushNotification( + title, + body, + topic, + id, + notificationPriority, + messageDeliveredPriority, + PushNotificationBreadcrumb.NotificationType.Builder.notificationTypeFor(hasData, isNotification) + ); + } + } + + /** + * Determine if a network call should be captured based on the network capture rules + * + * @param url the url of the network call + * @param method the method of the network call + * @return the network capture rule to apply or null + */ + @InternalApi + public boolean shouldCaptureNetworkBody(@NonNull String url, @NonNull String method) { + if (isStarted()) { + return impl.shouldCaptureNetworkCall(url, method); + } else { + internalEmbraceLogger.logSDKNotInitialized("Embrace SDK is not initialized yet, cannot check for capture rules."); + return false; + } + } + + @InternalApi + public void setProcessStartedByNotification() { + impl.setProcessStartedByNotification(); + } + + @Override + public void trackWebViewPerformance(@NonNull String tag, @NonNull ConsoleMessage consoleMessage) { + if (verifyNonNullParameters("trackWebViewPerformance", tag, consoleMessage)) { + if (consoleMessage.message() != null) { + trackWebViewPerformance(tag, consoleMessage.message()); + } else { + logger.logDebug("Empty WebView console message."); + } + } + } + + @Override + public void trackWebViewPerformance(@NonNull String tag, @NonNull String message) { + if (verifyNonNullParameters("trackWebViewPerformance", tag, message)) { + impl.trackWebViewPerformance(tag, message); + } + } + + /** + * Get the ID for the current session. + * Returns null if a session has not been started yet or the SDK hasn't been initialized. + * + * @return The ID for the current Session, if available. + */ + @Nullable + @Override + public String getCurrentSessionId() { + return impl.getCurrentSessionId(); + } + + @NonNull + @Override + public LastRunEndState getLastRunEndState() { + return impl.getLastRunEndState(); + } + + /** + * Enum representing the end state of the last run of the application. + */ + public enum LastRunEndState { + /** + * The SDK has not been started yet. + */ + INVALID(0), + + /** + * The last run resulted in a crash. + */ + CRASH(1), + + /** + * The last run did not result in a crash. + */ + CLEAN_EXIT(2); + + private final int value; + + LastRunEndState(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private boolean verifyNonNullParameters(@NonNull String functionName, @NonNull Object... params) { + for (Object param : params) { + if (param == null) { + final String errorMessage = functionName + NULL_PARAMETER_ERROR_MESSAGE_TEMPLATE; + if (isStarted()) { + internalEmbraceLogger.logError(errorMessage, new IllegalArgumentException(errorMessage), true); + } else { + internalEmbraceLogger.logSDKNotInitialized(errorMessage); + } + return false; + } + } + return true; + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAndroidApi.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAndroidApi.java new file mode 100644 index 0000000000..006ffb4943 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAndroidApi.java @@ -0,0 +1,98 @@ +package io.embrace.android.embracesdk; + +import android.content.Context; + +import androidx.annotation.NonNull; + +/** + * Declares the functions that consist of Embrace's public API - specifically + * those that are only declared on Android. You should not use + * {@link EmbraceAndroidApi} directly or implement it in your own custom classes, + * as new functions may be added in future. Use the {@link Embrace} class instead. + */ +interface EmbraceAndroidApi extends EmbraceApi { + + /** + * Starts instrumentation of the Android application using the Embrace SDK. This should be + * called during creation of the application, as early as possible. + *

+ * See Embrace Docs for + * integration instructions. For compatibility with other networking SDKs such as Akamai, + * the Embrace SDK must be initialized after any other SDK. + * + * @param context an instance of the application context + */ + void start(@NonNull Context context); + + /** + * Starts instrumentation of the Android application using the Embrace SDK. This should be + * called during creation of the application, as early as possible. + *

+ * See Embrace Docs for + * integration instructions. For compatibility with other networking SDKs such as Akamai, + * the Embrace SDK must be initialized after any other SDK. + * + * @param context an instance of context + * @param enableIntegrationTesting if true, debug sessions (those which are not part of a + * release APK) will go to the live integration testing tab + * of the dashboard. If false, they will appear in 'recent + * sessions'. + */ + void start(@NonNull Context context, + boolean enableIntegrationTesting); + + /** + * Starts instrumentation of the Android application using the Embrace SDK. This should be + * called during creation of the application, as early as possible. + *

+ * See Embrace Docs for + * integration instructions. For compatibility with other networking SDKs such as Akamai, + * the Embrace SDK must be initialized after any other SDK. + * + * @param context an instance of context + * @param enableIntegrationTesting if true, debug sessions (those which are not part of a + * release APK) will go to the live integration testing tab + * of the dashboard. If false, they will appear in 'recent + * sessions'. + */ + void start(@NonNull Context context, + boolean enableIntegrationTesting, + @NonNull Embrace.AppFramework appFramework); + + /** + * Whether or not the SDK has been started. + * + * @return true if the SDK is started, false otherwise + */ + boolean isStarted(); + + /** + * Records that a view 'started'. You should call this when your app starts displaying an + * activity, a fragment, a screen, or any custom UI element, and you want to capture a + * breadcrumb that this happens. + *

+ * A matching call to {@link #endView(String)} must be made when the view is no longer + * displayed. + *

+ * A maximum of 100 breadcrumbs will be recorded per session, with a maximum length of 256 + * characters per view name. + * + * @param name the name of the view to log + */ + boolean startView(@NonNull String name); + + /** + * Records that a view 'ended'. You should call this when your app stops displaying an + * activity, a fragment, a screen, or any custom UI element, and you want to capture a + * breadcrumb that this happens. + *

+ * A matching call to {@link #startView(String)} must be made when the view is first + * displayed, or no breadcrumb will be logged. + *

+ * A maximum of 100 breadcrumbs will be recorded per session, with a maximum length of 256 + * characters per view name. + * + * @param name the name of the view to log + */ + boolean endView(@NonNull String name); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceApi.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceApi.java new file mode 100644 index 0000000000..b2e8359bbe --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceApi.java @@ -0,0 +1,90 @@ +package io.embrace.android.embracesdk; + +import android.webkit.ConsoleMessage; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.embrace.android.embracesdk.spans.TracingApi; + +/** + * Declares the functions that consist of Embrace's public API. You should not use + * {@link EmbraceApi} directly or implement it in your own custom classes, + * as new functions may be added in future. Use the {@link Embrace} class instead. + */ +interface EmbraceApi extends LogsApi, MomentsApi, NetworkRequestApi, SessionApi, UserApi, TracingApi { + /** + * Sets a custom app ID that overrides the one specified at build time. Must be called before + * the SDK is started. + * + * @param appId custom app ID + * @return true if the app ID could be set, false otherwise. + */ + boolean setAppId(@NonNull String appId); + + /** + * Adds a breadcrumb. + *

+ * Breadcrumbs track a user's journey through the application and will be shown on the timeline. + * + * @param message the name of the breadcrumb to log + */ + void addBreadcrumb(@NonNull String message); + + /** + * Retrieve the HTTP request header to extract trace ID from. + * + * @return the Trace ID header. + */ + @NonNull + String getTraceIdHeader(); + + /** + * Randomly generate a W3C-compliant traceparent + */ + @NonNull + String generateW3cTraceparent(); + + /** + * Get the user identifier assigned to the device by Embrace + * + * @return the device identifier created by Embrace + */ + @NonNull + String getDeviceId(); + + /** + * Listen to performance-tracking JavaScript previously embedded in the website's code. + * The WebView being tracked must have JavaScript enabled. + * + * @param tag a name used to identify the WebView being tracked + * @param consoleMessage the console message collected from the WebView + */ + void trackWebViewPerformance(@NonNull String tag, @NonNull ConsoleMessage consoleMessage); + + /** + * Listen to performance-tracking JavaScript previously embedded in the website's code. + * The WebView being tracked must have JavaScript enabled. + * + * @param tag a name used to identify the WebView being tracked + * @param message the console message collected from the WebView + */ + void trackWebViewPerformance(@NonNull String tag, @NonNull String message); + + /** + * Get the ID for the current session. + * Returns null if a session has not been started yet or the SDK hasn't been initialized. + * + * @return The ID for the current Session, if available. + */ + @Nullable + String getCurrentSessionId(); + + /** + * Get the end state of the last run of the application. + * + * @return LastRunEndState enum value representing the end state of the last run. + */ + @NonNull + Embrace.LastRunEndState getLastRunEndState(); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAutomaticVerification.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAutomaticVerification.kt new file mode 100644 index 0000000000..e0e60623e9 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceAutomaticVerification.kt @@ -0,0 +1,339 @@ +package io.embrace.android.embracesdk + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logger +import io.embrace.android.embracesdk.samples.AutomaticVerificationChecker +import io.embrace.android.embracesdk.samples.VerificationActions +import io.embrace.android.embracesdk.samples.VerifyIntegrationException +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.ActivityService +import java.io.IOException +import java.util.concurrent.Executors +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import kotlin.system.exitProcess + +/** + * Class that includes the logic to run the automatic Verification executing EmbraceSamples.verifyIntegration() method. + * + * Under the hood this function will create a marker File. If a marker file doesn't already exist, then it execute the following steps: + * 1. Runs {@link io.embrace.android.embracesdk.samples.VerificationActions#runActions()} + * 2. Relaunch the application after the action crash + * 3. Ends session manually and display result + * + */ +internal class EmbraceAutomaticVerification( + private val scheduledExecutorService: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() +) : ActivityListener { + private val handler = Handler(Looper.getMainLooper()) + + private var foregroundEventTriggered = false + + @VisibleForTesting + internal lateinit var activityService: ActivityService + + @VisibleForTesting + var automaticVerificationChecker = AutomaticVerificationChecker() + + @VisibleForTesting + var verificationActions = VerificationActions(Embrace.getInstance(), automaticVerificationChecker) + + /** + * This flag track if the verification result popup was displayed or not, + * in case the session continues after running the verification + */ + private var isResultDisplayed = false + + companion object { + internal const val TAG = "[EmbraceVerification]" + private const val ON_FOREGROUND_DELAY = 5000L + private const val EMBRACE_CONTACT_EMAIL = "support@embrace.io" + private const val VERIFY_INTEGRATION_DELAY = 200L + private const val ON_FOREGROUND_TIMEOUT = 5000L + internal val instance = EmbraceAutomaticVerification() + } + + fun verifyIntegration() { + instance.setActivityListener() + instance.runVerifyIntegration() + } + + @VisibleForTesting + fun setActivityListener() { + if (!::activityService.isInitialized) { + activityService = checkNotNull(Embrace.getImpl().activityService) + } + activityService.addListener(this) + } + + /** + * Started point to run the verification. + * We use a [ScheduledExecutorService] to give enough time to the onForeground callback + * to be executed in order to have a valid context/activity + */ + private fun runVerifyIntegration() { + try { + scheduledExecutorService.schedule( + { startVerification() }, + VERIFY_INTEGRATION_DELAY, + TimeUnit.MILLISECONDS + ) + } catch (e: RejectedExecutionException) { + logger.logError("$TAG - Start verification rejected", e) + } + } + + @VisibleForTesting + fun startVerification() { + val activity = activityService.foregroundActivity + if (activity != null) { + try { + if (automaticVerificationChecker.createFile(activity)) { + // should run the verification actions + showToast(activity, activity.getString(R.string.automatic_verification_started)) + verificationActions.runActions() + } else { + // the verification was already started + logger.logInfo("$TAG Verification almost ready...") + handler.postDelayed({ + verifyLifecycle() + }, ON_FOREGROUND_TIMEOUT) + } + } catch (e: IOException) { + logger.logError("$TAG Embrace SDK cannot run the verification in this moment", e) + showToast(activity, activity.getString(R.string.automatic_verification_not_started)) + } + } else { + logger.logError("$TAG Embrace SDK cannot run the verification in this moment, Activity is not present") + } + } + + private fun verifyLifecycle() { + if (!foregroundEventTriggered) { + logger.logError("$TAG OnForeground event was not triggered") + val exceptionsService = checkNotNull(Embrace.getImpl().exceptionsService) + if (verifyIfInitializerIsDisabled()) { + exceptionsService.handleInternalError( + VerifyIntegrationException("ProcessLifecycleInitializer disabled") + ) + showDialogWithError(R.string.automatic_verification_no_initializer_message) + } else { + exceptionsService.handleInternalError( + VerifyIntegrationException("onForeground not invoked") + ) + showDialogWithError(R.string.automatic_verification_lifecycle_error_message) + } + } + } + + @VisibleForTesting + fun runEndSession() { + Embrace.getInstance().endSession() + logger.logInfo("$TAG End session manually") + } + + /** + * Tries to detect the condition where the ProcessLifecycleInitializer is removed in the build file + * + * @return true if it detects that ProcessLifecycleInitializer is disabled, false otherwise + */ + private fun verifyIfInitializerIsDisabled(): Boolean { + logger.logInfo("Trying to verify lifecycle annotations") + try { + val appInitializerClass: Class<*>? + try { + appInitializerClass = Class.forName("androidx.startup.AppInitializer") + } catch (cnfe: ClassNotFoundException) { + logger.logDeveloper( + "EmbraceAutomaticVerification", + "AppInitializer not found. Assuming that appCompat < 1.4.1" + ) + return false + } + + Embrace.getImpl().application?.also { app -> + val getInstance = appInitializerClass.getMethod("getInstance", Context::class.java) + val isEagerlyInitialized = + appInitializerClass.getMethod("isEagerlyInitialized", Class::class.java) + val lifecycleInitializerClass = + Class.forName("androidx.lifecycle.ProcessLifecycleInitializer") + val appInitializer = getInstance.invoke(null, app) + + val result = isEagerlyInitialized.invoke(appInitializer, lifecycleInitializerClass) as Boolean + return result.not() + } ?: run { + logger.logDeveloper( + "EmbraceAutomaticVerification", + "Null application object, can not verify lifecycle annotations" + ) + return false + } + } catch (e: Exception) { + logger.logWarning("$TAG Could not verify if lifecycle annotations are working: $e") + } + return false + } + + /** + * Restarts the app after a forced VerifyIntegrationException + * was captured as part of the automatic verification + */ + fun restartAppFromPendingIntent() { + val exitStatus = 2 + val activity = activityService.foregroundActivity + if (activity != null) { + val intent = activity.intent + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + intent.putExtra("from_verification", true) + + with(activity) { + finish() + startActivity(intent) + } + exitProcess(exitStatus) + } else { + logger.logError("Cannot restart app, activity is not present") + } + } + + override fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) { + foregroundEventTriggered = true + val activity = activityService.foregroundActivity + + if (activity != null) { + val fromVerification = activity.intent.getBooleanExtra("from_verification", false) + + if (!fromVerification) { + return + } + + if (isResultDisplayed) { + logger.logDebug("onForeground called but the result was already displayed") + return + } + + handler.postDelayed({ + runEndSession() + displayResult() + clearUserData() + automaticVerificationChecker.deleteFile() + }, ON_FOREGROUND_DELAY) + } else { + logger.logError("Cannot restart app, activity is not present") + } + } + + private fun clearUserData() { + Embrace.getInstance().clearUserEmail() + Embrace.getInstance().clearUsername() + Embrace.getInstance().clearAllUserPersonas() + Embrace.getInstance().clearUserIdentifier() + Embrace.getInstance().clearUserAsPayer() + } + + private fun displayResult() { + isResultDisplayed = true + + automaticVerificationChecker.isVerificationCorrect()?.also { isCorrect -> + if (isCorrect) { + logger.logInfo("$TAG Successful - Embrace is ready to go! 🎉") + showSuccessDialog() + } else { + logger.logInfo("$TAG Error - Something is wrong with the Embrace Configuration ⚠️") + showDialogWithError() + } + } ?: logger.logError("Cannot display end message") + } + + private fun showToast(activity: Activity, message: String) { + activity.runOnUiThread { + Toast.makeText( + activity, + message, + Toast.LENGTH_LONG + ).show() + } + } + + private fun showSuccessDialog() { + val activity = activityService.foregroundActivity + if (activity != null) { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder + .setTitle(activity.getString(R.string.automatic_verification_success_title)) + .setMessage(activity.getString(R.string.automatic_verification_success_message)) + .setCancelable(true) + .setPositiveButton(activity.getString(R.string.got_it)) { dialog, _ -> + dialog.dismiss() + } + dialogBuilder.create().show() + } else { + logger.logInfo("Verification success! - Cannot display popup") + } + } + + private fun showDialogWithError(errorMessage: Int? = null) { + val activity = activityService.foregroundActivity + if (activity != null) { + val exceptions = automaticVerificationChecker.getExceptions().map { it.message }.toMutableList() + + if (errorMessage != null) { + exceptions.add(activity.getString(errorMessage)) + } + + val errorString = if (exceptions.isNotEmpty()) { + activity.getString(R.string.embrace_verification_errors) + .replace("[X]", exceptions.joinToString("\n👉 ", "👉 ")) + } else { + activity.getString(R.string.automatic_verification_default_error_message) + } + + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder + .setTitle(activity.getString(R.string.automatic_verification_error_title)) + .setMessage(errorString) + .setCancelable(true) + .setNegativeButton(activity.getString(R.string.send_error_log)) { dialog, _ -> + sendErrorLog(activity, errorString) + dialog.dismiss() + } + .setPositiveButton(activity.getString(R.string.close)) { dialog, _ -> + dialog.dismiss() + } + dialogBuilder.create().show() + } else { + logger.logError("Verification error - Cannot display popup") + } + } + + private fun sendErrorLog(activity: Activity, errorMessage: String) { + val errorLog = generateErrorLog(errorMessage) + val selectorIntent = Intent(Intent.ACTION_SENDTO).setData(Uri.parse("mailto:$EMBRACE_CONTACT_EMAIL")) + + val emailIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_EMAIL, arrayOf(EMBRACE_CONTACT_EMAIL)) + putExtra(Intent.EXTRA_SUBJECT, "Android Verification Log") + putExtra(Intent.EXTRA_TEXT, errorLog) + selector = selectorIntent + } + + activity.startActivity(Intent.createChooser(emailIntent, "Send Email")) + } + + private fun generateErrorLog(errorMessage: String): String { + var errorLog = "App ID: ${Embrace.getImpl().metadataService?.getAppId()}\n" + + "App Version: ${Embrace.getImpl().metadataService?.getAppVersionName()}" + errorLog += "\n\n-----------------\n\n" + errorLog += errorMessage + return errorLog + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceEvent.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceEvent.kt new file mode 100644 index 0000000000..4d6c1edbbc --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceEvent.kt @@ -0,0 +1,64 @@ +package io.embrace.android.embracesdk + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.EmbraceEvent.Type + +/** + * Wraps the event [Type]. This class is purely used for backwards-compatibility. + */ +internal class EmbraceEvent private constructor() { + + /** + * This actually belongs in [Event], but to maintain backwards-compatibility of the API, + * this enum has been moved here rather than making [Event] public. + */ + @InternalApi + enum class Type( + + /** + * The abbreviation used in the story ID header when sending the event to the Embrace + * API using the [ApiClient]. + * + * @return the abbreviation for the event type + */ + val abbreviation: String + ) { + + @SerializedName("start") + START("s"), + + @SerializedName("late") + LATE("l"), + + @SerializedName("interrupt") + INTERRUPT("i"), + + @SerializedName("crash") + CRASH("c"), + + @SerializedName("end") + END("e"), + + @SerializedName("info") + INFO_LOG("il"), + + @SerializedName("error") + ERROR_LOG("el"), + + @SerializedName("warning") + WARNING_LOG("wl"), + + @SerializedName("network") + NETWORK_LOG("n"); + + companion object { + fun fromSeverity(severity: Severity): Type { + return when (severity) { + Severity.INFO -> INFO_LOG + Severity.WARNING -> WARNING_LOG + Severity.ERROR -> ERROR_LOG + } + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java new file mode 100644 index 0000000000..7bf68d2c9c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java @@ -0,0 +1,1703 @@ +package io.embrace.android.embracesdk; + +import static io.embrace.android.embracesdk.event.EmbraceEventService.STARTUP_EVENT_NAME; + +import android.app.Application; +import android.content.Context; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import io.embrace.android.embracesdk.anr.AnrService; +import io.embrace.android.embracesdk.anr.ndk.EmbraceNativeThreadSamplerServiceKt; +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerInstaller; +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerService; +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbService; +import io.embrace.android.embracesdk.capture.crumbs.PushNotificationCaptureService; +import io.embrace.android.embracesdk.capture.crumbs.activity.ActivityLifecycleBreadcrumbService; +import io.embrace.android.embracesdk.capture.memory.MemoryService; +import io.embrace.android.embracesdk.capture.metadata.MetadataService; +import io.embrace.android.embracesdk.capture.strictmode.StrictModeService; +import io.embrace.android.embracesdk.capture.user.UserService; +import io.embrace.android.embracesdk.capture.webview.WebViewService; +import io.embrace.android.embracesdk.clock.Clock; +import io.embrace.android.embracesdk.config.ConfigService; +import io.embrace.android.embracesdk.config.behavior.NetworkBehavior; +import io.embrace.android.embracesdk.config.behavior.SessionBehavior; +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger; +import io.embrace.android.embracesdk.event.EventService; +import io.embrace.android.embracesdk.injection.AndroidServicesModule; +import io.embrace.android.embracesdk.injection.AndroidServicesModuleImpl; +import io.embrace.android.embracesdk.injection.AnrModuleImpl; +import io.embrace.android.embracesdk.injection.CoreModule; +import io.embrace.android.embracesdk.injection.CoreModuleImpl; +import io.embrace.android.embracesdk.injection.CrashModule; +import io.embrace.android.embracesdk.injection.CrashModuleImpl; +import io.embrace.android.embracesdk.injection.CustomerLogModuleImpl; +import io.embrace.android.embracesdk.injection.DataCaptureServiceModule; +import io.embrace.android.embracesdk.injection.DataCaptureServiceModuleImpl; +import io.embrace.android.embracesdk.injection.DataContainerModule; +import io.embrace.android.embracesdk.injection.DataContainerModuleImpl; +import io.embrace.android.embracesdk.injection.DeliveryModule; +import io.embrace.android.embracesdk.injection.DeliveryModuleImpl; +import io.embrace.android.embracesdk.injection.EssentialServiceModule; +import io.embrace.android.embracesdk.injection.EssentialServiceModuleImpl; +import io.embrace.android.embracesdk.injection.InitModule; +import io.embrace.android.embracesdk.injection.InitModuleImpl; +import io.embrace.android.embracesdk.injection.SdkObservabilityModule; +import io.embrace.android.embracesdk.injection.SdkObservabilityModuleImpl; +import io.embrace.android.embracesdk.injection.SystemServiceModule; +import io.embrace.android.embracesdk.injection.SystemServiceModuleImpl; +import io.embrace.android.embracesdk.internal.ApkToolsConfig; +import io.embrace.android.embracesdk.internal.BuildInfo; +import io.embrace.android.embracesdk.internal.DeviceArchitecture; +import io.embrace.android.embracesdk.internal.DeviceArchitectureImpl; +import io.embrace.android.embracesdk.internal.MessageType; +import io.embrace.android.embracesdk.internal.TraceparentGenerator; +import io.embrace.android.embracesdk.internal.crash.LastRunCrashVerifier; +import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService; +import io.embrace.android.embracesdk.internal.spans.EmbraceTracer; +import io.embrace.android.embracesdk.internal.utils.ThrowableUtilsKt; +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService; +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger; +import io.embrace.android.embracesdk.logging.InternalErrorLogger; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; +import io.embrace.android.embracesdk.ndk.NativeModule; +import io.embrace.android.embracesdk.ndk.NativeModuleImpl; +import io.embrace.android.embracesdk.ndk.NdkService; +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; +import io.embrace.android.embracesdk.network.http.HttpUrlConnectionTracker; +import io.embrace.android.embracesdk.network.http.NetworkCaptureData; +import io.embrace.android.embracesdk.network.logging.NetworkCaptureService; +import io.embrace.android.embracesdk.network.logging.NetworkLoggingService; +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb; +import io.embrace.android.embracesdk.payload.Session; +import io.embrace.android.embracesdk.payload.TapBreadcrumb; +import io.embrace.android.embracesdk.prefs.PreferencesService; +import io.embrace.android.embracesdk.registry.ServiceRegistry; +import io.embrace.android.embracesdk.session.ActivityService; +import io.embrace.android.embracesdk.session.BackgroundActivityService; +import io.embrace.android.embracesdk.session.EmbraceActivityService; +import io.embrace.android.embracesdk.session.EmbraceSessionProperties; +import io.embrace.android.embracesdk.session.EmbraceSessionService; +import io.embrace.android.embracesdk.injection.SessionModule; +import io.embrace.android.embracesdk.injection.SessionModuleImpl; +import io.embrace.android.embracesdk.session.SessionService; +import io.embrace.android.embracesdk.utils.PropertyUtils; +import io.embrace.android.embracesdk.worker.ExecutorName; +import io.embrace.android.embracesdk.worker.WorkerThreadModule; +import io.embrace.android.embracesdk.worker.WorkerThreadModuleImpl; +import kotlin.Lazy; +import kotlin.LazyKt; +import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function11; +import kotlin.jvm.functions.Function2; +import kotlin.jvm.functions.Function3; +import kotlin.jvm.functions.Function5; + +/** + * Implementation class of the SDK. Embrace.java forms our public API and calls functions in this + * class. + *

+ * Any non-public APIs or functionality related to the Embrace.java client should ideally be put + * here instead. + */ +final class EmbraceImpl { + + private static final String ERROR_USER_UPDATES_DISABLED = "User updates are disabled, ignoring user persona update."; + + private static final Pattern appIdPattern = Pattern.compile("^[A-Za-z0-9]{5}$"); + + @NonNull + final Lazy tracer; + + /** + * Whether the Embrace SDK has been started yet. + */ + @NonNull + private final AtomicBoolean started = new AtomicBoolean(false); + + @NonNull + private final InitModule initModule; + + @NonNull + private final InternalEmbraceLogger internalEmbraceLogger = InternalStaticEmbraceLogger.logger; + + /** + * Custom app ID that overrides the one specified at build time + */ + @Nullable + volatile String customAppId; + + /** + * The application being instrumented by the SDK. + */ + @Nullable + private volatile Application application; + + /** + * The type of application being instrumented by this SDK instance, whether it's directly used by an Android app, or used via a hosted + * SDK like Flutter, React Native, or Unity. + */ + @Nullable + private volatile Embrace.AppFramework appFramework; + + @Nullable + private volatile BreadcrumbService breadcrumbService; + + @Nullable + private volatile SessionService sessionService; + + @Nullable + private volatile BackgroundActivityService backgroundActivityService; + + @Nullable + private volatile MetadataService metadataService; + + @Nullable + private volatile ActivityService activityService; + + @Nullable + private volatile NetworkLoggingService networkLoggingService; + + @Nullable + private volatile AnrService anrService; + + /** + * TODO: rename to match convention + */ + @Nullable + private volatile EmbraceRemoteLogger remoteLogger; + + @Nullable + private volatile ConfigService configService; + + @Nullable + private volatile PreferencesService preferencesService; + + @Nullable + private volatile EventService eventService; + + @Nullable + private volatile UserService userService; + + @Nullable + private volatile EmbraceInternalErrorService exceptionsService; + + @Nullable + private volatile NdkService ndkService; + + @Nullable + private volatile NetworkCaptureService networkCaptureService; + + @Nullable + private volatile WebViewService webViewService; + + @Nullable + private NativeThreadSamplerService nativeThreadSampler; + + @Nullable + private NativeThreadSamplerInstaller nativeThreadSamplerInstaller; + + @Nullable + private EmbraceInternalInterface embraceInternalInterface; + + @Nullable + private ReactNativeInternalInterface reactNativeInternalInterface; + + @Nullable + private UnityInternalInterface unityInternalInterface; + + @Nullable + private FlutterInternalInterface flutterInternalInterface; + + @Nullable + private PushNotificationCaptureService pushNotificationService; + + @Nullable + private WorkerThreadModule workerThreadModule; + + @Nullable + private ServiceRegistry serviceRegistry; + + @Nullable + private LastRunCrashVerifier crashVerifier; + + @NonNull + private final Clock sdkClock; + + @NonNull + private final Function2 coreModuleSupplier; + + @NonNull + private final Function1 systemServiceModuleSupplier; + + @NonNull + private final Function3 androidServicesModuleSupplier; + + @NonNull + private final Function0 workerThreadModuleSupplier; + + @NonNull + private final Function11, Function0, DeviceArchitecture, EssentialServiceModule> essentialServiceModuleSupplier; + + @NonNull + private final Function5 dataCaptureServiceModuleSupplier; + + @NonNull + private final Function5 + deliveryModuleSupplier; + + //variable pointing to the composeActivityListener instance obtained using reflection + @Nullable + Object composeActivityListenerInstance; + + EmbraceImpl(@NonNull Function0 initModuleSupplier, + @NonNull Function2 coreModuleSupplier, + @NonNull Function0 workerThreadModuleSupplier, + @NonNull Function1 systemServiceModuleSupplier, + @NonNull Function3 androidServiceModuleSupplier, + @NonNull Function11, Function0, DeviceArchitecture, EssentialServiceModule> + essentialServiceModuleSupplier, + @NonNull Function5 dataCaptureServiceModuleSupplier, + @NonNull Function5 deliveryModuleSupplier) { + initModule = initModuleSupplier.invoke(); + sdkClock = initModule.getClock(); + this.coreModuleSupplier = coreModuleSupplier; + this.workerThreadModuleSupplier = workerThreadModuleSupplier; + this.systemServiceModuleSupplier = systemServiceModuleSupplier; + this.androidServicesModuleSupplier = androidServiceModuleSupplier; + this.essentialServiceModuleSupplier = essentialServiceModuleSupplier; + this.dataCaptureServiceModuleSupplier = dataCaptureServiceModuleSupplier; + this.deliveryModuleSupplier = deliveryModuleSupplier; + this.tracer = LazyKt.lazy(() -> new EmbraceTracer(initModule.getSpansService())); + } + + EmbraceImpl() { + this( + InitModuleImpl::new, + CoreModuleImpl::new, + WorkerThreadModuleImpl::new, + SystemServiceModuleImpl::new, + AndroidServicesModuleImpl::new, + EssentialServiceModuleImpl::new, + DataCaptureServiceModuleImpl::new, + DeliveryModuleImpl::new + ); + } + + /** + * Starts instrumentation of the Android application using the Embrace SDK. This should be + * called during creation of the application, as early as possible. + *

+ * See Embrace Docs for + * integration instructions. For compatibility with other networking SDKs such as Akamai, + * the Embrace SDK must be initialized after any other SDK. + * + * @param context an instance of context + * @param enableIntegrationTesting if true, debug sessions (those which are not part of a + * release APK) will go to the live integration testing tab + * of the dashboard. If false, they will appear in 'recent + * sessions'. + */ + public void start(@NonNull Context context, + boolean enableIntegrationTesting, + @NonNull Embrace.AppFramework appFramework) { + try { + startImpl(context, enableIntegrationTesting, appFramework); + } catch (Exception ex) { + internalEmbraceLogger.logError( + "Exception occurred while initializing the Embrace SDK. Instrumentation may be disabled.", ex, true); + } + } + + private void startImpl(@NonNull Context context, + boolean enableIntegrationTesting, + @NonNull Embrace.AppFramework framework) { + if (application != null) { + // We don't hard fail if the SDK has been already initialized. + InternalStaticEmbraceLogger.logWarning("Embrace SDK has already been initialized"); + return; + } + if (ApkToolsConfig.IS_SDK_DISABLED) { + internalEmbraceLogger.logInfo("SDK disabled through ApkToolsConfig"); + stop(); + return; + } + + final long startTime = sdkClock.now(); + internalEmbraceLogger.logDeveloper("Embrace", "Starting SDK for framework " + framework.name()); + + final CoreModule coreModule = coreModuleSupplier.invoke(context, framework); + serviceRegistry = coreModule.getServiceRegistry(); + serviceRegistry.registerService(initModule.getSpansService()); + application = coreModule.getApplication(); + appFramework = coreModule.getAppFramework(); + + final WorkerThreadModule nonNullWorkerThreadModule = workerThreadModuleSupplier.invoke(); + workerThreadModule = nonNullWorkerThreadModule; + + final SystemServiceModule systemServiceModule = systemServiceModuleSupplier.invoke(coreModule); + final AndroidServicesModule androidServicesModule = + androidServicesModuleSupplier.invoke(initModule, coreModule, workerThreadModule); + preferencesService = androidServicesModule.getPreferencesService(); + serviceRegistry.registerService(preferencesService); + + // bootstrap initialization. ConfigService not created yet... + final EssentialServiceModule essentialServiceModule = essentialServiceModuleSupplier.invoke( + initModule, + coreModule, + nonNullWorkerThreadModule, + systemServiceModule, + androidServicesModule, + BuildInfo.fromResources(coreModule.getResources(), coreModule.getContext().getPackageName()), + customAppId, + enableIntegrationTesting, + () -> { + Embrace.getImpl().stop(); + return null; + }, + () -> null, + new DeviceArchitectureImpl()); + + final ActivityService nonNullActivityService = essentialServiceModule.getActivityService(); + activityService = nonNullActivityService; + final MetadataService nonNullMetadataService = essentialServiceModule.getMetadataService(); + metadataService = nonNullMetadataService; + final ConfigService nonNullConfigService = essentialServiceModule.getConfigService(); + configService = nonNullConfigService; + + // example usage. + serviceRegistry.registerServices( + activityService, + metadataService, + configService + ); + + // only call after ConfigService has initialized. + nonNullMetadataService.precomputeValues(); + + DataCaptureServiceModule dataCaptureServiceModule = dataCaptureServiceModuleSupplier.invoke( + initModule, + coreModule, + systemServiceModule, + essentialServiceModule, + workerThreadModule + ); + + webViewService = dataCaptureServiceModule.getWebviewService(); + MemoryService memoryService = dataCaptureServiceModule.getMemoryService(); + ((EmbraceActivityService) essentialServiceModule.getActivityService()) + .setMemoryService(dataCaptureServiceModule.getMemoryService()); + serviceRegistry.registerServices( + webViewService, + memoryService + ); + + /* + * Since onForeground() is called sequential in the order that services registered for it, + * it is important to initialize the `EmbraceAnrService`, and thus register the `onForeground() + * listener for it, before the `EmbraceSessionService`. + * The onForeground() call inside the EmbraceAnrService should be called before the + * EmbraceSessionService call. This is necessary since the EmbraceAnrService should be able to + * force a Main thread health check and close the pending ANR intervals that happened on the + * background before the next session is created. + */ + AnrModuleImpl anrModule = new AnrModuleImpl( + initModule, + coreModule, + systemServiceModule, + essentialServiceModule + ); + AnrService nonNullAnrService = anrModule.getAnrService(); + anrService = nonNullAnrService; + serviceRegistry.registerService(anrService); + + // set callbacks and pass in non-placeholder config. + nonNullAnrService.finishInitialization( + essentialServiceModule.getConfigService() + ); + + serviceRegistry.registerService(dataCaptureServiceModule.getPowerSaveModeService()); + + // initialize the logger early so that logged exceptions have a good chance of + // being appended to the exceptions service rather than logcat + SdkObservabilityModule sdkObservabilityModule = new SdkObservabilityModuleImpl( + initModule, + essentialServiceModule + ); + + final EmbraceInternalErrorService nonNullExceptionsService = sdkObservabilityModule.getExceptionService(); + exceptionsService = nonNullExceptionsService; + serviceRegistry.registerService(exceptionsService); + internalEmbraceLogger.addLoggerAction(sdkObservabilityModule.getInternalErrorLogger()); + + serviceRegistry.registerService(dataCaptureServiceModule.getNetworkConnectivityService()); + + final DeliveryModule deliveryModule = deliveryModuleSupplier.invoke( + initModule, + coreModule, + essentialServiceModule, + dataCaptureServiceModule, + nonNullWorkerThreadModule + ); + + serviceRegistry.registerService(deliveryModule.getDeliveryService()); + + final EmbraceSessionProperties sessionProperties = new EmbraceSessionProperties( + androidServicesModule.getPreferencesService(), + coreModule.getLogger(), + essentialServiceModule.getConfigService()); + + if (essentialServiceModule.getConfigService().isSdkDisabled()) { + internalEmbraceLogger.logInfo("the SDK is disabled"); + stop(); + return; + } + + nonNullExceptionsService.setConfigService(configService); + breadcrumbService = dataCaptureServiceModule.getBreadcrumbService(); + pushNotificationService = dataCaptureServiceModule.getPushNotificationService(); + serviceRegistry.registerServices(breadcrumbService, pushNotificationService); + + userService = essentialServiceModule.getUserService(); + serviceRegistry.registerServices(userService); + + CustomerLogModuleImpl customerLogModule = new CustomerLogModuleImpl( + initModule, + coreModule, + androidServicesModule, + essentialServiceModule, + deliveryModule, + sessionProperties, + dataCaptureServiceModule, + nonNullWorkerThreadModule + ); + remoteLogger = customerLogModule.getRemoteLogger(); + networkCaptureService = customerLogModule.getNetworkCaptureService(); + networkLoggingService = customerLogModule.getNetworkLoggingService(); + serviceRegistry.registerServices( + remoteLogger, + networkCaptureService, + networkLoggingService + ); + + NativeModule nativeModule = new NativeModuleImpl( + coreModule, + essentialServiceModule, + deliveryModule, + sessionProperties, + nonNullWorkerThreadModule + ); + + DataContainerModule dataContainerModule = new DataContainerModuleImpl( + initModule, + coreModule, + nonNullWorkerThreadModule, + systemServiceModule, + androidServicesModule, + essentialServiceModule, + dataCaptureServiceModule, + anrModule, + customerLogModule, + deliveryModule, + nativeModule, + sessionProperties, + startTime + ); + + final EventService nonNullEventService = dataContainerModule.getEventService(); + eventService = nonNullEventService; + serviceRegistry.registerServices( + dataContainerModule.getPerformanceInfoService(), + eventService, + dataContainerModule.getApplicationExitInfoService() + ); + + ndkService = nativeModule.getNdkService(); + nativeThreadSampler = nativeModule.getNativeThreadSamplerService(); + nativeThreadSamplerInstaller = nativeModule.getNativeThreadSamplerInstaller(); + + serviceRegistry.registerServices( + ndkService, + nativeThreadSampler + ); + + if (nativeThreadSampler != null && nativeThreadSamplerInstaller != null) { + // install the native thread sampler + nativeThreadSampler.setupNativeSampler(); + + // In Unity this should always run on the Unity thread. + if (coreModule.getAppFramework() == Embrace.AppFramework.UNITY && EmbraceNativeThreadSamplerServiceKt.isUnityMainThread()) { + sampleCurrentThreadDuringAnrs(); + } + } else { + internalEmbraceLogger.logDeveloper("Embrace", "Failed to load SO file embrace-native"); + } + + SessionModule sessionModule = new SessionModuleImpl( + initModule, + coreModule, + androidServicesModule, + essentialServiceModule, + nativeModule, + dataContainerModule, + deliveryModule, + sessionProperties, + dataCaptureServiceModule, + customerLogModule, + sdkObservabilityModule, + nonNullWorkerThreadModule + ); + + final SessionService nonNullSessionService = sessionModule.getSessionService(); + sessionService = nonNullSessionService; + backgroundActivityService = sessionModule.getBackgroundActivityService(); + serviceRegistry.registerServices(sessionService, backgroundActivityService); + + if (backgroundActivityService != null) { + internalEmbraceLogger.logInfo("Background activity capture enabled"); + } else { + internalEmbraceLogger.logInfo("Background activity capture disabled"); + } + + CrashModule crashModule = new CrashModuleImpl( + initModule, + essentialServiceModule, + deliveryModule, + nativeModule, + sessionModule, + anrModule, + dataContainerModule, + coreModule + ); + + loadCrashVerifier(crashModule, nonNullWorkerThreadModule); + + Thread.setDefaultUncaughtExceptionHandler(crashModule.getAutomaticVerificationExceptionHandler()); + serviceRegistry.registerService(crashModule.getCrashService()); + + StrictModeService strictModeService = dataCaptureServiceModule.getStrictModeService(); + serviceRegistry.registerService(strictModeService); + strictModeService.start(); + + serviceRegistry.registerService(dataCaptureServiceModule.getThermalStatusService()); + + ActivityLifecycleBreadcrumbService collector = dataCaptureServiceModule.getActivityLifecycleBreadcrumbService(); + if (collector instanceof Application.ActivityLifecycleCallbacks) { + coreModule.getApplication().registerActivityLifecycleCallbacks((Application.ActivityLifecycleCallbacks) collector); + serviceRegistry.registerService(collector); + } + + if (configService.getAutoDataCaptureBehavior().isComposeOnClickEnabled()) { + registerComposeActivityListener(coreModule); + } + + // initialize internal interfaces + InternalInterfaceModuleImpl internalInterfaceModule = new InternalInterfaceModuleImpl( + coreModule, + androidServicesModule, + essentialServiceModule, + this, + crashModule + ); + + embraceInternalInterface = internalInterfaceModule.getEmbraceInternalInterface(); + reactNativeInternalInterface = internalInterfaceModule.getReactNativeInternalInterface(); + unityInternalInterface = internalInterfaceModule.getUnityInternalInterface(); + flutterInternalInterface = internalInterfaceModule.getFlutterInternalInterface(); + + String startMsg = "Embrace SDK started. App ID: " + nonNullConfigService.getSdkModeBehavior().getAppId() + + " Version: " + BuildConfig.VERSION_NAME; + internalEmbraceLogger.logInfo(startMsg); + + NetworkBehavior networkBehavior = nonNullConfigService.getNetworkBehavior(); + if (networkBehavior.isNativeNetworkingMonitoringEnabled()) { + // Intercept Android network calls + internalEmbraceLogger.logDeveloper("Embrace", "Native Networking Monitoring enabled"); + HttpUrlConnectionTracker.registerFactory(networkBehavior.isRequestContentLengthCaptureEnabled()); + } + + final long endTime = sdkClock.now(); + started.set(true); + + nonNullWorkerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION).submit(() -> { + ((EmbraceSpansService) initModule.getSpansService()).initializeService(TimeUnit.MILLISECONDS.toNanos(startTime), + TimeUnit.MILLISECONDS.toNanos(endTime)); + return null; + }); + + long startupDuration = endTime - startTime; + ((EmbraceSessionService) nonNullSessionService).setSdkStartupDuration(startupDuration); + internalEmbraceLogger.logDeveloper("Embrace", "Startup duration: " + startupDuration + " millis"); + + // Sets up the registered services. This method is called after the SDK has been started and + // no more services can be added to the registry. It sets listeners for any services that were + // registered. + serviceRegistry.closeRegistration(); + serviceRegistry.registerActivityListeners(nonNullActivityService); + serviceRegistry.registerConfigListeners(nonNullConfigService); + serviceRegistry.registerMemoryCleanerListeners(essentialServiceModule.getMemoryCleanerService()); + + // Attempt to send the startup event if the app is already in the foreground. We registered to send this when + // we went to the foreground, but if an activity had already gone to the foreground, we may have missed + // sending this, so to ensure the startup message is sent, we force it to be sent here. + if (!nonNullActivityService.isInBackground()) { + internalEmbraceLogger.logDeveloper("Embrace", "Sending startup moment"); + nonNullEventService.sendStartupMoment(); + } + } + + /** + * Register ComposeActivityListener as Activity Lifecycle Callbacks into the Application + * + * @param coreModule instance containing a required set of dependencies + */ + private void registerComposeActivityListener(@NonNull CoreModule coreModule) { + try { + Class composeActivityListener = Class.forName("io.embrace.android.embracesdk.compose.ComposeActivityListener"); + composeActivityListenerInstance = composeActivityListener.newInstance(); + coreModule.getApplication().registerActivityLifecycleCallbacks((Application.ActivityLifecycleCallbacks) composeActivityListenerInstance); + } catch (Throwable e) { + internalEmbraceLogger.logError("registerComposeActivityListener error", e); + } + } + + + /** + * Register ComposeActivityListener as Activity Lifecycle Callbacks into the Application + * + * @param app Global application class + */ + private void unregisterComposeActivityListener(@NonNull Application app) { + try { + app.unregisterActivityLifecycleCallbacks((Application.ActivityLifecycleCallbacks) composeActivityListenerInstance); + } catch (Throwable e) { + internalEmbraceLogger.logError("Instantiation error for ComposeActivityListener", e); + } + } + + /** + * Whether or not the SDK has been started. + * + * @return true if the SDK is started, false otherwise + */ + public boolean isStarted() { + return started.get(); + } + + /** + * Sets a custom app ID that overrides the one specified at build time. Must be called before + * the SDK is started. + * + * @param appId custom app ID + * @return true if the app ID could be set, false otherwise. + */ + public boolean setAppId(@NonNull String appId) { + if (isStarted()) { + internalEmbraceLogger.logError("You must set the custom app ID before the SDK is started."); + return false; + } + if (appId.isEmpty()) { + internalEmbraceLogger.logError("App ID cannot be null or empty."); + return false; + } + if (!isValidAppId(appId)) { + internalEmbraceLogger.logError("Invalid app ID. Must be a 5-character string with " + + "characters from the set [A-Za-z0-9], but it was \"" + appId + "\"."); + return false; + } + + customAppId = appId; + internalEmbraceLogger.logDeveloper("Embrace", "App Id set"); + return true; + } + + static boolean isValidAppId(String appId) { + return appIdPattern.matcher(appId).find(); + } + + /** + * Shuts down the Embrace SDK. + */ + void stop() { + if (started.compareAndSet(true, false)) { + internalEmbraceLogger.logInfo("Shutting down Embrace SDK."); + try { + if (composeActivityListenerInstance != null) { + unregisterComposeActivityListener(application); + } + + application = null; + internalEmbraceLogger.logDeveloper("Embrace", "Attempting to close services..."); + serviceRegistry.close(); + internalEmbraceLogger.logDeveloper("Embrace", "Services closed"); + workerThreadModule.close(); + } catch (Exception ex) { + internalEmbraceLogger.logError("Error while shutting down Embrace SDK", ex); + } + } + } + + /** + * Sets the user ID. This would typically be some form of unique identifier such as a UUID or + * database key for the user. + * + * @param userId the unique identifier for the user + */ + public void setUserIdentifier(@Nullable String userId) { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring identifier update."); + return; + } + userService.setUserIdentifier(userId); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + if (userId != null) { + internalEmbraceLogger.logDebug("Set user ID to " + userId); + } else { + internalEmbraceLogger.logDebug("Cleared user ID by setting to null"); + } + } else { + internalEmbraceLogger.logSDKNotInitialized("set user identifier"); + } + } + + /** + * Clears the currently set user ID. For example, if the user logs out. + */ + public void clearUserIdentifier() { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring identifier update."); + return; + } + userService.clearUserIdentifier(); + internalEmbraceLogger.logDebug("Cleared user ID"); + } else { + internalEmbraceLogger.logSDKNotInitialized("clear user identifier"); + } + } + + /** + * Sets the current user's email address. + * + * @param email the email address of the current user + */ + public void setUserEmail(@Nullable String email) { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring email update."); + return; + } + userService.setUserEmail(email); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + if (email != null) { + internalEmbraceLogger.logDebug("Set email to " + email); + } else { + internalEmbraceLogger.logDebug("Cleared email by setting to null"); + } + } else { + internalEmbraceLogger.logSDKNotInitialized("clear user email"); + } + } + + /** + * Clears the currently set user's email address. + */ + public void clearUserEmail() { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring email update."); + return; + } + userService.clearUserEmail(); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + internalEmbraceLogger.logDebug("Cleared email"); + } else { + internalEmbraceLogger.logSDKNotInitialized("clear user email"); + } + } + + /** + * Sets this user as a paying user. This adds a persona to the user's identity. + */ + public void setUserAsPayer() { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring payer user update."); + return; + } + userService.setUserAsPayer(); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + } else { + internalEmbraceLogger.logSDKNotInitialized("set user as payer"); + } + } + + /** + * Clears this user as a paying user. This would typically be called if a user is no longer + * paying for the service and has reverted back to a basic user. + */ + public void clearUserAsPayer() { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring payer user update."); + return; + } + userService.clearUserAsPayer(); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + } else { + internalEmbraceLogger.logSDKNotInitialized("clear user as payer"); + } + } + + /** + * Sets a custom user persona. A persona is a trait associated with a given user. + * + * @param persona the persona to set + */ + public void addUserPersona(@NonNull String persona) { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning(ERROR_USER_UPDATES_DISABLED); + return; + } + userService.addUserPersona(persona); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + } else { + internalEmbraceLogger.logSDKNotInitialized("set user persona"); + } + } + + /** + * Clears the custom user persona, if it is set. + * + * @param persona the persona to clear + */ + public void clearUserPersona(@NonNull String persona) { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning(ERROR_USER_UPDATES_DISABLED); + return; + } + userService.clearUserPersona(persona); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + } else { + internalEmbraceLogger.logSDKNotInitialized("clear user persona"); + } + } + + /** + * Clears all custom user personas from the user. + */ + public void clearAllUserPersonas() { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning(ERROR_USER_UPDATES_DISABLED); + return; + } + userService.clearAllUserPersonas(); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + } else { + internalEmbraceLogger.logSDKNotInitialized("clear user personas"); + } + } + + /** + * Adds a property to the current session. + */ + public boolean addSessionProperty(@NonNull String key, @NonNull String value, boolean permanent) { + if (isStarted()) { + return sessionService.addProperty(key, value, permanent); + } + internalEmbraceLogger.logSDKNotInitialized("cannot add session property"); + return false; + } + + /** + * Removes a property from the current session. + */ + public boolean removeSessionProperty(@NonNull String key) { + if (isStarted()) { + return sessionService.removeProperty(key); + } + + internalEmbraceLogger.logSDKNotInitialized("remove session property"); + return false; + } + + /** + * Retrieves a map of the current session properties. + */ + @Nullable + public Map getSessionProperties() { + if (isStarted()) { + return sessionService.getProperties(); + } + + internalEmbraceLogger.logSDKNotInitialized("gets session properties"); + return null; + } + + /** + * Sets the username of the currently logged in user. + * + * @param username the username to set + */ + public void setUsername(@Nullable String username) { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring username update."); + return; + } + userService.setUsername(username); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + if (username != null) { + internalEmbraceLogger.logDebug("Set username to " + username); + } else { + internalEmbraceLogger.logDebug("Cleared username by setting to null"); + } + } else { + internalEmbraceLogger.logSDKNotInitialized("set username"); + } + } + + /** + * Clears the username of the currently logged in user, for example if the user has logged out. + */ + public void clearUsername() { + if (isStarted()) { + if (!configService.getDataCaptureEventBehavior().isMessageTypeEnabled(MessageType.USER)) { + internalEmbraceLogger.logWarning("User updates are disabled, ignoring username update."); + return; + } + userService.clearUsername(); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + internalEmbraceLogger.logDebug("Cleared username"); + } else { + internalEmbraceLogger.logSDKNotInitialized("clear username"); + } + } + + /** + * Starts a 'moment'. Moments are used for encapsulating particular activities within + * the app, such as a user adding an item to their shopping cart. + *

+ * The length of time a moment takes to execute is recorded. + * + * @param name a name identifying the moment + * @param identifier an identifier distinguishing between multiple moments with the same name + * @param properties custom key-value pairs to provide with the moment + */ + public void startMoment(@NonNull String name, + @Nullable String identifier, + @Nullable Map properties) { + if (isStarted()) { + eventService.startEvent(name, identifier, normalizeProperties(properties)); + onActivityReported(); + } else { + internalEmbraceLogger.logSDKNotInitialized("startMoment"); + } + } + + /** + * Signals the end of a moment with the specified name. + *

+ * The duration of the moment is computed. + * + * @param name the name of the moment to end + * @param identifier the identifier of the moment to end, distinguishing between moments with the same name + * @param properties custom key-value pairs to provide with the moment + */ + public void endMoment(@NonNull String name, @Nullable String identifier, @Nullable Map properties) { + if (isStarted()) { + eventService.endEvent(name, identifier, normalizeProperties(properties)); + onActivityReported(); + } else { + internalEmbraceLogger.logSDKNotInitialized("endMoment"); + } + } + + /** + * Signals that the app has completed startup. + * + * @param properties properties to include as part of the startup moment + */ + public void endAppStartup(@Nullable Map properties) { + endMoment(STARTUP_EVENT_NAME, null, properties); + } + + /** + * Retrieve the HTTP request header to extract trace ID from. + * + * @return the Trace ID header. + */ + @NonNull + public String getTraceIdHeader() { + if (isStarted() && configService != null) { + return configService.getNetworkBehavior().getTraceIdHeader(); + } + return NetworkBehavior.CONFIG_TRACE_ID_HEADER_DEFAULT_VALUE; + } + + @NonNull + public String generateW3cTraceparent() { + return TraceparentGenerator.generateW3CTraceparent(); + } + + public void recordNetworkRequest(@NonNull EmbraceNetworkRequest request) { + internalEmbraceLogger.logDeveloper("Embrace", "recordNetworkRequest()"); + + if (request == null) { + internalEmbraceLogger.logDeveloper("Embrace", "Request is null"); + return; + } + + logNetworkRequestImpl( + request.getNetworkCaptureData(), + request.getUrl(), + request.getHttpMethod(), + request.getStartTime(), + request.getResponseCode(), + request.getEndTime(), + request.getErrorType(), + request.getErrorMessage(), + request.getTraceId(), + request.getW3cTraceparent(), + request.getBytesOut(), + request.getBytesIn() + ); + } + + private void logNetworkRequestImpl(@Nullable NetworkCaptureData networkCaptureData, + String url, + String httpMethod, + Long startTime, + Integer responseCode, + Long endTime, + String errorType, + String errorMessage, + String traceId, + @Nullable String w3cTraceparent, + Long bytesOut, + Long bytesIn) { + if (!isStarted()) { + internalEmbraceLogger.logSDKNotInitialized("log network request"); + return; + } + + if (configService.getNetworkBehavior().isUrlEnabled(url)) { + if (errorType != null && + errorMessage != null && + !errorType.isEmpty() && + !errorMessage.isEmpty()) { + networkLoggingService.logNetworkError( + url, + httpMethod, + startTime, + endTime != null ? endTime : 0, + errorType, + errorMessage, + traceId, + w3cTraceparent, + networkCaptureData); + } else { + networkLoggingService.logNetworkCall( + url, + httpMethod, + responseCode != null ? responseCode : 0, + startTime, + endTime != null ? endTime : 0, + bytesOut, + bytesIn, + traceId, + w3cTraceparent, + networkCaptureData); + } + onActivityReported(); + } + } + + public void logMessage(@NonNull String message, + @NonNull Severity severity, + @Nullable Map properties) { + logMessage( + EmbraceEvent.Type.Companion.fromSeverity(severity), + message, + properties, + null, + null, + LogExceptionType.NONE, + null, + null + ); + } + + public void logException(@NonNull Throwable throwable, + @NonNull Severity severity, + @Nullable Map properties, + @Nullable String message) { + String exceptionMessage = throwable.getMessage() != null ? throwable.getMessage() : ""; + logMessage( + EmbraceEvent.Type.Companion.fromSeverity(severity), + message != null ? message : exceptionMessage, + properties, + ThrowableUtilsKt.getSafeStackTrace(throwable), + null, + LogExceptionType.HANDLED, + null, + null, + throwable.getClass().getSimpleName(), + exceptionMessage); + } + + public void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, + @NonNull Severity severity, + @Nullable Map properties, + @Nullable String message) { + logMessage( + EmbraceEvent.Type.Companion.fromSeverity(severity), + message != null ? message : "", + properties, + stacktraceElements, + null, + LogExceptionType.HANDLED, + null, + null, + null, + message); + } + + void logMessage( + @NonNull EmbraceEvent.Type type, + @NonNull String message, + @Nullable Map properties, + @Nullable StackTraceElement[] stackTraceElements, + @Nullable String customStackTrace, + @NonNull LogExceptionType logExceptionType, + @Nullable String context, + @Nullable String library) { + logMessage(type, + message, + properties, + stackTraceElements, + customStackTrace, + logExceptionType, + context, + library, + null, + null); + } + + void logMessage( + @NonNull EmbraceEvent.Type type, + @NonNull String message, + @Nullable Map properties, + @Nullable StackTraceElement[] stackTraceElements, + @Nullable String customStackTrace, + @NonNull LogExceptionType logExceptionType, + @Nullable String context, + @Nullable String library, + @Nullable String exceptionName, + @Nullable String exceptionMessage) { + internalEmbraceLogger.logDeveloper("Embrace", "Attempting to log message"); + if (isStarted()) { + try { + remoteLogger.log( + message, + type, + logExceptionType, + normalizeProperties(properties), + stackTraceElements, + customStackTrace, + appFramework, + context, + library, + exceptionName, + exceptionMessage); + onActivityReported(); + } catch (Exception ex) { + internalEmbraceLogger.logDebug("Failed to log message using Embrace SDK.", ex); + } + } else { + internalEmbraceLogger.logSDKNotInitialized("log message"); + } + } + + /** + * Logs a breadcrumb. + *

+ * Breadcrumbs track a user's journey through the application and will be shown on the timeline. + * + * @param message the name of the breadcrumb to log + */ + public void addBreadcrumb(@NonNull String message) { + internalEmbraceLogger.logDeveloper("Embrace", "Attempting to add breadcrumb"); + if (isStarted()) { + breadcrumbService.logCustom(message, sdkClock.now()); + onActivityReported(); + } else { + internalEmbraceLogger.logSDKNotInitialized("log breadcrumb"); + } + } + + /** + * Logs a React Native Redux Action. + */ + public void logRnAction(@NonNull String name, long startTime, long endTime, + @NonNull Map properties, int bytesSent, @NonNull String output) { + if (isStarted()) { + breadcrumbService.logRnAction(name, startTime, endTime, properties, bytesSent, output); + } else { + internalEmbraceLogger.logWarning("Embrace SDK is not initialized yet, cannot log breadcrumb."); + } + } + + /** + * Logs an internal error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logInternalError(@Nullable String message, @Nullable String details) { + if (isStarted()) { + if (message == null) { + return; + } + String messageWithDetails; + + if (details != null) { + messageWithDetails = message + ": " + details; + } else { + messageWithDetails = message; + } + exceptionsService.handleInternalError(new InternalErrorLogger.InternalError(messageWithDetails)); + } else { + internalEmbraceLogger.logSDKNotInitialized("logInternalError"); + } + } + + /** + * Logs an internal error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logInternalError(@NonNull Throwable error) { + if (isStarted()) { + exceptionsService.handleInternalError(error); + } else { + internalEmbraceLogger.logSDKNotInitialized("logInternalError"); + } + } + + /** + * Logs a Dart error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logDartException( + @Nullable String stack, + @Nullable String name, + @Nullable String message, + @Nullable String context, + @Nullable String library, + @NonNull LogExceptionType logExceptionType + ) { + if (flutterInternalInterface != null) { + if (logExceptionType == LogExceptionType.HANDLED) { + flutterInternalInterface.logHandledDartException(stack, name, message, context, library); + } else if (logExceptionType == LogExceptionType.UNHANDLED) { + flutterInternalInterface.logUnhandledDartException(stack, name, message, context, library); + } + onActivityReported(); + } + } + + /** + * Ends the current session and starts a new one. + *

+ * Cleans all the user info on the device. + */ + public synchronized void endSession(boolean clearUserInfo) { + if (isStarted()) { + SessionBehavior sessionBehavior = configService.getSessionBehavior(); + if (sessionBehavior.getMaxSessionSecondsAllowed() != null) { + internalEmbraceLogger.logWarning("Can't close the session, automatic session close enabled."); + return; + } + + if (sessionBehavior.isAsyncEndEnabled()) { + internalEmbraceLogger.logWarning("Can't close the session, session ending in background thread enabled."); + return; + } + + if (clearUserInfo) { + userService.clearAllUserInfo(); + // Update user info in NDK service + ndkService.onUserInfoUpdate(); + } + + sessionService.triggerStatelessSessionEnd(Session.SessionLifeEventType.MANUAL); + } else { + internalEmbraceLogger.logSDKNotInitialized("end session"); + } + } + + /** + * Get the user identifier assigned to the device by Embrace + * + * @return the device identifier created by Embrace + */ + @NonNull + public String getDeviceId() { + return preferencesService.getDeviceIdentifier(); + } + + /** + * Log the start of a fragment. + *

+ * A matching call to endFragment must be made. + * + * @param name the name of the fragment to log + */ + public boolean startView(@NonNull String name) { + if (isStarted()) { + internalEmbraceLogger.logDeveloper("Embrace", "Starting fragment: " + name); + return breadcrumbService.startView(name); + } + + internalEmbraceLogger.logDeveloper("Embrace", "Cannot start fragment, SDK is not started"); + return false; + } + + /** + * Log the end of a fragment. + *

+ * A matching call to startFragment must be made before this is called. + * + * @param name the name of the fragment to log + */ + public boolean endView(@NonNull String name) { + if (isStarted()) { + internalEmbraceLogger.logDeveloper("Embrace", "Ending fragment: " + name); + return breadcrumbService.endView(name); + } + + internalEmbraceLogger.logDeveloper("Embrace", "Cannot end fragment, SDK is not started"); + return false; + } + + @InternalApi + public void sampleCurrentThreadDuringAnrs() { + try { + AnrService service = anrService; + if (service != null && nativeThreadSamplerInstaller != null) { + nativeThreadSamplerInstaller.monitorCurrentThread( + nativeThreadSampler, + configService, + service + ); + } else { + internalEmbraceLogger.logDeveloper("Embrace", "nativeThreadSamplerInstaller not started, cannot sample current thread"); + } + } catch (Exception exc) { + internalEmbraceLogger.logError("Failed to sample current thread during ANRs", exc); + } + } + + /** + * Logs the fact that a particular view was entered. + *

+ * If the previously logged view has the same name, a duplicate view breadcrumb will not be + * logged. + * + * @param screen the name of the view to log + */ + void logView(String screen) { + if (isStarted()) { + breadcrumbService.logView(screen, sdkClock.now()); + onActivityReported(); + } + + internalEmbraceLogger.logDeveloper("Embrace", "SDK not started, cannot log view"); + } + + /** + * Logs the fact that a particular view was entered. + *

+ * If the previously logged view has the same name, a duplicate view breadcrumb will not be + * logged. + * + * @param screen the name of the view to log + */ + public void logRnView(@NonNull String screen) { + if (appFramework != Embrace.AppFramework.REACT_NATIVE) { + InternalStaticEmbraceLogger.logWarning("[Embrace] logRnView is only available on React Native"); + return; + } + + logView(screen); + } + + /** + * Logs that a particular WebView URL was loaded. + * + * @param url the url to log + */ + void logWebView(String url) { + if (isStarted()) { + breadcrumbService.logWebView(url, sdkClock.now()); + onActivityReported(); + } + + internalEmbraceLogger.logDeveloper("Embrace", "SDK not started, cannot log view"); + } + + /** + * Logs a tap on a screen element. + * + * @param point the coordinates of the screen tap + * @param elementName the name of the element which was tapped + * @param type the type of tap that occurred + */ + void logTap(Pair point, String elementName, TapBreadcrumb.TapBreadcrumbType type) { + if (isStarted()) { + breadcrumbService.logTap(point, elementName, sdkClock.now(), type); + onActivityReported(); + } else { + internalEmbraceLogger.logDeveloper("Embrace", "SDK not started, cannot log tap"); + } + } + + @Nullable + @InternalApi + public ConfigService getConfigService() { + if (isStarted()) { + return configService; + } else { + internalEmbraceLogger.logSDKNotInitialized("get local config"); + } + return null; + } + + @Nullable + EventService getEventService() { + return eventService; + } + + @Nullable + ActivityService getActivityService() { + return activityService; + } + + @Nullable + EmbraceRemoteLogger getRemoteLogger() { + return remoteLogger; + } + + @Nullable + EmbraceInternalErrorService getExceptionsService() { + return exceptionsService; + } + + @Nullable + MetadataService getMetadataService() { + return metadataService; + } + + @Nullable + SessionService getSessionService() { + return sessionService; + } + + @Nullable + Application getApplication() { + return application; + } + + @Nullable + private Map normalizeProperties(@Nullable Map properties) { + Map normalizedProperties = new HashMap<>(); + if (properties != null) { + try { + internalEmbraceLogger.logDeveloper("Embrace", "normalizing properties"); + normalizedProperties = PropertyUtils.sanitizeProperties(properties); + } catch (Exception e) { + internalEmbraceLogger.logError("Exception occurred while normalizing the properties.", e); + } + return normalizedProperties; + } else { + return null; + } + } + + /** + * Gets the {@link EmbraceInternalInterface} that should be used as the sole source of + * communication with other Android SDK modules. + */ + @NonNull + EmbraceInternalInterface getEmbraceInternalInterface() { + return embraceInternalInterface; + } + + /** + * Gets the {@link ReactNativeInternalInterface} that should be used as the sole source of + * communication with the Android SDK for React Native. + */ + @Nullable + ReactNativeInternalInterface getReactNativeInternalInterface() { + return reactNativeInternalInterface; + } + + /** + * Gets the {@link UnityInternalInterface} that should be used as the sole source of + * communication with the Android SDK for Unity. + */ + @Nullable + UnityInternalInterface getUnityInternalInterface() { + return unityInternalInterface; + } + + /** + * Gets the {@link FlutterInternalInterface} that should be used as the sole source of + * communication with the Android SDK for Flutter. + */ + @Nullable + FlutterInternalInterface getFlutterInternalInterface() { + return flutterInternalInterface; + } + + public void installUnityThreadSampler() { + if (isStarted()) { + sampleCurrentThreadDuringAnrs(); + } else { + internalEmbraceLogger.logSDKNotInitialized("installUnityThreadSampler"); + } + } + + /** + * Saves captured push notification information into session payload + * + * @param title the title of the notification as a string (or null) + * @param body the body of the notification as a string (or null) + * @param topic the notification topic (if a user subscribed to one), or null + * @param id A unique ID identifying the message + * @param notificationPriority the notificationPriority of the message (as resolved on the device) + * @param messageDeliveredPriority the priority of the message (as resolved on the server) + */ + void logPushNotification( + @Nullable String title, + @Nullable String body, + @Nullable String topic, + @Nullable String id, + @Nullable Integer notificationPriority, + Integer messageDeliveredPriority, + PushNotificationBreadcrumb.NotificationType type) { + + pushNotificationService.logPushNotification( + title, + body, + topic, + id, + notificationPriority, + messageDeliveredPriority, + type + ); + onActivityReported(); + } + + private void onActivityReported() { + if (backgroundActivityService != null) { + backgroundActivityService.save(); + } + } + + public boolean shouldCaptureNetworkCall(String url, String method) { + return !networkCaptureService.getNetworkCaptureRules(url, method).isEmpty(); + } + + public void setProcessStartedByNotification() { + eventService.setProcessStartedByNotification(); + } + + public void trackWebViewPerformance(@NonNull String tag, @NonNull String message) { + if (configService.getWebViewVitalsBehavior().isWebViewVitalsEnabled()) { + webViewService.collectWebData(tag, message); + } + } + + /** + * Get the ID for the current session. + * Returns null if a session has not been started yet or the SDK hasn't been initialized. + * + * @return The ID for the current Session, if available. + */ + @Nullable + public String getCurrentSessionId() { + MetadataService localMetaDataService = metadataService; + if (isStarted() && localMetaDataService != null) { + String sessionId = localMetaDataService.getActiveSessionId(); + if (sessionId != null) { + return sessionId; + } else { + internalEmbraceLogger.logInfo("Session ID is null"); + } + } else { + internalEmbraceLogger.logSDKNotInitialized("getCurrentSessionId"); + } + return null; + } + + /** + * Get the end state of the last run of the application. + * + * @return LastRunEndState enum value representing the end state of the last run. + */ + @NonNull + public Embrace.LastRunEndState getLastRunEndState() { + if (isStarted() && crashVerifier != null) { + if (crashVerifier.didLastRunCrash()) { + return Embrace.LastRunEndState.CRASH; + } else { + return Embrace.LastRunEndState.CLEAN_EXIT; + } + } else { + return Embrace.LastRunEndState.INVALID; + } + } + + /** + * Loads the crash verifier to get the end state of the app crashed in the last run. + * This method is called when the app starts. + * + * @param crashModule an instance of {@link CrashModule} + * @param workerThreadModule an instance of {@link WorkerThreadModule} + */ + private void loadCrashVerifier(CrashModule crashModule, WorkerThreadModule workerThreadModule) { + crashVerifier = crashModule.getLastRunCrashVerifier(); + crashVerifier.readAndCleanMarkerAsync( + workerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION) + ); + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterface.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterface.java new file mode 100644 index 0000000000..6349808b69 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterface.java @@ -0,0 +1,215 @@ +package io.embrace.android.embracesdk; + +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; +import io.embrace.android.embracesdk.network.http.HttpMethod; +import io.embrace.android.embracesdk.network.http.NetworkCaptureData; + +/** + * Provides an internal interface to Embrace that is intended for use by hosted SDKs as their + * sole source of communication with the Android SDK. + */ +interface EmbraceInternalInterface { + + /** + * {@see Embrace#logInfo} + */ + void logInfo(@NonNull String message, + @Nullable Map properties); + + /** + * {@see Embrace#logWarning} + */ + void logWarning(@NonNull String message, + @Nullable Map properties, + @Nullable String stacktrace); + + /** + * {@see Embrace#logError} + */ + void logError(@NonNull String message, + @Nullable Map properties, + @Nullable String stacktrace, + boolean isException); + + /** + * {@see Embrace#logHandledException} + */ + void logHandledException(@NonNull Throwable throwable, + @NonNull LogType type, + @Nullable Map properties, + @Nullable StackTraceElement[] customStackTrace); + + /** + * {@see Embrace#logBreadcrumb} + */ + void addBreadcrumb(@NonNull String message); + + /** + * {@see Embrace#getDeviceId} + */ + @NonNull + String getDeviceId(); + + /** + * {@see Embrace#setUsername} + */ + void setUsername(@Nullable String username); + + /** + * {@see Embrace#clearUsername} + */ + void clearUsername(); + + /** + * {@see Embrace#setUserIdentifier} + */ + void setUserIdentifier(@Nullable String userId); + + /** + * {@see Embrace#clearUserIdentifier} + */ + void clearUserIdentifier(); + + /** + * {@see Embrace#setUserEmail} + */ + void setUserEmail(@Nullable String email); + + /** + * {@see Embrace#clearUserEmail} + */ + void clearUserEmail(); + + /** + * {@see Embrace#setUserAsPayer} + */ + void setUserAsPayer(); + + /** + * {@see Embrace#clearUserAsPayer} + */ + void clearUserAsPayer(); + + /** + * {@see Embrace#addUserPersona} + */ + void addUserPersona(@NonNull String persona); + + /** + * {@see Embrace#clearUserPersona} + */ + void clearUserPersona(@NonNull String persona); + + /** + * {@see Embrace#clearAllUserPersonas} + */ + void clearAllUserPersonas(); + + /** + * {@see Embrace#addSessionProperty} + */ + boolean addSessionProperty(@NonNull String key, + @NonNull String value, + boolean permanent); + + /** + * {@see Embrace#removeSessionProperty} + */ + boolean removeSessionProperty(@NonNull String key); + + /** + * {@see Embrace#getSessionProperties} + */ + @Nullable + Map getSessionProperties(); + + /** + * {@see Embrace#startEvent} + */ + void startMoment(@NonNull String name, + @Nullable String identifier, + @Nullable Map properties); + + /** + * {@see Embrace#endMoment} + */ + void endMoment(@NonNull String name, + @Nullable String identifier, + @Nullable Map properties); + + /** + * {@see Embrace#startFragment} + */ + boolean startView(@NonNull String name); + + /** + * {@see Embrace#endFragment} + */ + boolean endView(@NonNull String name); + + /** + * {@see Embrace#endAppStartup} + */ + void endAppStartup(@NonNull Map properties); + + /** + * {@see Embrace#logInternalError} + */ + void logInternalError(@Nullable String message, @Nullable String details); + + /** + * {@see Embrace#endSession} + */ + void endSession(boolean clearUserInfo); + + /** + * See {@link Embrace#recordNetworkRequest(EmbraceNetworkRequest)} + */ + void recordCompletedNetworkRequest(@NonNull String url, + @NonNull String httpMethod, + long startTime, + long endTime, + long bytesSent, + long bytesReceived, + int statusCode, + @Nullable String traceId, + @Nullable NetworkCaptureData networkCaptureData); + + /** + * See {@link Embrace#recordNetworkRequest(EmbraceNetworkRequest)} + */ + void recordIncompleteNetworkRequest(@NonNull String url, + @NonNull String httpMethod, + long startTime, + long endTime, + @Nullable Throwable error, + @Nullable String traceId, + @Nullable NetworkCaptureData networkCaptureData); + + /** + * See {@link Embrace#recordNetworkRequest(EmbraceNetworkRequest)} + */ + void recordIncompleteNetworkRequest(@NonNull String url, + @NonNull String httpMethod, + long startTime, + long endTime, + @Nullable String errorType, + @Nullable String errorMessage, + @Nullable String traceId, + @Nullable NetworkCaptureData networkCaptureData); + + /** + * Logs a tap on a Compose screen element. + * + * @param point the coordinates of the screen tap + * @param elementName the name of the element which was tapped + */ + void logComposeTap(@NonNull Pair point, @NonNull String elementName); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt new file mode 100644 index 0000000000..e988cb4352 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt @@ -0,0 +1,254 @@ +package io.embrace.android.embracesdk + +import android.util.Pair +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.payload.TapBreadcrumb + +internal class EmbraceInternalInterfaceImpl( + private val embrace: EmbraceImpl +) : EmbraceInternalInterface { + + override fun logInfo(message: String, properties: Map?) { + embrace.logMessage( + EmbraceEvent.Type.INFO_LOG, + message, + properties, + null, + null, + LogExceptionType.NONE, + null, + null + ) + } + + override fun logWarning( + message: String, + properties: Map?, + stacktrace: String? + ) { + embrace.logMessage( + EmbraceEvent.Type.WARNING_LOG, + message, + properties, + null, + stacktrace, + LogExceptionType.NONE, + null, + null + ) + } + + override fun logError( + message: String, + properties: Map?, + stacktrace: String?, + isException: Boolean, + ) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + message, + properties, + null, + stacktrace, + LogExceptionType.NONE, + null, + null + ) + } + + override fun logHandledException( + throwable: Throwable, + type: LogType, + properties: Map?, + customStackTrace: Array? + ) { + embrace.logMessage( + type.toEventType(), + throwable.message ?: "", + properties, + customStackTrace ?: throwable.stackTrace, + null, + LogExceptionType.NONE, + null, + null + ) + } + + override fun addBreadcrumb(message: String) { + embrace.addBreadcrumb(message) + } + + override fun getDeviceId(): String { + return embrace.deviceId + } + + override fun setUserIdentifier(userId: String?) { + embrace.setUserIdentifier(userId) + } + + override fun clearUserIdentifier() { + embrace.clearUserIdentifier() + } + + override fun setUsername(username: String?) { + embrace.setUsername(username) + } + + override fun clearUsername() { + embrace.clearUsername() + } + + override fun setUserEmail(email: String?) { + embrace.setUserEmail(email) + } + + override fun clearUserEmail() { + embrace.clearUserEmail() + } + + override fun setUserAsPayer() { + embrace.setUserAsPayer() + } + + override fun clearUserAsPayer() { + embrace.clearUserAsPayer() + } + + override fun addUserPersona(persona: String) { + embrace.addUserPersona(persona) + } + + override fun clearUserPersona(persona: String) { + embrace.clearUserPersona(persona) + } + + override fun clearAllUserPersonas() { + embrace.clearAllUserPersonas() + } + + override fun addSessionProperty(key: String, value: String, permanent: Boolean): Boolean { + return embrace.addSessionProperty(key, value, permanent) + } + + override fun removeSessionProperty(key: String): Boolean { + return embrace.removeSessionProperty(key) + } + + override fun getSessionProperties(): Map? { + return embrace.sessionProperties + } + + override fun startMoment( + name: String, + identifier: String?, + properties: Map? + ) { + embrace.startMoment(name, identifier, properties) + } + + override fun endMoment(name: String, identifier: String?, properties: Map?) { + embrace.endMoment(name, identifier, properties) + } + + override fun startView(name: String): Boolean { + return embrace.startView(name) + } + + override fun endView(name: String): Boolean { + return embrace.endView(name) + } + + override fun endAppStartup(properties: Map) { + embrace.endAppStartup(properties) + } + + override fun logInternalError(message: String?, details: String?) { + embrace.logInternalError(message, details) + } + + override fun endSession(clearUserInfo: Boolean) { + embrace.endSession(clearUserInfo) + } + + override fun logComposeTap(point: Pair, elementName: String) { + embrace.logTap(point, elementName, TapBreadcrumb.TapBreadcrumbType.TAP) + } + + override fun recordCompletedNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + bytesSent: Long, + bytesReceived: Long, + statusCode: Int, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) { + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromCompletedRequest( + url, + HttpMethod.fromString(httpMethod), + startTime, + endTime, + bytesSent, + bytesReceived, + statusCode, + traceId, + null, + networkCaptureData + ) + ) + } + + override fun recordIncompleteNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + error: Throwable?, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) { + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromIncompleteRequest( + url, + HttpMethod.fromString(httpMethod), + startTime, + endTime, + error?.javaClass?.canonicalName ?: "", + error?.localizedMessage ?: "", + traceId, + null, + networkCaptureData + ) + ) + } + + override fun recordIncompleteNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + errorType: String?, + errorMessage: String?, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) { + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromIncompleteRequest( + url, + HttpMethod.fromString(httpMethod), + startTime, + endTime, + errorType ?: "", + errorMessage ?: "", + traceId, + null, + networkCaptureData + ) + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceSamples.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceSamples.kt new file mode 100644 index 0000000000..b1139a49d8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceSamples.kt @@ -0,0 +1,75 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.samples.EmbraceCrashSamples + +/** + * Helps to verify and test embrace SDK integration. + * it allows users to execute code that automatically verifies the integration by calling the verifyIntegration method. + * It also provides example code to generate ANR and JVM/NDK crashes + */ +public object EmbraceSamples { + + private val embraceCrashSamples = EmbraceCrashSamples + private val embraceAutomaticVerification = EmbraceAutomaticVerification() + + /** + * Starts an automatic verification of the following Embrace features: + * - Log a Breadcrumb + * - Set user data + * - Add info, warning and error logs + * - Start and end a moment + * - Executes a GET request + * - Add the trace id to the request (default or the one specified in the local config) + * - Check the current and the latest SDK version + * - Execute a POST request + * - Execute a bad request + * - Trigger an ANR + * - Throw an Exception (yep, the application will be relaunch) + * + * Then, that information can be verified in user sessions dashboard + */ + @JvmStatic + public fun verifyIntegration() { + embraceAutomaticVerification.verifyIntegration() + } + + /** + * Throw a custom JVM crash to be part of current session. + * + * It is recommended to implement this method call via a button press once the app has loaded. + * + * After a crash is sent, the app should be restarted in order to see the error in the dashboard. + * + * @throws EmbraceSampleCodeException + */ + @JvmStatic + public fun throwJvmException() { + embraceCrashSamples.throwJvmException() + } + + /** + * Force a short ANR that lasts 4 seconds + */ + @JvmStatic + public fun triggerAnr() { + embraceCrashSamples.blockMainThreadForShortInterval() + } + + /** + * Force a long ANR that lasts 30 seconds + */ + @JvmStatic + public fun triggerLongAnr() { + embraceCrashSamples.triggerLongAnr() + } + + // NDK Crashes sections + + /** + * Throws a ndk SigIllegalInstruction + */ + @JvmStatic + public fun causeNdkIllegalInstruction() { + embraceCrashSamples.triggerNdkSigIllegalInstruction() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt new file mode 100644 index 0000000000..1a2e4a69cf --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk + +/** + * Provides an internal interface to Embrace that is intended for use by Flutter as its + * sole source of communication with the Android SDK. + */ +internal interface FlutterInternalInterface : EmbraceInternalInterface { + + /** + * See [Embrace.setEmbraceFlutterSdkVersion] + */ + fun setEmbraceFlutterSdkVersion(version: String?) + + /** + * See [Embrace.setDartVersion] + */ + fun setDartVersion(version: String?) + + /** + * See [Embrace.logHandledDartException] + */ + fun logHandledDartException( + stack: String?, + name: String?, + message: String?, + context: String?, + library: String? + ) + + /** + * See [Embrace.logUnhandledDartException] + */ + fun logUnhandledDartException( + stack: String?, + name: String?, + message: String?, + context: String?, + library: String? + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt new file mode 100644 index 0000000000..09f47f8f6c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt @@ -0,0 +1,78 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger + +internal class FlutterInternalInterfaceImpl( + private val embrace: EmbraceImpl, + private val impl: EmbraceInternalInterface, + private val metadataService: MetadataService, + private val logger: InternalEmbraceLogger +) : EmbraceInternalInterface by impl, FlutterInternalInterface { + + override fun setEmbraceFlutterSdkVersion(version: String?) { + if (embrace.isStarted) { + if (version != null) { + metadataService.setEmbraceFlutterSdkVersion(version) + } + } else { + logger.logSDKNotInitialized("setEmbraceFlutterSdkVersion") + } + } + + override fun setDartVersion(version: String?) { + if (embrace.isStarted) { + if (version != null) { + metadataService.setDartVersion(version) + } + } else { + logger.logSDKNotInitialized("setDartVersion") + } + } + + override fun logHandledDartException( + stack: String?, + name: String?, + message: String?, + context: String?, + library: String? + ) { + logDartException(stack, name, message, context, library, LogExceptionType.HANDLED) + } + + override fun logUnhandledDartException( + stack: String?, + name: String?, + message: String?, + context: String?, + library: String? + ) { + logDartException(stack, name, message, context, library, LogExceptionType.UNHANDLED) + } + + private fun logDartException( + stack: String?, + name: String?, + message: String?, + context: String?, + library: String?, + exceptionType: LogExceptionType + ) { + if (embrace.isStarted) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "Dart error", + null, + null, + stack, + exceptionType, + context, + library, + name, + message + ) + } else { + logger.logSDKNotInitialized("logDartError") + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/HttpPathOverrideRequest.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/HttpPathOverrideRequest.java new file mode 100644 index 0000000000..0c1609bb75 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/HttpPathOverrideRequest.java @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk; + +@InternalApi +public interface HttpPathOverrideRequest { + String getHeaderByName(String name); + + @SuppressWarnings("AbbreviationAsWordInNameCheck") + String getOverriddenURL(String pathOverride); + + @SuppressWarnings("AbbreviationAsWordInNameCheck") + String getURLString(); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalApi.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalApi.kt new file mode 100644 index 0000000000..949eb99f94 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalApi.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk + +/** + * Classes and methods marked with this annotation are part of Embrace's Internal API + * and may change without warning, so are used at your own risk. Please contact support@embrace.io + * if you have a use case that cannot be fulfilled using our public API. + */ +public annotation class InternalApi diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt new file mode 100644 index 0000000000..6bdfe6c318 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt @@ -0,0 +1,57 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.injection.AndroidServicesModule +import io.embrace.android.embracesdk.injection.CoreModule +import io.embrace.android.embracesdk.injection.CrashModule +import io.embrace.android.embracesdk.injection.EssentialServiceModule +import io.embrace.android.embracesdk.injection.singleton + +internal interface InternalInterfaceModule { + val embraceInternalInterface: EmbraceInternalInterface + val reactNativeInternalInterface: ReactNativeInternalInterface + val unityInternalInterface: UnityInternalInterface + val flutterInternalInterface: FlutterInternalInterface +} + +internal class InternalInterfaceModuleImpl( + coreModule: CoreModule, + androidServicesModule: AndroidServicesModule, + essentialServiceModule: EssentialServiceModule, + embrace: EmbraceImpl, + crashModule: CrashModule +) : InternalInterfaceModule { + + override val embraceInternalInterface: EmbraceInternalInterface by singleton { + EmbraceInternalInterfaceImpl(embrace) + } + + override val reactNativeInternalInterface: ReactNativeInternalInterface by singleton { + ReactNativeInternalInterfaceImpl( + embrace, + embraceInternalInterface, + coreModule.appFramework, + androidServicesModule.preferencesService, + crashModule.crashService, + essentialServiceModule.metadataService, + coreModule.logger + ) + } + + override val unityInternalInterface: UnityInternalInterface by singleton { + UnityInternalInterfaceImpl( + embrace, + embraceInternalInterface, + androidServicesModule.preferencesService, + coreModule.logger + ) + } + + override val flutterInternalInterface: FlutterInternalInterface by singleton { + FlutterInternalInterfaceImpl( + embrace, + embraceInternalInterface, + essentialServiceModule.metadataService, + coreModule.logger + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogExceptionType.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogExceptionType.kt new file mode 100644 index 0000000000..47055572fd --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogExceptionType.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk + +/** + * Enum representing the type of exception that occurred. + * NONE is for a native android log, whether have or not an exception. + * HANDLED or UNHANDLED are ONLY for Unity and Flutter handled and unhandled exceptions. + */ +@InternalApi +public enum class LogExceptionType(internal val value: String) { + NONE("none"), + HANDLED("handled"), + UNHANDLED("unhandled") +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogType.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogType.java new file mode 100644 index 0000000000..d3d34a436d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogType.java @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk; + +/** + * Deprecated: use Severity instead. This enum is deprecated and will be removed in + * a future release. + * + * Will flag the message as one of info, warning, or error for filtering on the dashboard + */ +@Deprecated +public enum LogType { + INFO, + WARNING, + ERROR; + + final EmbraceEvent.Type toEventType() { + switch (this) { + case WARNING: + return EmbraceEvent.Type.WARNING_LOG; + case ERROR: + return EmbraceEvent.Type.ERROR_LOG; + default: + return EmbraceEvent.Type.INFO_LOG; + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogsApi.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogsApi.java new file mode 100644 index 0000000000..caa73b264c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/LogsApi.java @@ -0,0 +1,175 @@ +package io.embrace.android.embracesdk; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +/** + * The public API that is used to send log messages. + */ +interface LogsApi { + + /** + * Remotely logs a message at the given severity level. These log messages will appear as part of the session + * timeline, and can be used to describe what was happening at a particular time within the app. + * + * @param message the message to remotely log + * @param severity the severity level of the log message + */ + void logMessage(@NonNull String message, + @NonNull Severity severity); + + /** + * Remotely logs a message at the given severity level. These log messages will appear as part of the session + * timeline, and can be used to describe what was happening at a particular time within the app. + * + * @param message the message to remotely log + * @param severity the severity level of the log message + * @param properties the properties to attach to the log message + */ + void logMessage(@NonNull String message, + @NonNull Severity severity, + @Nullable Map properties); + + /** + * Remotely logs a message at INFO level. These log messages will appear as part of the session + * timeline, and can be used to describe what was happening at a particular time within the app. + * + * @param message the message to remotely log + */ + void logInfo(@NonNull String message); + + /** + * Remotely logs a message at WARN level. These log messages will appear as part of the session + * timeline, and can be used to describe what was happening at a particular time within the app. + * + * @param message the message to remotely log + */ + void logWarning(@NonNull String message); + + /** + * Remotely logs a message at ERROR level. These log messages will appear as part of the session + * timeline, and can be used to describe what was happening at a particular time within the app. + * + * @param message the message to remotely log + */ + void logError(@NonNull String message); + + /** + * Remotely logs a Throwable/Exception at ERROR level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param throwable the throwable to remotely log + */ + void logException(@NonNull Throwable throwable); + + /** + * Remotely logs a Throwable/Exception at given severity level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param throwable the throwable to remotely log + * @param severity the severity level of the log message + */ + void logException(@NonNull Throwable throwable, + @NonNull Severity severity); + + /** + * Remotely logs a Throwable/Exception at given severity level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param throwable the throwable to remotely log + * @param severity the severity level of the log message + * @param properties custom key-value pairs to include with the log message + */ + void logException(@NonNull Throwable throwable, + @NonNull Severity severity, + @Nullable Map properties); + + /** + * Remotely logs a Throwable/Exception at given severity level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param throwable the throwable to remotely log + * @param severity the severity level of the log message + * @param properties custom key-value pairs to include with the log message + * @param message the message to remotely log instead of the throwable message + */ + void logException(@NonNull Throwable throwable, + @NonNull Severity severity, + @Nullable Map properties, + @Nullable String message); + + /** + * Remotely logs a custom stacktrace at ERROR level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param stacktraceElements the stacktrace to remotely log + */ + void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements); + + /** + * Remotely logs a custom stacktrace at given severity level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param stacktraceElements the stacktrace to remotely log + * @param severity the severity level of the log message + */ + void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, + @NonNull Severity severity); + + /** + * Remotely logs a custom stacktrace at given severity level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param stacktraceElements the stacktrace to remotely log + * @param severity the severity level of the log message + * @param properties custom key-value pairs to include with the log message + */ + void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, + @NonNull Severity severity, + @Nullable Map properties); + + /** + * Remotely logs a custom stacktrace at given severity level. These log messages and stacktraces + * will appear as part of the session timeline, and can be used to describe what was happening + * at a particular time within the app. + * + * @param stacktraceElements the stacktrace to remotely log + * @param severity the severity level of the log message + * @param properties custom key-value pairs to include with the log message + * @param message the message to remotely log instead of the throwable message + */ + void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, + @NonNull Severity severity, + @Nullable Map properties, + @Nullable String message); + + /** + * Saves captured push notification information into session payload + * + * @param title the title of the notification as a string (or null) + * @param body the body of the notification as a string (or null) + * @param topic the notification topic (if a user subscribed to one), or null + * @param id A unique ID identifying the message + * @param notificationPriority the notificationPriority of the message (as resolved on the device) + * @param messageDeliveredPriority the priority of the message (as resolved on the server) + * @param isNotification if it is a notification message. + * @param hasData if the message contains payload data. + */ + void logPushNotification(@Nullable String title, + @Nullable String body, + @Nullable String topic, + @Nullable String id, + @Nullable Integer notificationPriority, + @NonNull Integer messageDeliveredPriority, + @NonNull Boolean isNotification, + @NonNull Boolean hasData); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/MomentsApi.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/MomentsApi.java new file mode 100644 index 0000000000..d65c31c71b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/MomentsApi.java @@ -0,0 +1,102 @@ +package io.embrace.android.embracesdk; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +/** + * The public API that is used to start & end moments. + */ +interface MomentsApi { + + /** + * Starts a 'moment'. Moments are used for encapsulating particular activities within + * the app, such as a user adding an item to their shopping cart. + *

+ * The length of time a moment takes to execute is recorded. + * + * @param name a name identifying the moment + */ + void startMoment(@NonNull String name); + + /** + * Starts a 'moment'. Moments are used for encapsulating particular activities within + * the app, such as a user adding an item to their shopping cart. + *

+ * The length of time a moment takes to execute is recorded. + * + * @param name a name identifying the moment + * @param identifier an identifier distinguishing between multiple moments with the same name + */ + void startMoment(@NonNull String name, @Nullable String identifier); + + /** + * Starts a 'moment'. Moments are used for encapsulating particular activities within + * the app, such as a user adding an item to their shopping cart. + *

+ * The length of time a moment takes to execute is recorded + * + * @param name a name identifying the moment + * @param identifier an identifier distinguishing between multiple moments with the same name + * @param properties custom key-value pairs to provide with the moment + */ + void startMoment(@NonNull String name, + @Nullable String identifier, + @Nullable Map properties); + + /** + * Signals the end of a moment with the specified name. + *

+ * The duration of the moment is computed. + * + * @param name the name of the moment to end + */ + void endMoment(@NonNull String name); + + /** + * Signals the end of a moment with the specified name. + *

+ * The duration of the moment is computed. + * + * @param name the name of the moment to end + * @param identifier the identifier of the moment to end, distinguishing between moments with the same name + */ + void endMoment(@NonNull String name, @Nullable String identifier); + + /** + * Signals the end of a moment with the specified name. + *

+ * The duration of the moment is computed. + * + * @param name the name of the moment to end + * @param properties custom key-value pairs to provide with the moment + */ + void endMoment(@NonNull String name, + @Nullable Map properties); + + /** + * Signals the end of a moment with the specified name. + *

+ * The duration of the moment is computed. + * + * @param name the name of the moment to end + * @param identifier the identifier of the moment to end, distinguishing between moments with the same name + * @param properties custom key-value pairs to provide with the moment + */ + void endMoment(@NonNull String name, + @Nullable String identifier, + @Nullable Map properties); + + /** + * Signals that the app has completed startup. + */ + void endAppStartup(); + + /** + * Signals that the app has completed startup. + * + * @param properties properties to include as part of the startup moment + */ + void endAppStartup(@NonNull Map properties); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/NetworkRequestApi.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/NetworkRequestApi.kt new file mode 100644 index 0000000000..99724d67b1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/NetworkRequestApi.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest + +/** + * The public API that is used for capturing network requests manually + */ +internal interface NetworkRequestApi { + + /** + * Logs the fact that a network call occurred. These are recorded and sent to Embrace as part + * of a particular session. + * + * You can create an instance of [EmbraceNetworkRequest] using the factory functions. + */ + fun recordNetworkRequest(networkRequest: EmbraceNetworkRequest) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt new file mode 100644 index 0000000000..0cf73a40cd --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt @@ -0,0 +1,42 @@ +package io.embrace.android.embracesdk + +import android.content.Context + +/** + * Provides an internal interface to Embrace that is intended for use by React Native as its + * sole source of communication with the Android SDK. + */ +internal interface ReactNativeInternalInterface : EmbraceInternalInterface { + + /** + * See [Embrace.logUnhandledJsException] + */ + fun logUnhandledJsException( + name: String, + message: String, + type: String?, + stacktrace: String? + ) + + fun logHandledJsException( + name: String, + message: String, + properties: Map, + stacktrace: String? + ) + + /** + * See [Embrace.setJavaScriptPatchNumber] + */ + fun setJavaScriptPatchNumber(number: String?) + + /** + * See [Embrace.setReactNativeVersionNumber] + */ + fun setReactNativeVersionNumber(version: String?) + + /** + * See [Embrace.setJavaScriptBundleURL] + */ + fun setJavaScriptBundleUrl(context: Context, url: String) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt new file mode 100644 index 0000000000..67e94f4945 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt @@ -0,0 +1,111 @@ +package io.embrace.android.embracesdk + +import android.content.Context +import io.embrace.android.embracesdk.Embrace.AppFramework +import io.embrace.android.embracesdk.capture.crash.CrashService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.JsException +import io.embrace.android.embracesdk.prefs.PreferencesService + +internal class ReactNativeInternalInterfaceImpl( + private val embrace: EmbraceImpl, + private val impl: EmbraceInternalInterface, + private val framework: AppFramework, + private val preferencesService: PreferencesService, + private val crashService: CrashService, + private val metadataService: MetadataService, + private val logger: InternalEmbraceLogger +) : EmbraceInternalInterface by impl, ReactNativeInternalInterface { + + override fun logUnhandledJsException( + name: String, + message: String, + type: String?, + stacktrace: String? + ) { + if (embrace.isStarted) { + val exception = JsException(name, message, type, stacktrace) + logger.logDeveloper( + "Embrace", + "Log Unhandled JS exception: $name -- stacktrace: $stacktrace" + ) + crashService.logUnhandledJsException(exception) + } else { + logger.logSDKNotInitialized("log JS exception") + } + } + + override fun logHandledJsException( + name: String, + message: String, + properties: Map, + stacktrace: String? + ) { + if (embrace.isStarted) { + logger.logDeveloper( + "Embrace", + "Log Handled JS exception: $name -- stacktrace: $stacktrace" + ) + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + message, + properties, + null, + stacktrace, + LogExceptionType.HANDLED, + null, + null + ) + } else { + logger.logSDKNotInitialized("log JS exception") + } + } + + override fun setJavaScriptPatchNumber(number: String?) { + if (embrace.isStarted) { + if (number == null) { + logger.logError("JavaScript patch number must not be null") + return + } + if (number.isEmpty()) { + logger.logError("JavaScript patch number must have non-zero length") + return + } + preferencesService.javaScriptPatchNumber = number + } else { + logger.logSDKNotInitialized("set JavaScript patch number") + } + } + + override fun setReactNativeVersionNumber(version: String?) { + if (embrace.isStarted) { + if (version == null) { + logger.logError("ReactNative version must not be null") + return + } + if (version.isEmpty()) { + logger.logError("ReactNative version must have non-zero length") + return + } + preferencesService.reactNativeVersionNumber = version + } else { + logger.logSDKNotInitialized("set React Native version number") + } + } + + override fun setJavaScriptBundleUrl(context: Context, url: String) { + if (embrace.isStarted) { + if (framework != AppFramework.REACT_NATIVE) { + logger.logError( + "Failed to set Java Script bundle ID URL. Current framework: " + + framework.name + " is not React Native." + ) + return + } + metadataService.setReactNativeBundleId(context, url) + } else { + logger.logSDKNotInitialized("set JavaScript bundle URL") + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/SessionApi.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/SessionApi.kt new file mode 100644 index 0000000000..21ef3ca1f6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/SessionApi.kt @@ -0,0 +1,52 @@ +package io.embrace.android.embracesdk + +/** + * The public API that is used to interact with sessions. + */ +internal interface SessionApi { + + /** + * Adds a property to the current session, overwriting any previous property set with the given key. If a permanent property + * already exists with the given name and a non-permanent one is to be added, the permanent one will be removed (and vice versa). + * + * @param key The case-sensitive key to be used for this property. The maximum length for this is 128 characters. A key passed in + * that exceeds the maximum length will be truncated. + * @param value The value associated with the given key. The maximum length for this is 1024 characters. A value passed in that + * exceeds the maximum length will be truncated. + * @param permanent True if this property should be added to subsequent sessions going forward, persisting through app launches. + * + * @return True if the property was successfully added. Reasons this may fail include an invalid key or value, or if the + * session has exceeded its total properties limit. + */ + fun addSessionProperty( + key: String, + value: String, + permanent: Boolean + ): Boolean + + /** + * Removes a property from the current session. + * + * @return true if a property with that name had previously existed. + */ + fun removeSessionProperty(key: String): Boolean + + /** + * Retrieves a map of the current session properties. + * + * @return a new immutable map containing the current session properties, or null if the SDK has not been started or has been stopped. + */ + fun getSessionProperties(): Map? + + /** + * Ends the current session and starts a new one. + */ + fun endSession() + + /** + * Ends the current session and starts a new one. + * + * @param clearUserInfo Pass in true to clear all user info set on this device. + */ + fun endSession(clearUserInfo: Boolean) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Severity.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Severity.java new file mode 100644 index 0000000000..3d22de1850 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Severity.java @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk; + +/** + * The severity of the log message. + */ +public enum Severity { + + /** + * Reports log messages with info level severity. + */ + INFO, + + /** + * Reports log messages with warning level severity. + */ + WARNING, + + /** + * Reports log messages with error level severity. + */ + ERROR +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt new file mode 100644 index 0000000000..a10e3f0b3a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk + +/** + * Provides an internal interface to Embrace that is intended for use by Unity as its + * sole source of communication with the Android SDK. + */ +internal interface UnityInternalInterface : EmbraceInternalInterface { + + /** + * See [Embrace.setUnityMetaData] + */ + fun setUnityMetaData(unityVersion: String?, buildGuid: String?, unitySdkVersion: String?) + + /** + * See [Embrace.logUnhandledUnityException] + */ + fun logUnhandledUnityException( + name: String, + message: String, + stacktrace: String? + ) + + /** + * See [Embrace.logHandledUnityException] + */ + fun logHandledUnityException( + name: String, + message: String, + stacktrace: String? + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt new file mode 100644 index 0000000000..c32e224131 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt @@ -0,0 +1,108 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.PreferencesService + +internal class UnityInternalInterfaceImpl( + private val embrace: EmbraceImpl, + private val impl: EmbraceInternalInterface, + private val preferencesService: PreferencesService, + private val logger: InternalEmbraceLogger +) : EmbraceInternalInterface by impl, UnityInternalInterface { + + override fun setUnityMetaData( + unityVersion: String?, + buildGuid: String?, + unitySdkVersion: String? + ) { + if (embrace.isStarted) { + if (unityVersion == null || buildGuid == null) { + val sdkVersionMessage = unitySdkVersion ?: "null or previous than 1.7.5" + logger.logError( + "Unity metadata is corrupted or malformed. Unity version is " + + unityVersion + ", Unity build id is " + buildGuid + + " and Unity SDK version is " + sdkVersionMessage + ) + return + } + val unityVersionNumber = preferencesService.unityVersionNumber + if (unityVersionNumber != null) { + logger.logDeveloper("Embrace", "Unity version number is present") + if (unityVersion != unityVersionNumber) { + logger.logDeveloper( + "Embrace", + "Setting a new Unity version number" + ) + preferencesService.unityVersionNumber = unityVersion + } + } else { + logger.logDeveloper("Embrace", "Setting Unity version number") + preferencesService.unityVersionNumber = unityVersion + } + val unityBuildIdNumber = preferencesService.unityBuildIdNumber + if (unityBuildIdNumber != null) { + logger.logDeveloper("Embrace", "Unity build id is present") + if (buildGuid != unityBuildIdNumber) { + logger.logDeveloper("Embrace", "Setting a Unity new build id") + preferencesService.unityBuildIdNumber = buildGuid + } + } else { + logger.logDeveloper("Embrace", "Setting Unity build id") + preferencesService.unityBuildIdNumber = buildGuid + } + if (unitySdkVersion == null) { + logger.logDeveloper("Embrace", "Unity SDK version is null.") + return + } + val unitySdkVersionNumber = preferencesService.unitySdkVersionNumber + if (unitySdkVersionNumber != null) { + logger.logDeveloper("Embrace", "Unity SDK version number is present") + if (unitySdkVersion != unitySdkVersionNumber) { + logger.logDeveloper( + "Embrace", + "Setting a new Unity SDK version number" + ) + preferencesService.unitySdkVersionNumber = unitySdkVersion + } + } else { + logger.logDeveloper("Embrace", "Setting Unity SDK version number") + preferencesService.unitySdkVersionNumber = unitySdkVersion + } + } else { + logger.logSDKNotInitialized("set Unity metadata") + } + } + + override fun logUnhandledUnityException(name: String, message: String, stacktrace: String?) { + logUnityException(name, message, stacktrace, LogExceptionType.UNHANDLED) + } + + override fun logHandledUnityException(name: String, message: String, stacktrace: String?) { + logUnityException(name, message, stacktrace, LogExceptionType.HANDLED) + } + + private fun logUnityException( + name: String?, + message: String, + stacktrace: String?, + exceptionType: LogExceptionType + ) { + if (embrace.isStarted) { + logger.logError("message: $message -- stacktrace: $stacktrace") + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "Unity exception", + null, + null, + stacktrace, + exceptionType, + null, + null, + name, + message + ) + } else { + logger.logSDKNotInitialized("log Unity exception") + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UserApi.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UserApi.kt new file mode 100644 index 0000000000..15c19ae0ff --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UserApi.kt @@ -0,0 +1,76 @@ +package io.embrace.android.embracesdk + +/** + * The public API that is used to set user information. + */ +internal interface UserApi { + + /** + * Sets the user ID. This would typically be some form of unique identifier such as a UUID or database key for the user. + * This ID will persist across app launches until it is explicitly removed using [.clearUserIdentifier] + * or otherwise cleared. + * + * @param userId the unique identifier for the user + */ + fun setUserIdentifier(userId: String?) + + /** + * Clears the currently set user ID. For example, if the user logs out. + */ + fun clearUserIdentifier() + + /** + * Sets the current user's email address. + * + * @param email the email address of the current user + */ + fun setUserEmail(email: String?) + + /** + * Clears the currently set user's email address. + */ + fun clearUserEmail() + + /** + * Sets this user as a paying user. This adds a persona to the user's identity. + */ + fun setUserAsPayer() + + /** + * Clears this user as a paying user. This would typically be called if a user is no longer + * paying for the service and has reverted back to a basic user. + */ + fun clearUserAsPayer() + + /** + * Adds a custom user persona. A persona is a trait associated with a given user. A maximum + * of 10 personas can be set. + * + * @param persona the persona to set + */ + fun addUserPersona(persona: String) + + /** + * Clears the custom user persona, if it is set. + * + * @param persona the persona to clear + */ + fun clearUserPersona(persona: String) + + /** + * Clears all custom user personas from the user. + */ + fun clearAllUserPersonas() + + /** + * Sets the username of the currently logged in user. + * + * @param username the username to set + */ + fun setUsername(username: String?) + + /** + * Clears the username of the currently logged in user, for example if the user has logged out. + */ + fun clearUsername() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ViewSwazzledHooks.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ViewSwazzledHooks.java new file mode 100644 index 0000000000..0b0d212ac7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ViewSwazzledHooks.java @@ -0,0 +1,64 @@ +package io.embrace.android.embracesdk; + +import android.util.Pair; + +import io.embrace.android.embracesdk.payload.TapBreadcrumb.TapBreadcrumbType; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; + +@InternalApi +public final class ViewSwazzledHooks { + + private static final String UNKNOWN_ELEMENT_NAME = "Unknown element"; + + private ViewSwazzledHooks() { + } + + static void logOnClickEvent(android.view.View view, TapBreadcrumbType breadcrumbType) { + try { + String viewName = ""; + try { + viewName = view.getResources().getResourceName(view.getId()); + } catch (Exception e) { + viewName = UNKNOWN_ELEMENT_NAME; + } + Pair point = null; + try { + point = new Pair<>(view.getX(), view.getY()); + } catch (Exception e) { + point = new Pair<>(0.0F, 0.0F); + } + Embrace.getImpl().logTap(point, viewName, breadcrumbType); + } catch (NoSuchMethodError exception) { + // The customer may be overwriting View with their own implementation, and some of the + // methods we use are missing. + InternalStaticEmbraceLogger.logError("Could not log onClickEvent. Some methods are missing. ", + exception); + } catch (Exception exception) { + InternalStaticEmbraceLogger.logError("Could not log onClickEvent.", exception); + } + } + + @InternalApi + public static final class OnClickListener { + private OnClickListener() { + } + + @SuppressWarnings("MethodNameCheck") + public static void _preOnClick(android.view.View.OnClickListener thiz, android.view.View view) { + logOnClickEvent(view, TapBreadcrumbType.TAP); + } + } + + @InternalApi + public static final class OnLongClickListener { + private OnLongClickListener() { + } + + @SuppressWarnings("MethodNameCheck") + public static void _preOnLongClick(android.view.View.OnLongClickListener thiz, android.view.View view) { + if (thiz != null) { + logOnClickEvent(view, TapBreadcrumbType.LONG_PRESS); + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewChromeClientSwazzledHooks.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewChromeClientSwazzledHooks.java new file mode 100644 index 0000000000..a22c5ab2be --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewChromeClientSwazzledHooks.java @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk; + +import static io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.logger; + +import android.webkit.ConsoleMessage; + +import androidx.annotation.NonNull; + +@InternalApi +public final class WebViewChromeClientSwazzledHooks { + + private WebViewChromeClientSwazzledHooks() { + } + + @SuppressWarnings("MethodNameCheck") + public static void _preOnConsoleMessage(@NonNull ConsoleMessage consoleMessage) { + logger.logInfo("webview _preOnConsoleMessage"); + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooks.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooks.java new file mode 100644 index 0000000000..566adacf38 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooks.java @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk; + +@InternalApi +public final class WebViewClientSwazzledHooks { + + private WebViewClientSwazzledHooks() { + } + + @SuppressWarnings("MethodNameCheck") + public static void _preOnPageStarted(android.webkit.WebView view, + java.lang.String url, + android.graphics.Bitmap favicon) { + + Embrace.getImpl().logWebView(url); + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/annotation/StartupActivity.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/annotation/StartupActivity.java new file mode 100644 index 0000000000..cfa54905e0 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/annotation/StartupActivity.java @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @Documented} means that the annotation indicates that elements using this annotation should be documented by JavaDoc. + *

+ * {@code @Target} specifies where we can use the annotation. + * If you do not define any Target type that means annotation can be applied to any element. + *

+ * {@code @Inherited} signals that a custom annotation used in a class should be inherited by all of its sub classes. + *

+ * {@code @Retention} indicates how long annotations with the annotated type are to be retained. + * RetentionPolicy.RUNTIME means the annotation should be available at runtime, for inspection via java reflection. + */ + +@Documented +@Target(ElementType.TYPE) +@Inherited +@Retention(RetentionPolicy.RUNTIME) +public @interface StartupActivity { +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrService.kt new file mode 100644 index 0000000000..2247517a70 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrService.kt @@ -0,0 +1,43 @@ +package io.embrace.android.embracesdk.anr + +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorStateInfo +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.payload.AnrInterval +import java.io.Closeable + +/** + * Service which detects when the application is not responding. + */ +internal interface AnrService : DataCaptureService>, Closeable { + + /** + * Gets all available anr process error states obtained from the OS. + * + * @param startTime the time to search from + * @return the list of ANR process errors + */ + fun getAnrProcessErrors(startTime: Long): List + + /** + * Forces ANR tracking stop by closing the monitoring thread when a crash is + * handled by the [EmbraceCrashService]. + */ + fun forceAnrTrackingStopOnCrash() + + /** + * Finishes initialization of the AnrService so that it can react appropriately to + * lifecycle changes and capture the correct data according to the config. This is necessary + * as the service can be initialized before the rest of the SDK. + * + * @param configService the configService + */ + fun finishInitialization( + configService: ConfigService + ) + + /** + * Adds a listener which is invoked when the thread becomes blocked/unblocked. + */ + fun addBlockedThreadListener(listener: BlockedThreadListener) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrStacktraceSampler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrStacktraceSampler.kt new file mode 100644 index 0000000000..6ad4fe0e83 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/AnrStacktraceSampler.kt @@ -0,0 +1,149 @@ +package io.embrace.android.embracesdk.anr + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.anr.detection.ThreadMonitoringState +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.enforceThread +import io.embrace.android.embracesdk.payload.AnrInterval +import io.embrace.android.embracesdk.payload.AnrSample +import io.embrace.android.embracesdk.payload.AnrSampleList +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.ExecutorService +import java.util.concurrent.atomic.AtomicReference + +/** + * This class is responsible for tracking the state of JVM stacktraces sampled during an ANR. + */ +internal class AnrStacktraceSampler( + private var configService: ConfigService, + private val clock: Clock, + targetThread: Thread, + private val anrMonitorThread: AtomicReference, + private val anrExecutorService: ExecutorService +) : BlockedThreadListener, MemoryCleanerListener { + + @VisibleForTesting + internal val anrIntervals = CopyOnWriteArrayList() + private val samples = mutableListOf() + private var lastUnblockedMs: Long = 0 + private val threadInfoCollector = ThreadInfoCollector(targetThread) + + fun setConfigService(configService: ConfigService) { + this.configService = configService + } + + internal fun size() = samples.size + + override fun onThreadBlocked(thread: Thread, timestamp: Long) { + threadInfoCollector.clearStacktraceCache() + lastUnblockedMs = timestamp + } + + override fun onThreadBlockedInterval(thread: Thread, timestamp: Long) { + val limit = configService.anrBehavior.getMaxStacktracesPerInterval() + val anrSample = if (size() >= limit) { + AnrSample(timestamp, null, 0, AnrSample.CODE_SAMPLE_LIMIT_REACHED) + } else { + val start = clock.now() + val threads = threadInfoCollector.captureSample(configService) + val sampleOverheadMs = clock.now() - start + AnrSample(timestamp, threads, sampleOverheadMs) + } + samples.add(anrSample) + } + + override fun onThreadUnblocked(thread: Thread, timestamp: Long) { + // Finalize AnrInterval + val responseMs = lastUnblockedMs + val anrInterval = AnrInterval( + responseMs, + null, + timestamp, + AnrInterval.Type.UI, + AnrSampleList(samples.toList()) + ) + + synchronized(anrIntervals) { + if (anrIntervals.size < MAX_ANR_INTERVAL_COUNT) { + anrIntervals.add(anrInterval) + + while (reachedAnrStacktraceCaptureLimit()) { + findLeastValuableIntervalWithSamples()?.let { entry -> + val index = anrIntervals.indexOf(entry) + anrIntervals.remove(entry) + anrIntervals.add(index, entry.clearSamples()) + } + } + } + } + + // reset state + samples.clear() + lastUnblockedMs = timestamp + threadInfoCollector.clearStacktraceCache() + } + + /** + * Finds the 'least valuable' ANR interval. This is used when the maximum number of ANR + * intervals with samples has been reached & the SDK needs to discard samples. We attempt + * to pick the least valuable interval in this case. + */ + @VisibleForTesting + internal fun findLeastValuableIntervalWithSamples() = + findIntervalsWithSamples().minByOrNull(AnrInterval::duration) + + override fun cleanCollections() { + anrExecutorService.submit { + enforceThread(anrMonitorThread) + anrIntervals.clear() + } + } + + @VisibleForTesting + internal fun reachedAnrStacktraceCaptureLimit(): Boolean { + val limit = configService.anrBehavior.getMaxAnrIntervalsPerSession() + val count = findIntervalsWithSamples().size + return count > limit + } + + private fun findIntervalsWithSamples() = anrIntervals.filter(AnrInterval::hasSamples) + + /** + * Retrieves ANR intervals that match the given start/time windows. + */ + fun getAnrIntervals( + state: ThreadMonitoringState, + clock: Clock + ): List { + synchronized(anrIntervals) { + val results = anrIntervals.toMutableList() + + // add any in-progress ANRs + if (state.anrInProgress) { + val intervalEndTime = clock.now() + val responseMs = state.lastTargetThreadResponseMs + val anrInterval = AnrInterval( + responseMs, + intervalEndTime, + null, + AnrInterval.Type.UI, + AnrSampleList(samples.toList()) + ) + results.add(anrInterval) + } + return results.map(AnrInterval::deepCopy) + } + } + + companion object { + + /** + * Hard limit for the maximum number of ANR intervals an SDK wants to send in a payload. + * Not all of these intervals will have stacktrace samples associated with them + * (that is set via a configurable limit). + */ + private const val MAX_ANR_INTERVAL_COUNT = 100 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/BlockedThreadListener.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/BlockedThreadListener.kt new file mode 100644 index 0000000000..1325ee066a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/BlockedThreadListener.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.anr + +/** + * Listens to when a thread is blocked and provides actions. + */ +internal interface BlockedThreadListener { + + /** + * Called when a thread becomes blocked for at least the configured + * interval in [Config.AnrConfig.getIntervalMs] + */ + fun onThreadBlocked(thread: Thread, timestamp: Long) + + /** + * Called when a thread is already blocked and hits another interval as configured + * in [Config.AnrConfig.getIntervalMs] + */ + fun onThreadBlockedInterval(thread: Thread, timestamp: Long) + + /** + * Called when a thread becomes unblocked, after being blocked for at least the configured + * interval in [Config.AnrConfig.getIntervalMs] + */ + fun onThreadUnblocked(thread: Thread, timestamp: Long) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/EmbraceAnrService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/EmbraceAnrService.kt new file mode 100644 index 0000000000..650d7209f7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/EmbraceAnrService.kt @@ -0,0 +1,200 @@ +package io.embrace.android.embracesdk.anr + +import android.os.Looper +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorSampler +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorStateInfo +import io.embrace.android.embracesdk.anr.detection.LivenessCheckScheduler +import io.embrace.android.embracesdk.anr.detection.ThreadMonitoringState +import io.embrace.android.embracesdk.anr.detection.UnbalancedCallDetector +import io.embrace.android.embracesdk.anr.sigquit.SigquitDetectionService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.enforceThread +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.AnrInterval +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import java.util.concurrent.Callable +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +/** + * Checks whether the target thread is still responding by using the following strategy: + * + * 1. Creating a [android.os.Handler], on the target thread, and an executor on a monitor thread + * 1. Using the 'monitoring' thread to message the target thread with a heartbeat + * 1. Determining whether the target thread responds in time, and if not logging an ANR + */ +internal class EmbraceAnrService( + var configService: ConfigService, + looper: Looper, + logger: InternalEmbraceLogger, + sigquitDetectionService: SigquitDetectionService, + livenessCheckScheduler: LivenessCheckScheduler, + anrExecutorService: ScheduledExecutorService, + state: ThreadMonitoringState, + private val anrProcessErrorSampler: AnrProcessErrorSampler, + @field:VisibleForTesting val clock: Clock, + private val anrMonitorThread: AtomicReference +) : AnrService, MemoryCleanerListener, ActivityListener, BlockedThreadListener { + + private val state: ThreadMonitoringState + private val targetThread: Thread + private val anrExecutorService: ScheduledExecutorService + val stacktraceSampler: AnrStacktraceSampler + private val logger: InternalEmbraceLogger + private val sigquitDetectionService: SigquitDetectionService + private val targetThreadHeartbeatScheduler: LivenessCheckScheduler + + @VisibleForTesting + val listeners = CopyOnWriteArrayList() + + init { + targetThread = looper.thread + this.logger = logger + this.sigquitDetectionService = sigquitDetectionService + this.state = state + this.anrExecutorService = anrExecutorService + targetThreadHeartbeatScheduler = livenessCheckScheduler + stacktraceSampler = AnrStacktraceSampler(configService, clock, targetThread, anrMonitorThread, anrExecutorService) + + // add listeners + listeners.add(stacktraceSampler) + listeners.add(UnbalancedCallDetector(logger)) + listeners.add( + anrProcessErrorSampler + ) + livenessCheckScheduler.listener = this + } + + private fun startAnrCapture() { + anrExecutorService.submit { + targetThreadHeartbeatScheduler.startMonitoringThread() + } + } + + override fun finishInitialization( + configService: ConfigService + ) { + this.configService = configService + stacktraceSampler.setConfigService(configService) + sigquitDetectionService.configService = configService + targetThreadHeartbeatScheduler.configService = configService + logger.logDeveloper("EmbraceAnrService", "Finish initialization") + sigquitDetectionService.initializeGoogleAnrTracking() + startAnrCapture() + } + + override fun addBlockedThreadListener(listener: BlockedThreadListener) { + listeners.add(listener) + } + + /** + * Gets the intervals during which the application was not responding (ANR). + * + * All functions in this class MUST be called from the same thread as [BlockedThreadDetector]. + * This is part of the synchronization strategy that ensures ANR data is not corrupted. + */ + override fun getCapturedData(): List { + return try { + val callable = Callable { + checkNotNull(stacktraceSampler.getAnrIntervals(state, clock)) { + "ANR samples to be cached is null" + } + } + anrExecutorService.submit(callable).get(MAX_DATA_WAIT_MS, TimeUnit.MILLISECONDS) + } catch (exc: Exception) { + logger.logError("Failed to getAnrIntervals()", exc, true) + emptyList() + } + } + + override fun getAnrProcessErrors( + startTime: Long + ): List { + return anrProcessErrorSampler.getAnrProcessErrors(startTime) + } + + override fun forceAnrTrackingStopOnCrash() { + anrExecutorService.submit { + targetThreadHeartbeatScheduler.stopMonitoringThread() + } + } + + override fun close() { + } + + override fun cleanCollections() { + stacktraceSampler.cleanCollections() + sigquitDetectionService.cleanCollections() + } + + override fun onThreadBlocked(thread: Thread, timestamp: Long) { + enforceThread(anrMonitorThread) + for (listener in listeners) { + listener.onThreadBlocked(thread, timestamp) + } + } + + override fun onThreadBlockedInterval(thread: Thread, timestamp: Long) { + enforceThread(anrMonitorThread) + processAnrTick(timestamp) + } + + override fun onThreadUnblocked(thread: Thread, timestamp: Long) { + enforceThread(anrMonitorThread) + for (listener in listeners) { + listener.onThreadUnblocked(thread, timestamp) + } + } + + @VisibleForTesting + internal fun processAnrTick(timestamp: Long) { + // Check if ANR capture is enabled + if (!configService.anrBehavior.isAnrCaptureEnabled()) { + logger.logDeveloper("EmbraceAnrService", "ANR capture is disabled, ignoring ANR tick") + return + } + + // Invoke callbacks + for (listener in listeners) { + listener.onThreadBlockedInterval(targetThread, timestamp) + } + } + + /** + * When app goes to foreground, we need to monitor the target thread again to + * capture ANRs. + */ + override fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) { + anrExecutorService.submit { + enforceThread(anrMonitorThread) + state.resetState() + targetThreadHeartbeatScheduler.startMonitoringThread() + } + } + + /** + * When app goes to background, we stop monitoring the target thread + * because we don't need to capture ANRs on background and we don't + * want to affect customer's app performance. + */ + override fun onBackground(timestamp: Long) { + anrExecutorService.submit { + targetThreadHeartbeatScheduler.stopMonitoringThread() + } + } + + companion object { + + /** + * The maximum number of milliseconds we should wait to retrieve ANR intervals to the + * session payload. The vast majority of times this wait time should effectively be 0ms - + * a limit is included to avoid blocking the main thread/sampling. + */ + private const val MAX_DATA_WAIT_MS = 1000L + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/NoOpAnrService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/NoOpAnrService.kt new file mode 100644 index 0000000000..a7844011c3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/NoOpAnrService.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.anr + +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorStateInfo +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.payload.AnrInterval + +internal class NoOpAnrService : AnrService { + + override fun getCapturedData(): List { + return emptyList() + } + + override fun getAnrProcessErrors(startTime: Long): List { + return emptyList() + } + + override fun forceAnrTrackingStopOnCrash() { + } + + override fun finishInitialization( + configService: ConfigService + ) { + } + + override fun addBlockedThreadListener(listener: BlockedThreadListener) { + } + + override fun close() { + } + + override fun cleanCollections() { + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ThreadInfoCollector.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ThreadInfoCollector.kt new file mode 100644 index 0000000000..400fad64a2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ThreadInfoCollector.kt @@ -0,0 +1,99 @@ +package io.embrace.android.embracesdk.anr + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.payload.ThreadInfo +import java.util.regex.Pattern + +internal class ThreadInfoCollector( + private val targetThread: Thread +) { + + private val currentStacktraceStates: MutableMap = HashMap() + + /** + * Clears the stacktrace cache for all threads. + */ + fun clearStacktraceCache() = currentStacktraceStates.clear() + + /** + * Captures the thread traces required for the given sample. + */ + fun captureSample(configService: ConfigService): List { + val threads = getAllowedThreads(configService) + val sanitizedThreads = mutableListOf() + + threads.forEach { threadInfo -> + // Compares every thread with the last known thread state via hashcode. If hashcode changed + // it should be added to the anrInfo list and also the currentAnrInfoState must be updated. + val threadId = threadInfo.threadId + val cache: ThreadInfo? = currentStacktraceStates[threadId] + + // only serialize if the previous stacktrace doesn't match. + if (cache == null || threadInfo != cache) { + sanitizedThreads.add(threadInfo) + currentStacktraceStates[threadId] = threadInfo + } + } + return sanitizedThreads + } + + /** + * Filter the thread list based on allow/block list get by config. + * + * @return filtered threads + */ + @VisibleForTesting + internal fun getAllowedThreads(configService: ConfigService): Set { + val allowed: MutableSet = HashSet() + val anrBehavior = configService.anrBehavior + val blockList = anrBehavior.blockPatternList + val allowList = anrBehavior.allowPatternList + val anrStacktracesMaxLength = anrBehavior.getStacktraceFrameLimit() + val priority = anrBehavior.getMinThreadPriority() + + if (anrBehavior.shouldCaptureMainThreadOnly()) { + val threadInfo = ThreadInfo.ofThread( + targetThread, + targetThread.stackTrace, + anrStacktracesMaxLength + ) + allowed.add(threadInfo) + } else { + Thread.getAllStackTraces().forEach { (thread, stacktrace) -> + val allowedByPriority = isAllowedByPriority(priority, thread.priority) + val allowedByLists = isAllowedByLists(allowList, blockList, thread.name) + + if (allowedByPriority && allowedByLists) { + allowed.add( + ThreadInfo.ofThread( + thread, + stacktrace, + anrStacktracesMaxLength + ) + ) + } + } + } + return allowed + } + + private fun isAllowedByLists( + allowList: List?, + blockList: List?, + name: String + ): Boolean { + return matchesList(allowList, name) || !matchesList(blockList, name) + } + + private fun matchesList(allowed: List?, name: String): Boolean { + if (allowed == null || allowed.isEmpty()) { + return false + } + return allowed.any { pattern -> + pattern.matcher(name).find() + } + } + + fun isAllowedByPriority(priority: Int, observedPriority: Int) = observedPriority >= priority +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorChecker.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorChecker.kt new file mode 100644 index 0000000000..49f37fb7ef --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorChecker.kt @@ -0,0 +1,64 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.app.ActivityManager +import android.os.Process +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.clock.Clock + +private const val DATA_LIMIT_BYTES = 16 * 1024 + +internal fun findAnrProcessErrorStateInfo( + clock: Clock, + activityManager: ActivityManager?, + pid: Int = Process.myPid() +): AnrProcessErrorStateInfo? { + return activityManager?.processesInErrorState + ?.filter { it.pid == pid } + ?.filter { it.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING } + ?.map { info -> + AnrProcessErrorStateInfo( + info.tag, + info.shortMsg.take(DATA_LIMIT_BYTES), + info.longMsg.take(DATA_LIMIT_BYTES), + info.stackTrace.take(DATA_LIMIT_BYTES), + clock.now() + ) + } + ?.singleOrNull() +} + +/** + * Holds information about the ANR as reported by [AnrProcessErrorStateInfo]. + */ +internal data class AnrProcessErrorStateInfo( + + /** + * The activity name associated with the error, if known. May be null. + */ + @SerializedName("t") + val tag: String? = null, + + /** + * A short message describing the error condition. + */ + @SerializedName("sm") + val shortMsg: String? = null, + + /** + * A long message describing the error condition. + */ + @SerializedName("lm") + val longMsg: String? = null, + + /** + * The stack trace where the error originated. May be null. + */ + @SerializedName("st") + val stackTrace: String? = null, + + /** + * The timestamp where the process error info was first detected. + */ + @SerializedName("ts") + val timestamp: Long? = null, +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler.kt new file mode 100644 index 0000000000..32c0e1cf75 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSampler.kt @@ -0,0 +1,229 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.app.ActivityManager +import android.os.Process +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.anr.BlockedThreadListener +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.util.NavigableMap +import java.util.concurrent.ConcurrentSkipListMap +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import kotlin.math.absoluteValue + +/** + * Checks whether the current process has been marked in an error state by the OS. The error + * state gives useful debug information about the ANR and helps us narrow down whether the OS + * detected a problem or not. + * + * https://developer.android.com/reference/android/app/ActivityManager#getProcessesInErrorState() + * + * This class can only check one anr process error at a time. + */ +internal class AnrProcessErrorSampler( + private val activityManager: ActivityManager?, + private val configService: ConfigService, + private val anrExecutor: ScheduledExecutorService, + private val clock: Clock, + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger, + private val pid: Int = Process.myPid() +) : BlockedThreadListener { + + private var intervalMs: Long = configService.anrBehavior.getAnrProcessErrorsIntervalMs() + + @VisibleForTesting + var scheduledFuture: ScheduledFuture<*>? = null + + @VisibleForTesting + var anrProcessErrors: NavigableMap = ConcurrentSkipListMap() + + // timestamp when the thread has been unblocked + @VisibleForTesting + var threadUnblockedMs: Long? = null + + override fun onThreadBlocked(thread: Thread, timestamp: Long) { + if (isFeatureEnabled()) { + // just in case a scheduler is running, let's reset because this is a new anr + // note that this class does not support to have a scheduler looking for 2 different anr's + // at the same time + reset() + + scheduleAnrProcessErrorsChecker(timestamp) + } + } + + override fun onThreadUnblocked(thread: Thread, timestamp: Long) { + if (isFeatureEnabled()) { + threadUnblockedMs = timestamp + } + } + + override fun onThreadBlockedInterval(thread: Thread, timestamp: Long) { + // nothing to do here + } + + /** + * It gets all collected anr process errors for given range. + * + * @param startTime + */ + fun getAnrProcessErrors( + startTime: Long + ): List { + val bgAnrCaptureEnabled = configService.anrBehavior.isBgAnrCaptureEnabled() + val filteredProcessErrors: Collection = + if (!bgAnrCaptureEnabled) { + // Filter out ANRs that started before session start + anrProcessErrors.filter { + // check that timestamp of the anr process error is within session bounds + it.key >= startTime + }.values + } else { + anrProcessErrors.values + } + + // do not return original, safer to copy instead + return filteredProcessErrors.toMutableList() + } + + /** + * Called when we need to search for an anr process error. + * + * @param threadBlockedTimestamp timestamp of when the thread has been blocked + */ + @VisibleForTesting + internal fun onSearchForProcessErrors(threadBlockedTimestamp: Long) { + val shouldStopScheduler = !isSchedulerAllowedToRun() + if (shouldStopScheduler) { + logger.logDeveloper( + "EmbraceAnrService", + "Anr process errors scheduler is not allowed to keep running. Stopping it" + ) + // even if scheduler can not run anymore, let's not interrupt this run because we may find an anr + scheduledFuture?.cancel(false) + } + + val anrProcessErrorState = findAnrProcessErrorStateInfo(clock, activityManager, pid) + if (anrProcessErrorState != null) { + // anr process error found + handleProcessErrorState(anrProcessErrorState, threadBlockedTimestamp) + } else { + logger.logDeveloper( + "EmbraceAnrService", + "Anr process errors were not found. This is expected, report has " + + "probably not been generated yet" + ) + + // only perform rescheduling if scheduler should not be stopped + if (!shouldStopScheduler && intervalMs != configService.anrBehavior.getAnrProcessErrorsIntervalMs()) { + logger.logDeveloper( + "EmbraceAnrService", + "Different capture anr process errors interval detected, restarting runnable" + ) + + // we don't want to interrupt this Runnable while it's running + scheduledFuture?.cancel(false) + + // reschedule scheduler at the new cadence + scheduleAnrProcessErrorsChecker(threadBlockedTimestamp) + } + } + } + + /** + * It determines if the scheduler is allowed to keep running. + * Basically, once the thread has been unblocked, we still have [schedulerExtraTimeAllowance] + * ms for the scheduler to keep running. + */ + @VisibleForTesting + internal fun isSchedulerAllowedToRun(): Boolean { + return when (val ms = threadUnblockedMs) { + null -> true + else -> (clock.now() - ms).absoluteValue <= configService.anrBehavior.getAnrProcessErrorsSchedulerExtraTimeAllowanceMs() + } + } + + private fun scheduleAnrProcessErrorsChecker(threadBlockedTimestamp: Long) { + try { + val runnable = Runnable { + onSearchForProcessErrors(threadBlockedTimestamp) + } + + // the OS does not generate anr process errors right away + val delay = configService.anrBehavior.getAnrProcessErrorsDelayMs() + logger.logDeveloper( + "EmbraceAnrService", + "About to schedule runnable to look for anr process errors, with " + + "delay=$delay - intervalMs=$intervalMs" + ) + intervalMs = configService.anrBehavior.getAnrProcessErrorsIntervalMs() + scheduledFuture = anrExecutor.scheduleAtFixedRate( + runnable, + delay, + intervalMs, + TimeUnit.MILLISECONDS + ) + } catch (exc: Exception) { + // ignore any RejectedExecution - ScheduledExecutorService only throws when shutting down. + val message = "capture ANR process errors initialization failed" + logger.logError(message, exc, true) + } + } + + private fun handleProcessErrorState( + processErrorStateInfo: AnrProcessErrorStateInfo, + timestamp: Long + ) { + logger.logDeveloper( + "EmbraceAnrService", + "Anr process error state found. " + + "Cancelled scheduler so to stop looking for it." + ) + + anrProcessErrors[timestamp] = processErrorStateInfo + + logDebugInfo(processErrorStateInfo) + + // once anr process error is saved, let's cancel scheduler because we only need to fetch + // this once per ANR + scheduledFuture?.cancel(true) + } + + private fun logDebugInfo(processErrorStateInfo: AnrProcessErrorStateInfo) { + with(processErrorStateInfo) { + logger.logDeveloper( + "EmbraceAnrService", + """AnrProcessErrorStateInfo= + |tag=$tag + |shortMsg=$shortMsg + |longMsg=$longMsg + |stacktrace=$stackTrace + """.trimMargin() + ) + } + } + + private fun reset() { + logger.logDeveloper( + "EmbraceAnrService", + "Resetting AnrProcessErrorSampler scheduler state" + ) + threadUnblockedMs = null + scheduledFuture?.cancel(true) + } + + private fun isFeatureEnabled() = + if (!configService.anrBehavior.isAnrProcessErrorsCaptureEnabled()) { + logger.logDeveloper( + "EmbraceAnrService", + "ANR process errors capture is disabled" + ) + false + } else { + true + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetector.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetector.kt new file mode 100644 index 0000000000..dbd766a582 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetector.kt @@ -0,0 +1,152 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.os.Debug +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.anr.BlockedThreadListener +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.enforceThread +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.util.concurrent.atomic.AtomicReference + +/** + * The number of milliseconds which the monitor thread is allowed to timeout before we + * assume that the process has been put into the cached state. + * + * All functions in this class MUST be called from the same thread - this is part of the + * synchronization strategy that ensures ANR data is not corrupted. + */ +private const val MONITOR_THREAD_TIMEOUT_MS = 60000L + +/** + * The % of a regular interval that must have elapsed for us to consider taking another sample. + * + * This helps avoid two scenarios: performing too much work and contributing to ANRs, and + * taking many samples within a few ms of each other (when the monitor thread has not been + * scheduled enough CPU time). + */ +private const val SAMPLE_BACKOFF_FACTOR = 0.5 + +/** + * Responsible for deciding whether a thread is blocked or not. The actual scheduling happens in + * [LivenessCheckScheduler] whereas this class contains the business logic. + */ +internal class BlockedThreadDetector constructor( + var configService: ConfigService, + private val clock: Clock, + var listener: BlockedThreadListener? = null, + private val state: ThreadMonitoringState, + private val targetThread: Thread, + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger, + private val anrMonitorThread: AtomicReference +) { + + /** + * Called when the target thread process the message. This indicates that the target thread is + * responsive and (usually) means an ANR is about to end. + * + * All functions in this class MUST be called from the same thread - this is part of the + * synchronization strategy that ensures ANR data is not corrupted. + */ + fun onTargetThreadResponse(timestamp: Long) { + enforceThread(anrMonitorThread) + + state.lastTargetThreadResponseMs = timestamp + + if (isDebuggerEnabled()) { + return + } + + if (state.anrInProgress) { + // Application was not responding, but recovered + logger.logDebug("Main thread recovered from not responding for > 1s") + + // Invoke callbacks + state.anrInProgress = false + listener?.onThreadUnblocked(targetThread, timestamp) + } + } + + /** + * Called at regular intervals by the monitor thread. We should check whether the + * target thread has been unresponsive & decide whether this means an ANR is happening. + * + * All functions in this class MUST be called from the same thread - this is part of the + * synchronization strategy that ensures ANR data is not corrupted. + */ + fun updateAnrTracking(timestamp: Long) { + enforceThread(anrMonitorThread) + + if (isDebuggerEnabled()) { + return + } + + if (!state.anrInProgress && isAnrDurationThresholdExceeded(timestamp)) { + logger.logDebug("Main thread not responding for > 1s") + state.anrInProgress = true + listener?.onThreadBlocked(targetThread, state.lastTargetThreadResponseMs) + } + if (state.anrInProgress && shouldAttemptAnrSample(timestamp)) { + listener?.onThreadBlockedInterval( + targetThread, + timestamp + ) + state.lastSampleAttemptMs = clock.now() + } + state.lastMonitorThreadResponseMs = clock.now() + } + + /** + * Decides whether we should attempt an ANR sample or not. In ordinary conditions this + * function will always return true. If the thread has been unable run due to priority then + * several scheduled tasks may run in very quick succession of each other (e.g. 1ms apart). + * + * To avoid useless samples grouped within a few ms of each other, this function will return + * false & thus avoid sampling if less than half of the interval MS has passed. + */ + @VisibleForTesting + internal fun shouldAttemptAnrSample(timestamp: Long): Boolean { + val lastMonitorThreadResponseMs = state.lastMonitorThreadResponseMs + val delta = timestamp - lastMonitorThreadResponseMs // time since last check + val intervalMs = configService.anrBehavior.getSamplingIntervalMs() + return delta > intervalMs * SAMPLE_BACKOFF_FACTOR + } + + /** + * Checks whether the ANR duration threshold has been exceeded or not. + * + * This defaults to the main thread not having processed a message within 1s. + */ + @VisibleForTesting + internal fun isAnrDurationThresholdExceeded(timestamp: Long): Boolean { + enforceThread(anrMonitorThread) + + val monitorThreadLag = timestamp - state.lastMonitorThreadResponseMs + val targetThreadLag = timestamp - state.lastTargetThreadResponseMs + + // If the last monitor thread check greatly exceeds the ANR threshold + // then it is very probable that the process has been cached or frozen. In this case + // we need to ignore the first heartbeat as the clock won't have been ticking + // while the process was cached and this could cause a false positive. + // + // Therefore we reset the last response time from the target + monitor threads to + // the current time so that we can start monitoring for ANRs again. + // https://developer.android.com/guide/components/activities/process-lifecycle + if (monitorThreadLag > MONITOR_THREAD_TIMEOUT_MS) { + logger.logDeveloper("EmbraceAnrService", "Exceeded monitor thread timeout") + val now = clock.now() + state.lastTargetThreadResponseMs = now + state.lastMonitorThreadResponseMs = now + return false + } + val minTriggerDuration = configService.anrBehavior.getMinDuration() + return targetThreadLag > minTriggerDuration + } + + /** + * Returns true if the debugger is enabled - as we want to eliminate false positive ANRs. + */ + private fun isDebuggerEnabled(): Boolean = + Debug.isDebuggerConnected() || Debug.waitingForDebugger() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler.kt new file mode 100644 index 0000000000..7f248c3524 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckScheduler.kt @@ -0,0 +1,152 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.os.Message +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.anr.detection.TargetThreadHandler.Companion.HEARTBEAT_REQUEST +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.enforceThread +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +/** + * Responsible for scheduling 'heartbeat' checks on a background thread & posting messages on the + * target thread. + * + * If the target thread does not respond within a given time, an ANR is assumed to have happened. + * + * This class is responsible solely for the complicated logic of enqueuing a regular message on the + * target thread & scheduling regular checks on a background thread. The [BlockedThreadDetector] + * class is responsible for the actual business logic that checks whether a thread is blocked or not. + */ +internal class LivenessCheckScheduler internal constructor( + configService: ConfigService, + private val anrExecutor: ScheduledExecutorService, + private val clock: Clock, + private val state: ThreadMonitoringState, + private val targetThreadHandler: TargetThreadHandler, + private val blockedThreadDetector: BlockedThreadDetector, + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger, + private val anrMonitorThread: AtomicReference +) { + + var configService + set(value) { + blockedThreadDetector.configService = value + } + get() = blockedThreadDetector.configService + + var listener + set(value) { + blockedThreadDetector.listener = value + } + get() = blockedThreadDetector.listener + + private var intervalMs: Long = configService.anrBehavior.getSamplingIntervalMs() + private var monitorFuture: ScheduledFuture<*>? = null + + init { + targetThreadHandler.action = blockedThreadDetector::onTargetThreadResponse + targetThreadHandler.start() + } + + /** + * Starts monitoring the target thread for blockages. + */ + fun startMonitoringThread() { + enforceThread(anrMonitorThread) + if (!state.started.getAndSet(true)) { + logger.logInfo("Started heartbeats to capture ANRs.") + scheduleRegularHeartbeats() + } + } + + /** + * Stops monitoring the target thread. + */ + fun stopMonitoringThread() { + enforceThread(anrMonitorThread) + + if (state.started.get()) { + monitorFuture?.let { monitorTask -> + monitorTask.cancel(false) + if (monitorTask.isDone) { + logger.logInfo("Stopped heartbeats to capture ANRs.") + state.started.set(false) + } else { + logger.logError("Scheduled heartbeat could not be stopped.") + } + } ?: logger.logError( + "Scheduled heartbeat could not be stopped. " + + "monitorFuture is null" + ) + } + } + + private fun scheduleRegularHeartbeats() { + enforceThread(anrMonitorThread) + + intervalMs = configService.anrBehavior.getSamplingIntervalMs() + val runnable = Runnable(::onMonitorThreadHeartbeat) + try { + logger.logDeveloper("EmbraceAnrService", "Heartbeat Interval: $intervalMs") + monitorFuture = + anrExecutor.scheduleAtFixedRate(runnable, 0, intervalMs, TimeUnit.MILLISECONDS) + } catch (exc: Exception) { + // ignore any RejectedExecution - ScheduledExecutorService only throws when shutting down. + val message = "ANR capture initialization failed" + logger.logError(message, exc, true) + } + } + + /** + * Called at regular intervals on the monitor thread. This function posts a message to the + * main thread that is used to check whether it is live or not. + */ + @VisibleForTesting + internal fun onMonitorThreadHeartbeat() { + enforceThread(anrMonitorThread) + + try { + with(configService.anrBehavior.getMonitorThreadPriority()) { + android.os.Process.setThreadPriority(this) + } + + if (intervalMs != configService.anrBehavior.getSamplingIntervalMs()) { + logger.logDeveloper( + "EmbraceAnrService", + "Different interval detected, restarting runnable" + ) + + // we don't want to interrupt this Runnable while it's running + monitorFuture?.cancel(false) + + // reschedule a heartbeat at the new cadence + scheduleRegularHeartbeats() + } else { + val now = clock.now() + if (!targetThreadHandler.hasMessages(HEARTBEAT_REQUEST)) { + sendHeartbeatMessage() + } + blockedThreadDetector.updateAnrTracking(now) + } + } catch (exc: Exception) { + logger.logError("Failed to process ANR monitor thread heartbeat", exc, true) + } + } + + private fun sendHeartbeatMessage() { + val heartbeatMessage = Message.obtain(targetThreadHandler, HEARTBEAT_REQUEST) + if (!targetThreadHandler.sendMessage(heartbeatMessage)) { + logger.logError( + "Failed to send message to targetHandler, main thread likely shutting down.", + IllegalStateException("Failed to send message to targetHandler"), + true + ) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LooperCompat.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LooperCompat.java new file mode 100644 index 0000000000..af4b3f11b9 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/LooperCompat.java @@ -0,0 +1,46 @@ +package io.embrace.android.embracesdk.anr.detection; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.Looper; +import android.os.MessageQueue; + +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; + +class LooperCompat { + + /** + * Retrieves the MessageQueue from a {@link Looper} via reflection. This is only required for + * API <23 as {@link Looper#getQueue()} is available on newer APIs. + *

+ * An alternative strategy would be {@link Looper#myQueue()} but that requires submitting to + * the main thread's handler - not an option if it might be already blocked at the point + * our SDK is initialized. + *

+ * This hidden API shows up in the Android SDK's restricted interfaces, but as we don't + * execute the code on newer API levels it should stay a false positive if a customer does + * happen to flag this up due to automated scanners etc. + * + * ... + */ + @Nullable + @SuppressWarnings("JavaReflectionMemberAccess") + @SuppressLint("PrivateApi") + static MessageQueue getMessageQueue(Looper looper) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return looper.getQueue(); + } else { + Class clz = Looper.class; + + try { + Field field = clz.getDeclaredField("mQueue"); + Object o = field.get(looper); + return (MessageQueue) o; + } catch (Throwable exc) { + return null; + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt new file mode 100644 index 0000000000..a1018a7107 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt @@ -0,0 +1,90 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.MessageQueue +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.enforceThread +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logError +import java.util.concurrent.ExecutorService +import java.util.concurrent.atomic.AtomicReference + +/** + * A [Handler] that processes messages enqueued on the target [Looper]. If a message is not + * processed by this class in a timely manner then it indicates the target thread is blocked + * with too much work. + * + * When this class processes the message it submits the [action] for execution on the supplied + * [ExecutorService]. + * + * Basically speaking: if [handleMessage] takes a long time, the monitor thread assumes there is + * an ANR after a certain time threshold. Once [handleMessage] is invoked, the monitor thread + * knows for sure that the target thread is responsive, so resets the timer for any ANRs. + */ +internal class TargetThreadHandler( + looper: Looper, + private val anrExecutorService: ExecutorService, + private val anrMonitorThread: AtomicReference, + private val configService: ConfigService, + private val messageQueue: MessageQueue? = LooperCompat.getMessageQueue(looper), + private val clock: Clock +) : Handler(looper) { + + lateinit var action: (time: Long) -> Unit + + @Volatile + var installed: Boolean = false + + fun start() { + // set an IdleHandler that automatically gets invoked when the Handler + // has processed all pending messages. We retain the callback to avoid + // unnecessary allocations. + + if (configService.anrBehavior.isIdleHandlerEnabled()) { + messageQueue?.addIdleHandler(::onIdleThread) + installed = true + } + } + + @VisibleForTesting + internal fun onIdleThread(): Boolean { + onMainThreadUnblocked() + return true + } + + override fun handleMessage(msg: Message) { + try { + if (msg.what == HEARTBEAT_REQUEST) { + // We couldn't obtain the target thread message queue. This should not happen, + // but if it does then we just log an internal error & consider the ANR ended at + // this point. + if (messageQueue == null || !installed) { + logDebug("Failed to obtain main thread MessageQueue - using fallback ANR strategy.") + onMainThreadUnblocked() + } + } + } catch (ex: Exception) { + logError("ANR healthcheck failed in main (monitored) thread", ex, true) + } + } + + private fun onMainThreadUnblocked() { + val timestamp = clock.now() + anrExecutorService.submit { + enforceThread(anrMonitorThread) + action.invoke(timestamp) + } + } + + companion object { + + /** + * Unique ID for message (arbitrary number). + */ + internal const val HEARTBEAT_REQUEST = 34593 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/ThreadMonitoringState.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/ThreadMonitoringState.kt new file mode 100644 index 0000000000..16826ef858 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/ThreadMonitoringState.kt @@ -0,0 +1,54 @@ +package io.embrace.android.embracesdk.anr.detection + +import io.embrace.android.embracesdk.clock.Clock +import java.util.concurrent.atomic.AtomicBoolean + +/** + * This class holds state that is used when monitoring a thread. For instance, the last response + * time of the target/main threads. + */ +internal class ThreadMonitoringState( + private val clock: Clock +) { + + /** + * Whether blocked thread detection has already been started or not. + */ + @JvmField + val started = AtomicBoolean(false) + + /** + * The last response time of the target thread in ms. + */ + @Volatile + var lastTargetThreadResponseMs: Long = clock.now() + + /** + * The last response time of the monitoring thread in ms. + */ + @Volatile + var lastMonitorThreadResponseMs: Long = clock.now() + + /** + * The last sample attempt in ms. + */ + @Volatile + var lastSampleAttemptMs: Long = 0 + + /** + * Whether an ANR is already in progress or not. + */ + @Volatile + var anrInProgress = false + + /** + * Resets state properties to the initial values + */ + fun resetState() { + anrInProgress = false + started.set(false) + lastTargetThreadResponseMs = clock.now() + lastMonitorThreadResponseMs = clock.now() + lastSampleAttemptMs = 0 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector.kt new file mode 100644 index 0000000000..703b9cd41d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetector.kt @@ -0,0 +1,49 @@ +package io.embrace.android.embracesdk.anr.detection + +import io.embrace.android.embracesdk.anr.BlockedThreadListener +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger + +internal class UnbalancedCallDetector( + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger +) : BlockedThreadListener { + + @Volatile + private var blocked: Boolean = false + + @Volatile + private var lastTimestamp: Long = 0 + + override fun onThreadBlocked(thread: Thread, timestamp: Long) { + checkUnbalancedCall("onThreadBlocked()", false) + blocked = true + checkTimeTravel("onThreadBlocked()", timestamp) + } + + override fun onThreadBlockedInterval(thread: Thread, timestamp: Long) { + checkUnbalancedCall("onThreadBlockedInterval()", true) + checkTimeTravel("onThreadBlockedInterval()", timestamp) + } + + override fun onThreadUnblocked(thread: Thread, timestamp: Long) { + checkUnbalancedCall("onThreadUnblocked()", true) + blocked = false + checkTimeTravel("onThreadUnblocked()", timestamp) + } + + private fun checkTimeTravel(name: String, timestamp: Long) { + if (lastTimestamp > timestamp) { + val msg = "Time travel in $name. $lastTimestamp to $timestamp" + logger.logError(msg, IllegalStateException(msg), true) + } + lastTimestamp = timestamp + } + + private fun checkUnbalancedCall(name: String, expected: Boolean) { + if (blocked != expected) { + val threadName = Thread.currentThread().name + val msg = "Unbalanced call to $name in ANR detection. Thread=$threadName" + logger.logError(msg, IllegalStateException(msg), true) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/EmbraceNativeThreadSamplerService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/EmbraceNativeThreadSamplerService.kt new file mode 100644 index 0000000000..6512903fa0 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/EmbraceNativeThreadSamplerService.kt @@ -0,0 +1,269 @@ +package io.embrace.android.embracesdk.anr.ndk + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.AnrBehavior +import io.embrace.android.embracesdk.internal.DeviceArchitecture +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.NativeThreadAnrInterval +import io.embrace.android.embracesdk.payload.NativeThreadAnrSample +import io.embrace.android.embracesdk.payload.mapThreadState +import java.util.Random +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +/** + * Samples the target thread stacktrace when the thread is detected as blocked. + * + * The NDK layer must be enabled in order to use this functionality as this class + * calls native code. + */ +internal class EmbraceNativeThreadSamplerService @JvmOverloads constructor( + private val configService: ConfigService, + private val symbols: Lazy?>, + private val random: Random = Random(), + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger, + private val delegate: NdkDelegate = NativeThreadSamplerNdkDelegate(), + private val executorService: ScheduledExecutorService, + private val deviceArchitecture: DeviceArchitecture +) : NativeThreadSamplerService { + + companion object { + const val MAX_NATIVE_SAMPLES = 10 + } + + internal interface NdkDelegate { + fun setupNativeThreadSampler(is32Bit: Boolean): Boolean + fun monitorCurrentThread(): Boolean + fun startSampling(unwinderOrdinal: Int, intervalMs: Long) + fun finishSampling(): List? + } + + @VisibleForTesting + internal var ignored = true + + @VisibleForTesting + internal var sampling = false + + @VisibleForTesting + internal var count = -1 + + @VisibleForTesting + internal var factor = -1 + + @VisibleForTesting + internal var intervals: MutableList = mutableListOf() + + @VisibleForTesting + internal val currentInterval: NativeThreadAnrInterval? + get() = intervals.lastOrNull() + + private var targetThread: Thread = Thread.currentThread() + + override fun setupNativeSampler(): Boolean { + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Target thread found, attempting to install NativeThreadSampler" + ) + return delegate.setupNativeThreadSampler(deviceArchitecture.is32BitDevice) + } + + override fun monitorCurrentThread(): Boolean { + targetThread = Thread.currentThread() + return delegate.monitorCurrentThread() + } + + override fun onThreadBlocked(thread: Thread, timestamp: Long) { + logger.logDeveloper("EmbraceNativeThreadSamplerService", "onThreadBlocked") + + // use consistent config for the duration of this ANR interval. + val anrBehavior = configService.anrBehavior + ignored = !containsAllowedStackframes(anrBehavior, targetThread.stackTrace) + if (ignored || shouldSkipNewSample(anrBehavior)) { + // we've reached the data capture limit - ignore any thread blocked intervals. + logger.logDeveloper( + "NativeThreadSamplerInstaller", + "Data capture limit reached. Ignoring thread blocked intervals." + ) + ignored = true + return + } + + val unwinder = anrBehavior.getNativeThreadAnrSamplingUnwinder() + factor = anrBehavior.getNativeThreadAnrSamplingFactor() + val offset = random.nextInt(factor) + count = (factor - offset) % factor + + logger.logDeveloper("EmbraceNativeThreadSamplerService", "add NativeThreadSample samples") + intervals.add( + NativeThreadAnrInterval( + targetThread.id, + targetThread.name, + targetThread.priority, + offset * anrBehavior.getSamplingIntervalMs(), + timestamp, + mutableListOf(), + mapThreadState(targetThread.state), + unwinder + ) + ) + } + + override fun onThreadBlockedInterval(thread: Thread, timestamp: Long) { + logger.logDeveloper("EmbraceNativeThreadSamplerService", "onThreadBlockedInterval") + + val limit = configService.anrBehavior.getMaxStacktracesPerInterval() + if (count >= limit) { + logger.logDebug("ANR stacktrace not captured. Maximum allowed ticks per ANR interval reached.") + return + } + + if (ignored || !configService.anrBehavior.isNativeThreadAnrSamplingEnabled()) { + logger.logDeveloper( + "NativeThreadSamplerInstaller", + "Ignoring thread blocked interval" + ) + return + } + if (count % factor == 0) { + count = 0 + + if (!sampling) { + sampling = true + + // start sampling the native thread + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Initiating sampling of the target thread" + ) + + val anrBehavior = configService.anrBehavior + val unwinder = anrBehavior.getNativeThreadAnrSamplingUnwinder() + val intervalMs = anrBehavior.getNativeThreadAnrSamplingIntervalMs() + delegate.startSampling( + unwinder.code, + intervalMs + ) + + executorService.schedule({ + fetchIntervals() + }, intervalMs * MAX_NATIVE_SAMPLES, TimeUnit.MILLISECONDS) + } + } + count++ + } + + override fun onThreadUnblocked(thread: Thread, timestamp: Long) { + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Thread unblocked: ${thread.id}" + ) + + if (sampling) { + executorService.submit { + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Fetching samples on JVM bg thread" + ) + fetchIntervals() + } + } else { + logger.logDeveloper( + "NativeThreadSamplerInstaller", + "Ignoring thread blocked interval" + ) + } + + ignored = true + sampling = false + } + + private fun fetchIntervals() { + currentInterval?.let { interval -> + delegate.finishSampling()?.let { samples -> + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Fetched samples. Count=${samples.size}" + ) + + interval.samples?.run { + clear() + addAll(samples) + } + } + } + } + + override fun cleanCollections() { + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Clean collections" + ) + intervals = mutableListOf() + } + + private fun shouldSkipNewSample(anrBehavior: AnrBehavior): Boolean { + val sessionLimit = anrBehavior.getMaxAnrIntervalsPerSession() + return !configService.anrBehavior.isNativeThreadAnrSamplingEnabled() || intervals.size >= sessionLimit + } + + override fun getNativeSymbols(): Map? = symbols.value + + override fun getCapturedIntervals(receivedTermination: Boolean?): List? { + if (!configService.anrBehavior.isNativeThreadAnrSamplingEnabled()) { + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Native thread Sampling not enabled" + ) + return null + } + + // optimization: avoid trying to make a JNI call every 2s due to regular session caching! + if (sampling && receivedTermination == false) { + // fetch JNI samples (blocks main thread, but no way around it if we want + // the information in the session) + fetchIntervals() + } + + // the ANR might end before samples with offsets are recorded - avoid + // recording an empty sample in the payload if this is the case. + val usefulSamples = intervals.toList().filter { it.samples?.isNotEmpty() ?: false } + if (usefulSamples.isEmpty()) { + return null + } + return usefulSamples.toList() + } + + /** + * Determines whether or not we should sample the target thread based on the thread stacktrace + * and the ANR config. + */ + @VisibleForTesting + internal fun containsAllowedStackframes( + anrBehavior: AnrBehavior, + stacktrace: Array + ): Boolean { + if (anrBehavior.isNativeThreadAnrSamplingAllowlistIgnored()) { + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "Ignore native thread ANR sampling allow list" + ) + return true + } + val allowlist = anrBehavior.getNativeThreadAnrSamplingAllowlist() + + logger.logDeveloper( + "EmbraceNativeThreadSamplerService", + "getNativeThreadAnrSamplingAllowlist size: " + allowlist.size + ) + + return stacktrace.any { frame -> + allowlist.any { allowed -> + frame.methodName == allowed.method && frame.className == allowed.clz + } + } + } +} + +internal fun isUnityMainThread(): Boolean = "UnityMain" == Thread.currentThread().name diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerInstaller.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerInstaller.kt new file mode 100644 index 0000000000..7e40501609 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerInstaller.kt @@ -0,0 +1,129 @@ +package io.embrace.android.embracesdk.anr.ndk + +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.NativeThreadAnrSample +import java.util.concurrent.atomic.AtomicBoolean + +internal class NativeThreadSamplerNdkDelegate : EmbraceNativeThreadSamplerService.NdkDelegate { + external override fun setupNativeThreadSampler(is32Bit: Boolean): Boolean + external override fun monitorCurrentThread(): Boolean + external override fun startSampling(unwinderOrdinal: Int, intervalMs: Long) + external override fun finishSampling(): List? +} + +internal class NativeThreadSamplerInstaller( + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger +) { + + private val isMonitoring = AtomicBoolean(false) + private var targetHandler: Handler? = null + + @VisibleForTesting + internal var currentThread: Thread? = null + + private fun prepareTargetHandler() { + // We create a Handler here so that when the functionality is disabled locally + // but enabled remotely, the config change callback also runs the install + // on the target thread. + if (Looper.myLooper() == null) { + Looper.prepare() + } + + val looper = Looper.myLooper() + targetHandler = when { + looper != null -> Handler(looper) + else -> null + } + if (targetHandler == null) { + logger.logError( + "Native thread sampler init failed: Failed to create Handler for target native thread" + ) + return + } + } + + fun monitorCurrentThread( + sampler: NativeThreadSamplerService, + configService: ConfigService, + anrService: AnrService + ) { + if (isMonitoringCurrentThread()) { + logger.logDeveloper( + "NativeThreadSamplerInstaller", + "Skipping monitorCurrentThread as current thread already monitored." + ) + return + } else { + // disable monitoring since we can end up here if monitoring was enabled, + // but the target thread has changed + isMonitoring.set(false) + } + + currentThread = Thread.currentThread() + prepareTargetHandler() + + if (configService.anrBehavior.isNativeThreadAnrSamplingEnabled()) { + monitorCurrentThread(sampler, anrService) + } else { + InternalStaticEmbraceLogger.logDeveloper( + "NativeThreadSamplerInstaller", + "isNativeThreadAnrSamplingEnabled disabled." + ) + } + + // always install the handler. if config subsequently changes we take the decision + // to just ignore anr intervals, rather than attempting to uninstall the handler + configService.addListener { + onConfigChange(configService, sampler, anrService) + } + } + + private fun isMonitoringCurrentThread(): Boolean { + return isMonitoring.get() && Thread.currentThread().id == currentThread?.id + } + + private fun onConfigChange( + configService: ConfigService, + sampler: NativeThreadSamplerService, + anrService: AnrService + ) { + targetHandler?.post( + Runnable { + if (configService.anrBehavior.isNativeThreadAnrSamplingEnabled() && !isMonitoring.get()) { + InternalStaticEmbraceLogger.logDeveloper( + "NativeThreadSamplerInstaller", + "Native Thread ANR Sampling Enabled, proceed to install" + ) + monitorCurrentThread(sampler, anrService) + } + } + ) + } + + private fun monitorCurrentThread(sampler: NativeThreadSamplerService, anrService: AnrService) { + synchronized(this) { + if (!isMonitoring.get()) { + logger.logInfo("Installing native sampling on '${Thread.currentThread().name}'") + if (sampler.monitorCurrentThread()) { + InternalStaticEmbraceLogger.logDeveloper( + "NativeThreadSamplerInstaller", + "Native sampler installed" + ) + anrService.addBlockedThreadListener(sampler) + isMonitoring.set(true) + } + } else { + InternalStaticEmbraceLogger.logDeveloper( + "NativeThreadSamplerInstaller", + "NativeThreadSamplerService already installed" + ) + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService.kt new file mode 100644 index 0000000000..259478f816 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadSamplerService.kt @@ -0,0 +1,30 @@ +package io.embrace.android.embracesdk.anr.ndk + +import io.embrace.android.embracesdk.anr.BlockedThreadListener +import io.embrace.android.embracesdk.payload.NativeThreadAnrInterval +import io.embrace.android.embracesdk.session.MemoryCleanerListener + +/** + * Samples the target thread stacktrace when the thread is detected as blocked. + * + * The NDK layer must be enabled in order to use this functionality as this class + * calls native code. + */ +internal interface NativeThreadSamplerService : + BlockedThreadListener, + MemoryCleanerListener { + + /** + * Performs one-time setup of the native stacktrace sampler (but doesn't start any monitoring). + */ + fun setupNativeSampler(): Boolean + + /** + * Monitors the current thread. + */ + fun monitorCurrentThread(): Boolean + + fun getNativeSymbols(): Map? + + fun getCapturedIntervals(receivedTermination: Boolean?): List? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegate.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegate.kt new file mode 100644 index 0000000000..e370c51149 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegate.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import java.io.File + +internal class FilesDelegate { + fun getThreadsFileForCurrentProcess(): File { + return File("/proc/self/task") + } + + fun getCommandFileForThread(threadId: String): File { + return File("/proc/$threadId/comm") + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FindGoogleThread.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FindGoogleThread.kt new file mode 100644 index 0000000000..98b37ce4b1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/FindGoogleThread.kt @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger + +internal class FindGoogleThread( + private val logger: InternalEmbraceLogger, + private val getThreadsInCurrentProcess: GetThreadsInCurrentProcess, + private val getThreadCommand: GetThreadCommand +) { + + /** + * Search the app's threads to find the Google ANR watcher thread. + * + * @return the thread ID for the Google ANR watcher thread, or 0 if one cannot be found + */ + + operator fun invoke(): Int { + logger.logInfo("Searching for Google thread ID for ANR detection") + val threads = getThreadsInCurrentProcess() + for (threadId in threads) { + val command = getThreadCommand(threadId) + if (command.startsWith("Signal Catcher")) { + return threadId.toIntOrNull() ?: 0 + } + } + return 0 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommand.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommand.kt new file mode 100644 index 0000000000..704b61a94d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommand.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.anr.sigquit + +internal class GetThreadCommand( + private val filesDelegate: FilesDelegate +) { + + /** + * Get command name associated with thread id + * + * @return a command name, or empty if none found + */ + + operator fun invoke(threadId: String): String { + val file = filesDelegate.getCommandFileForThread(threadId) + return try { + file.readText() + } catch (e: Exception) { + "" + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess.kt new file mode 100644 index 0000000000..c08df3ccb8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcess.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.anr.sigquit + +internal class GetThreadsInCurrentProcess( + private val filesDelegate: FilesDelegate +) { + + /** + * Get threads associated with the current process + * + * @return a list of numerical thread ids + */ + + operator fun invoke(): List { + val dir = filesDelegate.getThreadsFileForCurrentProcess() + return try { + dir.listFiles()?.map { it.name } ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate.kt new file mode 100644 index 0000000000..e4011f4d6c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrHandlerNativeDelegate.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger + +// IMPORTANT: This class is referenced by anr.c. Move or rename both at the same time, or it will break. +internal class GoogleAnrHandlerNativeDelegate( + private val googleAnrTimestampRepository: GoogleAnrTimestampRepository, + private val logger: InternalEmbraceLogger +) { + + fun install(googleThreadId: Int): Int { + return try { + installGoogleAnrHandler(googleThreadId) + } catch (exception: UnsatisfiedLinkError) { + logger.logError("Could not install ANR Handler. Exception: $exception") + 1 + } + } + + @Synchronized + fun saveGoogleAnr(timestamp: Long) { + logger.logInfo("got Google ANR timestamp $timestamp") + googleAnrTimestampRepository.add(timestamp) + } + + private external fun installGoogleAnrHandler(googleThreadId: Int): Int +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository.kt new file mode 100644 index 0000000000..1cf124c2a4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepository.kt @@ -0,0 +1,56 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger + +// we never want anything the SDK gathers to be unbounded, so we set this limit on the number of Google ANR +// timestamps we collect even though it is unlikely that this limit will be reached. +private const val MAX_GOOGLE_ANR_COUNT: Long = 50 + +/* We don't want to miss ANRs too close to the start or end time of the session. We'd rather get more ANRs than + necessary than to miss an ANR. We extend the range of time in which we are searching ANRs by this time margin.*/ +private const val TIME_MARGIN = 5L + +internal class GoogleAnrTimestampRepository(private val logger: InternalEmbraceLogger) { + private val googleAnrTimestamps = ArrayList() + + fun add(timestamp: Long) { + if (googleAnrTimestamps.size >= MAX_GOOGLE_ANR_COUNT) { + logger.logWarning("The max number of Google ANR intervals has been reached. Ignoring this one.") + return + } + googleAnrTimestamps.add(timestamp) + } + + /** + * Gets the intervals during which the application was not responding (ANR). + * + * @param startTime the time to search from + * @param endTime the time to search until + * @return the list of Google ANR timestamps + */ + + fun getGoogleAnrTimestamps(startTime: Long, endTime: Long): List { + synchronized(this) { + val extendedStartTime = startTime - TIME_MARGIN + val extendedEndTime = endTime + TIME_MARGIN + val results = mutableListOf() + if (extendedStartTime > extendedEndTime) { + return emptyList() + } + for (value in googleAnrTimestamps) { + // Values were added to the end of the list, so once we have a value past + // the end time, any other values would also be past the end time. + if (value > extendedEndTime) { + break + } else if (value >= extendedStartTime) { + results.add(value) + } + } + return results + } + } + + fun clear() { + googleAnrTimestamps.clear() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService.kt new file mode 100644 index 0000000000..38a0400584 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/sigquit/SigquitDetectionService.kt @@ -0,0 +1,83 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import io.embrace.android.embracesdk.utils.ThreadUtils +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean + +internal class SigquitDetectionService( + private val sharedObjectLoader: SharedObjectLoader, + private val findGoogleThread: FindGoogleThread, + private val googleAnrHandlerNativeDelegate: GoogleAnrHandlerNativeDelegate, + private val googleAnrTimestampRepository: GoogleAnrTimestampRepository, + var configService: ConfigService, + private val logger: InternalEmbraceLogger +) : MemoryCleanerListener { + + private val googleAnrTrackerInstalled = AtomicBoolean(false) + + private fun installGoogleAnrHandler(googleThreadId: Int) { + val res = googleAnrHandlerNativeDelegate.install(googleThreadId) + if (res > 0) { + googleAnrTrackerInstalled.set(false) + logger.logError( + String.format( + Locale.US, + "Could not initialize Google ANR tracking {code=%d}", + res + ) + ) + } else { + logger.logInfo("Google Anr Tracker handler installed successfully") + } + } + + fun initializeGoogleAnrTracking() { + logger.logDeveloper( + "EmbraceAnrService", + "Deciding whether to initialize Google ANR Tracking" + ) + if (configService.anrBehavior.isGoogleAnrCaptureEnabled()) { + setupGoogleAnrTracking() + } else { + // always install the handler. if config subsequently changes we won't install the tracker twice, nor + // we will install it if it's disabled. + configService.addListener { setupGoogleAnrTracking() } + } + } + + private fun setupGoogleAnrTracking() { + if (configService.anrBehavior.isGoogleAnrCaptureEnabled() && !googleAnrTrackerInstalled.getAndSet(true)) { + ThreadUtils.runOnMainThread { setupGoogleAnrHandler() } + } + } + + @VisibleForTesting + fun setupGoogleAnrHandler() { + logger.logDeveloper("EmbraceAnrService", "Setting up Google ANR Handler") + // TODO: split up the ANR tracking and NDK crash reporter libs + if (!sharedObjectLoader.loadEmbraceNative()) { + googleAnrTrackerInstalled.set(false) + return + } + + // we must find the Google watcher thread in order to install the Google ANR handle. + val googleThreadId = findGoogleThread.invoke() + if (googleThreadId <= 0) { + logger.logError("Could not initialize Google ANR tracking: Google thread not found.") + googleAnrTrackerInstalled.set(false) + return + } + // run the JNI call from main thread since JNI calls return to the thread where + // they were called. + installGoogleAnrHandler(googleThreadId) + } + + override fun cleanCollections() { + googleAnrTimestampRepository.clear() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/DataCaptureService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/DataCaptureService.kt new file mode 100644 index 0000000000..9b1a3cb19c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/DataCaptureService.kt @@ -0,0 +1,24 @@ +package io.embrace.android.embracesdk.arch + +import io.embrace.android.embracesdk.session.MemoryCleanerListener + +/** + * Represents a service that captures data passively, usually in response to callbacks, system + * events, or just at regular intervals. This interface currently has two contracts: + * + * 1. The service must reset all its state (including captured data) when [cleanCollections] is called. + * 2. The service must return all the data it has captured so far when [getCapturedData] is called. + * + * This approach avoids needing any knowledge about session boundaries or what the session payload + * looks like. It also simplifies testing. + */ +internal interface DataCaptureService : MemoryCleanerListener { + + /** + * Returns a representation of all the data that has already been captured so far. + * + * This does NOT mean that implementations should go capture data - they should just return + * what has already been captured, if anything. + */ + fun getCapturedData(): T +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt new file mode 100644 index 0000000000..2357316d63 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt @@ -0,0 +1,83 @@ +package io.embrace.android.embracesdk.capture + +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerService +import io.embrace.android.embracesdk.anr.sigquit.GoogleAnrTimestampRepository +import io.embrace.android.embracesdk.capture.aei.ApplicationExitInfoService +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.memory.MemoryService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.powersave.PowerSaveModeService +import io.embrace.android.embracesdk.capture.strictmode.StrictModeService +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.network.logging.NetworkLoggingService +import io.embrace.android.embracesdk.payload.NetworkRequests +import io.embrace.android.embracesdk.payload.PerformanceInfo + +internal class EmbracePerformanceInfoService( + private val anrService: AnrService?, + private val networkConnectivityService: NetworkConnectivityService, + private val networkLoggingService: NetworkLoggingService, + private val powerSaveModeService: PowerSaveModeService, + private val memoryService: MemoryService, + private val metadataService: MetadataService, + private val googleAnrTimestampRepository: GoogleAnrTimestampRepository, + private val applicationExitInfoService: ApplicationExitInfoService?, + private val strictModeService: StrictModeService, + private val nativeThreadSamplerService: NativeThreadSamplerService? +) : PerformanceInfoService { + + override fun getSessionPerformanceInfo( + sessionStart: Long, + sessionLastKnownTime: Long, + coldStart: Boolean, + receivedTermination: Boolean? + ): PerformanceInfo { + logDeveloper( + "EmbracePerformanceInfoService", + "Session performance info start time: $sessionStart" + ) + val requests = NetworkRequests( + networkLoggingService.getNetworkCallsForSession( + sessionStart, + sessionLastKnownTime + ) + ) + val info = getPerformanceInfo(sessionStart, sessionLastKnownTime, coldStart) + + return info.copy( + appExitInfoData = when { + applicationExitInfoService != null && + coldStart -> ArrayList(applicationExitInfoService.getCapturedData()) + else -> null + }, + networkRequests = requests, + anrIntervals = anrService?.getCapturedData()?.toList(), + anrProcessErrors = anrService?.getAnrProcessErrors(sessionStart)?.toList(), + googleAnrTimestamps = googleAnrTimestampRepository.getGoogleAnrTimestamps( + sessionStart, + sessionLastKnownTime + ).toList(), + powerSaveModeIntervals = powerSaveModeService.getCapturedData()?.toList(), + strictmodeViolations = strictModeService.getCapturedData()?.toList(), + nativeThreadAnrIntervals = nativeThreadSamplerService?.getCapturedIntervals( + receivedTermination + ) + ) + } + + override fun getPerformanceInfo( + startTime: Long, + endTime: Long, + coldStart: Boolean + ): PerformanceInfo { + logDeveloper("EmbracePerformanceInfoService", "Building performance info") + + return PerformanceInfo( + diskUsage = metadataService.getDiskUsage()?.copy(), + memoryWarnings = memoryService.getCapturedData()?.toList(), + networkInterfaceIntervals = networkConnectivityService.getCapturedData()?.toList(), + powerSaveModeIntervals = powerSaveModeService.getCapturedData()?.toList(), + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt new file mode 100644 index 0000000000..596d0e8789 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.capture + +import io.embrace.android.embracesdk.payload.PerformanceInfo + +/** + * Generates performance payloads using combined performance metrics from the device, including: + * + * * CPU + * * Power + * * ANR intervals + * * Network calls + * * Memory usage + * * Disk usage + * * Signal strength + * * Connection quality class + * + */ +internal interface PerformanceInfoService { + + /** + * Gets the device performance information payload for an event. This is sent only with the + * following events, because they have a corresponding start event so a time window can + * be computed for capturing the performance information: + * + * * LATE + * * END + * * INTERRUPT + * + * + * @param startTime the start time of the performance information to retrieve + * @param endTime the end time of the performance information to retrieve + * @return the performance information + */ + fun getPerformanceInfo( + startTime: Long, + endTime: Long, + coldStart: Boolean + ): PerformanceInfo + + /** + * Gets the device performance information payload to send with the session message. This + * indicates activity on the device during that particular session, and is used to build a + * timeline of events. This is like the [PerformanceInfo], but contains a timeline + * of network events. + * + * @param sessionStart the start time of the session + * @param sessionLastKnownTime the last known time of the session + * @param coldStart whether the session was a cold start or not + * @param receivedTermination whether the session received a termination or not + * @param isNotCachedSession whether the process is terminating due to a crash or it ended up cleanly and is not forceQuit + * @return the performance information for the session + */ + fun getSessionPerformanceInfo( + sessionStart: Long, + sessionLastKnownTime: Long, + coldStart: Boolean, + receivedTermination: Boolean? + ): PerformanceInfo +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService.kt new file mode 100644 index 0000000000..2f7856a243 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/ApplicationExitInfoService.kt @@ -0,0 +1,6 @@ +package io.embrace.android.embracesdk.capture.aei + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.AppExitInfoData + +internal interface ApplicationExitInfoService : DataCaptureService> diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService.kt new file mode 100644 index 0000000000..513099287b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/EmbraceApplicationExitInfoService.kt @@ -0,0 +1,239 @@ +package io.embrace.android.embracesdk.capture.aei + +import android.app.ActivityManager +import android.app.ApplicationExitInfo +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.config.ConfigListener +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.AppExitInfoBehavior +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logInfoWithException +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logWarningWithException +import io.embrace.android.embracesdk.payload.AppExitInfoData +import io.embrace.android.embracesdk.prefs.PreferencesService +import java.io.BufferedReader +import java.io.IOException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.atomic.AtomicBoolean + +@RequiresApi(Build.VERSION_CODES.R) +internal class EmbraceApplicationExitInfoService constructor( + private val executorService: ExecutorService, + private val configService: ConfigService, + private val activityManager: ActivityManager?, + private val preferencesService: PreferencesService, + private val deliveryService: DeliveryService +) : ApplicationExitInfoService, ConfigListener { + + companion object { + private const val SDK_AEI_SEND_LIMIT = 32 + } + + @VisibleForTesting + @Volatile + var backgroundExecution: Future<*>? = null + + private val sessionApplicationExitInfoData: MutableList = mutableListOf() + private var isSessionApplicationExitInfoDataReady = AtomicBoolean(false) + + init { + configService.addListener(this) + if (configService.isAppExitInfoCaptureEnabled()) { + startService() + } + } + + private fun startService() { + backgroundExecution = try { + executorService.submit { + try { + processApplicationExitInfo() + } catch (exc: Throwable) { + logWarningWithException("AEI - Failed to process AEIs due to unexpected error", exc, true) + } + } + } catch (exc: RejectedExecutionException) { + logWarningWithException("AEI - Failed to schedule AEI processing", exc, true) + null + } + } + + private fun processApplicationExitInfo() { + val historicalProcessExitReasons = getHistoricalProcessExitReasons() + + val unsentExitReasons = getUnsentExitReasons(historicalProcessExitReasons) + + unsentExitReasons.forEach { + sessionApplicationExitInfoData.add(buildSessionAppExitInfoData(it, null, null)) + } + + isSessionApplicationExitInfoDataReady.set(true) + + // now send AEIs with blobs. + processApplicationExitInfoBlobs(unsentExitReasons) + } + + private fun processApplicationExitInfoBlobs(unsentExitReasons: List) { + unsentExitReasons.forEach { aei: ApplicationExitInfo -> + val traceResult = collectExitInfoTrace(aei) + if (traceResult != null) { + val payload = buildSessionAppExitInfoData( + aei, + getTrace(traceResult), + getTraceStatus(traceResult) + ) + sendApplicationExitInfoWithTraces(listOf(payload)) + } + } + } + + private fun getHistoricalProcessExitReasons(): List { + // A process ID that used to belong to this package but died later; + // a value of 0 means to ignore this parameter and return all matching records. + val pid = 0 + + // number of results to be returned; a value of 0 means to ignore this parameter and return + // all matching records with a maximum of 16 entries + val maxNum = configService.appExitInfoBehavior.appExitInfoMaxNum() + + var historicalProcessExitReasons: List = + activityManager?.getHistoricalProcessExitReasons(null, pid, maxNum) ?: return emptyList() + + if (historicalProcessExitReasons.size > SDK_AEI_SEND_LIMIT) { + logInfoWithException("AEI - size greater than $SDK_AEI_SEND_LIMIT") + historicalProcessExitReasons = historicalProcessExitReasons.take(SDK_AEI_SEND_LIMIT) + } + + return historicalProcessExitReasons + } + + private fun getUnsentExitReasons(historicalProcessExitReasons: List): List { + // Generates the set of current aei captured + val allAeiHashCodes = historicalProcessExitReasons.map(::generateUniqueHash).toSet() + + // Get hash codes that were previously delivered + val deliveredHashCodes = preferencesService.applicationExitInfoHistory ?: emptySet() + + // Subtracts aei hashcodes of already sent information to get new entries + val unsentHashCodes = allAeiHashCodes.subtract(deliveredHashCodes) + + // Updates preferences with the new set of hashcodes + preferencesService.applicationExitInfoHistory = allAeiHashCodes + + // Get AEI objects that were not sent + val unsentAeiObjects = historicalProcessExitReasons.filter { + unsentHashCodes.contains(generateUniqueHash(it)) + } + + return unsentAeiObjects + } + + @VisibleForTesting + fun buildSessionAppExitInfoData(appExitInfo: ApplicationExitInfo, trace: String?, traceStatus: String?): AppExitInfoData { + val sessionId = String(appExitInfo.processStateSummary ?: ByteArray(0)) + + return AppExitInfoData( + sessionId = sessionId, + sessionIdError = getSessionIdValidationError(sessionId), + importance = appExitInfo.importance, + pss = appExitInfo.pss, + reason = appExitInfo.reason, + rss = appExitInfo.rss, + status = appExitInfo.status, + timestamp = appExitInfo.timestamp, + trace = trace, + description = appExitInfo.description, + traceStatus = traceStatus + ) + } + + private fun getTrace(traceResult: AppExitInfoBehavior.CollectTracesResult): String? { + return when (traceResult) { + is AppExitInfoBehavior.CollectTracesResult.Success -> traceResult.result + is AppExitInfoBehavior.CollectTracesResult.TooLarge -> traceResult.result + else -> null + } + } + + private fun getTraceStatus(traceResult: AppExitInfoBehavior.CollectTracesResult): String? { + return when (traceResult) { + is AppExitInfoBehavior.CollectTracesResult.Success -> null + is AppExitInfoBehavior.CollectTracesResult.TooLarge -> "Trace was too large, sending truncated trace" + else -> traceResult.result + } + } + + private fun sendApplicationExitInfoWithTraces(appExitInfoWithTraces: List) { + if (appExitInfoWithTraces.isNotEmpty()) { + deliveryService.sendAEIBlob(appExitInfoWithTraces) + } + } + + private fun collectExitInfoTrace(appExitInfo: ApplicationExitInfo): AppExitInfoBehavior.CollectTracesResult? { + try { + val trace = appExitInfo.traceInputStream?.bufferedReader()?.use(BufferedReader::readText) + + if (trace == null) { + logDebug("AEI - No info trace collected") + return null + } + + val traceMaxLimit = configService.appExitInfoBehavior.getTraceMaxLimit() + if (trace.length > traceMaxLimit) { + logInfoWithException("AEI - Blob size was reduced. Current size is ${trace.length} and the limit is $traceMaxLimit") + return AppExitInfoBehavior.CollectTracesResult.TooLarge(trace.take(traceMaxLimit)) + } + + return AppExitInfoBehavior.CollectTracesResult.Success(trace) + } catch (e: IOException) { + logWarningWithException("AEI - IOException: ${e.message}", e, true) + return AppExitInfoBehavior.CollectTracesResult.TraceException(("ioexception: ${e.message}")) + } catch (e: OutOfMemoryError) { + logWarningWithException("AEI - Out of Memory: ${e.message}", e, true) + return AppExitInfoBehavior.CollectTracesResult.TraceException(("oom: ${e.message}")) + } catch (tr: Throwable) { + logWarningWithException("AEI - An error occurred: ${tr.message}", tr, true) + return AppExitInfoBehavior.CollectTracesResult.TraceException(("error: ${tr.message}")) + } + } + + private fun getSessionIdValidationError(sid: String): String { + return if (sid.isEmpty() || sid.matches(Regex("^[0-9a-fA-F]{32}\$"))) { + "" + } else { + "invalid session ID: $sid" + } + } + + private fun generateUniqueHash(appExitInfo: ApplicationExitInfo): String { + return "${appExitInfo.timestamp}_${appExitInfo.pid}" + } + + override fun cleanCollections() { + } + + override fun getCapturedData() = + sessionApplicationExitInfoData.takeIf { isSessionApplicationExitInfoDataReady.get() } ?: emptyList() + + override fun onConfigChange(configService: ConfigService) { + if (backgroundExecution == null && configService.isAppExitInfoCaptureEnabled()) { + startService() + } else if (!configService.isAppExitInfoCaptureEnabled()) { + endService() + } + } + + private fun endService() { + try { + backgroundExecution?.cancel(true) + backgroundExecution = null + } catch (t: Throwable) { + logWarningWithException("AEI - Failed to disable EmbraceApplicationExitInfoService work", t) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/NoOpApplicationExitInfoService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/NoOpApplicationExitInfoService.kt new file mode 100644 index 0000000000..5fdef34039 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/aei/NoOpApplicationExitInfoService.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.capture.aei + +import io.embrace.android.embracesdk.payload.AppExitInfoData + +/** + * No-op class to represent EmbraceApplicationExitInfo implementation for Android version below 11 + */ +internal class NoOpApplicationExitInfoService : ApplicationExitInfoService { + + override fun cleanCollections() { + } + + override fun getCapturedData(): List = ArrayList() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService.kt new file mode 100644 index 0000000000..d830059d3d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/EmbraceNetworkConnectivityService.kt @@ -0,0 +1,187 @@ +package io.embrace.android.embracesdk.capture.connectivity + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.delivery.NetworkStatus +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.payload.Interval +import java.net.Inet4Address +import java.net.NetworkInterface +import java.util.NavigableMap +import java.util.TreeMap +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService + +@Suppress("DEPRECATION") // uses deprecated APIs for backwards compat +internal class EmbraceNetworkConnectivityService( + private val context: Context, + private val clock: Clock, + private val registrationExecutorService: ExecutorService, + private val logger: InternalEmbraceLogger, + private val connectivityManager: ConnectivityManager? +) : BroadcastReceiver(), NetworkConnectivityService { + + private val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + private val networkReachable: NavigableMap = TreeMap() + private val networkConnectivityListeners = mutableListOf() + override val ipAddress by lazy { calculateIpAddress() } + + init { + registerConnectivityActionReceiver() + } + + override fun onReceive(context: Context, intent: Intent) = handleNetworkStatus(true) + + override fun getCapturedData(): List { + logger.logDeveloper("EmbraceNetworkConnectivityService", "getNetworkInterfaceIntervals") + val endTime = clock.now() + synchronized(this) { + val results: MutableList = ArrayList() + networkReachable.subMap(0, endTime).forEach { (currentTime, value) -> + val next = networkReachable.higherKey(currentTime) + results.add(Interval(currentTime, next ?: endTime, value.value)) + } + return results + } + } + + override fun networkStatusOnSessionStarted(startTime: Long) = handleNetworkStatus(false, startTime) + + private fun handleNetworkStatus(notifyListeners: Boolean, timestamp: Long = clock.now()) { + try { + logger.logDeveloper("EmbraceNetworkConnectivityService", "handleNetworkStatus") + val networkStatus = getCurrentNetworkStatus() + val savedStatus = saveStatus(timestamp, networkStatus) + if (savedStatus && notifyListeners) { + logger.logInfo("Network status changed to: " + networkStatus.name) + notifyNetworkConnectivityListeners(networkStatus) + } + } catch (ex: Exception) { + logger.logDebug("Failed to record network connectivity", ex) + } + } + + override fun getCurrentNetworkStatus(): NetworkStatus { + var networkStatus: NetworkStatus + try { + val networkInfo = connectivityManager?.activeNetworkInfo + if (networkInfo != null && networkInfo.isConnected) { + // Network is reachable + when (networkInfo.type) { + ConnectivityManager.TYPE_WIFI -> { + logger.logDeveloper( + "EmbraceNetworkConnectivityService", + "Network connected to WIFI" + ) + networkStatus = NetworkStatus.WIFI + } + + ConnectivityManager.TYPE_MOBILE -> { + logger.logDeveloper( + "EmbraceNetworkConnectivityService", + "Network connected to MOBILE" + ) + networkStatus = NetworkStatus.WAN + } + + else -> { + logger.logDeveloper( + "EmbraceNetworkConnectivityService", + "Network is reachable but type is not WIFI or MOBILE" + ) + networkStatus = NetworkStatus.UNKNOWN + } + } + } else { + // Network is not reachable + logger.logDeveloper("EmbraceNetworkConnectivityService", "Network not reachable") + networkStatus = NetworkStatus.NOT_REACHABLE + } + } catch (e: java.lang.Exception) { + logger.logError("Error while trying to get connectivity status.", e) + networkStatus = NetworkStatus.UNKNOWN + } + return networkStatus + } + + private fun saveStatus(timestamp: Long, networkStatus: NetworkStatus): Boolean { + synchronized(this) { + if (networkReachable.isEmpty() || networkReachable.lastEntry()?.value != networkStatus) { + networkReachable[timestamp] = networkStatus + return true + } + } + return false + } + + private fun registerConnectivityActionReceiver() { + registrationExecutorService.submit( + Callable { + try { + context.registerReceiver(this, intentFilter) + } catch (ex: Exception) { + logger.logDebug( + "Failed to register EmbraceNetworkConnectivityService " + + "broadcast receiver. Connectivity status will be unavailable.", + ex + ) + } + null + } + ) + } + + override fun close() { + context.unregisterReceiver(this) + logger.logDeveloper("EmbraceNetworkConnectivityService", "closed") + } + + override fun cleanCollections() { + networkReachable.clear() + logger.logDeveloper("EmbraceNetworkConnectivityService", "Collections cleaned") + } + + /** + * Adds a listener for changes in the connectivity status. + */ + override fun addNetworkConnectivityListener(listener: NetworkConnectivityListener) { + networkConnectivityListeners.add(listener) + } + + /** + * Removes a listener for changes in the connectivity status. + */ + override fun removeNetworkConnectivityListener(listener: NetworkConnectivityListener) { + networkConnectivityListeners.remove(listener) + } + + private fun notifyNetworkConnectivityListeners(status: NetworkStatus) { + for (listener in networkConnectivityListeners) { + listener.onNetworkConnectivityStatusChanged(status) + } + } + + private fun calculateIpAddress(): String? { + try { + val en = NetworkInterface.getNetworkInterfaces() + while (en.hasMoreElements()) { + val intf = en.nextElement() + val enumIpAddr = intf.inetAddresses + while (enumIpAddr.hasMoreElements()) { + val inetAddress = enumIpAddr.nextElement() + if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) { + return inetAddress.getHostAddress() + } + } + } + } catch (ex: Exception) { + logDebug("Cannot get IP Address") + } + return null + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityListener.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityListener.kt new file mode 100644 index 0000000000..ac983a3216 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityListener.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.capture.connectivity + +import io.embrace.android.embracesdk.comms.delivery.NetworkStatus + +internal interface NetworkConnectivityListener { + + /** + * Called when the network status has changed. + */ + fun onNetworkConnectivityStatusChanged(status: NetworkStatus) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService.kt new file mode 100644 index 0000000000..4bbd05507b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NetworkConnectivityService.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.capture.connectivity + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.comms.delivery.NetworkStatus +import io.embrace.android.embracesdk.payload.Interval +import java.io.Closeable + +/** + * Detects and records which network the device is connected to. + */ +internal interface NetworkConnectivityService : DataCaptureService?>, Closeable { + + /** + * Record the connection type at the start of the session and open a connectivity interval with it, + * with a start time that matches the session start time. + * + * @param startTime of the session + */ + fun networkStatusOnSessionStarted(startTime: Long) + + /** + * Adds a listener for changes in the connectivity status. + */ + fun addNetworkConnectivityListener(listener: NetworkConnectivityListener) + + /** + * Removes a listener for changes in the connectivity status. + */ + fun removeNetworkConnectivityListener(listener: NetworkConnectivityListener) + + /** + * Returns the current NetworkStatus. + */ + fun getCurrentNetworkStatus(): NetworkStatus + + /** + * Calculate the device's IP address + */ + val ipAddress: String? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NoOpNetworkConnectivityService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NoOpNetworkConnectivityService.kt new file mode 100644 index 0000000000..c3cc6dad96 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/connectivity/NoOpNetworkConnectivityService.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.capture.connectivity + +import io.embrace.android.embracesdk.comms.delivery.NetworkStatus +import io.embrace.android.embracesdk.payload.Interval + +internal class NoOpNetworkConnectivityService : NetworkConnectivityService { + override fun close() {} + + override fun cleanCollections() { + } + + override fun getCapturedData(): List = emptyList() + + override fun networkStatusOnSessionStarted(startTime: Long) {} + + override fun addNetworkConnectivityListener(listener: NetworkConnectivityListener) {} + + override fun removeNetworkConnectivityListener(listener: NetworkConnectivityListener) {} + + override fun getCurrentNetworkStatus(): NetworkStatus { + return NetworkStatus.UNKNOWN + } + + override val ipAddress: String? = null +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/CpuInfoDelegate.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/CpuInfoDelegate.kt new file mode 100644 index 0000000000..083b5ef619 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/CpuInfoDelegate.kt @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk.capture.cpu + +/** + * Component to get detailed CPU information from a device + */ +internal interface CpuInfoDelegate { + /** + * Get the name of the primary CPU of the device + */ + fun getCpuName(): String? + + /** + * Get the ELG of the primary CPU of the device + */ + fun getElg(): String? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegate.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegate.kt new file mode 100644 index 0000000000..5667624c60 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegate.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.capture.cpu + +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger + +internal class EmbraceCpuInfoDelegate( + private val sharedObjectLoader: SharedObjectLoader, + private val logger: InternalEmbraceLogger +) : CpuInfoDelegate { + + override fun getCpuName(): String? { + return if (sharedObjectLoader.loadEmbraceNative()) { + try { + getNativeCpuName() + } catch (exception: LinkageError) { + logger.logError("Could not get the CPU name. Exception: $exception", exception) + null + } + } else null + } + + override fun getElg(): String? { + return if (sharedObjectLoader.loadEmbraceNative()) { + try { + getNativeEgl() + } catch (exception: LinkageError) { + logger.logError("Could not get the EGL name. Exception: $exception", exception) + null + } + } else null + } + + private external fun getNativeCpuName(): String + + private external fun getNativeEgl(): String +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/CrashService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/CrashService.kt new file mode 100644 index 0000000000..1d01180bb9 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/CrashService.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.capture.crash + +import io.embrace.android.embracesdk.payload.JsException + +/** + * Service for handling crashes intercepted by the [EmbraceUncaughtExceptionHandler] and + * forwarding them on for processing. + */ +internal interface CrashService { + + /** + * Handles crashes from the [EmbraceUncaughtExceptionHandler]. + * + * @param thread the crashing thread + * @param exception the exception thrown by the thread + */ + fun handleCrash(thread: Thread, exception: Throwable) + + /** + * Associates an unhandled JS exception with a crash + * + * @param exception the [JsException] to associate with the crash + */ + fun logUnhandledJsException(exception: JsException) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceCrashService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceCrashService.kt new file mode 100644 index 0000000000..5972402db7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceCrashService.kt @@ -0,0 +1,168 @@ +package io.embrace.android.embracesdk.capture.crash + +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.internal.crash.CrashFileMarker +import io.embrace.android.embracesdk.internal.utils.Uuid.getEmbUuid +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.Crash +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.JsException +import io.embrace.android.embracesdk.session.BackgroundActivityService +import io.embrace.android.embracesdk.session.SessionService + +/** + * Intercepts uncaught Java exceptions and forwards them to the Embrace API. + */ +internal class EmbraceCrashService( + configService: ConfigService, + private val sessionService: SessionService, + private val metadataService: MetadataService, + private val deliveryService: DeliveryService, + private val userService: UserService, + private val eventService: EventService, + private val anrService: AnrService?, + private val ndkService: NdkService, + private val gatingService: GatingService, + private val backgroundActivityService: BackgroundActivityService?, + private val crashMarker: CrashFileMarker, + private val clock: Clock +) : CrashService { + + private var mainCrashHandled = false + private var jsException: JsException? = null + + init { + if (configService.autoDataCaptureBehavior.isUncaughtExceptionHandlerEnabled() && !ApkToolsConfig.IS_EXCEPTION_CAPTURE_DISABLED) { + logDeveloper("EmbraceCrashService", "crash handler enabled") + registerExceptionHandler() + } + } + + /** + * Handles a crash caught by the [EmbraceUncaughtExceptionHandler] by constructing a + * JSON message containing a description of the crash, device, and context, and then sending + * it to the Embrace API. + * + * @param thread the crashing thread + * @param exception the exception thrown by the thread + */ + override fun handleCrash(thread: Thread, exception: Throwable) { + logDeveloper("EmbraceCrashService", "Attempting to handle crash") + if (!mainCrashHandled) { + mainCrashHandled = true + + // Stop ANR tracking first to avoid capture ANR when crash message is being sent + anrService?.forceAnrTrackingStopOnCrash() + logDeveloper( + "EmbraceCrashService", + "JsException is present: ${if (jsException != null) "true" else "false"}" + ) + + // Check if the unity crash id exists. If so, means that the native crash capture + // is enabled for an Unity build. When a native crash occurs and the NDK sends an + // uncaught exception the SDK assign the unity crash id as the java crash id. + val unityCrashId = ndkService.getUnityCrashId() + val crash = if (unityCrashId != null) { + logDeveloper( + "EmbraceCrashService", + "unityCrashId is $unityCrashId" + ) + Crash.ofThrowable(exception, jsException, unityCrashId) + } else { + Crash.ofThrowable(exception, jsException) + } + logDeveloper("EmbraceCrashService", "crashId = " + crash.crashId) + + val optionalSessionId = metadataService.activeSessionId + val sessionId = if (optionalSessionId != null) { + logDeveloper("EmbraceCrashService", "Session id is present:$optionalSessionId") + optionalSessionId + } else { + logDeveloper("EmbraceCrashService", "Session id is not present:") + null + } + + val event = Event( + CRASH_REPORT_EVENT_NAME, + null, + getEmbUuid(), + sessionId, + EmbraceEvent.Type.CRASH, + clock.now(), + null, + false, + null, + metadataService.getAppState(), + null, + sessionService.getProperties(), + eventService.getActiveEventIds(), + null, + null, + null, + null + ) + val versionedEvent = EventMessage( + event, + crash, + metadataService.getDeviceInfo(), + metadataService.getAppInfo(), + userService.getUserInfo(), + null, + null, + ApiClient.MESSAGE_VERSION, + null + ) + + // Sanitize crash event + val crashEvent = gatingService.gateEventMessage(versionedEvent) + logDeveloper("EmbraceCrashService", "Attempting to send event...") + + // Save the crash to file + deliveryService.saveCrash(crashEvent) + // End, cache and send the session + sessionService.handleCrash(crash.crashId) + backgroundActivityService?.handleCrash(crash.crashId) + // Send the crash + deliveryService.sendCrash(crashEvent) + // Indicate that a crash happened so we can know that in the next launch + crashMarker.mark() + } + } + + /** + * Registers the Embrace [java.lang.Thread.UncaughtExceptionHandler] to intercept uncaught + * exceptions and forward them to the Embrace API as crashes. + */ + private fun registerExceptionHandler() { + logDeveloper("EmbraceCrashService", "registerExceptionHandler") + val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() + val embraceHandler = EmbraceUncaughtExceptionHandler(defaultHandler, this) + Thread.setDefaultUncaughtExceptionHandler(embraceHandler) + } + + /** + * Associates an unhandled JS exception with a crash + * + * @param exception the unhandled JS exception + */ + override fun logUnhandledJsException(exception: JsException) { + logDeveloper("EmbraceCrashService", "logUnhandledJsException") + this.jsException = exception + } + + companion object { + private const val CRASH_REPORT_EVENT_NAME = "_crash_report" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceUncaughtExceptionHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceUncaughtExceptionHandler.kt new file mode 100644 index 0000000000..468b65ad29 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crash/EmbraceUncaughtExceptionHandler.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.capture.crash + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug + +/** + * Intercepts uncaught exceptions from the JVM and forwards them to the Embrace API. Once handled, + * the exception is then delegated to the default [Thread.UncaughtExceptionHandler]. + */ +internal class EmbraceUncaughtExceptionHandler( + + /** + * The default uncaught exception handler; is null if not set. + */ + private val defaultHandler: Thread.UncaughtExceptionHandler?, + + /** + * The crash service which will submit the exception to the API as a crash + */ + private val crashService: CrashService +) : Thread.UncaughtExceptionHandler { + + init { + logDebug("Registered EmbraceUncaughtExceptionHandler") + } + + override fun uncaughtException(thread: Thread, exception: Throwable) { + try { + crashService.handleCrash(thread, exception) + } catch (ex: Exception) { + logDebug("Error occurred in the uncaught exception handler", ex) + } finally { + logDebug("Finished handling exception. Delegating to default handler.", exception) + defaultHandler?.uncaughtException(thread, exception) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/Breadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/Breadcrumb.kt new file mode 100644 index 0000000000..e5eb14bd8e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/Breadcrumb.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.capture.crumbs + +/** + * Describes a user's journey through the application. + */ +internal interface Breadcrumb { + + /** + * Gets the timestamp of the event. + * + * @return the timestamp + */ + fun getStartTime(): Long +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbService.kt new file mode 100644 index 0000000000..caea5d737f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbService.kt @@ -0,0 +1,229 @@ +package io.embrace.android.embracesdk.capture.crumbs + +import android.util.Pair +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.CustomBreadcrumb +import io.embrace.android.embracesdk.payload.FragmentBreadcrumb +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb.NotificationType +import io.embrace.android.embracesdk.payload.RnActionBreadcrumb +import io.embrace.android.embracesdk.payload.TapBreadcrumb +import io.embrace.android.embracesdk.payload.TapBreadcrumb.TapBreadcrumbType +import io.embrace.android.embracesdk.payload.ViewBreadcrumb +import io.embrace.android.embracesdk.payload.WebViewBreadcrumb + +/** + * Service which stores breadcrumbs for the application. + */ +internal interface BreadcrumbService { + + /** + * Gets the view breadcrumbs in the specified time window. + * If the number of elements exceeds the limit, this will return the newest (latest) ones. + * + * @param start the start time + * @param end the end time + * @return the list of Breadcrumbs + */ + fun getViewBreadcrumbsForSession(start: Long, end: Long): List + + /** + * Gets the Taps breadcrumbs in the specified time window. + * If the number of elements exceeds the limit, this will return the newest (latest) ones. + * + * @param start the start time + * @param end the end time + * @return the list of Breadcrumbs + */ + fun getTapBreadcrumbsForSession(start: Long, end: Long): List + + /** + * Gets the Custom breadcrumbs in the specified time window. + * If the number of elements exceeds the limit, this will return the newest (latest) ones. + * + * @param start the start time + * @param end the end time + * @return the list of Breadcrumbs + */ + fun getCustomBreadcrumbsForSession(start: Long, end: Long): List + + /** + * Gets the WebView breadcrumbs in the specified time window. + * If the number of elements exceeds the limit, this will return the newest (latest) ones. + * + * @param start the start time + * @param end the end time + * @return the list of Breadcrumbs + */ + fun getWebViewBreadcrumbsForSession(start: Long, end: Long): List + + /** + * Gets the WebView breadcrumbs in the specified time window. + * If the number of elements exceeds the limit, this will return the newest (latest) ones. + * + * @param startTime the start time + * @param endTime the end time + * @return the list of Breadcrumbs + */ + fun getFragmentBreadcrumbsForSession(startTime: Long, endTime: Long): List + + /** + * Gets the RN Actions breadcrumbs in the specified time window. + * If the number of elements exceeds the limit, this will return the newest (latest) ones. + * + * @param startTime the start time + * @param endTime the end time + * @return the list of Breadcrumbs + */ + fun getRnActionBreadcrumbForSession(startTime: Long, endTime: Long): List + + /** + * Gets the captured Push Notifications breadcrumbs in the specified time window. + * If the number of elements exceeds the limit, this will return the newest (latest) ones. + * + * @param startTime the start time + * @param endTime the end time + * @return the list of Breadcrumbs + */ + fun getPushNotificationsBreadcrumbsForSession( + startTime: Long, + endTime: Long + ): List + + /** + * Gets all breadcrumbs within the specified time window. If the number of elements exceeds the + * limit for each breadcrumb type, only the latest will be returned. + * + * @param start the start time + * @param end the end time + * @return the breadcrumbs + */ + fun getBreadcrumbs(start: Long, end: Long): Breadcrumbs + + /** + * Gets all breacrumbs and clear the lists + * + * @return the breadcrumbs + */ + fun flushBreadcrumbs(): Breadcrumbs + + /** + * Registers a view breadcrumb. + * The view breadcrumb will not be registered if the last view breadcrumb registry has the same + * screen name. + * + * @param screen name of the screen. + * @param timestamp time of occurrence of the tap event. + */ + fun logView(screen: String?, timestamp: Long) + + /** + * Unlike [EmbraceBreadcrumbService.logView] + * this function logs the view despite the previous one in the queue has the same screen name. + * + * @param screen name of the screen. + * @param timestamp time of occurrence of the tap event. + */ + fun forceLogView(screen: String?, timestamp: Long) + + /** + * this function replaces the first session view in order to have it the + * scope time of the session. + * + * @param timestamp time to set as beginning of the view. + */ + fun replaceFirstSessionView(screen: String?, timestamp: Long) + + /** + * Logs the start of a view. Must be matched by a call to + * [EmbraceBreadcrumbService.endView]. + * + * @param name name of the view. + */ + fun startView(name: String?): Boolean + + /** + * Logs the end of a view. A call to + * [EmbraceBreadcrumbService.startView] must have been made before this + * call is made. + * + * @param name name of the view. + */ + fun endView(name: String?): Boolean + + /** + * Registers a tap event as a breadcrumb. + * + * @param point coordinates of the tapped element. + * @param element tapped element view. + * @param timestamp time of occurrence of the tap event. + * @param type type of tap event + */ + fun logTap( + point: Pair, + element: String, + timestamp: Long, + type: TapBreadcrumbType + ) + + /** + * Registers a custom event as a breadcrumb. + * + * @param message message for the custom breadcrumb. + */ + fun logCustom(message: String, timestamp: Long) + + /** + * Registers a RN Action as a breadcrumb. + * + * @param name The Action name. + * @param startTime The Action start time. + * @param endTime The Action end time. + * @param properties Extra properties that are not covered in the others. + * @param output The supported values are SUCCESS, FAIL and INCOMPLETE + */ + fun logRnAction( + name: String, + startTime: Long, + endTime: Long, + properties: Map, + bytesSent: Int, + output: String + ) + + /** + * Registers a WebView breadcrumb. + * + * @param url the URL navigated to. + * @param startTime the start time of the web view + */ + fun logWebView(url: String?, startTime: Long) + + /** + * This function can be used to recover the last view breadcrumb registered. + * + * @return the last registered view breadcrumb in the queue. + */ + fun getLastViewBreadcrumbScreenName(): String? + + /** + * Saves captured push notification information into session payload + * + * @param title the title of the notification as a string (or null) + * @param body the body of the notification as a string (or null) + * @param topic the notification topic (if a user subscribed to one), or null + * @param id A unique ID identifying the message + * @param notificationPriority the notificationPriority of the message (as resolved on the device) + * @param messageDeliveredPriority the delivered priority of the message (as resolved on the server) + * @param type the notification type + */ + fun logPushNotification( + title: String?, + body: String?, + topic: String?, + id: String?, + notificationPriority: Int?, + messageDeliveredPriority: Int, + type: NotificationType + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizer.kt new file mode 100644 index 0000000000..09341618e1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizer.kt @@ -0,0 +1,98 @@ +package io.embrace.android.embracesdk.capture.crumbs + +import io.embrace.android.embracesdk.gating.Sanitizable +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_CUSTOM +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_CUSTOM_VIEWS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_TAPS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_VIEWS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_WEB_VIEWS +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.Breadcrumbs + +internal class BreadcrumbsSanitizer( + private val breadcrumbs: Breadcrumbs?, + private val enabledComponents: Set +) : + Sanitizable { + + override fun sanitize(): Breadcrumbs? { + InternalStaticEmbraceLogger.logger.logDeveloper( + "BreadcrumbsSanitizer", + "sanitize: " + (breadcrumbs != null).toString() + ) + return breadcrumbs?.let { + + val customBreadcrumbs = if (shouldAddCustomBreadcrumbs()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "BreadcrumbsSanitizer", + "shouldAddCustomBreadcrumbs" + ) + breadcrumbs.customBreadcrumbs + } else { + null + } + + val viewBreadcrumbs = if (shouldAddViewBreadcrumbs()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "BreadcrumbsSanitizer", + "shouldAddViewBreadcrumbs" + ) + breadcrumbs.viewBreadcrumbs + } else { + null + } + + val fragmentBreadcrumbs = if (shouldAddCustomViewBreadcrumbs()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "BreadcrumbsSanitizer", + "shouldAddCustomViewBreadcrumbs" + ) + breadcrumbs.fragmentBreadcrumbs + } else { + null + } + + val tapBreadcrumbs = if (shouldAddTapBreadcrumbs()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "BreadcrumbsSanitizer", + "shouldAddTapBreadcrumbs" + ) + breadcrumbs.tapBreadcrumbs + } else { + null + } + + val webViewBreadcrumbs = if (shouldAddWebViewBreadcrumbs()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "BreadcrumbsSanitizer", + "shouldAddWebViewBreadcrumbs" + ) + breadcrumbs.webViewBreadcrumbs + } else { + null + } + return Breadcrumbs( + customBreadcrumbs = customBreadcrumbs, + viewBreadcrumbs = viewBreadcrumbs, + fragmentBreadcrumbs = fragmentBreadcrumbs, + tapBreadcrumbs = tapBreadcrumbs, + webViewBreadcrumbs = webViewBreadcrumbs + ) + } + } + + private fun shouldAddTapBreadcrumbs() = + enabledComponents.contains(BREADCRUMBS_TAPS) + + private fun shouldAddViewBreadcrumbs() = + enabledComponents.contains(BREADCRUMBS_VIEWS) + + private fun shouldAddCustomViewBreadcrumbs() = + enabledComponents.contains(BREADCRUMBS_CUSTOM_VIEWS) + + private fun shouldAddWebViewBreadcrumbs() = + enabledComponents.contains(BREADCRUMBS_WEB_VIEWS) + + private fun shouldAddCustomBreadcrumbs() = + enabledComponents.contains(BREADCRUMBS_CUSTOM) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService.kt new file mode 100644 index 0000000000..5159367ed3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbService.kt @@ -0,0 +1,562 @@ +package io.embrace.android.embracesdk.capture.crumbs + +import android.app.Activity +import android.text.TextUtils +import android.util.Pair +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.internal.CacheableValue +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.CustomBreadcrumb +import io.embrace.android.embracesdk.payload.FragmentBreadcrumb +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb.NotificationType +import io.embrace.android.embracesdk.payload.RnActionBreadcrumb +import io.embrace.android.embracesdk.payload.TapBreadcrumb +import io.embrace.android.embracesdk.payload.TapBreadcrumb.TapBreadcrumbType +import io.embrace.android.embracesdk.payload.ViewBreadcrumb +import io.embrace.android.embracesdk.payload.WebViewBreadcrumb +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import io.embrace.android.embracesdk.utils.filter +import java.util.Collections +import java.util.Deque +import java.util.concurrent.LinkedBlockingDeque + +/** + * Handles the logging of breadcrumbs. + * + * Breadcrumbs record a user's journey through the app and are split into: + * + * * View breadcrumbs: Each time the user changes view in the app + * * Tap breadcrumbs: Each time the user taps a UI element in the app + * * Custom breadcrumbs: User-defined interactions within the app + * + * Breadcrumbs are limited at query-time by default to 100 per session, but this can be overridden + * in server-side configuration. They are stored in an unbounded queue. + */ +internal class EmbraceBreadcrumbService( + clock: Clock, + configService: ConfigService, + logger: InternalEmbraceLogger +) : BreadcrumbService, ActivityListener, MemoryCleanerListener { + + /** + * Clock used by the service + */ + private val clock: Clock + + /** + * The config service, for retrieving the breadcrumb limit. + */ + private val configService: ConfigService + + /** + * A deque of breadcrumbs. + */ + private val viewBreadcrumbs = LinkedBlockingDeque() + private val tapBreadcrumbs = LinkedBlockingDeque() + + @VisibleForTesting + val customBreadcrumbs = LinkedBlockingDeque() + private val rnActionBreadcrumbs = LinkedBlockingDeque() + val webViewBreadcrumbs = LinkedBlockingDeque() + val fragmentBreadcrumbs = LinkedBlockingDeque() + val fragmentStack = Collections.synchronizedList(ArrayList()) + val pushNotifications = LinkedBlockingDeque() + private val viewBreadcrumbsCache: CacheableValue> + private val tapBreadcrumbsCache: CacheableValue> + private val customBreadcrumbsCache: CacheableValue> + private val rnActionsCache: CacheableValue> + private val webviewCache: CacheableValue> + private val fragmentsCache: CacheableValue> + private val pushNotificationsCache: CacheableValue> + private val logger: InternalEmbraceLogger + + init { + viewBreadcrumbsCache = CacheableValue { isCacheValid(viewBreadcrumbs) } + tapBreadcrumbsCache = CacheableValue { isCacheValid(tapBreadcrumbs) } + customBreadcrumbsCache = CacheableValue { isCacheValid(customBreadcrumbs) } + rnActionsCache = CacheableValue { isCacheValid(rnActionBreadcrumbs) } + webviewCache = CacheableValue { isCacheValid(webViewBreadcrumbs) } + fragmentsCache = CacheableValue { isCacheValid(fragmentBreadcrumbs) } + pushNotificationsCache = CacheableValue { isCacheValid(pushNotifications) } + this.clock = clock + this.configService = configService + this.logger = logger + } + + override fun logView(screen: String?, timestamp: Long) { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + logger.logDeveloper("EmbraceBreadcrumbsService", "logView") + addToViewLogsQueue(screen, timestamp, false) + } + + override fun forceLogView(screen: String?, timestamp: Long) { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + logger.logDeveloper("EmbraceBreadcrumbsService", "forceLogView") + addToViewLogsQueue(screen, timestamp, true) + } + + @Synchronized + override fun replaceFirstSessionView(screen: String?, timestamp: Long) { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + logger.logDeveloper("EmbraceBreadcrumbsService", "replaceFirstSessionView") + viewBreadcrumbs.removeLast() + val limit = configService.breadcrumbBehavior.getViewBreadcrumbLimit() + tryAddBreadcrumb(viewBreadcrumbs, ViewBreadcrumb(screen, timestamp), limit) + } + + override fun startView(name: String?): Boolean { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED || name == null) { + return false + } + logger.logDeveloper("EmbraceBreadcrumbsService", "Starting view: $name") + synchronized(this) { + if (fragmentStack.size >= DEFAULT_VIEW_STACK_SIZE) { + val msg = + "Cannot add view, view stack exceed the limit of " + DEFAULT_VIEW_STACK_SIZE + logger.logDeveloper("EmbraceBreadcrumbsService", msg) + return false + } + logger.logDeveloper("EmbraceBreadcrumbsService", "View added: $name") + return fragmentStack.add(FragmentBreadcrumb(name, clock.now(), 0)) + } + } + + override fun endView(name: String?): Boolean { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED || name == null) { + return false + } + logger.logDeveloper("EmbraceBreadcrumbsService", "Ending view: $name") + var start: FragmentBreadcrumb + val end = FragmentBreadcrumb(name, 0, clock.now()) + synchronized(this) { + val crumbs = filter(fragmentStack) { crumb: FragmentBreadcrumb -> crumb.name == name } + if (crumbs.isEmpty()) { + logger.logDeveloper("EmbraceBreadcrumbsService", "Cannot end view") + return false + } + start = crumbs[0] + fragmentStack.remove(start) + } + end.setStartTime(start.getStartTime()) + logger.logDeveloper("EmbraceBreadcrumbsService", "View ended") + val limit = configService.breadcrumbBehavior.getFragmentBreadcrumbLimit() + tryAddBreadcrumb(fragmentBreadcrumbs, end, limit) + return true + } + + override fun logTap( + point: Pair, + element: String, + timestamp: Long, + type: TapBreadcrumbType + ) { + var point = point + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + logger.logDeveloper("EmbraceBreadcrumbsService", "log tap") + try { + if (!configService.breadcrumbBehavior.isTapCoordinateCaptureEnabled()) { + point = Pair(0.0f, 0.0f) + } else { + logger.logDeveloper("EmbraceBreadcrumbsService", "Cannot capture tap coordinates") + } + val limit = configService.breadcrumbBehavior.getTapBreadcrumbLimit() + tryAddBreadcrumb(tapBreadcrumbs, TapBreadcrumb(point, element, timestamp, type), limit) + } catch (ex: Exception) { + logger.logError("Failed to log tap breadcrumb for element $element", ex) + } + } + + override fun logCustom(message: String, timestamp: Long) { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + logger.logDeveloper("EmbraceBreadcrumbsService", "log custom breadcrumb") + if (TextUtils.isEmpty(message)) { + logger.logWarning("Breadcrumb message must not be blank") + return + } + try { + val limit = configService.breadcrumbBehavior.getCustomBreadcrumbLimit() + tryAddBreadcrumb(customBreadcrumbs, CustomBreadcrumb(message, timestamp), limit) + } catch (ex: Exception) { + logger.logError("Failed to log custom breadcrumb with message $message", ex) + } + } + + override fun logRnAction( + name: String, + startTime: Long, + endTime: Long, + properties: Map, + bytesSent: Int, + output: String + ) { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + if (!RnActionBreadcrumb.validateRnBreadcrumbOutputName(output)) { + logger.logWarning( + "RN Action output is invalid, the valid values are ${RnActionBreadcrumb.getValidRnBreadcrumbOutputName()}" + ) + return + } + if (TextUtils.isEmpty(name)) { + logger.logWarning("RN Action name must not be blank") + return + } + try { + val limit = configService.breadcrumbBehavior.getCustomBreadcrumbLimit() + tryAddBreadcrumb( + rnActionBreadcrumbs, + RnActionBreadcrumb(name, startTime, endTime, properties, bytesSent, output), limit + ) + } catch (ex: Exception) { + logger.logDebug("Failed to log RN Action breadcrumb with name $name", ex) + } + } + + override fun logWebView(url: String?, startTime: Long) { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + if (!configService.breadcrumbBehavior.isWebViewBreadcrumbCaptureEnabled()) { + logger.logDeveloper("EmbraceBreadcrumbsService", "Web capture not enabled") + return + } + if (url == null) { + logger.logDeveloper("EmbraceBreadcrumbsService", "Web url is NULL") + return + } + try { + // Check if web view query params should be captured. + var parsedUrl: String = url + if (!configService.breadcrumbBehavior.isQueryParamCaptureEnabled()) { + val queryOffset = url.indexOf(QUERY_PARAMETER_DELIMITER) + if (queryOffset > 0) { + parsedUrl = url.substring(0, queryOffset) + logger.logDeveloper("EmbraceBreadcrumbsService", "Parsed url is: $parsedUrl") + } else { + logger.logDeveloper("EmbraceBreadcrumbsService", "no query parameters") + } + } else { + logger.logDeveloper( + "EmbraceBreadcrumbsService", + "query parameters capture not enabled" + ) + } + val limit = configService.breadcrumbBehavior.getWebViewBreadcrumbLimit() + tryAddBreadcrumb(webViewBreadcrumbs, WebViewBreadcrumb(parsedUrl, startTime), limit) + } catch (ex: Exception) { + logger.logError("Failed to log WebView breadcrumb for url $url") + } + } + + override fun getViewBreadcrumbsForSession( + start: Long, + end: Long + ): List { + return viewBreadcrumbsCache.value { + filterBreadcrumbsForTimeWindow( + viewBreadcrumbs, + start, + end + ) + } + } + + override fun getTapBreadcrumbsForSession(start: Long, end: Long): List { + return tapBreadcrumbsCache.value { + filterBreadcrumbsForTimeWindow( + tapBreadcrumbs, + start, + end + ) + } + } + + override fun getCustomBreadcrumbsForSession( + start: Long, + end: Long + ): List { + return customBreadcrumbsCache.value { + filterBreadcrumbsForTimeWindow( + customBreadcrumbs, + start, + end + ) + } + } + + override fun getRnActionBreadcrumbForSession( + startTime: Long, + endTime: Long + ): List { + return rnActionsCache.value { + filterBreadcrumbsForTimeWindow( + rnActionBreadcrumbs, + startTime, + endTime + ) + } + } + + override fun getWebViewBreadcrumbsForSession( + start: Long, + end: Long + ): List { + return webviewCache.value { + filterBreadcrumbsForTimeWindow( + webViewBreadcrumbs, + start, + end + ) + } + } + + override fun getFragmentBreadcrumbsForSession( + startTime: Long, + endTime: Long + ): List { + return fragmentsCache.value { + filterBreadcrumbsForTimeWindow( + fragmentBreadcrumbs, + startTime, + endTime + ) + } + } + + override fun getPushNotificationsBreadcrumbsForSession( + startTime: Long, + endTime: Long + ): List { + return pushNotificationsCache.value { + filterBreadcrumbsForTimeWindow( + pushNotifications, + startTime, + endTime + ) + } + } + + override fun getBreadcrumbs(start: Long, end: Long): Breadcrumbs { + return Breadcrumbs( + customBreadcrumbs = getCustomBreadcrumbsForSession(start, end).filterNotNull(), + tapBreadcrumbs = getTapBreadcrumbsForSession(start, end).filterNotNull(), + viewBreadcrumbs = getViewBreadcrumbsForSession(start, end).filterNotNull(), + webViewBreadcrumbs = getWebViewBreadcrumbsForSession(start, end).filterNotNull(), + fragmentBreadcrumbs = getFragmentBreadcrumbsForSession(start, end).filterNotNull(), + rnActionBreadcrumbs = getRnActionBreadcrumbForSession(start, end).filterNotNull(), + pushNotifications = getPushNotificationsBreadcrumbsForSession(start, end).filterNotNull() + ) + } + + override fun flushBreadcrumbs(): Breadcrumbs { + // given that start and end are ignored because of the cache, we can just pass 0 + val breadcrumbs = getBreadcrumbs(0, clock.now()) + cleanCollections() + return breadcrumbs + } + + private fun isCacheValid(deque: Deque): Int { + val last = deque.peekLast() + val code = last?.hashCode() ?: 0 + return deque.size + code + } + + override fun getLastViewBreadcrumbScreenName(): String? { + if (viewBreadcrumbs.isEmpty()) { + logger.logDeveloper("EmbraceBreadcrumbsService", "View breadcrumb stack is empty") + } else { + val crumb = viewBreadcrumbs.peek() + if (crumb != null) { + val lastViewBreadcrumb = crumb.screen + logger.logDeveloper( + "EmbraceBreadcrumbsService", + "Last view breadcrumb is: $lastViewBreadcrumb" + ) + return lastViewBreadcrumb + } + } + return null + } + + override fun logPushNotification( + title: String?, + body: String?, + topic: String?, + id: String?, + notificationPriority: Int?, + messageDeliveredPriority: Int, + type: NotificationType + ) { + if (ApkToolsConfig.IS_BREADCRUMB_TRACKING_DISABLED) { + return + } + try { + val captureFcmPiiData = configService.breadcrumbBehavior.isCaptureFcmPiiDataEnabled() + val pn = PushNotificationBreadcrumb( + if (captureFcmPiiData) title else null, + if (captureFcmPiiData) body else null, + if (captureFcmPiiData) topic else null, + id, + notificationPriority, + type.type, + clock.now() + ) + val limit = configService.breadcrumbBehavior.getCustomBreadcrumbLimit() + tryAddBreadcrumb(pushNotifications, pn, limit) + } catch (ex: Exception) { + logger.logError("Failed to capture push notification", ex) + } + } + + override fun onView(activity: Activity) { + if (configService.breadcrumbBehavior.isActivityBreadcrumbCaptureEnabled()) { + logView(activity.javaClass.name, clock.now()) + } + } + + /** + * Close all open fragments when the activity closes + */ + override fun onViewClose(activity: Activity) { + if (!configService.breadcrumbBehavior.isActivityBreadcrumbCaptureEnabled()) { + return + } + try { + val lastViewBreadcrumb = viewBreadcrumbs.peek() + if (lastViewBreadcrumb != null) { + lastViewBreadcrumb.end = clock.now() + logger.logDeveloper( + "EmbraceBreadcrumbsService", + "End set for breadcrumb $lastViewBreadcrumb" + ) + } else { + logger.logDeveloper("EmbraceBreadcrumbsService", "There are no breadcrumbs to end") + } + } catch (ex: Exception) { + logger.logDebug("Failed to add set end time for breadcrumb", ex) + } + if (fragmentStack.size == 0) { + logger.logDeveloper( + "EmbraceBreadcrumbsService", + "There are no breadcrumbs fragments to clear" + ) + return + } + val ts = clock.now() + synchronized(fragmentStack) { + logger.logDeveloper("EmbraceBreadcrumbsService", "Ending breadcrumb fragments") + for (fragment in fragmentStack) { + fragment.endTime = ts + val limit = configService.breadcrumbBehavior.getFragmentBreadcrumbLimit() + tryAddBreadcrumb(fragmentBreadcrumbs, fragment, limit) + } + fragmentStack.clear() + } + } + + override fun cleanCollections() { + viewBreadcrumbs.clear() + tapBreadcrumbs.clear() + customBreadcrumbs.clear() + webViewBreadcrumbs.clear() + fragmentBreadcrumbs.clear() + fragmentStack.clear() + pushNotifications.clear() + rnActionBreadcrumbs.clear() + logger.logDeveloper("EmbraceBreadcrumbsService", "Collections cleaned") + } + + /** + * Adds the view breadcrumb to the queue. + * + * @param screen name of the screen. + * @param timestamp time of occurrence of the tap event. + * @param force will run no duplication checks on the previous view breadcrumb registry. + */ + @Synchronized + private fun addToViewLogsQueue(screen: String?, timestamp: Long, force: Boolean) { + try { + val lastViewBreadcrumb = viewBreadcrumbs.peek() + var lastScreen = if (lastViewBreadcrumb != null) lastViewBreadcrumb.screen else "" + if (lastScreen == null) { + lastScreen = "" + } + if (force || lastViewBreadcrumb == null || !lastScreen.equals( + screen.toString(), + ignoreCase = true + ) + ) { + // TODO: is `lastViewBreadcrumb` a copy or the actual object in the queue? + if (lastViewBreadcrumb != null) { + logger.logDeveloper( + "EmbraceBreadcrumbsService", + "Ending lastViewBreadcrumb to add another" + ) + lastViewBreadcrumb.end = timestamp + } + val limit = configService.breadcrumbBehavior.getViewBreadcrumbLimit() + tryAddBreadcrumb(viewBreadcrumbs, ViewBreadcrumb(screen, timestamp), limit) + } + } catch (ex: Exception) { + logger.logError("Failed to add view breadcrumb for $screen", ex) + } + } + + /** + * Returns the latest breadcrumbs within the specified interval, up to the maximum queue size or + * configured limit in the app configuration. + * + * @param breadcrumbs breadcrumbs list to filter. + * @param startTime beginning of the time window. + * @param endTime end of the time window. + * @return filtered breadcrumbs from the provided FixedSizeDeque. + */ + private fun filterBreadcrumbsForTimeWindow( + breadcrumbs: Deque, + startTime: Long, + endTime: Long + ): List { + logger.logDeveloper("EmbraceBreadcrumbsService", "Filtering breadcrumbs for time window") + return filter(breadcrumbs) { crumb: T -> crumb!!.getStartTime() >= startTime && (endTime <= 0L || crumb.getStartTime() <= endTime) } + } + + private fun tryAddBreadcrumb( + breadcrumbs: LinkedBlockingDeque, + breadcrumb: T, + limit: Int + ) { + if (!breadcrumbs.isEmpty() && breadcrumbs.size >= limit) { + breadcrumbs.removeLast() + logger.logDeveloper("EmbraceBreadcrumbsService", "removed last breadcrumb from stack") + } + breadcrumbs.push(breadcrumb) + logger.logDeveloper("EmbraceBreadcrumbsService", "added breadcrumb") + } + + companion object { + private const val QUERY_PARAMETER_DELIMITER = "?" + + /** + * The default limit for how many open tracked fragments are allowed, which can be overridden + * by [RemoteConfig]. + */ + private const val DEFAULT_VIEW_STACK_SIZE = 20 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService.kt new file mode 100644 index 0000000000..874fa18ce8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureService.kt @@ -0,0 +1,149 @@ +package io.embrace.android.embracesdk.capture.crumbs + +import android.app.Activity +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb.NotificationType +import io.embrace.android.embracesdk.session.ActivityListener + +/** + * In charge of handling all notifications related functionality. + */ +internal class PushNotificationCaptureService( + private val breadCrumbService: BreadcrumbService, + private val logger: InternalEmbraceLogger +) : ActivityListener { + + @VisibleForTesting + companion object Utils { + + enum class PRIORITY(val priority: Int) { + PRIORITY_UNKNOWN(0), + PRIORITY_HIGH(1), + PRIORITY_NORMAL(2) + } + + private const val RESERVED_PREFIX_COM_GOOGLE_FIREBASE = "com.google.firebase" + private const val RESERVED_PREFIX_PAYLOAD_KEYS = "google." + private const val RESERVED_PREFIX_NOTIFICATION_KEYS = "gcm." + private const val RESERVED_FROM = "from" + private const val RESERVED_MESSAGE_TYPE = "message_type" + private const val RESERVED_COLLAPSE_KEY = "collapse_key" + private const val RESERVED_GOOGLE_MESSAGE_ID = "google.message_id" + private const val RESERVED_GOOGLE_DELIVERED_PRIORITY = "google.delivered_priority" + + /** + * This is so to have compatibility with com.google.firebase.messaging.RemoteMessage. + * For some reason they don't use String, but convert it to int instead. It is either doing + * this, or adding com.google.firebase:firebase-messaging as a dependency on the sdk and + * add some more complex code. + */ + @VisibleForTesting + fun getMessagePriority(priority: String?) = + when (priority) { + "high" -> PRIORITY.PRIORITY_HIGH.priority + "normal" -> PRIORITY.PRIORITY_NORMAL.priority + else -> PRIORITY.PRIORITY_UNKNOWN.priority + } + + @VisibleForTesting + fun extractDeveloperDefinedPayload(bundle: Bundle): Map { + val keySet = bundle.keySet() ?: return emptyMap() + return keySet.filter { + // let's filter all google reserved words, leaving us with user defined keys + !it.startsWith(RESERVED_PREFIX_PAYLOAD_KEYS) && + !it.startsWith(RESERVED_PREFIX_NOTIFICATION_KEYS) && + !it.startsWith(RESERVED_PREFIX_COM_GOOGLE_FIREBASE) && + it != RESERVED_FROM && + it != RESERVED_MESSAGE_TYPE && + it != RESERVED_COLLAPSE_KEY + }.associateWith { bundle.getString(it) ?: "" } + } + } + + /** + * Saves captured push notification information into session payload + * + * @param title the title of the notification as a string (or null) + * @param body the body of the notification as a string (or null) + * @param topic the notification topic (if a user subscribed to one), or null + * @param id A unique ID identifying the message + * @param notificationPriority the priority of the message (as resolved on the device) + * @param messageDeliveredPriority the priority of the message (as resolved on the server) + * @param type the notification type + */ + fun logPushNotification( + title: String?, + body: String?, + topic: String?, + id: String?, + notificationPriority: Int?, + messageDeliveredPriority: Int, + type: NotificationType + ) { + breadCrumbService.logPushNotification( + title, + body, + topic, + id, + notificationPriority, + messageDeliveredPriority, + type + ) + } + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + if (isComingFromPushNotification(activity)) { + logger.logInfo("Coming from a Firebase push notification") + with(activity.intent.extras) { + if (this == null) { + logger.logWarning( + "It seems like we are coming from a Google Push " + + "Notification, but intent extras is null. Will not be able to log it " + + "to our dashboard." + ) + return + } + + logPushNotification( + // ** all these fields do not come as part of the Intent ** // + title = null, + body = null, + notificationPriority = null, + // ** // + topic = getString(RESERVED_FROM), + id = getString(RESERVED_GOOGLE_MESSAGE_ID), + messageDeliveredPriority = getMessagePriority( + getString(RESERVED_GOOGLE_DELIVERED_PRIORITY) + ), + type = determineNotificationType(this) + ) + } + } + } + + private fun determineNotificationType(bundle: Bundle): NotificationType { + val hasData = extractDeveloperDefinedPayload(bundle).isNotEmpty() + + // whenever we come through this flow of push notification, we know certainly that it has + // notification block. This is because if the push notification is data only + // (w/o notification block), then it wouldn't come through this flow, but through + // FirebaseSwazzledHooks._onMessageReceived instead. Now it is a matter of determining if + // it's a notification only or notification + data. + return if (hasData) { + NotificationType.NOTIFICATION_AND_DATA + } else { + NotificationType.NOTIFICATION + } + } + + /** + * It determines if this Activity is coming from a Google push notification. + */ + private fun isComingFromPushNotification(activity: Activity): Boolean { + return activity.intent?.extras?.keySet()?.containsAll( + listOf(RESERVED_FROM, RESERVED_GOOGLE_MESSAGE_ID) + ) ?: false + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/ActivityLifecycleBreadcrumbService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/ActivityLifecycleBreadcrumbService.kt new file mode 100644 index 0000000000..a06d015bdf --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/ActivityLifecycleBreadcrumbService.kt @@ -0,0 +1,7 @@ +package io.embrace.android.embracesdk.capture.crumbs.activity + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.ActivityLifecycleData + +internal interface ActivityLifecycleBreadcrumbService : + DataCaptureService?> diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/EmbraceActivityLifecycleBreadcrumbService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/EmbraceActivityLifecycleBreadcrumbService.kt new file mode 100644 index 0000000000..a1bb2427ba --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/crumbs/activity/EmbraceActivityLifecycleBreadcrumbService.kt @@ -0,0 +1,146 @@ +package io.embrace.android.embracesdk.capture.crumbs.activity + +import android.app.Activity +import android.app.Application +import android.os.Build +import android.os.Bundle +import androidx.annotation.RequiresApi +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.payload.ActivityLifecycleBreadcrumb +import io.embrace.android.embracesdk.payload.ActivityLifecycleData +import io.embrace.android.embracesdk.payload.ActivityLifecycleState +import java.util.Queue +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +// TODO future: decide on a configurable limit? +private const val LIMIT = 80 + +/** + * Captures activity lifecycle breadcrumbs whenever the system alters the lifecycle of any + * Activity in the app. + * + * Breadcrumbs are captured separately for each activity and the duration of each activity + * lifecycle is also collected. This allows in principle for aggregate metrics on + * abnormally long lifecycle to be detected and shown to the user. + */ +@RequiresApi(Build.VERSION_CODES.Q) +internal class EmbraceActivityLifecycleBreadcrumbService( + private val configService: ConfigService, + private val clock: Clock +) : Application.ActivityLifecycleCallbacks, ActivityLifecycleBreadcrumbService { + + // store breadcrumbs in a map with the activity hash code as the key + private val crumbs = ConcurrentHashMap>() + + // onCreate() + override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) = + createBreadcrumb(activity, ActivityLifecycleState.ON_CREATE, savedInstanceState != null) + + override fun onActivityPostCreated(activity: Activity, savedInstanceState: Bundle?) = + endBreadcrumb(activity) + + // onStart() + override fun onActivityPreStarted(activity: Activity) = createBreadcrumb( + activity, + ActivityLifecycleState.ON_START + ) + + override fun onActivityPostStarted(activity: Activity) = endBreadcrumb(activity) + + // onResume() + override fun onActivityPreResumed(activity: Activity) = createBreadcrumb( + activity, + ActivityLifecycleState.ON_RESUME + ) + + override fun onActivityPostResumed(activity: Activity) = endBreadcrumb(activity) + + // onPause() + override fun onActivityPrePaused(activity: Activity) = createBreadcrumb( + activity, + ActivityLifecycleState.ON_PAUSE + ) + + override fun onActivityPostPaused(activity: Activity) = endBreadcrumb(activity) + + // onStop() + override fun onActivityPreStopped(activity: Activity) = createBreadcrumb( + activity, + ActivityLifecycleState.ON_STOP + ) + + override fun onActivityPostStopped(activity: Activity) = endBreadcrumb(activity) + + // onDestroy() + override fun onActivityPreDestroyed(activity: Activity) = createBreadcrumb( + activity, + ActivityLifecycleState.ON_DESTROY + ) + + override fun onActivityPostDestroyed(activity: Activity) = endBreadcrumb(activity) + + // onSaveInstanceState() + override fun onActivityPreSaveInstanceState(activity: Activity, outState: Bundle) = + createBreadcrumb(activity, ActivityLifecycleState.ON_SAVE_INSTANCE_STATE) + + override fun onActivityPostSaveInstanceState(activity: Activity, outState: Bundle) = + endBreadcrumb(activity) + + // no-ops + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + + /** + * Creates a breadcrumb for the upcoming state change in the Activity lifecycle. + */ + private fun createBreadcrumb( + activity: Activity, + state: ActivityLifecycleState, + bundlePresent: Boolean? = false + ) { + val name = activity.javaClass.simpleName + val queue = crumbs.getOrPut(name) { ConcurrentLinkedQueue() } + val crumb = ActivityLifecycleBreadcrumb( + name, + state, + clock.now(), + bundlePresent + ) + queue.add(crumb) + + while (queue.size > LIMIT) { + queue.poll() + } + } + + private fun endBreadcrumb(activity: Activity) { + val name = activity.javaClass.simpleName + val queue = crumbs[name] + val crumb = queue?.lastOrNull() ?: return + crumb.end = clock.now() + } + + override fun cleanCollections() { + crumbs.clear() + } + + override fun getCapturedData(): List = when { + configService.sdkModeBehavior.isBetaFeaturesEnabled() -> transformToSessionData(crumbs.values) + else -> emptyList() + } + + private fun transformToSessionData(data: Collection>) = data + .filter { it.isNotEmpty() } + .map { entry -> + val copy = entry.toList() + val name = copy.firstOrNull()?.activity + ActivityLifecycleData(name, copy) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/EmbraceMemoryService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/EmbraceMemoryService.kt new file mode 100644 index 0000000000..0e35d13471 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/EmbraceMemoryService.kt @@ -0,0 +1,47 @@ +package io.embrace.android.embracesdk.capture.memory + +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.MemoryWarning +import java.util.NavigableMap +import java.util.concurrent.ConcurrentSkipListMap + +/** + * Polls for the device's available and used memory. + * + * Stores memory warnings when the [ActivityService] detects a memory trim event. + */ +internal class EmbraceMemoryService( + private val clock: Clock +) : MemoryService { + + private val memoryTimestamps = LongArray(MAX_CAPTURED_MEMORY_WARNINGS) + private var offset = 0 + + override fun onMemoryWarning() { + InternalStaticEmbraceLogger.logDeveloper( + "EmbraceMemoryService", + "Memory warning number: $offset" + ) + if (offset < MAX_CAPTURED_MEMORY_WARNINGS) { + memoryTimestamps[offset] = clock.now() + offset++ + } + } + + override fun getCapturedData(): List { + val memoryWarnings: NavigableMap = ConcurrentSkipListMap() + for (i in 0 until offset) { + memoryWarnings[memoryTimestamps[i]] = MemoryWarning(memoryTimestamps[i]) + } + return ArrayList(memoryWarnings.subMap(0, Long.MAX_VALUE).values) + } + + override fun cleanCollections() { + offset = 0 + } + + companion object { + private const val MAX_CAPTURED_MEMORY_WARNINGS = 100 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/MemoryService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/MemoryService.kt new file mode 100644 index 0000000000..f4d91ac196 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/MemoryService.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk.capture.memory + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.MemoryWarning + +/** + * Provides access to information about the state of the device's memory usage. + */ +internal interface MemoryService : DataCaptureService?> { + + /** + * Called when the memory is 'trimmed' by Android and records a + * [low memory warning](https://developer.android.com/reference/android/content/ComponentCallbacks2). + * + * Android trims memory when it is running low. + */ + fun onMemoryWarning() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/NoOpMemoryService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/NoOpMemoryService.kt new file mode 100644 index 0000000000..e6459fe9dd --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/memory/NoOpMemoryService.kt @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk.capture.memory + +import io.embrace.android.embracesdk.payload.MemoryWarning + +internal class NoOpMemoryService : MemoryService { + + override fun onMemoryWarning() { + } + + override fun cleanCollections() { + } + + override fun getCapturedData(): List { + return emptyList() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService.kt new file mode 100644 index 0000000000..9f2f5b5129 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataService.kt @@ -0,0 +1,767 @@ +package io.embrace.android.embracesdk.capture.metadata + +import android.app.ActivityManager +import android.app.usage.StorageStatsManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Environment +import android.os.StatFs +import android.view.WindowManager +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.Embrace.AppFramework +import io.embrace.android.embracesdk.capture.cpu.CpuInfoDelegate +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.BuildInfo +import io.embrace.android.embracesdk.internal.DeviceArchitecture +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logError +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.DiskUsage +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.utils.eagerLazyLoad +import java.io.ByteArrayOutputStream +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.InputStream +import java.security.MessageDigest +import java.util.Locale +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService + +/** + * Provides information about the state of the device, retrieved from Android system services, + * which is used as metadata with telemetry submitted to the Embrace API. + */ +internal class EmbraceMetadataService private constructor( + private val windowManager: WindowManager?, + private val packageManager: PackageManager, + private val storageStatsManager: StorageStatsManager?, + private val activityManager: ActivityManager?, + private val buildInfo: BuildInfo, + private val configService: ConfigService, + private val applicationInfo: ApplicationInfo, + private val deviceId: Lazy, + private val packageName: String, + private val appVersionName: String, + private val appVersionCode: String, + private val appFramework: AppFramework, + /** + * This field is defined during instantiation as by the end of the startup + */ + private val appUpdated: Lazy, + private val osUpdated: Lazy, + private val preferencesService: PreferencesService, + private val activityService: ActivityService, + reactNativeBundleId: Lazy, + javaScriptPatchNumber: String?, + reactNativeVersion: String?, + unityVersion: String?, + buildGuid: String?, + unitySdkVersion: String?, + rnSdkVersion: String?, + private val metadataRetrieveExecutorService: ExecutorService, + private val clock: Clock, + private val embraceCpuInfoDelegate: CpuInfoDelegate, + private val deviceArchitecture: DeviceArchitecture +) : MetadataService, ActivityListener { + + private val statFs = lazy { StatFs(Environment.getDataDirectory().path) } + private val javaScriptPatchNumber: String? + private val reactNativeVersion: String? + private val unityVersion: String? + private val buildGuid: String? + private val unitySdkVersion: String? + private var reactNativeBundleId: Lazy + + @Volatile + private var sessionId: String? = null + + @Volatile + private var diskUsage: DiskUsage? = null + + @Volatile + private var screenResolution: String? = null + + @Volatile + private var cpuName: String? = null + + @Volatile + private var egl: String? = null + + @Volatile + private var isJailbroken: Boolean? = null + private var embraceFlutterSdkVersion: String? = null + private var dartVersion: String? = null + private var rnSdkVersion: String? + + init { + if (appFramework == AppFramework.REACT_NATIVE) { + logDeveloper("EmbraceMetadataService", "Setting RN settings") + this.reactNativeBundleId = reactNativeBundleId + this.javaScriptPatchNumber = javaScriptPatchNumber + this.reactNativeVersion = reactNativeVersion + this.rnSdkVersion = rnSdkVersion + } else { + this.reactNativeBundleId = lazy { buildInfo.buildId } + this.javaScriptPatchNumber = null + this.reactNativeVersion = null + this.rnSdkVersion = null + } + if (appFramework == AppFramework.UNITY) { + logDeveloper("EmbraceMetadataService", "Setting Unity settings") + this.unityVersion = unityVersion + this.buildGuid = buildGuid + this.unitySdkVersion = unitySdkVersion + } else { + this.unityVersion = null + this.buildGuid = null + this.unitySdkVersion = null + } + } + + /** + * Queues in a single thread executor callables to retrieve values in background + */ + override fun precomputeValues() { + logDeveloper( + "EmbraceMetadataService", + "Precomputing values asynchronously: Jailbroken/ScreenResolution/DiskUsage" + ) + asyncRetrieveIsJailbroken() + asyncRetrieveScreenResolution() + asyncRetrieveAdditionalDeviceInfo() + + // Always retrieve the DiskUsage last because it can take the longest to run + asyncRetrieveDiskUsage(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + } + + private fun asyncRetrieveAdditionalDeviceInfo() { + if (!configService.autoDataCaptureBehavior.isNdkEnabled()) { + logDeveloper("EmbraceMetadataService", "NDK not enabled") + return + } + if (!cpuName.isNullOrEmpty() && !egl.isNullOrEmpty()) { + logDeveloper("EmbraceMetadataService", "Additional device info already exists") + return + } + metadataRetrieveExecutorService.submit { + logDeveloper("EmbraceMetadataService", "Async retrieve cpuName & egl") + val storedCpuName = preferencesService.cpuName + val storedEgl = preferencesService.egl + if (storedCpuName != null) { + cpuName = storedCpuName + } else { + cpuName = embraceCpuInfoDelegate.getCpuName() + preferencesService.cpuName = cpuName + logDeveloper("EmbraceMetadataService", "cpu name computed and stored") + } + if (storedEgl != null) { + egl = storedEgl + } else { + egl = embraceCpuInfoDelegate.getElg() + preferencesService.egl = egl + logDeveloper("EmbraceMetadataService", "egl computed and stored") + } + } + } + + private fun asyncRetrieveScreenResolution() { + // if the screenResolution exists in memory, don't try to retrieve it + if (!screenResolution.isNullOrEmpty()) { + logDeveloper("EmbraceMetadataService", "Screen resolution already exists") + return + } + metadataRetrieveExecutorService.submit { + logDeveloper("EmbraceMetadataService", "Async retrieve screen resolution") + val storedScreenResolution = preferencesService.screenResolution + // get from shared preferences + if (storedScreenResolution != null) { + logDeveloper( + "EmbraceMetadataService", + "Screen resolution is present, loading from store" + ) + screenResolution = storedScreenResolution + } else { + screenResolution = MetadataUtils.getScreenResolution( + windowManager + ) + preferencesService.screenResolution = screenResolution + logDeveloper("EmbraceMetadataService", "Screen resolution computed and stored") + } + } + } + + private fun asyncRetrieveIsJailbroken() { + logDeveloper("EmbraceMetadataService", "Async retrieve Jailbroken") + + // if the isJailbroken property exists in memory, don't try to retrieve it + if (isJailbroken != null) { + logDeveloper("EmbraceMetadataService", "Jailbroken already exists") + return + } + metadataRetrieveExecutorService.submit( + Callable { + logDeveloper("EmbraceMetadataService", "Async retrieve jailbroken") + val storedIsJailbroken = preferencesService.jailbroken + // load value from shared preferences + if (storedIsJailbroken != null) { + logDeveloper("EmbraceMetadataService", "Jailbroken is present, loading from store") + isJailbroken = storedIsJailbroken + } else { + isJailbroken = MetadataUtils.isJailbroken() + preferencesService.jailbroken = isJailbroken + logDeveloper("EmbraceMetadataService", "Jailbroken processed and stored") + } + logDeveloper("EmbraceMetadataService", "Jailbroken: $isJailbroken") + null + } + ) + } + + @VisibleForTesting + fun asyncRetrieveDiskUsage(isAndroid26OrAbove: Boolean) { + metadataRetrieveExecutorService.submit( + Callable { + logDeveloper("EmbraceMetadataService", "Async retrieve disk usage") + val free = MetadataUtils.getInternalStorageFreeCapacity(statFs.value) + if (isAndroid26OrAbove && configService.autoDataCaptureBehavior.isDiskUsageReportingEnabled()) { + val deviceDiskAppUsage = MetadataUtils.getDeviceDiskAppUsage( + storageStatsManager, packageManager, packageName + ) + if (deviceDiskAppUsage != null) { + logDeveloper("EmbraceMetadataService", "Disk usage is present") + diskUsage = DiskUsage(deviceDiskAppUsage, free) + } + } + if (diskUsage == null) { + diskUsage = DiskUsage(null, free) + } + logDeveloper("EmbraceMetadataService", "Device disk free: $free") + null + } + ) + } + + @VisibleForTesting + fun getReactNativeBundleId(): String? = reactNativeBundleId.value + + override fun getDeviceId(): String = deviceId.value + + override fun getAppVersionCode(): String = appVersionCode + + override fun getAppVersionName(): String = appVersionName + + override fun getDeviceInfo(): DeviceInfo = getDeviceInfo(true) + + private fun getDeviceInfo(populateAllFields: Boolean): DeviceInfo { + val storageCapacityBytes = when { + populateAllFields -> MetadataUtils.getInternalStorageTotalCapacity(statFs.value) + else -> 0 + } + return DeviceInfo( + MetadataUtils.getDeviceManufacturer(), + MetadataUtils.getModel(), + deviceArchitecture.architecture, + isJailbroken(), + MetadataUtils.getLocale(), + storageCapacityBytes, + MetadataUtils.getOperatingSystemType(), + MetadataUtils.getOperatingSystemVersion(), + MetadataUtils.getOperatingSystemVersionCode(), + getScreenResolution(), + MetadataUtils.getTimezoneId(), + MetadataUtils.getSystemUptime(), + MetadataUtils.getNumberOfCores(), + if (populateAllFields) getCpuName() else null, + if (populateAllFields) getEgl() else null + ) + } + + override fun getLightweightDeviceInfo(): DeviceInfo = getDeviceInfo(false) + + override fun getAppInfo(): AppInfo = getAppInfo(true) + + @Suppress("CyclomaticComplexMethod", "ComplexMethod") + private fun getAppInfo(populateAllFields: Boolean): AppInfo { + var infoPlatformVersion: String? = null + var hostedSdkVersion: String? = null + var infoUnityBuildIdNumber: String? = null + var infoReactNativeBundle: String? = null + var infoJavaScriptPatchNumber: String? = null + var infoReactNativeVersion: String? = null + // applies to Unity builds only. + if (appFramework == AppFramework.UNITY) { + infoPlatformVersion = unityVersion ?: preferencesService.unityVersionNumber + infoUnityBuildIdNumber = buildGuid ?: preferencesService.unityBuildIdNumber + hostedSdkVersion = unitySdkVersion ?: preferencesService.unitySdkVersionNumber + } + + // applies to React Native builds only + if (appFramework == AppFramework.REACT_NATIVE) { + infoReactNativeBundle = reactNativeBundleId.value + infoJavaScriptPatchNumber = javaScriptPatchNumber + infoReactNativeVersion = reactNativeVersion + hostedSdkVersion = getRnSdkVersion() + } + + // applies to Flutter builds only + if (appFramework == AppFramework.FLUTTER) { + infoPlatformVersion = dartSdkVersion + hostedSdkVersion = getEmbraceFlutterSdkVersion() + } + return AppInfo( + appVersionName, + appFramework.value, + buildInfo.buildId, + buildInfo.buildType, + buildInfo.buildFlavor, + MetadataUtils.appEnvironment(applicationInfo), + when { + populateAllFields -> appUpdated.value + else -> false + }, + when { + populateAllFields -> appUpdated.value + else -> false + }, + appVersionCode, + when { + populateAllFields -> osUpdated.value + else -> false + }, + when { + populateAllFields -> osUpdated.value + else -> false + }, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE, + infoReactNativeBundle, + infoJavaScriptPatchNumber, + infoReactNativeVersion, + infoPlatformVersion, + infoUnityBuildIdNumber, + hostedSdkVersion + ) + } + + override fun getLightweightAppInfo(): AppInfo = getAppInfo(false) + + private fun getRnSdkVersion(): String? = rnSdkVersion ?: preferencesService.rnSdkVersion + + private val dartSdkVersion: String? + get() = dartVersion ?: preferencesService.dartSdkVersion + + private fun getEmbraceFlutterSdkVersion(): String? = + embraceFlutterSdkVersion ?: preferencesService.embraceFlutterSdkVersion + + override fun getAppId(): String { + return configService.sdkModeBehavior.appId + } + + override fun isAppUpdated(): Boolean = appUpdated.value + + override fun isOsUpdated(): Boolean = osUpdated.value + + override val activeSessionId: String? + get() = sessionId + + override fun setActiveSessionId(sessionId: String?) { + logDeveloper("EmbraceMetadataService", "Active session Id: $sessionId") + this.sessionId = sessionId + setSessionIdToProcessStateSummary(this.sessionId) + } + + override fun removeActiveSessionId(sessionId: String?) { + if (this.sessionId != null && this.sessionId == sessionId) { + logDeveloper("EmbraceMetadataService", "Nulling active session Id") + setActiveSessionId(null) + } + } + + /** + * On android 11+, we use ActivityManager#setProcessStateSummary to store sessionId + * Then, this information will be included in the record of ApplicationExitInfo on the death of the current calling process + * + * @param sessionId current session id + */ + private fun setSessionIdToProcessStateSummary(sessionId: String?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (sessionId != null) { + try { + activityManager?.setProcessStateSummary(sessionId.toByteArray()) + } catch (e: Throwable) { + logError("Couldn't set Process State Summary", e) + } + } + } + } + + override fun getAppState(): String { + return if (activityService.isInBackground) { + logDeveloper("EmbraceMetadataService", "App state: BACKGROUND") + "background" + } else { + logDeveloper("EmbraceMetadataService", "App state: ACTIVE") + "active" + } + } + + override fun getDiskUsage(): DiskUsage? = diskUsage + + override fun getScreenResolution(): String? = screenResolution + + override fun isJailbroken(): Boolean? = isJailbroken + + override fun getCpuName(): String? = cpuName + + override fun getEgl(): String? = egl + + override fun setReactNativeBundleId(context: Context, jsBundleIdUrl: String?) { + if (jsBundleIdUrl.isNullOrEmpty()) { + InternalStaticEmbraceLogger.logError("JavaScript bundle URL must have non-zero length") + reactNativeBundleId = lazy { buildInfo.buildId } + return + } + val currentUrl = preferencesService.javaScriptBundleURL + if (currentUrl != null && currentUrl == jsBundleIdUrl) { + // if the JS bundle ID URL didn't change, use the value from preferences + InternalStaticEmbraceLogger.logDebug( + "JavaScript bundle URL already exists and didn't change. " + + "Using: " + currentUrl + "." + ) + reactNativeBundleId = lazy { buildInfo.buildId } + return + } + + // if doesn't exists or if is a new JS bundle ID URL, save the new value in preferences + preferencesService.javaScriptBundleURL = jsBundleIdUrl + + // get the hashed bundle ID file from the bundle ID URL + reactNativeBundleId = metadataRetrieveExecutorService.eagerLazyLoad( + Callable { + computeReactNativeBundleId( + context, + jsBundleIdUrl, + buildInfo.buildId + ) + } + ) + } + + override fun setEmbraceFlutterSdkVersion(version: String?) { + embraceFlutterSdkVersion = version + preferencesService.embraceFlutterSdkVersion = version + } + + override fun setRnSdkVersion(version: String?) { + rnSdkVersion = version + preferencesService.rnSdkVersion = version + } + + override fun setDartVersion(version: String?) { + dartVersion = version + preferencesService.dartSdkVersion = version + } + + override fun applicationStartupComplete() { + val appVersion = getAppVersionName() + val osVersion = Build.VERSION.RELEASE + val localDeviceId = getDeviceId() + val installDate = clock.now() + logDebug( + String.format( + Locale.getDefault(), + "Setting metadata on preferences service. " + + "App version: {%s}, OS version {%s}, device ID: {%s}, install date: {%d}", + appVersion, + osVersion, + localDeviceId, + installDate + ) + ) + preferencesService.appVersion = appVersion + preferencesService.osVersion = osVersion + preferencesService.deviceIdentifier = localDeviceId + if (preferencesService.installDate == null) { + preferencesService.installDate = installDate + } + logDeveloper("EmbraceMetadataService", "- Application Startup Complete -") + } + + companion object { + + /** + * Default string value for app info missing strings + */ + private const val UNKNOWN_VALUE = "UNKNOWN" + + /** + * Creates an instance of the [EmbraceMetadataService] from the device's [Context] + * for creating Android system services. + * + * @param context the [Context] + * @param buildInfo the build information + * @param appFramework the framework used by the app + * @param preferencesService the preferences service + * @return an instance + */ + @JvmStatic + @Suppress("LongParameterList") + fun ofContext( + context: Context, + buildInfo: BuildInfo, + configService: ConfigService, + appFramework: AppFramework, + preferencesService: PreferencesService, + activityService: ActivityService, + metadataRetrieveExecutorService: ExecutorService, + storageStatsManager: StorageStatsManager?, + windowManager: WindowManager?, + activityManager: ActivityManager?, + clock: Clock, + embraceCpuInfoDelegate: CpuInfoDelegate, + deviceArchitecture: DeviceArchitecture + ): EmbraceMetadataService { + val packageInfo: PackageInfo + var appVersionName: String + var appVersionCode: String + val packageManager = context.packageManager + try { + packageInfo = packageManager.getPackageInfo(context.packageName, 0) + // some customers have trailing white-space for the app version. remove this. + appVersionName = packageInfo.versionName.toString().trim { it <= ' ' } + appVersionCode = packageInfo.versionCode.toString() + logDeveloper( + "EmbraceMetadataService", + "App version name: $appVersionName - App version code: $appVersionCode" + ) + } catch (e: Exception) { + logDeveloper( + "EmbraceMetadataService", + "Cannot set appVersionName and appVersionCode, setting UNKNOWN_VALUE", e + ) + appVersionName = UNKNOWN_VALUE + appVersionCode = UNKNOWN_VALUE + } + val finalAppVersionName = appVersionName + val isAppUpdated = lazy { + val lastKnownAppVersion = preferencesService.appVersion + val appUpdated = ( + lastKnownAppVersion != null && + !lastKnownAppVersion.equals(finalAppVersionName, ignoreCase = true) + ) + logDeveloper("EmbraceMetadataService", "App updated: $appUpdated") + appUpdated + } + val isOsUpdated = lazy { + val lastKnownOsVersion = preferencesService.osVersion + val osUpdated = ( + lastKnownOsVersion != null && + !lastKnownOsVersion.equals( + Build.VERSION.RELEASE, + ignoreCase = true + ) + ) + logDeveloper("EmbraceMetadataService", "OS updated: $osUpdated") + osUpdated + } + val deviceIdentifier = lazy(preferencesService::deviceIdentifier) + var javaScriptPatchNumber: String? = null + val reactNativeVersion: String? = null + var rnSdkVersion: String? = null + val reactNativeBundleId: Lazy + if (appFramework == AppFramework.REACT_NATIVE) { + reactNativeBundleId = + metadataRetrieveExecutorService.eagerLazyLoad( + Callable { + val lastKnownJsBundleUrl = preferencesService.javaScriptBundleURL + if (lastKnownJsBundleUrl != null) { + computeReactNativeBundleId( + context, + lastKnownJsBundleUrl, + buildInfo.buildId + ) + } else { + // If JS bundle ID URL is not found we assume that the App is not using Codepush. + // Use JS bundle ID URL as React Native bundle ID. + logDeveloper( + "EmbraceMetadataService", + "setting JSBundleUrl as buildId: " + buildInfo.buildId + ) + buildInfo.buildId + } + } + ) + javaScriptPatchNumber = preferencesService.javaScriptPatchNumber + if (javaScriptPatchNumber != null) { + logDeveloper( + "EmbraceMetadataService", + "Java script patch number: $javaScriptPatchNumber" + ) + } + rnSdkVersion = preferencesService.rnSdkVersion + if (rnSdkVersion != null) { + logDeveloper("EmbraceMetadataService", "RN Embrace SDK version: $rnSdkVersion") + } + } else { + reactNativeBundleId = lazy { buildInfo.buildId } + logDeveloper("EmbraceMetadataService", "setting default RN as buildId") + } + var unityVersion: String? = null + var buildGuid: String? = null + var unitySdkVersion: String? = null + if (appFramework == AppFramework.UNITY) { + unityVersion = preferencesService.unityVersionNumber + if (unityVersion != null) { + logDeveloper("EmbraceMetadataService", "Unity version: $unityVersion") + } else { + logDeveloper("EmbraceMetadataService", "Unity version is not present") + } + buildGuid = preferencesService.unityBuildIdNumber + if (buildGuid != null) { + logDeveloper("EmbraceMetadataService", "Unity build id: $buildGuid") + } else { + logDeveloper("EmbraceMetadataService", "Unity build id number is not present") + } + unitySdkVersion = preferencesService.unitySdkVersionNumber + if (unitySdkVersion != null) { + logDeveloper("EmbraceMetadataService", "Unity SDK version: $unitySdkVersion") + } else { + logDeveloper("EmbraceMetadataService", "Unity SDK version is not present") + } + } + return EmbraceMetadataService( + windowManager, + packageManager, + storageStatsManager, + activityManager, + buildInfo, + configService, + context.applicationInfo, + deviceIdentifier, + context.packageName, + appVersionName, + appVersionCode, + appFramework, + isAppUpdated, + isOsUpdated, + preferencesService, + activityService, + reactNativeBundleId, + javaScriptPatchNumber, + reactNativeVersion, + unityVersion, + buildGuid, + unitySdkVersion, + rnSdkVersion, + metadataRetrieveExecutorService, + clock, + embraceCpuInfoDelegate, + deviceArchitecture + ) + } + + private fun getBundleAssetName(bundleUrl: String): String { + val name = bundleUrl.substring(bundleUrl.indexOf("://") + 3) + logDeveloper("EmbraceMetadataService", "Asset name: $name") + return name + } + + private fun getBundleAsset(context: Context, bundleUrl: String): InputStream? { + try { + logDeveloper( + "EmbraceMetadataService", + "Attempting to read bundle asset: $bundleUrl" + ) + return context.assets.open(getBundleAssetName(bundleUrl)) + } catch (e: Exception) { + InternalStaticEmbraceLogger.logError("Failed to retrieve RN bundle file from assets.", e) + } + return null + } + + private fun getCustomBundleStream(bundleUrl: String): InputStream? { + try { + logDeveloper( + "EmbraceMetadataService", + "Attempting to load bundle from custom path: $bundleUrl" + ) + return FileInputStream(bundleUrl) + } catch (e: NullPointerException) { + InternalStaticEmbraceLogger.logError("Failed to retrieve the custom RN bundle file.", e) + } catch (e: FileNotFoundException) { + InternalStaticEmbraceLogger.logError("Failed to retrieve the custom RN bundle file.", e) + } + return null + } + + internal fun computeReactNativeBundleId( + context: Context, + bundleUrl: String, + defaultBundleId: String? + ): String? { + val bundleStream: InputStream? + // checks if the bundle url is an asset + if (bundleUrl.contains("assets")) { + // looks for the bundle file in assets + bundleStream = getBundleAsset(context, bundleUrl) + logDeveloper("EmbraceMetadataService", "Loaded bundle file asset: $bundleStream") + } else { + // looks for the bundle file from the custom path + bundleStream = getCustomBundleStream(bundleUrl) + logDeveloper( + "EmbraceMetadataService", + "Loaded bundle file from custom path: $bundleStream" + ) + } + if (bundleStream == null) { + logDeveloper( + "EmbraceMetadataService", + "Setting default RN bundleId: $defaultBundleId" + ) + return defaultBundleId + } + try { + bundleStream.use { inputStream -> + ByteArrayOutputStream().use { buffer -> + var read: Int + // The hash size for the MD5 algorithm is 128 bits - 16 bytes. + val data = ByteArray(16) + while (inputStream.read(data, 0, data.size).also { read = it } != -1) { + buffer.write(data, 0, read) + } + return hashBundleToMd5(buffer.toByteArray()) + } + } + } catch (e: Exception) { + logError("Failed to compute the RN bundle file.", e) + } + logDeveloper("EmbraceMetadataService", "Setting default RN bundleId: $defaultBundleId") + // if the hashing of the JS bundle URL fails, returns the default bundle ID + return defaultBundleId + } + + private fun hashBundleToMd5(bundle: ByteArray): String { + val hashBundle: String + val md = MessageDigest.getInstance("MD5") + val bundleHashed = md.digest(bundle) + val sb = StringBuilder() + for (b in bundleHashed) { + sb.append(String.format(Locale.getDefault(), "%02x", b.toInt() and 0xff)) + } + hashBundle = sb.toString().toUpperCase(Locale.getDefault()) + logDeveloper("EmbraceMetadataService", "Setting RN bundleId: $hashBundle") + return hashBundle + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataService.kt new file mode 100644 index 0000000000..ed41c861b5 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataService.kt @@ -0,0 +1,171 @@ +package io.embrace.android.embracesdk.capture.metadata + +import android.content.Context +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.DiskUsage + +internal interface MetadataService { + + /** + * Gets information about the current application being instrumented. This is sent with the + * following events, as well as all sessions: + * + * * START + * * INFO_LOG + * * ERROR_LOG + * * WARNING_LOG + * * CRASH + * + * @return the application information + */ + fun getAppInfo(): AppInfo + + /** + * Same as [.getAppInfo] but does not search for information (in preferences, for example) that is + * not already loaded in memory in the service. + */ + fun getLightweightAppInfo(): AppInfo + + /** + * Gets the app ID which is defined as part of the configuration. + * + * @return the app ID. + */ + fun getAppId(): String + + /** + * Gets information and specifications of the current device. This is sent with the following + * events, as well as all sessions: + * + * * START + * * INFO_LOG + * * ERROR_LOG + * * WARNING_LOG + * * CRASH + * + * + * @return the device information + */ + fun getDeviceInfo(): DeviceInfo + + /** + * Same as [.getDeviceInfo] but does not get storage information from the file system. + */ + fun getLightweightDeviceInfo(): DeviceInfo + + /** + * Gets the current device's disk usage and space available. + * + * @return the device's disk usage statistics + */ + fun getDiskUsage(): DiskUsage? + + /** + * Gets the device's screen resolution. + * + * @return the device's screen resolution + */ + fun getScreenResolution(): String? + + /** + * Gets if the device is jailbroken. + * + * @return if the device is Jailbroken + */ + fun isJailbroken(): Boolean? + + /** + * Gets the unique ID from the device. This is an MD5 hash of the Android Secure ID. + * + * @return the unique device ID + */ + fun getDeviceId(): String + + /** + * @return the app version code. + */ + fun getAppVersionCode(): String? + + /** + * @return the app version name. + */ + fun getAppVersionName(): String + + /** + * @return is the app was updated since last launch. + */ + fun isAppUpdated(): Boolean + + /** + * @return is the OS was updated since last launch. + */ + fun isOsUpdated(): Boolean + + /** + * Gets the currently active session ID, if present. + * + * @return an optional containing the currently active session ID + */ + val activeSessionId: String? + + /** + * Sets the currently active session ID; + * + * @param sessionId the session ID that is currently active + */ + fun setActiveSessionId(sessionId: String?) + + /** + * If the currently active session ID is @param sessionId, set it to null + * If the currently active session is different, do nothing + * + * @param sessionId null current session id only if it euals this one + */ + fun removeActiveSessionId(sessionId: String?) + + /** + * Returns 'active' if the application is in the foreground, or 'background' if the app is in + * the background. + * + * @return the current state of the app + */ + fun getAppState(): String? + + /** + * Sets React Native Bundle ID from a custom JavaScript Bundle URL. + */ + fun setReactNativeBundleId(context: Context, jsBundleIdUrl: String?) + + /** + * Sets the Embrace Flutter SDK version + */ + fun setEmbraceFlutterSdkVersion(version: String?) + + /** + * Sets the Embrace React Native SDK version + */ + fun setRnSdkVersion(version: String?) + + /** + * Sets the Dart version + */ + fun setDartVersion(version: String?) + + /** + * Queues in a single thread executor callables to retrieve values in background + */ + fun precomputeValues() + + /** + * + * @return cpu name + */ + fun getCpuName(): String? + + /** + * + * @return egl name + */ + fun getEgl(): String? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataUtils.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataUtils.java new file mode 100644 index 0000000000..3474784e92 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/metadata/MetadataUtils.java @@ -0,0 +1,267 @@ +package io.embrace.android.embracesdk.capture.metadata; + +import android.annotation.TargetApi; +import android.app.usage.StorageStats; +import android.app.usage.StorageStatsManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Process; +import android.os.StatFs; +import android.os.SystemClock; +import android.os.storage.StorageManager; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowManager; + +import androidx.annotation.Nullable; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Pattern; + +import io.embrace.android.embracesdk.injection.CoreModuleKt; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; + +/** + * Utilities for retrieving metadata from the device's {@link Context}. This metadata is passed + * to the API with certain messages to provide device information. + */ +final class MetadataUtils { + private static final String OS_VERSION = "Android OS"; + + private static final String ENVIRONMENT_DEV = "dev"; + + private static final String ENVIRONMENT_PROD = "prod"; + + private static final List JAILBREAK_LOCATIONS = Arrays.asList( + "/sbin/", + "/system/bin/", + "/system/xbin/", + "/data/local/xbin/", + "/data/local/bin/", + "/system/sd/xbin/", + "/system/bin/failsafe/", + "/data/local/"); + + + /** + * Gets the name of the manufacturer of the device. + * + * @return the name of the device manufacturer + */ + static String getDeviceManufacturer() { + return Build.MANUFACTURER; + } + + /** + * Gets the name of the model of the device. + * + * @return the name of the model of the device + */ + static String getModel() { + return Build.MODEL; + } + + /** + * Gets the locale of the device, represented as "language_country". + * + * @return the locale of the device + */ + static String getLocale() { + return Locale.getDefault().getLanguage() + "_" + Locale.getDefault().getCountry(); + } + + /** + * Gets the operating system of the device. This is hard-coded to Android OS. + * + * @return the device's operating system + */ + static String getOperatingSystemType() { + return OS_VERSION; + } + + /** + * Gets the version of the installed operating system on the device. + * + * @return the version of the operating system + */ + static String getOperatingSystemVersion() { + return String.valueOf(Build.VERSION.RELEASE); + } + + /** + * Gets the version code of the running Android SDK. + * + * @return the running Android SDK version code + */ + static int getOperatingSystemVersionCode() { + return Build.VERSION.SDK_INT; + } + + /** + * Gets the system uptime in milliseconds. + * + * @return the system uptime in milliseconds + */ + static Long getSystemUptime() { + return SystemClock.uptimeMillis(); + } + + /** + * Gets the device's screen resolution. + * + * @param windowManager the {@link WindowManager} from the {@link Context} + * @return the device's screen resolution + */ + @Nullable + static String getScreenResolution(WindowManager windowManager) { + try { + InternalStaticEmbraceLogger.logDeveloper("MetadataUtils", "Computing screen resolution"); + Display display = windowManager.getDefaultDisplay(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + display.getMetrics(displayMetrics); + return String.format(Locale.US, "%dx%d", displayMetrics.widthPixels, displayMetrics.heightPixels); + } catch (Exception ex) { + InternalStaticEmbraceLogger.logDebug("Could not determine screen resolution", ex); + return null; + } + } + + /** + * Gets a ID of the device's timezone, e.g. 'Europe/London'. + * + * @return the ID of the device's timezone + */ + static String getTimezoneId() { + return TimeZone.getDefault().getID(); + } + + /** + * Gets the total storage capacity of the device. + * + * @param statFs the {@link StatFs} service for the device + * @return the total storage capacity in bytes + */ + static long getInternalStorageTotalCapacity(StatFs statFs) { + return statFs.getTotalBytes(); + } + + /** + * Gets the free capacity of the internal storage of the device. + * + * @param statFs the {@link StatFs} service for the device + * @return the total free capacity of the internal storage of the device in bytes + */ + static long getInternalStorageFreeCapacity(StatFs statFs) { + InternalStaticEmbraceLogger.logDeveloper("MetadataUtils", "Getting internal storage free capacity"); + return statFs.getFreeBytes(); + } + + /** + * Attempts to determine the disk usage of the app on the device. + *

+ * If the disk usage cannot be determined, null is returned. + * + * @param storageStatsManager the {@link StorageStatsManager} + * @param packageManager the {@link PackageManager} + * @param contextPackageName the name of the package from the {@link Context} + * @return optionally the disk usage of the app on the device + */ + @TargetApi(Build.VERSION_CODES.O) + @Nullable + static Long getDeviceDiskAppUsage( + StorageStatsManager storageStatsManager, + PackageManager packageManager, + String contextPackageName) { + InternalStaticEmbraceLogger.logDeveloper("MetadataUtils", "Getting device disk app usage"); + try { + PackageInfo packageInfo = packageManager.getPackageInfo(contextPackageName, 0); + if (packageInfo != null && packageInfo.packageName != null) { + StorageStats stats = storageStatsManager.queryStatsForPackage( + StorageManager.UUID_DEFAULT, + packageInfo.packageName, + Process.myUserHandle()); + return stats.getAppBytes() + stats.getDataBytes() + stats.getCacheBytes(); + } else { + InternalStaticEmbraceLogger.logDeveloper("MetadataUtils", "Cannot get disk usage, packageInfo is null"); + } + } catch (Exception ex) { + // The package name and storage volume should always exist + InternalStaticEmbraceLogger.logError("Error retrieving device disk usage", ex); + } + return null; + } + + /** + * Tries to determine whether the device is jailbroken by looking for specific directories which + * exist on jailbroken devices. Emulators are excluded and will always return false. + * + * @return true if the device is jailbroken and not an emulator, false otherwise + */ + static boolean isJailbroken() { + InternalStaticEmbraceLogger.logDeveloper("MetadataUtils", "Processing jailbroken"); + + if (isEmulator()) { + InternalStaticEmbraceLogger.logDeveloper("MetadataUtils", "Device is an emulator, Jailbroken=false"); + return false; + } + + for (String location : JAILBREAK_LOCATIONS) { + if (new File(location + "su").exists()) { + return true; + } + } + return false; + } + + /** + * Tries to determine whether the device is an emulator by looking for known models and + * manufacturers which correspond to emulators. + * + * @return true if the device is detected to be an emulator, false otherwise + */ + static boolean isEmulator() { + boolean isEmulator = Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || "google_sdk".equals(Build.PRODUCT); + + InternalStaticEmbraceLogger.logDeveloper("MetadataUtils", "Device is an Emulator = " + isEmulator); + return isEmulator; + } + + /** + * Creates a "prod" or "dev" environment string, depending on whether or not this is a debug + * build of the app. + * + * @param applicationInfo the application info used for introspecting flags + * @return a string representation of the environment name + */ + static String appEnvironment(ApplicationInfo applicationInfo) { + return CoreModuleKt.isDebug(applicationInfo) ? ENVIRONMENT_DEV : ENVIRONMENT_PROD; + } + + /** + * Get the number of available cores for device info + * + * @return Number of cores in long + */ + static int getNumberOfCores() { + return Runtime.getRuntime().availableProcessors(); + } + + private MetadataUtils() { + // Restricted constructor + } + +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/EmbraceOrientationService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/EmbraceOrientationService.kt new file mode 100644 index 0000000000..6485f848fb --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/EmbraceOrientationService.kt @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.capture.orientation + +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.payload.Orientation +import java.util.LinkedList + +internal class EmbraceOrientationService( + private val clock: Clock +) : OrientationService { + + /** + * States the activity orientations. + */ + private val orientations = LinkedList() + + override fun onOrientationChanged(orientation: Int?) { + logDeveloper("EmbraceOrientationService", "onOrientationChanged") + if (orientation != null && (orientations.isEmpty() || orientations.last.internalOrientation != orientation)) { + orientations.add(Orientation(orientation, clock.now())) + logDeveloper("EmbraceOrientationService", "added new orientation $orientation") + } + } + + override fun getCapturedData(): List = orientations + + override fun cleanCollections() = orientations.clear() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/NoOpOrientationService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/NoOpOrientationService.kt new file mode 100644 index 0000000000..7e20acf6a0 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/NoOpOrientationService.kt @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk.capture.orientation + +import io.embrace.android.embracesdk.payload.Orientation + +internal class NoOpOrientationService : OrientationService { + override fun onOrientationChanged(orientation: Int?) { + return + } + + override fun cleanCollections() { + } + + override fun getCapturedData(): List { + return emptyList() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/OrientationService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/OrientationService.kt new file mode 100644 index 0000000000..371f62ab08 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/orientation/OrientationService.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.capture.orientation + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.Orientation + +internal interface OrientationService : DataCaptureService?> { + fun onOrientationChanged(orientation: Int?) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService.kt new file mode 100644 index 0000000000..73153db468 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/EmbracePowerSaveModeService.kt @@ -0,0 +1,104 @@ +package io.embrace.android.embracesdk.capture.powersave + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.PowerManager +import android.os.PowerManager.ACTION_POWER_SAVE_MODE_CHANGED +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.payload.PowerModeInterval +import io.embrace.android.embracesdk.session.ActivityListener +import java.util.concurrent.ExecutorService + +internal class EmbracePowerSaveModeService( + private val context: Context, + private val executorService: ExecutorService, + private val clock: Clock, + private val powerManager: PowerManager? +) : BroadcastReceiver(), PowerSaveModeService, ActivityListener { + + private val tag = "EmbracePowerSaveModeService" + + private val powerSaveIntentFilter = IntentFilter(ACTION_POWER_SAVE_MODE_CHANGED) + + private val powerSaveModeIntervals = mutableListOf() + + init { + registerPowerSaveModeReceiver() + } + + private fun registerPowerSaveModeReceiver() { + executorService.submit { + try { + context.registerReceiver(this, powerSaveIntentFilter) + logDeveloper(tag, "registered power save mode changed") + } catch (ex: Exception) { + InternalStaticEmbraceLogger.logError( + "Failed to register: $tag broadcast receiver. Power save mode status will be unavailable.", + ex + ) + } + } + } + + override fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) { + if (powerManager?.isPowerSaveMode == true) { + powerSaveModeIntervals.add(PowerChange(timestamp, Kind.START)) + } + } + + override fun onReceive(context: Context, intent: Intent) { + logDeveloper(tag, "onReceive") + try { + when (intent.action) { + ACTION_POWER_SAVE_MODE_CHANGED -> + powerSaveModeIntervals.add( + PowerChange( + clock.now(), + if (powerManager?.isPowerSaveMode == true) Kind.START else Kind.END + ) + ) + } + } catch (ex: Exception) { + InternalStaticEmbraceLogger.logError("Failed to handle " + intent.action, ex) + } + } + + override fun getCapturedData(): List { + val intervals = mutableListOf() + for (powerChange in powerSaveModeIntervals) { + if (powerChange.time >= 0) { + when (powerChange.kind) { + Kind.START -> { + intervals.add(PowerModeInterval(powerChange.time)) + } + + Kind.END -> { + if (intervals.isNotEmpty() && intervals.last().startTime != 0L) { + intervals[intervals.size - 1] = + intervals.last().copy(endTime = powerChange.time) + } else { + intervals.add(PowerModeInterval(0, powerChange.time)) + } + } + } + } + } + return intervals + } + + override fun close() { + logDebug("Stopping $tag") + context.unregisterReceiver(this) + } + + override fun cleanCollections() = powerSaveModeIntervals.clear() + + data class PowerChange(val time: Long, val kind: Kind) + + enum class Kind { START, END } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/NoOpPowerSaveModeService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/NoOpPowerSaveModeService.kt new file mode 100644 index 0000000000..22d966dac8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/NoOpPowerSaveModeService.kt @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk.capture.powersave + +import io.embrace.android.embracesdk.payload.PowerModeInterval + +internal class NoOpPowerSaveModeService : PowerSaveModeService { + + override fun close() { + } + + override fun cleanCollections() { + } + + override fun getCapturedData(): List? { + return null + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/PowerSaveModeService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/PowerSaveModeService.kt new file mode 100644 index 0000000000..86ed7eee73 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/powersave/PowerSaveModeService.kt @@ -0,0 +1,7 @@ +package io.embrace.android.embracesdk.capture.powersave + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.PowerModeInterval +import java.io.Closeable + +internal interface PowerSaveModeService : DataCaptureService?>, Closeable diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/EmbraceStrictModeService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/EmbraceStrictModeService.kt new file mode 100644 index 0000000000..fc67754a9d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/EmbraceStrictModeService.kt @@ -0,0 +1,54 @@ +package io.embrace.android.embracesdk.capture.strictmode + +import android.os.Build +import android.os.StrictMode +import android.os.strictmode.Violation +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.payload.ExceptionInfo +import io.embrace.android.embracesdk.payload.StrictModeViolation +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService + +@RequiresApi(Build.VERSION_CODES.P) +internal class EmbraceStrictModeService( + private val configService: ConfigService, + private val executorService: ExecutorService, + private val clock: Clock +) : StrictModeService { + + private val violations = mutableListOf() + + override fun start() { + addStrictModeListener(executorService, StrictMode.OnThreadViolationListener(::addViolation)) + } + + @VisibleForTesting + internal fun addViolation(violation: Violation) { + if (violations.size < configService.anrBehavior.getStrictModeViolationLimit()) { + val exceptionInfo = ExceptionInfo.ofThrowable(violation) + violations.add(StrictModeViolation(exceptionInfo, clock.now())) + } + } + + private fun addStrictModeListener( + executor: Executor, + listener: StrictMode.OnThreadViolationListener + ) { + // only detect I/O strict mode errors for now. + val builder = StrictMode.ThreadPolicy.Builder().apply { + detectDiskReads() + detectDiskWrites() + detectUnbufferedIo() + }.penaltyListener(executor, listener) + StrictMode.setThreadPolicy(builder.build()) + } + + override fun cleanCollections() { + violations.clear() + } + + override fun getCapturedData(): List = violations +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService.kt new file mode 100644 index 0000000000..6fd2661621 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/NoOpStrictModeService.kt @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.capture.strictmode + +import io.embrace.android.embracesdk.payload.StrictModeViolation + +internal class NoOpStrictModeService : StrictModeService { + override fun start() {} + override fun cleanCollections() {} + + override fun getCapturedData(): List? { + return null + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/StrictModeService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/StrictModeService.kt new file mode 100644 index 0000000000..fccd97f552 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/strictmode/StrictModeService.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.capture.strictmode + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.StrictModeViolation + +internal interface StrictModeService : DataCaptureService?> { + fun start() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/EmbraceThermalStatusService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/EmbraceThermalStatusService.kt new file mode 100644 index 0000000000..3d24fd2ce1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/EmbraceThermalStatusService.kt @@ -0,0 +1,65 @@ +package io.embrace.android.embracesdk.capture.thermalstate + +import android.os.Build +import android.os.PowerManager +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.ThermalState +import java.util.LinkedList +import java.util.concurrent.Executor + +private const val CAPTURE_LIMIT = 100 + +@RequiresApi(Build.VERSION_CODES.Q) +internal class EmbraceThermalStatusService( + executor: Executor, + private val clock: Clock, + private val logger: InternalEmbraceLogger, + private val pm: PowerManager? +) : ThermalStatusService { + + private val thermalStates = LinkedList() + + private val thermalStatusListener = PowerManager.OnThermalStatusChangedListener { + handleThermalStateChange(it) + } + + init { + pm?.let { + logger.logDeveloper("ThermalStatusService", "Adding thermal status listener") + it.addThermalStatusListener(executor, thermalStatusListener) + } + } + + @VisibleForTesting + fun handleThermalStateChange(status: Int?) { + if (status == null) { + logger.logDeveloper("ThermalStatusService", "Null thermal status, no-oping.") + return + } + + logger.logDeveloper("ThermalStatusService", "Thermal status change: $status") + thermalStates.add(ThermalState(clock.now(), status)) + + if (thermalStates.size > CAPTURE_LIMIT) { + logger.logDeveloper( + "ThermalStatusService", + "Exceeded capture limit, removing oldest thermal status sample." + ) + thermalStates.removeFirst() + } + } + + override fun cleanCollections() = thermalStates.clear() + + override fun getCapturedData(): List = thermalStates + + override fun close() { + pm?.let { + logger.logDeveloper("ThermalStatusService", "Removing thermal status listener") + it.removeThermalStatusListener(thermalStatusListener) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/NoOpThermalStatusService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/NoOpThermalStatusService.kt new file mode 100644 index 0000000000..57e934cfc3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/NoOpThermalStatusService.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.capture.thermalstate + +import io.embrace.android.embracesdk.payload.ThermalState + +internal class NoOpThermalStatusService : ThermalStatusService { + override fun close() {} + + override fun cleanCollections() { + } + + override fun getCapturedData(): List? { + return null + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService.kt new file mode 100644 index 0000000000..b9c970e6eb --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/thermalstate/ThermalStatusService.kt @@ -0,0 +1,7 @@ +package io.embrace.android.embracesdk.capture.thermalstate + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.ThermalState +import java.io.Closeable + +internal interface ThermalStatusService : DataCaptureService?>, Closeable diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/EmbraceUserService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/EmbraceUserService.kt new file mode 100644 index 0000000000..bf46463aab --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/EmbraceUserService.kt @@ -0,0 +1,147 @@ +package io.embrace.android.embracesdk.capture.user + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.UserInfo +import io.embrace.android.embracesdk.payload.UserInfo.Companion.ofStored +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.ActivityListener +import java.util.regex.Pattern + +internal class EmbraceUserService( + private val preferencesService: PreferencesService, + private val logger: InternalEmbraceLogger +) : ActivityListener, UserService { + + @Volatile + @VisibleForTesting + internal var info: UserInfo = ofStored(preferencesService) + + override fun loadUserInfoFromDisk(): UserInfo? { + return try { + ofStored(preferencesService) + } catch (ex: Exception) { + logger.logError("Failed to load user info from persistent storage.", ex, true) + null + } + } + + override fun getUserInfo(): UserInfo = info.copy() + + override fun setUserIdentifier(userId: String?) { + val currentUserId = info.userId + if (currentUserId != null && currentUserId == userId) { + return + } + info = info.copy(userId = userId) + preferencesService.userIdentifier = userId + } + + override fun clearUserIdentifier() { + setUserIdentifier(null) + } + + override fun setUsername(username: String?) { + val currentUserName = info.username + if (currentUserName != null && currentUserName == username) { + return + } + info = info.copy(username = username) + preferencesService.username = username + } + + override fun clearUsername() { + setUsername(null) + } + + override fun setUserEmail(email: String?) { + val currentEmail = info.email + if (currentEmail != null && currentEmail == email) { + return + } + info = info.copy(email = email) + preferencesService.userEmailAddress = email + } + + override fun clearUserEmail() { + setUserEmail(null) + } + + override fun setUserAsPayer() { + addUserPersona(UserInfo.PERSONA_PAYER) + } + + override fun clearUserAsPayer() { + clearUserPersona(UserInfo.PERSONA_PAYER) + } + + override fun addUserPersona(persona: String?) { + if (persona == null) { + return + } + if (!VALID_PERSONA.matcher(persona).matches()) { + logger.logWarning("Ignoring persona " + persona + " as it does not match " + VALID_PERSONA.pattern()) + return + } + val currentPersonas = info.personas + if (currentPersonas != null) { + if (currentPersonas.size >= PERSONA_LIMIT) { + logger.logWarning("Cannot set persona as the limit of " + PERSONA_LIMIT + " has been reached") + return + } + if (currentPersonas.contains(persona)) { + return + } + } + + val newPersonas: Set = info.personas?.plus(persona) ?: mutableSetOf(persona) + info = info.copy(personas = newPersonas) + preferencesService.userPersonas = newPersonas + } + + override fun clearUserPersona(persona: String?) { + if (persona == null) { + return + } + val currentPersonas = info.personas + if (currentPersonas != null && !currentPersonas.contains(persona)) { + logger.logWarning("Persona '$persona' is not set") + return + } + + val newPersonas: Set = info.personas?.minus(persona) ?: mutableSetOf() + info = info.copy(personas = newPersonas) + preferencesService.userPersonas = newPersonas + } + + override fun clearAllUserPersonas() { + val currentPersonas = info.personas + if (currentPersonas != null && currentPersonas.isEmpty()) { + return + } + val personas: MutableSet = HashSet() + if (preferencesService.userPayer) { + personas.add(UserInfo.PERSONA_PAYER) + } + if (preferencesService.isUsersFirstDay()) { + personas.add(UserInfo.PERSONA_FIRST_DAY_USER) + } + info = info.copy(personas = personas) + preferencesService.userPersonas = personas + } + + override fun clearAllUserInfo() { + clearUserIdentifier() + clearUserEmail() + clearUsername() + clearAllUserPersonas() + } + + companion object { + // Valid persona regex representation. + val VALID_PERSONA: Pattern = Pattern.compile("^[a-zA-Z0-9_]{1,32}$") + + // Maximum number of allowed personas. + const val PERSONA_LIMIT = 10 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/UserService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/UserService.kt new file mode 100644 index 0000000000..29f0931e39 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/user/UserService.kt @@ -0,0 +1,93 @@ +package io.embrace.android.embracesdk.capture.user + +import io.embrace.android.embracesdk.payload.UserInfo + +/** + * Manages user information, allowing the setting and removing of various user attributes. + */ +internal interface UserService { + + /** + * Gets information on the current user of the device. This is sent with all events and sessions. + * + * @return the current user information + */ + fun getUserInfo(): UserInfo + + /** + * Cleans the user info. + */ + fun clearAllUserInfo() + + /** + * Gets user information from persistent storage. + * + * @return the current user information from persistent storage + */ + fun loadUserInfoFromDisk(): UserInfo? + + /** + * Sets the user's ID. This could be a UUID, for example. + * + * @param userId the user's unique identifier + */ + fun setUserIdentifier(userId: String?) + + /** + * Removes the user's unique identifier. + */ + fun clearUserIdentifier() + + /** + * Sets the user's email address. + * + * @param email the user's email address + */ + fun setUserEmail(email: String?) + + /** + * Removes the user's email address. + */ + fun clearUserEmail() + + /** + * Sets the user as a paying user, attaching the paying persona to the user. + */ + fun setUserAsPayer() + + /** + * Unsets the user as a paying user, removing the paying persona from the user. + */ + fun clearUserAsPayer() + + /** + * Attaches the specified persona to the user. This can be used for user segmentation. + * + * @param persona the persona to attach to the user + */ + fun addUserPersona(persona: String?) + + /** + * Removes the specified user persona from the user. + * + * @param persona the persona to remove from the user + */ + fun clearUserPersona(persona: String?) + + /** + * Clears all personas from the user, except for system personas. + */ + fun clearAllUserPersonas() + + /** + * Sets the user's username. + * + * @param username the username to set + */ + fun setUsername(username: String?) + + /** + * Removes the user's username. + */ + fun clearUsername() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/EmbraceWebViewService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/EmbraceWebViewService.kt new file mode 100644 index 0000000000..1f03a0b820 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/EmbraceWebViewService.kt @@ -0,0 +1,120 @@ +package io.embrace.android.embracesdk.capture.webview + +import com.google.gson.reflect.TypeToken +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.WebViewInfo +import io.embrace.android.embracesdk.payload.WebVitalType +import java.util.EnumMap + +internal class EmbraceWebViewService( + val configService: ConfigService, + private val serializer: EmbraceSerializer +) : WebViewService { + + /** + * The information collected for each WebView + */ + private val webViewInfoMap = hashMapOf() + private val webVitalType = object : TypeToken() {}.type + + override fun collectWebData(tag: String, message: String) { + InternalStaticEmbraceLogger.logger.logDeveloper("EmbraceWebViewService", "Collecting WebView log: $message") + + if (message.contains(MESSAGE_KEY_FOR_METRICS)) { + collectWebVital(message, tag) + } else { + InternalStaticEmbraceLogger.logger.logDebug("WebView console message ignored.") + } + } + + override fun getCapturedData(): List { + return webViewInfoMap.values.toList().take(configService.webViewVitalsBehavior.getMaxWebViewVitals()) + } + + private fun collectWebVital(message: String, tag: String) { + InternalStaticEmbraceLogger.logger.logDeveloper("EmbraceWebViewService", "Collecting web metric") + + if (webViewInfoMap.size >= configService.webViewVitalsBehavior.getMaxWebViewVitals()) { + InternalStaticEmbraceLogger.logger.logDebug("Max webview vitals per session exceeded") + return + } + val collectedWebVitals = parseWebVital(message) + collectedWebVitals?.let { + if (webViewInfoMap[it.url + it.startTime] == null) { + webViewInfoMap[it.url + it.startTime] = it.copy( + tag = tag, + webVitalMap = EnumMap( + WebVitalType::class.java + ) + ) + } + + webViewInfoMap[it.url + it.startTime] = processVitalList(it, webViewInfoMap[it.url + it.startTime]!!) + + InternalStaticEmbraceLogger.logger.logDebug("Collected WebView core vital: $message") + } + } + + /** + * The WebView can emit multiple metrics of the same type. Depending on the type of metric, a different filter + * should be applied: + * - CLS: keep the metric with the longest duration (worst performance) + * - LCP: keep the last generated metric (highest start time) + * - FID and FCP: keep the first metric that arrives + */ + private fun processVitalList(newMessage: WebViewInfo, storedMessage: WebViewInfo): WebViewInfo { + newMessage.webVitals.forEach { newVital -> + storedMessage.webVitalMap[newVital.type].let { + if (it == null) { + storedMessage.webVitalMap[newVital.type] = newVital + } else { + when (it.type) { + WebVitalType.CLS -> { + if (newVital.duration > it.duration) { // largest CLS metric + storedMessage.webVitalMap[it.type] = newVital + } + } + WebVitalType.LCP -> { + if (newVital.startTime > it.startTime) { // most recent capture st time + storedMessage.webVitalMap[it.type] = newVital + } + } + else -> { + // do nothing + } + } + } + } + } + + return storedMessage.copy(webVitals = storedMessage.webVitalMap.values.toMutableList()) + } + + private fun parseWebVital(message: String): WebViewInfo? { + try { + if (message.length < SCRIPT_MESSAGE_MAXIMUM_ALLOWED_LENGTH) { + return serializer.fromJson(message, webVitalType) + } else { + InternalStaticEmbraceLogger.logger.logError("Web Vital info is too large to parse") + } + } catch (e: Exception) { + InternalStaticEmbraceLogger.logger.logError("Cannot parse Web Vital", e) + } + return null + } + + override fun cleanCollections() { + webViewInfoMap.clear() + } + + companion object { + private const val SCRIPT_MESSAGE_MAXIMUM_ALLOWED_LENGTH = 2000 + + /** + * Metrics have this attribute to recognize and parse them. + */ + private const val MESSAGE_KEY_FOR_METRICS = "EMBRACE_METRIC" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/WebViewService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/WebViewService.kt new file mode 100644 index 0000000000..73361fb705 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/webview/WebViewService.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.capture.webview + +import io.embrace.android.embracesdk.arch.DataCaptureService +import io.embrace.android.embracesdk.payload.WebViewInfo + +/** + * Collects WebViews information, like view properties, console logs, or core web vitals. + */ +internal interface WebViewService : DataCaptureService?> { + + /** + * Collects WebView logs triggered by the Embrace JS Plugin. + * + * @param tag a name for the WebView + * @param message the console message to process + * + */ + fun collectWebData(tag: String, message: String) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/Clock.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/Clock.kt new file mode 100644 index 0000000000..27920f95da --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/Clock.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.clock + +internal fun interface Clock { + + /** + * Returns the current milliseconds from epoch. + */ + fun now(): Long +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/NormalizedIntervalClock.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/NormalizedIntervalClock.kt new file mode 100644 index 0000000000..7ec1255712 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/NormalizedIntervalClock.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk.clock + +/** + * A clock which uses [android.os.SystemClock.elapsedRealtime] that is normalized + * to the first [System.currentTimeMillis] value. + * + * This is useful when it is necessary to perform interval timing but the results must be + * sent to the API in a way that matches the device time. + */ +internal class NormalizedIntervalClock(systemClock: SystemClock) : Clock { + private val baseline: Long + + init { + baseline = systemClock.now() - android.os.SystemClock.elapsedRealtime() + } + + override fun now(): Long = baseline + android.os.SystemClock.elapsedRealtime() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/SystemClock.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/SystemClock.kt new file mode 100644 index 0000000000..10f29b9a74 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/clock/SystemClock.kt @@ -0,0 +1,7 @@ +package io.embrace.android.embracesdk.clock + +internal class SystemClock : Clock { + override fun now(): Long { + return System.currentTimeMillis() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiClient.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiClient.kt new file mode 100644 index 0000000000..da030336a8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiClient.kt @@ -0,0 +1,178 @@ +package io.embrace.android.embracesdk.comms.api + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.util.zip.GZIPOutputStream + +/** + * Client for calling the Embrace API. This service handles all calls to the Embrace API. + * + * Sessions can be sent to either the production or development endpoint. The development + * endpoint shows sessions on the 'integration testing' screen within the dashboard, whereas + * the production endpoint sends sessions to 'recent sessions'. + * + * The development endpoint is only used if the build is a debug build, and if integration + * testing is enabled when calling [Embrace.start()]. + */ +internal class ApiClient @JvmOverloads constructor( + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger +) { + + companion object { + + /** + * The version of the API message format. + */ + const val MESSAGE_VERSION = 13 + + const val NO_HTTP_RESPONSE = -1 + } + + var timeoutMs = 60 * 1000 + + private fun setTimeouts(connection: EmbraceConnection) { + connection.setConnectTimeout(timeoutMs) + connection.setReadTimeout(timeoutMs) + } + + /** + * Executes a GET request with the ApiRequest object, returning the response from the server + * as a string. + */ + @Throws(IllegalStateException::class) + fun executeGet(request: ApiRequest): ApiResponse { + var connection: EmbraceConnection? = null + + try { + connection = request.toConnection() + setTimeouts(connection) + connection.connect() + return executeHttpRequest(connection) + } catch (ex: Throwable) { + throw IllegalStateException(ex.localizedMessage ?: "", ex) + } finally { + runCatching { + connection?.inputStream?.close() + } + } + } + + /** + * Posts a payload according to the ApiRequest parameter. The payload will be gzip compressed. + */ + fun post(request: ApiRequest, payload: ByteArray): String = rawPost(request, gzip(payload)) + + /** + * Posts a payload according to the ApiRequest parameter. The payload will not be gzip compressed. + */ + fun rawPost(request: ApiRequest, payload: ByteArray?): String { + logger.logDeveloper("ApiClient", request.httpMethod.toString() + " " + request.url) + logger.logDeveloper("ApiClient", "Request details: $request") + + var connection: EmbraceConnection? = null + return try { + connection = request.toConnection() + setTimeouts(connection) + if (payload != null) { + logger.logDeveloper("ApiClient", "Payload size: " + payload.size) + connection.outputStream?.write(payload) + connection.connect() + } + val response = executeHttpRequest(connection) + + // pre-existing behavior. handle this better in future. + if (response.statusCode != HttpURLConnection.HTTP_OK) { + @Suppress("UseCheckOrError") throw IllegalStateException("Failed to retrieve from Embrace server.") + } + response.body ?: "" + } catch (ex: Throwable) { + throw IllegalStateException(ex.localizedMessage ?: "", ex) + } finally { + runCatching { + connection?.inputStream?.close() + } + } + } + + /** + * Executes a HTTP call using the specified connection, returning the response from the + * server as a string. + */ + private fun executeHttpRequest(connection: EmbraceConnection): ApiResponse { + try { + val responseCode = readHttpResponseCode(connection) + val headers = readHttpResponseHeaders(connection) + return ApiResponse( + responseCode, + headers, + readResponseBodyAsString(connection.inputStream) + ) + } catch (exc: Throwable) { + throw IllegalStateException("Error occurred during HTTP request execution", exc) + } + } + + private fun readHttpResponseCode(connection: EmbraceConnection): Int { + var responseCode: Int? = null + try { + responseCode = connection.responseCode + logger.logDeveloper("ApiClient", "Response status: $responseCode") + } catch (ex: IOException) { + logger.logDeveloper("ApiClient", "Connection failed or unexpected response code") + } + return responseCode ?: NO_HTTP_RESPONSE + } + + private fun readHttpResponseHeaders(connection: EmbraceConnection): Map { + val headers = connection.headerFields?.mapValues { it.value.joinToString() } ?: emptyMap() + + headers.forEach { entry -> + logger.logDeveloper("ApiClient", "Response header: ${entry.key}: ${entry.value}") + } + return headers + } + + /** + * Reads an [InputStream] into a String. + * + * @param inputStream the input stream to read + * @return the string representation + */ + private fun readResponseBodyAsString(inputStream: InputStream?): String { + return try { + val body = InputStreamReader(inputStream).buffered().use { + it.readText() + } + logger.logDeveloper("ApiClient", "Successfully read response body.") + body + } catch (ex: IOException) { + logger.logDeveloper("ApiClient", "Failed to read response body.", ex) + throw IllegalStateException("Failed to read response body.", ex) + } + } + + /** + * Compresses a given byte array using the GZIP compression algorithm. + * + * @param bytes the byte array to compress + * @return the compressed byte array + */ + private fun gzip(bytes: ByteArray): ByteArray { + return try { + ByteArrayOutputStream().use { baos -> + GZIPOutputStream(baos).use { gzipStream -> + gzipStream.write(bytes) + gzipStream.finish() + } + baos.toByteArray() + } + } catch (ex: IOException) { + throw IllegalStateException("Failed to gzip payload.", ex) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiRequest.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiRequest.kt new file mode 100644 index 0000000000..3c23e9dfbf --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiRequest.kt @@ -0,0 +1,65 @@ +package io.embrace.android.embracesdk.comms.api + +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.network.http.HttpMethod +import java.io.IOException + +internal data class ApiRequest( + val contentType: String = "application/json", + + val userAgent: String = "Embrace/a/" + BuildConfig.VERSION_NAME, + + val contentEncoding: String? = null, + + val accept: String = "application/json", + + val acceptEncoding: String? = null, + + val appId: String? = null, + + val deviceId: String? = null, + + val eventId: String? = null, + + val logId: String? = null, + + val url: EmbraceUrl, + + val httpMethod: HttpMethod = HttpMethod.POST, + + val eTag: String? = null +) { + + fun getHeaders(): Map { + val headers = mutableMapOf( + "Accept" to accept, + "User-Agent" to userAgent, + "Content-Type" to contentType + ) + contentEncoding?.let { headers["Content-Encoding"] = it } + acceptEncoding?.let { headers["Accept-Encoding"] = it } + appId?.let { headers["X-EM-AID"] = it } + deviceId?.let { headers["X-EM-DID"] = it } + eventId?.let { headers["X-EM-SID"] = it } + logId?.let { headers["X-EM-LID"] = it } + eTag?.let { headers["If-None-Match"] = it } + return headers + } + + fun toConnection(): EmbraceConnection { + try { + val connection = url.openConnection() + + getHeaders().forEach { + connection.setRequestProperty(it.key, it.value) + } + connection.setRequestMethod(httpMethod.name) + if (httpMethod == HttpMethod.POST) { + connection.setDoOutput(true) + } + return connection + } catch (ex: IOException) { + throw IllegalStateException(ex.localizedMessage ?: "", ex) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponse.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponse.kt new file mode 100644 index 0000000000..6bf734ce97 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponse.kt @@ -0,0 +1,7 @@ +package io.embrace.android.embracesdk.comms.api + +internal data class ApiResponse( + val statusCode: Int?, + val headers: Map, + val body: T? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponseCache.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponseCache.kt new file mode 100644 index 0000000000..906ae24b48 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiResponseCache.kt @@ -0,0 +1,102 @@ +package io.embrace.android.embracesdk.comms.api + +import android.net.http.HttpResponseCache +import com.google.gson.stream.JsonReader +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.io.Closeable +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.net.CacheResponse +import java.net.URI + +/** + * Caches HTTP requests made via HttpUrlConnection using [HttpResponseCache]. This is + * currently only used to cache responses from the config endpoint, which contain etags in the + * response headers. + * + * This class therefore provides functions to retrieve the etag for any cached responses. This + * means the eTag can be set in the request header & we can avoid unnecessary work on the client + * & on the server. + */ +internal class ApiResponseCache @JvmOverloads constructor( + private val serializer: EmbraceSerializer, + cacheDirProvider: () -> File, + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger +) : Closeable { + + companion object { + private const val MAX_CACHE_SIZE_BYTES: Long = 2 * 1024 * 1024 // 2 MiB + private const val ETAG_HEADER = "ETag" + } + + @Volatile + private var cache: HttpResponseCache? = null + private val cacheDir: File by lazy { cacheDirProvider() } + private val lock = Object() + + private fun initializeIfNeeded() { + if (cache == null) { + synchronized(lock) { + if (cache == null) { + cache = try { + HttpResponseCache.install(cacheDir, MAX_CACHE_SIZE_BYTES) + } catch (exc: IOException) { + logger.logWarning("Failed to initialize HTTP cache.", exc) + null + } + } + } + } + } + + override fun close() { + cache?.flush() + } + + fun retrieveCachedConfig(url: String, request: ApiRequest): CachedConfig { + val cachedResponse = retrieveCacheResponse(url, request) + val obj = cachedResponse?.runCatching { + JsonReader(InputStreamReader(body).buffered()).use { + serializer.loadObject(it, RemoteConfig::class.java) + } + }?.getOrNull() + val eTag = cachedResponse?.let { retrieveETag(cachedResponse) } + return CachedConfig(obj, eTag) + } + + /** + * Retrieves the cache response for the given request, if any exists. + */ + private fun retrieveCacheResponse(url: String, request: ApiRequest): CacheResponse? { + initializeIfNeeded() + val obj = cache ?: return null + + return try { + val uri = URI.create(url) + val requestMethod = request.httpMethod.toString() + val headerFields = request.getHeaders().mapValues { listOf(it.value) } + obj.get(uri, requestMethod, headerFields) + } catch (exc: IOException) { + null + } + } + + /** + * Searches the cache to see whether a request has a cached response, and if so returns its etag. + */ + private fun retrieveETag(cacheResponse: CacheResponse): String? { + try { + val eTag = cacheResponse.headers[ETAG_HEADER] + if (!eTag.isNullOrEmpty()) { + return eTag[0] + } + } catch (exc: IOException) { + logger.logWarning("Failed to find ETag", exc) + } + return null + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiService.kt new file mode 100644 index 0000000000..c35d4bc28b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiService.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.comms.api + +import io.embrace.android.embracesdk.config.remote.RemoteConfig + +internal interface ApiService { + fun getConfig(): RemoteConfig? + fun getCachedConfig(): CachedConfig +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiUrlBuilder.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiUrlBuilder.kt new file mode 100644 index 0000000000..547c4bff58 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/ApiUrlBuilder.kt @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.comms.api + +import android.os.Build +import android.os.Debug +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.SdkEndpointBehavior +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger + +internal class ApiUrlBuilder( + private val configService: ConfigService, + private val metadataService: MetadataService, + private val enableIntegrationTesting: Boolean, + private val isDebug: Boolean +) { + + companion object { + private const val API_VERSION = 1 + private const val CONFIG_API_VERSION = 2 + } + + private val baseUrls: SdkEndpointBehavior + get() = configService.sdkEndpointBehavior + + private fun getConfigBaseUrl() = buildUrl(baseUrls.getConfig(getAppId()), CONFIG_API_VERSION, "config") + private fun getOperatingSystemCode() = Build.VERSION.SDK_INT.toString() + ".0.0" + + private fun getCoreBaseUrl(): String = if (isDebugBuild()) { + "${baseUrls.getDataDev(getAppId())}" + } else { + "${baseUrls.getData(getAppId())}" + } + + private fun getAppVersion(): String = metadataService.getAppVersionName() + + private fun getAppId() = metadataService.getAppId() + + private fun isDebugBuild(): Boolean { + return isDebug && enableIntegrationTesting && + (Debug.isDebuggerConnected() || Debug.waitingForDebugger()) + } + + private fun buildUrl(config: String, configApiVersion: Int, path: String): String { + return "$config/v$configApiVersion/$path" + } + + fun getConfigUrl(): String { + return "${getConfigBaseUrl()}?appId=${getAppId()}&osVersion=${getOperatingSystemCode()}" + + "&appVersion=${getAppVersion()}&deviceId=${metadataService.getDeviceId()}" + } + + fun getEmbraceUrlWithSuffix(suffix: String): String { + InternalStaticEmbraceLogger.logDeveloper( + "ApiUrlBuilder", + "getEmbraceUrlWithSuffix - suffix: $suffix" + ) + return "${getCoreBaseUrl()}/v$API_VERSION/log/$suffix" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/CachedConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/CachedConfig.kt new file mode 100644 index 0000000000..9845cedf55 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/CachedConfig.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.comms.api + +import io.embrace.android.embracesdk.config.remote.RemoteConfig + +internal class CachedConfig( + val config: RemoteConfig? = null, + val eTag: String? = null +) { + fun isValid() = config != null && eTag != null +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceApiService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceApiService.kt new file mode 100644 index 0000000000..7d21996c37 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceApiService.kt @@ -0,0 +1,83 @@ +package io.embrace.android.embracesdk.comms.api + +import com.google.gson.stream.JsonReader +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.network.http.HttpMethod +import java.io.StringReader +import java.net.HttpURLConnection + +internal class EmbraceApiService( + private val apiClientProvider: () -> ApiClient, + private val urlBuilder: ApiUrlBuilder, + private val serializer: EmbraceSerializer, + private val cachedConfigProvider: (url: String, request: ApiRequest) -> CachedConfig, + private val logger: InternalEmbraceLogger, +) : ApiService { + + /** + * Asynchronously gets the app's SDK configuration. + * + * These settings define app-specific settings, such as disabled log patterns, whether + * screenshots are enabled, as well as limits and thresholds. + * + * @return a future containing the configuration. + */ + @Throws(IllegalStateException::class) + override fun getConfig(): RemoteConfig? { + val url = urlBuilder.getConfigUrl() + var request = prepareConfigRequest(url) + val cachedResponse = cachedConfigProvider(url, request) + + if (cachedResponse.isValid()) { // only bother if we have a useful response. + request = request.copy(eTag = cachedResponse.eTag) + } + val apiClient = apiClientProvider.invoke() + val response = apiClient.executeGet(request) + return handleRemoteConfigResponse(response, cachedResponse.config) + } + + override fun getCachedConfig(): CachedConfig { + val url = urlBuilder.getConfigUrl() + val request = prepareConfigRequest(url) + return cachedConfigProvider(url, request) + } + + private fun prepareConfigRequest(url: String) = ApiRequest( + contentType = "application/json", + userAgent = "Embrace/a/" + BuildConfig.VERSION_NAME, + accept = "application/json", + url = EmbraceUrl.getUrl(url), + httpMethod = HttpMethod.GET, + ) + + private fun handleRemoteConfigResponse( + response: ApiResponse, + cachedConfig: RemoteConfig? + ): RemoteConfig? { + return when (response.statusCode) { + HttpURLConnection.HTTP_OK -> { + logger.logInfo("Fetched new config successfully.") + val jsonReader = JsonReader(StringReader(response.body)) + serializer.loadObject(jsonReader, RemoteConfig::class.java) + } + + HttpURLConnection.HTTP_NOT_MODIFIED -> { + logger.logInfo("Confirmed config has not been modified.") + cachedConfig + } + + ApiClient.NO_HTTP_RESPONSE -> { + logger.logInfo("Failed to fetch config (no response).") + null + } + + else -> { + logger.logWarning("Unexpected status code when fetching config: ${response.statusCode}") + null + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnection.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnection.java new file mode 100644 index 0000000000..dbc65ed747 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnection.java @@ -0,0 +1,64 @@ +package io.embrace.android.embracesdk.comms.api; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ProtocolException; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLSocketFactory; + +interface EmbraceConnection { + + boolean isHttps(); + + void setRequestMethod(@NonNull String method) throws ProtocolException; + + void setDoOutput(@NonNull Boolean doOutput); + + void setConnectTimeout(@NonNull Integer timeout); + + void setReadTimeout(@NonNull Integer readTimeout); + + void setRequestProperty(@NonNull String key, @Nullable String value); + + @NonNull + @SuppressWarnings("AbbreviationAsWordInNameCheck") + EmbraceUrl getURL(); + + @Nullable + String getRequestMethod(); + + @Nullable + String getHeaderField(@NonNull String key); + + @Nullable + Map> getHeaderFields(); + + @Nullable + OutputStream getOutputStream() throws IOException; + + @Nullable + InputStream getInputStream() throws IOException; + + @Nullable + InputStream getErrorStream(); + + void connect() throws IOException; + + int getResponseCode() throws IOException; + + @Nullable + String getResponseMessage() throws IOException; + + @SuppressWarnings("AbbreviationAsWordInNameCheck") + void setSSLSocketFactory(@Nullable SSLSocketFactory factory); + + @Nullable + @SuppressWarnings("AbbreviationAsWordInNameCheck") + SSLSocketFactory getSSLSocketFactory(); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl.java new file mode 100644 index 0000000000..b6b79a8fa8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceConnectionImpl.java @@ -0,0 +1,118 @@ +package io.embrace.android.embracesdk.comms.api; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +class EmbraceConnectionImpl implements EmbraceConnection { + private HttpURLConnection httpUrlConnection; + private EmbraceUrl url; + private Integer responseCode = null; + + public EmbraceConnectionImpl(HttpURLConnection embraceConnection, EmbraceUrl url) { + this.httpUrlConnection = embraceConnection; + this.url = url; + } + + + @Override + public boolean isHttps() { + return httpUrlConnection instanceof HttpsURLConnection; + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + httpUrlConnection.setRequestMethod(method); + } + + @Override + public void setDoOutput(Boolean doOutput) { + httpUrlConnection.setDoOutput(doOutput); + } + + @Override + public void setConnectTimeout(Integer timeout) { + httpUrlConnection.setConnectTimeout(timeout); + } + + @Override + public void setReadTimeout(Integer readTimeout) { + httpUrlConnection.setReadTimeout(readTimeout); + } + + @Override + public void setRequestProperty(String key, String value) { + httpUrlConnection.setRequestProperty(key, value); + } + + @Override + public EmbraceUrl getURL() { + return url; + } + + @Override + public String getRequestMethod() { + return httpUrlConnection.getRequestMethod(); + } + + @Override + public String getHeaderField(String key) { + return httpUrlConnection.getHeaderField(key); + } + + @Override + public Map> getHeaderFields() { + return httpUrlConnection.getHeaderFields(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return httpUrlConnection.getOutputStream(); + } + + @Override + public InputStream getInputStream() throws IOException { + return httpUrlConnection.getInputStream(); + } + + @Override + public InputStream getErrorStream() { + return httpUrlConnection.getErrorStream(); + } + + @Override + public void connect() throws IOException { + httpUrlConnection.connect(); + } + + @Override + public int getResponseCode() throws IOException { + if (responseCode == null) { + return httpUrlConnection.getResponseCode(); + } else { + return responseCode; + } + } + + @Override + public String getResponseMessage() throws IOException { + return httpUrlConnection.getResponseMessage(); + } + + @Override + public void setSSLSocketFactory(SSLSocketFactory factory) { + ((HttpsURLConnection) httpUrlConnection).setSSLSocketFactory(factory); + } + + @Override + public SSLSocketFactory getSSLSocketFactory() { + return ((HttpsURLConnection) httpUrlConnection).getSSLSocketFactory(); + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrl.kt new file mode 100644 index 0000000000..ff183fcd71 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrl.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.comms.api + +import java.io.IOException +import java.net.MalformedURLException + +internal abstract class EmbraceUrl { + + override fun toString(): String { + return file + } + + @Throws(IOException::class) + abstract fun openConnection(): EmbraceConnection + abstract val file: String + + internal interface UrlFactory { + fun getInstance(url: String): EmbraceUrl + } + + companion object { + private var embraceUrlFactory: UrlFactory? = null + + @JvmStatic + fun setEmbraceUrlFactory(urlConstructor: UrlFactory?) { + embraceUrlFactory = urlConstructor + } + + @Throws(MalformedURLException::class) + @JvmStatic + fun getUrl(url: String): EmbraceUrl { + embraceUrlFactory?.let { + try { + return it.getInstance(url) + } catch (expected: Exception) { + } + } + return EmbraceUrlImpl(url) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter.kt new file mode 100644 index 0000000000..94c1c2cda8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlAdapter.kt @@ -0,0 +1,30 @@ +package io.embrace.android.embracesdk.comms.api + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +internal class EmbraceUrlAdapter : TypeAdapter() { + + override fun write(jsonWriter: JsonWriter, embraceUrl: EmbraceUrl?) { + jsonWriter.run { + beginObject() + name("url").value(embraceUrl?.toString()) + endObject() + } + } + + override fun read(jsonReader: JsonReader): EmbraceUrl? { + var embraceUrl: EmbraceUrl? = null + + jsonReader.beginObject() + while (jsonReader.hasNext()) { + if (jsonReader.nextName() == "url") { + embraceUrl = EmbraceUrl.getUrl(jsonReader.nextString()) + } + } + jsonReader.endObject() + + return embraceUrl + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlImpl.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlImpl.java new file mode 100644 index 0000000000..7edded54e5 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/api/EmbraceUrlImpl.java @@ -0,0 +1,50 @@ +package io.embrace.android.embracesdk.comms.api; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +class EmbraceUrlImpl extends EmbraceUrl { + private URL url; + + EmbraceUrlImpl(@NonNull String url) throws MalformedURLException { + this.url = new URL(url); + } + + EmbraceUrlImpl(@NonNull URL url) { + this.url = url; + } + + @NonNull + @Override + public EmbraceConnection openConnection() throws IOException { + return new EmbraceConnectionImpl((HttpURLConnection) url.openConnection(), this); + } + + @Override + public String getFile() { + return url.getFile(); + } + + @Override + public String toString() { + return url.toString(); + } + + @Override + public int hashCode() { + return url.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof EmbraceUrlImpl) { + return url.equals(((EmbraceUrlImpl) obj).url); + } + return url.equals(obj); + } + +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/CacheService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/CacheService.kt new file mode 100644 index 0000000000..0b839de6c3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/CacheService.kt @@ -0,0 +1,82 @@ +package io.embrace.android.embracesdk.comms.delivery + +/** + * Handles the caching of objects. + */ +internal interface CacheService { + /** + * Caches the specified object. + * + * @param name the name of the object to cache + * @param objectToCache the object to cache + * @param clazz the class of the object to cache + * @param the type of the object + */ + fun cacheObject(name: String, objectToCache: T, clazz: Class) + + /** + * Reads the specified object from the cache, if it exists. + * + * @param name the name of the object to read from the cache + * @param clazz the class of the cached object + * @param the type of the cached object + * @return optionally the object, if it can be read successfully + */ + fun loadObject(name: String, clazz: Class): T? + + /** + * Caches a byte array to disk. + * + * @param name the name of this cache in disk + * @param bytes the bytes to write + */ + fun cacheBytes(name: String, bytes: ByteArray?) + + /** + * Reads the bytes from a cached file, if it exists. + * + * @param name the name of the file to read + * @return the byte array, if it can be read successfully + */ + fun loadBytes(name: String): ByteArray? + + /** + * Delete a file from the cache + * + * @param name the name of the file to delete + */ + fun deleteFile(name: String): Boolean + + /** + * Deletes the specified object from the cache. + * + * @param name the name of the object to delete + * @return true if the file was successfully deleted, false otherwise + */ + fun deleteObject(name: String): Boolean + + /** + * Deletes the objects which names match with the specified regex from the cache. + * + * @param regex the regex to match to the name of the object to delete + * @return true if the files were successfully deleted, false otherwise + */ + fun deleteObjectsByRegex(regex: String): Boolean + + /** + * Moves the object using the current name to a new file called name. + * + * @param src the source file name + * @param dst the destination file name + * @return true if the file was successfully moved, false otherwise + */ + fun moveObject(src: String, dst: String): Boolean + + /** + * Get file names in cache that start with a given prefix. + * + * @param prefix start of the file names to look for + * @return list of file names + */ + fun listFilenamesByPrefix(prefix: String): List? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager.kt new file mode 100644 index 0000000000..9b659400be --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryCacheManager.kt @@ -0,0 +1,268 @@ +package io.embrace.android.embracesdk.comms.delivery + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.BackgroundActivityMessage +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.session.SessionMessageSerializer +import java.io.Closeable +import java.util.concurrent.ExecutorService + +internal class DeliveryCacheManager( + private val cacheService: CacheService, + private val executorService: ExecutorService, + private val logger: InternalEmbraceLogger, + private val clock: Clock, + private val serializer: EmbraceSerializer +) : Closeable { + + companion object { + /** + * File names for all cached sessions start with this prefix + */ + private const val SESSION_FILE_PREFIX = "last_session" + + /** + * Full file name for a session saved with a previous version of the SDK. Note that to + * preserve backward compatibility, SESSION_FILE_PREFIX must be the start of OLD_VERSION_FILE_NAME + */ + private const val OLD_VERSION_FILE_NAME = "last_session.json" + + /** + * File name to cache JVM crash information + */ + private const val CRASH_FILE_NAME = "crash.json" + + /** + * File names for failed api calls + */ + private const val FAILED_API_CALLS_FILE_NAME = "failed_api_calls.json" + + @VisibleForTesting + const val MAX_SESSIONS_CACHED = 64 + + private const val TAG = "DeliveryCacheManager" + } + + private val sessionMessageSerializer by lazy { + SessionMessageSerializer(serializer) + } + + // The session id is used as key for this map + // This list is initialized when getAllCachedSessions() is called. + private val cachedSessions = mutableMapOf() + + fun saveSession(sessionMessage: SessionMessage): ByteArray? { + if (cachedSessions.size >= MAX_SESSIONS_CACHED) { + deleteOldestSessions() + } + val sessionBytes: ByteArray = sessionMessageSerializer.serialize(sessionMessage).toByteArray() + saveBytes(sessionMessage.session.sessionId, sessionBytes) + return sessionBytes + } + + fun loadSession(sessionId: String): SessionMessage? { + cachedSessions[sessionId]?.let { cachedSession -> + return loadSession(cachedSession) + } + logger.logError("Session $sessionId is not in cache") + return null + } + + fun loadSessionBytes(sessionId: String): ByteArray? { + cachedSessions[sessionId]?.let { cachedSession -> + return executorService.submit { loadPayload(cachedSession.filename) }.get() + } + logger.logError("Session $sessionId is not in cache") + return null + } + + fun deleteSession(sessionId: String) { + cachedSessions[sessionId]?.let { cachedSession -> + executorService.submit { + try { + cacheService.deleteFile(cachedSession.filename) + cachedSessions.remove(sessionId) + } catch (ex: Exception) { + logger.logError("Could not remove session from cache: $sessionId") + } + } + } + } + + fun getAllCachedSessionIds(): List { + val allSessions = cacheService.listFilenamesByPrefix(SESSION_FILE_PREFIX) + allSessions?.forEach { filename -> + if (filename == OLD_VERSION_FILE_NAME) { + // If a cached session from a previous version of the SDK is found, + // load and save it again using the new naming schema + val previousSdkSession = + cacheService.loadObject(filename, SessionMessage::class.java) + previousSdkSession?.also { + // When saved, the new session filename is also added to cachedSessions + saveSession(it) + executorService.submit { + cacheService.deleteFile(filename) + } + } + } + val values = filename.split('.') + if (values.size != 4) { + logger.logError("Unrecognized cached file: $filename") + return@forEach + } + val timestamp = values[1].toLongOrNull() + timestamp?.also { + val sessionId = values[2] + cachedSessions[sessionId] = CachedSession(sessionId, it) + } ?: run { + logger.logError("Could not parse timestamp ${values[2]}") + } + } + return cachedSessions.keys.toList() + } + + fun saveBackgroundActivity(backgroundActivityMessage: BackgroundActivityMessage): ByteArray? { + val baId = backgroundActivityMessage.backgroundActivity.sessionId + val baBytes = serializer.bytesFromPayload( + backgroundActivityMessage, + BackgroundActivityMessage::class.java + ) + // Do not add background activities to disk if we are over the limit + if (cachedSessions.size < MAX_SESSIONS_CACHED || cachedSessions.containsKey(baId)) { + baBytes?.let { saveBytes(baId, it) } + } + return baBytes + } + + fun loadBackgroundActivity(backgroundActivityId: String): ByteArray? { + cachedSessions[backgroundActivityId]?.let { cachedSession -> + return executorService.submit { loadPayload(cachedSession.filename) }.get() + } + logger.logWarning("Background activity $backgroundActivityId is not in cache") + return null + } + + fun saveCrash(crash: EventMessage) { + cacheService.cacheObject(CRASH_FILE_NAME, crash, EventMessage::class.java) + } + + fun loadCrash(): EventMessage? { + return cacheService.loadObject(CRASH_FILE_NAME, EventMessage::class.java) + } + + fun deleteCrash() { + cacheService.deleteFile(CRASH_FILE_NAME) + } + + fun savePayload(bytes: ByteArray): String { + val name = "payload_" + Uuid.getEmbUuid() + executorService.submit { + cacheService.cacheBytes(name, bytes) + } + return name + } + + fun loadPayload(name: String): ByteArray? { + return cacheService.loadBytes(name) + } + + fun deletePayload(name: String) { + executorService.submit { + cacheService.deleteFile(name) + } + } + + fun saveFailedApiCalls(failedApiCalls: DeliveryFailedApiCalls) { + logger.logDeveloper(TAG, "Saving failed api calls") + serializer.bytesFromPayload( + failedApiCalls, + DeliveryFailedApiCalls::class.java + )?.let { + executorService.submit { + cacheService.cacheBytes(FAILED_API_CALLS_FILE_NAME, it) + } + } + } + + fun loadFailedApiCalls(): DeliveryFailedApiCalls { + logger.logDeveloper(TAG, "Loading failed api calls") + val cached = executorService.submit { + cacheService.loadObject(FAILED_API_CALLS_FILE_NAME, DeliveryFailedApiCalls::class.java) + }.get() + return if (cached != null) { + cached + } else { + logger.logDeveloper(TAG, "No failed api calls cache found") + DeliveryFailedApiCalls() + } + } + + override fun close() { + } + + private fun loadSession(cachedSession: CachedSession): SessionMessage? { + return executorService.submit { + try { + val sessionMessage = cacheService.loadObject( + cachedSession.filename, + SessionMessage::class.java + ) + if (sessionMessage != null) { + logger.logDeveloper(TAG, "Successfully fetched previous session message.") + return@submit sessionMessage + } + } catch (ex: Exception) { + logger.logError("Failed to load previous cached session message", ex) + } + null + }.get() + } + + private fun deleteOldestSessions() { + cachedSessions.values + .sortedBy { it.timestamp } + .take(cachedSessions.size - MAX_SESSIONS_CACHED + 1) + .forEach { deleteSession(it.sessionId) } + } + + private fun saveBytes(sessionId: String, bytes: ByteArray) { + executorService.submit { + try { + val cachedSession = cachedSessions.getOrElse(sessionId) { + CachedSession( + sessionId, + clock.now() + ) + } + cacheService.cacheBytes(cachedSession.filename, bytes) + if (!cachedSessions.containsKey(cachedSession.sessionId)) { + cachedSessions[cachedSession.sessionId] = cachedSession + } + logger.logDeveloper(TAG, "Session message successfully cached.") + } catch (ex: Exception) { + logger.logError("Failed to cache current active session", ex) + } + } + } + + @VisibleForTesting + data class CachedSession( + val filename: String, + val sessionId: String, + val timestamp: Long? + ) { + constructor( + sessionId: String, + timestamp: Long + ) : this( + "$SESSION_FILE_PREFIX.$timestamp.$sessionId.json", + sessionId, + timestamp + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls.kt new file mode 100644 index 0000000000..5d6046171f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryFailedApiCalls.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.comms.delivery + +import io.embrace.android.embracesdk.comms.api.ApiRequest +import java.util.concurrent.ConcurrentLinkedQueue + +internal class DeliveryFailedApiCalls : ConcurrentLinkedQueue() + +internal data class DeliveryFailedApiCall(val apiRequest: ApiRequest, val cachedPayload: String) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager.kt new file mode 100644 index 0000000000..5bc90c1cfa --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManager.kt @@ -0,0 +1,524 @@ +package io.embrace.android.embracesdk.comms.delivery + +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityListener +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.comms.api.ApiRequest +import io.embrace.android.embracesdk.comms.api.ApiUrlBuilder +import io.embrace.android.embracesdk.comms.api.EmbraceUrl +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalErrorLogger +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.embrace.android.embracesdk.payload.AppExitInfoData +import io.embrace.android.embracesdk.payload.BlobMessage +import io.embrace.android.embracesdk.payload.BlobSession +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.NetworkEvent +import io.embrace.android.embracesdk.utils.exceptions.Unchecked +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import kotlin.math.max + +private const val TAG = "DeliveryNetworkManager" +private const val CRASH_TIMEOUT = 1L // Seconds to wait before timing out when sending a crash +private const val RETRY_PERIOD = 120L // In seconds +private const val MAX_EXPONENTIAL_RETRY_PERIOD = 3600 // In seconds +private const val MAX_FAILED_CALLS = 200 // Max number of failed calls that will be cached for retry + +internal class DeliveryNetworkManager( + private val metadataService: MetadataService, + private val urlBuilder: ApiUrlBuilder, + private val apiClient: ApiClient, + private val cacheManager: DeliveryCacheManager, + private val logger: InternalEmbraceLogger, + private val configService: ConfigService, + private val scheduledExecutorService: ScheduledExecutorService, + networkConnectivityService: NetworkConnectivityService, + private val serializer: EmbraceSerializer, + private val userService: UserService +) : DeliveryServiceNetwork, NetworkConnectivityListener { + + private val retryQueue: DeliveryFailedApiCalls by lazy { cacheManager.loadFailedApiCalls() } + + private var lastRetryTask: ScheduledFuture<*>? = null + + private var lastNetworkStatus: NetworkStatus = NetworkStatus.UNKNOWN + + init { + logger.logDeveloper(TAG, "start") + networkConnectivityService.addNetworkConnectivityListener(this) + lastNetworkStatus = networkConnectivityService.getCurrentNetworkStatus() + scheduledExecutorService.submit( + this::scheduleFailedApiCallsRetry + ) + } + + override fun onNetworkConnectivityStatusChanged(status: NetworkStatus) { + lastNetworkStatus = status + logger.logDebug("Network status is now: $lastNetworkStatus") + when (status) { + NetworkStatus.UNKNOWN, + NetworkStatus.WIFI, + NetworkStatus.WAN -> { + scheduleFailedApiCallsRetry() + } + + NetworkStatus.NOT_REACHABLE -> { + synchronized(this) { + lastRetryTask?.let { task -> + if (task.cancel(false)) { + logger.logDebug("Failed Calls Retry Action was stopped because there is no connection. ") + lastRetryTask = null + } else { + logger.logError("Failed Calls Retry Action could not be stopped.") + } + } + } + } + } + } + + /** + * Sends a log message to the API. + * + * @param eventMessage the event message containing the log entry + * @return a future containing the response body from the server + */ + override fun sendLogs(eventMessage: EventMessage) { + logger.logDeveloper(TAG, "sendLogs") + checkNotNull(eventMessage.event) { "event must be set" } + val event = eventMessage.event + checkNotNull(event.type) { "event type must be set" } + checkNotNull(event.eventId) { "event ID must be set" } + val url = Unchecked.wrap { + EmbraceUrl.getUrl( + urlBuilder.getEmbraceUrlWithSuffix("logging") + ) + } + val abbreviation = event.type.abbreviation + val logIdentifier = abbreviation + ":" + event.messageId + val request: ApiRequest = eventBuilder(url).copy(logId = logIdentifier) + postEvent(eventMessage, request) + } + + /** + * Sends an Application Exit Info (AEI) blob message to the API. + * + * @param appExitInfoData the Application exit info list to send + * @return a future containing the response body from the server + */ + override fun sendAEIBlob(appExitInfoData: List) { + logger.logDeveloper(TAG, "send BlobMessage") + val url = Unchecked.wrap { + EmbraceUrl.getUrl( + urlBuilder.getEmbraceUrlWithSuffix("blobs") + ) + } + val request: ApiRequest = eventBuilder(url).copy( + deviceId = metadataService.getDeviceId(), + appId = metadataService.getAppId(), + url = url, + httpMethod = HttpMethod.POST, + contentEncoding = "gzip" + ) + + val blob = BlobMessage( + metadataService.getAppInfo(), + appExitInfoData, + metadataService.getDeviceInfo(), + BlobSession(metadataService.activeSessionId), + userService.getUserInfo() + ) + + postAEIBlob(blob, request) + } + + /** + * Sends a network event to the API. + * + * @param networkEvent the event containing the network call information + */ + override fun sendNetworkCall(networkEvent: NetworkEvent) { + logger.logDeveloper(TAG, "sendNetworkCall") + + val url = Unchecked.wrap { + EmbraceUrl.getUrl( + urlBuilder.getEmbraceUrlWithSuffix("network") + ) + } + val abbreviation = EmbraceEvent.Type.NETWORK_LOG.abbreviation + val networkIdentifier = "$abbreviation:${networkEvent.eventId}" + + logger.logDeveloper(TAG, "network call to: $url - abbreviation: $abbreviation") + + val request: ApiRequest = eventBuilder(url).copy(logId = networkIdentifier) + postNetworkEvent(networkEvent, request) + } + + /** + * Sends an event to the API. + * + * @param eventMessage the event message containing the event + */ + override fun sendEvent(eventMessage: EventMessage) { + postEvent(eventMessage, createRequest(eventMessage)) + } + + /** + * Sends an event to the API and waits for the request to be completed + * + * @param eventMessage the event message containing the event + */ + override fun sendEventAndWait(eventMessage: EventMessage) { + postEvent(eventMessage, createRequest(eventMessage))?.get() + } + + /** + * Sends a crash event to the API and reschedules it if the request times out + * + * @param crash the event message containing the crash + */ + override fun sendCrash(crash: EventMessage) { + val request = createRequest(crash) + try { + postEvent(crash, request) { cacheManager.deleteCrash() }?.get( + CRASH_TIMEOUT, + TimeUnit.SECONDS + ) + } catch (e: Exception) { + logger.logError("The crash report request has timed out.") + } + } + + fun sendSession(sessionPayload: ByteArray, onComplete: (() -> Unit)?): Future<*> { + logger.logDeveloper(TAG, "sendSession") + val url = Unchecked.wrap { + EmbraceUrl.getUrl( + urlBuilder.getEmbraceUrlWithSuffix("sessions") + ) + } + val request: ApiRequest = eventBuilder(url).copy( + deviceId = metadataService.getDeviceId(), + appId = metadataService.getAppId(), + url = url, + httpMethod = HttpMethod.POST, + contentEncoding = "gzip" + ) + + return postOnExecutor(sessionPayload, request, true, onComplete) + } + + /** + * Returns true if there is an active pending retry task + */ + fun isRetryTaskActive(): Boolean = + lastRetryTask?.let { task -> + !task.isCancelled && !task.isDone + } ?: false + + /** + * Returns the number of failed API calls that will be retried + */ + fun pendingRetriesCount() = retryQueue.size + + private fun createRequest(eventMessage: EventMessage): ApiRequest { + logger.logDeveloper(TAG, "sendEvent") + checkNotNull(eventMessage.event) { "event must be set" } + val event = eventMessage.event + logger.logDeveloper(TAG, "sendEvent - event: " + event.name) + logger.logDeveloper(TAG, "sendEvent - event: " + event.type) + checkNotNull(event.type) { "event type must be set" } + checkNotNull(event.eventId) { "event ID must be set" } + val url = Unchecked.wrap { + EmbraceUrl.getUrl( + urlBuilder.getEmbraceUrlWithSuffix("events") + ) + } + val abbreviation = event.type.abbreviation + val eventIdentifier: String = if (event.type == EmbraceEvent.Type.CRASH) { + createCrashActiveEventsHeader(abbreviation, event.activeEventIds) + } else { + abbreviation + ":" + event.eventId + } + return eventBuilder(url).copy(eventId = eventIdentifier) + } + + private fun postEvent(eventMessage: EventMessage, request: ApiRequest): Future<*>? { + return postEvent(eventMessage, request, null) + } + + private fun verifyDeviceInfo(eventMessage: EventMessage) { + if (eventMessage.deviceInfo == null) { + logger.logError( + eventMessage.event.name + "device Info null", + InternalErrorLogger.IntegrationModeException(eventMessage.event.name + ": No deviceInfo") + ) + } + } + + private fun verifyAppInfo(eventMessage: EventMessage) { + if (eventMessage.appInfo == null) { + logger.logError( + eventMessage.event.name + "app Info null", + InternalErrorLogger.IntegrationModeException(eventMessage.event.name + ": No appInfo") + ) + } + } + + private fun verifyNativeCrashSymbols(eventMessage: EventMessage) { + if (eventMessage.event.type == EmbraceEvent.Type.CRASH && eventMessage.nativeCrash != null) { + if (eventMessage.nativeCrash.symbols.isNullOrEmpty()) { + logger.logError( + "No symbols for native crash: " + eventMessage.nativeCrash.id, + InternalErrorLogger.IntegrationModeException("No symbols for native crash") + ) + } + + if (eventMessage.nativeCrash.errors.isNullOrEmpty()) { + logger.logError( + "No NativeCrashData error data for crash id: " + eventMessage.nativeCrash.id, + InternalErrorLogger.IntegrationModeException("No NativeCrashData error data") + ) + } + } + } + + private fun postEvent( + eventMessage: EventMessage, + request: ApiRequest, + onComplete: (() -> Unit)? + ): Future<*>? { + val bytes = serializer.bytesFromPayload(eventMessage, EventMessage::class.java) + + if (configService.sdkModeBehavior.isIntegrationModeEnabled()) { + verifyDeviceInfo(eventMessage) + verifyAppInfo(eventMessage) + verifyNativeCrashSymbols(eventMessage) + } + + bytes?.let { + logger.logDeveloper(TAG, "Post event") + return postOnExecutor(it, request, true, onComplete) + } + logger.logError("Failed to serialize event") + return null + } + + private fun postNetworkEvent( + event: NetworkEvent, + request: ApiRequest + ): Future<*>? { + val bytes = serializer.bytesFromPayload(event, NetworkEvent::class.java) + + bytes?.let { + logger.logDeveloper(TAG, "Post Network Event") + return postOnExecutor(it, request, true, null) + } + logger.logError("Failed to serialize event") + return null + } + + private fun postAEIBlob( + blob: BlobMessage, + request: ApiRequest + ): Future<*>? { + val bytes = serializer.bytesFromPayload(blob, BlobMessage::class.java) + + bytes?.let { + logger.logDeveloper(TAG, "Post AEI Blob message") + return postOnExecutor(it, request, true, null) + } + logger.logError("Failed to serialize event") + return null + } + + private fun postOnExecutor( + payload: ByteArray, + request: ApiRequest, + compress: Boolean, + onComplete: (() -> Any)? + ): Future<*> { + return scheduledExecutorService.submit { + try { + if (lastNetworkStatus != NetworkStatus.NOT_REACHABLE) { + if (compress) { + apiClient.post(request, payload) + } else { + apiClient.rawPost(request, payload) + } + } else { + scheduleForRetry(request, payload) + logger.logWarning("No connection available. Request was queued to retry later.") + } + } catch (ex: Exception) { + logger.logWarning("Failed to post Embrace API call. Will retry.", ex) + scheduleForRetry(request, payload) + throw ex + } finally { + onComplete?.invoke() + } + } + } + + private fun eventBuilder(url: EmbraceUrl): ApiRequest { + logger.logDeveloper(TAG, "eventBuilder") + return ApiRequest( + url = url, + httpMethod = HttpMethod.POST, + appId = metadataService.getAppId(), + deviceId = metadataService.getDeviceId(), + contentEncoding = "gzip" + ) + } + + /** + * Crashes are sent with a header containing the list of active stories. + * + * @param abbreviation the abbreviation for the event type + * @param eventIds the list of story IDs + * @return the header + */ + private fun createCrashActiveEventsHeader( + abbreviation: String, + eventIds: List? + ): String { + logger.logDeveloper(TAG, "createCrashActiveEventsHeader") + val stories = eventIds?.joinToString(",") ?: "" + return "$abbreviation:$stories" + } + + private fun scheduleForRetry(request: ApiRequest, payload: ByteArray) { + logger.logDeveloper(TAG, "Scheduling api call for retry") + if (pendingRetriesCount() < MAX_FAILED_CALLS) { + val scheduleJob = retryQueue.isEmpty() + val cachedPayloadName = cacheManager.savePayload(payload) + val failedApiCall = DeliveryFailedApiCall(request, cachedPayloadName) + retryQueue.add(failedApiCall) + cacheManager.saveFailedApiCalls(retryQueue) + + // By default there are no scheduled retry jobs pending. If the retry queue was initially empty, try to schedule a retry. + if (scheduleJob) { + scheduleFailedApiCallsRetry(RETRY_PERIOD) + } + } + } + + /** + * Return true if the conditions are met that a retry should be scheduled + */ + private fun shouldScheduleRetry(): Boolean { + return !isRetryTaskActive() && retryQueue.isNotEmpty() + } + + /** + * Schedules an action to retry failed API calls. If the retry doesn't send all the failed API requests, it will recursively schedule + * itself with an exponential backoff delay, starting with [RETRY_PERIOD], doubling after that until + * [MAX_EXPONENTIAL_RETRY_PERIOD] is reached, after which case it stops trying until the next cold start. + */ + private fun scheduleFailedApiCallsRetry(delayInSeconds: Long = 0L) { + try { + synchronized(this) { + if (shouldScheduleRetry()) { + lastRetryTask = scheduledExecutorService.schedule( + { + var noFailedRetries = true + if (lastNetworkStatus != NetworkStatus.NOT_REACHABLE) { + try { + logger.logInfo("Retrying failed API calls") + logger.logDeveloper(TAG, "Retrying failed API calls") + val retries = pendingRetriesCount() + repeat(retries) { + retryQueue.poll()?.let { failedApiCall -> + val callSucceeded = retryFailedApiCall(failedApiCall) + if (callSucceeded) { + // if the retry succeeded, save the modified queue in cache. + cacheManager.saveFailedApiCalls(retryQueue) + } else { + // if the retry failed, add the call back to the queue. + retryQueue.add(failedApiCall) + noFailedRetries = false + } + } + } + } catch (ex: Exception) { + logger.logDebug("Error when retrying failed API call", ex) + } + if (retryQueue.isNotEmpty()) { + scheduledExecutorService.submit { + scheduleNextFailedApiCallsRetry( + noFailedRetries, + delayInSeconds + ) + } + } + } else { + logger.logInfo( + "Did not retry network calls as scheduled because the network is not reachable" + ) + } + }, + delayInSeconds, + TimeUnit.SECONDS + ) + logger.logInfo( + "Scheduled failed API calls to retry ${if (delayInSeconds == 0L) "now" else "in $delayInSeconds seconds"}" + ) + } + } + } catch (e: RejectedExecutionException) { + // This happens if the executor has shutdown previous to the schedule call + logger.logError("Cannot schedule retry failed calls.", e) + } + } + + /** + * Executes the network call for a DeliveryFailedApiCall. + */ + private fun retryFailedApiCall(call: DeliveryFailedApiCall): Boolean { + val payload = cacheManager.loadPayload(call.cachedPayload) + if (payload != null) { + try { + logger.logDeveloper(TAG, "Retrying failed API call") + apiClient.post(call.apiRequest, payload) + cacheManager.deletePayload(call.cachedPayload) + } catch (ex: Exception) { + logger.logDeveloper( + TAG, + "retried call but fail again, scheduling to retry later", + ex + ) + return false + } + } else { + logger.logError("Could not retrieve cached api payload") + // If payload is null, the file could have been removed. + // We don't need to retry sending in the future as we'd get the same result. + // That's the reason for returning true. + } + return true + } + + /** + * Schedules the next call to retry sending the failed_api_calls again. The delay will be extended if the previous retry yielded + * at least one failed request. + */ + private fun scheduleNextFailedApiCallsRetry(noFailedRetries: Boolean, delay: Long) { + val nextDelay = if (noFailedRetries) { + RETRY_PERIOD + } else { + // if a network call failed, the retries will use exponential backoff + max(RETRY_PERIOD, delay * 2) + } + if (nextDelay <= MAX_EXPONENTIAL_RETRY_PERIOD) { + scheduleFailedApiCallsRetry(nextDelay) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryService.kt new file mode 100644 index 0000000000..b1ddf46f99 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/DeliveryService.kt @@ -0,0 +1,30 @@ +package io.embrace.android.embracesdk.comms.delivery + +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.AppExitInfoData +import io.embrace.android.embracesdk.payload.BackgroundActivityMessage +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.NetworkEvent +import io.embrace.android.embracesdk.payload.SessionMessage + +internal enum class SessionMessageState { START, END, END_WITH_CRASH } + +internal interface DeliveryService : DeliveryServiceNetwork { + fun saveSession(sessionMessage: SessionMessage) + fun sendSession(sessionMessage: SessionMessage, state: SessionMessageState) + fun sendCachedSessions(isNdkEnabled: Boolean, ndkService: NdkService, currentSession: String?) + fun saveCrash(crash: EventMessage) + fun sendEventAsync(eventMessage: EventMessage) + fun saveBackgroundActivity(backgroundActivityMessage: BackgroundActivityMessage) + fun sendBackgroundActivity(backgroundActivityMessage: BackgroundActivityMessage) + fun sendBackgroundActivities() +} + +internal interface DeliveryServiceNetwork { + fun sendLogs(eventMessage: EventMessage) + fun sendNetworkCall(networkEvent: NetworkEvent) + fun sendEvent(eventMessage: EventMessage) + fun sendEventAndWait(eventMessage: EventMessage) + fun sendCrash(crash: EventMessage) + fun sendAEIBlob(appExitInfoData: List) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceCacheService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceCacheService.kt new file mode 100644 index 0000000000..6b5d768a20 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceCacheService.kt @@ -0,0 +1,169 @@ +package io.embrace.android.embracesdk.comms.delivery + +import android.content.Context +import com.google.gson.stream.JsonReader +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import java.io.File +import java.io.FileNotFoundException +import java.util.regex.Pattern + +/** + * Handles the reading and writing of objects from the app's cache. + */ +internal class EmbraceCacheService( + context: Context, + private val serializer: EmbraceSerializer, + logger: InternalEmbraceLogger +) : CacheService { + + private val cacheDir: Lazy + private val logger: InternalEmbraceLogger + + override fun cacheBytes(name: String, bytes: ByteArray?) { + logger.logDeveloper(TAG, "Attempting to write bytes to $name") + if (bytes != null) { + val file = File(cacheDir.value, EMBRACE_PREFIX + name) + try { + file.writeBytes(bytes) + logger.logDeveloper(TAG, "Bytes cached") + } catch (ex: Exception) { + logger.logWarning("Failed to store cache object " + file.path, ex) + } + } else { + logger.logWarning("No bytes to save to file $name") + } + } + + override fun loadBytes(name: String): ByteArray? { + logger.logDeveloper(TAG, "Attempting to read bytes from $name") + val file = File(cacheDir.value, EMBRACE_PREFIX + name) + try { + return file.readBytes() + } catch (ex: FileNotFoundException) { + logger.logWarning("Cache file cannot be found " + file.path) + } catch (ex: Exception) { + logger.logWarning("Failed to read cache object " + file.path, ex) + } + return null + } + + /** + * Writes a file to the cache. Must be serializable by GSON. + * + * + * If writing the object to the cache fails, an exception is logged. + * + * @param name the name of the object to write + * @param objectToCache the object to write + * @param clazz the class of the object to write + * @param the type of the object to write + */ + override fun cacheObject(name: String, objectToCache: T, clazz: Class) { + logger.logDeveloper(TAG, "Attempting to cache object: $name") + val file = File(cacheDir.value, EMBRACE_PREFIX + name) + try { + file.bufferedWriter().use { + serializer.writeToFile(objectToCache, clazz, it) + } + } catch (ex: Exception) { + logger.logDebug("Failed to store cache object " + file.path, ex) + } + } + + override fun loadObject(name: String, clazz: Class): T? { + val file = File(cacheDir.value, EMBRACE_PREFIX + name) + try { + file.bufferedReader().use { bufferedReader -> + JsonReader(bufferedReader).use { jsonreader -> + jsonreader.isLenient = true + val obj = serializer.loadObject(jsonreader, clazz) + if (obj != null) { + return obj + } else { + logger.logDeveloper("EmbraceCacheService", "Object $name not found") + } + } + } + } catch (ex: FileNotFoundException) { + logger.logDebug("Cache file cannot be found " + file.path) + } catch (ex: Exception) { + logger.logDebug("Failed to read cache object " + file.path, ex) + } + return null + } + + override fun deleteFile(name: String): Boolean { + logger.logDeveloper("EmbraceCacheService", "Attempting to delete file from cache: $name") + val file = File(cacheDir.value, EMBRACE_PREFIX + name) + try { + return file.delete() + } catch (ex: Exception) { + logger.logDebug("Failed to delete cache object " + file.path) + } + return false + } + + override fun deleteObject(name: String): Boolean { + logger.logDeveloper("EmbraceCacheService", "Attempting to delete: $name") + val file = File(cacheDir.value, EMBRACE_PREFIX + name) + try { + return file.delete() + } catch (ex: Exception) { + logger.logDebug("Failed to delete cache object " + file.path) + } + return false + } + + override fun deleteObjectsByRegex(regex: String): Boolean { + logger.logDeveloper("EmbraceCacheService", "Attempting to delete objects by regex: $regex") + val pattern = Pattern.compile(regex) + var result = false + val filesInCache = cacheDir.value.listFiles() + if (filesInCache != null) { + for (cache in filesInCache) { + if (pattern.matcher(cache.name).find()) { + try { + result = cache.delete() + } catch (ex: Exception) { + logger.logDebug("Failed to delete cache object " + cache.path) + } + } else { + logger.logDeveloper("EmbraceCacheService", "Objects not found by regex") + } + } + } else { + logger.logDeveloper("EmbraceCacheService", "There are not files in cache") + } + return result + } + + override fun moveObject(src: String, dst: String): Boolean { + val cacheDir = cacheDir.value + val srcFile = File(cacheDir, EMBRACE_PREFIX + src) + if (!srcFile.exists()) { + logger.logDeveloper("EmbraceCacheService", "Source file doesn't exist: $src") + return false + } + val dstFile = File(cacheDir, EMBRACE_PREFIX + dst) + logger.logDeveloper("EmbraceCacheService", "Object moved from $src to $dst") + return srcFile.renameTo(dstFile) + } + + override fun listFilenamesByPrefix(prefix: String): List? { + val cacheDir = cacheDir.value + return cacheDir.listFiles { file -> + file.name.startsWith(EMBRACE_PREFIX + prefix) + }?.map { file -> file.name.substring(EMBRACE_PREFIX.length) } + } + + companion object { + private const val EMBRACE_PREFIX = "emb_" + private const val TAG = "EmbraceCacheService" + } + + init { + this.logger = logger + cacheDir = lazy { context.cacheDir } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService.kt new file mode 100644 index 0000000000..e2aff0f724 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryService.kt @@ -0,0 +1,306 @@ +package io.embrace.android.embracesdk.comms.delivery + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalErrorLogger +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.BackgroundActivityMessage +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.NativeCrashData +import io.embrace.android.embracesdk.payload.SessionMessage +import java.util.concurrent.ExecutorService +import java.util.concurrent.TimeUnit +import kotlin.math.abs + +internal class EmbraceDeliveryService( + private val cacheManager: DeliveryCacheManager, + private val networkManager: DeliveryNetworkManager, + private val cachedSessionsExecutorService: ExecutorService, + private val sendSessionsExecutorService: ExecutorService, + private val logger: InternalEmbraceLogger, + private val configService: ConfigService, +) : DeliveryService, DeliveryServiceNetwork by networkManager { + + companion object { + private const val TAG = "EmbraceDeliveryService" + + private const val SEND_SESSION_TIMEOUT = 1L + private const val CRASH_MAX_DIFF_WITH_SESSION_END = 7000 + } + + private val backgroundActivities by lazy { mutableSetOf() } + + /** + * Caches a generated session message, with performance information generated up to the current + * point. + */ + override fun saveSession(sessionMessage: SessionMessage) { + cacheManager.saveSession(sessionMessage) + } + + /** + * Caches and sends over the network a session message + * + * @param sessionMessage The session message to send + * @param state Whether this message is for the session start or end + */ + override fun sendSession(sessionMessage: SessionMessage, state: SessionMessageState) { + logger.logDeveloper(TAG, "Sending session message") + + sendSessionsExecutorService.submit { + logger.logDeveloper(TAG, "Sending session message - background job started") + val sessionBytes = cacheManager.saveSession(sessionMessage) + + sessionBytes?.also { session -> + logger.logDeveloper(TAG, "Serialized session message ready to be sent") + + try { + var onFinish: (() -> Unit)? = null + if (state == SessionMessageState.END || state == SessionMessageState.END_WITH_CRASH) { + onFinish = { cacheManager.deleteSession(sessionMessage.session.sessionId) } + if (configService.sdkModeBehavior.isIntegrationModeEnabled()) { + validateNetworkCalls(sessionMessage) + validateSessionTimestamps(sessionMessage) + } + } + + if (state == SessionMessageState.END_WITH_CRASH) { + // perform session request synchronously + networkManager.sendSession( + session, + onFinish + )[SEND_SESSION_TIMEOUT, TimeUnit.SECONDS] + logger.logDeveloper(TAG, "Session message sent.") + } else { + // perform session request asynchronously + networkManager.sendSession(session, onFinish) + logger.logDeveloper(TAG, "Session message queued to be sent.") + } + logger.logDeveloper( + TAG, + "Current session has been successfully removed from cache." + ) + } catch (ex: Exception) { + logger.logInfo( + "Failed to send session end message. Embrace will store the " + + "session message and attempt to deliver it at a future date." + ) + } + } + } + } + + /** + * Caches a background activity message + * + * @param backgroundActivityMessage The background activity message to cache + */ + override fun saveBackgroundActivity(backgroundActivityMessage: BackgroundActivityMessage) { + backgroundActivities.add(backgroundActivityMessage.backgroundActivity.sessionId) + cacheManager.saveBackgroundActivity(backgroundActivityMessage) + } + + /** + * Caches and sends a background activity message + * + * @param backgroundActivityMessage The background activity message to send + */ + override fun sendBackgroundActivity(backgroundActivityMessage: BackgroundActivityMessage) { + logger.logDeveloper(TAG, "Sending background activity message") + + sendSessionsExecutorService.submit { + logger.logDeveloper(TAG, "Sending background activity message - background job started") + val baBytes = cacheManager.saveBackgroundActivity(backgroundActivityMessage) + + baBytes?.also { backgroundActivity -> + logger.logDeveloper(TAG, "Serialized session message ready to be sent") + + try { + val onFinish: (() -> Unit) = + { cacheManager.deleteSession(backgroundActivityMessage.backgroundActivity.sessionId) } + networkManager.sendSession(backgroundActivity, onFinish) + logger.logDeveloper(TAG, "Session message queued to be sent.") + } catch (ex: Exception) { + logger.logInfo( + "Failed to send background activity message. Embrace will " + + "attempt to deliver it at a future date." + ) + } + } + } + } + + /** + * Sends cached background activities messages + * + */ + override fun sendBackgroundActivities() { + logger.logDeveloper(TAG, "Sending background activity message") + + sendSessionsExecutorService.submit { + backgroundActivities.forEach { backgroundActivityId -> + logger.logDeveloper(TAG, "Sending background activity message - background job started") + val baBytes = cacheManager.loadBackgroundActivity(backgroundActivityId) + + baBytes?.also { backgroundActivity -> + logger.logDeveloper(TAG, "Serialized session message ready to be sent") + + try { + val onFinish: () -> Unit = { cacheManager.deleteSession(backgroundActivityId) } + networkManager.sendSession(backgroundActivity, onFinish) + logger.logDeveloper(TAG, "Session message queued to be sent.") + } catch (ex: Exception) { + logger.logInfo( + "Failed to send background activity message. Embrace will " + + "attempt to deliver it at a future date." + ) + } + } + } + } + } + + private fun validateSessionTimestamps(sessionMessage: SessionMessage) { + val endTime = sessionMessage.session.endTime ?: 0 + if (endTime <= sessionMessage.session.startTime) { + logger.logError( + "Session end time less or equal to start time", + InternalErrorLogger.IntegrationModeException("wrong session start/end time") + ) + } + } + + private fun validateNetworkCalls(sessionMessage: SessionMessage) { + val p = sessionMessage.performanceInfo + val networkRequests = p?.networkRequests + if (networkRequests?.networkSessionV2?.requestCounts?.isEmpty() == true) { + logger.logError( + "Session with no network calls", + InternalErrorLogger.IntegrationModeException("No network calls") + ) + } + } + + override fun sendCachedSessions( + isNdkEnabled: Boolean, + ndkService: NdkService, + currentSession: String? + ) { + sendCachedCrash() + if (isNdkEnabled) { + sendCachedSessionsWithNdk(ndkService, currentSession) + } else { + sendCachedSessionsWithoutNdk(currentSession) + } + } + + /** + * Persist crash to disk so it can be sent on the next SDK start. + */ + override fun saveCrash(crash: EventMessage) { + cacheManager.saveCrash(crash) + } + + private fun sendCachedCrash() { + val crash = cacheManager.loadCrash() + crash?.let { + networkManager.sendCrash(it) + } + } + + private fun sendCachedSessionsWithoutNdk(currentSession: String?) { + cachedSessionsExecutorService.submit { + sendCachedSessions(cacheManager.getAllCachedSessionIds(), currentSession) + } + } + + private fun sendCachedSessionsWithNdk(ndkService: NdkService, currentSession: String?) { + cachedSessionsExecutorService.submit { + val allSessions = cacheManager.getAllCachedSessionIds() + logger.logDeveloper(TAG, "NDK enabled, checking for native crashes") + val nativeCrashData = ndkService.checkForNativeCrash() + if (nativeCrashData != null) { + addCrashDataToCachedSession(nativeCrashData) + } + sendCachedSessions(allSessions, currentSession) + } + } + + private fun addCrashDataToCachedSession(nativeCrashData: NativeCrashData) { + cacheManager.loadSession(nativeCrashData.sessionId) + ?.also { sessionMessage -> + // Create a new session message with the specified crash id + val newSessionMessage = + attachCrashToSession(nativeCrashData, sessionMessage) + // Replace the cached file for the corresponding session + cacheManager.saveSession(newSessionMessage) + } ?: run { + logger.logError( + "Could not find session with id ${nativeCrashData.sessionId} to " + + "add native crash" + ) + } + } + + private fun attachCrashToSession( + nativeCrashData: NativeCrashData, + sessionMessage: SessionMessage + ): SessionMessage { + logger.logDeveloper( + TAG, + "Attaching native crash ${nativeCrashData.nativeCrashId} to session ${sessionMessage.session.sessionId}" + ) + + if (configService.sdkModeBehavior.isIntegrationModeEnabled()) { + verifyCrashTimeStamp(nativeCrashData, sessionMessage) + } + + val session = sessionMessage.session.copy(crashReportId = nativeCrashData.nativeCrashId) + return sessionMessage.copy(session = session) + } + + // Crash must occur within 7 seconds of the session end + private fun verifyCrashTimeStamp( + nativeCrashData: NativeCrashData, + sessionMessage: SessionMessage + ) { + val endTime = sessionMessage.session.endTime ?: 0 + if (abs(nativeCrashData.timestamp - endTime) >= CRASH_MAX_DIFF_WITH_SESSION_END) { + logger.logError( + "Crash " + nativeCrashData.nativeCrashId + " happened outside 7 seconds of session end", + InternalErrorLogger.IntegrationModeException(nativeCrashData.nativeCrashId + " outside 7 secs range") + ) + } + } + + private fun sendCachedSessions(ids: List, currentSession: String?) { + ids.forEach { id -> + if (id != currentSession) { + try { + val payload = cacheManager.loadSessionBytes(id) + if (payload != null) { + if (configService.sdkModeBehavior.isIntegrationModeEnabled()) { + logger.logError( + "send cached sessions", + InternalErrorLogger.IntegrationModeException("Found cached session $id") + ) + } + + // The network requests will be executed sequentially in a single-threaded executor by the network manager + networkManager.sendSession(payload) { cacheManager.deleteSession(id) } + } else { + logger.logError("Session $id not found") + } + } catch (ex: Exception) { + logger.logError("Could not send cached session $id") + } + } + } + } + + override fun sendEventAsync(eventMessage: EventMessage) { + sendSessionsExecutorService.submit { + networkManager.sendEvent(eventMessage) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/NetworkStatus.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/NetworkStatus.kt new file mode 100644 index 0000000000..aabc122562 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/comms/delivery/NetworkStatus.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.comms.delivery + +internal enum class NetworkStatus(val value: String) { + NOT_REACHABLE("none"), + WIFI("wifi"), + WAN("wan"), + UNKNOWN("unknown") +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigListener.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigListener.kt new file mode 100644 index 0000000000..8ea398b7b7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigListener.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.config + +/** + * Notifies listeners on changes in the [ConfigService]. + */ +internal fun interface ConfigListener { + + /** + * Called when the [ConfigService] has been updated in some way. This typically means + * that the remote config has been fetched from the server. This allows callers + * to check config when this happens if they wish. + */ + fun onConfigChange(configService: ConfigService) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigService.kt new file mode 100644 index 0000000000..27aa2691ed --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/ConfigService.kt @@ -0,0 +1,147 @@ +package io.embrace.android.embracesdk.config + +import io.embrace.android.embracesdk.config.behavior.AnrBehavior +import io.embrace.android.embracesdk.config.behavior.AppExitInfoBehavior +import io.embrace.android.embracesdk.config.behavior.AutoDataCaptureBehavior +import io.embrace.android.embracesdk.config.behavior.BackgroundActivityBehavior +import io.embrace.android.embracesdk.config.behavior.BreadcrumbBehavior +import io.embrace.android.embracesdk.config.behavior.DataCaptureEventBehavior +import io.embrace.android.embracesdk.config.behavior.LogMessageBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.config.behavior.SdkEndpointBehavior +import io.embrace.android.embracesdk.config.behavior.SdkModeBehavior +import io.embrace.android.embracesdk.config.behavior.SessionBehavior +import io.embrace.android.embracesdk.config.behavior.SpansBehavior +import io.embrace.android.embracesdk.config.behavior.StartupBehavior +import io.embrace.android.embracesdk.config.behavior.WebViewVitalsBehavior +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import java.io.Closeable + +/** + * Provides access to the configuration for the customer's app. + * + * Configuration is configured for the user's app, and exposed via the API. + */ +internal interface ConfigService : Closeable { + + /** + * How background activity functionality should behave. + */ + val backgroundActivityBehavior: BackgroundActivityBehavior + + /** + * How automatic data capture functionality should behave. + */ + val autoDataCaptureBehavior: AutoDataCaptureBehavior + + /** + * How automatic breadcrumb functionality should behave. + */ + val breadcrumbBehavior: BreadcrumbBehavior + + /** + * How log message functionality should behave. + */ + val logMessageBehavior: LogMessageBehavior + + /** + * How ANR functionality should behave. + */ + val anrBehavior: AnrBehavior + + /** + * How sessions should behave. + */ + val sessionBehavior: SessionBehavior + + /** + * How network call capture should behave. + */ + val networkBehavior: NetworkBehavior + + /** + * How spans should behave + */ + val spansBehavior: SpansBehavior + + /** + * How the startup moment should behave + */ + val startupBehavior: StartupBehavior + + /** + * How the SDK should handle events where data can be captured. This could be a moment, etc... + */ + val dataCaptureEventBehavior: DataCaptureEventBehavior + + /** + * Provides whether the SDK should enable certain 'behavior' modes, such as 'integration mode' + */ + val sdkModeBehavior: SdkModeBehavior + + /** + * Provides base endpoints the SDK should send data to + */ + val sdkEndpointBehavior: SdkEndpointBehavior + + /** + * Provides whether the SDK should enable certain 'behavior' of web vitals + */ + val webViewVitalsBehavior: WebViewVitalsBehavior + + /** + * Provides behavior for the app exit info feature + */ + val appExitInfoBehavior: AppExitInfoBehavior + + /** + * How the network span forwarding feature should behave + */ + val networkSpanForwardingBehavior: NetworkSpanForwardingBehavior + + /** + * Adds a listener for changes to the [RemoteConfig]. The listeners will be notified when the + * [ConfigService] refreshes its configuration. + * + * @param configListener the listener to add + */ + fun addListener(configListener: ConfigListener) + + /** + * Checks if the SDK is enabled. + * + * The SDK can be configured to disable a percentage of devices based on the normalization of + * their device ID between 1-100. This threshold is set in [RemoteConfig]. + * + * @return true if the sdk is enabled, false otherwise + */ + fun isSdkDisabled(): Boolean + + /** + * Checks if the capture of background activity is enabled. + * + * + * The background activity capture can be configured to enable a percentage of + * devices based on the normalization of their device ID between 1-100. + * + * @return true if background activity capture is enabled. + */ + fun isBackgroundActivityCaptureEnabled(): Boolean + + /** + * Returns true if the remote config has been fetched and is not expired. Generally speaking + * use of this function should be discouraged - but it can be useful to prevent running risky + * behavior that should only be switched on via remote config. + * + * Most callers will not need this function - try not to abuse it. + */ + fun hasValidRemoteConfig(): Boolean + + /** + * Checks if the capture of Application Exit Info is enabled. + * + * @return true if AEI capture is enabled. + */ + fun isAppExitInfoCaptureEnabled(): Boolean +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/EmbraceConfigService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/EmbraceConfigService.kt new file mode 100644 index 0000000000..4e050cb000 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/EmbraceConfigService.kt @@ -0,0 +1,362 @@ +package io.embrace.android.embracesdk.config + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.api.ApiService +import io.embrace.android.embracesdk.config.behavior.AnrBehavior +import io.embrace.android.embracesdk.config.behavior.AppExitInfoBehavior +import io.embrace.android.embracesdk.config.behavior.AutoDataCaptureBehavior +import io.embrace.android.embracesdk.config.behavior.BackgroundActivityBehavior +import io.embrace.android.embracesdk.config.behavior.BehaviorThresholdCheck +import io.embrace.android.embracesdk.config.behavior.BreadcrumbBehavior +import io.embrace.android.embracesdk.config.behavior.DataCaptureEventBehavior +import io.embrace.android.embracesdk.config.behavior.LogMessageBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.config.behavior.SdkEndpointBehavior +import io.embrace.android.embracesdk.config.behavior.SdkModeBehavior +import io.embrace.android.embracesdk.config.behavior.SessionBehavior +import io.embrace.android.embracesdk.config.behavior.SpansBehavior +import io.embrace.android.embracesdk.config.behavior.StartupBehavior +import io.embrace.android.embracesdk.config.behavior.WebViewVitalsBehavior +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.utils.stream +import java.util.concurrent.Callable +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.ExecutorService +import java.util.concurrent.RejectedExecutionException +import kotlin.math.min + +/** + * Loads configuration for the app from the Embrace API. + */ +internal class EmbraceConfigService @JvmOverloads constructor( + private val localConfig: LocalConfig, + private val apiServiceProvider: () -> ApiService, + private val preferencesService: PreferencesService, + private val clock: Clock, + private val logger: InternalEmbraceLogger, + private val executorService: ExecutorService, + isDebug: Boolean, + private val stopBehavior: () -> Unit = {}, + internal val thresholdCheck: BehaviorThresholdCheck = BehaviorThresholdCheck(preferencesService::deviceIdentifier) +) : ConfigService, ActivityListener { + + /** + * The listeners subscribed to configuration changes. + */ + private val listeners: MutableSet = CopyOnWriteArraySet() + private val lock = Any() + + @VisibleForTesting + @Volatile + private var configProp = RemoteConfig() + + @VisibleForTesting + @Volatile + var lastUpdated: Long = 0 + + @Volatile + private var lastRefreshConfigAttempt: Long = 0 + + @Volatile + private var configRetrySafeWindow = DEFAULT_RETRY_WAIT_TIME.toDouble() + + private val remoteSupplier: () -> RemoteConfig? = { getConfig() } + + override val backgroundActivityBehavior: BackgroundActivityBehavior = + BackgroundActivityBehavior( + thresholdCheck = thresholdCheck, + localSupplier = localConfig.sdkConfig::backgroundActivityConfig, + remoteSupplier = { getConfig().backgroundActivityConfig } + ) + + override val autoDataCaptureBehavior: AutoDataCaptureBehavior = + AutoDataCaptureBehavior( + thresholdCheck = thresholdCheck, + localSupplier = { localConfig }, + remoteSupplier = remoteSupplier + ) + + override val breadcrumbBehavior: BreadcrumbBehavior = + BreadcrumbBehavior( + thresholdCheck, + localSupplier = localConfig::sdkConfig, + remoteSupplier = remoteSupplier + ) + + override val logMessageBehavior: LogMessageBehavior = + LogMessageBehavior( + thresholdCheck, + remoteSupplier = { getConfig().logConfig } + ) + + override val anrBehavior: AnrBehavior = + AnrBehavior( + thresholdCheck, + localSupplier = localConfig.sdkConfig::anr, + remoteSupplier = { getConfig().anrConfig } + ) + + override val sessionBehavior: SessionBehavior = + SessionBehavior( + thresholdCheck, + localSupplier = localConfig.sdkConfig::sessionConfig, + remoteSupplier = { getConfig() } + ) + + override val networkBehavior: NetworkBehavior = + NetworkBehavior( + thresholdCheck = thresholdCheck, + localSupplier = localConfig::sdkConfig, + remoteSupplier = remoteSupplier + ) + + override val startupBehavior: StartupBehavior = + StartupBehavior( + thresholdCheck = thresholdCheck, + localSupplier = localConfig.sdkConfig::startupMoment + ) + + override val spansBehavior: SpansBehavior = + SpansBehavior( + thresholdCheck = thresholdCheck, + remoteSupplier = { getConfig().spansConfig } + ) + + override val dataCaptureEventBehavior: DataCaptureEventBehavior = DataCaptureEventBehavior( + thresholdCheck = thresholdCheck, + remoteSupplier = remoteSupplier + ) + + override val sdkModeBehavior: SdkModeBehavior = + SdkModeBehavior( + isDebug = isDebug, + thresholdCheck = thresholdCheck, + localSupplier = { localConfig }, + remoteSupplier = remoteSupplier + ) + + override val sdkEndpointBehavior: SdkEndpointBehavior = + SdkEndpointBehavior( + thresholdCheck = thresholdCheck, + localSupplier = localConfig.sdkConfig::baseUrls, + ) + + override val appExitInfoBehavior: AppExitInfoBehavior = AppExitInfoBehavior( + thresholdCheck = thresholdCheck, + localSupplier = localConfig.sdkConfig::appExitInfoConfig, + remoteSupplier = remoteSupplier + ) + + override val networkSpanForwardingBehavior: NetworkSpanForwardingBehavior = + NetworkSpanForwardingBehavior( + thresholdCheck = thresholdCheck, + remoteSupplier = { getConfig().networkSpanForwardingRemoteConfig } + ) + + override val webViewVitalsBehavior: WebViewVitalsBehavior = + WebViewVitalsBehavior( + thresholdCheck = thresholdCheck, + remoteSupplier = remoteSupplier + ) + + init { + performInitialConfigLoad() + attemptConfigRefresh() + } + + /** + * Schedule an action that loads the config from the cache. + * This is deferred to lessen it´s impact upon startup. + */ + private fun performInitialConfigLoad() { + logger.logDeveloper("EmbraceConfigService", "performInitialConfigLoad") + try { + executorService.submit( + Callable { + loadConfigFromCache() + null + } + ) + } catch (ex: RejectedExecutionException) { + logger.logDebug("Failed to schedule initial config load from cache.", ex) + } + } + + /** + * Load Config from cache if present. + */ + @VisibleForTesting + fun loadConfigFromCache() { + logger.logDeveloper("EmbraceConfigService", "Attempting to load config from cache") + val apiService = apiServiceProvider.invoke() + val cachedConfig = apiService.getCachedConfig() + val obj = cachedConfig.config + + if (obj != null) { + val oldConfig = configProp + logger.logDeveloper("EmbraceConfigService", "Loaded config from cache") + updateConfig(oldConfig, obj) + } else { + logger.logDeveloper("EmbraceConfigService", "config not found in local cache") + } + } + + private fun getConfig(): RemoteConfig { + attemptConfigRefresh() + return configProp + } + + private fun attemptConfigRefresh() { + if (configRequiresRefresh() && configRetryIsSafe()) { + synchronized(lock) { + if (configRequiresRefresh() && configRetryIsSafe()) { + lastRefreshConfigAttempt = clock.now() + logger.logDeveloper("EmbraceConfigService", "Attempting to update config") + // Attempt to asynchronously update the config if it is out of date + refreshConfig() + } + } + } + } + + private fun refreshConfig() { + logger.logDeveloper("EmbraceConfigService", "Attempting to refresh config") + val previousConfig = configProp + executorService.submit( + Callable { + logger.logDeveloper("EmbraceConfigService", "Updating config in background thread") + + // Ensure that another thread didn't refresh it already in the meantime + if (configRequiresRefresh()) { + try { + lastRefreshConfigAttempt = clock.now() + val apiService = apiServiceProvider.invoke() + val newConfig = apiService.getConfig() + if (newConfig != null) { + updateConfig(previousConfig, newConfig) + lastUpdated = clock.now() + } + configRetrySafeWindow = DEFAULT_RETRY_WAIT_TIME.toDouble() + logger.logDeveloper("EmbraceConfigService", "Config updated") + } catch (ex: Exception) { + configRetrySafeWindow = + min( + MAX_ALLOWED_RETRY_WAIT_TIME.toDouble(), + configRetrySafeWindow * 2 + ) + logger.logWarning( + "Failed to load SDK config from the server. " + + "Trying again in " + configRetrySafeWindow + " seconds." + ) + } + } + configProp + } + ) + } + + private fun updateConfig(previousConfig: RemoteConfig, newConfig: RemoteConfig) { + if (newConfig != previousConfig) { + configProp = newConfig + persistConfig() + logger.logDeveloper("EmbraceConfigService", "Notify listeners about new config") + // Only notify listeners if the config has actually changed value + notifyListeners() + } + } + + private fun persistConfig() { + logger.logDeveloper("EmbraceConfigService", "persistConfig") + // TODO: future get rid of these prefs from PrefService entirely? + preferencesService.sdkDisabled = sdkModeBehavior.isSdkDisabled() + preferencesService.backgroundActivityEnabled = backgroundActivityBehavior.isEnabled() + } + + // TODO: future extract these out to SdkBehavior interface + override fun isSdkDisabled(): Boolean { + return preferencesService.sdkDisabled + } + + override fun isBackgroundActivityCaptureEnabled(): Boolean { + return preferencesService.backgroundActivityEnabled + } + + override fun addListener(configListener: ConfigListener) { + listeners.add(configListener) + } + + override fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) { + // Refresh the config on resume if it has expired + getConfig() + if (Embrace.getInstance().isStarted && isSdkDisabled()) { + logger.logInfo("Embrace SDK disabled by config") + stopBehavior() + } + } + + /** + * Notifies the listeners that a new config was fetched from the server. + */ + private fun notifyListeners() { + stream(listeners) { listener: ConfigListener -> + try { + listener.onConfigChange(this) + } catch (ex: Exception) { + logger.logDebug("Failed to notify ConfigListener", ex) + } + } + } + + /** + * Checks if the time diff since the last fetch exceeds the + * [EmbraceConfigService.CONFIG_TTL] millis. + * + * @return if the config requires to be fetched from the remote server again or not. + */ + private fun configRequiresRefresh(): Boolean { + return clock.now() - lastUpdated > CONFIG_TTL + } + + /** + * Checks if the time diff since the last attempt is enough to try again. + * + * @return if the config can be fetched from the remote server again or not. + */ + private fun configRetryIsSafe(): Boolean { + return clock.now() > lastRefreshConfigAttempt + configRetrySafeWindow * 1000 + } + + override fun close() { + logger.logDebug("Shutting down EmbraceConfigService") + } + + override fun hasValidRemoteConfig(): Boolean = !configRequiresRefresh() + override fun isAppExitInfoCaptureEnabled(): Boolean { + return appExitInfoBehavior.isEnabled() + } + + companion object { + + /** + * Config lives for 1 hour before attempting to retrieve again. + */ + private const val CONFIG_TTL = 60 * 60 * 1000L + + /** + * Config refresh default retry period. + */ + private const val DEFAULT_RETRY_WAIT_TIME: Long = 20 // 20 seconds + + /** + * Config max allowed refresh retry period. + */ + private const val MAX_ALLOWED_RETRY_WAIT_TIME: Long = 300 // 5 minutes + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AnrBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AnrBehavior.kt new file mode 100644 index 0000000000..b01e94d569 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AnrBehavior.kt @@ -0,0 +1,240 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.AnrLocalConfig +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig.Unwinder +import java.util.regex.Pattern + +/** + * Provides the behavior that the ANR feature should follow. + */ +internal class AnrBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> AnrLocalConfig?, + remoteSupplier: () -> AnrRemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + + companion object { + private const val CAPTURE_GOOGLE_DEFAULT = false + private const val DEFAULT_ANR_PCT_ENABLED = true + private const val DEFAULT_ANR_PROCESS_ERRORS_PCT_ENABLED = false + private const val DEFAULT_ANR_BG_PCT_ENABLED = false + private const val DEFAULT_ANR_INTERVAL_MS: Long = 100 + private const val DEFAULT_ANR_PROCESS_ERRORS_INTERVAL_MS: Long = 1000 + private const val DEFAULT_ANR_PROCESS_ERRORS_DELAY_MS: Long = 5 * 1000 + private const val DEFAULT_ANR_PROCESS_ERRORS_SCHEDULER_EXTRA_TIME_ALLOWANCE: Long = + 30 * 1000 + private const val DEFAULT_ANR_MAX_PER_INTERVAL = 80 + private const val DEFAULT_STACKTRACE_FRAME_LIMIT = 100 + private const val DEFAULT_ANR_MIN_THREAD_PRIORITY_TO_CAPTURE = 0 + private const val DEFAULT_ANR_MAX_ANR_INTERVALS_PER_SESSION = 5 + private const val DEFAULT_ANR_MIN_CAPTURE_DURATION = 1000 + private const val DEFAULT_ANR_MAIN_THREAD_ONLY = true + private const val DEFAULT_NATIVE_THREAD_ANR_SAMPLING_FACTOR = 5 + private const val DEFAULT_NATIVE_THREAD_ANR_SAMPLING_ENABLED = false + private const val DEFAULT_NATIVE_THREAD_ANR_OFFSET_ENABLED = true + private const val DEFAULT_IDLE_HANDLER_ENABLED = false + private const val DEFAULT_STRICT_MODE_LISTENER_ENABLED = false + private const val DEFAULT_STRICT_MODE_VIOLATION_LIMIT = 25 + private const val DEFAULT_IGNORE_NATIVE_THREAD_ANR_SAMPLING_ALLOWLIST = true + private val DEFAULT_NATIVE_THREAD_ANR_SAMPLING_ALLOWLIST = listOf( + AnrRemoteConfig.AllowedNdkSampleMethod("UnityPlayer", "pauseUnity") + ) + private const val DEFAULT_MONITOR_THREAD_PRIORITY = + android.os.Process.THREAD_PRIORITY_DEFAULT + } + + /** + * Control whether Google ANR capture is enabled. + */ + fun isGoogleAnrCaptureEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.googlePctEnabled) + ?: local?.captureGoogle + ?: CAPTURE_GOOGLE_DEFAULT + } + + /** + * Allow listed threads by pattern + */ + val allowPatternList: List by lazy { + remote?.allowList?.map(Pattern::compile) ?: emptyList() + } + + /** + * Black listed threads by pattern + */ + val blockPatternList: List by lazy { + remote?.blockList?.map(Pattern::compile) ?: emptyList() + } + + /** + * Percentage of users for which ANR stack trace capture is enabled. + */ + fun isAnrCaptureEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctEnabled) + ?: DEFAULT_ANR_PCT_ENABLED + } + + /** + * Percentage of users for which ANR process errors capture is enabled. + */ + fun isAnrProcessErrorsCaptureEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctAnrProcessErrorsEnabled) + ?: DEFAULT_ANR_PROCESS_ERRORS_PCT_ENABLED + } + + /** + * The priority that should be used for the monitor thread. + */ + fun getMonitorThreadPriority(): Int = + remote?.monitorThreadPriority ?: DEFAULT_MONITOR_THREAD_PRIORITY + + /** + * Whether Background ANR capture is enabled. + */ + fun isBgAnrCaptureEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctBgEnabled) + ?: DEFAULT_ANR_BG_PCT_ENABLED + } + + /** + * Time between stack trace captures for time intervals after the start of an ANR. + */ + fun getSamplingIntervalMs(): Long = remote?.sampleIntervalMs ?: DEFAULT_ANR_INTERVAL_MS + + /** + * Time between ANR process errors checks for time intervals after the start of ANR process + * errors check. + */ + fun getAnrProcessErrorsIntervalMs(): Long = + remote?.anrProcessErrorsIntervalMs ?: DEFAULT_ANR_PROCESS_ERRORS_INTERVAL_MS + + /** + * Time for which to delay the search of anr process errors. This would be the delay since + * the thread has been blocked. + */ + fun getAnrProcessErrorsDelayMs(): Long = + remote?.anrProcessErrorsDelayMs ?: DEFAULT_ANR_PROCESS_ERRORS_DELAY_MS + + /** + * This is the maximum time that the scheduler is allowed to keep on running since thread has + * been unblocked. + */ + fun getAnrProcessErrorsSchedulerExtraTimeAllowanceMs(): Long = + remote?.anrProcessErrorsSchedulerExtraTimeAllowance + ?: DEFAULT_ANR_PROCESS_ERRORS_SCHEDULER_EXTRA_TIME_ALLOWANCE + + /** + * Maximum captured stacktraces for a single ANR interval. + */ + fun getMaxStacktracesPerInterval(): Int = + remote?.maxStacktracesPerInterval ?: DEFAULT_ANR_MAX_PER_INTERVAL + + /** + * Maximum number of frames to keep in ANR stacktrace samples + */ + fun getStacktraceFrameLimit(): Int = + remote?.stacktraceFrameLimit ?: DEFAULT_STACKTRACE_FRAME_LIMIT + + /** + * Maximum captured anr for a session. + */ + fun getMaxAnrIntervalsPerSession(): Int = + remote?.anrPerSession ?: DEFAULT_ANR_MAX_ANR_INTERVALS_PER_SESSION + + /** + * The min thread priority that should be captured + */ + fun getMinThreadPriority(): Int = + remote?.minThreadPriority ?: DEFAULT_ANR_MIN_THREAD_PRIORITY_TO_CAPTURE + + /** + * Minimum duration of an ANR interval + */ + fun getMinDuration(): Int = remote?.minDuration ?: DEFAULT_ANR_MIN_CAPTURE_DURATION + + /** + * Whether only the main thread should be captured + */ + fun shouldCaptureMainThreadOnly(): Boolean = + remote?.mainThreadOnly ?: DEFAULT_ANR_MAIN_THREAD_ONLY + + /** + * The sampling factor for native thread ANR stacktrace sampling. This should be multiplied by + * the [getSamplingIntervalMs] to give the NDK sampling interval. + */ + fun getNativeThreadAnrSamplingFactor(): Int = + remote?.nativeThreadAnrSamplingFactor ?: DEFAULT_NATIVE_THREAD_ANR_SAMPLING_FACTOR + + /** + * The unwinder used for native thread ANR stacktrace sampling. + */ + fun getNativeThreadAnrSamplingUnwinder(): Unwinder { + return runCatching { + Unwinder.values().find { + it.name.equals(remote?.nativeThreadAnrSamplingUnwinder, true) + } ?: Unwinder.LIBUNWIND + }.getOrDefault(Unwinder.LIBUNWIND) + } + + /** + * Whether native thread ANR sampling is enabled + */ + fun isNativeThreadAnrSamplingEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctNativeThreadAnrSamplingEnabled) + ?: local?.captureUnityThread + ?: DEFAULT_NATIVE_THREAD_ANR_SAMPLING_ENABLED + } + + /** + * Whether offsets are enabled for native thread ANR stacktrace sampling. + */ + fun isNativeThreadAnrSamplingOffsetEnabled(): Boolean = + remote?.nativeThreadAnrSamplingOffsetEnabled ?: DEFAULT_NATIVE_THREAD_ANR_OFFSET_ENABLED + + /** + * The percentage of enabled devices for testing IdleHandler to terminate ANRs. + */ + fun isIdleHandlerEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctIdleHandlerEnabled) + ?: DEFAULT_IDLE_HANDLER_ENABLED + } + + /** + * Whether the StrictMode listener experiment is enabled. + */ + fun isStrictModeListenerEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctStrictModeListenerEnabled) + ?: DEFAULT_STRICT_MODE_LISTENER_ENABLED + } + + /** + * Whether the Strictmode listener experiment is enabled. + */ + fun getStrictModeViolationLimit(): Int = + remote?.strictModeViolationLimit ?: DEFAULT_STRICT_MODE_VIOLATION_LIMIT + + /** + * Whether the allow list is ignored or not. + */ + fun isNativeThreadAnrSamplingAllowlistIgnored(): Boolean = + remote?.ignoreNativeThreadAnrSamplingAllowlist + ?: DEFAULT_IGNORE_NATIVE_THREAD_ANR_SAMPLING_ALLOWLIST + + /** + * The allowed list of classes/methods for NDK stacktrace sampling + */ + fun getNativeThreadAnrSamplingAllowlist(): List = + remote?.nativeThreadAnrSamplingAllowlist ?: DEFAULT_NATIVE_THREAD_ANR_SAMPLING_ALLOWLIST + + /** + * The sampling factor for native thread ANR stacktrace sampling. This is calculated by + * multiplying the [getSamplingIntervalMs] against [getNativeThreadAnrSamplingFactor]. + */ + fun getNativeThreadAnrSamplingIntervalMs() = + getSamplingIntervalMs() * getNativeThreadAnrSamplingFactor() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior.kt new file mode 100644 index 0000000000..e800b81e9e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehavior.kt @@ -0,0 +1,49 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.AppExitInfoLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig + +/** + * Provides the behavior that should be followed for select services that automatically + * capture data. + */ +internal class AppExitInfoBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> AppExitInfoLocalConfig?, + remoteSupplier: () -> RemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + companion object { + /** + * Max size of bytes to allow capturing AppExitInfo ndk/anr traces + */ + private const val MAX_TRACE_SIZE_BYTES = 2097152 // 2MB + const val AEI_MAX_NUM_DEFAULT = 0 // 0 means no limit + const val AEI_ENABLED_DEFAULT = true + } + + sealed class CollectTracesResult(val result: String?) { + class Success(result: String?) : CollectTracesResult(result) + class TooLarge(result: String?) : CollectTracesResult(result) + class TraceException(message: String?) : CollectTracesResult(message) + } + + fun getTraceMaxLimit(): Int = + remote?.appExitInfoConfig?.appExitInfoTracesLimit + ?: local?.appExitInfoTracesLimit + ?: MAX_TRACE_SIZE_BYTES + + /** + * Whether the feature is enabled or not. + */ + fun isEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.appExitInfoConfig?.pctAeiCaptureEnabled) + ?: local?.aeiCaptureEnabled + ?: AEI_ENABLED_DEFAULT + } + + fun appExitInfoMaxNum() = remote?.appExitInfoConfig?.aeiMaxNum ?: AEI_MAX_NUM_DEFAULT +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior.kt new file mode 100644 index 0000000000..68a344fe0c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehavior.kt @@ -0,0 +1,109 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.internal.ApkToolsConfig + +/** + * Provides the behavior that should be followed for select services that automatically + * capture data. + */ +internal class AutoDataCaptureBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> LocalConfig?, + remoteSupplier: () -> RemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + + companion object { + const val MEMORY_SERVICE_ENABLED_DEFAULT = true + const val POWER_SAVE_MODE_SERVICE_ENABLED_DEFAULT = true + const val NETWORK_CONNECTIVITY_SERVICE_ENABLED_DEFAULT = true + const val ANR_SERVICE_ENABLED_DEFAULT = true + const val CRASH_HANDLER_ENABLED_DEFAULT = true + const val CAPTURE_COMPOSE_ONCLICK_DEFAULT = false + const val REPORT_DISK_USAGE_DEFAULT = true + } + + /** + * Returns true if [io.embrace.android.embracesdk.MemoryService] should + * automatically capture data. + */ + fun isMemoryServiceEnabled(): Boolean { + return local?.sdkConfig?.automaticDataCaptureConfig?.memoryServiceEnabled + ?: MEMORY_SERVICE_ENABLED_DEFAULT + } + + /** + * Returns true if [io.embrace.android.embracesdk.PowerSaveModeService] should + * automatically capture data. + */ + fun isPowerSaveModeServiceEnabled(): Boolean { + return local?.sdkConfig?.automaticDataCaptureConfig?.powerSaveModeServiceEnabled + ?: POWER_SAVE_MODE_SERVICE_ENABLED_DEFAULT + } + + /** + * Returns true if [io.embrace.android.embracesdk.NetworkConnectivityService] should + * automatically capture data. + */ + fun isNetworkConnectivityServiceEnabled(): Boolean { + return local?.sdkConfig?.automaticDataCaptureConfig?.networkConnectivityServiceEnabled + ?: NETWORK_CONNECTIVITY_SERVICE_ENABLED_DEFAULT + } + + /** + * Returns true if [io.embrace.android.embracesdk.anr.AnrService] should + * automatically capture data. + */ + fun isAnrServiceEnabled(): Boolean { + return local?.sdkConfig?.automaticDataCaptureConfig?.anrServiceEnabled + ?: ANR_SERVICE_ENABLED_DEFAULT + } + + /** + * Control whether the Embrace SDK automatically attaches to the uncaught exception handler. + */ + fun isUncaughtExceptionHandlerEnabled(): Boolean = + local?.sdkConfig?.crashHandler?.enabled ?: CRASH_HANDLER_ENABLED_DEFAULT + + /** + * Whether Jetpack Compose click events should be captured + */ + fun isComposeOnClickEnabled(): Boolean { + return when (remote?.killSwitchConfig?.jetpackCompose) { + null, true -> { + // no remote: use local + // remote is true: it can be explicitly disabled locally + local?.sdkConfig?.composeConfig?.captureComposeOnClick ?: CAPTURE_COMPOSE_ONCLICK_DEFAULT + } + false -> { + // remote is false: the killswitch ignores local + false + } + } + } + + /** + * Whether embrace should attempt to overwrite other signal handlers + */ + fun isSigHandlerDetectionEnabled(): Boolean { + return remote?.killSwitchConfig?.sigHandlerDetection + ?: local?.sdkConfig?.sigHandlerDetection ?: true + } + + /** + * Whether NDK error capture is enabled + */ + fun isNdkEnabled(): Boolean = local?.ndkEnabled ?: false && !ApkToolsConfig.IS_NDK_DISABLED + + /** + * Control whether we scan for and report app disk usage. This can be a costly operation + * for apps with a lot of local files. + */ + fun isDiskUsageReportingEnabled(): Boolean = + local?.sdkConfig?.app?.reportDiskUsage ?: REPORT_DISK_USAGE_DEFAULT +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior.kt new file mode 100644 index 0000000000..00cad5562f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehavior.kt @@ -0,0 +1,55 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.BackgroundActivityLocalConfig +import io.embrace.android.embracesdk.config.remote.BackgroundActivityRemoteConfig + +/** + * Provides the behavior that the Background Activity feature should follow. + */ +internal class BackgroundActivityBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> BackgroundActivityLocalConfig?, + remoteSupplier: () -> BackgroundActivityRemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + + companion object { + const val BACKGROUND_ACTIVITY_CAPTURE_ENABLED_DEFAULT = false + const val MANUAL_BACKGROUND_ACTIVITY_LIMIT_DEFAULT = 100 + const val MIN_BACKGROUND_ACTIVITY_DURATION_DEFAULT = 5000L + const val MAX_CACHED_ACTIVITIES_DEFAULT = 30 + } + + /** + * Whether the feature is enabled or not. + */ + fun isEnabled(): Boolean { + return remote?.threshold?.let(thresholdCheck::isBehaviorEnabled) + ?: local?.backgroundActivityCaptureEnabled + ?: BACKGROUND_ACTIVITY_CAPTURE_ENABLED_DEFAULT + } + + /** + * Specify a maximum number of client defined background activities. + */ + fun getManualBackgroundActivityLimit(): Int { + return local?.manualBackgroundActivityLimit ?: MANUAL_BACKGROUND_ACTIVITY_LIMIT_DEFAULT + } + + /** + * Specify a minimum duration for a client defined background activity. + */ + fun getMinBackgroundActivityDuration(): Long { + return local?.minBackgroundActivityDuration ?: MIN_BACKGROUND_ACTIVITY_DURATION_DEFAULT + } + + /** + * Specify the max number of background activities cached to disk at the same time. + */ + fun getMaxCachedActivities(): Int { + return local?.maxCachedActivities ?: MAX_CACHED_ACTIVITIES_DEFAULT + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck.kt new file mode 100644 index 0000000000..09fa07033c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BehaviorThresholdCheck.kt @@ -0,0 +1,69 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logger +import kotlin.math.pow + +/** + * Checks whether a percent-based config value is over a threshold where it should be enabled. + */ +internal class BehaviorThresholdCheck( + private val deviceIdProvider: () -> String +) { + + /** + * An implementation of [isBehaviorEnabled] that returns null if the pctEnabled parameter + * is null. + */ + fun isBehaviorEnabled(pctEnabled: Float?): Boolean? = pctEnabled?.let(::isBehaviorEnabled) + + /** + * An implementation of [isBehaviorEnabled] that returns null if the pctEnabled parameter + * is null. + */ + fun isBehaviorEnabled(pctEnabled: Int?): Boolean? = pctEnabled?.toFloat().let(::isBehaviorEnabled) + + /** + * Determines whether behaviour is enabled for a percentage roll-out. This is achieved + * by taking a normalized hex value from the last 6 digits of the device ID, and comparing + * it against the enabled percentage. This ensures that devices are consistently in a given + * group for beta functionality. + * + * + * The normalized device ID has 16^6 possibilities (roughly 1.6m) which should be sufficient + * granularity for our needs. + * + * @param pctEnabled the % enabled for a given config value. This should be a float rather than + * an integer for maximum granularity. + * @return whether the behaviour is enabled or not. + */ + fun isBehaviorEnabled(pctEnabled: Float): Boolean { + if (pctEnabled <= 0 || pctEnabled > 100) { + logger.logDeveloper("EmbraceConfigService", "behaviour enabled") + return false + } + val deviceId = getNormalizedLargeDeviceId() + return pctEnabled >= deviceId + } + + fun getNormalizedLargeDeviceId(): Float = getNormalizedDeviceId(6) + + /** + * Use [.isBehaviorEnabled] instead as it allows rollouts to be controlled + * at greater granularity. + */ + @Deprecated("") + fun getNormalizedDeviceId(): Float = getNormalizedDeviceId(2) + + private fun getNormalizedDeviceId(digits: Int): Float { + val deviceId = deviceIdProvider() + val finalChars = deviceId.substring(deviceId.length - digits) + + // Normalize the device ID to a value between 0.0 - 100.0 + val radix = 16 + val space = (radix.toDouble().pow(digits.toDouble()) - 1).toInt() + val value = Integer.valueOf(finalChars, radix) + val normalizedDeviceId = value.toFloat() / space * 100 + logger.logDeveloper("EmbraceConfigService", "normalizedDeviceId: $normalizedDeviceId") + return normalizedDeviceId + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior.kt new file mode 100644 index 0000000000..655e54797c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehavior.kt @@ -0,0 +1,64 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig + +/** + * Provides the behavior that should be followed for select services that automatically + * capture data. + */ +internal class BreadcrumbBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> SdkLocalConfig?, + remoteSupplier: () -> RemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + + companion object { + + /** + * The default breadcrumbs capture limit. + */ + const val DEFAULT_BREADCRUMB_LIMIT = 100 + const val CAPTURE_TAP_COORDINATES_DEFAULT = true + const val ENABLE_AUTOMATIC_ACTIVITY_CAPTURE_DEFAULT = true + const val WEB_VIEW_CAPTURE_DEFAULT = true + const val WEB_VIEW_QUERY_PARAMS_CAPTURE_DEFAULT = true + } + + fun getCustomBreadcrumbLimit(): Int = remote?.uiConfig?.breadcrumbs ?: DEFAULT_BREADCRUMB_LIMIT + fun getFragmentBreadcrumbLimit(): Int = remote?.uiConfig?.fragments ?: DEFAULT_BREADCRUMB_LIMIT + fun getTapBreadcrumbLimit(): Int = remote?.uiConfig?.taps ?: DEFAULT_BREADCRUMB_LIMIT + fun getViewBreadcrumbLimit(): Int = remote?.uiConfig?.views ?: DEFAULT_BREADCRUMB_LIMIT + fun getWebViewBreadcrumbLimit(): Int = remote?.uiConfig?.webViews ?: DEFAULT_BREADCRUMB_LIMIT + + /** + * Controls whether tap coordinates are captured in breadcrumbs + */ + fun isTapCoordinateCaptureEnabled(): Boolean = + local?.taps?.captureCoordinates ?: CAPTURE_TAP_COORDINATES_DEFAULT + + /** + * Controls whether activity lifecycle changes are captured in breadcrumbs + */ + fun isActivityBreadcrumbCaptureEnabled() = + local?.viewConfig?.enableAutomaticActivityCapture + ?: ENABLE_AUTOMATIC_ACTIVITY_CAPTURE_DEFAULT + + /** + * Controls whether webviews are captured. + */ + fun isWebViewBreadcrumbCaptureEnabled(): Boolean = + local?.webViewConfig?.captureWebViews ?: WEB_VIEW_CAPTURE_DEFAULT + + /** + * Control whether query params for webviews are captured. + */ + fun isQueryParamCaptureEnabled(): Boolean = + local?.webViewConfig?.captureQueryParams ?: WEB_VIEW_QUERY_PARAMS_CAPTURE_DEFAULT + + fun isCaptureFcmPiiDataEnabled(): Boolean = local?.captureFcmPiiData ?: false +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior.kt new file mode 100644 index 0000000000..d0c8fa6b3a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehavior.kt @@ -0,0 +1,48 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.internal.MessageType +import io.embrace.android.embracesdk.internal.PatternCache +import java.util.Locale + +internal class DataCaptureEventBehavior( + thresholdCheck: BehaviorThresholdCheck, + remoteSupplier: () -> RemoteConfig? = { null } +) : MergedConfigBehavior( + thresholdCheck, + { null }, + remoteSupplier +) { + + companion object { + private const val DEFAULT_INTERNAL_EXCEPTION_CAPTURE = true + } + + private val patternCache = PatternCache() + + fun isMessageTypeEnabled(type: MessageType): Boolean { + return when (val disabledTypes = remote?.disabledMessageTypes) { + null -> true + else -> !disabledTypes.contains(type.name.toLowerCase(Locale.getDefault())) + } + } + + fun isInternalExceptionCaptureEnabled(): Boolean = + remote?.internalExceptionCaptureEnabled ?: DEFAULT_INTERNAL_EXCEPTION_CAPTURE + + fun isEventEnabled(eventName: String): Boolean { + return when (val disabledTypes = remote?.disabledEventAndLogPatterns) { + null -> true + else -> !patternCache.doesStringMatchesPatternInSet(eventName, disabledTypes) + } + } + + fun isLogMessageEnabled(logMessage: String): Boolean { + return when (val disabledTypes = remote?.disabledEventAndLogPatterns) { + null -> true + else -> !patternCache.doesStringMatchesPatternInSet(logMessage, disabledTypes) + } + } + + fun getEventLimits(): Map = remote?.eventLimits ?: emptyMap() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehavior.kt new file mode 100644 index 0000000000..111915f229 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehavior.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.LogRemoteConfig + +/** + * Provides the behavior that should be followed for remote log message functionality. + */ +internal class LogMessageBehavior( + thresholdCheck: BehaviorThresholdCheck, + remoteSupplier: () -> LogRemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + { null }, + remoteSupplier +) { + + companion object { + private const val DEFAULT_LOG_INFO_LIMIT = 100 + private const val DEFAULT_LOG_WARNING_LIMIT = 100 + private const val DEFAULT_LOG_ERROR_LIMIT = 250 + internal const val LOG_MESSAGE_MAXIMUM_ALLOWED_LENGTH = 128 + } + + fun getLogMessageMaximumAllowedLength(): Int { + return remote?.logMessageMaximumAllowedLength ?: LOG_MESSAGE_MAXIMUM_ALLOWED_LENGTH + } + + fun getInfoLogLimit(): Int = remote?.logInfoLimit ?: DEFAULT_LOG_INFO_LIMIT + fun getWarnLogLimit(): Int = remote?.logWarnLimit ?: DEFAULT_LOG_WARNING_LIMIT + fun getErrorLogLimit(): Int = remote?.logErrorLimit ?: DEFAULT_LOG_ERROR_LIMIT +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/MergedConfigBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/MergedConfigBehavior.kt new file mode 100644 index 0000000000..d30bb8ecc8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/MergedConfigBehavior.kt @@ -0,0 +1,48 @@ +package io.embrace.android.embracesdk.config.behavior + +/** + * Merges multiple sources of config and tells the SDK how its functionality should behave. This + * means the caller doesn't need to worry about whether the remote config has been fetched or its + * precedence rules - it just gets told whether it should enable something or not. + * + * There are three sources of config: remote (from the config endpoint); local (from the + * embrace-config.json); and default (defined in subclasses of this type). + * + * Config is typically evaluated in the following precedence: Remote > Local > Default. Remote/local + * configs might not exist for every single field, as it doesn't always make sense for every value + * to be configurable by end-users. However, there should always be a default value. + */ +internal open class MergedConfigBehavior( + + /** + * Checks whether percent-based thresholds should be enabled or not. We should always return + * booleans about whether functionality is enabled - and should never expose percentages etc + * to the caller. + */ + protected val thresholdCheck: BehaviorThresholdCheck, + + /** + * Supplier for local config, from the embrace-config.json file. + */ + private val localSupplier: () -> L? = { null }, + + /** + * Supplier for remote config, from the config endpoint. + */ + private val remoteSupplier: () -> R? = { null } +) { + + /** + * The local config. This property always returns the most up-to-date value, or null if + * no local config is available. + */ + protected val local: L? + get() = localSupplier() + + /** + * The remote config. This property always returns the most up-to-date value, or null if + * no remote config is available. + */ + protected val remote: R? + get() = remoteSupplier() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkBehavior.kt new file mode 100644 index 0000000000..e1292671b8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkBehavior.kt @@ -0,0 +1,132 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.NetworkCaptureRuleRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import java.util.regex.Pattern + +/** + * Provides the behavior that functionality relating to network call capture should follow. + */ +internal class NetworkBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> SdkLocalConfig?, + remoteSupplier: () -> RemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + + companion object { + + /** + * Sets the default name of the HTTP request header to extract trace ID from. + */ + const val CONFIG_TRACE_ID_HEADER_DEFAULT_VALUE = "x-emb-trace-id" + + /** + * Capture request content length by default. + */ + const val CAPTURE_REQUEST_CONTENT_LENGTH = false + + /** + * Enable native monitoring by default. + */ + const val ENABLE_NATIVE_MONITORING_DEFAULT = true + const val DEFAULT_NETWORK_CALL_LIMIT = 1000 + + private val dirtyKeyList = listOf( + "-----BEGIN PUBLIC KEY-----", + "-----END PUBLIC KEY-----", + "\\r", + "\\n", + "\\t", + " " + ) + } + + /** + * The Trace ID Header that can be used to trace a particular request. + */ + fun getTraceIdHeader(): String = + local?.networking?.traceIdHeader ?: CONFIG_TRACE_ID_HEADER_DEFAULT_VALUE + + /** + * Control whether request size for native Android requests is captured. + */ + fun isRequestContentLengthCaptureEnabled(): Boolean = + local?.networking?.captureRequestContentLength ?: CAPTURE_REQUEST_CONTENT_LENGTH + + /** + * Enable the native monitoring. + */ + fun isNativeNetworkingMonitoringEnabled(): Boolean = + local?.networking?.enableNativeMonitoring ?: ENABLE_NATIVE_MONITORING_DEFAULT + + /** + * List of domains to be limited for tracking. + */ + fun getNetworkCallLimitsPerDomain(): Map { + return remote?.networkConfig?.domainLimits + ?: transformLocalDomainCfg() + ?: HashMap() + } + + private fun transformLocalDomainCfg(): Map? { + val mergedLimits: MutableMap = HashMap() + for (domain in local?.networking?.domains ?: return null) { + if (domain.domain != null && domain.limit != null) { + mergedLimits[domain.domain] = domain.limit + } + } + return mergedLimits + } + + /** + * Gets the capture limit for network calls. + */ + fun getNetworkCaptureLimit(): Int { + return remote?.networkConfig?.defaultCaptureLimit + ?: local?.networking?.defaultCaptureLimit + ?: DEFAULT_NETWORK_CALL_LIMIT + } + + /** + * Checks if the url is allowed to be reported based on the specified disabled pattern. + * + * @param url the url to test + * @return true if the url is enabled for reporting, false otherwise + */ + fun isUrlEnabled(url: String): Boolean { + val patterns = + remote?.disabledUrlPatterns ?: local?.networking?.disabledUrlPatterns ?: emptySet() + val regexes = patterns.mapNotNull { + runCatching { Pattern.compile(it) }.getOrNull() + }.toSet() + return regexes.none { it.matcher(url).find() } + } + + /** + * Whether network bodies should be captured & encrypted in the payload + */ + fun isCaptureBodyEncryptionEnabled(): Boolean = getCapturePublicKey() != null + + /** + * Supplies the public key used for network capture + */ + fun getCapturePublicKey(): String? { + var keyToClean = local?.capturePublicKey + if (keyToClean != null) { + for (dirty in dirtyKeyList) { + keyToClean = keyToClean?.replace(dirty.toRegex(), "") + } + } + return keyToClean + } + + /** + * Gets the rules for capturing network call bodies + */ + fun getNetworkCaptureRules(): Set = remote?.networkCaptureRules ?: emptySet() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior.kt new file mode 100644 index 0000000000..81623ac3d7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehavior.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.NetworkSpanForwardingRemoteConfig + +internal class NetworkSpanForwardingBehavior( + thresholdCheck: BehaviorThresholdCheck, + remoteSupplier: () -> NetworkSpanForwardingRemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + { null }, + remoteSupplier +) { + companion object { + /** + * Header name for the W3C traceparent + */ + const val TRACEPARENT_HEADER_NAME = "traceparent" + + private const val DEFAULT_PCT_ENABLED = 0.0f + } + + fun isNetworkSpanForwardingEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctEnabled ?: DEFAULT_PCT_ENABLED) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior.kt new file mode 100644 index 0000000000..16a7e5bed4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehavior.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.BaseUrlLocalConfig + +/** + * Provides the behavior that the Background Activity feature should follow. + */ +internal class SdkEndpointBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> BaseUrlLocalConfig?, +) : MergedConfigBehavior( + thresholdCheck, + localSupplier +) { + + companion object { + const val CONFIG_DEFAULT = "config.emb-api.com" + const val DATA_DEFAULT = "data.emb-api.com" + const val DATA_DEV_DEFAULT = "data-dev.emb-api.com" + } + + /** + * Data base URL. + */ + fun getData(appId: String): String = local?.data ?: "https://a-$appId.$DATA_DEFAULT" + + /** + * Data dev base URL. + */ + fun getDataDev(appId: String): String = local?.dataDev ?: "https://a-$appId.$DATA_DEV_DEFAULT" + + /** + * Config base URL. + */ + fun getConfig(appId: String): String = local?.config ?: "https://a-$appId.$CONFIG_DEFAULT" +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehavior.kt new file mode 100644 index 0000000000..7442c965ba --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehavior.kt @@ -0,0 +1,90 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import kotlin.math.max +import kotlin.math.min + +/** + * Provides whether the SDK should enable certain 'behavior' modes, such as 'integration mode' + */ +internal class SdkModeBehavior( + private val isDebug: Boolean, + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> LocalConfig?, + remoteSupplier: () -> RemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + + companion object { + + /** + * The percentage of devices which should have beta features initialized. + * + * The range of allowed values is 0.0f to 100.0f, and the default is 1.0f (1% of devices). + */ + private const val DEFAULT_BETA_FEATURES_PCT = 1.0f + + /** + * The default percentage of devices for which the SDK is enabled. + */ + private const val DEFAULT_THRESHOLD = 100 + + /** + * The default percentage offset of devices for which the SDK is enabled. + */ + private const val DEFAULT_OFFSET = 0 + } + + fun isIntegrationModeEnabled(): Boolean = local?.sdkConfig?.integrationModeEnabled ?: false + + /** + * Checks if beta features are enabled for this device. + * + * @return true if beta features should run for this device, otherwise false. + */ + fun isBetaFeaturesEnabled(): Boolean { + if (local?.sdkConfig?.betaFeaturesEnabled == false) { + return false + } + + if (isDebug) { + return true + } + + val pct = remote?.pctBetaFeaturesEnabled ?: DEFAULT_BETA_FEATURES_PCT + return thresholdCheck.isBehaviorEnabled(pct) + } + + /** + * The Embrace app ID. This is used to identify the app within the database. + */ + val appId: String by lazy { local?.appId ?: error("App ID not supplied.") } + + /** + * The % of devices that should be enabled. + */ + private fun getThreshold(): Int = remote?.threshold ?: DEFAULT_THRESHOLD + + /** + * The % at which to start enabling devices. + */ + private fun getOffset(): Int = remote?.offset ?: DEFAULT_OFFSET + + /** + * Given a Config instance, computes if the SDK is enabled based on the threshold and the offset. + * + * @return true if the sdk is enabled, false otherwise + */ + fun isSdkDisabled(): Boolean { + @Suppress("DEPRECATION") val result = thresholdCheck.getNormalizedDeviceId() + // Check if this is lower than the threshold, to determine whether + // we should enable/disable the SDK. + val lowerBound = max(0, getOffset()) + val upperBound = min(getOffset() + getThreshold(), 100) + return (lowerBound == upperBound || result < lowerBound || result > upperBound) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SessionBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SessionBehavior.kt new file mode 100644 index 0000000000..3caf6d6e53 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SessionBehavior.kt @@ -0,0 +1,153 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.EmbraceEvent.Type +import io.embrace.android.embracesdk.config.local.SessionLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.payload.EventMessage +import java.util.Locale + +/** + * Provides the behavior that functionality relating to sessions should follow. + */ +internal class SessionBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> SessionLocalConfig?, + remoteSupplier: () -> RemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + remoteSupplier +) { + + companion object { + + /** + * Default minimum allowed end session time. + */ + const val MINIMUM_SESSION_SECONDS_DEFAULT = 60 + + /** + * Do not use async mode for session end messages by default. + */ + const val ASYNC_END_DEFAULT = false + + /** + * By default, prevents to capture internal error logs as part of session payload + */ + const val ERROR_LOG_STRICT_MODE_DEFAULT = false + + const val SESSION_PROPERTY_LIMIT = 10 + } + + /** + * The maximum number of seconds a session is allowed to last for (in kiosk mode) + */ + fun getMaxSessionSecondsAllowed(): Int? { + val seconds = local?.maxSessionSeconds + if (seconds != null && seconds >= MINIMUM_SESSION_SECONDS_DEFAULT) { + return seconds + } + return null + } + + /** + * Whether sessions are allowed to be persisted async or not. + */ + fun isAsyncEndEnabled(): Boolean = + remote?.sessionConfig?.endAsync ?: local?.asyncEnd ?: ASYNC_END_DEFAULT + + /** + * Whether the limit on the number of internal exceptions in the payload should be increased + * for strict mode. + */ + fun isSessionErrorLogStrictModeEnabled(): Boolean = + local?.sessionEnableErrorLogStrictMode ?: ERROR_LOG_STRICT_MODE_DEFAULT + + /** + * The whitelist of events (crashes, errors) that should send a full session payload even + * if the gating feature is enabled. + * + * @return a whitelist of events allowed to send full session payloads + */ + fun getFullSessionEvents(): Set { + val strings = remote?.sessionConfig?.fullSessionEvents ?: local?.fullSessionEvents ?: emptySet() + return strings.map { it.toLowerCase(Locale.US) }.toSet() + } + + /** + * Returns the session components that should be recorded (e.g. breadcrumbs). + */ + fun getSessionComponents(): Set? = + remote?.sessionConfig?.sessionComponents ?: local?.sessionComponents + + /** + * Determines if the gating feature is enabled based on the presence of the session + * components list property. + * + * @return true if the gating feature is enabled + */ + fun isGatingFeatureEnabled(): Boolean = getSessionComponents() != null + + /** + * Whether session control is enabled, meaning that features should be gated. + */ + fun isSessionControlEnabled(): Boolean = remote?.sessionConfig?.isEnabled ?: false + + /** + * Returns the maximum number of properties that can be attached to a session + */ + fun getMaxSessionProperties(): Int = remote?.maxSessionProperties ?: SESSION_PROPERTY_LIMIT + + /** + * Check if should gate Moment based on gating config. + */ + fun shouldGateMoment() = shouldGateFeature(SessionGatingKeys.SESSION_MOMENTS) + + /** + * Check if should gate Info Logs based on gating config. + */ + fun shouldGateInfoLog() = shouldGateFeature(SessionGatingKeys.LOGS_INFO) + + /** + * Check if should gate Warning Logs based on gating config. + */ + fun shouldGateWarnLog() = shouldGateFeature(SessionGatingKeys.LOGS_WARN) + + /** + * Check if should gate Startup moment based on gating config. + */ + fun shouldGateStartupMoment() = shouldGateFeature(SessionGatingKeys.STARTUP_MOMENT) + + /** + * Whether a full payload should be sent for this [EventMessage] or whether payload fields + * should be gated. + */ + fun shouldSendFullMessage(eventMessage: EventMessage): Boolean { + val type = eventMessage.event.type + return (type == Type.ERROR_LOG && shouldSendFullForErrorLog()) || + (type == Type.CRASH && shouldSendFullForCrash()) + } + + /** + * Checks if a full payload should be sent for a session with an associated crash + */ + fun shouldSendFullForCrash() = + getFullSessionEvents().contains(SessionGatingKeys.FULL_SESSION_CRASHES) + + /** + * Checks if a full payload should be sent for a session with an associated error log + */ + fun shouldSendFullForErrorLog() = + getFullSessionEvents().contains(SessionGatingKeys.FULL_SESSION_ERROR_LOGS) + + /** + * Checks whether a feature should be gated. + * If [getSessionComponents] is null, this will return false. + * If [getSessionComponents] is empty, this will return true. + * If [getSessionComponents] contains the key representing the feature, this will return false. + * Otherwise, this will return true. + */ + private fun shouldGateFeature(key: String) = + getSessionComponents()?.let { !it.contains(key) } ?: false +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SpansBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SpansBehavior.kt new file mode 100644 index 0000000000..7ec0ea4b21 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/SpansBehavior.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.InternalApi +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig + +@InternalApi +internal class SpansBehavior( + thresholdCheck: BehaviorThresholdCheck, + remoteSupplier: () -> SpansRemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + { null }, + remoteSupplier +) { + companion object { + private const val DEFAULT_PCT_ENABLED = 0.0f + } + + fun isSpansEnabled(): Boolean { + return thresholdCheck.isBehaviorEnabled(remote?.pctEnabled ?: DEFAULT_PCT_ENABLED) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/StartupBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/StartupBehavior.kt new file mode 100644 index 0000000000..0f1e90205b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/StartupBehavior.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.StartupMomentLocalConfig + +/** + * Provides the behavior that the Startup moment feature should follow. + */ +internal class StartupBehavior( + thresholdCheck: BehaviorThresholdCheck, + localSupplier: () -> StartupMomentLocalConfig? +) : MergedConfigBehavior( + thresholdCheck, + localSupplier, + { null } +) { + + companion object { + const val AUTOMATICALLY_END_DEFAULT = true + } + + /** + * Controls whether the startup moment is automatically ended. + */ + fun isAutomaticEndEnabled(): Boolean = local?.automaticallyEnd ?: AUTOMATICALLY_END_DEFAULT +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/UnimplementedConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/UnimplementedConfig.kt new file mode 100644 index 0000000000..70379bdc0a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/UnimplementedConfig.kt @@ -0,0 +1,6 @@ +package io.embrace.android.embracesdk.config.behavior + +/** + * When a local or remote config doesn't exist, this type can be used as a placeholder. + */ +internal typealias UnimplementedConfig = Unit? diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior.kt new file mode 100644 index 0000000000..eab1aabe84 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/behavior/WebViewVitalsBehavior.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.RemoteConfig + +internal class WebViewVitalsBehavior( + thresholdCheck: BehaviorThresholdCheck, + remoteSupplier: () -> RemoteConfig? +) : MergedConfigBehavior( + thresholdCheck, + { null }, + remoteSupplier +) { + + companion object { + /** + * The percentage of devices which should collect web vitals + */ + private const val DEFAULT_WEB_VITALS_PCT = 100f + + /** + * The default max vitals + */ + private const val DEFAULT_MAX_VITALS = 300 + } + + private fun getWebVitalsPct(): Float = remote?.webViewVitals?.pctEnabled ?: DEFAULT_WEB_VITALS_PCT + + fun getMaxWebViewVitals(): Int = remote?.webViewVitals?.maxVitals ?: DEFAULT_MAX_VITALS + + fun isWebViewVitalsEnabled(): Boolean = thresholdCheck.isBehaviorEnabled(getWebVitalsPct()) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AnrLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AnrLocalConfig.kt new file mode 100644 index 0000000000..1f18ea76f4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AnrLocalConfig.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class AnrLocalConfig( + @SerializedName("capture_google") + val captureGoogle: Boolean? = null, + + @SerializedName("capture_unity_thread") + val captureUnityThread: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig.kt new file mode 100644 index 0000000000..b8e19b345b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppExitInfoLocalConfig.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class AppExitInfoLocalConfig( + /** + * Defines the max size of bytes to allow capturing AppExitInfo ndk/anr traces + */ + @SerializedName("app_exit_info_traces_limit") + val appExitInfoTracesLimit: Int? = null, + + @SerializedName("aei_enabled") + val aeiCaptureEnabled: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppLocalConfig.kt new file mode 100644 index 0000000000..bd41eea535 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AppLocalConfig.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class AppLocalConfig( + + @SerializedName("report_disk_usage") + val reportDiskUsage: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig.kt new file mode 100644 index 0000000000..f60ee90821 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfig.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class AutomaticDataCaptureLocalConfig( + @SerializedName("memory_info") + val memoryServiceEnabled: Boolean? = null, + + @SerializedName("power_save_mode_info") + val powerSaveModeServiceEnabled: Boolean? = null, + + @SerializedName("network_connectivity_info") + val networkConnectivityServiceEnabled: Boolean? = null, + + @SerializedName("anr_info") + val anrServiceEnabled: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfig.kt new file mode 100644 index 0000000000..7ad271cd41 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfig.kt @@ -0,0 +1,20 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +/** + * Represents the background activity configuration element specified in the Embrace config file. + */ +internal class BackgroundActivityLocalConfig( + @SerializedName("capture_enabled") + val backgroundActivityCaptureEnabled: Boolean? = null, + + @SerializedName("manual_background_activity_limit") + val manualBackgroundActivityLimit: Int? = null, + + @SerializedName("min_background_activity_duration") + val minBackgroundActivityDuration: Long? = null, + + @SerializedName("max_cached_activities") + val maxCachedActivities: Int? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfig.kt new file mode 100644 index 0000000000..7aa0dd0be1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfig.kt @@ -0,0 +1,20 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +/** + * Represents the base URLs element specified in the Embrace config file. + */ +internal class BaseUrlLocalConfig( + @SerializedName("config") + val config: String? = null, + + @SerializedName("data") + val data: String? = null, + + @SerializedName("data_dev") + val dataDev: String? = null, + + @SerializedName("images") + val images: String? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ComposeLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ComposeLocalConfig.kt new file mode 100644 index 0000000000..3e8060a5b3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ComposeLocalConfig.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class ComposeLocalConfig( + @SerializedName("capture_compose_onclick") + val captureComposeOnClick: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig.kt new file mode 100644 index 0000000000..3e15e057f2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfig.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +/** + * Represents the crash handler element specified in the Embrace config file. + */ +internal class CrashHandlerLocalConfig( + @SerializedName("enabled") + val enabled: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/DomainLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/DomainLocalConfig.kt new file mode 100644 index 0000000000..a63229f0b7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/DomainLocalConfig.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +/** + * Represents each domain element specified in the Embrace config file. + */ +internal class DomainLocalConfig( + + /** + * Url for the domain. + */ + @SerializedName("domain_name") + val domain: String? = null, + + /** + * Limit for the number of requests to be tracked. + */ + @SerializedName("domain_limit") + val limit: Int? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/LocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/LocalConfig.kt new file mode 100644 index 0000000000..32835efa53 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/LocalConfig.kt @@ -0,0 +1,116 @@ +package io.embrace.android.embracesdk.config.local + +import android.util.Base64 +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.internal.AndroidResourcesService +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logError +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logInfo +import java.lang.Boolean.parseBoolean + +internal class LocalConfig( + + /** + * The Embrace app ID. This is used to identify the app within the database. + */ + val appId: String, + + /** + * Control whether the Embrace SDK is able to capture native crashes. + */ + @SerializedName("ndk_enabled") + val ndkEnabled: Boolean, + + /** + * Local config values for the SDK, supplied by the customer. + */ + val sdkConfig: SdkLocalConfig +) { + + companion object { + + /** + * Build info app id name. + */ + const val BUILD_INFO_APP_ID = "emb_app_id" + + /** + * Build info sdk config id name. + */ + private const val BUILD_INFO_SDK_CONFIG = "emb_sdk_config" + + /** + * Build info ndk enabled. + */ + const val BUILD_INFO_NDK_ENABLED = "emb_ndk_enabled" + + /** + * The default value for native crash capture enabling + */ + const val NDK_ENABLED_DEFAULT = false + + /** + * Loads the build information from resources provided by the config file packaged within the application by Gradle at + * build-time. + * + * @return the local configuration + */ + @JvmStatic + fun fromResources( + resources: AndroidResourcesService, + packageName: String, + customAppId: String?, + serializer: EmbraceSerializer + ): LocalConfig { + return try { + val appId: String = customAppId ?: resources.getString(resources.getIdentifier(BUILD_INFO_APP_ID, "string", packageName)) + val ndkEnabledJsonId = resources.getIdentifier(BUILD_INFO_NDK_ENABLED, "string", packageName) + val ndkEnabled = when { + ndkEnabledJsonId != 0 -> parseBoolean(resources.getString(ndkEnabledJsonId)) + else -> NDK_ENABLED_DEFAULT + } && !ApkToolsConfig.IS_NDK_DISABLED + val sdkConfigJsonId = resources.getIdentifier(BUILD_INFO_SDK_CONFIG, "string", packageName) + + val sdkConfigJson: String? = when { + sdkConfigJsonId != 0 -> { + val encodedConfig = resources.getString(sdkConfigJsonId) + String(Base64.decode(encodedConfig, Base64.DEFAULT)) + } + + else -> null + } + buildConfig(appId, ndkEnabled, sdkConfigJson, serializer) + } catch (ex: Exception) { + throw IllegalStateException("Failed to load local config from resources.", ex) + } + } + + fun buildConfig( + appId: String?, + ndkEnabled: Boolean, + sdkConfigs: String?, + serializer: EmbraceSerializer + ): LocalConfig { + require(!appId.isNullOrEmpty()) { "Embrace AppId cannot be null or empty." } + + val enabledStr = when { + ndkEnabled -> "enabled" + else -> "disabled" + } + logInfo("Native crash capture is $enabledStr") + var configs: SdkLocalConfig? = null + if (!sdkConfigs.isNullOrEmpty()) { + try { + configs = serializer.fromJson(sdkConfigs, SdkLocalConfig::class.java) + } catch (ex: Exception) { + logError("Failed to parse Embrace config from config json file.", ex) + } + } + if (configs == null) { + configs = SdkLocalConfig() + } + return LocalConfig(appId, ndkEnabled, configs) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfig.kt new file mode 100644 index 0000000000..76d91e6f0f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfig.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +/** + * Represents the networking configuration element specified in the Embrace config file. + */ +internal class NetworkLocalConfig( + @SerializedName("trace_id_header") + val traceIdHeader: String? = null, + + /** + * The default capture limit for the specified domains. + */ + @SerializedName("default_capture_limit") + val defaultCaptureLimit: Int? = null, + + @SerializedName("domains") + val domains: List? = null, + + @SerializedName("capture_request_content_length") + val captureRequestContentLength: Boolean? = null, + + @SerializedName("disabled_url_patterns") + val disabledUrlPatterns: List? = null, + + @SerializedName("enable_native_monitoring") + val enableNativeMonitoring: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SdkLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SdkLocalConfig.kt new file mode 100644 index 0000000000..3bcfe6e54f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SdkLocalConfig.kt @@ -0,0 +1,116 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class SdkLocalConfig( + /** + * Service enablement config settings + */ + @SerializedName("automatic_data_capture") + val automaticDataCaptureConfig: AutomaticDataCaptureLocalConfig? = null, + + /** + * Taps + */ + @SerializedName("taps") + val taps: TapsLocalConfig? = null, + + /** + * Webview settings + */ + @SerializedName("view_config") + val viewConfig: ViewLocalConfig? = null, + + /** + * Webview settings + */ + @SerializedName("webview") + val webViewConfig: WebViewLocalConfig? = null, + + /** + * Whether integration mode should be enabled or not + */ + @SerializedName("integration_mode") + val integrationModeEnabled: Boolean? = null, + + /** + * Whether beta features should be enabled or not + */ + @SerializedName("beta_features_enabled") + val betaFeaturesEnabled: Boolean? = null, + + /** + * Crash handler settings + */ + @SerializedName("crash_handler") + val crashHandler: CrashHandlerLocalConfig? = null, + + /** + * Compose settings + */ + @SerializedName("compose") + val composeConfig: ComposeLocalConfig? = null, + + /** + * Whether fcm PII data should be hidden or not + */ + @SerializedName("capture_fcm_pii_data") + val captureFcmPiiData: Boolean? = null, + + /** + * Networking moment settings + */ + @SerializedName("networking") + val networking: NetworkLocalConfig? = null, + + @SerializedName("capture_public_key") + val capturePublicKey: String? = null, + + /** + * ANR settings + */ + @SerializedName("anr") + val anr: AnrLocalConfig? = null, + + /** + * App settings + */ + @SerializedName("app") + val app: AppLocalConfig? = null, + + /** + * Background activity config settings + */ + @SerializedName("background_activity") + val backgroundActivityConfig: BackgroundActivityLocalConfig? = null, + + /** + * Base URL settings + */ + @SerializedName("base_urls") + val baseUrls: BaseUrlLocalConfig? = null, + + /** + * Startup moment settings + */ + @SerializedName("startup_moment") + val startupMoment: StartupMomentLocalConfig? = null, + + /** + * Session config settings + */ + @SerializedName("session") + val sessionConfig: SessionLocalConfig? = null, + + /** + * Whether signal handler detection should be enabled or not + */ + @SerializedName("sig_handler_detection") + val sigHandlerDetection: Boolean? = null, + + /** + * Background activity config settings + */ + @SerializedName("app_exit_info") + val appExitInfoConfig: AppExitInfoLocalConfig? = null, +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SessionLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SessionLocalConfig.kt new file mode 100644 index 0000000000..aab578b804 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/SessionLocalConfig.kt @@ -0,0 +1,42 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +/** + * Represents the session configuration element specified in the Embrace config file. + */ +internal class SessionLocalConfig( + + /** + * Specify a maximum time before a session is allowed to exist before it is ended. + */ + @SerializedName("max_session_seconds") + val maxSessionSeconds: Int? = null, + + /** + * End session messages are sent asynchronously. + */ + @SerializedName("async_end") + val asyncEnd: Boolean? = null, + + /** + * A whitelist of session components (i.e. Breadcrumbs, Session properties, etc) that should be + * included in the session payload. The presence of this property denotes that the gating + * feature is enabled. + */ + @SerializedName("components") + val sessionComponents: Set? = null, + + /** + * A list of events (crashes, errors, etc) allowed to send a full session payload if the + * gating feature is enabled. + */ + @SerializedName("send_full_for") + val fullSessionEvents: Set? = null, + + /** + * Local/Internal logs with ERROR severity are going to be captured as part of our session payload tp monitor potential issues + */ + @SerializedName("error_log_strict_mode") + val sessionEnableErrorLogStrictMode: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfig.kt new file mode 100644 index 0000000000..2cae873504 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfig.kt @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +/** + * Represents the startup moment configuration element specified in the Embrace config file. + */ +internal class StartupMomentLocalConfig( + + @SerializedName("automatically_end") + val automaticallyEnd: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/TapsLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/TapsLocalConfig.kt new file mode 100644 index 0000000000..9df26dd5f5 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/TapsLocalConfig.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class TapsLocalConfig( + @SerializedName("capture_coordinates") + val captureCoordinates: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ViewLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ViewLocalConfig.kt new file mode 100644 index 0000000000..d93101e101 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/ViewLocalConfig.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class ViewLocalConfig( + + @SerializedName("enable_automatic_activity_capture") + val enableAutomaticActivityCapture: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfig.kt new file mode 100644 index 0000000000..df8cc1da82 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfig.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.annotations.SerializedName + +internal class WebViewLocalConfig( + @SerializedName("enable") + val captureWebViews: Boolean? = null, + + @SerializedName("capture_query_params") + val captureQueryParams: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AnrRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AnrRemoteConfig.kt new file mode 100644 index 0000000000..0e4767b35e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AnrRemoteConfig.kt @@ -0,0 +1,101 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * Configuration values relating to the ANR tracking on the app. + */ +internal data class AnrRemoteConfig( + + @SerializedName("pct_enabled") + val pctEnabled: Int? = null, + + @SerializedName("pct_pe_enabled") + val pctAnrProcessErrorsEnabled: Int? = null, + + @SerializedName("pct_bg_enabled") + val pctBgEnabled: Int? = null, + + @SerializedName("interval") + val sampleIntervalMs: Long? = null, + + @SerializedName("anr_pe_interval") + val anrProcessErrorsIntervalMs: Long? = null, + + @SerializedName("anr_pe_delay") + val anrProcessErrorsDelayMs: Long? = null, + + @SerializedName("anr_pe_sc_extra_time") + val anrProcessErrorsSchedulerExtraTimeAllowance: Long? = null, + + @SerializedName("per_interval") + val maxStacktracesPerInterval: Int? = null, + + @SerializedName("max_depth") + val stacktraceFrameLimit: Int? = null, + + @SerializedName("per_session") + val anrPerSession: Int? = null, + + @SerializedName("main_thread_only") + val mainThreadOnly: Boolean? = null, + + @SerializedName("priority") + val minThreadPriority: Int? = null, + + @SerializedName("min_duration") + val minDuration: Int? = null, + + @SerializedName("white_list") + val allowList: List? = null, + + @SerializedName("black_list") + val blockList: List? = null, + + @SerializedName("unity_ndk_sampling_factor") + val nativeThreadAnrSamplingFactor: Int? = null, + + @SerializedName("unity_ndk_sampling_unwinder") + val nativeThreadAnrSamplingUnwinder: String? = null, + + @SerializedName("pct_unity_thread_capture_enabled") + val pctNativeThreadAnrSamplingEnabled: Float? = null, + + @SerializedName("ndk_sampling_offset_enabled") + val nativeThreadAnrSamplingOffsetEnabled: Boolean? = null, + + @SerializedName("pct_idle_handler_enabled") + val pctIdleHandlerEnabled: Float? = null, + + @SerializedName("pct_strictmode_listener_enabled") + val pctStrictModeListenerEnabled: Float? = null, + + @SerializedName("strictmode_violation_limit") + val strictModeViolationLimit: Int? = null, + + @SerializedName("ignore_unity_ndk_sampling_allowlist") + val ignoreNativeThreadAnrSamplingAllowlist: Boolean? = null, + + @SerializedName("unity_ndk_sampling_allowlist") + val nativeThreadAnrSamplingAllowlist: List? = null, + + /** + * Percentage of users for which Google ANR timestamp capture is enabled. + */ + @SerializedName("google_pct_enabled") + val googlePctEnabled: Int? = null, + + @SerializedName("monitor_thread_priority") + val monitorThreadPriority: Int? = null +) { + + enum class Unwinder(internal val code: Int) { + LIBUNWIND(0), + LIBUNWINDSTACK(1); + } + + internal class AllowedNdkSampleMethod( + @SerializedName("c") val clz: String? = null, + @SerializedName("m") val method: String? = null + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AppExitInfoConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AppExitInfoConfig.kt new file mode 100644 index 0000000000..c96fadbdd3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/AppExitInfoConfig.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.config.behavior.AppExitInfoBehavior.Companion.AEI_MAX_NUM_DEFAULT + +internal class AppExitInfoConfig( + /** + * Defines the max size of bytes to allow capturing AppExitInfo ndk/anr traces + */ + @SerializedName("app_exit_info_traces_limit") + val appExitInfoTracesLimit: Int? = null, + + @SerializedName("pct_aei_enabled_v2") + val pctAeiCaptureEnabled: Float? = null, + + @SerializedName("aei_max_num") + val aeiMaxNum: Int = AEI_MAX_NUM_DEFAULT, +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig.kt new file mode 100644 index 0000000000..456e8cbf4f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/BackgroundActivityRemoteConfig.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * Configuration values relating to the background activity capturing on the app. + */ +internal data class BackgroundActivityRemoteConfig( + @SerializedName("threshold") + val threshold: Float? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig.kt new file mode 100644 index 0000000000..c282679fec --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/KillSwitchRemoteConfig.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * This contains config values which can turn risky functionality completely off. + * In normal circumstances these should never actually be used 🤞. + */ +internal data class KillSwitchRemoteConfig( + @SerializedName("sig_handler_detection") + val sigHandlerDetection: Boolean? = null, + @SerializedName("jetpack_compose") + val jetpackCompose: Boolean? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/LogRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/LogRemoteConfig.kt new file mode 100644 index 0000000000..db77abae6c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/LogRemoteConfig.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * Configuration values relating to the logs of the app. + */ +internal data class LogRemoteConfig( + + /** + * Used to truncate log messages. + */ + @SerializedName("max_length") + val logMessageMaximumAllowedLength: Int? = null, + + /** + * Limit of info logs that user is able to send. + */ + @SerializedName("info_limit") + val logInfoLimit: Int? = null, + + /** + * Limit of warning logs that user is able to send. + */ + @SerializedName("warn_limit") + val logWarnLimit: Int? = null, + + /** + * Limit of error logs that user is able to send. + */ + @SerializedName("error_limit") + val logErrorLimit: Int? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkCaptureRuleRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkCaptureRuleRemoteConfig.kt new file mode 100644 index 0000000000..789aa390ca --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkCaptureRuleRemoteConfig.kt @@ -0,0 +1,62 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +private const val NETWORK_BODY_RULE_DEFAULT_MAX_COUNT = 5 +private const val NETWORK_BODY_RULE_DEFAULT_MAX_SIZE_BYTES = 102400L + +/** + * Criteria to determine if a network body call should be captured or not. + */ +internal data class NetworkCaptureRuleRemoteConfig( + + /** + * Rule id + */ + @SerializedName("id") + val id: String, + + /** + * Duration of the network call in milliseconds. Disregard if it is less than 5000ms. + */ + @SerializedName("duration") + val duration: Long?, + + /** + * Http method to be captured. + */ + @SerializedName("method") + val method: String, + + /** + * Url regex. If the url matches this the call must be captured. + */ + @SerializedName("url") + val urlRegex: String, + + /** + * Remaining milliseconds until the rule expires. + */ + @SerializedName("expires_in") + val expiresIn: Long = 0, + + /** + * Maximum size of the network body. The data must be trimmed if it exceeds it. + */ + @SerializedName("max_size") + val maxSize: Long = NETWORK_BODY_RULE_DEFAULT_MAX_SIZE_BYTES, + + /** + * How many times this rule should be applied. + */ + @SerializedName("max_count") + val maxCount: Int = NETWORK_BODY_RULE_DEFAULT_MAX_COUNT, + + /** + * Status codes to be captured. + * -1 for capturing fail network requests. + */ + @SerializedName("status_codes") + val statusCodes: Set = setOf() + +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkRemoteConfig.kt new file mode 100644 index 0000000000..8d422d7f7d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkRemoteConfig.kt @@ -0,0 +1,24 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * Configures limit of number of requests for network calls per domain. + * + * + * If the default capture limit is specified as zero, then the config operates in allow-list + * mode, meaning only specified domains will be tracked. + */ +internal data class NetworkRemoteConfig( + + /** + * The default request capture limit for non-specified domains. + */ + val defaultCaptureLimit: Int? = null, + + /** + * Map of domain suffix to maximum number of requests. + */ + @SerializedName("domains") + val domainLimits: Map? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig.kt new file mode 100644 index 0000000000..6f0b0d9897 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/NetworkSpanForwardingRemoteConfig.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +internal data class NetworkSpanForwardingRemoteConfig( + @SerializedName("pct_enabled") + val pctEnabled: Float? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/RemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/RemoteConfig.kt new file mode 100644 index 0000000000..55eb393cd6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/RemoteConfig.kt @@ -0,0 +1,116 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * Configuration of the SDK set by the Embrace API. + */ +internal data class RemoteConfig( + + /** + * Used to determine whether or not the SDK should be activated for this device. The threshold + * identifies the percentage of devices for which the SDK is enabled. A threshold of 100 means + * that the SDK is enabled for all devices, whilst 0 means it is disabled for all devices. + */ + @SerializedName("threshold") + val threshold: Int? = null, + + /** + * Used to shift the offset of devices for which the SDK is enabled/disabled. + */ + @SerializedName("offset") + val offset: Int? = null, + + /** + * The time in milliseconds after which a particular event ID is considered 'late'. + */ + @SerializedName("event_limits") + val eventLimits: Map? = null, + + /** + * The list of [io.embrace.android.embracesdk.MessageType] which are disabled. + */ + @SerializedName("disabled_message_types") + val disabledMessageTypes: Set? = null, + + /** + * List of regular expressions matching event names and log messages which should be disabled. + */ + @SerializedName("disabled_event_and_log_patterns") + val disabledEventAndLogPatterns: Set? = null, + + /** + * List of regular expressions of URLs which should not be logged. + */ + @SerializedName("disabled_url_patterns") + val disabledUrlPatterns: Set? = null, + + /** + * Rules that will allow the specification of network requests to be captured + */ + @SerializedName("network_capture") + val networkCaptureRules: Set? = null, + + /** + * Settings relating to the user interface, such as the breadcrumb limits. + */ + @SerializedName("ui") + val uiConfig: UiRemoteConfig? = null, + + /** + * Settings defining the capture limits for network calls. + */ + @SerializedName("network") + val networkConfig: NetworkRemoteConfig? = null, + + /** + * Settings defining session control is enabled or not + */ + @SerializedName("session_control") + val sessionConfig: SessionRemoteConfig? = null, + + /** + * Settings defining the log configuration. + */ + @SerializedName("logs") + val logConfig: LogRemoteConfig? = null, + + @SerializedName("anr") + val anrConfig: AnrRemoteConfig? = null, + + @SerializedName("killswitch") + val killSwitchConfig: KillSwitchRemoteConfig? = null, + + /** + * Settings defining if internal exception capture is enabled or not + */ + @SerializedName("internal_exception_capture_enabled") + val internalExceptionCaptureEnabled: Boolean? = null, + + @SerializedName("pct_beta_features_enabled") + val pctBetaFeaturesEnabled: Float? = null, + + @SerializedName("app_exit_info") + val appExitInfoConfig: AppExitInfoConfig? = null, + + @SerializedName("background") + val backgroundActivityConfig: BackgroundActivityRemoteConfig? = null, + + /** + * The maximum number of properties that can be attached to a session + */ + @SerializedName("max_session_properties") + val maxSessionProperties: Int? = null, + + @SerializedName("spans") + val spansConfig: SpansRemoteConfig? = null, + + @SerializedName("network_span_forwarding") + val networkSpanForwardingRemoteConfig: NetworkSpanForwardingRemoteConfig? = null, + + /** + * Web view vitals settings + */ + @SerializedName("webview_vitals_beta") + val webViewVitals: WebViewVitals? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SessionRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SessionRemoteConfig.kt new file mode 100644 index 0000000000..22becf2e3b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SessionRemoteConfig.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * It serves as a session controller components. It determines if session may be ended in + * the background. It also determines which components will be sent as part of the + * session payload. This feature may be enabled/disabled. + */ +internal data class SessionRemoteConfig( + @SerializedName("enable") + val isEnabled: Boolean? = null, + + @SerializedName("async_end") + val endAsync: Boolean? = null, + + /** + * A list of session components (i.e. Breadcrumbs, Session properties, etc) that will be + * included in the session payload. If components list exists, the services should restrict + * the data that is provided to the session. + */ + @SerializedName("components") + val sessionComponents: Set? = null, + + /** + * A list of session components allowed to send a full session payload (only if "components" + * exists) + */ + @SerializedName("send_full_for") + val fullSessionEvents: Set? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SpansRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SpansRemoteConfig.kt new file mode 100644 index 0000000000..816a9d2970 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/SpansRemoteConfig.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.InternalApi + +/** + * Configuration values for the spans feature + */ +@InternalApi +internal data class SpansRemoteConfig( + @SerializedName("pct_enabled") + val pctEnabled: Float? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/UiRemoteConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/UiRemoteConfig.kt new file mode 100644 index 0000000000..106fbec4cb --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/UiRemoteConfig.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +/** + * Configuration values relating to the user interface of the app. + */ +internal data class UiRemoteConfig( + + /** + * The maximum number of custom breadcrumbs to send per session. + */ + val breadcrumbs: Int? = null, + val taps: Int? = null, + val views: Int? = null, + @SerializedName("web_views") + val webViews: Int? = null, + val fragments: Int? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/WebViewVitals.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/WebViewVitals.kt new file mode 100644 index 0000000000..e544df2f15 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/config/remote/WebViewVitals.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.config.remote + +import com.google.gson.annotations.SerializedName + +internal data class WebViewVitals @JvmOverloads constructor( + @SerializedName("pct_enabled") + val pctEnabled: Float? = null, + + @SerializedName("max_vitals") + val maxVitals: Int? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceEventService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceEventService.kt new file mode 100644 index 0000000000..2bf9298951 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceEventService.kt @@ -0,0 +1,309 @@ +package io.embrace.android.embracesdk.event + +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.CacheableValue +import io.embrace.android.embracesdk.internal.EventDescription +import io.embrace.android.embracesdk.internal.StartupEventInfo +import io.embrace.android.embracesdk.internal.spans.SpansService +import io.embrace.android.embracesdk.internal.spans.toEmbraceSpanName +import io.embrace.android.embracesdk.internal.utils.Uuid.getEmbUuid +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import io.embrace.android.embracesdk.utils.stream +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule +import java.util.NavigableMap +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.ConcurrentSkipListMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.TimeUnit + +/** + * Handles the lifecycle of events (moments). + * + * + * An event is started, timed, and then ended. If the event takes longer than a specified period of + * time, then the event is considered late. + */ +internal class EmbraceEventService( + private val startupStartTime: Long, + deliveryService: DeliveryService, + private val configService: ConfigService, + metadataService: MetadataService, + performanceInfoService: PerformanceInfoService, + userService: UserService, + private val sessionProperties: EmbraceSessionProperties, + private val logger: InternalEmbraceLogger, + workerThreadModule: WorkerThreadModule, + private val clock: Clock, + private val spansService: SpansService +) : EventService, ActivityListener, MemoryCleanerListener { + private val executorService: ExecutorService + + /** + * Timeseries of event IDs, keyed on the start time of the event. + */ + private val eventIds: NavigableMap = ConcurrentSkipListMap() + private val eventIdsCache = CacheableValue> { eventIds.size } + + /** + * Map of active events, keyed on their event ID (event name + identifier). + */ + val activeEvents: ConcurrentMap = ConcurrentHashMap() + + private var startupEventInfo: StartupEventInfo? = null + private var startupSent = false + private var processStartedByNotification = false + var eventHandler: EventHandler + + init { + + // Session properties + eventHandler = EventHandler( + metadataService, + configService, + userService, + performanceInfoService, + deliveryService, + logger, + clock, + workerThreadModule.scheduledExecutor(ExecutorName.SCHEDULED_REGISTRATION) + ) + executorService = + workerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION) + } + + override fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) { + logDeveloper("EmbraceEventService", "coldStart: $coldStart") + if (coldStart) { + // Using the system current timestamp here as the startup timestamp is related to the + // the actual SDK starts ( when the app context starts ). The app context can start + // in the background, registering a startup time that will later be sent with the + // app coming to foreground, resulting in a *pretty* long startup moment. + sendStartupMoment() + } + } + + override fun applicationStartupComplete() { + if (processStartedByNotification) { + activeEvents.remove(STARTUP_EVENT_NAME) + logDeveloper("EmbraceEventService", "Application startup started by data notification") + } else if (configService.startupBehavior.isAutomaticEndEnabled()) { + logDeveloper("EmbraceEventService", "Automatically ending startup event") + endEvent(STARTUP_EVENT_NAME) + } else { + logDeveloper("EmbraceEventService", "Application startup automatically end is disabled") + } + } + + override fun sendStartupMoment() { + logDeveloper("EmbraceEventService", "sendStartupMoment") + synchronized(this) { + if (startupSent) { + logDeveloper("EmbraceEventService", "Startup is already sent") + return + } + startupSent = true + } + logger.logDebug("Sending startup start event.") + startEvent( + STARTUP_EVENT_NAME, + null, + null, + startupStartTime + ) + } + + override fun setProcessStartedByNotification() { + processStartedByNotification = true + } + + override fun startEvent(name: String) { + // extract constant + startEvent(name, null, null, null) + } + + override fun startEvent(name: String, identifier: String?) { + startEvent(name, identifier, null, null) + } + + override fun startEvent(name: String, identifier: String?, properties: Map?) { + startEvent(name, identifier, properties, null) + } + + override fun startEvent( + name: String, + identifier: String?, + properties: Map?, + startTime: Long? + ) { + var sanitizedStartTime = startTime + try { + logDeveloper("EmbraceEventService", "Start event: $name") + if (!eventHandler.isAllowedToStart(name)) { + logDeveloper("EmbraceEventService", "Event handler not allowed to start ") + return + } + val eventKey = getInternalEventKey(name, identifier) + if (activeEvents.containsKey(eventKey)) { + logDeveloper("EmbraceEventService", "Ending previous event with same name") + endEvent(name, identifier, false, null) + } + val now = clock.now() + if (sanitizedStartTime == null) { + sanitizedStartTime = now + } + val eventId = getEmbUuid() + eventIds[now] = eventId + val eventDescription = eventHandler.onEventStarted( + eventId, + name, + sanitizedStartTime, + sessionProperties, + properties, + Runnable { endEvent(name, identifier, true, null) } + ) + + // event started, update active events + activeEvents[eventKey] = eventDescription + logDeveloper("EmbraceEventService", "Event started : $name") + } catch (ex: Exception) { + logger.logError( + "Cannot start event with name: $name, identifier: $identifier due to an exception", + ex, false + ) + } + } + + override fun endEvent(name: String) { + endEvent(name, null, false, null) + } + + override fun endEvent(name: String, identifier: String?) { + endEvent(name, identifier, false, null) + } + + override fun endEvent(name: String, properties: Map?) { + endEvent(name, null, false, properties) + } + + override fun endEvent(name: String, identifier: String?, properties: Map?) { + endEvent(name, identifier, false, properties) + } + + private fun endEvent( + name: String, + identifier: String?, + late: Boolean, + properties: Map? + ) { + try { + logDeveloper("EmbraceEventService", "Ending event: $name") + if (!eventHandler.isAllowedToEnd()) { + logDeveloper("EmbraceEventService", "Event handler not allowed to end") + return + } + val eventKey = getInternalEventKey(name, identifier) + val originEventDescription: EventDescription? = when { + late -> activeEvents[eventKey] + else -> activeEvents.remove(eventKey) + } + if (originEventDescription == null) { + // We avoid logging that there's no startup event in the activeEvents collection + // as the user might have completed it manually on a @StartupActivity. + if (!isStartupEvent(name)) { + logger.logError( + "No start event found when ending an event with name: $name, identifier: $identifier" + ) + } + return + } + val (event) = eventHandler.onEventEnded( + originEventDescription, + late, + properties, + sessionProperties + ) + if (isStartupEvent(name)) { + logStartupSpan() + logDeveloper("EmbraceEventService", "Ending Startup Ending") + startupEventInfo = eventHandler.buildStartupEventInfo( + originEventDescription.event, + event + ) + } + } catch (ex: Exception) { + logger.logError( + "Cannot end event with name: $name, identifier: $identifier due to an exception", + ex + ) + } + } + + override fun findEventIdsForSession(startTime: Long, endTime: Long): List { + logDeveloper("EmbraceEventService", "findEventIdsForSession") + return eventIdsCache.value { ArrayList(eventIds.subMap(startTime, endTime).values) } + } + + override fun getActiveEventIds(): List { + val ids: MutableList = ArrayList() + stream(activeEvents.values) { (_, event): EventDescription -> + event.eventId?.let(ids::add) + } + return ids + } + + override fun getStartupMomentInfo(): StartupEventInfo? = startupEventInfo + + override fun close() { + cleanCollections() + logDeveloper("EmbraceEventService", "close") + } + + override fun cleanCollections() { + eventIds.clear() + activeEvents.clear() + logDeveloper("EmbraceEventService", "collections cleaned") + } + + /** + * Return the active event with the given name and identifier if it exists. Return null otherwise. + */ + fun getActiveEvent(eventName: String, identifier: String?): EventDescription? { + return activeEvents[getInternalEventKey(eventName, identifier)] + } + + private fun logStartupSpan() { + val startupEndTimeMillis = clock.now() + executorService.submit { + spansService.recordCompletedSpan( + name = STARTUP_SPAN_NAME, + startTimeNanos = TimeUnit.MILLISECONDS.toNanos(startupStartTime), + endTimeNanos = TimeUnit.MILLISECONDS.toNanos(startupEndTimeMillis), + internal = false + ) + } + } + + companion object { + const val STARTUP_EVENT_NAME = "_startup" + private val STARTUP_SPAN_NAME = "startup-moment".toEmbraceSpanName() + + internal fun getInternalEventKey(eventName: String, identifier: String?): String = + when (identifier) { + null, "" -> eventName + else -> "$eventName#$identifier" + } + + internal fun isStartupEvent(eventName: String): Boolean = STARTUP_EVENT_NAME == eventName + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceRemoteLogger.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceRemoteLogger.kt new file mode 100644 index 0000000000..73e5ee408f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EmbraceRemoteLogger.kt @@ -0,0 +1,504 @@ +package io.embrace.android.embracesdk.event + +import io.embrace.android.embracesdk.Embrace.AppFramework +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.LogExceptionType +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.LogMessageBehavior +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.internal.CacheableValue +import io.embrace.android.embracesdk.internal.MessageType +import io.embrace.android.embracesdk.internal.utils.Uuid.getEmbUuid +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.NetworkCapturedCall +import io.embrace.android.embracesdk.payload.NetworkEvent +import io.embrace.android.embracesdk.payload.Stacktraces +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import java.sql.Timestamp +import java.util.NavigableMap +import java.util.concurrent.Callable +import java.util.concurrent.ConcurrentSkipListMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.atomic.AtomicInteger + +/** + * Logs messages remotely, so that they can be viewed as events during a user's session. + */ +internal class EmbraceRemoteLogger constructor( + private val metadataService: MetadataService, + private val deliveryService: DeliveryService, + private val userService: UserService, + private val configService: ConfigService, + private val sessionProperties: EmbraceSessionProperties, + private val logger: InternalEmbraceLogger, + private val clock: Clock, + private val executorService: ExecutorService, + private val gatingService: GatingService, + private val networkConnectivityService: NetworkConnectivityService +) : MemoryCleanerListener { + + private val lock = Any() + private val infoLogIds: NavigableMap = ConcurrentSkipListMap() + private val warningLogIds: NavigableMap = ConcurrentSkipListMap() + private val errorLogIds: NavigableMap = ConcurrentSkipListMap() + private val networkLogIds: NavigableMap = ConcurrentSkipListMap() + private val logsInfoCount = AtomicInteger(0) + private val logsErrorCount = AtomicInteger(0) + private val logsWarnCount = AtomicInteger(0) + private val unhandledExceptionCount = AtomicInteger(0) + private val infoLogIdsCache = CacheableValue> { infoLogIds.size } + private val warningLogIdsCache = CacheableValue> { warningLogIds.size } + private val errorLogIdsCache = CacheableValue> { errorLogIds.size } + private val networkLogIdsCache = CacheableValue> { networkLogIds.size } + + constructor( + metadataService: MetadataService, + deliveryService: DeliveryService, + userService: UserService, + configService: ConfigService, + sessionProperties: EmbraceSessionProperties, + logger: InternalEmbraceLogger, + clock: Clock, + sessionGatingService: GatingService, + networkConnectivityService: NetworkConnectivityService, + executorService: ExecutorService + ) : this( + metadataService, + deliveryService, + userService, + configService, + sessionProperties, + logger, + clock, + executorService, + sessionGatingService, + networkConnectivityService + ) + + /** + * Creates a network event. + * + * @param networkCaptureCall the captured network information + */ + fun logNetwork(networkCaptureCall: NetworkCapturedCall?) { + val networkEventTimestamp = clock.now() + if (networkCaptureCall == null) { + logDebug("NetworkCaptureCall is null, nothing to log") + return + } + try { + logDeveloper("EmbraceRemoteLogger", "Attempting to log network data") + executorService.submit( + Callable { + synchronized(lock) { + val id = getEmbUuid() + networkLogIds[networkEventTimestamp] = id + val optionalSessionId = metadataService.activeSessionId + val networkEvent = NetworkEvent( + metadataService.getAppId(), + metadataService.getAppInfo(), + metadataService.getDeviceId(), + id, + networkCaptureCall, + Timestamp(networkEventTimestamp).toString(), + networkConnectivityService.ipAddress, + optionalSessionId + ) + logDeveloper("EmbraceRemoteLogger", "Attempt to Send NETWORK Event") + deliveryService.sendNetworkCall(networkEvent) + logDeveloper( + "EmbraceRemoteLogger", + "LogNetwork api call running in background job" + ) + } + null + } + ) + } catch (ex: Exception) { + logDebug("Failed to log network call using Embrace SDK.", ex) + } + } + + /** + * Creates a remote log. + * + * @param message the message to log + * @param type the type of message to log, which must be INFO_LOG, WARNING_LOG, or ERROR_LOG + * @param properties custom properties to send as part of the event + */ + fun log( + message: String, + type: EmbraceEvent.Type, + properties: Map? + ) { + log( + message, + type, + LogExceptionType.NONE, + properties, + null, + null, + AppFramework.NATIVE, + null, + null, + null, + null + ) + } + + /** + * Creates a remote log. + * + * @param message the message to log + * @param type the type of message to log, which must be INFO_LOG, WARNING_LOG, or ERROR_LOG + * @param logExceptionType whether the log is a handled exception, unhandled, or non an exception + * @param properties custom properties to send as part of the event + * @param stackTraceElements the stacktrace elements of a throwable + * @param customStackTrace stacktrace string for non-JVM exceptions + * @param exceptionName the exception name of a Throwable is it is present + * @param exceptionMessage the exception message of a Throwable is it is present + */ + @Suppress("CyclomaticComplexMethod", "ComplexMethod", "LongParameterList") + fun log( + message: String, + type: EmbraceEvent.Type, + logExceptionType: LogExceptionType, + properties: Map?, + stackTraceElements: Array?, + customStackTrace: String?, + framework: AppFramework, + context: String?, + library: String?, + exceptionName: String?, + exceptionMessage: String? + ) { + logDeveloper("EmbraceRemoteLogger", "Attempting to log") + val timestamp = clock.now() + val stacktraces = Stacktraces( + if (stackTraceElements != null) getWrappedStackTrace(stackTraceElements) else getWrappedStackTrace(), + customStackTrace, + framework, + context, + library + ) + + // As the event is sent asynchronously and user info may change, preserve the user info + // at the time of the log call + val logUserInfo = userService.getUserInfo() + logDeveloper("EmbraceRemoteLogger", "Added user info to log") + executorService.submit( + Callable { + synchronized(lock) { + if (!configService.dataCaptureEventBehavior.isLogMessageEnabled(message)) { + logger.logWarning("Log message disabled. Ignoring log with message $message") + return@Callable null + } + if (!configService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.LOG)) { + logger.logWarning("Log message disabled. Ignoring all Logs.") + return@Callable null + } + val id = getEmbUuid() + if (type == EmbraceEvent.Type.INFO_LOG) { + logDeveloper("EmbraceRemoteLogger", "New INFO log") + logsInfoCount.incrementAndGet() + if (infoLogIds.size < configService.logMessageBehavior.getInfoLogLimit()) { + logDeveloper( + "EmbraceRemoteLogger", + "Logging INFO log number $logsInfoCount" + ) + infoLogIds[timestamp] = id + } else { + logger.logWarning("Info Log limit has been reached.") + return@Callable null + } + } else if (type == EmbraceEvent.Type.WARNING_LOG) { + logsWarnCount.incrementAndGet() + if (warningLogIds.size < configService.logMessageBehavior.getWarnLogLimit()) { + logDeveloper( + "EmbraceRemoteLogger", + "Logging WARNING log number $logsWarnCount" + ) + warningLogIds[timestamp] = id + } else { + logger.logWarning("Warning Log limit has been reached.") + return@Callable null + } + } else if (type == EmbraceEvent.Type.ERROR_LOG) { + logsErrorCount.incrementAndGet() + if (errorLogIds.size < configService.logMessageBehavior.getErrorLogLimit()) { + logDeveloper( + "EmbraceRemoteLogger", + "Logging ERROR log number $logsErrorCount" + ) + errorLogIds[timestamp] = id + } else { + logger.logWarning("Error Log limit has been reached.") + return@Callable null + } + } else { + logger.logWarning("Unknown log level $type") + return@Callable null + } + val processedMessage: String + if (framework == AppFramework.UNITY) { + logDeveloper("EmbraceRemoteLogger", "Process Unity Log message") + processedMessage = processUnityLogMessage(message) + if (logExceptionType == LogExceptionType.UNHANDLED) { + unhandledExceptionCount.incrementAndGet() + } + } else if (framework == AppFramework.FLUTTER) { + logDeveloper("EmbraceRemoteLogger", "Process Flutter Log message") + processedMessage = processLogMessage(message) + if (logExceptionType == LogExceptionType.UNHANDLED) { + unhandledExceptionCount.incrementAndGet() + } + } else { + logDeveloper("EmbraceRemoteLogger", "Process simple Log message") + processedMessage = processLogMessage(message) + } + + // TODO validate event metadata here! + var sessionId: String? = null + val optionalSessionId = metadataService.activeSessionId + if (optionalSessionId != null) { + logDeveloper("EmbraceRemoteLogger", "Adding SessionId to event") + sessionId = optionalSessionId + } + val event = Event( + processedMessage, + id, + getEmbUuid(), + sessionId, + type, + clock.now(), + null, + screenshotTaken = false, + null, + metadataService.getAppState(), + properties, + sessionProperties.get(), + null, + logExceptionType.value, + exceptionName, + exceptionMessage, + framework.value + ) + + // Build event message + val eventMessage = EventMessage( + event, + null, + metadataService.getDeviceInfo(), + metadataService.getAppInfo(), + logUserInfo, + null, + stacktraces, + ApiClient.MESSAGE_VERSION, + null + ) + if (checkIfShouldGateLog(type)) { + logger.logDebug("$type was gated by config. The event wasnot sent.") + return@Callable null + } + + // Sanitize log event + val logEvent = gatingService.gateEventMessage(eventMessage) + logDeveloper("EmbraceRemoteLogger", "Attempt to Send log Event") + deliveryService.sendLogs(logEvent) + logDeveloper("EmbraceRemoteLogger", "LogEvent api call running in background job") + } + null + } + ) + } + + /** + * Finds all IDs of log events at info level within the given time window. + * + * @param startTime the beginning of the time window + * @param endTime the end of the time window + * @return the list of log IDs within the specified range + */ + fun findInfoLogIds(startTime: Long, endTime: Long): List { + return findLogIds(startTime, endTime, infoLogIdsCache, infoLogIds) + } + + /** + * Finds all IDs of log events at warning level within the given time window. + * + * @param startTime the beginning of the time window + * @param endTime the end of the time window + * @return the list of log IDs within the specified range + */ + fun findWarningLogIds(startTime: Long, endTime: Long): List { + return findLogIds(startTime, endTime, warningLogIdsCache, warningLogIds) + } + + /** + * Finds all IDs of log events at error level within the given time window. + * + * @param startTime the beginning of the time window + * @param endTime the end of the time window + * @return the list of log IDs within the specified range + */ + fun findErrorLogIds(startTime: Long, endTime: Long): List { + return findLogIds(startTime, endTime, errorLogIdsCache, errorLogIds) + } + + /** + * Finds all IDs of log network events within the given time window. + * + * @param startTime the beginning of the time window + * @param endTime the end of the time window + * @return the list of log IDs within the specified range + */ + fun findNetworkLogIds(startTime: Long, endTime: Long): List { + return findLogIds(startTime, endTime, networkLogIdsCache, networkLogIds) + } + + private fun findLogIds( + startTime: Long, + endTime: Long, + cache: CacheableValue>, + logIds: NavigableMap + ): List { + return cache.value { ArrayList(logIds.subMap(startTime, endTime).values) } + } + + /** + * The total number of info logs that the app attempted to send. + */ + fun getInfoLogsAttemptedToSend(): Int = logsInfoCount.get() + + /** + * The total number of warning logs that the app attempted to send. + */ + fun getWarnLogsAttemptedToSend(): Int = logsWarnCount.get() + + /** + * The total number of error logs that the app attempted to send. + */ + fun getErrorLogsAttemptedToSend(): Int = logsErrorCount.get() + fun getUnhandledExceptionsSent(): Int { + if (unhandledExceptionCount.get() > 0) { + logDeveloper( + "EmbraceRemoteLogger", + "UnhandledException number: $unhandledExceptionCount" + ) + } + return unhandledExceptionCount.get() + } + + private fun processLogMessage( + message: String, + maxLength: Int = configService.logMessageBehavior.getLogMessageMaximumAllowedLength() + ): String { + return if (message.length > maxLength) { + logDeveloper( + "EmbraceRemoteLogger", + "Message length exceeds the allowed max length" + ) + val endChars = "..." + + // ensure that we never end up with a negative offset when extracting substring, regardless of the config value set + val allowedLength = when { + maxLength >= endChars.length -> maxLength - endChars.length + else -> LogMessageBehavior.LOG_MESSAGE_MAXIMUM_ALLOWED_LENGTH - endChars.length + } + logger.logWarning("Truncating message to ${message.length} characters") + message.substring(0, allowedLength) + endChars + } else { + logDeveloper("EmbraceRemoteLogger", "Allowed message length") + message + } + } + + private fun processUnityLogMessage(message: String): String { + return processLogMessage(message, LOG_MESSAGE_UNITY_MAXIMUM_ALLOWED_LENGTH) + } + + /** + * Checks if the info or warning log event should be gated based on gating config. Error logs + * should never be gated. + * + * @param type of the log event + * @return true if the log should be gated + */ + fun checkIfShouldGateLog(type: EmbraceEvent.Type?): Boolean { + return when (type) { + EmbraceEvent.Type.INFO_LOG -> { + val shouldGate = configService.sessionBehavior.shouldGateInfoLog() + logDeveloper( + "EmbraceRemoteLogger", + "Should gate INFO log: $shouldGate" + ) + shouldGate + } + + EmbraceEvent.Type.WARNING_LOG -> { + val shouldGate = configService.sessionBehavior.shouldGateWarnLog() + logDeveloper( + "EmbraceRemoteLogger", + "Should gate WARN log: $shouldGate" + ) + shouldGate + } + + else -> { + logDeveloper( + "EmbraceRemoteLogger", + "Should gate log: false" + ) + false + } + } + } + + override fun cleanCollections() { + logsInfoCount.set(0) + logsWarnCount.set(0) + logsErrorCount.set(0) + unhandledExceptionCount.set(0) + infoLogIds.clear() + warningLogIds.clear() + errorLogIds.clear() + networkLogIds.clear() + logDeveloper("EmbraceRemoteLogger", "Collections cleaned") + } + + companion object { + + /** + * The default limit of Unity log messages that can be sent. + */ + private const val LOG_MESSAGE_UNITY_MAXIMUM_ALLOWED_LENGTH = 16384 + + /** + * Gets the stack trace of the throwable. + * + * @return the stack trace of a throwable + */ + @JvmStatic + fun getWrappedStackTrace( + stackTraceElements: Array = Thread.currentThread().stackTrace + ): List { + logDeveloper("EmbraceRemoteLogger", "Processing wrapped stack trace") + val augmentedStackReturnAddresses: MutableList = ArrayList() + for (element in stackTraceElements) { + augmentedStackReturnAddresses.add(element.toString()) + } + return augmentedStackReturnAddresses + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventHandler.kt new file mode 100644 index 0000000000..b6d6b809c6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventHandler.kt @@ -0,0 +1,239 @@ +package io.embrace.android.embracesdk.event + +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.EventDescription +import io.embrace.android.embracesdk.internal.MessageType +import io.embrace.android.embracesdk.internal.StartupEventInfo +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +/** + * The time default period after which an event is considered 'late'. + */ +private const val DEFAULT_LATE_THRESHOLD_MILLIS = 5000L + +/** + * This class is in charge of building events and sending them to our servers. + */ +internal class EventHandler( + private val metadataService: MetadataService, + private val configService: ConfigService, + private val userService: UserService, + private val performanceInfoService: PerformanceInfoService, + private val deliveryService: DeliveryService, + private val logger: InternalEmbraceLogger, + private val clock: Clock, + private val scheduledExecutor: ScheduledExecutorService +) { + /** + * Responsible for handling the start of an event. + */ + fun onEventStarted( + eventId: String, + eventName: String, + startTime: Long, + sessionProperties: EmbraceSessionProperties, + eventProperties: Map?, + timeoutCallback: Runnable + ): EventDescription { + val threshold = calculateLateThreshold(eventId) + val event = buildStartEvent( + eventId, + eventName, + startTime, + threshold, + sessionProperties, + eventProperties + ) + + val timer = scheduledExecutor.schedule( + timeoutCallback, + threshold - calculateOffset(startTime, threshold), + TimeUnit.MILLISECONDS + ) + + if (shouldSendMoment(eventName)) { + val eventMessage = buildStartEventMessage(event) + deliveryService.sendEventAsync(eventMessage) + } else { + logger.logDebug("$eventName start moment not sent based on gating config.") + } + + return EventDescription(timer, event) + } + + /** + * Responsible for handling ending an event. + * + * @return the event message for the end event + */ + fun onEventEnded( + originEventDescription: EventDescription, + late: Boolean, + eventProperties: Map?, + sessionProperties: EmbraceSessionProperties + ): EventMessage { + val event: Event = originEventDescription.event + val startTime = event.timestamp ?: 0 + val endTime = clock.now() + val duration = Math.max(0, endTime - startTime) + // cancel late scheduler + originEventDescription.lateTimer.cancel(false) + + val endEvent = buildEndEvent( + event, + endTime, + duration, + late, + sessionProperties, + eventProperties + ) + val endEventMessage = buildEndEventMessage(endEvent, startTime, endTime) + + if (shouldSendMoment(event.name)) { + deliveryService.sendEventAsync(endEventMessage) + } else { + logger.logDebug("${event.name} end moment not sent based on gating config.") + } + + return endEventMessage + } + + /** + * It determines if given event is allowed to be started. + */ + fun isAllowedToStart(eventName: String): Boolean { + return if (eventName.isNullOrEmpty()) { + logger.logWarning("Event name is empty. Ignoring this event.") + false + } else if (!configService.dataCaptureEventBehavior.isEventEnabled(eventName)) { + logger.logWarning("Event disabled. Ignoring event with name $eventName") + false + } else if (!configService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.EVENT)) { + logger.logWarning("Event message disabled. Ignoring all Events.") + false + } else if (scheduledExecutor.isShutdown()) { + logger.logError("Cannot start event as service is shut down") + false + } else { + true + } + } + + /** + * It determines the handler is allowed to end + */ + fun isAllowedToEnd(): Boolean { + return if (!configService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.EVENT)) { + logger.logWarning("Event message disabled. Ignoring all Events.") + false + } else { + true + } + } + + fun buildStartupEventInfo(originEvent: Event, endEvent: Event): StartupEventInfo = + StartupEventInfo( + endEvent.duration, + originEvent.lateThreshold + ) + + private fun buildEndEventMessage(event: Event, startTime: Long, endTime: Long) = + EventMessage( + event = event, + performanceInfo = performanceInfoService.getPerformanceInfo(startTime, endTime, false), + userInfo = userService.getUserInfo() + ) + + /** + * Checks if the moment (startup moment or a regular moment) should not be sent based on the + * gating config. + * + * @param name of the moment + * @return true if should be gated + */ + private fun shouldSendMoment(name: String?): Boolean { + return if (name == EmbraceEventService.STARTUP_EVENT_NAME) { + !configService.sessionBehavior.shouldGateStartupMoment() + } else { + !configService.sessionBehavior.shouldGateMoment() + } + } + + private fun buildStartEventMessage(event: Event) = + EventMessage( + event = event, + userInfo = userService.getUserInfo(), + appInfo = metadataService.getAppInfo(), + deviceInfo = metadataService.getDeviceInfo() + ) + + private fun buildStartEvent( + eventId: String, + eventName: String, + startTime: Long, + threshold: Long, + sessionProperties: EmbraceSessionProperties, + eventProperties: Map? + ): Event { + return Event( + name = eventName, + sessionId = metadataService.activeSessionId, + eventId = eventId, + type = EmbraceEvent.Type.START, + appState = metadataService.getAppState(), + lateThreshold = threshold, + timestamp = startTime, + sessionProperties = sessionProperties.get(), + customProperties = eventProperties + ) + } + + private fun buildEndEvent( + originEvent: Event, + endTime: Long, + duration: Long, + late: Boolean, + sessionProperties: EmbraceSessionProperties, + eventProperties: Map? + ): Event { + return Event( + name = originEvent.name, + eventId = originEvent.eventId, + sessionId = metadataService.activeSessionId, + timestamp = endTime, + duration = duration, + appState = metadataService.getAppState(), + type = if (late) EmbraceEvent.Type.LATE else EmbraceEvent.Type.END, + customProperties = eventProperties, + sessionProperties = sessionProperties.get() + ) + } + + private fun calculateOffset(startTime: Long, threshold: Long): Long { + // Ensure we adjust the threshold to take into account backdated events + return Math.min(threshold, Math.max(0, clock.now() - startTime)) + } + + private fun calculateLateThreshold(eventId: String): Long { + // Check whether a late threshold has been configured, otherwise use the default + val limits = configService.dataCaptureEventBehavior.getEventLimits() + + val value = limits[eventId] + + return when { + value == null || !limits.containsKey(eventId) -> DEFAULT_LATE_THRESHOLD_MILLIS + else -> value + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventService.kt new file mode 100644 index 0000000000..cfa73feb4f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/event/EventService.kt @@ -0,0 +1,129 @@ +package io.embrace.android.embracesdk.event + +import io.embrace.android.embracesdk.internal.StartupEventInfo +import java.io.Closeable + +/** + * Provides event lifecycle management for the SDK. + * + * A start event is submitted, followed by an end event, and then the duration is timed. These + * events appear on the session timeline. + * + * A story ID refers to the UUID for a particular event. An event ID is the concatenation of the + * user-supplied event name, and the event identifier. + */ +internal interface EventService : Closeable { + + /** + * Starts an event. + * + * @param name the name of the event + */ + fun startEvent(name: String) + + /** + * Starts an event. + * + * @param name the name of the event + * @param identifier the identifier, to uniquely distinguish between events with the same name + */ + fun startEvent(name: String, identifier: String?) + + /** + * Starts an event. + * + * @param name the name of the event + * @param identifier the identifier, to uniquely distinguish between events with the same name + * @param properties custom properties which can be sent as part of the request + */ + fun startEvent( + name: String, + identifier: String?, + properties: Map? + ) + + /** + * Starts an event. + * + * @param name the name of the event + * @param identifier the identifier, to uniquely distinguish between events with the same name + * @param properties custom properties which can be sent as part of the request + * @param startTime a back-dated time at which the event occurred + */ + fun startEvent( + name: String, + identifier: String?, + properties: Map?, + startTime: Long? + ) + + /** + * Ends an event which matches the given name. + * + * @param name the name of the event to terminate + */ + fun endEvent(name: String) + + /** + * Ends an event which matches the given name and identifier. + * + * @param name the name of the event to terminate + * @param identifier the unique identifier of the event to terminate + */ + fun endEvent(name: String, identifier: String?) + + /** + * Ends an event which matches the given name and identifier. + * + * @param name the name of the event to terminate + * @param properties custom properties which can be sent as part of the moment + */ + fun endEvent(name: String, properties: Map?) + + /** + * Ends an event which matches the given name and identifier. + * + * @param name the name of the event to terminate + * @param identifier the unique identifier of the event to terminate + * @param properties custom properties which can be sent as part of the moment + */ + fun endEvent( + name: String, + identifier: String?, + properties: Map? + ) + + /** + * Finds all event IDs (event UUIDs) within the given time window. + * + * @param startTime the start time of the window to search + * @param endTime the end time of the window to search + * @return the list of story IDs within the specified range + */ + fun findEventIdsForSession(startTime: Long, endTime: Long): List + + /** + * Gets all of the IDs of the currently active moments. + * + * @return list of IDs for the currently active moments + */ + fun getActiveEventIds(): List? + + /** + * Get startup duration and startup threshold info of the cold start session. + * + * @return the startup moment info + */ + fun getStartupMomentInfo(): StartupEventInfo? + + /** + * Triggered when the application startup has started; + */ + fun sendStartupMoment() + + /** + * Set a flag if the process was started by a data notification; + * Used to avoid track startup times since arrive a notification; + */ + fun setProcessStartedByNotification() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EmbraceGatingService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EmbraceGatingService.kt new file mode 100644 index 0000000000..3324709ed7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EmbraceGatingService.kt @@ -0,0 +1,88 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.SessionMessage + +/** + * Receives the local and remote config to build the Gating config and define the amount of + * data (components) that the SDK sends to the backend as part of sessions or event messages. + * The service is listening to the remote config changes and determines if the gating config should + * be updated. + * Event service, Session service and Crash service check if should gate data based on the gating config. + * Also defines if a full session data should be sent under certain conditions based on configurations. + */ +internal class EmbraceGatingService( + private val configService: ConfigService +) : GatingService { + + /** + * This class manages the configuration of the Gating feature. The Gating configuration consists of two lists: + * 'components' and a secondary list for special events. + * + * sessionComponents: This list functions as a whitelist for determining the information to include + * in the next session message. Its state impacts the gating feature as follows: + * + * - If the 'components' list is null, the gating feature is disabled and all data can be included + * in the next session message. + * - If the 'components' list is empty, the gating feature is enabled but blocks all components from + * being included in the next session message. + * - If the 'components' list contains specific fields, only those fields should be included in the + * next session message. + * + * fullSessionEvents list: If this list contains entries such as "CRASH" or "EVENT", + * the SDK should include the full payload for sessions that incorporate a crash or event, + * regardless of the 'components' list status. + * + */ + override fun gateSessionMessage(sessionMessage: SessionMessage): SessionMessage { + val components = configService.sessionBehavior.getSessionComponents() + if (components != null && configService.sessionBehavior.isGatingFeatureEnabled()) { + InternalStaticEmbraceLogger.logDebug("Session gating feature enabled. Attempting to sanitize the session message") + + // check if the session has error logs IDs. If so, send the full session payload. + if (sessionMessage.session.errorLogIds?.isNotEmpty() == true && + configService.sessionBehavior.shouldSendFullForErrorLog() + ) { + logDeveloper( + "EmbraceGatingService", "Error logs detected - Sending full session payload" + ) + return sessionMessage + } + + // check if the session has a crash report id. If so, send the full session payload. + if (sessionMessage.session.crashReportId != null) { + logDeveloper( + "EmbraceGatingService", "Crash detected - Sending full session payload" + ) + return sessionMessage + } + + return SessionSanitizerFacade(sessionMessage, components).getSanitizedMessage() + } + + logDeveloper("EmbraceGatingService", "Gating feature disabled") + return sessionMessage + } + + override fun gateEventMessage(eventMessage: EventMessage): EventMessage { + val components = configService.sessionBehavior.getSessionComponents() + if (components != null && configService.sessionBehavior.isGatingFeatureEnabled()) { + InternalStaticEmbraceLogger.logDebug("Session gating feature enabled. Attempting to sanitize the event message") + + if (configService.sessionBehavior.shouldSendFullMessage(eventMessage)) { + logDeveloper( + "EmbraceGatingService", "Crash or error detected - Sending full session payload" + ) + return eventMessage + } + + return EventSanitizerFacade(eventMessage, components).getSanitizedMessage() + } + + logDeveloper("EmbraceGatingService", "Gating feature disabled") + return eventMessage + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizer.kt new file mode 100644 index 0000000000..85abd3787f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizer.kt @@ -0,0 +1,57 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.gating.SessionGatingKeys.LOG_PROPERTIES +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_PROPERTIES +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.Event + +internal class EventSanitizer( + private val event: Event, + private val enabledComponents: Set +) : Sanitizable { + + override fun sanitize(): Event { + InternalStaticEmbraceLogger.logger.logDeveloper("EventSanitizer", "sanitize") + var customPropertiesMap = event.customPropertiesMap + var sessionPropertiesMap = event.sessionPropertiesMap + + InternalStaticEmbraceLogger.logger.logDeveloper( + "EventSanitizer", + "isLogEvent: " + isLogEvent() + ) + if (isLogEvent()) { + if (!shouldSendLogProperties()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "EventSanitizer", + "not shouldSendLogProperties" + ) + customPropertiesMap = null + } + } + + if (!shouldSendSessionProperties()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "EventSanitizer", + "not shouldSendSessionProperties" + ) + sessionPropertiesMap = null + } + + return event.copy( + customProperties = customPropertiesMap, + sessionProperties = sessionPropertiesMap + ) + } + + private fun isLogEvent() = + event.type == EmbraceEvent.Type.ERROR_LOG || + event.type == EmbraceEvent.Type.WARNING_LOG || + event.type == EmbraceEvent.Type.INFO_LOG + + private fun shouldSendLogProperties() = + enabledComponents.contains(LOG_PROPERTIES) + + private fun shouldSendSessionProperties() = + enabledComponents.contains(SESSION_PROPERTIES) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizerFacade.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizerFacade.kt new file mode 100644 index 0000000000..9b4e42db17 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/EventSanitizerFacade.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.EventMessage + +internal class EventSanitizerFacade( + private val eventMessage: EventMessage, + private val components: Set +) { + + fun getSanitizedMessage(): EventMessage { + InternalStaticEmbraceLogger.logger.logDeveloper( + "EventSanitizerFacade", + "getSanitizedMessage" + ) + val sanitizedEvent = EventSanitizer(eventMessage.event, components).sanitize() + val sanitizedUserInfo = UserInfoSanitizer(eventMessage.userInfo, components).sanitize() + val sanitizedPerformanceInfo = + PerformanceInfoSanitizer(eventMessage.performanceInfo, components).sanitize() + + return eventMessage.copy( + event = sanitizedEvent, + userInfo = sanitizedUserInfo, + performanceInfo = sanitizedPerformanceInfo + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/GatingService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/GatingService.kt new file mode 100644 index 0000000000..9d899b67a6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/GatingService.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.SessionMessage + +internal interface GatingService { + + /** + * Sanitizes a session message before send it to backend based on the Gating configuration. + * Breadcrumbs, session properties, ANRs, logs, etc can be removed from the session payload. + * This method should be called before send the session message to the ApiClient class. + * + * @param sessionMessage to be sanitized + */ + fun gateSessionMessage(sessionMessage: SessionMessage): SessionMessage + + /** + * Sanitizes an event message before send it to backend based on the Gating configuration. + * Log properties, stacktraces, etc can be removed from the event payload. + * This method should be called before send the event message to the ApiClient class. + * + * @param eventMessage to be sanitized + */ + fun gateEventMessage(eventMessage: EventMessage): EventMessage +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/PerformanceInfoSanitizer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/PerformanceInfoSanitizer.kt new file mode 100644 index 0000000000..1416eb8e99 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/PerformanceInfoSanitizer.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_ANR +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_CONNECTIVITY +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_CURRENT_DISK_USAGE +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_LOW_MEMORY +import io.embrace.android.embracesdk.payload.PerformanceInfo + +internal class PerformanceInfoSanitizer( + private val info: PerformanceInfo?, + private val enabledComponents: Set +) : + Sanitizable { + override fun sanitize(): PerformanceInfo? { + return info?.copy( + anrIntervals = anrIntervals(info), + networkInterfaceIntervals = networkInterfaceIntervals(info), + memoryWarnings = memoryWarnings(info), + diskUsage = diskUsage(info), + networkRequests = networkRequests(info) + ) + } + + private fun anrIntervals(performanceInfo: PerformanceInfo) = when { + shouldSendANRs() -> performanceInfo.anrIntervals + else -> null + } + + private fun networkInterfaceIntervals(performanceInfo: PerformanceInfo) = when { + shouldSendNetworkConnectivityIntervals() -> performanceInfo.networkInterfaceIntervals + else -> null + } + + private fun memoryWarnings(performanceInfo: PerformanceInfo) = when { + shouldSendLowMemoryWarnings() -> performanceInfo.memoryWarnings + else -> null + } + + private fun diskUsage(performanceInfo: PerformanceInfo) = when { + shouldSendCurrentDiskUsage() -> performanceInfo.diskUsage + else -> null + } + + private fun networkRequests(performanceInfo: PerformanceInfo) = when { + shouldSendCapturedNetwork() -> performanceInfo.networkRequests + else -> null + } + + private fun shouldSendANRs() = + enabledComponents.contains(PERFORMANCE_ANR) + + private fun shouldSendCurrentDiskUsage() = + enabledComponents.contains(PERFORMANCE_CURRENT_DISK_USAGE) + + private fun shouldSendNetworkConnectivityIntervals() = + enabledComponents.contains(PERFORMANCE_CONNECTIVITY) + + private fun shouldSendLowMemoryWarnings() = + enabledComponents.contains(PERFORMANCE_LOW_MEMORY) + + private fun shouldSendCapturedNetwork() = + enabledComponents.contains(SessionGatingKeys.PERFORMANCE_NETWORK) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/Sanitizable.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/Sanitizable.kt new file mode 100644 index 0000000000..2084ab9265 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/Sanitizable.kt @@ -0,0 +1,6 @@ +package io.embrace.android.embracesdk.gating + +internal interface Sanitizable { + + fun sanitize(): T? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionGatingKeys.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionGatingKeys.kt new file mode 100644 index 0000000000..51e56acc78 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionGatingKeys.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.gating + +/** + * Represents the session components that should be added to the payload if the feature gating is + * enabled by config. + * Also defines events (like crashes or error logs) that must send full session payloads. + */ +internal object SessionGatingKeys { + const val BREADCRUMBS_TAPS = "br_tb" + const val BREADCRUMBS_VIEWS = "br_vb" + const val BREADCRUMBS_CUSTOM_VIEWS = "br_cv" + const val BREADCRUMBS_WEB_VIEWS = "br_wv" + const val BREADCRUMBS_CUSTOM = "br_cb" + const val LOG_PROPERTIES = "log_pr" + const val SESSION_PROPERTIES = "s_props" + const val SESSION_ORIENTATIONS = "s_oc" + const val SESSION_USER_TERMINATION = "s_tr" + const val SESSION_MOMENTS = "s_mts" + const val LOGS_INFO = "log_in" + const val LOGS_WARN = "log_war" + const val STARTUP_MOMENT = "mts_st" + const val USER_PERSONAS = "ur_per" + const val PERFORMANCE_ANR = "pr_anr" + const val PERFORMANCE_CONNECTIVITY = "pr_ns" + const val PERFORMANCE_NETWORK = "pr_nr" + const val PERFORMANCE_CPU = "pr_cp" + const val PERFORMANCE_LOW_MEMORY = "pr_mw" + const val PERFORMANCE_CURRENT_DISK_USAGE = "pr_ds" + + // Events that can send full session payloads + const val FULL_SESSION_CRASHES = "crashes" + const val FULL_SESSION_ERROR_LOGS = "errors" +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizer.kt new file mode 100644 index 0000000000..4ba1833c75 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizer.kt @@ -0,0 +1,102 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.gating.SessionGatingKeys.LOGS_INFO +import io.embrace.android.embracesdk.gating.SessionGatingKeys.LOGS_WARN +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_MOMENTS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_ORIENTATIONS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_PROPERTIES +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_USER_TERMINATION +import io.embrace.android.embracesdk.gating.SessionGatingKeys.STARTUP_MOMENT +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logger +import io.embrace.android.embracesdk.payload.Session + +internal class SessionSanitizer( + private val session: Session, + private val enabledComponents: Set +) : Sanitizable { + + @Suppress("ComplexMethod") + override fun sanitize(): Session { + logger.logDeveloper("SessionSanitizer", "sanitize") + + val properties = when { + !shouldSendSessionProperties() -> null + else -> session.properties + } + val orientations = when { + !shouldSendTrackedOrientations() -> null + else -> session.orientations + } + val terminationTime = when { + !shouldSendUserTerminations() -> null + else -> session.terminationTime + } + val receivedTermination = when { + !shouldSendUserTerminations() -> null + else -> session.isReceivedTermination + } + val infoLogIds = when { + !shouldSendInfoLog() -> null + else -> session.infoLogIds + } + val infoLogsAttemptedToSend = when { + !shouldSendInfoLog() -> null + else -> session.infoLogsAttemptedToSend + } + val warnLogIds = when { + !shouldSendWarnLog() -> null + else -> session.warningLogIds + } + val warnLogsAttemptedToSend = when { + !shouldSendWarnLog() -> null + else -> session.warnLogsAttemptedToSend + } + val eventIds = when { + !shouldSendMoment() -> null + else -> session.eventIds + } + val startupDuration = when { + !shouldSendStartupMoment() -> null + else -> session.startupDuration + } + val startupThreshold = when { + !shouldSendStartupMoment() -> null + else -> session.startupThreshold + } + return session.copy( + betaFeatures = null, // always disable beta features if the gating config has been enabled + properties = properties, + orientations = orientations, + terminationTime = terminationTime, + isReceivedTermination = receivedTermination, + infoLogIds = infoLogIds, + infoLogsAttemptedToSend = infoLogsAttemptedToSend, + warningLogIds = warnLogIds, + warnLogsAttemptedToSend = warnLogsAttemptedToSend, + eventIds = eventIds, + startupDuration = startupDuration, + startupThreshold = startupThreshold + ) + } + + private fun shouldSendSessionProperties() = + enabledComponents.contains(SESSION_PROPERTIES) + + private fun shouldSendTrackedOrientations() = + enabledComponents.contains(SESSION_ORIENTATIONS) + + private fun shouldSendUserTerminations() = + enabledComponents.contains(SESSION_USER_TERMINATION) + + private fun shouldSendMoment() = + enabledComponents.contains(SESSION_MOMENTS) + + private fun shouldSendInfoLog() = + enabledComponents.contains(LOGS_INFO) + + private fun shouldSendWarnLog() = + enabledComponents.contains(LOGS_WARN) + + private fun shouldSendStartupMoment() = + enabledComponents.contains(STARTUP_MOMENT) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizerFacade.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizerFacade.kt new file mode 100644 index 0000000000..e5d578070d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/SessionSanitizerFacade.kt @@ -0,0 +1,30 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbsSanitizer +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.SessionMessage + +internal class SessionSanitizerFacade( + private val sessionMessage: SessionMessage, + private val components: Set +) { + + fun getSanitizedMessage(): SessionMessage { + InternalStaticEmbraceLogger.logger.logDeveloper( + "SessionSanitizerFacade", + "getSanitizedMessage" + ) + val sanitizedSession = SessionSanitizer(sessionMessage.session, components).sanitize() + val sanitizedUserInfo = UserInfoSanitizer(sessionMessage.userInfo, components).sanitize() + val sanitizedPerformanceInfo = PerformanceInfoSanitizer(sessionMessage.performanceInfo, components).sanitize() + val sanitizedBreadcrumbs = + BreadcrumbsSanitizer(sessionMessage.breadcrumbs, components).sanitize() + + return sessionMessage.copy( + session = sanitizedSession, + userInfo = sanitizedUserInfo, + performanceInfo = sanitizedPerformanceInfo, + breadcrumbs = sanitizedBreadcrumbs + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/UserInfoSanitizer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/UserInfoSanitizer.kt new file mode 100644 index 0000000000..ff36cf44b1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/gating/UserInfoSanitizer.kt @@ -0,0 +1,34 @@ +package io.embrace.android.embracesdk.gating + +import io.embrace.android.embracesdk.gating.SessionGatingKeys.USER_PERSONAS +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.payload.UserInfo + +internal class UserInfoSanitizer( + private val userInfo: UserInfo?, + private val enabledComponents: Set +) : Sanitizable { + + override fun sanitize(): UserInfo { + if (userInfo == null) { + return UserInfo() + } + + if (!shouldSendUserPersonas()) { + InternalStaticEmbraceLogger.logger.logDeveloper( + "UserInfoSanitizer", + "not shouldSendUserPersonas" + ) + return userInfo.copy(personas = null) + } + + InternalStaticEmbraceLogger.logger.logDeveloper( + "UserInfoSanitizer", + "sanitize - userInfo: " + userInfo.userId + ) + return userInfo + } + + private fun shouldSendUserPersonas() = + enabledComponents.contains(USER_PERSONAS) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AndroidServicesModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AndroidServicesModule.kt new file mode 100644 index 0000000000..2d6de48e6e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AndroidServicesModule.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.injection + +import android.preference.PreferenceManager +import io.embrace.android.embracesdk.prefs.EmbracePreferencesService +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +internal interface AndroidServicesModule { + val preferencesService: PreferencesService +} + +internal class AndroidServicesModuleImpl( + initModule: InitModule, + coreModule: CoreModule, + workerThreadModule: WorkerThreadModule, +) : AndroidServicesModule { + override val preferencesService: PreferencesService by singleton { + val lazyPrefs = lazy { + PreferenceManager.getDefaultSharedPreferences( + coreModule.context + ) + } + EmbracePreferencesService( + workerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION), + lazyPrefs, + initModule.clock, + coreModule.jsonSerializer + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AnrModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AnrModule.kt new file mode 100644 index 0000000000..efd59afb11 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/AnrModule.kt @@ -0,0 +1,143 @@ +package io.embrace.android.embracesdk.injection + +import android.os.Looper +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.anr.EmbraceAnrService +import io.embrace.android.embracesdk.anr.NoOpAnrService +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorSampler +import io.embrace.android.embracesdk.anr.detection.BlockedThreadDetector +import io.embrace.android.embracesdk.anr.detection.LivenessCheckScheduler +import io.embrace.android.embracesdk.anr.detection.TargetThreadHandler +import io.embrace.android.embracesdk.anr.detection.ThreadMonitoringState +import io.embrace.android.embracesdk.anr.sigquit.FilesDelegate +import io.embrace.android.embracesdk.anr.sigquit.FindGoogleThread +import io.embrace.android.embracesdk.anr.sigquit.GetThreadCommand +import io.embrace.android.embracesdk.anr.sigquit.GetThreadsInCurrentProcess +import io.embrace.android.embracesdk.anr.sigquit.GoogleAnrHandlerNativeDelegate +import io.embrace.android.embracesdk.anr.sigquit.GoogleAnrTimestampRepository +import io.embrace.android.embracesdk.anr.sigquit.SigquitDetectionService +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicReference + +internal interface AnrModule { + val googleAnrTimestampRepository: GoogleAnrTimestampRepository + val anrService: AnrService +} + +internal class AnrModuleImpl( + initModule: InitModule, + coreModule: CoreModule, + systemServiceModule: SystemServiceModule, + essentialServiceModule: EssentialServiceModule +) : AnrModule { + + private val configService = essentialServiceModule.configService + + override val googleAnrTimestampRepository: GoogleAnrTimestampRepository by singleton { + GoogleAnrTimestampRepository(coreModule.logger) + } + + override val anrService: AnrService by singleton { + if (configService.autoDataCaptureBehavior.isAnrServiceEnabled() && !ApkToolsConfig.IS_ANR_MONITORING_DISABLED) { + // the customer didn't enable early ANR detection, so construct the service + // as part of normal initialization. + EmbraceAnrService( + configService = configService, + looper = looper, + logger = coreModule.logger, + sigquitDetectionService = sigquitDetectionService, + livenessCheckScheduler = livenessCheckScheduler, + anrExecutorService = anrExecutorService, + state = state, + anrProcessErrorSampler = anrProcessErrorSampler, + clock = initModule.clock, + anrMonitorThread = anrMonitorThread + ) + } else { + NoOpAnrService() + } + } + + private val looper by singleton { Looper.getMainLooper() } + + private val state by singleton { ThreadMonitoringState(initModule.clock) } + + private val targetThreadHandler by singleton { + TargetThreadHandler( + looper = looper, + anrExecutorService = anrExecutorService, + anrMonitorThread = anrMonitorThread, + configService = configService, + clock = initModule.clock + ) + } + + private val blockedThreadDetector by singleton { + BlockedThreadDetector( + configService = configService, + clock = initModule.clock, + state = state, + targetThread = looper.thread, + anrMonitorThread = anrMonitorThread + ) + } + + private val livenessCheckScheduler by singleton { + LivenessCheckScheduler( + configService = configService, + anrExecutor = anrExecutorService, + clock = initModule.clock, + state = state, + targetThreadHandler = targetThreadHandler, + blockedThreadDetector = blockedThreadDetector, + anrMonitorThread = anrMonitorThread + ) + } + + private val anrProcessErrorSampler by singleton { + AnrProcessErrorSampler( + activityManager = systemServiceModule.activityManager, + configService = configService, + anrExecutor = anrExecutorService, + clock = initModule.clock, + logger = coreModule.logger + ) + } + + private val sigquitDetectionService: SigquitDetectionService by singleton { + val filesDelegate = FilesDelegate() + + SigquitDetectionService( + sharedObjectLoader = SharedObjectLoader(), + findGoogleThread = FindGoogleThread( + coreModule.logger, + GetThreadsInCurrentProcess(filesDelegate), + GetThreadCommand(filesDelegate) + ), + googleAnrHandlerNativeDelegate = GoogleAnrHandlerNativeDelegate(googleAnrTimestampRepository, coreModule.logger), + googleAnrTimestampRepository = googleAnrTimestampRepository, + configService = configService, + logger = coreModule.logger + ) + } + + private val anrMonitorThreadFactory = ThreadFactory { runnable: Runnable -> + Executors.defaultThreadFactory().newThread(runnable).apply { + anrMonitorThread.set(this) + name = "emb-anr-monitor" + } + } + + private val anrExecutorService: ScheduledExecutorService by lazy { + // must only have one thread in executor pool - synchronization model relies on this fact. + Executors.newSingleThreadScheduledExecutor( + anrMonitorThreadFactory + ) + } + + private val anrMonitorThread = AtomicReference() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CoreModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CoreModule.kt new file mode 100644 index 0000000000..e623fbae4c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CoreModule.kt @@ -0,0 +1,92 @@ +package io.embrace.android.embracesdk.injection + +import android.app.Application +import android.content.Context +import android.content.pm.ApplicationInfo +import io.embrace.android.embracesdk.Embrace.AppFramework +import io.embrace.android.embracesdk.internal.AndroidResourcesService +import io.embrace.android.embracesdk.internal.EmbraceAndroidResourcesService +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.registry.ServiceRegistry + +/** + * Contains a core set of dependencies that are required by most services/classes in the SDK. + * This includes a reference to the application context, a clock, logger, etc... + */ +internal interface CoreModule { + + /** + * Reference to the context. This will always return the application context so won't leak. + */ + val context: Context + + /** + * Reference to the current application. + */ + val application: Application + + /** + * The framework the SDK is running on + */ + val appFramework: AppFramework + + /** + * Returns an interface that logs messages + */ + val logger: InternalEmbraceLogger + + /** + * Returns the service registry. This is used to register services that need to be closed + */ + val serviceRegistry: ServiceRegistry + + /** + * Returns the serializer used to serialize data to JSON + */ + val jsonSerializer: EmbraceSerializer + + /** + * Returns an service to retrieve Android resources + */ + val resources: AndroidResourcesService + + /** + * Whether the application is a debug build + */ + val isDebug: Boolean +} + +internal class CoreModuleImpl( + ctx: Context, + override val appFramework: AppFramework +) : CoreModule { + + override val context: Context by singleton { + when (ctx) { + is Application -> ctx + else -> ctx.applicationContext + } + } + + override val application by singleton { context as Application } + + override val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger + + override val serviceRegistry: ServiceRegistry by singleton { + ServiceRegistry(logger) + } + + override val jsonSerializer: EmbraceSerializer by singleton { + EmbraceSerializer() + } + + override val resources: AndroidResourcesService by singleton { + EmbraceAndroidResourcesService(context) + } + + override val isDebug: Boolean by lazy { context.applicationInfo.isDebug() } +} + +internal fun ApplicationInfo.isDebug() = flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CrashModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CrashModule.kt new file mode 100644 index 0000000000..b511458905 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CrashModule.kt @@ -0,0 +1,61 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.capture.crash.CrashService +import io.embrace.android.embracesdk.capture.crash.EmbraceCrashService +import io.embrace.android.embracesdk.internal.crash.CrashFileMarker +import io.embrace.android.embracesdk.internal.crash.LastRunCrashVerifier +import io.embrace.android.embracesdk.ndk.NativeModule +import io.embrace.android.embracesdk.samples.AutomaticVerificationExceptionHandler +import java.io.File + +/** + * Contains dependencies that capture crashes + */ +internal interface CrashModule { + val lastRunCrashVerifier: LastRunCrashVerifier + val crashService: CrashService + val automaticVerificationExceptionHandler: AutomaticVerificationExceptionHandler +} + +internal class CrashModuleImpl( + initModule: InitModule, + essentialServiceModule: EssentialServiceModule, + deliveryModule: DeliveryModule, + nativeModule: NativeModule, + sessionModule: SessionModule, + anrModule: AnrModule, + dataContainerModule: DataContainerModule, + coreModule: CoreModule +) : CrashModule { + + private val crashMarker: CrashFileMarker by singleton { + val markerFile = lazy { File(coreModule.context.cacheDir.path, CrashFileMarker.CRASH_MARKER_FILE_NAME) } + CrashFileMarker(markerFile) + } + + override val lastRunCrashVerifier: LastRunCrashVerifier by singleton { + LastRunCrashVerifier(crashMarker) + } + + override val crashService: CrashService by singleton { + EmbraceCrashService( + essentialServiceModule.configService, + sessionModule.sessionService, + essentialServiceModule.metadataService, + deliveryModule.deliveryService, + essentialServiceModule.userService, + dataContainerModule.eventService, + anrModule.anrService, + nativeModule.ndkService, + essentialServiceModule.gatingService, + sessionModule.backgroundActivityService, + crashMarker, + initModule.clock + ) + } + + override val automaticVerificationExceptionHandler: AutomaticVerificationExceptionHandler by singleton { + val prevHandler = Thread.getDefaultUncaughtExceptionHandler() + AutomaticVerificationExceptionHandler(prevHandler) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CustomerLogModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CustomerLogModule.kt new file mode 100644 index 0000000000..94a03a9b8a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/CustomerLogModule.kt @@ -0,0 +1,64 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.network.logging.EmbraceNetworkCaptureService +import io.embrace.android.embracesdk.network.logging.EmbraceNetworkLoggingService +import io.embrace.android.embracesdk.network.logging.NetworkCaptureService +import io.embrace.android.embracesdk.network.logging.NetworkLoggingService +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +/** + * Holds dependencies that are required for a customer to send log messages to the backend. + */ +internal interface CustomerLogModule { + val networkCaptureService: NetworkCaptureService + val networkLoggingService: NetworkLoggingService + val remoteLogger: EmbraceRemoteLogger +} + +internal class CustomerLogModuleImpl( + initModule: InitModule, + coreModule: CoreModule, + androidServicesModule: AndroidServicesModule, + essentialServiceModule: EssentialServiceModule, + deliveryModule: DeliveryModule, + sessionProperties: EmbraceSessionProperties, + dataCaptureServiceModule: DataCaptureServiceModule, + workerThreadModule: WorkerThreadModule +) : CustomerLogModule { + + override val networkCaptureService: NetworkCaptureService by singleton { + EmbraceNetworkCaptureService( + essentialServiceModule.metadataService, + androidServicesModule.preferencesService, + remoteLogger, + essentialServiceModule.configService, + coreModule.jsonSerializer + ) + } + + override val networkLoggingService: NetworkLoggingService by singleton { + EmbraceNetworkLoggingService( + essentialServiceModule.configService, + coreModule.logger, + networkCaptureService + ) + } + + override val remoteLogger: EmbraceRemoteLogger by singleton { + EmbraceRemoteLogger( + essentialServiceModule.metadataService, + deliveryModule.deliveryService, + essentialServiceModule.userService, + essentialServiceModule.configService, + sessionProperties, + coreModule.logger, + initModule.clock, + essentialServiceModule.gatingService, + dataCaptureServiceModule.networkConnectivityService, + workerThreadModule.backgroundExecutor(ExecutorName.REMOTE_LOGGING) + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModule.kt new file mode 100644 index 0000000000..b451207311 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModule.kt @@ -0,0 +1,182 @@ +package io.embrace.android.embracesdk.injection + +import android.os.Build +import io.embrace.android.embracesdk.capture.connectivity.EmbraceNetworkConnectivityService +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.connectivity.NoOpNetworkConnectivityService +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbService +import io.embrace.android.embracesdk.capture.crumbs.EmbraceBreadcrumbService +import io.embrace.android.embracesdk.capture.crumbs.PushNotificationCaptureService +import io.embrace.android.embracesdk.capture.crumbs.activity.ActivityLifecycleBreadcrumbService +import io.embrace.android.embracesdk.capture.crumbs.activity.EmbraceActivityLifecycleBreadcrumbService +import io.embrace.android.embracesdk.capture.memory.EmbraceMemoryService +import io.embrace.android.embracesdk.capture.memory.MemoryService +import io.embrace.android.embracesdk.capture.memory.NoOpMemoryService +import io.embrace.android.embracesdk.capture.powersave.EmbracePowerSaveModeService +import io.embrace.android.embracesdk.capture.powersave.NoOpPowerSaveModeService +import io.embrace.android.embracesdk.capture.powersave.PowerSaveModeService +import io.embrace.android.embracesdk.capture.strictmode.EmbraceStrictModeService +import io.embrace.android.embracesdk.capture.strictmode.NoOpStrictModeService +import io.embrace.android.embracesdk.capture.strictmode.StrictModeService +import io.embrace.android.embracesdk.capture.thermalstate.EmbraceThermalStatusService +import io.embrace.android.embracesdk.capture.thermalstate.NoOpThermalStatusService +import io.embrace.android.embracesdk.capture.thermalstate.ThermalStatusService +import io.embrace.android.embracesdk.capture.webview.EmbraceWebViewService +import io.embrace.android.embracesdk.capture.webview.WebViewService +import io.embrace.android.embracesdk.utils.BuildVersionChecker +import io.embrace.android.embracesdk.utils.VersionChecker +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +/** + * This modules provides services that capture data from within an application. It could be argued + * that a lot of classes could fit in this module, so to keep it small (<15 properties) it's best + * to only include services whose main responsibility is just capturing data. It would be well + * worth reassessing the grouping once this module grows larger. + */ +internal interface DataCaptureServiceModule { + + /** + * Captures breadcrumbs + */ + val breadcrumbService: BreadcrumbService + + /** + * Captures memory events + */ + val memoryService: MemoryService + + /** + * Captures intervals where power save mode was enabled + */ + val powerSaveModeService: PowerSaveModeService + + /** + * Captures intervals where the network was/wasn't connected + */ + val networkConnectivityService: NetworkConnectivityService + + /** + * Captures information from webviews + */ + val webviewService: WebViewService + + /** + * Captures push notifications + */ + val pushNotificationService: PushNotificationCaptureService + + /** + * Captures strict mode violations + */ + val strictModeService: StrictModeService + + /** + * Captures thermal state events + */ + val thermalStatusService: ThermalStatusService + + /** + * Captures breadcrumbs of the activity lifecycle + */ + val activityLifecycleBreadcrumbService: ActivityLifecycleBreadcrumbService? +} + +internal class DataCaptureServiceModuleImpl @JvmOverloads constructor( + initModule: InitModule, + coreModule: CoreModule, + systemServiceModule: SystemServiceModule, + essentialServiceModule: EssentialServiceModule, + workerThreadModule: WorkerThreadModule, + versionChecker: VersionChecker = BuildVersionChecker +) : DataCaptureServiceModule { + + private val backgroundExecutorService = workerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION) + private val scheduledExecutor = workerThreadModule.scheduledExecutor(ExecutorName.SCHEDULED_REGISTRATION) + private val configService = essentialServiceModule.configService + + override val memoryService: MemoryService by singleton { + if (configService.autoDataCaptureBehavior.isMemoryServiceEnabled()) { + EmbraceMemoryService(initModule.clock) + } else { + NoOpMemoryService() + } + } + + override val powerSaveModeService: PowerSaveModeService by singleton { + if (configService.autoDataCaptureBehavior.isPowerSaveModeServiceEnabled() && versionChecker.isAtLeast( + Build.VERSION_CODES.LOLLIPOP + ) + ) { + EmbracePowerSaveModeService( + coreModule.context, + backgroundExecutorService, + initModule.clock, + systemServiceModule.powerManager + ) + } else { + NoOpPowerSaveModeService() + } + } + + override val networkConnectivityService: NetworkConnectivityService by singleton { + if (configService.autoDataCaptureBehavior.isNetworkConnectivityServiceEnabled()) { + EmbraceNetworkConnectivityService( + coreModule.context, + initModule.clock, + backgroundExecutorService, + coreModule.logger, + systemServiceModule.connectivityManager + ) + } else { + NoOpNetworkConnectivityService() + } + } + + override val webviewService: WebViewService by singleton { + EmbraceWebViewService(configService, coreModule.jsonSerializer) + } + + override val breadcrumbService: BreadcrumbService by singleton { + EmbraceBreadcrumbService( + initModule.clock, + configService, + coreModule.logger + ) + } + + override val pushNotificationService: PushNotificationCaptureService by singleton { + PushNotificationCaptureService( + breadcrumbService, coreModule.logger + ) + } + + override val strictModeService: StrictModeService by singleton { + if (versionChecker.isAtLeast(Build.VERSION_CODES.P) && configService.anrBehavior.isStrictModeListenerEnabled()) { + EmbraceStrictModeService(configService, scheduledExecutor, initModule.clock) + } else { + NoOpStrictModeService() + } + } + + override val thermalStatusService: ThermalStatusService by singleton { + if (configService.sdkModeBehavior.isBetaFeaturesEnabled() && versionChecker.isAtLeast(Build.VERSION_CODES.Q)) { + EmbraceThermalStatusService( + scheduledExecutor, + initModule.clock, + coreModule.logger, + systemServiceModule.powerManager + ) + } else { + NoOpThermalStatusService() + } + } + + override val activityLifecycleBreadcrumbService: EmbraceActivityLifecycleBreadcrumbService? by singleton { + if (configService.sdkModeBehavior.isBetaFeaturesEnabled() && versionChecker.isAtLeast(Build.VERSION_CODES.Q)) { + EmbraceActivityLifecycleBreadcrumbService(configService, initModule.clock) + } else { + null + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataContainerModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataContainerModule.kt new file mode 100644 index 0000000000..5429bbca5f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DataContainerModule.kt @@ -0,0 +1,87 @@ +package io.embrace.android.embracesdk.injection + +import android.os.Build +import io.embrace.android.embracesdk.capture.EmbracePerformanceInfoService +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.aei.ApplicationExitInfoService +import io.embrace.android.embracesdk.capture.aei.EmbraceApplicationExitInfoService +import io.embrace.android.embracesdk.capture.aei.NoOpApplicationExitInfoService +import io.embrace.android.embracesdk.event.EmbraceEventService +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.ndk.NativeModule +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.utils.BuildVersionChecker +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +/** + * Holds dependencies that normally act as a 'container' for other data. For example, + * a span, an Event, PerformanceInfo, etc. + */ +internal interface DataContainerModule { + val applicationExitInfoService: ApplicationExitInfoService + val performanceInfoService: PerformanceInfoService + val eventService: EventService +} + +internal class DataContainerModuleImpl( + initModule: InitModule, + coreModule: CoreModule, + workerThreadModule: WorkerThreadModule, + systemServiceModule: SystemServiceModule, + androidServicesModule: AndroidServicesModule, + essentialServiceModule: EssentialServiceModule, + dataCaptureServiceModule: DataCaptureServiceModule, + anrModule: AnrModule, + customerLogModule: CustomerLogModule, + deliveryModule: DeliveryModule, + nativeModule: NativeModule, + sessionProperties: EmbraceSessionProperties, + startTime: Long +) : DataContainerModule { + + override val applicationExitInfoService: ApplicationExitInfoService by singleton { + if (BuildVersionChecker.isAtLeast(Build.VERSION_CODES.R)) { + EmbraceApplicationExitInfoService( + workerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION), + essentialServiceModule.configService, + systemServiceModule.activityManager, + androidServicesModule.preferencesService, + deliveryModule.deliveryService + ) + } else { + NoOpApplicationExitInfoService() + } + } + + override val performanceInfoService: PerformanceInfoService by singleton { + EmbracePerformanceInfoService( + anrModule.anrService, + dataCaptureServiceModule.networkConnectivityService, + customerLogModule.networkLoggingService, + dataCaptureServiceModule.powerSaveModeService, + dataCaptureServiceModule.memoryService, + essentialServiceModule.metadataService, + anrModule.googleAnrTimestampRepository, + applicationExitInfoService, + dataCaptureServiceModule.strictModeService, + nativeModule.nativeThreadSamplerService + ) + } + + override val eventService: EventService by singleton { + EmbraceEventService( + startTime, + deliveryModule.deliveryService, + essentialServiceModule.configService, + essentialServiceModule.metadataService, + performanceInfoService, + essentialServiceModule.userService, + sessionProperties, + coreModule.logger, + workerThreadModule, + initModule.clock, + initModule.spansService + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DeliveryModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DeliveryModule.kt new file mode 100644 index 0000000000..11b2e12d5a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DeliveryModule.kt @@ -0,0 +1,71 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.comms.delivery.CacheService +import io.embrace.android.embracesdk.comms.delivery.DeliveryCacheManager +import io.embrace.android.embracesdk.comms.delivery.DeliveryNetworkManager +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.comms.delivery.EmbraceCacheService +import io.embrace.android.embracesdk.comms.delivery.EmbraceDeliveryService +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +internal interface DeliveryModule { + val cacheService: CacheService + val deliveryCacheManager: DeliveryCacheManager + val deliveryNetworkManager: DeliveryNetworkManager + val deliveryService: DeliveryService +} + +internal class DeliveryModuleImpl( + initModule: InitModule, + coreModule: CoreModule, + essentialServiceModule: EssentialServiceModule, + dataCaptureServiceModule: DataCaptureServiceModule, + workerThreadModule: WorkerThreadModule +) : DeliveryModule { + + private val cachedSessionsExecutorService = workerThreadModule.backgroundExecutor(ExecutorName.CACHED_SESSIONS) + private val sendSessionsExecutorService = workerThreadModule.backgroundExecutor(ExecutorName.SEND_SESSIONS) + private val deliveryCacheExecutorService = workerThreadModule.backgroundExecutor(ExecutorName.DELIVERY_CACHE) + private val apiRetryExecutor = workerThreadModule.scheduledExecutor(ExecutorName.API_RETRY) + + override val cacheService: CacheService by singleton { + EmbraceCacheService(coreModule.context, coreModule.jsonSerializer, coreModule.logger) + } + + override val deliveryCacheManager: DeliveryCacheManager by singleton { + DeliveryCacheManager( + cacheService, + deliveryCacheExecutorService, + coreModule.logger, + initModule.clock, + coreModule.jsonSerializer + ) + } + + override val deliveryNetworkManager: DeliveryNetworkManager by singleton { + DeliveryNetworkManager( + essentialServiceModule.metadataService, + essentialServiceModule.urlBuilder, + essentialServiceModule.apiClient, + deliveryCacheManager, + coreModule.logger, + essentialServiceModule.configService, + apiRetryExecutor, + dataCaptureServiceModule.networkConnectivityService, + coreModule.jsonSerializer, + essentialServiceModule.userService + ) + } + + override val deliveryService: DeliveryService by singleton { + EmbraceDeliveryService( + deliveryCacheManager, + deliveryNetworkManager, + cachedSessionsExecutorService, + sendSessionsExecutorService, + coreModule.logger, + essentialServiceModule.configService + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DependencyInjection.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DependencyInjection.kt new file mode 100644 index 0000000000..8caf4cbfa9 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/DependencyInjection.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk.injection + +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * How the dependency should be loaded. + */ +internal enum class LoadType { + + /** + * The dependency should be instantiated as soon as the module is created. + */ + EAGER, + + /** + * The dependency should be instantiated at the point where it is required. + */ + LAZY +} + +/** + * Creates a new dependency that is a singleton, meaning only one object will ever be created in + * this module. By default this dependency will be created lazily. + * + * Lazy dependencies are NOT thread safe. It is assumed that dependencies will always be + * initialized on the same thread. + */ +internal inline fun singleton( + loadType: LoadType = LoadType.LAZY, + noinline provider: () -> T +): ReadOnlyProperty = SingletonDelegate(loadType, provider) + +/** + * Creates a new dependency from a factory, meaning every time this property is called a + * new object will be created. + */ +internal inline fun factory( + noinline provider: () -> T +): ReadOnlyProperty = FactoryDelegate(provider) + +internal class FactoryDelegate(private inline val provider: () -> T) : ReadOnlyProperty { + + override fun getValue(thisRef: Any?, property: KProperty<*>): T = provider() +} + +internal class SingletonDelegate( + loadType: LoadType, + provider: () -> T +) : ReadOnlyProperty { + + // optimization: use atomic checks rather than synchronized in lazy. + // Taking out a lock is overkill for the vast majority of objects on the graph + private val value: T by lazy(LazyThreadSafetyMode.PUBLICATION, provider) + + init { + if (loadType == LoadType.EAGER) { + value + } + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T = value +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/EssentialServiceModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/EssentialServiceModule.kt new file mode 100644 index 0000000000..07b8ad88df --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/EssentialServiceModule.kt @@ -0,0 +1,167 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.capture.cpu.CpuInfoDelegate +import io.embrace.android.embracesdk.capture.cpu.EmbraceCpuInfoDelegate +import io.embrace.android.embracesdk.capture.metadata.EmbraceMetadataService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.orientation.NoOpOrientationService +import io.embrace.android.embracesdk.capture.orientation.OrientationService +import io.embrace.android.embracesdk.capture.user.EmbraceUserService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.comms.api.ApiRequest +import io.embrace.android.embracesdk.comms.api.ApiResponseCache +import io.embrace.android.embracesdk.comms.api.ApiService +import io.embrace.android.embracesdk.comms.api.ApiUrlBuilder +import io.embrace.android.embracesdk.comms.api.EmbraceApiService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.EmbraceConfigService +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.gating.EmbraceGatingService +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.internal.BuildInfo +import io.embrace.android.embracesdk.internal.DeviceArchitecture +import io.embrace.android.embracesdk.internal.DeviceArchitectureImpl +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.session.EmbraceActivityService +import io.embrace.android.embracesdk.session.EmbraceMemoryCleanerService +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule +import java.io.File + +/** + * This module contains services that are essential for bootstrapping other functionality in + * the SDK during initialization. + */ +internal interface EssentialServiceModule { + val memoryCleanerService: MemoryCleanerService + val orientationService: OrientationService + val activityService: ActivityService + val metadataService: MetadataService + val configService: ConfigService + val gatingService: GatingService + val userService: UserService + val urlBuilder: ApiUrlBuilder + val cache: ApiResponseCache + val apiClient: ApiClient + val apiService: ApiService + val sharedObjectLoader: SharedObjectLoader + val cpuInfoDelegate: CpuInfoDelegate + val deviceArchitecture: DeviceArchitecture +} + +internal class EssentialServiceModuleImpl( + initModule: InitModule, + coreModule: CoreModule, + workerThreadModule: WorkerThreadModule, + systemServiceModule: SystemServiceModule, + androidServicesModule: AndroidServicesModule, + buildInfo: BuildInfo, + customAppId: String?, + enableIntegrationTesting: Boolean, + private val configStopAction: () -> Unit, + private val configServiceProvider: () -> ConfigService? = { null }, + override val deviceArchitecture: DeviceArchitecture = DeviceArchitectureImpl() +) : EssentialServiceModule { + + private val backgroundExecutorService = + workerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION) + + override val memoryCleanerService: MemoryCleanerService by singleton { + EmbraceMemoryCleanerService() + } + + override val orientationService: OrientationService by singleton { + // Embrace is not processing orientation changes on this moment, so return no-op service. + NoOpOrientationService() + } + + override val activityService: ActivityService by singleton { + EmbraceActivityService(coreModule.application, orientationService, initModule.clock) + } + + override val configService: ConfigService by singleton { + configServiceProvider.invoke() + ?: EmbraceConfigService( + LocalConfig.fromResources(coreModule.resources, coreModule.context.packageName, customAppId, coreModule.jsonSerializer), + { apiService }, + androidServicesModule.preferencesService, + initModule.clock, + coreModule.logger, + backgroundExecutorService, + coreModule.isDebug, + configStopAction + ) + } + + override val sharedObjectLoader: SharedObjectLoader by singleton { + SharedObjectLoader() + } + + override val cpuInfoDelegate: CpuInfoDelegate by singleton { + EmbraceCpuInfoDelegate(sharedObjectLoader, coreModule.logger) + } + + override val metadataService: MetadataService by singleton { + EmbraceMetadataService.ofContext( + coreModule.context, + buildInfo, + configService, + coreModule.appFramework, + androidServicesModule.preferencesService, + activityService, + backgroundExecutorService, + systemServiceModule.storageManager, + systemServiceModule.windowManager, + systemServiceModule.activityManager, + initModule.clock, + cpuInfoDelegate, + deviceArchitecture + ) + } + + override val urlBuilder by singleton { + ApiUrlBuilder( + configService, + metadataService, + enableIntegrationTesting, + coreModule.isDebug + ) + } + + override val cache by singleton { + ApiResponseCache( + coreModule.jsonSerializer, + { File(coreModule.context.cacheDir, "emb_config_cache") } + ) + } + + override val gatingService: GatingService by singleton { + EmbraceGatingService(configService) + } + + override val userService: UserService by singleton { + EmbraceUserService( + androidServicesModule.preferencesService, + coreModule.logger + ) + } + + override val apiService: ApiService by singleton { + EmbraceApiService( + { apiClient }, + urlBuilder, + coreModule.jsonSerializer, + { url: String, request: ApiRequest -> cache.retrieveCachedConfig(url, request) }, + coreModule.logger + ) + } + + override val apiClient by singleton { + ApiClient( + coreModule.logger + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/InitModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/InitModule.kt new file mode 100644 index 0000000000..78f0e05e66 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/InitModule.kt @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.clock.NormalizedIntervalClock +import io.embrace.android.embracesdk.clock.SystemClock +import io.embrace.android.embracesdk.internal.OpenTelemetryClock +import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService +import io.embrace.android.embracesdk.internal.spans.SpansService + +/** + * A module of components and services required at [EmbraceImpl] instantiation time, i.e. before the SDK evens starts + */ +internal interface InitModule { + /** + * Clock instance locked to the time of creation used by the SDK throughout its lifetime + */ + val clock: Clock + + /** + * Service to log traces + */ + val spansService: SpansService +} + +internal class InitModuleImpl( + override val clock: Clock = NormalizedIntervalClock(systemClock = SystemClock()), + override val spansService: SpansService = EmbraceSpansService(clock = OpenTelemetryClock(embraceClock = clock)) +) : InitModule diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SdkObservabilityModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SdkObservabilityModule.kt new file mode 100644 index 0000000000..0e8de3a95e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SdkObservabilityModule.kt @@ -0,0 +1,34 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.logging.AndroidLogger +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.logging.InternalErrorLogger + +/** + * Contains dependencies that are used to gain internal observability into how the SDK + * is performing. + */ +internal interface SdkObservabilityModule { + val exceptionService: EmbraceInternalErrorService + val internalErrorLogger: InternalErrorLogger +} + +internal class SdkObservabilityModuleImpl( + initModule: InitModule, + essentialServiceModule: EssentialServiceModule +) : SdkObservabilityModule { + + private val logStrictMode by lazy { + val configService = essentialServiceModule.configService + configService.sessionBehavior.isSessionErrorLogStrictModeEnabled() || + configService.sdkModeBehavior.isIntegrationModeEnabled() + } + + override val exceptionService: EmbraceInternalErrorService by singleton { + EmbraceInternalErrorService(essentialServiceModule.activityService, initModule.clock, logStrictMode) + } + + override val internalErrorLogger: InternalErrorLogger by singleton { + InternalErrorLogger(exceptionService, AndroidLogger(), logStrictMode) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SessionModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SessionModule.kt new file mode 100644 index 0000000000..0e4eb77852 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SessionModule.kt @@ -0,0 +1,99 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.ndk.NativeModule +import io.embrace.android.embracesdk.session.BackgroundActivityService +import io.embrace.android.embracesdk.session.EmbraceBackgroundActivityService +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.session.EmbraceSessionService +import io.embrace.android.embracesdk.session.SessionHandler +import io.embrace.android.embracesdk.session.SessionService +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +internal interface SessionModule { + val sessionHandler: SessionHandler + val sessionService: SessionService + val backgroundActivityService: BackgroundActivityService? +} + +internal class SessionModuleImpl( + initModule: InitModule, + coreModule: CoreModule, + androidServicesModule: AndroidServicesModule, + essentialServiceModule: EssentialServiceModule, + nativeModule: NativeModule, + dataContainerModule: DataContainerModule, + deliveryModule: DeliveryModule, + sessionProperties: EmbraceSessionProperties, + dataCaptureServiceModule: DataCaptureServiceModule, + customerLogModule: CustomerLogModule, + sdkObservabilityModule: SdkObservabilityModule, + workerThreadModule: WorkerThreadModule +) : SessionModule { + + override val sessionHandler: SessionHandler by singleton { + SessionHandler( + coreModule.logger, + essentialServiceModule.configService, + androidServicesModule.preferencesService, + essentialServiceModule.userService, + dataCaptureServiceModule.networkConnectivityService, + essentialServiceModule.metadataService, + essentialServiceModule.gatingService, + dataCaptureServiceModule.breadcrumbService, + essentialServiceModule.activityService, + nativeModule.ndkService, + dataContainerModule.eventService, + customerLogModule.remoteLogger, + sdkObservabilityModule.exceptionService, + dataContainerModule.performanceInfoService, + essentialServiceModule.memoryCleanerService, + deliveryModule.deliveryService, + dataCaptureServiceModule.webviewService, + dataCaptureServiceModule.activityLifecycleBreadcrumbService, + dataCaptureServiceModule.thermalStatusService, + nativeModule.nativeThreadSamplerService, + initModule.clock, + workerThreadModule.scheduledExecutor(ExecutorName.SESSION_CLOSER), + workerThreadModule.scheduledExecutor(ExecutorName.SESSION_CACHING), + workerThreadModule.backgroundExecutor(ExecutorName.SESSION) + ) + } + + override val sessionService: SessionService by singleton { + EmbraceSessionService( + essentialServiceModule.activityService, + nativeModule.ndkService, + sessionProperties, + coreModule.logger, + sessionHandler, + deliveryModule.deliveryService, + essentialServiceModule.configService.autoDataCaptureBehavior.isNdkEnabled(), + initModule.clock, + initModule.spansService + ) + } + + override val backgroundActivityService: BackgroundActivityService? by singleton { + if (essentialServiceModule.configService.isBackgroundActivityCaptureEnabled()) { + EmbraceBackgroundActivityService( + dataContainerModule.performanceInfoService, + essentialServiceModule.metadataService, + dataCaptureServiceModule.breadcrumbService, + essentialServiceModule.activityService, + dataContainerModule.eventService, + customerLogModule.remoteLogger, + essentialServiceModule.userService, + sdkObservabilityModule.exceptionService, + deliveryModule.deliveryService, + essentialServiceModule.configService, + nativeModule.ndkService, + initModule.clock, + initModule.spansService, + lazy { workerThreadModule.backgroundExecutor(ExecutorName.SESSION_CACHE_EXECUTOR) } + ) + } else { + null + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SystemServiceModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SystemServiceModule.kt new file mode 100644 index 0000000000..06a5e81f28 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/injection/SystemServiceModule.kt @@ -0,0 +1,52 @@ +package io.embrace.android.embracesdk.injection + +import android.app.ActivityManager +import android.app.usage.StorageStatsManager +import android.content.Context +import android.net.ConnectivityManager +import android.os.Build +import android.os.PowerManager +import android.view.WindowManager +import io.embrace.android.embracesdk.utils.BuildVersionChecker +import io.embrace.android.embracesdk.utils.VersionChecker + +internal interface SystemServiceModule { + val activityManager: ActivityManager? + val powerManager: PowerManager? + val connectivityManager: ConnectivityManager? + val storageManager: StorageStatsManager? + val windowManager: WindowManager? +} + +internal class SystemServiceModuleImpl @JvmOverloads constructor( + coreModule: CoreModule, + versionChecker: VersionChecker = BuildVersionChecker +) : SystemServiceModule { + + private val ctx = coreModule.context + + override val activityManager: ActivityManager? by singleton { + getSystemServiceSafe(Context.ACTIVITY_SERVICE) + } + + override val powerManager: PowerManager? by singleton { + getSystemServiceSafe(Context.POWER_SERVICE) + } + + override val connectivityManager: ConnectivityManager? by singleton { + getSystemServiceSafe(Context.CONNECTIVITY_SERVICE) + } + + override val storageManager: StorageStatsManager? = + if (versionChecker.isAtLeast(Build.VERSION_CODES.O)) { + getSystemServiceSafe(Context.STORAGE_STATS_SERVICE) + } else null + + override val windowManager: WindowManager? by singleton { + getSystemServiceSafe(Context.WINDOW_SERVICE) + } + + @Suppress("UNCHECKED_CAST") + private fun getSystemServiceSafe(name: String): T? = + runCatching { ctx.getSystemService(name) }.getOrNull() as T? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/AndroidResourcesService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/AndroidResourcesService.kt new file mode 100644 index 0000000000..dd61ea726f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/AndroidResourcesService.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.internal + +import android.content.Context +import android.content.res.Resources + +/** + * Interface for retrieving identifiers and strings from the app's [android.content.res.Resources] object. This can be used + * instead of directly accessing resources through the [Context] so we can more easily fake things during tests. + */ +internal interface AndroidResourcesService { + fun getIdentifier(name: String?, defType: String?, defPackage: String?): Int + + @Throws(Resources.NotFoundException::class) + fun getString(id: Int): String +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ApkToolsConfig.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ApkToolsConfig.kt new file mode 100644 index 0000000000..a61f909fc1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ApkToolsConfig.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.internal + +import io.embrace.android.embracesdk.BuildConfig + +/** + * The purpose of this class is allowing us to change its flags with APKTools, directly from the bytecode. + * This shouldn't have any uses in the code, besides unit tests. + */ +internal object ApkToolsConfig { + + @JvmField + var IS_DEVELOPER_LOGGING_ENABLED: Boolean = BuildConfig.DEBUG + + @JvmField + var IS_SDK_DISABLED: Boolean = false + + @JvmField + var IS_EXCEPTION_CAPTURE_DISABLED: Boolean = false + + @JvmField + var IS_NDK_DISABLED: Boolean = false + + @JvmField + var IS_ANR_MONITORING_DISABLED: Boolean = false + + @JvmField + var IS_BREADCRUMB_TRACKING_DISABLED: Boolean = false + + @JvmField + var IS_NETWORK_CAPTURE_DISABLED: Boolean = false +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/BuildInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/BuildInfo.kt new file mode 100644 index 0000000000..373bea042d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/BuildInfo.kt @@ -0,0 +1,74 @@ +package io.embrace.android.embracesdk.internal + +import android.content.res.Resources + +/** + * Specifies the application ID and build ID. + */ +internal class BuildInfo internal constructor( + /** + * The ID of the particular build, generated at compile-time. + */ + val buildId: String?, + + /** + * The BuildType name of the particular build, extracted at compile-time. + */ + val buildType: String?, + + /** + * The Flavor name of the particular build, extracted at compile-time. + */ + val buildFlavor: String? +) { + + internal companion object { + const val BUILD_INFO_BUILD_ID: String = "emb_build_id" + const val BUILD_INFO_BUILD_TYPE: String = "emb_build_type" + const val BUILD_INFO_BUILD_FLAVOR: String = "emb_build_flavor" + private const val RES_TYPE_STRING = "string" + + /** + * Loads the build information from resources provided by the config file packaged within the application by Gradle at + * build-time. + * + * @return the build information + */ + @JvmStatic + fun fromResources(resources: AndroidResourcesService, packageName: String): BuildInfo { + return BuildInfo( + getBuildResource(resources, packageName, BUILD_INFO_BUILD_ID), + getBuildResource(resources, packageName, BUILD_INFO_BUILD_TYPE), + getBuildResource(resources, packageName, BUILD_INFO_BUILD_FLAVOR) + ) + } + + /** + * Given a build property name and a build property type, retrieves the embrace build resource value. + */ + fun getBuildResource( + resources: AndroidResourcesService, + packageName: String, + buildProperty: String + ): String? { + return try { + val resourceId = resources.getIdentifier(buildProperty, RES_TYPE_STRING, packageName) + + // Flavor value is optional, so we should not hard fail if doesn't exists. + if (buildProperty == BUILD_INFO_BUILD_FLAVOR && resourceId == 0) { + null + } else resources.getString(resourceId) + } catch (ex: NullPointerException) { + throw IllegalArgumentException( + "No resource found for $buildProperty property. Failed to create build info.", + ex + ) + } catch (ex: Resources.NotFoundException) { + throw IllegalArgumentException( + "No resource found for $buildProperty property. Failed to create build info.", + ex + ) + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/CacheableValue.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/CacheableValue.kt new file mode 100644 index 0000000000..722cee5e0e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/CacheableValue.kt @@ -0,0 +1,44 @@ +package io.embrace.android.embracesdk.internal + +/** + * Holds a property whose value can be cached (if its inputs do not change). + */ +internal class CacheableValue( + + /** + * Used to generate a hashcode from all the inputs that might affect a value. If the hashcode + * is the same for the inputs, then it is assumed a cached value can be returned. If the + * hashcode is not the same, then the value is calculated from scratch. + * + * Be careful about defining inputs. For example, the hashcode for a collection generally + * won't change if new objects are added, so you need to be wary of accidentally + * returning stale values. + */ + private val input: () -> Any +) { + + private var initialized = false + private var prevHashCode = -1 + private var value: T? = null + + // input: used to determine whether inputs have changed since last call + // action: generates a value if inputs are changed (or haven't been initialized) + + /** + * Resolves and returns a value. If inputs are unchanged then a cached value will be returned. + * If inputs are changed or no cached value is present, then [action] will be invoked + * to calculate a value that is placed in the cache. + */ + fun value(action: () -> T): T { + val hashCode = input().hashCode() + + if (prevHashCode != hashCode || !initialized) { + initialized = true + value = action() + } + prevHashCode = hashCode + return checkNotNull(value) { + "Value to be cached is null" + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactory.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactory.kt new file mode 100644 index 0000000000..a281132fca --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactory.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.internal + +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory + +/** + * [ThreadFactory] that creates thread with a constant name. Useful if you want to ensure the same executor produces threads with the + * same name. Use the uniquePerInstance parameter to use the instance's hashcode to provide relative uniqueness across instances + * of this thread factory. + */ +internal class ConstantNameThreadFactory( + namePrefix: String = "thread", + uniquePerInstance: Boolean = false +) : ThreadFactory { + private val defaultFactory: ThreadFactory = Executors.defaultThreadFactory() + private val threadName = "emb-$namePrefix${if (uniquePerInstance) {"-${hashCode()}"} else ""}" + + override fun newThread(r: Runnable?): Thread = defaultFactory.newThread(r).apply { name = threadName } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitecture.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitecture.kt new file mode 100644 index 0000000000..c6bdc1a3bf --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitecture.kt @@ -0,0 +1,6 @@ +package io.embrace.android.embracesdk.internal + +internal interface DeviceArchitecture { + val architecture: String + val is32BitDevice: Boolean +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitectureImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitectureImpl.kt new file mode 100644 index 0000000000..1e2400da87 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/DeviceArchitectureImpl.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.internal + +import android.os.Build +import android.text.TextUtils + +internal open class DeviceArchitectureImpl : DeviceArchitecture { + override val architecture: String + get() = Build.SUPPORTED_ABIS[0] + + override val is32BitDevice: Boolean + get() = !TextUtils.join( + ", ", + Build.SUPPORTED_ABIS + ).contains("64") +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService.kt new file mode 100644 index 0000000000..57d8d62593 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceAndroidResourcesService.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.internal + +import android.annotation.SuppressLint +import android.content.Context + +/** + * Implementation used in production that just defers to the given [Context] + */ +internal class EmbraceAndroidResourcesService(private val context: Context) : AndroidResourcesService { + @SuppressLint("DiscouragedApi") + override fun getIdentifier(name: String?, defType: String?, defPackage: String?): Int = + context.resources.getIdentifier(name, defType, defPackage) + + override fun getString(id: Int): String = context.resources.getString(id) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceSerializer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceSerializer.kt new file mode 100644 index 0000000000..a87b1a51a5 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceSerializer.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk.internal + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonIOException +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import io.embrace.android.embracesdk.comms.api.EmbraceUrl +import io.embrace.android.embracesdk.comms.api.EmbraceUrlAdapter +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.io.BufferedWriter +import java.lang.reflect.Type +import java.nio.charset.Charset + +/** + * A wrapper around Gson to allow for thread-safe serialization. + */ +internal class EmbraceSerializer { + + private val gson: ThreadLocal = object : ThreadLocal() { + override fun initialValue(): Gson { + return GsonBuilder() + .registerTypeAdapter(EmbraceUrl::class.java, EmbraceUrlAdapter()) + .create() + } + } + + fun toJson(src: T): String { + return gson.get()?.toJson(src) ?: throw JsonIOException("Failed converting object to JSON.") + } + + fun toJson(src: T, type: Type): String { + return gson.get()?.toJson(src, type) + ?: throw JsonIOException("Failed converting object to JSON.") + } + + fun fromJson(json: String, type: Type): T? { + return gson.get()?.fromJson(json, type) + } + + fun fromJson(json: String, clz: Class): T? { + return gson.get()?.fromJson(json, clz) + } + + fun writeToFile(any: T, clazz: Class, bw: BufferedWriter): Boolean { + return try { + gson.get()?.toJson(any, clazz, JsonWriter(bw)) + true + } catch (e: Exception) { + InternalStaticEmbraceLogger.logDebug("cannot write to bufferedWriter", e) + false + } + } + + fun loadObject(jsonReader: JsonReader, clazz: Class): T? { + return gson.get()?.fromJson(jsonReader, clazz) + } + + fun bytesFromPayload(payload: T, clazz: Class): ByteArray? { + val json: String? = gson.get()?.toJson(payload, clazz.genericSuperclass) + return json?.toByteArray(Charset.forName("UTF-8")) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EventDescription.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EventDescription.kt new file mode 100644 index 0000000000..905c0a5e74 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EventDescription.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.internal + +import io.embrace.android.embracesdk.payload.Event +import java.util.concurrent.Future + +internal data class EventDescription( + val lateTimer: Future<*>, + val event: Event +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/MessageType.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/MessageType.kt new file mode 100644 index 0000000000..6860523309 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/MessageType.kt @@ -0,0 +1,5 @@ +package io.embrace.android.embracesdk.internal + +internal enum class MessageType { + EVENT, LOG, SESSION, USER +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/OpenTelemetryClock.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/OpenTelemetryClock.kt new file mode 100644 index 0000000000..824515674a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/OpenTelemetryClock.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.internal + +import android.os.SystemClock +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.utils.BuildVersionChecker +import java.util.concurrent.TimeUnit + +/** + * A clock that is compatible with the OpenTelemetry SDK that defers to the internal clock used by Embrace. This allows the times recorded + * internally by the OpenTelemetry SDK to use the same clock instance as the Embrace SDK, which is anchored to the start time of the app + * so as to not be affected by any client-side clock changes or drifts. + * + * The one caveat about this implementation is that the precision for obtaining the current time only goes to the millisecond, which is + * considered enough for client side operation timings at this time. + */ +internal class OpenTelemetryClock( + private val embraceClock: Clock +) : io.opentelemetry.sdk.common.Clock { + + override fun now(): Long = TimeUnit.MILLISECONDS.toNanos(embraceClock.now()) + + override fun nanoTime(): Long { + return if (BuildVersionChecker.isAtLeast(17)) { + SystemClock.elapsedRealtimeNanos() + } else { + TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime()) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/PatternCache.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/PatternCache.kt new file mode 100644 index 0000000000..413e2b718c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/PatternCache.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.internal + +import java.util.regex.Pattern + +internal class PatternCache { + + private val memo: MutableMap, Collection> = HashMap() + + fun doesStringMatchesPatternInSet(string: String, patternSet: Set): Boolean { + val patterns = memo.getOrPut(patternSet) { + patternSet.map(Pattern::compile) + } + return patterns.any { it.matcher(string).matches() } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/SharedObjectLoader.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/SharedObjectLoader.kt new file mode 100644 index 0000000000..aae7a49f87 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/SharedObjectLoader.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk.internal + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger + +/** + * Loads shared object files. + */ +internal class SharedObjectLoader { + + fun loadEmbraceNative() = try { + System.loadLibrary("embrace-native") + true + } catch (exc: UnsatisfiedLinkError) { + InternalStaticEmbraceLogger.logError("Failed to load SO file embrace-native", exc) + false + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/StartupEventInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/StartupEventInfo.kt new file mode 100644 index 0000000000..c328a8d800 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/StartupEventInfo.kt @@ -0,0 +1,6 @@ +package io.embrace.android.embracesdk.internal + +internal data class StartupEventInfo( + val duration: Long? = null, + val threshold: Long? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/Systrace.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/Systrace.kt new file mode 100644 index 0000000000..deb5f7d481 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/Systrace.kt @@ -0,0 +1,48 @@ +package io.embrace.android.embracesdk.internal + +import android.os.Trace +import io.embrace.android.embracesdk.InternalApi + +/** + * Shim to add custom events to system traces if running in the applicable API versions. Basic alternative to using androidx.tracing. + */ +@InternalApi +internal class Systrace private constructor() { + companion object { + + /** + * Start a trace section. The name of the section will be [sectionName] prefixed by "emb-" + */ + fun start(sectionName: String) { + Trace.beginSection("emb-$sectionName") + } + + /** + * Close the last trace section that was started + */ + fun end() { + Trace.endSection() + } + + /** + * Create a trace section around the lambda passed in and return the result. + * The name of the section will be [sectionName] prefixed by "emb-" + * + * Note: rethrowing the same [Throwable] that was caught is appropriate here because use of this should not change the code path. + */ + @Suppress("RethrowCaughtException") + inline fun trace(sectionName: String, code: () -> T): T { + val returnValue: T + try { + start(sectionName) + returnValue = code() + } catch (t: Throwable) { + throw t + } finally { + end() + } + + return returnValue + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheck.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheck.kt new file mode 100644 index 0000000000..9a26274677 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheck.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.internal + +import io.embrace.android.embracesdk.BuildConfig +import java.util.concurrent.atomic.AtomicReference + +/** + * Asserts that a function is called on a thread with a specific name + */ +internal fun enforceThread(expectedThreadReference: AtomicReference) { + if (BuildConfig.DEBUG) { + val expectedThread = expectedThreadReference.get() + val currentThread = Thread.currentThread() + if (expectedThread.name != currentThread.name) { + throw WrongThreadException( + "Called on wrong thread. Expected ${expectedThread.name}, got ${currentThread.name}" + ) + } + } +} + +internal class WrongThreadException(message: String) : IllegalStateException(message) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/TraceparentGenerator.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/TraceparentGenerator.kt new file mode 100644 index 0000000000..d473d51d01 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/TraceparentGenerator.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk.internal + +import io.opentelemetry.api.trace.SpanId +import io.opentelemetry.api.trace.TraceId +import kotlin.random.Random + +internal class TraceparentGenerator( + private val random: Random = Random.Default +) { + /** + * Generate a valid W3C-compliant traceparent. See the format here: https://www.w3.org/TR/trace-context/#traceparent-header-field-values + * + * Note: because Embrace may be recording a span on our side for the given traceparent, we have set the "sampled" flag to indicate that. + */ + fun generate(): String = + "00-" + TraceId.fromLongs(validRandomLong(), validRandomLong()) + "-" + SpanId.fromLong(validRandomLong()) + "-01" + + private fun validRandomLong(): Long { + var value: Long + do { + value = random.nextLong() + } while (value == 0L) + return value + } + + companion object { + private val INSTANCE = TraceparentGenerator() + + @JvmStatic + fun generateW3CTraceparent() = INSTANCE.generate() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarker.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarker.kt new file mode 100644 index 0000000000..3b6f52e4bd --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarker.kt @@ -0,0 +1,103 @@ +package io.embrace.android.embracesdk.internal.crash + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.io.File +import java.lang.Exception + +/** + * CrashFileMarker uses a file to indicate that a crash has occurred. This file is accessed in the + * next launch of the app to determine if a crash occurred in the previous launch. + */ +internal class CrashFileMarker(private val markerFile: Lazy) { + + private val lock = Any() + + /** + * Creates a file in the cache directory to indicate that a crash has occurred. + * If the file could not be created, it will try again. + */ + fun mark() { + synchronized(lock) { + val markerFileCreated = createMarkerFile() + if (!markerFileCreated) { + createMarkerFile() + } + } + } + + /** + * Deletes the file in the cache directory that indicates that a crash has occurred. + * If the file could not be deleted, it will try again. + */ + fun removeMark() { + synchronized(lock) { + if (markerFile.value.exists()) { + val markerFileDeleted = deleteMarkerFile() + if (!markerFileDeleted) { + deleteMarkerFile() + } + } + } + } + + /** + * Returns true if the crash marker file in the cache directory exists. + * If the file could not be accessed, it will try again. + */ + fun isMarked(): Boolean { + synchronized(lock) { + return markerFileExists() ?: markerFileExists() ?: false + } + } + + /** + * Returns true if the crash marker file in the cache directory exists and deletes it. + */ + fun getAndCleanMarker(): Boolean { + synchronized(lock) { + val isMarked = isMarked() + removeMark() + return isMarked + } + } + + private fun createMarkerFile(): Boolean { + return try { + markerFile.value.writeText(CRASH_MARKER_SOURCE_JVM) + true + } catch (e: Exception) { + InternalStaticEmbraceLogger.logError("Error creating the marker file: ${markerFile.value.path}", e) + false + } + } + + private fun deleteMarkerFile(): Boolean { + return try { + val deleted = markerFile.value.delete() + if (!deleted) { + InternalStaticEmbraceLogger.logError( + "Error deleting the marker file: ${markerFile.value.path}.", + Throwable("File not deleted") + ) + } + deleted + } catch (e: SecurityException) { + InternalStaticEmbraceLogger.logError("Error deleting the marker file: ${markerFile.value.path}.", e) + false + } + } + + private fun markerFileExists(): Boolean? { + return try { + return markerFile.value.exists() + } catch (e: SecurityException) { + InternalStaticEmbraceLogger.logError("Error checking the marker file: ${markerFile.value.path}", e) + null + } + } + + companion object { + const val CRASH_MARKER_FILE_NAME: String = "embrace_crash_marker" + private const val CRASH_MARKER_SOURCE_JVM: String = "1" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier.kt new file mode 100644 index 0000000000..d590a81cc3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifier.kt @@ -0,0 +1,51 @@ +package io.embrace.android.embracesdk.internal.crash + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future + +/** + * Verifies if the last run crashed. + * This is done by checking if the crash marker file exists. + */ +internal class LastRunCrashVerifier(private val crashFileMarker: CrashFileMarker) { + + private var didLastRunCrashFuture: Future? = null + private var didLastRunCrash: Boolean? = null + + /** + * Returns true if the app crashed in the last run, false otherwise. + */ + fun didLastRunCrash(): Boolean { + return didLastRunCrash ?: didLastRunCrashFuture?.let { future -> + try { + future.get() + } catch (e: Throwable) { + InternalStaticEmbraceLogger.logError("[Embrace] didLastRunCrash: error while getting the result", e) + null + } + } ?: readAndCleanMarker() + } + + /** + * Reads and clean the last run crash marker in a background thread. + * This method is called when the SDK is started. + */ + fun readAndCleanMarkerAsync(executorService: ExecutorService) { + if (didLastRunCrash == null) { + this.didLastRunCrashFuture = executorService.submit { + readAndCleanMarker() + } + } + } + + /** + * Reads and clean the last run crash marker. + * @return true if the app crashed in the last run, false otherwise + */ + private fun readAndCleanMarker(): Boolean { + return crashFileMarker.getAndCleanMarker().also { + this.didLastRunCrash = it + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceExtensions.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceExtensions.kt new file mode 100644 index 0000000000..8f9ce6f5a2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceExtensions.kt @@ -0,0 +1,220 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.internal.spans.EmbraceAttributes.Attribute +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.api.common.AttributesBuilder +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanBuilder +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.context.Context +import java.util.concurrent.TimeUnit + +/** + * Extension functions and constants to augment the core OpenTelemetry SDK and provide Embrace-specific customizations + * + * Note: there's no explicit tests for these extensions as their functionality will be validated as part of other tests. + */ + +/** + * Prefix added to [Span] names for all Spans recorded internally by the SDK + */ +private const val EMBRACE_SPAN_NAME_PREFIX = "emb-" + +/** + * Prefix added to all [Span] attribute keys for all attributes added by the SDK + */ +private const val EMBRACE_ATTRIBUTE_NAME_PREFIX = "emb." + +/** + * Attribute name for the monotonically increasing sequence ID given to completed [Span] that expected to sent to the server + */ +private const val SEQUENCE_ID_ATTRIBUTE_NAME = EMBRACE_ATTRIBUTE_NAME_PREFIX + "sequence_id" + +/** + * Denotes an important span to be listed in the Spans listing page in the UI. Currently defined as any spans that are the root of a trace + */ +private const val KEY_SPAN_ATTRIBUTE_NAME = EMBRACE_ATTRIBUTE_NAME_PREFIX + "key" + +/** + * Denotes a private span logged by Embrace for diagnostic purposes and should not be displayed to customers in the dashboard, but + * should be shown to Embrace employees. + */ +private const val PRIVATE_SPAN_ATTRIBUTE_NAME = EMBRACE_ATTRIBUTE_NAME_PREFIX + "private" + +/** + * Creates a new [SpanBuilder] with the correctly prefixed name, to be used for recording Spans in the SDK internally + */ +internal fun Tracer.embraceSpanBuilder(name: String, internal: Boolean): SpanBuilder { + return if (internal) { + spanBuilder(EMBRACE_SPAN_NAME_PREFIX + name).makePrivate() + } else { + spanBuilder(name) + } +} + +/** + * Sets and returns the [EmbraceAttributes.Type] attribute for the given [SpanBuilder] + */ +internal fun SpanBuilder.setType(value: EmbraceAttributes.Type): SpanBuilder { + setAttribute(value.keyName(), value.toString()) + return this +} + +/** + * Mark the span generated by this builder as an important one, to be listed in the Spans listing page in the UI. It is currently used + * for any spans that are the root of a trace. + */ +internal fun SpanBuilder.makeKey(): SpanBuilder { + setAttribute(KEY_SPAN_ATTRIBUTE_NAME, true) + return this +} + +/** + * Mark the span generated by this builder as a private span logged by Embrace for diagnostic purposes and that should not be displayed + * to customers in the dashboard, but should be shown to Embrace employees. + */ +internal fun SpanBuilder.makePrivate(): SpanBuilder { + setAttribute(PRIVATE_SPAN_ATTRIBUTE_NAME, true) + return this +} + +/** + * Extract the parent span from an [EmbraceSpan] and set it as the parent. It's a no-op if the parent has not been started yet. + */ +internal fun SpanBuilder.updateParent(parent: EmbraceSpan?): SpanBuilder { + if (parent == null) { + makeKey() + } else if (parent is EmbraceSpanImpl) { + parent.wrappedSpan()?.let { + setParent(Context.current().with(it)) + } + } + return this +} + +/** + * Allow a [SpanBuilder] to take in a lambda around which a span will be created for its execution + */ +internal fun SpanBuilder.record(code: () -> T): T { + val returnValue: T + var span: Span? = null + + try { + span = startSpan() + returnValue = code() + span.endSpan() + } catch (t: Throwable) { + span?.endSpan(ErrorCode.FAILURE) + throw t + } + + return returnValue +} + +/** + * Monotonically increasing ID given to completed [Span] that expected to sent to the server. Can be used to track data loss on the server. + */ +internal fun Span.setSequenceId(id: Long): Span { + setAttribute(SEQUENCE_ID_ATTRIBUTE_NAME, id) + return this +} + +/** + * Ends the given [Span], and setting the correct properties per the optional [ErrorCode] passed in. If [errorCode] + * is not specified, it means the [Span] completed successfully, and no [ErrorCode] will be set. + */ +internal fun Span.endSpan(errorCode: ErrorCode? = null, endTimeNanos: Long? = null): Span { + if (errorCode == null) { + setStatus(StatusCode.OK) + } else { + setStatus(StatusCode.ERROR) + setAttribute(errorCode.keyName(), errorCode.toString()) + } + + if (endTimeNanos != null) { + end(endTimeNanos, TimeUnit.NANOSECONDS) + } else { + end() + } + + return this +} + +/** + * Populate an [AttributesBuilder] with String key-value pairs from a [Map] + */ +internal fun AttributesBuilder.fromMap(attributes: Map): AttributesBuilder { + attributes.filter { EmbraceSpanImpl.attributeValid(it.key, it.value) }.forEach { + put(it.key, it.value) + } + return this +} + +/** + * Returns true if the Span is a Key Span + */ +internal fun EmbraceSpanData.isKey(): Boolean = attributes[KEY_SPAN_ATTRIBUTE_NAME] == true.toString() + +/** + * Returns true if the Span is private + */ +internal fun EmbraceSpanData.isPrivate(): Boolean = attributes[PRIVATE_SPAN_ATTRIBUTE_NAME] == true.toString() + +/** + * Return the appropriate internal Embrace Span name given the current value + */ +internal fun String.toEmbraceSpanName(): String = EMBRACE_SPAN_NAME_PREFIX + this + +/** + * Contains the set of attributes (i.e. implementers of the [Attribute] interface) set on a [Span] by the SDK that has special meaning + * in the Embrace world. Each enum defines the attribute name used in the [Span] and specifies the set of valid values it can be set to. + */ +internal object EmbraceAttributes { + + /** + * Attribute to categorize a [Span] and give it a distinct semantic meaning. Spans of each [Type] may be treated differently by the + * backend and can be expected to contain a set of attributes to further flesh out the given semantic meanings. + */ + internal enum class Type : Attribute { + /** + * Spans that model an Embrace session or background activity. + */ + SESSION, + + /** + * A [Span] created by an SDK user to measure the performance of an operation + */ + PERFORMANCE; + + override val canonicalName = "type" + } + + /** + * The reason for the termination of a process span + */ + internal enum class AppTerminationCause : Attribute { + CRASH, + USER_TERMINATION, + UNKNOWN; + + override val canonicalName: String = "termination_cause" + } + + /** + * Denotes an attribute added by the SDK with a restricted set of valid values + */ + internal interface Attribute { + + /** + * The name used to identify this [Attribute] + */ + val canonicalName: String + + /** + * The name used as the key for the [Attribute] in the attributes map + */ + fun keyName(): String = EMBRACE_ATTRIBUTE_NAME_PREFIX + canonicalName + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanData.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanData.kt new file mode 100644 index 0000000000..ac1e105c82 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanData.kt @@ -0,0 +1,68 @@ +package io.embrace.android.embracesdk.internal.spans + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.sdk.trace.data.EventData +import io.opentelemetry.sdk.trace.data.SpanData + +/** + * Serializable representation of [EmbraceSpanData] + */ +internal data class EmbraceSpanData( + @SerializedName("trace_id") + val traceId: String, + + @SerializedName("span_id") + val spanId: String, + + @SerializedName("parent_span_id") + val parentSpanId: String?, + + @SerializedName("name") + val name: String, + + @SerializedName("start_time_unix_nano") + val startTimeNanos: Long, + + @SerializedName("end_time_unix_nano") + val endTimeNanos: Long, + + @SerializedName("status") + val status: StatusCode = StatusCode.UNSET, + + @SerializedName("events") + val events: List = emptyList(), + + @SerializedName("attributes") + val attributes: Map = emptyMap() +) { + internal constructor(spanData: SpanData) : this( + traceId = spanData.spanContext.traceId, + spanId = spanData.spanContext.spanId, + parentSpanId = spanData.parentSpanId, + name = spanData.name, + startTimeNanos = spanData.startEpochNanos, + endTimeNanos = spanData.endEpochNanos, + status = spanData.status.statusCode, + events = fromEventData(eventDataList = spanData.events), + attributes = spanData.attributes.asMap().entries.associate { it.key.key to it.value.toString() } + ) + + companion object { + fun fromEventData(eventDataList: List?): List { + val events = mutableListOf() + eventDataList?.forEach { eventData -> + val event = EmbraceSpanEvent.create( + name = eventData.name, + timestampNanos = eventData.epochNanos, + attributes = eventData.attributes.asMap().entries.associate { it.key.key to it.value.toString() } + ) + if (event != null) { + events.add(event) + } + } + return events + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanExporter.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanExporter.kt new file mode 100644 index 0000000000..34a93fd3ae --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanExporter.kt @@ -0,0 +1,24 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.InternalApi +import io.opentelemetry.api.trace.Span +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.sdk.trace.export.SpanExporter + +/** + * Exports the given completed [Span] to the given [SpansService] + * + * Note: no explicit tests exist for this as its functionality is tested via the tests for [SpansServiceImpl] + */ +@InternalApi +internal class EmbraceSpanExporter(private val spansService: SpansService) : SpanExporter { + @Synchronized + override fun export(spans: MutableCollection): CompletableResultCode = + spansService.storeCompletedSpans(spans.toList()) + + override fun flush(): CompletableResultCode = CompletableResultCode.ofSuccess() + + @Synchronized + override fun shutdown(): CompletableResultCode = CompletableResultCode.ofSuccess() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl.kt new file mode 100644 index 0000000000..69124d03a2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl.kt @@ -0,0 +1,125 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent.Companion.inputsValid +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanBuilder +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +internal class EmbraceSpanImpl( + private val spanBuilder: SpanBuilder, + override val parent: EmbraceSpan? = null +) : EmbraceSpan { + + init { + spanBuilder.updateParent(parent) + } + + private val startedSpan: AtomicReference = AtomicReference(null) + private val eventCount = AtomicInteger(0) + private val attributeCount = AtomicInteger(0) + + override val traceId: String? + get() = startedSpan.get()?.spanContext?.traceId + + override val spanId: String? + get() = startedSpan.get()?.spanContext?.spanId + + override val isRecording: Boolean + get() = startedSpan.get()?.isRecording == true + + override fun start(): Boolean { + return if (startedSpan.get() != null) { + false + } else { + synchronized(startedSpan) { + startedSpan.set(spanBuilder.startSpan()) + startedSpan.get() != null + } + } + } + + override fun stop(): Boolean = stop(errorCode = null) + + override fun stop(errorCode: ErrorCode?): Boolean { + return if (startedSpan.get()?.isRecording == false) { + false + } else { + synchronized(startedSpan) { + startedSpan.get()?.endSpan(errorCode) + startedSpan.get()?.isRecording == false + } + } + } + + override fun addEvent(name: String): Boolean = addEvent(name = name, time = null, attributes = null) + + override fun addEvent(name: String, time: Long?, attributes: Map?): Boolean { + if (eventCount.get() < MAX_EVENT_COUNT && inputsValid(name, attributes)) { + synchronized(eventCount) { + if (eventCount.get() < MAX_EVENT_COUNT) { + spanInProgress()?.let { span -> + if (time != null && !attributes.isNullOrEmpty()) { + span.addEvent(name, Attributes.builder().fromMap(attributes).build(), time, TimeUnit.MILLISECONDS) + } else if (time != null) { + span.addEvent(name, time, TimeUnit.MILLISECONDS) + } else if (!attributes.isNullOrEmpty()) { + span.addEvent(name, Attributes.builder().fromMap(attributes).build()) + } else { + span.addEvent(name) + } + eventCount.incrementAndGet() + return true + } + } + } + } + + return false + } + + override fun addAttribute(key: String, value: String): Boolean { + if (attributeCount.get() < MAX_ATTRIBUTE_COUNT && attributeValid(key, value)) { + synchronized(attributeCount) { + if (attributeCount.get() < MAX_ATTRIBUTE_COUNT) { + spanInProgress()?.let { + it.setAttribute(key, value) + attributeCount.incrementAndGet() + return true + } + } + } + } + + return false + } + + internal fun wrappedSpan(): Span? = startedSpan.get() + + /** + * Returns the underlying [Span] if it's currently recording + */ + private fun spanInProgress(): Span? = startedSpan.get().takeIf { isRecording } + + companion object { + internal const val MAX_NAME_LENGTH = 50 + internal const val MAX_EVENT_COUNT = 10 + internal const val MAX_ATTRIBUTE_COUNT = 50 + internal const val MAX_ATTRIBUTE_KEY_LENGTH = 50 + internal const val MAX_ATTRIBUTE_VALUE_LENGTH = 200 + + internal fun inputsValid(name: String, events: List? = null, attributes: Map? = null) = + name.isNotBlank() && + name.length <= MAX_NAME_LENGTH && + (events == null || events.size <= MAX_EVENT_COUNT) && + (attributes == null || attributes.size <= MAX_ATTRIBUTE_COUNT) + + internal fun attributeValid(key: String, value: String) = + key.length <= MAX_ATTRIBUTE_KEY_LENGTH && value.length <= MAX_ATTRIBUTE_VALUE_LENGTH + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor.kt new file mode 100644 index 0000000000..2de1a2f7d8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanProcessor.kt @@ -0,0 +1,35 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.InternalApi +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import io.opentelemetry.sdk.trace.ReadWriteSpan +import io.opentelemetry.sdk.trace.ReadableSpan +import io.opentelemetry.sdk.trace.SpanProcessor +import io.opentelemetry.sdk.trace.export.SpanExporter +import java.util.concurrent.atomic.AtomicLong + +/** + * [SpanProcessor] that adds custom attributes to a [Span] when it starts, and exports it to the given [SpanExporter] when it finishes + * + * Note: no explicit tests exist for this as its functionality is tested via the tests for [SpansServiceImpl] + */ +@InternalApi +internal class EmbraceSpanProcessor(private val spanExporter: SpanExporter) : SpanProcessor { + + // TODO: sequence-id should be persisted across cold starts to better gauge data loss + private val counter = AtomicLong(1) + + override fun onStart(parentContext: Context, span: ReadWriteSpan) { + span.setSequenceId(counter.getAndIncrement()) + } + + override fun onEnd(span: ReadableSpan) { + // TODO: consider exporting this to a buffer that will export the collected Spans in bulk for performance reasons + spanExporter.export(mutableListOf(span.toSpanData())) + } + + override fun isStartRequired() = false + + override fun isEndRequired() = true +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansService.kt new file mode 100644 index 0000000000..aabc834ab9 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansService.kt @@ -0,0 +1,172 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.config.ConfigListener +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.sdk.common.Clock +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A [SpansService] that can be instantiated quickly. At that time, it will defer calls to the [SpansService] interface to a stubby + * implementation that does nothing, to be continually used if the spans feature is not turned in. If after instantiate, we know that + * the feature is on, you can change its delegation to an actual functioning [SpansService] implementation (i.e. [SpansServiceImpl]) + * by calling the [initializeService] method. It is recommended that this is done in the background rather than on the main thread because + * it may not be fast and doing it in the background doesn't affect how it works. + */ +internal class EmbraceSpansService(private val clock: Clock) : Initializable, SpansService, ConfigListener { + /** + * When this instance has been initialized with an instance of [SpansService] that does the proper spans logging + */ + private val initialized = AtomicBoolean(false) + private val spansEnabled = AtomicBoolean(false) + + // Not putting a limit on the number of calls to buffer because this is only being used internally + // Once this is exposed to customers directly, we will restrict the calls to accept to something reasonable + private val bufferedCalls = ConcurrentLinkedQueue() + + @Volatile + private var sdkInitStartTime: Long? = null + + @Volatile + private var sdkInitEndTime: Long? = null + + @Volatile + private var currentDelegate: SpansService = SpansService.featureDisabledSpansService + + override fun initializeService(sdkInitStartTimeNanos: Long, sdkInitEndTimeNanos: Long) { + if (!initialized.get()) { + sdkInitStartTime = sdkInitStartTimeNanos + sdkInitEndTime = sdkInitEndTimeNanos + synchronized(spansEnabled) { + if (!initialized.get() && spansEnabled.get()) { + currentDelegate = SpansServiceImpl( + sdkInitStartTimeNanos = sdkInitStartTimeNanos, + sdkInitEndTimeNanos = sdkInitEndTimeNanos, + clock = clock + ) + initialized.set(true) + recordBufferedCalls() + } + } + } + } + + override fun initialized(): Boolean = initialized.get() + + override fun createSpan(name: String, parent: EmbraceSpan?, type: EmbraceAttributes.Type, internal: Boolean): EmbraceSpan? = + currentDelegate.createSpan(name = name, parent = parent, type = type, internal = internal) + + override fun recordSpan(name: String, parent: EmbraceSpan?, type: EmbraceAttributes.Type, internal: Boolean, code: () -> T): T = + currentDelegate.recordSpan(name = name, parent = parent, type = type, internal = internal, code = code) + + override fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + parent: EmbraceSpan?, + type: EmbraceAttributes.Type, + internal: Boolean, + attributes: Map, + events: List, + errorCode: ErrorCode? + ): Boolean { + return if (!initialized()) { + /** + * Note: there's no way to public way to create an [EmbraceSpan] before the service is initialized, so while we buffer + * the passed in [parent], we should never get a valid non-null value for it here. + */ + bufferedCalls.add( + BufferedRecordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + parent = parent, + type = type, + internal = internal, + attributes = attributes, + events = events, + errorCode = errorCode + ) + ) + recordBufferedCalls() + true + } else { + currentDelegate.recordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + parent = parent, + type = type, + internal = internal, + attributes = attributes, + events = events, + errorCode = errorCode + ) + } + } + + override fun storeCompletedSpans(spans: List): CompletableResultCode = + currentDelegate.storeCompletedSpans(spans = spans) + + override fun completedSpans(): List? = currentDelegate.completedSpans() + + override fun flushSpans(appTerminationCause: EmbraceAttributes.AppTerminationCause?): List? = + currentDelegate.flushSpans(appTerminationCause = appTerminationCause) + + override fun onConfigChange(configService: ConfigService) { + if (!initialized.get() && configService.spansBehavior.isSpansEnabled()) { + synchronized(spansEnabled) { + spansEnabled.set(true) + if (!initialized.get()) { + val startTime = sdkInitStartTime + val endTime = sdkInitEndTime + if (startTime != null && endTime != null) { + initializeService(sdkInitStartTimeNanos = startTime, sdkInitEndTimeNanos = endTime) + } + } + } + } + } + + private fun recordBufferedCalls() { + if (initialized()) { + synchronized(bufferedCalls) { + do { + bufferedCalls.poll()?.let { + currentDelegate.recordCompletedSpan( + name = it.name, + startTimeNanos = it.startTimeNanos, + endTimeNanos = it.endTimeNanos, + parent = it.parent, + type = it.type, + internal = it.internal, + attributes = it.attributes, + events = it.events, + errorCode = it.errorCode + ) + } + } while (bufferedCalls.isNotEmpty()) + } + } + } +} + +/** + * Represents a call to [EmbraceSpansService.recordCompletedSpan] that is saved to be invoked later when the service is initialized + */ +private data class BufferedRecordCompletedSpan( + val name: String, + val startTimeNanos: Long, + val endTimeNanos: Long, + val parent: EmbraceSpan?, + val type: EmbraceAttributes.Type, + val internal: Boolean, + val attributes: Map, + val events: List, + val errorCode: ErrorCode? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracer.kt new file mode 100644 index 0000000000..c7f8257d4b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracer.kt @@ -0,0 +1,115 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.embrace.android.embracesdk.spans.TracingApi + +internal class EmbraceTracer(private val spansService: SpansService) : TracingApi { + override fun isTracingAvailable(): Boolean = spansService is Initializable && spansService.initialized() + + override fun createSpan(name: String): EmbraceSpan? = + createSpan(name = name, parent = null) + + override fun createSpan(name: String, parent: EmbraceSpan?): EmbraceSpan? = + spansService.createSpan( + name = name, + parent = parent, + internal = false + ) + + override fun recordSpan(name: String, code: () -> T): T = recordSpan(name = name, parent = null, code = code) + + override fun recordSpan(name: String, parent: EmbraceSpan?, code: () -> T): T = + spansService.recordSpan( + name = name, + parent = parent, + internal = false, + code = code + ) + + override fun recordCompletedSpan(name: String, startTimeNanos: Long, endTimeNanos: Long): Boolean = + recordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + errorCode = null, + parent = null, + attributes = null, + events = null + ) + + override fun recordCompletedSpan(name: String, startTimeNanos: Long, endTimeNanos: Long, errorCode: ErrorCode?): Boolean = + recordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + errorCode = errorCode, + parent = null, + attributes = null, + events = null + ) + + override fun recordCompletedSpan(name: String, startTimeNanos: Long, endTimeNanos: Long, parent: EmbraceSpan?): Boolean = + recordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + errorCode = null, + parent = parent, + attributes = null, + events = null + ) + + override fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + errorCode: ErrorCode?, + parent: EmbraceSpan? + ): Boolean = recordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + errorCode = errorCode, + parent = parent, + attributes = null, + events = null + ) + + override fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + attributes: Map?, + events: List? + ): Boolean = recordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + errorCode = null, + parent = null, + attributes = attributes, + events = events + ) + + override fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + errorCode: ErrorCode?, + parent: EmbraceSpan?, + attributes: Map?, + events: List? + ): Boolean = + spansService.recordCompletedSpan( + name = name, + startTimeNanos = startTimeNanos, + endTimeNanos = endTimeNanos, + parent = parent, + internal = false, + attributes = attributes ?: emptyMap(), + events = events ?: emptyList(), + errorCode = errorCode + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService.kt new file mode 100644 index 0000000000..68e92fe8c5 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/FeatureDisabledSpansService.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.InternalApi +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.trace.data.SpanData + +/** + * An implementation of [SpansService] that does nothing, to be used when the feature is not enabled + */ +@InternalApi +internal class FeatureDisabledSpansService : SpansService { + override fun createSpan(name: String, parent: EmbraceSpan?, type: EmbraceAttributes.Type, internal: Boolean): EmbraceSpan? = null + + override fun recordSpan(name: String, parent: EmbraceSpan?, type: EmbraceAttributes.Type, internal: Boolean, code: () -> T) = code() + + override fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + parent: EmbraceSpan?, + type: EmbraceAttributes.Type, + internal: Boolean, + attributes: Map, + events: List, + errorCode: ErrorCode? + ): Boolean = false + + override fun storeCompletedSpans(spans: List): CompletableResultCode = CompletableResultCode.ofFailure() + + override fun completedSpans(): List? = null + + override fun flushSpans(appTerminationCause: EmbraceAttributes.AppTerminationCause?): List? = null +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/Initializable.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/Initializable.kt new file mode 100644 index 0000000000..a012dce45c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/Initializable.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk.internal.spans + +/** + * Exposes logic to initialize the implementing service. This lives in its own interface so the users of the service will not have + * direct access to initialize it because it's none of their business. + */ +internal interface Initializable { + /** + * Initialize the service so the SDK can start logging spans. Spans will not be logged by this service until this call completes. + */ + fun initializeService(sdkInitStartTimeNanos: Long, sdkInitEndTimeNanos: Long) + + /** + * Returns true if this service is initialized already + */ + fun initialized(): Boolean +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansService.kt new file mode 100644 index 0000000000..48117d9c3f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansService.kt @@ -0,0 +1,74 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.api.trace.Span +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.trace.data.SpanData + +/** + * Public interface for an internal service that manages the recording, storage, and propagation of Spans + */ +internal interface SpansService { + /** + * Return an [EmbraceSpan] that can be started and stopped + */ + fun createSpan( + name: String, + parent: EmbraceSpan? = null, + type: EmbraceAttributes.Type = EmbraceAttributes.Type.PERFORMANCE, + internal: Boolean = true + ): EmbraceSpan? + + /** + * Record a key span around the given lambda with the current session span as its parent where the start time will be when the lambda + * starts and the end time will be when the lambda ends. If the lambda throws an exception, it will be recorded as a + * [ErrorCode.FAILURE]. The name of the span will be the provided name with the appropriate prefix prepended to it + * if [internal] is true. + */ + fun recordSpan( + name: String, + parent: EmbraceSpan? = null, + type: EmbraceAttributes.Type = EmbraceAttributes.Type.PERFORMANCE, + internal: Boolean = true, + code: () -> T + ): T + + /** + * Record a completed [Span] for work that has already been done. Returns true if the span was recorded or queued to be recorded, + * false if it wasn't. + */ + fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + parent: EmbraceSpan? = null, + type: EmbraceAttributes.Type = EmbraceAttributes.Type.PERFORMANCE, + internal: Boolean = true, + attributes: Map = emptyMap(), + events: List = emptyList(), + errorCode: ErrorCode? = null + ): Boolean + + /** + * Store the given list of completed Spans to be sent to the backend at the next available time + */ + fun storeCompletedSpans(spans: List): CompletableResultCode + + /** + * Return the list of the currently stored completed Spans that have not been sent to the backend + */ + fun completedSpans(): List? + + /** + * Flush and return all of the stored completed Spans. This should be called when the stored completed spans are ready to be sent to + * the backend. If the flush is being triggered because the app is about to terminate, set [appTerminationCause] to the appropriate + * value. Setting this to null means the app is not terminating. + */ + fun flushSpans(appTerminationCause: EmbraceAttributes.AppTerminationCause? = null): List? + + companion object { + val featureDisabledSpansService = FeatureDisabledSpansService() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImpl.kt new file mode 100644 index 0000000000..f82155dd4e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImpl.kt @@ -0,0 +1,294 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.InternalApi +import io.embrace.android.embracesdk.internal.Systrace +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanBuilder +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.common.Clock +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +/** + * Implementation of the core logic for [SpansService] + */ +@InternalApi +internal class SpansServiceImpl( + sdkInitStartTimeNanos: Long, + sdkInitEndTimeNanos: Long, + private val clock: Clock +) : SpansService { + private val sdkTracerProvider: SdkTracerProvider + by lazy { + Systrace.start("spans-service-init") + Systrace.trace("init-sdk-tracer-provider") { + SdkTracerProvider + .builder() + .addSpanProcessor(EmbraceSpanProcessor(EmbraceSpanExporter(this))) + .setClock(clock) + .build() + } + } + + private val openTelemetry: OpenTelemetry + by lazy { + Systrace.trace("init-otel-sdk") { + OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .build() + } + } + + private val tracer: Tracer + by lazy { + Systrace.trace("init-tracer") { + openTelemetry.getTracer(BuildConfig.LIBRARY_PACKAGE_NAME, BuildConfig.VERSION_NAME) + } + } + + /** + * Number of traces created in the current session. This value should be reset when a new session is created. + */ + private val currentSessionTraceCount = AtomicInteger(0) + + private val currentSessionChildSpansCount = mutableMapOf() + + /** + * The span that models the lifetime of the current session or background activity + */ + private val currentSessionSpan: AtomicReference = AtomicReference(startSessionSpan(sdkInitStartTimeNanos)) + + /** + * Spans that have finished, successfully or not, that will be sent with the next session or background activity payload. These + * should be cached along with the other data in the payload. + */ + private val completedSpans: MutableList = mutableListOf() + + init { + Systrace.trace("log-sdk-init") { + val events = EmbraceSpanEvent.create( + name = "start-time", + timestampNanos = sdkInitStartTimeNanos, + attributes = null + )?.let { listOf(it) } ?: emptyList() + + recordCompletedSpan( + name = "sdk-init", + startTimeNanos = sdkInitStartTimeNanos, + endTimeNanos = sdkInitEndTimeNanos, + events = events + ) + } + Systrace.end() + } + + override fun createSpan(name: String, parent: EmbraceSpan?, type: EmbraceAttributes.Type, internal: Boolean): EmbraceSpan? { + return if (EmbraceSpanImpl.inputsValid(name) && validateAndUpdateContext(parent, internal)) { + EmbraceSpanImpl( + spanBuilder = createRootSpanBuilder(name = name, type = type, internal = internal), + parent = parent + ) + } else { + null + } + } + + override fun recordSpan( + name: String, + parent: EmbraceSpan?, + type: EmbraceAttributes.Type, + internal: Boolean, + code: () -> T + ): T { + return if (EmbraceSpanImpl.inputsValid(name) && validateAndUpdateContext(parent, internal)) { + Systrace.start("log-span-$name") + try { + createRootSpanBuilder(name = name, type = type, internal = internal).updateParent(parent).record(code) + } finally { + Systrace.end() + } + } else { + code() + } + } + + override fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + parent: EmbraceSpan?, + type: EmbraceAttributes.Type, + internal: Boolean, + attributes: Map, + events: List, + errorCode: ErrorCode? + ): Boolean { + if (startTimeNanos > endTimeNanos) { + return false + } + + return if (EmbraceSpanImpl.inputsValid(name, events, attributes) && validateAndUpdateContext(parent, internal)) { + Systrace.trace("log-completed-span-$name") { + val span = createRootSpanBuilder(name = name, type = type, internal = internal) + .updateParent(parent) + .setStartTimestamp(startTimeNanos, TimeUnit.NANOSECONDS) + .startSpan() + .setAllAttributes(Attributes.builder().fromMap(attributes).build()) + + events.forEach { event -> + if (EmbraceSpanEvent.inputsValid(event.name, event.attributes)) { + span.addEvent( + event.name, + Attributes.builder().fromMap(event.attributes).build(), + event.timestampNanos, + TimeUnit.NANOSECONDS + ) + } + } + + span.endSpan(errorCode, endTimeNanos) + } + true + } else { + false + } + } + + override fun storeCompletedSpans(spans: List): CompletableResultCode { + try { + synchronized(completedSpans) { + completedSpans += spans.map { EmbraceSpanData(spanData = it) } + } + } catch (t: Throwable) { + return CompletableResultCode.ofFailure() + } + + return CompletableResultCode.ofSuccess() + } + + override fun completedSpans(): List { + synchronized(completedSpans) { + return completedSpans.toList() + } + } + + override fun flushSpans(appTerminationCause: EmbraceAttributes.AppTerminationCause?): List { + synchronized(completedSpans) { + if (appTerminationCause == null) { + currentSessionSpan.get().endSpan() + currentSessionSpan.set(startSessionSpan(TimeUnit.MILLISECONDS.toNanos(clock.now()))) + } else { + currentSessionSpan.get()?.let { + it.setAttribute(appTerminationCause.keyName(), appTerminationCause.name) + it.endSpan() + } + } + + val flushedSpans = completedSpans.toList() + completedSpans.clear() + return flushedSpans + } + } + + /** + * Creating a new Span is only possible if the current session span is active, the parent has already been started, and the total + * session trace limit has not been reached. Once this method returns true, a new span is assumed to have been created and will + * be counted as such towards the limits, so make sure there's no case afterwards where a Span is not created. + */ + private fun validateAndUpdateContext(parent: EmbraceSpan?, internal: Boolean): Boolean { + if (!currentSessionSpan.get().isRecording || (parent != null && parent.spanId == null)) { + return false + } + + if (!internal) { + if (parent == null) { + if (currentSessionTraceCount.get() < MAX_TRACE_COUNT_PER_SESSION) { + synchronized(currentSessionTraceCount) { + if (currentSessionTraceCount.get() < MAX_TRACE_COUNT_PER_SESSION) { + currentSessionTraceCount.incrementAndGet() + } else { + return false + } + } + } else { + return false + } + } else { + val rootSpanId = getRootSpanId(parent) + val currentSpanCount = currentSessionChildSpansCount[rootSpanId] + if (currentSpanCount == null) { + updateChildrenCount(rootSpanId) + } else if (currentSpanCount < MAX_SPAN_COUNT_PER_TRACE) { + synchronized(currentSessionChildSpansCount) { + val currentSpanCountAgain = currentSessionChildSpansCount[rootSpanId] + if (currentSpanCountAgain == null || currentSpanCountAgain < MAX_SPAN_COUNT_PER_TRACE) { + updateChildrenCount(rootSpanId) + } else { + return false + } + } + } else { + return false + } + } + } + + return true + } + + private fun getRootSpanId(span: EmbraceSpan): String { + var currentSpan: EmbraceSpan = span + while (currentSpan.parent != null) { + currentSpan.parent?.let { currentSpan = it } + } + + return currentSpan.spanId ?: "" + } + + private fun updateChildrenCount(rootSpanId: String) { + val currentCount = currentSessionChildSpansCount[rootSpanId] + if (currentCount == null) { + // The first time we'll know a root span ID is when a child is being added to it. Prior to that, when adding a prospective + // root span, the ID is not known yet. So the first time a root span is encountered add both it and the new child to the count. + // + // NOTE: Because we don't know whether the root span is internal or not at this point, it is assumed that it isn't. + // Therefore, it will count towards the limit if a non-internal span is added to the trace. + currentSessionChildSpansCount[rootSpanId] = 2 + } else { + currentSessionChildSpansCount[rootSpanId] = currentCount + 1 + } + } + + /** + * This method should always be used when starting a new session span + */ + private fun startSessionSpan(startTimeNanos: Long): Span { + currentSessionTraceCount.set(0) + return createEmbraceSpanBuilder(name = "session-span", type = EmbraceAttributes.Type.SESSION) + .setNoParent() + .setStartTimestamp(startTimeNanos, TimeUnit.NANOSECONDS) + .startSpan() + } + + private fun createRootSpanBuilder(name: String, type: EmbraceAttributes.Type, internal: Boolean): SpanBuilder = + createEmbraceSpanBuilder(name = name, type = type, internal = internal).setNoParent() + + private fun createEmbraceSpanBuilder(name: String, type: EmbraceAttributes.Type, internal: Boolean = true): SpanBuilder = + tracer.embraceSpanBuilder(name, internal).setType(type) + + companion object { + const val MAX_TRACE_COUNT_PER_SESSION = 100 + const val MAX_SPAN_COUNT_PER_TRACE = 10 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/MessageUtils.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/MessageUtils.kt new file mode 100644 index 0000000000..9775181f4f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/MessageUtils.kt @@ -0,0 +1,56 @@ +package io.embrace.android.embracesdk.internal.utils + +internal object MessageUtils { + + fun boolToStr(value: Boolean?): String { + return value?.toString() ?: "null" + } + + fun withNull(value: Number?): String { + return when (value) { + null -> "null" + else -> "\"" + value + "\"" + } + } + + fun withNull(value: String?): String { + return when (value) { + null -> "null" + else -> "\"" + value + "\"" + } + } + + fun withSet(set: Set?): String { + if (set.isNullOrEmpty()) { + return "[]" + } + val sb = StringBuilder() + sb.append("[") + for (v in set) { + sb.append(withNull(v)) + sb.append(",") + } + if (sb[sb.length - 1] == ',') { + sb.deleteCharAt(sb.length - 1) + } + sb.append("]") + return sb.toString() + } + + @JvmStatic + fun withMap(map: Map?): String { + if (map.isNullOrEmpty()) { + return "{}" + } + val sb = StringBuilder() + sb.append("{") + for ((key, value) in map) { + sb.append(withNull(key) + ": " + withNull(value) + ",") + } + if (sb[sb.length - 1] == ',') { + sb.deleteCharAt(sb.length - 1) + } + sb.append("}") + return sb.toString() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtils.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtils.kt new file mode 100644 index 0000000000..10ba881223 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtils.kt @@ -0,0 +1,37 @@ +package io.embrace.android.embracesdk.internal.utils + +import io.embrace.android.embracesdk.InternalApi + +/** + * Utilities to handle edge cases related to working with Throwables + */ + +/** + * Extension function that returns null for the stacktrace of a [Throwable] if an exception is thrown while trying to get it + */ +@InternalApi +internal fun Throwable.getSafeStackTrace(): Array? { + return try { + this.stackTrace + } catch (ex: Exception) { + null + } +} + +/** + * Return the canonical name of the cause of a [Throwable]. Handles null elements throughout, + * including the throwable and its cause, in which case [defaultName] is returned + */ +@InternalApi +internal fun causeName(throwable: Throwable?, defaultName: String = ""): String { + return throwable?.cause?.javaClass?.canonicalName ?: defaultName +} + +/** + * Return the message of the cause of a [Throwable]. Handles null elements throughout, + * including the throwable and its cause, in which case [defaultMessage] is returned + */ +@InternalApi +internal fun causeMessage(throwable: Throwable?, defaultMessage: String = ""): String { + return throwable?.cause?.message ?: defaultMessage +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/Uuid.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/Uuid.kt new file mode 100644 index 0000000000..43c5d75b45 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/Uuid.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk.internal.utils + +import java.util.UUID + +internal object Uuid { + + /** + * Get the Embrace UUID. If the argument uuid is null, generates the Embrace UUID using a + * random UUID. + * + * @param uuid the uuid. + * @return the Embrace UUID. + */ + @JvmStatic + @JvmOverloads + fun getEmbUuid(uuid: String? = null): String { + val input = uuid ?: UUID.randomUUID().toString() + + // optimization: avoid expensive pattern compilation in replaceAll() + val buf = input.toCharArray() + val sb = StringBuilder() + for (c in buf) { + if (c != '-') { + when (c) { + ' ' -> sb.append('0') + 'a' -> sb.append('A') + 'b' -> sb.append('B') + 'c' -> sb.append('C') + 'd' -> sb.append('D') + 'e' -> sb.append('E') + 'f' -> sb.append('F') + else -> sb.append(c) + } + } + } + return sb.toString() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/VersionChecker.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/VersionChecker.kt new file mode 100644 index 0000000000..06bfcb5c02 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/utils/VersionChecker.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.internal.utils + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast + +internal fun interface VersionChecker { + @ChecksSdkIntAtLeast(parameter = 0) + fun isAtLeast(min: Int): Boolean +} + +internal object BuildVersionChecker : VersionChecker { + @ChecksSdkIntAtLeast(parameter = 0) + override fun isAtLeast(min: Int) = Build.VERSION.SDK_INT >= min +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/AndroidLogger.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/AndroidLogger.kt new file mode 100644 index 0000000000..33c9a24690 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/AndroidLogger.kt @@ -0,0 +1,24 @@ +package io.embrace.android.embracesdk.logging + +import android.util.Log + +private const val EMBRACE_TAG = "[Embrace]" +private const val DEVELOPER_EMBRACE_TAG = "[EmbraceDev]" + +internal class AndroidLogger : InternalEmbraceLogger.LoggerAction { + override fun log( + msg: String, + severity: InternalStaticEmbraceLogger.Severity, + throwable: Throwable?, + logStacktrace: Boolean + ) { + val exception = throwable?.takeIf { logStacktrace } + when (severity) { + InternalStaticEmbraceLogger.Severity.DEBUG -> Log.d(EMBRACE_TAG, msg, exception) + InternalStaticEmbraceLogger.Severity.INFO -> Log.i(EMBRACE_TAG, msg, exception) + InternalStaticEmbraceLogger.Severity.WARNING -> Log.w(EMBRACE_TAG, msg, exception) + InternalStaticEmbraceLogger.Severity.DEVELOPER -> Log.d(DEVELOPER_EMBRACE_TAG, msg, exception) + else -> Log.e(EMBRACE_TAG, msg, exception) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorService.kt new file mode 100644 index 0000000000..851ac45196 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorService.kt @@ -0,0 +1,142 @@ +package io.embrace.android.embracesdk.logging + +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.payload.ExceptionError +import io.embrace.android.embracesdk.session.ActivityService +import java.net.BindException +import java.net.ConnectException +import java.net.HttpRetryException +import java.net.NoRouteToHostException +import java.net.PortUnreachableException +import java.net.ProtocolException +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.net.UnknownServiceException + +/** + * Intercepts Embrace SDK's exceptions errors and forwards them to the Embrace API. + */ +internal class EmbraceInternalErrorService( + private val activityService: ActivityService, + private val clock: Clock, + private val logStrictMode: Boolean +) { + private var configService: ConfigService? = null + var currentExceptionError: ExceptionError? = null + private set + + // ignore network-related exceptions since they are expected + private val ignoredExceptionClasses by lazy { + setOf>( + BindException::class.java, + ConnectException::class.java, + HttpRetryException::class.java, + NoRouteToHostException::class.java, + PortUnreachableException::class.java, + ProtocolException::class.java, + SocketException::class.java, + SocketTimeoutException::class.java, + UnknownHostException::class.java, + UnknownServiceException::class.java + ) + } + + private val ignoredExceptionStrings by lazy { + ignoredExceptionClasses.map { it.name } + } + + fun setConfigService(configService: ConfigService?) { + this.configService = configService + } + + private fun ignoreThrowableCause( + throwable: Throwable?, + capturedThrowable: HashSet + ): Boolean { + return if (throwable != null) { + if (ignoredExceptionClasses.contains(throwable.javaClass)) { + logDeveloper( + "EmbraceInternalErrorService", + "Exception ignored: " + throwable.javaClass + ) + true + } else { + /* if Hashset#add returns true means that the throwable was properly added, + if it returns false, the object already exists in the set so we return false + because we are in presence of a cycle in the Throwable cause */ + val addResult = capturedThrowable.add(throwable) + addResult && ignoreThrowableCause(throwable.cause, capturedThrowable) + } + } else false + } + + @Synchronized + fun handleInternalError(throwable: Throwable) { + logDebug("ignoreThrowableCause - handleInternalError") + if (ignoredExceptionClasses.contains(throwable.javaClass)) { + logDeveloper("EmbraceInternalErrorService", "Exception ignored: " + throwable.javaClass) + return + } else { + val capturedThrowable = HashSet() + if (ignoreThrowableCause(throwable.cause, capturedThrowable)) { + return + } + } + + // If the exception has been wrapped in another exception, the ignored exception name will + // show up as the start of the message, delimited by a semicolon. + val message = throwable.message + + if (message != null && ignoredExceptionStrings.contains( + message.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + ) + ) { + logDeveloper("EmbraceInternalErrorService", "Ignored exception: $throwable") + return + } + if (currentExceptionError == null) { + currentExceptionError = ExceptionError(logStrictMode) + } + + // if the config service has not been set yet, capture the exception + if (configService == null || configService?.dataCaptureEventBehavior?.isInternalExceptionCaptureEnabled() == true) { + logDeveloper( + "EmbraceInternalErrorService", + "Capturing exception, config service is not set yet: $throwable" + ) + currentExceptionError?.addException( + throwable, + getApplicationState(), + clock + ) + } + } + + private fun getApplicationState(): String = when { + activityService.isInBackground -> APPLICATION_STATE_BACKGROUND + else -> APPLICATION_STATE_ACTIVE + } + + @Synchronized + fun resetExceptionErrorObject() { + currentExceptionError = null + } + + companion object { + + /** + * Signals to the API that the application was in the foreground. + */ + private const val APPLICATION_STATE_ACTIVE = "active" + + /** + * Signals to the API that the application was in the background. + */ + private const val APPLICATION_STATE_BACKGROUND = "background" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalEmbraceLogger.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalEmbraceLogger.kt new file mode 100644 index 0000000000..5c2596b53e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalEmbraceLogger.kt @@ -0,0 +1,108 @@ +package io.embrace.android.embracesdk.logging + +import android.util.Log +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Severity +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Wrapper for the Android [Log] utility. + * Can only be used internally, it's not part of the public API. + */ + +// Suppressing "Nothing to inline". These functions are used all around the codebase, pretty often, so we want them to +// perform as fast as possible. +@Suppress("NOTHING_TO_INLINE") +internal class InternalEmbraceLogger { + private val defaultLogger = AndroidLogger() + private val loggerActions = CopyOnWriteArrayList() + + init { + setToDefault() + } + + private var threshold = Severity.INFO + + interface LoggerAction { + fun log(msg: String, severity: Severity, throwable: Throwable?, logStacktrace: Boolean) + } + + inline fun addLoggerAction(action: LoggerAction) { + loggerActions.add(action) + } + + @JvmOverloads + inline fun logDeveloper(className: String, msg: String, throwable: Throwable? = null) { + log("[$className] $msg", Severity.DEVELOPER, throwable, true) + } + + @JvmOverloads + inline fun logDebug(msg: String, throwable: Throwable? = null) { + log(msg, Severity.DEBUG, throwable, true) + } + + inline fun logInfo(msg: String) { + log(msg, Severity.INFO, null, true) + } + + @JvmOverloads + inline fun logWarning(msg: String, throwable: Throwable? = null, logStacktrace: Boolean = false) { + log(msg, Severity.WARNING, throwable, logStacktrace) + } + + @JvmOverloads + inline fun logError(msg: String, throwable: Throwable? = null, logStacktrace: Boolean = false) { + log(msg, Severity.ERROR, throwable, logStacktrace) + } + + // Log with INFO severity that always contains a throwable as an internal exception to be sent to Grafana + inline fun logInfoWithException(msg: String, throwable: Throwable? = null, logStacktrace: Boolean = false) { + log(msg, Severity.INFO, throwable ?: InternalErrorLogger.NotAnException(msg), logStacktrace) + } + + // Log with WARNING severity that always contains a throwable as an internal exception to be sent to Grafana + inline fun logWarningWithException(msg: String, throwable: Throwable? = null, logStacktrace: Boolean = false) { + log(msg, Severity.WARNING, throwable ?: InternalErrorLogger.NotAnException(msg), logStacktrace) + } + + fun logSDKNotInitialized(action: String) { + val msg = "Embrace SDK is not initialized yet, cannot $action." + log( + msg, + Severity.ERROR, + Throwable(msg), + true + ) + } + + /** + * Logs a message. + * + * @param msg the message to log. + * @param severity how severe the log is. If it's lower than the threshold, the message will not be logged. + * @param throwable exception, if any. + * @param logStacktrace should add the throwable to the logging + */ + + fun log(msg: String, severity: Severity, throwable: Throwable?, logStacktrace: Boolean) { + if (shouldTriggerLoggerActions(severity)) { + loggerActions.forEach { + it.log(msg, severity, throwable, logStacktrace) + } + } + } + + private fun shouldTriggerLoggerActions(severity: Severity) = + ApkToolsConfig.IS_DEVELOPER_LOGGING_ENABLED || severity >= threshold + + fun setThreshold(severity: Severity) { + threshold = severity + } + + internal fun setToDefault() { + if (loggerActions.isNotEmpty()) { + loggerActions.clear() + } + loggerActions.add(defaultLogger) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalErrorLogger.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalErrorLogger.kt new file mode 100644 index 0000000000..7d5e64f61b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalErrorLogger.kt @@ -0,0 +1,39 @@ +package io.embrace.android.embracesdk.logging + +internal class InternalErrorLogger( + private val embraceInternalErrorService: EmbraceInternalErrorService, + private val logger: InternalEmbraceLogger.LoggerAction, + private val logStrictMode: Boolean = false +) : InternalEmbraceLogger.LoggerAction { + + // TODO: in future we should queue these messages up and add them to the payload when the + // exception service is ready, + // so that early error messages don't get lost. We should create a clickup task for this + // and add it to the Q2 stability work + override fun log( + msg: String, + severity: InternalStaticEmbraceLogger.Severity, + throwable: Throwable?, + logStacktrace: Boolean + ) { + val finalThrowable = when { + logStrictMode && severity == InternalStaticEmbraceLogger.Severity.ERROR && throwable == null -> LogStrictModeException( + msg + ) + else -> throwable + } + + if (finalThrowable != null) { + try { + embraceInternalErrorService.handleInternalError(finalThrowable) + } catch (exc: Exception) { + logger.log(exc.localizedMessage ?: "", InternalStaticEmbraceLogger.Severity.ERROR, null, false) + } + } + } + + class LogStrictModeException(msg: String) : Exception(msg) + class IntegrationModeException(msg: String) : Exception(msg) + class InternalError(msg: String) : Exception(msg) + class NotAnException(msg: String) : Exception(msg) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger.kt new file mode 100644 index 0000000000..2949ee0b2e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/logging/InternalStaticEmbraceLogger.kt @@ -0,0 +1,88 @@ +package io.embrace.android.embracesdk.logging + +import android.util.Log + +/** + * Wrapper for the Android [Log] utility. + * Can only be used internally, it's not part of the public API. + */ + +// Suppressing "Nothing to inline". These functions are used all around the codebase, pretty often, so we want them to +// perform as fast as possible. +@Suppress("NOTHING_TO_INLINE") +internal class InternalStaticEmbraceLogger private constructor() { + + enum class Severity { + DEVELOPER, DEBUG, INFO, WARNING, ERROR, NONE + } + + companion object : InternalEmbraceLogger.LoggerAction { + + @JvmField + val logger = InternalEmbraceLogger() + + @JvmStatic + inline fun logDeveloper(className: String, msg: String, throwable: Throwable) { + log("[$className] $msg", Severity.DEVELOPER, throwable, true) + } + + @JvmStatic + inline fun logDeveloper(className: String, msg: String) { + log("[$className] $msg", Severity.DEVELOPER, null, true) + } + + @JvmStatic + @JvmOverloads + inline fun logDebug(msg: String, throwable: Throwable? = null) { + log(msg, Severity.DEBUG, throwable, true) + } + + @JvmStatic + inline fun logInfo(msg: String) { + log(msg, Severity.INFO, null, true) + } + + @JvmStatic + @JvmOverloads + inline fun logWarning(msg: String, throwable: Throwable? = null) { + log(msg, Severity.WARNING, throwable, true) + } + + @JvmStatic + @JvmOverloads + inline fun logError(msg: String, throwable: Throwable? = null, logStacktrace: Boolean = false) { + log(msg, Severity.ERROR, throwable, logStacktrace) + } + + // Log with INFO severity that always contains a throwable as an internal exception to be sent to Grafana + inline fun logInfoWithException(msg: String, throwable: Throwable? = null, logStacktrace: Boolean = false) { + log(msg, Severity.INFO, throwable ?: InternalErrorLogger.NotAnException(msg), logStacktrace) + } + + // Log with WARNING severity that always contains a throwable as an internal exception to be sent to Grafana + inline fun logWarningWithException(msg: String, throwable: Throwable? = null, logStacktrace: Boolean = false) { + log(msg, Severity.WARNING, throwable ?: InternalErrorLogger.NotAnException(msg), logStacktrace) + } + + /** + * Logs a message. + * + * @param msg the message to log. + * @param severity how severe the log is. If it's lower than the threshold, the message will not be logged. + * @param throwable exception, if any. + * @param logStacktrace should add the throwable to the logging + */ + + @JvmStatic + override fun log( + msg: String, + severity: Severity, + throwable: Throwable?, + logStacktrace: Boolean + ) = + logger.log(msg, severity, throwable, logStacktrace) + + @JvmStatic + fun setThreshold(severity: Severity) = logger.setThreshold(severity) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkService.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkService.java new file mode 100644 index 0000000000..39bfc3727e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkService.java @@ -0,0 +1,736 @@ +package io.embrace.android.embracesdk.ndk; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import io.embrace.android.embracesdk.Embrace; +import io.embrace.android.embracesdk.EmbraceEvent; +import io.embrace.android.embracesdk.capture.metadata.MetadataService; +import io.embrace.android.embracesdk.capture.user.UserService; +import io.embrace.android.embracesdk.comms.api.ApiClient; +import io.embrace.android.embracesdk.comms.delivery.DeliveryService; +import io.embrace.android.embracesdk.config.ConfigService; +import io.embrace.android.embracesdk.internal.ApkToolsConfig; +import io.embrace.android.embracesdk.internal.DeviceArchitecture; +import io.embrace.android.embracesdk.internal.SharedObjectLoader; +import io.embrace.android.embracesdk.internal.crash.CrashFileMarker; +import io.embrace.android.embracesdk.internal.utils.Uuid; +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger; +import io.embrace.android.embracesdk.payload.Event; +import io.embrace.android.embracesdk.payload.EventMessage; +import io.embrace.android.embracesdk.payload.NativeCrashData; +import io.embrace.android.embracesdk.payload.NativeCrashDataError; +import io.embrace.android.embracesdk.payload.NativeCrashMetadata; +import io.embrace.android.embracesdk.payload.NativeSymbols; +import io.embrace.android.embracesdk.session.ActivityListener; +import io.embrace.android.embracesdk.session.ActivityService; +import io.embrace.android.embracesdk.session.EmbraceSessionProperties; +import io.embrace.android.embracesdk.session.SessionService; +import kotlin.Lazy; +import kotlin.LazyKt; + +class EmbraceNdkService implements NdkService, ActivityListener { + + /** + * Signals to the API that the application was in the foreground. + */ + private static final String APPLICATION_STATE_ACTIVE = "active"; + /** + * Signals to the API that the application was in the background. + */ + private static final String APPLICATION_STATE_BACKGROUND = "background"; + /** + * The NDK symbols name that matches with the resource name injected by the plugin. + */ + private static final String KEY_NDK_SYMBOLS = "emb_ndk_symbols"; + + private static final String CRASH_REPORT_EVENT_NAME = "_crash_report"; + + private static final String NATIVE_CRASH_FILE_PREFIX = "emb_ndk"; + + private static final String NATIVE_CRASH_FILE_SUFFIX = ".crash"; + + private static final String NATIVE_CRASH_ERROR_FILE_SUFFIX = ".error"; + + private static final String NATIVE_CRASH_MAP_FILE_SUFFIX = ".map"; + + private static final String NATIVE_CRASH_FILE_FOLDER = "ndk"; + + private static final int MAX_NATIVE_CRASH_FILES_ALLOWED = 4; + + private static final int EMB_DEVICE_META_DATA_SIZE = 2048; + + private static final int HANDLER_CHECK_DELAY_MS = 5000; + + /** + * Synchronization lock. + */ + private final Object lock = new Object(); + /** + * The device architecture. + */ + private final DeviceArchitecture deviceArchitecture; + /** + * Whether or not the NDK has been installed. + */ + private boolean isInstalled = false; + + private final Context context; + + private final MetadataService metadataService; + + private final ConfigService configService; + + private final DeliveryService deliveryService; + + private final UserService userService; + + private final EmbraceSessionProperties sessionProperties; + + private Lazy gson; + + private String unityCrashId; + + private final Lazy cacheDir; + + private final ExecutorService cleanCacheExecutorService; + private final ExecutorService ndkStartupExecutorService; + + private final SharedObjectLoader sharedObjectLoader; + private final InternalEmbraceLogger logger; + private final kotlin.Lazy> symbolsForArch; + + private final EmbraceNdkServiceRepository repository; + private final NdkServiceDelegate.NdkDelegate delegate; + + EmbraceNdkService( + @NonNull Context context, + @NonNull MetadataService metadataService, + @NonNull ActivityService activityService, + @NonNull ConfigService configService, + @NonNull DeliveryService deliveryService, + @NonNull UserService userService, + @NonNull EmbraceSessionProperties sessionProperties, + @NonNull Embrace.AppFramework appFramework, + @NonNull SharedObjectLoader sharedObjectLoader, + @NonNull InternalEmbraceLogger logger, + @NonNull EmbraceNdkServiceRepository repository, + @NonNull NdkServiceDelegate.NdkDelegate delegate, + @NonNull ExecutorService cleanCacheExecutorService, + @NonNull ExecutorService ndkStartupExecutorService, + @NonNull DeviceArchitecture deviceArchitecture) { + + this.context = context; + this.metadataService = metadataService; + this.configService = configService; + this.deliveryService = deliveryService; + this.userService = userService; + this.sessionProperties = sessionProperties; + this.sharedObjectLoader = sharedObjectLoader; + this.logger = logger; + this.repository = repository; + this.delegate = delegate; + this.deviceArchitecture = deviceArchitecture; + + this.symbolsForArch = LazyKt.lazy(() -> { + NativeSymbols nativeSymbols = getNativeSymbols(); + if (nativeSymbols != null) { + String arch = deviceArchitecture.getArchitecture(); + return nativeSymbols.getSymbolByArchitecture(arch); + } + return null; + }); + + this.cacheDir = LazyKt.lazy(context::getCacheDir); + this.cleanCacheExecutorService = cleanCacheExecutorService; + this.ndkStartupExecutorService = ndkStartupExecutorService; + + if (configService.getAutoDataCaptureBehavior().isNdkEnabled()) { + activityService.addListener(this); + this.gson = LazyKt.lazy(Gson::new); + + if (appFramework == Embrace.AppFramework.UNITY) { + this.unityCrashId = Uuid.getEmbUuid(); + } + + logger.logDeveloper("EmbraceNDKService", "NDK enabled - starting service installation."); + startNdk(); + cleanOldCrashFiles(); + } else { + logger.logDeveloper("EmbraceNDKService", "NDK disabled."); + } + } + + @Override + public void testCrash(boolean isCpp) { + if (isCpp) { + testCrashCpp(); + } else { + testCrashC(); + } + } + + @Override + public void updateSessionId(@NonNull String newSessionId) { + logger.logDeveloper("EmbraceNDKService", "NDK update (session ID): " + newSessionId); + + if (isInstalled) { + delegate._updateSessionId(newSessionId); + } + } + + @Override + public void onSessionPropertiesUpdate(@NonNull Map properties) { + logger.logDeveloper("EmbraceNDKService", "NDK update: (session properties): " + properties); + + if (isInstalled) { + updateDeviceMetaData(); + } + } + + @Override + public void onUserInfoUpdate() { + logger.logDeveloper("EmbraceNDKService", "NDK update (user)"); + + if (isInstalled) { + updateDeviceMetaData(); + } + } + + @Override + @Nullable + public String getUnityCrashId() { + return this.unityCrashId; + } + + @Override + public void onBackground(long timestamp) { + synchronized (lock) { + if (isInstalled) { + updateAppState(APPLICATION_STATE_BACKGROUND); + } + } + } + + @Override + public void onForeground(boolean coldStart, long startupTime, long timestamp) { + synchronized (lock) { + if (isInstalled) { + updateAppState(APPLICATION_STATE_ACTIVE); + } + } + } + + private void startNdk() { + try { + if (sharedObjectLoader.loadEmbraceNative()) { + installSignals(); + createCrashReportDirectory(); + Handler handler = new Handler(Looper.myLooper()); + handler.postDelayed(this::checkSignalHandlersOverwritten, HANDLER_CHECK_DELAY_MS); + logger.logInfo("NDK library successfully loaded"); + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to load embrace library - probable unsatisfied linkage."); + } + } catch (Exception ex) { + logger.logError("Failed to load NDK library", ex); + } + } + + @VisibleForTesting + void checkSignalHandlersOverwritten() { + if (configService.getAutoDataCaptureBehavior().isSigHandlerDetectionEnabled()) { + String culprit = delegate._checkForOverwrittenHandlers(); + + if (culprit != null) { + if (shouldIgnoreOverriddenHandler(culprit)) { + return; + } + String errMsg = "Embrace detected that another signal handler has replaced our signal handler.\n" + + "This may lead to unexpected behaviour and lost NDK crashes.\n" + + "We will attempt to reinstall our signal handler but please consider disabling\n" + + "other signal handlers if you observed unexpected behaviour.\n" + + "If you believe this is a false positive, please contact support@embrace.io.\n" + + "Handler origin: " + culprit; + RuntimeException exc = new RuntimeException(errMsg); + exc.setStackTrace(new StackTraceElement[0]); + logger.logWarningWithException(errMsg, exc, false); + delegate._reinstallSignalHandlers(); + } + } + } + + /** + * Contains a list of SO files which are known to install signal handlers that do not + * interfere with crash detection. This list will probably expand over time. + * + * @param culprit the culprit SO file as identified by dladdr + * @return true if we can safely ignore + */ + private boolean shouldIgnoreOverriddenHandler(@NonNull String culprit) { + List allowList = Collections.singletonList("libwebviewchromium.so"); + for (String allowed : allowList) { + if (culprit.contains(allowed)) { + return true; + } + } + return false; + } + + protected void createCrashReportDirectory() { + String directory = cacheDir.getValue() + "/" + NATIVE_CRASH_FILE_FOLDER; + File directoryFile = new File(directory); + + if (directoryFile.exists()) { + return; + } + + if (!directoryFile.mkdirs()) { + logger.logError("Failed to create crash report directory {crashDirPath=" + directoryFile.getAbsolutePath() + "}"); + } + } + + protected void installSignals() { + String reportBasePath = cacheDir.getValue().getAbsolutePath() + "/" + NATIVE_CRASH_FILE_FOLDER; + String markerFilePath = cacheDir.getValue().getAbsolutePath() + "/" + CrashFileMarker.CRASH_MARKER_FILE_NAME; + logger.logDeveloper("EmbraceNDKService", "Creating report path at " + reportBasePath); + + String nativeCrashId; + // Assign the native crash id to the unity crash id. Then when a unity crash occurs, the + // Embrace crash service will set the unity crash id to the java crash. + if (this.unityCrashId != null) { + nativeCrashId = this.unityCrashId; + } else { + nativeCrashId = Uuid.getEmbUuid(); + } + + boolean is32bit = deviceArchitecture.is32BitDevice(); + logger.logDeveloper("EmbraceNDKService", "Installing signal handlers. 32bit=" + is32bit + ", crashId=" + nativeCrashId); + + String initialMetaData = new NativeCrashMetadata( + this.metadataService.getLightweightAppInfo(), + this.metadataService.getLightweightDeviceInfo(), + this.userService.getUserInfo(), + this.sessionProperties.get()).toJson(); + + delegate._installSignalHandlers( + reportBasePath, + markerFilePath, + initialMetaData, + "null", + this.metadataService.getAppState(), + nativeCrashId, + Build.VERSION.SDK_INT, + is32bit, + ApkToolsConfig.IS_DEVELOPER_LOGGING_ENABLED); + + updateDeviceMetaData(); + + isInstalled = true; + } + + /** + * Find and parse a native error File to NativeCrashData Error List + * + * @return List of NativeCrashData error + */ + protected List getNativeCrashErrors(NativeCrashData nativeCrash, File errorFile) { + if (errorFile != null) { + String absolutePath = errorFile.getAbsolutePath(); + logger.logDeveloper("EmbraceNDKService", "Processing error file at " + absolutePath); + + String errorsRaw = delegate._getErrors(absolutePath); + if (errorsRaw != null) { + Type listOfNativeCrashError = new TypeToken>() { + }.getType(); + try { + return gson.getValue().fromJson(errorsRaw, listOfNativeCrashError); + } catch (JsonSyntaxException e) { + logger.logError("Failed to parse native crash error file {crashId=" + nativeCrash.getNativeCrashId() + + ", errorFilePath=" + absolutePath + "}"); + } + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to load errorsRaw."); + } + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to find error file for crash."); + } + + return null; + } + + /** + * Process map file for crash to read and return its content as String + */ + private String getMapFileContent(File mapFile) { + if (mapFile != null) { + logger.logDeveloper("EmbraceNDKService", "Processing map file at " + mapFile.getAbsolutePath()); + + String mapContents = readMapFile(mapFile); + if (mapContents != null) { + return mapContents; + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to load mapContents."); + } + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to find map file for crash."); + } + + return null; + } + + /** + * Check if a native crash file exists. Also checks for the symbols file in the build dir. + * If so, attempt to send an event message and call {@link SessionService} to update the crash + * report id in the appropriate pending session. + * + * @return Crash data, if a native crash file was found + */ + @Nullable + @Override + public NativeCrashData checkForNativeCrash() { + logger.logDeveloper("EmbraceNDKService", "Processing native crash check runnable."); + + NativeCrashData nativeCrash = null; + List matchingFiles = repository.sortNativeCrashes(false); + logger.logDeveloper("EmbraceNDKService", "Found " + matchingFiles.size() + " native crashes."); + + for (File crashFile : matchingFiles) { + try { + String path = crashFile.getPath(); + String crashRaw = delegate._getCrashReport(path); + logger.logDeveloper("EmbraceNDKService", "Processing native crash at " + path); + + if (crashRaw != null) { + nativeCrash = gson.getValue().fromJson(crashRaw, NativeCrashData.class); + + if (nativeCrash == null) { + logger.logError("Failed to deserialize native crash error file: " + crashFile.getAbsolutePath()); + } + } else { + logger.logError("Failed to load crash report at " + path); + } + + File errorFile = repository.errorFileForCrash(crashFile); + if (nativeCrash != null) { + List errors = getNativeCrashErrors(nativeCrash, errorFile); + if (errors != null) { + nativeCrash.setErrors(errors); + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to find error file for crash."); + } + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to find error file for crash."); + } + + File mapFile = repository.mapFileForCrash(crashFile); + if (mapFile != null && nativeCrash != null) { + nativeCrash.setMap(getMapFileContent(mapFile)); + } else { + logger.logDeveloper("EmbraceNDKService", "Failed to find map file for crash."); + } + + // Retrieve deobfuscated symbols + if (nativeCrash != null) { + final Map symbols = getSymbolsForCurrentArch(); + if (symbols == null) { + logger.logError("Failed to find symbols for native crash - stacktraces will not symbolicate correctly."); + } else { + nativeCrash.setSymbols(symbols); + logger.logDeveloper("EmbraceNDKService", "Added symbols for native crash"); + } + sendNativeCrash(nativeCrash); + } + + repository.deleteFiles(crashFile, errorFile, mapFile, nativeCrash); + + } catch (JsonSyntaxException ex) { + //noinspection ResultOfMethodCallIgnored + crashFile.delete(); + logger.logError("Failed to parse JSON from crash file {crashFilePath=" + crashFile.getAbsolutePath() + "}.", ex, true); + } catch (Exception ex) { + //noinspection ResultOfMethodCallIgnored + crashFile.delete(); + logger.logError("Failed to read native crash file {crashFilePath=" + crashFile.getAbsolutePath() + "}.", ex, true); + } + } + + return nativeCrash; + } + + @Override + @Nullable + public Map getSymbolsForCurrentArch() { + return symbolsForArch.getValue(); + } + + @SuppressWarnings("DiscouragedApi") + private NativeSymbols getNativeSymbols() { + Resources resources = context.getResources(); + int resourceId = resources.getIdentifier(KEY_NDK_SYMBOLS, "string", context.getPackageName()); + + if (resourceId != 0) { + try { + String encodedSymbols = new String(Base64.decode(context.getResources().getString(resourceId), Base64.DEFAULT)); + return gson.getValue().fromJson(encodedSymbols, NativeSymbols.class); + } catch (Exception ex) { + logger.logError(String.format(Locale.getDefault(), "Failed to decode symbols from resources {resourceId=%d}.", + resourceId), + ex); + } + } else { + logger.logError(String.format(Locale.getDefault(), "Failed to find symbols in resources {resourceId=%d}.", + resourceId) + ); + } + + return null; + } + + private File[] getNativeFiles(FilenameFilter filter) { + File[] matchingFiles = null; + final File[] files = cacheDir.getValue().listFiles(); + + if (files == null) { + return null; + } + + for (File cached : files) { + if (cached.isDirectory() && cached.getName().equals(NATIVE_CRASH_FILE_FOLDER)) { + matchingFiles = cached.listFiles(filter); + break; + } + } + + return matchingFiles; + } + + private File[] getNativeErrorFiles() { + FilenameFilter nativeCrashFilter = (f, name) -> name.startsWith(NATIVE_CRASH_FILE_PREFIX) && name.endsWith(NATIVE_CRASH_ERROR_FILE_SUFFIX); + return getNativeFiles(nativeCrashFilter); + } + + private File[] getNativeMapFiles() { + FilenameFilter nativeCrashFilter = (f, name) -> name.startsWith(NATIVE_CRASH_FILE_PREFIX) && name.endsWith(NATIVE_CRASH_MAP_FILE_SUFFIX); + return getNativeFiles(nativeCrashFilter); + } + + @Nullable + private String readMapFile(File mapFile) { + try (FileInputStream fin = new FileInputStream(mapFile); + BufferedReader reader = new BufferedReader(new InputStreamReader(fin))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } catch (IOException e) { + return null; + } + } + + private void cleanOldCrashFiles() { + cleanCacheExecutorService.submit(() -> { + logger.logDeveloper("EmbraceNDKService", "Processing clean of old crash files."); + + List sortedFiles = repository.sortNativeCrashes(true); + + int deleteCount = sortedFiles.size() - MAX_NATIVE_CRASH_FILES_ALLOWED; + + if (deleteCount > 0) { + LinkedList files = new LinkedList<>(sortedFiles); + + try { + for (int i = 0; i < deleteCount; i++) { + File removed = files.get(i); + if (files.get(i).delete()) { + logger.logDebug("Native crash file " + removed.getName() + " removed from cache"); + } + } + } catch (Exception ex) { + logger.logError("Failed to delete native crash from cache.", ex); + } + } + + // delete error files that don't have matching crash files + File[] errorFiles = getNativeErrorFiles(); + if (errorFiles != null) { + for (File errorFile : errorFiles) { + if (hasNativeCrashFile(errorFile)) { + logger.logDeveloper("EmbraceNDKService", + "Skipping error file as it has a matching crash file " + errorFile.getAbsolutePath()); + continue; + } + errorFile.delete(); + logger.logDeveloper("EmbraceNDKService", + "Deleting error file as it has no matching crash file " + errorFile.getAbsolutePath()); + } + } + + // delete map files that don't have matching crash files + File[] mapFiles = getNativeMapFiles(); + if (mapFiles != null) { + for (File mapFile : mapFiles) { + if (hasNativeCrashFile(mapFile)) { + logger.logDeveloper("EmbraceNDKService", + "Skipping map file as it has a matching crash file " + mapFile.getAbsolutePath()); + continue; + } + mapFile.delete(); + logger.logDeveloper("EmbraceNDKService", + "Deleting map file as it has no matching crash file " + mapFile.getAbsolutePath()); + } + } + + return null; + }); + } + + private boolean hasNativeCrashFile(File file) { + String filename = file.getAbsolutePath(); + if (!filename.contains(".")) { + return false; + } + String crashFilename = filename.substring(0, filename.lastIndexOf('.')) + NATIVE_CRASH_FILE_SUFFIX; + File crashFile = new File(crashFilename); + return crashFile.exists(); + } + + private void sendNativeCrash(NativeCrashData nativeCrash) { + logger.logDeveloper("EmbraceNDKService", "Constructing EventMessage from native crash."); + + NativeCrashMetadata metadata = nativeCrash.getMetadata(); + Event nativeCrashEvent = new Event( + CRASH_REPORT_EVENT_NAME, + null, + Uuid.getEmbUuid(), + nativeCrash.getSessionId(), + EmbraceEvent.Type.CRASH, + nativeCrash.getTimestamp(), + null, + false, + null, + nativeCrash.getAppState(), + null, + metadata != null ? metadata.getSessionProperties() : null, + null, + null, + null, + null, + null + ); + + EventMessage nativeCrashMessageEvent = new EventMessage( + nativeCrashEvent, + null, + metadata != null ? metadata.getDeviceInfo() : null, + metadata != null ? metadata.getAppInfo() : null, + metadata != null ? metadata.getUserInfo() : null, + null, + null, + ApiClient.MESSAGE_VERSION, + nativeCrash.getCrash()); + + try { + logger.logDeveloper("EmbraceNDKService", "About to send EventMessage from native crash."); + deliveryService.sendEventAndWait(nativeCrashMessageEvent); + logger.logDeveloper("EmbraceNDKService", "Finished send attempt for EventMessage from native crash."); + } catch (Exception ex) { + logger.logError("Failed to report native crash to the api {sessionId=" + nativeCrash.getSessionId() + + ", crashId=" + nativeCrash.getNativeCrashId(), + ex); + } + } + + private void updateAppState(String newAppState) { + logger.logDeveloper("EmbraceNDKService", "NDK update (app state): " + newAppState); + delegate._updateAppState(newAppState); + } + + /** + * Compute NDK metadata on a background thread. + */ + private void updateDeviceMetaData() { + ndkStartupExecutorService.submit(() -> { + logger.logDeveloper("EmbraceNDKService", "Processing NDK metadata update on bg thread."); + + String newDeviceMetaData = getMetaData(true); + logger.logDeveloper("EmbraceNDKService", "NDK update (metadata): " + newDeviceMetaData); + + if (newDeviceMetaData.length() >= EMB_DEVICE_META_DATA_SIZE) { + logger.logDebug("Removing session properties from metadata to avoid exceeding size limitation for NDK metadata."); + newDeviceMetaData = getMetaData(false); + } + + delegate._updateMetaData(newDeviceMetaData); + + return null; + }); + } + + private String getMetaData(Boolean includeSessionProperties) { + return new NativeCrashMetadata( + this.metadataService.getAppInfo(), + this.metadataService.getDeviceInfo(), + this.userService.getUserInfo(), + includeSessionProperties ? this.sessionProperties.get() : null).toJson(); + } + + private void uninstallSignals() { + delegate._uninstallSignals(); + } + + private void testCrashC() { + delegate._testNativeCrash_C(); + } + + private void testCrashCpp() { + delegate._testNativeCrash_CPP(); + } + + @Override + public void applicationStartupComplete() { + } + + @Override + public void onView(@NonNull Activity activity) { + } + + @Override + public void onViewClose(@NonNull Activity activity) { + } + + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) { + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository.kt new file mode 100644 index 0000000000..e6722814a9 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepository.kt @@ -0,0 +1,111 @@ +package io.embrace.android.embracesdk.ndk + +import android.content.Context +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.NativeCrashData +import java.io.File +import java.io.FilenameFilter + +private const val NATIVE_CRASH_FILE_PREFIX = "emb_ndk" +private const val NATIVE_CRASH_FILE_SUFFIX = ".crash" +private const val NATIVE_CRASH_FILE_FOLDER = "ndk" +private const val NATIVE_CRASH_ERROR_FILE_SUFFIX = ".error" +private const val NATIVE_CRASH_MAP_FILE_SUFFIX = ".map" + +/** + * Encapsulates the logic of managing Files to get, sort and or delete them + */ +internal class EmbraceNdkServiceRepository( + private val context: Context, + private val logger: InternalEmbraceLogger +) { + + fun sortNativeCrashes(byOldest: Boolean): List { + val nativeCrashFiles: Array? = getNativeCrashFiles() + val nativeCrashList: MutableList = mutableListOf() + + nativeCrashFiles?.let { + nativeCrashList.addAll(nativeCrashFiles) + val sorted: MutableMap = HashMap() + try { + for (f in nativeCrashList) { + sorted[f] = f.lastModified() + } + + val comparator: Comparator = if (byOldest) { + Comparator { first: File, next: File -> sorted[first]?.compareTo(sorted[next]!!)!! } + } else { + Comparator { first: File, next: File -> sorted[next]?.compareTo(sorted[first]!!)!! } + } + return nativeCrashList.sortedWith(comparator) + } catch (ex: Exception) { + logger.logError("Failed sorting native crashes.", ex) + } + } + + return nativeCrashList + } + + private fun getNativeCrashFiles(): Array? { + val nativeCrashFilter = + FilenameFilter { _: File?, name: String -> + name.startsWith( + NATIVE_CRASH_FILE_PREFIX + ) && name.endsWith(NATIVE_CRASH_FILE_SUFFIX) + } + return getNativeFiles(nativeCrashFilter) + } + + private fun getNativeFiles(filter: FilenameFilter): Array? { + var matchingFiles: Array? = null + val files: Array = context.cacheDir.listFiles() ?: return null + for (cached in files) { + if (cached.isDirectory && cached.name == NATIVE_CRASH_FILE_FOLDER) { + matchingFiles = cached.listFiles(filter) + break + } + } + return matchingFiles + } + + private fun companionFileForCrash(crashFile: File, suffix: String): File? { + val crashFilename = crashFile.absolutePath + val errorFilename = crashFilename.substring(0, crashFilename.lastIndexOf('.')) + suffix + val errorFile = File(errorFilename) + return if (!errorFile.exists()) { + null + } else { + errorFile + } + } + + fun errorFileForCrash(crashFile: File): File? { + return companionFileForCrash(crashFile, NATIVE_CRASH_ERROR_FILE_SUFFIX) + } + + fun mapFileForCrash(crashFile: File): File? { + return companionFileForCrash(crashFile, NATIVE_CRASH_MAP_FILE_SUFFIX) + } + + fun deleteFiles( + crashFile: File, + errorFile: File?, + mapFile: File?, + nativeCrash: NativeCrashData? + ) { + if (!crashFile.delete()) { + val msg: String = if (nativeCrash != null) { + "Failed to delete native crash file {sessionId=" + nativeCrash.sessionId + + ", crashId=" + nativeCrash.nativeCrashId + + ", crashFilePath=" + crashFile.absolutePath + "}" + } else { + "Failed to delete native crash file {crashFilePath=" + crashFile.absolutePath + "}" + } + logger.logWarning(msg) + } else { + logger.logDebug("Deleted processed crash file at " + crashFile.absolutePath) + } + errorFile?.delete() + mapFile?.delete() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NativeModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NativeModule.kt new file mode 100644 index 0000000000..8f1b6954e8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NativeModule.kt @@ -0,0 +1,80 @@ +package io.embrace.android.embracesdk.ndk + +import io.embrace.android.embracesdk.anr.ndk.EmbraceNativeThreadSamplerService +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerInstaller +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.injection.CoreModule +import io.embrace.android.embracesdk.injection.DeliveryModule +import io.embrace.android.embracesdk.injection.EssentialServiceModule +import io.embrace.android.embracesdk.injection.singleton +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +internal interface NativeModule { + val ndkService: NdkService + val nativeThreadSamplerService: NativeThreadSamplerService? + val nativeThreadSamplerInstaller: NativeThreadSamplerInstaller? +} + +internal class NativeModuleImpl( + coreModule: CoreModule, + essentialServiceModule: EssentialServiceModule, + deliveryModule: DeliveryModule, + sessionProperties: EmbraceSessionProperties, + workerThreadModule: WorkerThreadModule +) : NativeModule { + + override val ndkService: NdkService by singleton { + EmbraceNdkService( + coreModule.context, + essentialServiceModule.metadataService, + essentialServiceModule.activityService, + essentialServiceModule.configService, + deliveryModule.deliveryService, + essentialServiceModule.userService, + sessionProperties, + coreModule.appFramework, + essentialServiceModule.sharedObjectLoader, + coreModule.logger, + embraceNdkServiceRepository, + NdkDelegateImpl(), + workerThreadModule.backgroundExecutor(ExecutorName.NATIVE_CRASH_CLEANER), + workerThreadModule.backgroundExecutor(ExecutorName.NATIVE_STARTUP), + essentialServiceModule.deviceArchitecture, + ) + } + + override val nativeThreadSamplerService: NativeThreadSamplerService? by singleton { + if (nativeThreadSamplingEnabled(essentialServiceModule.configService, essentialServiceModule.sharedObjectLoader)) { + EmbraceNativeThreadSamplerService( + essentialServiceModule.configService, + lazy { ndkService.getSymbolsForCurrentArch() }, + executorService = workerThreadModule.scheduledExecutor(ExecutorName.SCHEDULED_REGISTRATION), + deviceArchitecture = essentialServiceModule.deviceArchitecture + ) + } else { + null + } + } + + override val nativeThreadSamplerInstaller: NativeThreadSamplerInstaller? by singleton { + if (nativeThreadSamplingEnabled(essentialServiceModule.configService, essentialServiceModule.sharedObjectLoader)) { + NativeThreadSamplerInstaller() + } else { + null + } + } + + private fun nativeThreadSamplingEnabled(configService: ConfigService, sharedObjectLoader: SharedObjectLoader) = + configService.autoDataCaptureBehavior.isNdkEnabled() && sharedObjectLoader.loadEmbraceNative() + + private val embraceNdkServiceRepository by singleton { + EmbraceNdkServiceRepository( + coreModule.context, + coreModule.logger + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkService.kt new file mode 100644 index 0000000000..ada04e9d80 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkService.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.ndk + +import io.embrace.android.embracesdk.payload.NativeCrashData + +internal interface NdkService { + fun updateSessionId(newSessionId: String) + fun onSessionPropertiesUpdate(properties: Map) + fun onUserInfoUpdate() + fun getUnityCrashId(): String? + + // TODO: remove this. Only for testing purposes. + fun testCrash(isCpp: Boolean) + fun checkForNativeCrash(): NativeCrashData? + + /** + * Retrieves symbol information for the current architecture. + */ + fun getSymbolsForCurrentArch(): Map? +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkServiceDelegate.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkServiceDelegate.kt new file mode 100644 index 0000000000..871744617a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ndk/NdkServiceDelegate.kt @@ -0,0 +1,56 @@ +@file:Suppress("FunctionNaming", "FunctionParameterNaming") + +package io.embrace.android.embracesdk.ndk + +internal class NdkServiceDelegate { + internal interface NdkDelegate { + fun _installSignalHandlers( + report_path: String?, + markerFilePath: String?, + device_meta_data: String?, + session_id: String?, + app_state: String?, + report_id: String?, + api_level: Int, + is_32bit: Boolean, + dev_logging: Boolean + ) + + fun _updateMetaData(new_device_meta_data: String?) + fun _updateSessionId(new_session_id: String?) + fun _updateAppState(new_app_state: String?) + fun _uninstallSignals() + fun _testNativeCrash_C() + fun _testNativeCrash_CPP() + fun _getCrashReport(path: String?): String? + fun _getErrors(path: String?): String? + fun _checkForOverwrittenHandlers(): String? + fun _reinstallSignalHandlers(): Boolean + } +} + +@Suppress("UnusedPrivateClass") +internal class NdkDelegateImpl : NdkServiceDelegate.NdkDelegate { // TODO: update JNI signatures. + external override fun _installSignalHandlers( + report_path: String?, + markerFilePath: String?, + device_meta_data: String?, + session_id: String?, + app_state: String?, + report_id: String?, + api_level: Int, + is_32bit: Boolean, + dev_logging: Boolean + ) + + external override fun _updateMetaData(new_device_meta_data: String?) + external override fun _updateSessionId(new_session_id: String?) + external override fun _updateAppState(new_app_state: String?) + external override fun _uninstallSignals() + external override fun _testNativeCrash_C() + external override fun _testNativeCrash_CPP() + external override fun _getCrashReport(path: String?): String? + external override fun _getErrors(path: String?): String? + external override fun _checkForOverwrittenHandlers(): String? + external override fun _reinstallSignalHandlers(): Boolean +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequest.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequest.java new file mode 100644 index 0000000000..ccec0b0fd4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequest.java @@ -0,0 +1,499 @@ +package io.embrace.android.embracesdk.network; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; +import io.embrace.android.embracesdk.network.http.HttpMethod; +import io.embrace.android.embracesdk.network.http.NetworkCaptureData; + +/** + * This class is used to create manually-recorded network requests. + */ +public final class EmbraceNetworkRequest { + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was returned. + * If no response was returned, use {@link #fromIncompleteRequest(String, HttpMethod, long, long, String, String)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param bytesSent the number of bytes sent. + * @param bytesReceived the number of bytes received. + * @param statusCode the status code of the response. + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromCompletedRequest(@NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + long bytesSent, + long bytesReceived, + int statusCode + ) { + return fromCompletedRequest(url, + httpMethod, + startTime, + endTime, + bytesSent, + bytesReceived, + statusCode, + null, + null); + } + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was returned. + * If no response was returned, use {@link #fromIncompleteRequest(String, HttpMethod, long, long, String, String)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param bytesSent the number of bytes sent. + * @param bytesReceived the number of bytes received. + * @param statusCode the status code of the response. + * @param traceId the trace ID of the request, used for distributed tracing. + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromCompletedRequest(@NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + long bytesSent, + long bytesReceived, + int statusCode, + @Nullable String traceId + ) { + return fromCompletedRequest(url, + httpMethod, + startTime, + endTime, + bytesSent, + bytesReceived, + statusCode, + traceId, + null, + null); + } + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was returned. + * If no response was returned, use {@link #fromIncompleteRequest(String, HttpMethod, long, long, String, String)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param bytesSent the number of bytes sent. + * @param bytesReceived the number of bytes received. + * @param statusCode the status code of the response. + * @param traceId the trace ID of the request, used for distributed tracing. + * @param networkCaptureData the network capture data for the request. + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromCompletedRequest(@NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + long bytesSent, + long bytesReceived, + int statusCode, + @Nullable String traceId, + @Nullable NetworkCaptureData networkCaptureData + ) { + return fromCompletedRequest(url, + httpMethod, + startTime, + endTime, + bytesSent, + bytesReceived, + statusCode, + traceId, + null, + networkCaptureData); + } + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was returned. + * If no response was returned, use {@link #fromIncompleteRequest(String, HttpMethod, long, long, String, String, String)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param bytesSent the number of bytes sent. + * @param bytesReceived the number of bytes received. + * @param statusCode the status code of the response. + * @param traceId the trace ID of the request, used for distributed tracing. + * @param w3cTraceparent W3C-compliant traceparent representing the network call that is being recorded + * @param networkCaptureData the network capture data for the request. + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromCompletedRequest(@NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + long bytesSent, + long bytesReceived, + int statusCode, + @Nullable String traceId, + @Nullable String w3cTraceparent, + @Nullable NetworkCaptureData networkCaptureData + ) { + return new EmbraceNetworkRequest(url, + httpMethod, + startTime, + endTime, + bytesSent, + bytesReceived, + statusCode, + null, + null, + traceId, + w3cTraceparent, + networkCaptureData); + } + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was not returned. + * If a response was returned, use {@link #fromCompletedRequest(String, HttpMethod, long, long, long, long, int)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param errorType the error type that occurred. + * @param errorMessage the error message + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromIncompleteRequest( + @NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + @NonNull String errorType, + @NonNull String errorMessage + ) { + return fromIncompleteRequest(url, + httpMethod, + startTime, + endTime, + errorType, + errorMessage, + null); + } + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was not returned. + * If a response was returned, use {@link #fromCompletedRequest(String, HttpMethod, long, long, long, long, int)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param errorType the error type that occurred. + * @param errorMessage the error message + * @param traceId the trace ID of the request, used for distributed tracing. + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromIncompleteRequest( + @NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + @NonNull String errorType, + @NonNull String errorMessage, + @Nullable String traceId + ) { + return fromIncompleteRequest(url, + httpMethod, + startTime, + endTime, + errorType, + errorMessage, + traceId, + null, + null); + } + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was not returned. + * If a response was returned, use {@link #fromCompletedRequest(String, HttpMethod, long, long, long, long, int)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param errorType the error type that occurred. + * @param errorMessage the error message + * @param traceId the trace ID of the request, used for distributed tracing. + * @param networkCaptureData the network capture data for the request. + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromIncompleteRequest( + @NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + @NonNull String errorType, + @NonNull String errorMessage, + @Nullable String traceId, + @Nullable NetworkCaptureData networkCaptureData + ) { + return new EmbraceNetworkRequest( + url, + httpMethod, + startTime, + endTime, + null, + null, + null, + errorType, + errorMessage, + traceId, + null, + networkCaptureData + ); + } + + /** + * Construct a new {@link EmbraceNetworkRequest} instance where a HTTP response was not returned. + * If a response was returned, use {@link #fromCompletedRequest(String, HttpMethod, long, long, long, long, int)} + * instead. + * + * @param url the URL of the request. + * @param httpMethod the HTTP method of the request. + * @param startTime the start time of the request. + * @param endTime the end time of the request. + * @param errorType the error type that occurred. + * @param errorMessage the error message + * @param traceId the trace ID of the request, used for distributed tracing. + * @param w3cTraceparent W3C-compliant traceparent representing the network call that is being recorded + * @param networkCaptureData the network capture data for the request. + * @return a new {@link EmbraceNetworkRequest} instance. + */ + @NonNull + public static EmbraceNetworkRequest fromIncompleteRequest( + @NonNull String url, + @NonNull HttpMethod httpMethod, + long startTime, + long endTime, + @NonNull String errorType, + @NonNull String errorMessage, + @Nullable String traceId, + @Nullable String w3cTraceparent, + @Nullable NetworkCaptureData networkCaptureData + ) { + return new EmbraceNetworkRequest( + url, + httpMethod, + startTime, + endTime, + null, + null, + null, + errorType, + errorMessage, + traceId, + w3cTraceparent, + networkCaptureData + ); + } + + /** + * The request's URL. Must start with http:// or https:// + */ + @NonNull + private final String url; + + /** + * The request's method. Must be one of the following: GET, PUT, POST, DELETE, PATCH. + */ + @NonNull + private final HttpMethod httpMethod; + + /** + * The time the request started. + */ + @NonNull + private final Long startTime; + + /** + * The time the request ended. Must be greater than the startTime. + */ + @NonNull + private final Long endTime; + + /** + * The number of bytes received. + */ + @Nullable + private final Long bytesReceived; + + /** + * The number of bytes sent. + */ + @Nullable + private final Long bytesSent; + + /** + * The response status of the request. Must be in the range 100 to 599. + */ + @Nullable + private final Integer responseCode; + + /** + * Error object that describes a non-HTTP error, e.g. a connection error. + */ + @Nullable + private final Throwable error; + + /** + * Error type that describes a non-HTTP error, e.g. a connection error. + */ + @Nullable + private final String errorType; + + /** + * Error message that describes a non-HTTP error, e.g. a connection error. + */ + @Nullable + private final String errorMessage; + + /** + * Optional trace ID that can be used to trace a particular request. Max length is 64 characters. + */ + @Nullable + private final String traceId; + + /** + * Optional W3C-compliant traceparent representing the network call that is being recorded + */ + @Nullable + private final String w3cTraceparent; + + /** + * Network capture data for the request. + */ + @Nullable + private final NetworkCaptureData networkCaptureData; + + private EmbraceNetworkRequest(@NonNull String url, + @NonNull HttpMethod httpMethod, + @NonNull Long startTime, + @NonNull Long endTime, + @Nullable Long bytesSent, + @Nullable Long bytesReceived, + @Nullable Integer responseCode, + @Nullable String errorType, + @Nullable String errorMessage, + @Nullable String traceId, + @Nullable String w3cTraceparent, + @Nullable NetworkCaptureData networkCaptureData) { + this.url = url; + this.httpMethod = httpMethod; + this.startTime = startTime; + this.endTime = endTime; + this.bytesSent = bytesSent; + this.bytesReceived = bytesReceived; + this.responseCode = responseCode; + this.errorType = errorType; + this.errorMessage = errorMessage; + this.error = null; + this.traceId = traceId; + this.w3cTraceparent = w3cTraceparent; + this.networkCaptureData = networkCaptureData; + } + + @NonNull + public String getUrl() { + return url; + } + + @NonNull + public String getHttpMethod() { + return httpMethod != null ? httpMethod.name().toUpperCase() : null; + } + + @NonNull + public Long getStartTime() { + return startTime; + } + + @NonNull + public Long getEndTime() { + return endTime; + } + + @NonNull + public Long getBytesIn() { + return bytesReceived == null ? 0 : bytesReceived; + } + + @NonNull + public Long getBytesOut() { + return bytesSent == null ? 0 : bytesSent; + } + + @Nullable + public Integer getResponseCode() { + return responseCode; + } + + @Nullable + public Throwable getError() { + return error; + } + + @Nullable + public String getTraceId() { + return traceId; + } + + @Nullable + public String getW3cTraceparent() { + return w3cTraceparent; + } + + @Nullable + public Long getBytesReceived() { + return bytesReceived; + } + + @Nullable + public Long getBytesSent() { + return bytesSent; + } + + @Nullable + public NetworkCaptureData getNetworkCaptureData() { + return networkCaptureData; + } + + @Nullable + public String getErrorType() { + return errorType; + } + + @Nullable + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/ConnectionState.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/ConnectionState.java new file mode 100644 index 0000000000..f8fa801e24 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/ConnectionState.java @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.network.http; + +/** + * Exposes the connectivity state of the implementer + */ +interface ConnectionState { + + /** + * @return true if this object is in a connected state + */ + boolean isConnected(); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingInputStreamWithCallback.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingInputStreamWithCallback.java new file mode 100644 index 0000000000..99e371dafe --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingInputStreamWithCallback.java @@ -0,0 +1,129 @@ +package io.embrace.android.embracesdk.network.http; + +import androidx.annotation.NonNull; + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicLong; + +import io.embrace.android.embracesdk.utils.Consumer; + +/** + * Counts the bytes read from an input stream and invokes a callback once the stream has reached + * the end. + */ +final class CountingInputStreamWithCallback extends FilterInputStream { + /** + * The mark. + */ + private volatile long streamMark = -1; + /** + * The callback to be invoked with num of bytes after reaching the end of the stream. + */ + private final Consumer callback; + + /** + * true if the callback has been invoked, false otherwise. + */ + private volatile boolean callbackCompleted; + + /** + * The count of the number of bytes which have been read. + */ + private final AtomicLong count = new AtomicLong(0); + + private final boolean shouldCaptureBody; + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + + /** + * Wraps another input stream, counting the number of bytes read. + * + * @param in the input stream to be wrapped + */ + CountingInputStreamWithCallback(InputStream in, + boolean shouldCaptureBody, + @NonNull Consumer callback) { + super(in); + this.callback = callback; + this.shouldCaptureBody = shouldCaptureBody; + } + + /** + * Returns the number of bytes read. + */ + public long getCount() { + return count.longValue(); + } + + @Override + public int read() throws IOException { + int result = in.read(); + if (result != -1) { + count.incrementAndGet(); + byte[] resultByte = {Integer.valueOf(result).byteValue()}; + conditionallyCaptureBody(resultByte, 0, 1); + } else if (!callbackCompleted) { + notifyCallback(); + } + return result; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int result = in.read(b, off, len); + if (result != -1) { + count.addAndGet(result); + conditionallyCaptureBody(b, off, result); + } else if (!callbackCompleted) { + notifyCallback(); + } + return result; + } + + @Override + public long skip(long n) throws IOException { + long result = in.skip(n); + count.addAndGet(result); + return result; + } + + @Override + public synchronized void mark(int readlimit) { + in.mark(readlimit); + streamMark = count.longValue(); + // it's okay to mark even if mark isn't supported, as reset won't work + } + + @Override + public synchronized void reset() throws IOException { + if (!in.markSupported()) { + throw new IOException("Mark not supported"); + } + if (streamMark == -1) { + throw new IOException("Mark not set"); + } + + in.reset(); + count.set(streamMark); + callbackCompleted = false; + } + + private void conditionallyCaptureBody(byte[] b, int off, int len) { + if (!shouldCaptureBody) { + return; + } + + if (b != null) { + os.write(b, off, len); + } + } + + private void notifyCallback() { + callbackCompleted = true; + callback.accept(count.longValue(), os.toByteArray()); + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingOutputStream.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingOutputStream.java new file mode 100644 index 0000000000..dc757aff4d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/CountingOutputStream.java @@ -0,0 +1,64 @@ +package io.embrace.android.embracesdk.network.http; + +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import io.embrace.android.embracesdk.InternalApi; + +/** + * Counts the number of bytes written to the output stream, also captures the request body. + */ +class CountingOutputStream extends FilterOutputStream { + private long count; + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + /** + * Wraps another output stream, counting the number of bytes written. + * + * @param out the output stream to be wrapped + */ + public CountingOutputStream(OutputStream out) { + super(out); + } + + /** + * Returns the number of bytes written. + */ + public long getCount() { + return count; + } + + /** + * Returns the request body written. + */ + byte[] getRequestBody() { + return os.toByteArray(); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + count += len; + if (b != null) { + os.write(b, off, len); + } + } + + @Override + public void write(int b) throws IOException { + out.write(b); + count++; + os.write(b); + } + + // Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior: + // it silently ignores any exception thrown by flush(). Instead, just close the delegate stream. + // It should flush itself if necessary. + @Override + public void close() throws IOException { + out.close(); + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride.java new file mode 100644 index 0000000000..d0757c7fbc --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpPathOverride.java @@ -0,0 +1,83 @@ +package io.embrace.android.embracesdk.network.http; + +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.regex.Pattern; + +import io.embrace.android.embracesdk.HttpPathOverrideRequest; +import io.embrace.android.embracesdk.InternalApi; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; + +@InternalApi +public class EmbraceHttpPathOverride { + /** + * Header used to override the URL's relative path + */ + protected static final String PATH_OVERRIDE = "x-emb-path"; + + /** + * Max length of relative path override + */ + private static final Integer RELATIVE_PATH_MAX_LENGTH = 1024; + + /** + * Allowed characters in relative path + * As per https://tools.ietf.org/html/rfc3986#section-2 and https://stackoverflow.com/a/1547940. + * Removed certain characters we do not want present: ?# + */ + private static final Pattern RELATIVE_PATH_PATTERN = Pattern.compile("[A-Za-z0-9-._~:/\\[\\]@!$&'()*+,;=]+"); + + @SuppressWarnings("AbbreviationAsWordInNameCheck") + public static String getURLString(HttpPathOverrideRequest request) { + return getURLString(request, request.getHeaderByName(PATH_OVERRIDE)); + } + + @SuppressWarnings("AbbreviationAsWordInNameCheck") + @InternalApi + public static String getURLString(HttpPathOverrideRequest request, String pathOverride) { + String url; + try { + if (pathOverride != null && validatePathOverride(pathOverride)) { + url = request.getOverriddenURL(pathOverride); + } else { + url = request.getURLString(); + } + } catch (Exception e) { + url = request.getURLString(); + } + return url; + } + + private static Boolean validatePathOverride(String path) { + if (path == null) { + InternalStaticEmbraceLogger.logError("URL relative path cannot be null"); + return false; + } + if (path.length() == 0) { + InternalStaticEmbraceLogger.logError("Relative path must have non-zero length"); + return false; + } + if (path.length() > RELATIVE_PATH_MAX_LENGTH) { + InternalStaticEmbraceLogger.logError(String.format(Locale.US, + "Relative path %s is greater than the maximum allowed length of %d. It will be ignored", + path, RELATIVE_PATH_MAX_LENGTH)); + return false; + } + if (!Charset.forName("US-ASCII").newEncoder().canEncode(path)) { + InternalStaticEmbraceLogger.logError("Relative path must not contain unicode " + + "characters. Relative path " + path + " will be ignored."); + return false; + } + if (!path.startsWith("/")) { + InternalStaticEmbraceLogger.logError("Relative path must start with a /"); + return false; + } + if (!RELATIVE_PATH_PATTERN.matcher(path).matches()) { + InternalStaticEmbraceLogger.logError("Relative path contains invalid chars. " + + "Relative path " + path + " will be ignored."); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnection.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnection.java new file mode 100644 index 0000000000..6f2c7eac13 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnection.java @@ -0,0 +1,297 @@ +package io.embrace.android.embracesdk.network.http; + +import android.annotation.TargetApi; +import android.os.Build; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; +import java.security.Permission; +import java.util.List; +import java.util.Map; + +class EmbraceHttpUrlConnection extends HttpURLConnection { + + private final EmbraceUrlConnectionService embraceConnectionService; + + /** + * Wraps an existing {@link HttpURLConnection} with the Embrace network logic. + * + * @param connection the connection to wrap + * @param enableWrapIoStreams true if we should transparently ungzip the response, else false + */ + public EmbraceHttpUrlConnection(T connection, boolean enableWrapIoStreams) { + super(connection.getURL()); + embraceConnectionService = new EmbraceUrlConnectionOverride<>(connection, enableWrapIoStreams); + } + + @Override + public void addRequestProperty(String key, String value) { + embraceConnectionService.addRequestProperty(key, value); + } + + @Override + public void connect() throws IOException { + embraceConnectionService.connect(); + } + + @Override + public void disconnect() { + embraceConnectionService.disconnect(); + } + + @Override + public boolean getAllowUserInteraction() { + return embraceConnectionService.getAllowUserInteraction(); + } + + @Override + public void setAllowUserInteraction(boolean allowUserInteraction) { + embraceConnectionService.setAllowUserInteraction(allowUserInteraction); + } + + @Override + public int getConnectTimeout() { + return embraceConnectionService.getConnectTimeout(); + } + + @Override + public void setConnectTimeout(int timeout) { + embraceConnectionService.setConnectTimeout(timeout); + } + + @Override + public Object getContent() throws IOException { + return embraceConnectionService.getContent(); + } + + @Override + public Object getContent(Class[] classes) throws IOException { + return embraceConnectionService.getContent(classes); + } + + @Override + public String getContentEncoding() { + return embraceConnectionService.getContentEncoding(); + } + + @Override + public int getContentLength() { + return embraceConnectionService.getContentLength(); + } + + @Override + @TargetApi(24) + public long getContentLengthLong() { + return embraceConnectionService.getContentLengthLong(); + } + + @Override + public String getContentType() { + return embraceConnectionService.getContentType(); + } + + @Override + public long getDate() { + return embraceConnectionService.getDate(); + } + + @Override + public boolean getDefaultUseCaches() { + return embraceConnectionService.getDefaultUseCaches(); + } + + @Override + public void setDefaultUseCaches(boolean defaultUseCaches) { + embraceConnectionService.setDefaultUseCaches(defaultUseCaches); + } + + @Override + public boolean getDoInput() { + return embraceConnectionService.getDoInput(); + } + + @Override + public void setDoInput(boolean doInput) { + embraceConnectionService.setDoInput(doInput); + } + + @Override + public boolean getDoOutput() { + return embraceConnectionService.getDoOutput(); + } + + @Override + public void setDoOutput(boolean doOutput) { + embraceConnectionService.setDoOutput(doOutput); + } + + @Override + public InputStream getErrorStream() { + return embraceConnectionService.getErrorStream(); + } + + @Override + public String getHeaderField(int n) { + return embraceConnectionService.getHeaderField(n); + } + + @Override + public String getHeaderField(String name) { + return embraceConnectionService.getHeaderField(name); + } + + @Override + public long getHeaderFieldDate(String name, long defaultValue) { + return embraceConnectionService.getHeaderFieldDate(name, defaultValue); + } + + @Override + public int getHeaderFieldInt(String name, int defaultValue) { + return embraceConnectionService.getHeaderFieldInt(name, defaultValue); + } + + @Override + public String getHeaderFieldKey(int n) { + return embraceConnectionService.getHeaderFieldKey(n); + } + + @Override + @TargetApi(24) + public long getHeaderFieldLong(String name, long defaultValue) { + return embraceConnectionService.getHeaderFieldLong(name, defaultValue); + } + + @Override + public Map> getHeaderFields() { + return embraceConnectionService.getHeaderFields(); + } + + @Override + public long getIfModifiedSince() { + return embraceConnectionService.getIfModifiedSince(); + } + + @Override + public void setIfModifiedSince(long ifModifiedSince) { + embraceConnectionService.setIfModifiedSince(ifModifiedSince); + } + + @Override + public InputStream getInputStream() throws IOException { + return embraceConnectionService.getInputStream(); + } + + @Override + public boolean getInstanceFollowRedirects() { + return embraceConnectionService.getInstanceFollowRedirects(); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + embraceConnectionService.setInstanceFollowRedirects(followRedirects); + } + + @Override + public long getLastModified() { + return embraceConnectionService.getLastModified(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return embraceConnectionService.getOutputStream(); + } + + @Override + public Permission getPermission() throws IOException { + return embraceConnectionService.getPermission(); + } + + @Override + public int getReadTimeout() { + return embraceConnectionService.getReadTimeout(); + } + + @Override + public void setReadTimeout(int timeout) { + embraceConnectionService.setReadTimeout(timeout); + } + + @Override + public String getRequestMethod() { + return embraceConnectionService.getRequestMethod(); + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + embraceConnectionService.setRequestMethod(method); + } + + @Override + public Map> getRequestProperties() { + return embraceConnectionService.getRequestProperties(); + } + + @Override + public String getRequestProperty(String key) { + return embraceConnectionService.getRequestProperty(key); + } + + @Override + public int getResponseCode() throws IOException { + return embraceConnectionService.getResponseCode(); + } + + @Override + public String getResponseMessage() throws IOException { + return embraceConnectionService.getResponseMessage(); + } + + @Override + public URL getURL() { + return embraceConnectionService.getUrl(); + } + + @Override + public boolean getUseCaches() { + return embraceConnectionService.getUseCaches(); + } + + @Override + public void setUseCaches(boolean useCaches) { + embraceConnectionService.setUseCaches(useCaches); + } + + @Override + public void setChunkedStreamingMode(int chunkLen) { + embraceConnectionService.setChunkedStreamingMode(chunkLen); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + embraceConnectionService.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setFixedLengthStreamingMode(long contentLength) { + embraceConnectionService.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setRequestProperty(String key, String value) { + embraceConnectionService.setRequestProperty(key, value); + } + + @Override + public String toString() { + return embraceConnectionService.toString(); + } + + @Override + public boolean usingProxy() { + return embraceConnectionService.usingProxy(); + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride.java new file mode 100644 index 0000000000..ac49cd172a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlConnectionOverride.java @@ -0,0 +1,41 @@ +package io.embrace.android.embracesdk.network.http; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Locale; + +import io.embrace.android.embracesdk.HttpPathOverrideRequest; +import io.embrace.android.embracesdk.InternalApi; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; + +class EmbraceHttpUrlConnectionOverride implements HttpPathOverrideRequest { + + private final HttpURLConnection connection; + + EmbraceHttpUrlConnectionOverride(HttpURLConnection connection) { + this.connection = connection; + } + + @Override + public String getHeaderByName(String name) { + return connection.getRequestProperty(name); + } + + @Override + public String getOverriddenURL(String pathOverride) { + try { + return new URL(connection.getURL().getProtocol(), connection.getURL().getHost(), + connection.getURL().getPort(), pathOverride).toString(); + } catch (MalformedURLException e) { + InternalStaticEmbraceLogger.logError("Failed to override path of " + + connection.getURL() + " with " + pathOverride); + return connection.getURL().toString(); + } + } + + @Override + public String getURLString() { + return connection.getURL().toString(); + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler.java new file mode 100644 index 0000000000..deac192130 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpUrlStreamHandler.java @@ -0,0 +1,69 @@ +package io.embrace.android.embracesdk.network.http; + +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import io.embrace.android.embracesdk.Embrace; + +/** + * HTTP-specific implementation of EmbraceURLStreamHandler. + */ +final class EmbraceHttpUrlStreamHandler extends EmbraceUrlStreamHandler { + static final int PORT = 80; + + /** + * Given the base URLStreamHandler that will be wrapped, constructs the instance. + */ + public EmbraceHttpUrlStreamHandler(URLStreamHandler handler) { + super(handler); + } + + EmbraceHttpUrlStreamHandler(URLStreamHandler handler, Embrace embrace) { + super(handler, embrace); + } + + @Override + public int getDefaultPort() { + return PORT; + } + + @Override + protected Method getMethodOpenConnection(Class url) throws NoSuchMethodException { + Method method = this.handler.getClass().getDeclaredMethod(METHOD_NAME_OPEN_CONNECTION, url); + + method.setAccessible(true); + return method; + } + + @Override + protected Method getMethodOpenConnection(Class url, Class proxy) throws NoSuchMethodException { + Method method = this.handler.getClass().getDeclaredMethod(METHOD_NAME_OPEN_CONNECTION, url, proxy); + + method.setAccessible(true); + return method; + } + + @Override + protected URLConnection newEmbraceUrlConnection(URLConnection connection) { + if (!(connection instanceof HttpURLConnection)) { + return connection; + } + + injectTraceparent(connection); + + if (enableRequestSizeCapture && !connection.getRequestProperties().containsKey("Accept-Encoding")) { + // This disables automatic gzip decompression by HttpUrlConnection so that we can + // accurately count the number of bytes. We handle the decompression ourselves. + connection.setRequestProperty("Accept-Encoding", "gzip"); + return new EmbraceHttpUrlConnection<>((HttpURLConnection) connection, true); + } else { + // Do not transparently decompress if the user has specified an encoding themselves. + // Even if they pass in 'gzip', we should return them the compressed response. + return new EmbraceHttpUrlConnection<>((HttpURLConnection) connection, false); + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection.java new file mode 100644 index 0000000000..f4bedc296d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlConnection.java @@ -0,0 +1,355 @@ +package io.embrace.android.embracesdk.network.http; + +import android.annotation.TargetApi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ProtocolException; +import java.net.URL; +import java.security.Permission; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocketFactory; + +/** + * Wrapper implementation of HttpsURLConnection that logs network calls to Embrace. + *

+ * Since Java does not support multiple inheritance, much of the logic in this class is duplicated from + * {@link EmbraceHttpUrlConnection} in order to ensure that this class inherits directly from + * {@link HttpsURLConnection}. + */ +class EmbraceHttpsUrlConnection extends HttpsURLConnection { + + private final EmbraceSslUrlConnectionService embraceSslUrlConnectionService; + + + /** + * Wraps an existing {@link HttpsURLConnection} with the Embrace network logic. + * + * @param connection the connection to wrap + * @param enableWrapIoStreams true if we should transparently ungzip the response, else false + */ + public EmbraceHttpsUrlConnection(T connection, boolean enableWrapIoStreams) { + super(connection.getURL()); + embraceSslUrlConnectionService = new EmbraceUrlConnectionOverride<>(connection, enableWrapIoStreams); + } + + @Override + public void addRequestProperty(String key, String value) { + this.embraceSslUrlConnectionService.addRequestProperty(key, value); + } + + @Override + public void connect() throws IOException { + this.embraceSslUrlConnectionService.connect(); + } + + @Override + public void disconnect() { + this.embraceSslUrlConnectionService.disconnect(); + } + + @Override + public boolean getAllowUserInteraction() { + return this.embraceSslUrlConnectionService.getAllowUserInteraction(); + } + + @Override + public void setAllowUserInteraction(boolean allowUserInteraction) { + this.embraceSslUrlConnectionService.setAllowUserInteraction(allowUserInteraction); + } + + @Override + public int getConnectTimeout() { + return this.embraceSslUrlConnectionService.getConnectTimeout(); + } + + @Override + public void setConnectTimeout(int timeout) { + this.embraceSslUrlConnectionService.setConnectTimeout(timeout); + } + + @Override + public Object getContent() throws IOException { + return this.embraceSslUrlConnectionService.getContent(); + } + + @Override + public Object getContent(Class[] classes) throws IOException { + return this.embraceSslUrlConnectionService.getContent(classes); + } + + @Override + public String getContentEncoding() { + return this.embraceSslUrlConnectionService.getContentEncoding(); + } + + @Override + public int getContentLength() { + return this.embraceSslUrlConnectionService.getContentLength(); + } + + @Override + @TargetApi(24) + public long getContentLengthLong() { + return this.embraceSslUrlConnectionService.getContentLengthLong(); + } + + @Override + public String getContentType() { + return this.embraceSslUrlConnectionService.getContentType(); + } + + @Override + public long getDate() { + return this.embraceSslUrlConnectionService.getDate(); + } + + @Override + public boolean getDefaultUseCaches() { + return this.embraceSslUrlConnectionService.getDefaultUseCaches(); + } + + @Override + public void setDefaultUseCaches(boolean defaultUseCaches) { + this.embraceSslUrlConnectionService.setDefaultUseCaches(defaultUseCaches); + } + + @Override + public boolean getDoInput() { + return this.embraceSslUrlConnectionService.getDoInput(); + } + + @Override + public void setDoInput(boolean doInput) { + this.embraceSslUrlConnectionService.setDoInput(doInput); + } + + @Override + public boolean getDoOutput() { + return this.embraceSslUrlConnectionService.getDoOutput(); + } + + @Override + public void setDoOutput(boolean doOutput) { + this.embraceSslUrlConnectionService.setDoOutput(doOutput); + } + + @Override + public InputStream getErrorStream() { + return this.embraceSslUrlConnectionService.getErrorStream(); + } + + @Override + public String getHeaderField(int n) { + return this.embraceSslUrlConnectionService.getHeaderField(n); + } + + @Override + public String getHeaderField(String name) { + return this.embraceSslUrlConnectionService.getHeaderField(name); + } + + @Override + public long getHeaderFieldDate(String name, long defaultValue) { + return this.embraceSslUrlConnectionService.getHeaderFieldDate(name, defaultValue); + } + + @Override + public int getHeaderFieldInt(String name, int defaultValue) { + return this.embraceSslUrlConnectionService.getHeaderFieldInt(name, defaultValue); + } + + @Override + public String getHeaderFieldKey(int n) { + return this.embraceSslUrlConnectionService.getHeaderFieldKey(n); + } + + @Override + @TargetApi(24) + public long getHeaderFieldLong(String name, long defaultValue) { + return this.embraceSslUrlConnectionService.getHeaderFieldLong(name, defaultValue); + } + + @Override + public Map> getHeaderFields() { + return this.embraceSslUrlConnectionService.getHeaderFields(); + } + + @Override + public long getIfModifiedSince() { + return this.embraceSslUrlConnectionService.getIfModifiedSince(); + } + + @Override + public void setIfModifiedSince(long ifModifiedSince) { + this.embraceSslUrlConnectionService.setIfModifiedSince(ifModifiedSince); + } + + @Override + public InputStream getInputStream() throws IOException { + return this.embraceSslUrlConnectionService.getInputStream(); + } + + @Override + public boolean getInstanceFollowRedirects() { + return this.embraceSslUrlConnectionService.getInstanceFollowRedirects(); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + this.embraceSslUrlConnectionService.setInstanceFollowRedirects(followRedirects); + } + + @Override + public long getLastModified() { + return this.embraceSslUrlConnectionService.getLastModified(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return this.embraceSslUrlConnectionService.getOutputStream(); + } + + @Override + public Permission getPermission() throws IOException { + return this.embraceSslUrlConnectionService.getPermission(); + } + + @Override + public int getReadTimeout() { + return this.embraceSslUrlConnectionService.getReadTimeout(); + } + + @Override + public void setReadTimeout(int timeout) { + this.embraceSslUrlConnectionService.setReadTimeout(timeout); + } + + @Override + public String getRequestMethod() { + return this.embraceSslUrlConnectionService.getRequestMethod(); + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + this.embraceSslUrlConnectionService.setRequestMethod(method); + } + + @Override + public Map> getRequestProperties() { + return this.embraceSslUrlConnectionService.getRequestProperties(); + } + + @Override + public String getRequestProperty(String key) { + return this.embraceSslUrlConnectionService.getRequestProperty(key); + } + + @Override + public int getResponseCode() throws IOException { + return this.embraceSslUrlConnectionService.getResponseCode(); + } + + @Override + public String getResponseMessage() throws IOException { + return this.embraceSslUrlConnectionService.getResponseMessage(); + } + + @Override + public URL getURL() { + return this.embraceSslUrlConnectionService.getUrl(); + } + + @Override + public boolean getUseCaches() { + return this.embraceSslUrlConnectionService.getUseCaches(); + } + + @Override + public void setUseCaches(boolean useCaches) { + this.embraceSslUrlConnectionService.setUseCaches(useCaches); + } + + @Override + public void setChunkedStreamingMode(int chunkLen) { + this.embraceSslUrlConnectionService.setChunkedStreamingMode(chunkLen); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + this.embraceSslUrlConnectionService.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setFixedLengthStreamingMode(long contentLength) { + this.embraceSslUrlConnectionService.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setRequestProperty(String key, String value) { + this.embraceSslUrlConnectionService.setRequestProperty(key, value); + } + + @Override + public String toString() { + return this.embraceSslUrlConnectionService.toString(); + } + + @Override + public boolean usingProxy() { + return this.embraceSslUrlConnectionService.usingProxy(); + } + + @Override + public String getCipherSuite() { + return this.embraceSslUrlConnectionService.getCipherSuite(); + } + + @Override + public Certificate[] getLocalCertificates() { + return this.embraceSslUrlConnectionService.getLocalCertificates(); + } + + @Override + public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { + return this.embraceSslUrlConnectionService.getServerCertificates(); + } + + @Override + public SSLSocketFactory getSSLSocketFactory() { + return this.embraceSslUrlConnectionService.getSslSocketFactory(); + } + + @Override + public void setSSLSocketFactory(SSLSocketFactory factory) { + this.embraceSslUrlConnectionService.setSslSocketFactory(factory); + } + + @Override + public HostnameVerifier getHostnameVerifier() { + return this.embraceSslUrlConnectionService.getHostnameVerifier(); + } + + @Override + public void setHostnameVerifier(HostnameVerifier verifier) { + this.embraceSslUrlConnectionService.setHostnameVerifier(verifier); + } + + + public Principal getLocalPrincipal() { + return embraceSslUrlConnectionService.getLocalPrincipal(); + } + + + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + return embraceSslUrlConnectionService.getPeerPrincipal(); + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler.java new file mode 100644 index 0000000000..e29731c8ec --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceHttpsUrlStreamHandler.java @@ -0,0 +1,70 @@ +package io.embrace.android.embracesdk.network.http; + +import java.lang.reflect.Method; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import javax.net.ssl.HttpsURLConnection; + +import io.embrace.android.embracesdk.Embrace; + +/** + * HTTPS-specific implementation of EmbraceUrlStreamHandler. + */ +final class EmbraceHttpsUrlStreamHandler extends EmbraceUrlStreamHandler { + static final int PORT = 443; + + /** + * Given the base URLStreamHandler that will be wrapped, constructs the instance. + */ + public EmbraceHttpsUrlStreamHandler(URLStreamHandler handler) { + super(handler); + } + + EmbraceHttpsUrlStreamHandler(URLStreamHandler handler, Embrace embrace) { + super(handler, embrace); + } + + @Override + public int getDefaultPort() { + return PORT; + } + + @Override + protected Method getMethodOpenConnection(Class url) throws NoSuchMethodException { + Method method = this.handler.getClass().getSuperclass().getDeclaredMethod(METHOD_NAME_OPEN_CONNECTION, url); + + method.setAccessible(true); + return method; + } + + @Override + protected Method getMethodOpenConnection(Class url, Class proxy) throws NoSuchMethodException { + Method method = this.handler.getClass().getSuperclass().getDeclaredMethod(METHOD_NAME_OPEN_CONNECTION, url, proxy); + + method.setAccessible(true); + return method; + } + + @Override + protected URLConnection newEmbraceUrlConnection(URLConnection connection) { + if (!(connection instanceof HttpsURLConnection)) { + return connection; + } + + injectTraceparent(connection); + + if (enableRequestSizeCapture && !connection.getRequestProperties().containsKey("Accept-Encoding")) { + // This disables automatic gzip decompression by HttpUrlConnection so that we can + // accurately count the number of bytes. We handle the decompression ourselves. + connection.setRequestProperty("Accept-Encoding", "gzip"); + return new EmbraceHttpsUrlConnection<>((HttpsURLConnection) connection, true); + } else { + // Do not transparently decompress if the user has specified an encoding themselves. + // Even if they pass in 'gzip', we should return them the compressed response. + return new EmbraceHttpsUrlConnection<>((HttpsURLConnection) connection, false); + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceSslUrlConnectionService.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceSslUrlConnectionService.java new file mode 100644 index 0000000000..cca1f53607 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceSslUrlConnectionService.java @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.network.http; + + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.security.cert.Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocketFactory; + +/** + * Additional methods for Https network calls + */ +interface EmbraceSslUrlConnectionService extends EmbraceUrlConnectionService { + + @Nullable + String getCipherSuite(); + + @Nullable + Certificate[] getLocalCertificates(); + + @Nullable + Certificate[] getServerCertificates() throws SSLPeerUnverifiedException; + + @Nullable + SSLSocketFactory getSslSocketFactory(); + + void setSslSocketFactory(@NonNull SSLSocketFactory factory); + + @Nullable + HostnameVerifier getHostnameVerifier(); + + void setHostnameVerifier(@NonNull HostnameVerifier verifier); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java new file mode 100644 index 0000000000..112e6210f3 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java @@ -0,0 +1,904 @@ +package io.embrace.android.embracesdk.network.http; + +import static io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.TRACEPARENT_HEADER_NAME; + +import android.annotation.TargetApi; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; +import java.security.Permission; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPInputStream; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocketFactory; + +import io.embrace.android.embracesdk.Embrace; +import io.embrace.android.embracesdk.InternalApi; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; +import io.embrace.android.embracesdk.utils.NetworkUtils; +import io.embrace.android.embracesdk.utils.exceptions.function.CheckedSupplier; +import kotlin.jvm.functions.Function0; + + +/** + * Wraps @{link HttpUrlConnection} to log network calls to Embrace. The wrapper also wraps the + * InputStream to get an accurate count of bytes received if a Content-Length is not provided by + * the server. + *

+ * The wrapper handles gzip decompression itself and strips the {@code Content-Length} and + * {@code Content-Encoding} headers. Typically this is handled transparently by + * {@link HttpURLConnection} but would prevent us from accessing the {@code Content-Length}. + *

+ * Network logging currently does not follow redirects. The duration is logged from initiation of + * the network call (upon invocation of any method which would initiate the network call), to the + * retrieval of the response. + *

+ * As network calls are initiated lazily, we log the network call prior to the calling of any + * wrapped method which would result in the network call actually being executed, and store a + * flag to prevent duplication of calls. + */ +@InternalApi +class EmbraceUrlConnectionOverride + implements EmbraceUrlConnectionService, EmbraceSslUrlConnectionService { + + /** + * The content encoding HTTP header. + */ + private static final String CONTENT_ENCODING = "Content-Encoding"; + + /** + * The content length HTTP header. + */ + private static final String CONTENT_LENGTH = "Content-Length"; + + /** + * Reference to the wrapped connection. + */ + private final T connection; + + /** + * The time at which the connection was created. + */ + private final long createdTime; + + /** + * Whether transparent gzip compression is enabled. + */ + private final boolean enableWrapIoStreams; + + /** + * Reference to the SDK that is mockable and fakeable in tests + */ + private final Embrace embrace; + + /** + * A reference to the output stream wrapped in a counter, so we can determine the bytes sent. + */ + private volatile CountingOutputStream outputStream; + + /** + * Whether the network call has already been logged, to prevent duplication. + */ + private volatile boolean didLogNetworkCall = false; + + /** + * The time at which the network call ended. + */ + private volatile Long endTime; + + /** + * The time at which the network call was initiated. + */ + private volatile Long startTime; + + /** + * The trace id specified for the request + */ + private volatile String traceId; + + /** + * The request headers captured from the http connection. + */ + private volatile HashMap requestHeaders; + /** + * Indicates if the request throws a IOException + */ + private volatile Exception inputStreamAccessException; + + private volatile Exception lastConnectionAccessException; + + private final AtomicLong responseSize = new AtomicLong(-1); + + private final AtomicInteger responseCode = new AtomicInteger(0); + + private final AtomicReference>> headerFields = new AtomicReference<>(null); + + private final AtomicReference networkCaptureData = new AtomicReference<>(null); + + @Nullable + private volatile String traceparent = null; + + @Nullable + private volatile byte[] responseBody = null; + + /** + * Wraps an existing {@link HttpURLConnection} with the Embrace network logic. + * + * @param connection the connection to wrap + * @param enableWrapIoStreams true if we should transparently ungzip the response, else false + */ + public EmbraceUrlConnectionOverride(@NonNull T connection, boolean enableWrapIoStreams) { + this(connection, enableWrapIoStreams, Embrace.getInstance()); + } + + EmbraceUrlConnectionOverride(@NonNull T connection, boolean enableWrapIoStreams, + @NonNull Embrace embrace) { + this.connection = connection; + this.createdTime = System.currentTimeMillis(); + this.enableWrapIoStreams = enableWrapIoStreams; + this.embrace = embrace; + } + + @Override + public void addRequestProperty(@NonNull String key, @Nullable String value) { + this.connection.addRequestProperty(key, value); + } + + @Override + public void connect() throws IOException { + identifyTraceId(); + try { + if (NetworkUtils.isNetworkSpanForwardingEnabled(embrace.getConfigService())) { + traceparent = connection.getRequestProperty(TRACEPARENT_HEADER_NAME); + } + } catch (Exception e) { + // Ignore traceparent if there was a problem obtaining it + } + this.connection.connect(); + } + + @Override + public void disconnect() { + // The network call must be logged before we close the transport + internalLogNetworkCall(this.createdTime); + this.connection.disconnect(); + } + + @Override + public boolean getAllowUserInteraction() { + return this.connection.getAllowUserInteraction(); + } + + @Override + public void setAllowUserInteraction(boolean allowUserInteraction) { + this.connection.setAllowUserInteraction(allowUserInteraction); + } + + @Override + public int getConnectTimeout() { + return this.connection.getConnectTimeout(); + } + + @Override + public void setConnectTimeout(int timeout) { + this.connection.setConnectTimeout(timeout); + } + + @Override + @Nullable + public Object getContent() throws IOException { + identifyTraceId(); + return this.connection.getContent(); + } + + @Override + @Nullable + public Object getContent(@NonNull Class[] classes) throws IOException { + identifyTraceId(); + return this.connection.getContent(classes); + } + + @Override + @Nullable + public String getContentEncoding() { + return shouldUncompressGzip() ? null : this.connection.getContentEncoding(); + } + + @Override + public int getContentLength() { + return shouldUncompressGzip() ? -1 : this.connection.getContentLength(); + } + + @Override + @TargetApi(24) + public long getContentLengthLong() { + return (shouldUncompressGzip() || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) ? + -1 : this.connection.getContentLengthLong(); + } + + @Override + @Nullable + public String getContentType() { + return this.connection.getContentType(); + } + + @Override + public long getDate() { + return this.connection.getDate(); + } + + @Override + public boolean getDefaultUseCaches() { + return this.connection.getDefaultUseCaches(); + } + + @Override + public void setDefaultUseCaches(boolean defaultUseCaches) { + this.connection.setDefaultUseCaches(defaultUseCaches); + } + + @Override + public boolean getDoInput() { + return this.connection.getDoInput(); + } + + @Override + public void setDoInput(boolean doInput) { + this.connection.setDoInput(doInput); + } + + @Override + public boolean getDoOutput() { + return this.connection.getDoOutput(); + } + + @Override + public void setDoOutput(boolean doOutput) { + this.connection.setDoOutput(doOutput); + } + + @Override + + @Nullable + public InputStream getErrorStream() { + return getWrappedInputStream(this.connection.getErrorStream()); + } + + @Override + public boolean shouldInterceptHeaderRetrieval(@Nullable String key) { + return shouldUncompressGzip() && key != null && (key.equalsIgnoreCase(CONTENT_ENCODING) || key.equalsIgnoreCase(CONTENT_LENGTH)); + } + + @Override + @Nullable + public String getHeaderField(int n) { + String key = this.connection.getHeaderFieldKey(n); + return retrieveHeaderField(key, + null, + () -> connection.getHeaderField(n) + ); + } + + @Override + @Nullable + public String getHeaderField(@Nullable String name) { + return retrieveHeaderField(name, + null, + () -> connection.getHeaderField(name) + ); + } + + @Override + @Nullable + public String getHeaderFieldKey(int n) { + String key = this.connection.getHeaderFieldKey(n); + return retrieveHeaderField(key, + null, + () -> key + ); + } + + @Override + public long getHeaderFieldDate(@NonNull String name, long defaultValue) { + Long result = retrieveHeaderField(name, + defaultValue, + () -> connection.getHeaderFieldDate(name, defaultValue) + ); + + return result != null ? result : defaultValue; + } + + @Override + public int getHeaderFieldInt(@NonNull String name, int defaultValue) { + Integer result = retrieveHeaderField(name, + defaultValue, + () -> connection.getHeaderFieldInt(name, defaultValue) + ); + + return result != null ? result : defaultValue; + } + + + @Override + @TargetApi(24) + public long getHeaderFieldLong(@NonNull String name, long defaultValue) { + Long result = retrieveHeaderField(name, + defaultValue, + () -> Build.VERSION.SDK_INT < Build.VERSION_CODES.N ? -1 : + this.connection.getHeaderFieldLong(name, defaultValue) + + ); + return result != null ? result : defaultValue; + } + + @Override + @Nullable + public Map> getHeaderFields() { + final long startTime = System.currentTimeMillis(); + cacheResponseData(); + internalLogNetworkCall(startTime); + return headerFields.get(); + } + + + private R retrieveHeaderField(@Nullable String name, + R defaultValue, + Function0 action) { + if (name == null) { + return null; + } + long startTime = System.currentTimeMillis(); + if (shouldInterceptHeaderRetrieval(name)) { + // Strip the content encoding and length headers, as we transparently ungzip the content + return defaultValue; + } + + R result = action.invoke(); + cacheResponseData(); + internalLogNetworkCall(startTime); + return result; + } + + @Override + public long getIfModifiedSince() { + return this.connection.getIfModifiedSince(); + } + + @Override + public void setIfModifiedSince(long ifModifiedSince) { + this.connection.setIfModifiedSince(ifModifiedSince); + } + + @Override + @Nullable + public InputStream getInputStream() throws IOException { + try { + return getWrappedInputStream(this.connection.getInputStream()); + } catch (IOException e) { + inputStreamAccessException = e; + throw e; + } + } + + @Override + public boolean getInstanceFollowRedirects() { + return this.connection.getInstanceFollowRedirects(); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + this.connection.setInstanceFollowRedirects(followRedirects); + } + + @Override + public long getLastModified() { + return this.connection.getLastModified(); + } + + @Override + @Nullable + public OutputStream getOutputStream() throws IOException { + identifyTraceId(); + OutputStream out = connection.getOutputStream(); + if (enableWrapIoStreams && this.outputStream == null && out != null) { + this.outputStream = new CountingOutputStream(out); + return this.outputStream; + } + return out; + } + + @Override + @Nullable + public Permission getPermission() throws IOException { + return this.connection.getPermission(); + } + + @Override + public int getReadTimeout() { + return this.connection.getReadTimeout(); + } + + @Override + public void setReadTimeout(int timeout) { + this.connection.setReadTimeout(timeout); + } + + @Override + @NonNull + public String getRequestMethod() { + return this.connection.getRequestMethod(); + } + + @Override + public void setRequestMethod(@NonNull String method) throws ProtocolException { + this.connection.setRequestMethod(method); + } + + @Override + @Nullable + public Map> getRequestProperties() { + return this.connection.getRequestProperties(); + } + + @Override + @Nullable + public String getRequestProperty(@NonNull String key) { + return this.connection.getRequestProperty(key); + } + + @Override + public int getResponseCode() throws IOException { + identifyTraceId(); + long startTime = System.currentTimeMillis(); + cacheResponseData(); + internalLogNetworkCall(startTime); + return responseCode.get(); + } + + @Override + @Nullable + public String getResponseMessage() throws IOException { + identifyTraceId(); + long startTime = System.currentTimeMillis(); + String responseMsg = this.connection.getResponseMessage(); + cacheResponseData(); + internalLogNetworkCall(startTime); + return responseMsg; + } + + @Override + @Nullable + public URL getUrl() { + return this.connection.getURL(); + } + + @Override + public boolean getUseCaches() { + return this.connection.getUseCaches(); + } + + @Override + public void setUseCaches(boolean useCaches) { + this.connection.setUseCaches(useCaches); + } + + @Override + public void setChunkedStreamingMode(int chunkLen) { + this.connection.setChunkedStreamingMode(chunkLen); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + this.connection.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setFixedLengthStreamingMode(long contentLength) { + this.connection.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setRequestProperty(@NonNull String key, @Nullable String value) { + this.connection.setRequestProperty(key, value); + + if (hasNetworkCaptureRules()) { + this.requestHeaders = getProcessedHeaders(getRequestProperties()); + } + } + + @Override + @NonNull + public String toString() { + return this.connection.toString(); + } + + @Override + public boolean usingProxy() { + return this.connection.usingProxy(); + } + + /** + * Given a start time (in milliseconds), logs the network call to Embrace using the current time as the end time. + *

+ * If the network call has already been logged for this HttpURLConnection, this method is a no-op and is effectively + * ignored. + */ + synchronized void internalLogNetworkCall(long startTime) { + internalLogNetworkCall(startTime, System.currentTimeMillis(), false, null); + } + + /** + * Given a start time and end time (in milliseconds), logs the network call to Embrace. + *

+ * If the network call has already been logged for this HttpURLConnection, this method is a no-op and is effectively + * ignored. + */ + synchronized void internalLogNetworkCall(long startTime, long endTime, boolean overwrite, Long bytesIn) { + if (!this.didLogNetworkCall || overwrite) { + // We are proactive with setting this flag so that we don't get nested calls to log the network call by virtue of + // extracting the data we need to log the network call. + this.didLogNetworkCall = true; + this.startTime = startTime; + this.endTime = endTime; + + String url = EmbraceHttpPathOverride.getURLString(new EmbraceHttpUrlConnectionOverride(this.connection)); + + try { + long bytesOut = this.outputStream == null ? 0 : Math.max(this.outputStream.getCount(), 0); + long contentLength = bytesIn == null ? Math.max(0, responseSize.get()) : bytesIn; + + if (inputStreamAccessException == null && lastConnectionAccessException == null && responseCode.get() != 0) { + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromCompletedRequest( + url, + HttpMethod.fromString(getRequestMethod()), + startTime, + endTime, + bytesOut, + contentLength, + responseCode.get(), + traceId, + traceparent, + networkCaptureData.get() + ) + ); + } else { + String exceptionClass = null; + String exceptionMessage = null; + + // Error that happened when trying to obtain the input stream take precedent over connection access errors after that + if (inputStreamAccessException != null) { + exceptionClass = inputStreamAccessException.getClass().getCanonicalName(); + exceptionMessage = inputStreamAccessException.getMessage(); + } else if (lastConnectionAccessException != null) { + exceptionClass = lastConnectionAccessException.getClass().getCanonicalName(); + exceptionMessage = lastConnectionAccessException.getMessage(); + } + + String errorType = exceptionClass != null ? exceptionClass : "UnknownState"; + String errorMessage = exceptionMessage != null ? exceptionMessage : "HTTP response state unknown"; + + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromIncompleteRequest( + url, + HttpMethod.fromString(getRequestMethod()), + startTime, + endTime, + errorType, + errorMessage, + traceId, + traceparent, + networkCaptureData.get() + ) + ); + } + } catch (Exception e) { + InternalStaticEmbraceLogger.logError("Error logging native network request", e); + } + } + } + + @Nullable + private HashMap getProcessedHeaders(@Nullable Map> properties) { + if (properties == null) { + return null; + } + + HashMap headers = new HashMap<>(); + + for (Map.Entry> h : properties.entrySet()) { + StringBuilder builder = new StringBuilder(); + for (String value : h.getValue()) { + if (value != null) { + builder.append(value); + } + } + headers.put(h.getKey(), builder.toString()); + } + + return headers; + } + + /** + * Wraps an input stream with an input stream which counts the number of bytes read, and then + * updates the network call service with the correct number of bytes read once the stream has + * reached the end. + * + * @param inputStream the input stream to count + * @return the wrapped input stream + */ + private CountingInputStreamWithCallback countingInputStream(InputStream inputStream) { + return new CountingInputStreamWithCallback( + inputStream, + hasNetworkCaptureRules(), + (bytesCount, responseBody) -> { + if (this.startTime != null && this.endTime != null) { + this.responseBody = responseBody; + cacheResponseData(); + internalLogNetworkCall( + this.startTime, + this.endTime, + true, + bytesCount); + } + }); + } + + + /** + * We disable the automatic gzip decompression behavior of {@link HttpURLConnection} in the + * {@link EmbraceHttpUrlStreamHandler} to ensure that we can count the bytes in the response + * from the server. We decompress the response transparently to the user only if both: + *

    + *
  • The user did not specify an encoding
  • + *
  • The server returned a gzipped response
  • + *
+ *

+ * If the user specified an encoding, even if it is gzip, we do not transparently decompress + * the response. This is to mirror the behavior of {@link HttpURLConnection} whilst providing + * us access to the content length. + * + * @return true if we should decompress the response, false otherwise + * @see Android Docs + * @see + * Android Source Code + */ + private boolean shouldUncompressGzip() { + String contentEncoding = this.connection.getContentEncoding(); + return enableWrapIoStreams && + contentEncoding != null && + contentEncoding.equalsIgnoreCase("gzip"); + } + + private void identifyTraceId() { + if (traceId == null) { + try { + traceId = getRequestProperty(embrace.getTraceIdHeader()); + } catch (Exception e) { + InternalStaticEmbraceLogger.logDebug("Failed to retrieve actual trace id header. Current: " + traceId); + } + } + } + + @Override + @Nullable + public String getCipherSuite() { + if (this.connection instanceof HttpsURLConnection) { + return ((HttpsURLConnection) this.connection).getCipherSuite(); + } + + return null; + } + + @Override + @Nullable + public Certificate[] getLocalCertificates() { + if (this.connection instanceof HttpsURLConnection) { + return ((HttpsURLConnection) this.connection).getLocalCertificates(); + } + + return new Certificate[0]; + } + + @Override + @Nullable + public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { + if (this.connection instanceof HttpsURLConnection) { + return ((HttpsURLConnection) this.connection).getServerCertificates(); + } + + return new Certificate[0]; + } + + @Override + @Nullable + public SSLSocketFactory getSslSocketFactory() { + if (this.connection instanceof HttpsURLConnection) { + return ((HttpsURLConnection) this.connection).getSSLSocketFactory(); + } + + return null; + } + + @Override + public void setSslSocketFactory(@NonNull SSLSocketFactory factory) { + if (this.connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) this.connection).setSSLSocketFactory(factory); + } + } + + @Override + @Nullable + public HostnameVerifier getHostnameVerifier() { + if (this.connection instanceof HttpsURLConnection) { + return ((HttpsURLConnection) this.connection).getHostnameVerifier(); + } + + return null; + } + + @Override + public void setHostnameVerifier(@NonNull HostnameVerifier verifier) { + if (this.connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) this.connection).setHostnameVerifier(verifier); + } + } + + @Override + @Nullable + public Principal getLocalPrincipal() { + if (this.connection instanceof HttpsURLConnection) { + return ((HttpsURLConnection) this.connection).getLocalPrincipal(); + } + + return null; + } + + @Override + @Nullable + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + if (this.connection instanceof HttpsURLConnection) { + return ((HttpsURLConnection) this.connection).getPeerPrincipal(); + } + + return null; + } + + @Nullable + private InputStream getWrappedInputStream(InputStream connectionInputStream) { + identifyTraceId(); + long startTime = System.currentTimeMillis(); + + InputStream in = null; + if (shouldUncompressGzip()) { + try { + CheckedSupplier gzipInputStreamSupplier = () -> new GZIPInputStream(connectionInputStream); + in = countingInputStream(new BufferedInputStream(gzipInputStreamSupplier.get())); + } catch (Throwable t) { + // This handles the case where it's availability is 0, causing the GZIPInputStream instantiation to fail. + } + } + + if (in == null) { + in = enableWrapIoStreams ? + countingInputStream(new BufferedInputStream(connectionInputStream)) : connectionInputStream; + } + + cacheResponseData(); + internalLogNetworkCall(startTime); + return in; + } + + private boolean hasNetworkCaptureRules() { + if (this.connection.getURL() == null) { + return false; + } + String url = this.connection.getURL().toString(); + String method = this.connection.getRequestMethod(); + + return embrace.shouldCaptureNetworkBody(url, method); + } + + /** + * Cache values from response at the first point of availability so that we won't try to retrieve these values when the response + * is not available. + */ + private void cacheResponseData() { + if (headerFields.get() == null) { + synchronized (headerFields) { + if (headerFields.get() == null) { + try { + final Map> responseHeaders; + if (!enableWrapIoStreams) { + responseHeaders = connection.getHeaderFields(); + } else { + responseHeaders = new HashMap<>(connection.getHeaderFields()); + responseHeaders.remove(CONTENT_ENCODING); + responseHeaders.remove(CONTENT_LENGTH); + } + headerFields.set(responseHeaders); + } catch (Exception e) { + lastConnectionAccessException = e; + } + } + } + } + + if (responseCode.get() == 0) { + synchronized (responseCode) { + if (responseCode.get() == 0) { + try { + responseCode.set(connection.getResponseCode()); + } catch (Exception e) { + lastConnectionAccessException = e; + } + } + } + } + + if (responseSize.get() == -1) { + synchronized (responseSize) { + // Only try to retrieve the response size if the connection is connected. + // Doing so when the connection is finished and has disconnected will result in the re-execution of the request + if (responseSize.get() == -1) { + try { + responseSize.set(connection.getContentLength()); + } catch (Exception e) { + lastConnectionAccessException = e; + } + } + } + } + + if (shouldCaptureNetworkData() && networkCaptureData.get() == null) { + // If we don't have network capture rules, it's unnecessary to save these values + synchronized (networkCaptureData) { + if (shouldCaptureNetworkData() && networkCaptureData.get() == null) { + try { + Map requestHeaders = this.requestHeaders; + String requestQueryParams = connection.getURL().getQuery(); + byte[] requestBody = this.outputStream != null ? this.outputStream.getRequestBody() : null; + Map responseHeaders = getProcessedHeaders(headerFields.get()); + + networkCaptureData.set( + new NetworkCaptureData( + requestHeaders, + requestQueryParams, + requestBody, + responseHeaders, + responseBody, + null + ) + ); + } catch (Exception e) { + lastConnectionAccessException = e; + } + } + } + } + } + + private boolean shouldCaptureNetworkData() { + return hasNetworkCaptureRules() && (enableWrapIoStreams || inputStreamAccessException != null); + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionService.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionService.java new file mode 100644 index 0000000000..94405035cd --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionService.java @@ -0,0 +1,155 @@ +package io.embrace.android.embracesdk.network.http; + +import android.annotation.TargetApi; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ProtocolException; +import java.net.URL; +import java.security.Permission; +import java.security.Principal; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLPeerUnverifiedException; + +interface EmbraceUrlConnectionService { + + void addRequestProperty(@NonNull String key, @Nullable String value); + + void connect() throws IOException; + + void disconnect(); + + boolean getAllowUserInteraction(); + + void setAllowUserInteraction(boolean allowUserInteraction); + + int getConnectTimeout(); + + void setConnectTimeout(int timeout); + + @Nullable + Object getContent() throws IOException; + + @Nullable + Object getContent(Class[] classes) throws IOException; + + @Nullable + String getContentEncoding(); + + int getContentLength(); + + @TargetApi(24) + long getContentLengthLong(); + + @Nullable + String getContentType(); + + long getDate(); + + boolean getDefaultUseCaches(); + + void setDefaultUseCaches(boolean defaultUseCaches); + + boolean getDoInput(); + + void setDoInput(boolean doInput); + + boolean getDoOutput(); + + void setDoOutput(boolean doOutput); + + @Nullable + InputStream getErrorStream(); + + boolean shouldInterceptHeaderRetrieval(@Nullable String key); + + @Nullable + String getHeaderField(int n); + + @Nullable + String getHeaderField(@Nullable String name); + + long getHeaderFieldDate(@NonNull String name, long defaultValue); + + int getHeaderFieldInt(@NonNull String name, int defaultValue); + + @Nullable + String getHeaderFieldKey(int n); + + @TargetApi(24) + long getHeaderFieldLong(@NonNull String name, long defaultValue); + + @Nullable + Map> getHeaderFields(); + + long getIfModifiedSince(); + + void setIfModifiedSince(long ifModifiedSince); + + @Nullable + InputStream getInputStream() throws IOException; + + boolean getInstanceFollowRedirects(); + + void setInstanceFollowRedirects(boolean followRedirects); + + long getLastModified(); + + @Nullable + OutputStream getOutputStream() throws IOException; + + @Nullable + Permission getPermission() throws IOException; + + int getReadTimeout(); + + void setReadTimeout(int timeout); + + @NonNull + String getRequestMethod(); + + void setRequestMethod(@NonNull String method) throws ProtocolException; + + @Nullable + Map> getRequestProperties(); + + @Nullable + String getRequestProperty(@NonNull String key); + + int getResponseCode() throws IOException; + + @Nullable + String getResponseMessage() throws IOException; + + @Nullable + URL getUrl(); + + boolean getUseCaches(); + + void setUseCaches(boolean useCaches); + + void setChunkedStreamingMode(int chunkLen); + + void setFixedLengthStreamingMode(int contentLength); + + void setFixedLengthStreamingMode(long contentLength); + + void setRequestProperty(@NonNull String key, @Nullable String value); + + @NonNull + String toString(); + + boolean usingProxy(); + + @Nullable + Principal getLocalPrincipal(); + + @Nullable + Principal getPeerPrincipal() throws SSLPeerUnverifiedException; +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java new file mode 100644 index 0000000000..d171f526b4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java @@ -0,0 +1,122 @@ +package io.embrace.android.embracesdk.network.http; + +import static io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.TRACEPARENT_HEADER_NAME; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import io.embrace.android.embracesdk.Embrace; +import io.embrace.android.embracesdk.utils.NetworkUtils; + +/** + * Custom implementation of URLStreamHandler that wraps a base URLStreamHandler and provides a context for executing + * Embrace-specific logic. + */ +abstract class EmbraceUrlStreamHandler extends URLStreamHandler { + protected static final String METHOD_NAME_OPEN_CONNECTION = "openConnection"; + + protected static final String MSG_ERROR_OPEN_CONNECTION = + "An exception was thrown while attempting to open a connection"; + + protected final Embrace embrace; + + protected final URLStreamHandler handler; + + /** + * Method that corresponds to URLStreamHandler.openConnection(URL). + */ + private Method methodOpenConnection1; + + /** + * Method that corresponds to URLStreamHandler.openConnection(URL, Proxy). + */ + private Method methodOpenConnection2; + + /** + * This enables or disables automatic gzip decompression + */ + protected static Boolean enableRequestSizeCapture = false; + + /** + * Given the base URLStreamHandler that will be wrapped, constructs the instance. + */ + public EmbraceUrlStreamHandler(@NonNull URLStreamHandler handler) { + this(handler, Embrace.getInstance()); + } + + EmbraceUrlStreamHandler(@NonNull URLStreamHandler handler, @NonNull Embrace embrace) { + this.handler = handler; + this.embrace = embrace; + try { + this.methodOpenConnection1 = getMethodOpenConnection(URL.class); + this.methodOpenConnection2 = getMethodOpenConnection(URL.class, Proxy.class); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to initialize EmbraceUrlStreamHandler instance.", e); + } + } + + @Override + public abstract int getDefaultPort(); + + /** + * Sets the Request Size Capture flag value + */ + public static void setEnableRequestSizeCapture(Boolean value) { + enableRequestSizeCapture = value; + } + + /** + * Given the URL class instance, returns the Java method that corresponds to the URLStreamHandler.openConnection(URL) + * method. + */ + protected abstract Method getMethodOpenConnection(Class url) throws NoSuchMethodException; + + /** + * Given the URL class and Proxy class instances, returns the Java method that corresponds to the + * URLStreamHandler.openConnection(URL, Proxy) method. + */ + protected abstract Method getMethodOpenConnection(Class url, Class proxy) throws NoSuchMethodException; + + /** + * Given an instance of URLConnection, returns a new URLConnection that wraps the provided instance with additional + * Embrace-specific logic. + */ + protected abstract URLConnection newEmbraceUrlConnection(URLConnection connection); + + @Override + protected URLConnection openConnection(URL url) throws IOException { + try { + return newEmbraceUrlConnection((URLConnection) this.methodOpenConnection1.invoke(this.handler, url)); + } catch (Exception e) { + // We catch Exception here instead of the specific exceptions that can be thrown due to a change in the way some + // of these exceptions are compiled on different OS versions. + + throw new IOException(MSG_ERROR_OPEN_CONNECTION, e); + } + } + + @Override + protected URLConnection openConnection(URL url, Proxy proxy) throws IOException { + try { + return newEmbraceUrlConnection((URLConnection) this.methodOpenConnection2.invoke(this.handler, url, proxy)); + } catch (Exception e) { + // We catch Exception here instead of the specific exceptions that can be thrown due to a change in the way some + // of these exceptions are compiled on different OS versions. + + throw new IOException(MSG_ERROR_OPEN_CONNECTION, e); + } + } + + protected void injectTraceparent(@NonNull URLConnection connection) { + boolean networkSpanForwardingEnabled = NetworkUtils.isNetworkSpanForwardingEnabled(embrace.getConfigService()); + if (networkSpanForwardingEnabled && !connection.getRequestProperties().containsKey(TRACEPARENT_HEADER_NAME)) { + connection.addRequestProperty(TRACEPARENT_HEADER_NAME, embrace.generateW3cTraceparent()); + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory.java new file mode 100644 index 0000000000..9c3ea78cd7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerFactory.java @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.network.http; + +import android.os.Build; + +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.HashMap; +import java.util.Map; + +import io.embrace.android.embracesdk.InternalApi; +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; +import io.embrace.android.embracesdk.network.http.EmbraceHttpUrlStreamHandler; +import io.embrace.android.embracesdk.network.http.EmbraceHttpsUrlStreamHandler; + +/** + * Custom implementation of URLStreamHandlerFactory that is able to return URLStreamHandlers that log network data to + * Embrace. + */ +final class EmbraceUrlStreamHandlerFactory implements URLStreamHandlerFactory { + + private static final String PROTOCOL_HTTP = "http"; + private static final String PROTOCOL_HTTPS = "https"; + + private static final String CLASS_HTTP_LIBCORE_STREAM_HANDLER = "libcore.net.http.HttpHandler"; + private static final String CLASS_HTTP_OKHTTP_STREAM_HANDLER = "com.android.okhttp.HttpHandler"; + + private static final String CLASS_HTTPS_LIBCORE_STREAM_HANDLER = "libcore.net.http.HttpsHandler"; + private static final String CLASS_HTTPS_OKHTTP_STREAM_HANDLER = "com.android.okhttp.HttpsHandler"; + + private static final Map handlers = new HashMap<>(); + + static { + try { + // Pre-allocate and cache these stream handlers up front so no pre-fetch checks are required later. + handlers.put(PROTOCOL_HTTP, new EmbraceHttpUrlStreamHandler(newUrlStreamHandler(CLASS_HTTP_OKHTTP_STREAM_HANDLER))); + handlers.put(PROTOCOL_HTTPS, new EmbraceHttpsUrlStreamHandler(newUrlStreamHandler(CLASS_HTTPS_OKHTTP_STREAM_HANDLER))); + } catch (Exception ex) { + InternalStaticEmbraceLogger.logError("Failed initialize EmbraceUrlStreamHandlerFactory", ex); + } + } + + static URLStreamHandler newUrlStreamHandler(String className) { + try { + return (URLStreamHandler) Class.forName(className).newInstance(); + } catch (Exception e) { + // We catch Exception here instead of the specific exceptions that can be thrown due to a change in the way some + // of these exceptions are compiled on different OS versions. + + // TODO: Uncomment this after supporting dependency injection for EmbLogger. + // EmbLogger.logError("Failed to instantiate new URLStreamHandler instance: " + className, e); + return null; + } + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + return protocol != null ? handlers.get(protocol) : null; + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpMethod.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpMethod.java new file mode 100644 index 0000000000..85e983c9d0 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpMethod.java @@ -0,0 +1,80 @@ +package io.embrace.android.embracesdk.network.http; + +import java.util.Locale; + +import io.embrace.android.embracesdk.InternalApi; + +/** + * Enumeration of supported HTTP request methods. + *

+ * This class is part of the Embrace Public API. + */ +@InternalApi +public enum HttpMethod { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH; + + /** + * Given the string representation of the HTTP request method, returns the corresponding HttpMethod enum. + */ + public static HttpMethod fromString(String method) { + if (method == null) { + return null; + } + + // We expect that the HTTP method will be specified in English so we forcibly use the US locale. + switch (method.toUpperCase(Locale.US)) { + case "GET": + return HttpMethod.GET; + case "HEAD": + return HttpMethod.HEAD; + case "POST": + return HttpMethod.POST; + case "PUT": + return HttpMethod.PUT; + case "DELETE": + return HttpMethod.DELETE; + case "CONNECT": + return HttpMethod.CONNECT; + case "OPTIONS": + return HttpMethod.OPTIONS; + case "TRACE": + return HttpMethod.TRACE; + case "PATCH": + return HttpMethod.PATCH; + default: + return null; + } + } + + /** + * Given the int representation of the HTTP request method, returns the corresponding HttpMethod enum. + */ + public static HttpMethod fromInt(Integer method) { + if (method == null) { + return null; + } + + switch (method) { + case 1: + return HttpMethod.GET; + case 2: + return HttpMethod.POST; + case 3: + return HttpMethod.PUT; + case 4: + return HttpMethod.DELETE; + case 5: + return HttpMethod.PATCH; + default: + return null; + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker.kt new file mode 100644 index 0000000000..3921c5e05b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/HttpUrlConnectionTracker.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.network.http + +internal object HttpUrlConnectionTracker { + @JvmStatic + fun registerFactory(requestContentLengthCaptureEnabled: Boolean) { + StreamHandlerFactoryInstaller.registerFactory(requestContentLengthCaptureEnabled) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/NetworkCaptureData.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/NetworkCaptureData.kt new file mode 100644 index 0000000000..911e2b3e5e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/NetworkCaptureData.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk.network.http + +import io.embrace.android.embracesdk.InternalApi + +/** + * The additional data captured if network body capture is enabled for the URL + */ +@InternalApi +public data class NetworkCaptureData( + val requestHeaders: Map?, + val requestQueryParams: String?, + val capturedRequestBody: ByteArray?, + val responseHeaders: Map?, + val capturedResponseBody: ByteArray?, + val dataCaptureErrorMessage: String? = null +) { + val requestBodySize: Int + get() = capturedRequestBody?.size ?: 0 + + val responseBodySize: Int + get() = capturedResponseBody?.size ?: 0 +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/StreamHandlerFactoryInstaller.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/StreamHandlerFactoryInstaller.java new file mode 100644 index 0000000000..87062e9d06 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/StreamHandlerFactoryInstaller.java @@ -0,0 +1,199 @@ +package io.embrace.android.embracesdk.network.http; + +import androidx.annotation.NonNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +import javax.net.ssl.HttpsURLConnection; + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; +import io.embrace.android.embracesdk.utils.exceptions.Unchecked; + +/** + * Installs the correct type of {@link URLStreamHandlerFactory} in order to intercept network + * traffic in a way which is compatible with other SDKs. {@code URL.setURLStreamHandlerFactory} is + * a singleton, so if an existing factory is already registered, it is wrapped using reflection so + * that Embrace can intercept network traffic. + *

+ * This relies on the Embrace SDK being initialized second, so that the Embrace SDK is able to + * detect an existing {@link URLStreamHandlerFactory} and wrap it with its interception logic. + */ +class StreamHandlerFactoryInstaller { + + private StreamHandlerFactoryInstaller() { + // Restricted constructor + } + + /** + * Registers either a {@link EmbraceUrlStreamHandlerFactory} or a {@link WrappingFactory} + * depending on whether or not an existing factory has already been registered. + *

+ * If there is an exception thrown when attempting to detect or wrap the third-party factory + * using reflection, the method will fall back to trying to register a + * {@link EmbraceUrlStreamHandlerFactory} in the typical way. + */ + static void registerFactory(Boolean enableRequestSizeCapture) { + EmbraceUrlStreamHandler.setEnableRequestSizeCapture(enableRequestSizeCapture); + + try { + Object existingFactory = getFactoryField().get(null); + if (existingFactory == null) { + // No factory is registered, so we can simply register the Embrace factory + InternalStaticEmbraceLogger.logInfo("Registering EmbraceUrlStreamHandlerFactory."); + URL.setURLStreamHandlerFactory(new EmbraceUrlStreamHandlerFactory()); + } else { + InternalStaticEmbraceLogger.logInfo("Existing URLStreamHandlerFactory detected " + + "(" + existingFactory.getClass().getName() + "). Wrapping with Embrace factory " + + "to enable network traffic interception."); + WrappingFactory wrappingFactory = new WrappingFactory((URLStreamHandlerFactory) existingFactory, enableRequestSizeCapture); + clearFactory(); + URL.setURLStreamHandlerFactory(wrappingFactory); + } + } catch (Throwable ex) { + // Catching Throwable as URL.setURLStreamHandlerFactory throws an Error which we want to + // handle, rather than kill the application if we are unable to swap the factory. + String msg = "Error during wrapping of UrlStreamHandlerFactory. Will attempt to set the default Embrace factory"; + InternalStaticEmbraceLogger.logWarning(msg, ex); + try { + URL.setURLStreamHandlerFactory(new EmbraceUrlStreamHandlerFactory()); + } catch (Throwable ex2) { + InternalStaticEmbraceLogger.logDebug("Failed to register EmbraceUrlStreamHandlerFactory. Network capture disabled.", ex2); + } + } + } + + /** + * Gets the field within {@link URL} holding the factory. + * + * @return the field holding the factory. + */ + private static Field getFactoryField() { + // Use reflection to get the field holding the factory + final Field[] fields = URL.class.getDeclaredFields(); + for (Field current : fields) { + if (Modifier.isStatic(current.getModifiers()) && current.getType().equals(URLStreamHandlerFactory.class)) { + current.setAccessible(true); + return current; + } + } + throw new IllegalStateException("Unable to detect static field in the URL class for the URLStreamHandlerFactory."); + } + + /** + * Forcibly clears the existing factory from {@link URL} to allow us to attach a new one. + *

+ * The new factory must not be set on the field directly, as {@link URL} caches each + * {@link URLStreamHandler}. By clearing the factory and then calling + * {@code URL.setURLStreamHandlerFactory}, we ensure that the cached handlers are cleared. + */ + private static void clearFactory() { + try { + Field factoryField = getFactoryField(); + factoryField.set(null, null); + } catch (Exception ex) { + throw Unchecked.propagate(ex); + } + } + + /** + * A factory which generates a {@link URLConnection}, wrapping the {@link URLConnection} provided + * by the wrapped factory. The Embrace network logging is performed, then we delegate to the + * third-party {@link URLConnection}. + */ + private static class WrappingFactory implements URLStreamHandlerFactory { + + private final URLStreamHandlerFactory parent; + + /** + * This enables or disables automatic gzip decompression + */ + final Boolean enableRequestSizeCapture; + + /** + * Creates an instance of the wrapping factory. + * + * @param parent the {@link URLStreamHandlerFactory} to wrap + * @param enableRequestSizeCapture config flag to enabled content size measurements + */ + WrappingFactory(@NonNull URLStreamHandlerFactory parent, Boolean enableRequestSizeCapture) { + this.parent = parent; + this.enableRequestSizeCapture = enableRequestSizeCapture; + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + URLStreamHandler parentHandler; + try { + parentHandler = parent.createURLStreamHandler(protocol); + } catch (Exception ex) { + String msg = "Exception when trying to create stream handler with parent factory for protocol: " + protocol; + InternalStaticEmbraceLogger.logDebug(msg, ex); + return new EmbraceUrlStreamHandlerFactory().createURLStreamHandler(protocol); + } + if (parentHandler == null) { + // Fall back to the Embrace factory if the parent handler doesn't support the protocol + return new EmbraceUrlStreamHandlerFactory().createURLStreamHandler(protocol); + } + + return new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL url, Proxy proxy) { + try { + Method method = parentHandler.getClass().getDeclaredMethod("openConnection", URL.class, Proxy.class); + method.setAccessible(true); + URLConnection parentConnection = (URLConnection) method.invoke(parentHandler, url, proxy); + return wrapConnection(parentConnection); + } catch (Exception ex) { + String msg = "Exception when opening connection for protocol: " + protocol + " and URL: " + url; + InternalStaticEmbraceLogger.logDebug(msg, ex); + throw Unchecked.propagate(ex); + } + } + + @Override + protected URLConnection openConnection(URL url) { + try { + Method method = parentHandler.getClass().getDeclaredMethod("openConnection", URL.class); + method.setAccessible(true); + URLConnection parentConnection = (URLConnection) method.invoke(parentHandler, url); + return wrapConnection(parentConnection); + } catch (Exception ex) { + String msg = "Exception when opening connection for protocol: " + protocol + " and URL: " + url; + InternalStaticEmbraceLogger.logDebug(msg, ex); + throw Unchecked.propagate(ex); + } + } + + private URLConnection wrapConnection(URLConnection parentConnection) { + if (parentConnection instanceof HttpURLConnection) { + boolean transparentGzip = false; + if (enableRequestSizeCapture && !parentConnection.getRequestProperties().containsKey("Accept-Encoding")) { + // This disables automatic gzip decompression by HttpUrlConnection so that we can + // accurately count the number of bytes. We handle the decompression ourselves. + parentConnection.setRequestProperty("Accept-Encoding", "gzip"); + transparentGzip = true; + } + if (parentConnection instanceof HttpsURLConnection) { + return new EmbraceHttpsUrlConnection<>((HttpsURLConnection) parentConnection, transparentGzip); + } else { + return new EmbraceHttpUrlConnection<>((HttpURLConnection) parentConnection, transparentGzip); + + } + } else { + // We do not support wrapping this connection type + InternalStaticEmbraceLogger.logDebug("Cannot wrap unsupported protocol: " + protocol); + return parentConnection; + } + } + }; + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/DomainSettings.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/DomainSettings.kt new file mode 100644 index 0000000000..161630b987 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/DomainSettings.kt @@ -0,0 +1,6 @@ +package io.embrace.android.embracesdk.network.logging + +internal data class DomainSettings( + val limit: Int = 0, + val suffix: String? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService.kt new file mode 100644 index 0000000000..e92f6bc63c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureService.kt @@ -0,0 +1,167 @@ +package io.embrace.android.embracesdk.network.logging + +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.NetworkCaptureRuleRemoteConfig +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.payload.NetworkCapturedCall +import io.embrace.android.embracesdk.prefs.PreferencesService +import kotlin.math.max + +/** + * Determines if a network call body should be captured based on the network rules obtained from the remote config. + */ +internal class EmbraceNetworkCaptureService( + private val metadataService: MetadataService, + private val preferencesService: PreferencesService, + private val remoteLogger: EmbraceRemoteLogger, + private val configService: ConfigService, + private val serializer: EmbraceSerializer +) : NetworkCaptureService { + + companion object { + const val NETWORK_ERROR_CODE = -1 + } + + private val networkCaptureEncryptionManager = lazy { NetworkCaptureEncryptionManager() } + + /** + * Returns the network capture rule that matches the URL and method of the network call. + * The rule must be apply only the number of times set on NetworkCaptureRule.max_count. + * The rule expire_in field must be > 0. Otherwise the rule is expired and shouldn't be apply. + */ + override fun getNetworkCaptureRules(url: String, method: String): Set { + val networkCaptureRules = configService.networkBehavior.getNetworkCaptureRules().toMutableSet() + if (networkCaptureRules.isEmpty()) { + InternalStaticEmbraceLogger.logger.logDebug("No network capture rules") + return emptySet() + } + + // Embrace data endpoint cannot be captured, even if there is a rule for that. + if (url.contentEquals(configService.sdkEndpointBehavior.getData(metadataService.getAppId()))) { + InternalStaticEmbraceLogger.logger.logDebug("Cannot intercept Embrace endpoints") + return emptySet() + } + + val applicableRules = networkCaptureRules.filter { rule -> + rule.method.contains(method) && rule.urlRegex.toRegex().containsMatchIn(url) && rule.expiresIn > 0 + }.toMutableSet() + + val rulesToRemove = mutableSetOf() + applicableRules.forEach { rule -> + if (preferencesService.isNetworkCaptureRuleOver(rule.id)) { + rulesToRemove.add(rule) + } + } + + networkCaptureRules.removeAll(rulesToRemove) + applicableRules.removeAll(rulesToRemove) + + InternalStaticEmbraceLogger.logger.logDebug("Capture rule is: $applicableRules") + return applicableRules + } + + /** + * Logs the network captured data only if it matches the duration and status code set on the Network Rule. + */ + override fun logNetworkCapturedData( + url: String, + httpMethod: String, + statusCode: Int, + startTime: Long, + endTime: Long, + networkCaptureData: NetworkCaptureData?, + errorMessage: String? + ) { + + val duration = max(endTime - startTime, 0) + + getNetworkCaptureRules(url, httpMethod).forEach { rule -> + + if (shouldApplyRule(rule, duration, statusCode)) { + val requestBody = parseBody(networkCaptureData?.capturedRequestBody, rule.maxSize) + val responseBody = + networkCaptureData?.dataCaptureErrorMessage ?: parseBody(networkCaptureData?.capturedResponseBody, rule.maxSize) + preferencesService.decreaseNetworkCaptureRuleRemainingCount(rule.id, rule.maxCount) + + val capturedNetworkCall = NetworkCapturedCall( + duration = duration, + endTime = endTime, + httpMethod = httpMethod, + matchedUrl = rule.urlRegex, + requestBody = requestBody, + requestBodySize = networkCaptureData?.requestBodySize, + requestQuery = networkCaptureData?.requestQueryParams, + requestQueryHeaders = networkCaptureData?.requestHeaders, + requestSize = networkCaptureData?.requestBodySize, + responseBody = responseBody, + responseBodySize = networkCaptureData?.responseBodySize, + responseHeaders = networkCaptureData?.responseHeaders, + responseSize = networkCaptureData?.responseBodySize, + responseStatus = statusCode, + sessionId = metadataService.activeSessionId, + startTime = startTime, + url = url, + errorMessage = errorMessage + ) + + val networkLog = getNetworkPayload(capturedNetworkCall) + + // we will create an event with the network request type + remoteLogger.logNetwork( + networkLog + ) + + // if the network captured match at least one rule criteria, we logged that body and finish the foreach. + return + } else { + InternalStaticEmbraceLogger.logger.logDebug("The captured data doesn't match the rule criteria") + } + } + } + + private fun getNetworkPayload(capturedNetworkCall: NetworkCapturedCall): NetworkCapturedCall { + + return if (configService.networkBehavior.isCaptureBodyEncryptionEnabled()) { + val encryptedPayload = encryptNetworkCall(capturedNetworkCall) + NetworkCapturedCall(encryptedPayload = encryptedPayload) + } else { + capturedNetworkCall + } + } + + private fun encryptNetworkCall(capturedNetworkCall: NetworkCapturedCall): String? { + val capturePublicKey = configService.networkBehavior.getCapturePublicKey() ?: return null + return networkCaptureEncryptionManager.value.encrypt( + serializer.toJson(capturedNetworkCall), + capturePublicKey + ) + } + + private fun shouldApplyRule(rule: NetworkCaptureRuleRemoteConfig, duration: Long, statusCode: Int): Boolean { + return if (rule.statusCodes.contains(statusCode)) { + if (rule.duration == null || rule.duration == 0L) { + true + } else { + duration >= rule.duration + } + } else { + false + } + } + + /** + * Transform the ByteArray body to a String. + * Trim the String body if needed (maxSize is set in the network rule). + */ + private fun parseBody(body: ByteArray?, maxSize: Long): String? { + body?.also { + val endIndex = if (it.size > maxSize) maxSize else it.size + return it.decodeToString(0, endIndex.toInt(), false) + } + return null + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt new file mode 100644 index 0000000000..1f6d3cc299 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt @@ -0,0 +1,252 @@ +package io.embrace.android.embracesdk.network.logging + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.internal.CacheableValue +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.network.logging.EmbraceNetworkCaptureService.Companion.NETWORK_ERROR_CODE +import io.embrace.android.embracesdk.payload.NetworkCallV2 +import io.embrace.android.embracesdk.payload.NetworkSessionV2 +import io.embrace.android.embracesdk.payload.NetworkSessionV2.DomainCount +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import io.embrace.android.embracesdk.utils.NetworkUtils.getDomain +import io.embrace.android.embracesdk.utils.NetworkUtils.getValidTraceId +import io.embrace.android.embracesdk.utils.NetworkUtils.isIpAddress +import io.embrace.android.embracesdk.utils.NetworkUtils.stripUrl +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentSkipListMap +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.max + +/** + * Logs network calls according to defined limits per domain. + * + * + * Limits can be defined either in server-side configuration or within the embrace configuration file. + * A limit of 0 disables logging for the domain. All network calls are captured up to the limit, + * and the number of calls is also captured if the limit is exceeded. + */ +internal class EmbraceNetworkLoggingService( + private val configService: ConfigService, + private val logger: InternalEmbraceLogger, + private val networkCaptureService: NetworkCaptureService +) : NetworkLoggingService, MemoryCleanerListener { + + /** + * Network calls per domain prepared for the session. + */ + private val sessionNetworkCalls = ConcurrentSkipListMap() + private val networkCallCache = CacheableValue>(sessionNetworkCalls::size) + + private val domainSettings = ConcurrentHashMap() + + private val callsPerDomain = hashMapOf() + + private val ipAddressCount = AtomicInteger(0) + + override fun getNetworkCallsForSession(startTime: Long, lastKnownTime: Long): NetworkSessionV2 { + logger.logDeveloper("EmbraceNetworkLoggingService", "getNetworkCallsForSession") + + val calls = networkCallCache.value { + ArrayList(sessionNetworkCalls.subMap(startTime, lastKnownTime).values) + } + + val overLimit = hashMapOf() + for ((key, value) in callsPerDomain) { + if (value.requestCount > value.captureLimit) { + overLimit[key] = value + } + } + + // clear calls per domain and session network calls lists before be used by the next session + callsPerDomain.clear() + return NetworkSessionV2(calls, overLimit) + } + + override fun logNetworkCall( + url: String, + httpMethod: String, + statusCode: Int, + startTime: Long, + endTime: Long, + bytesSent: Long, + bytesReceived: Long, + traceId: String?, + w3cTraceparent: String?, + networkCaptureData: NetworkCaptureData? + ) { + val duration = max(endTime - startTime, 0) + val strippedUrl = stripUrl(url) + val validTraceId = getValidTraceId(traceId) + val networkCall = NetworkCallV2( + url = strippedUrl, + httpMethod = httpMethod, + responseCode = statusCode, + bytesSent = bytesSent, + bytesReceived = bytesReceived, + startTime = startTime, + endTime = endTime, + duration = duration, + traceId = validTraceId, + w3cTraceparent = w3cTraceparent + ) + + if (networkCaptureData != null) { + networkCaptureService.logNetworkCapturedData( + url, + httpMethod, + statusCode, + startTime, + endTime, + networkCaptureData + ) + } + + processNetworkCall(startTime, networkCall) + storeSettings(url) + } + + override fun logNetworkError( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + errorType: String?, + errorMessage: String?, + traceId: String?, + w3cTraceparent: String?, + networkCaptureData: NetworkCaptureData? + ) { + val duration = max(endTime - startTime, 0) + val strippedUrl = stripUrl(url) + val validTraceId = getValidTraceId(traceId) + val networkCall = NetworkCallV2( + url = strippedUrl, + httpMethod = httpMethod, + startTime = startTime, + endTime = endTime, + duration = duration, + traceId = validTraceId, + w3cTraceparent = w3cTraceparent, + errorMessage = errorMessage, + errorType = errorType + ) + + if (networkCaptureData != null) { + networkCaptureService.logNetworkCapturedData( + url, + httpMethod, + NETWORK_ERROR_CODE, + startTime, + endTime, + networkCaptureData, + errorMessage + ) + } + processNetworkCall(startTime, networkCall) + storeSettings(url) + } + + /** + * Process network calls to be ready when the session requests them. + * + * @param startTime is the time when the network call was captured + * @param networkCall that is going to be captured + */ + private fun processNetworkCall(startTime: Long, networkCall: NetworkCallV2) { + logger.logDeveloper("EmbraceNetworkLoggingService", "processNetworkCall at: $startTime") + + // Get the domain, if it can be successfully parsed + val domain = networkCall.url?.let { + getDomain(it) + } + + if (domain == null) { + logger.logDeveloper("EmbraceNetworkLoggingService", "Domain is not present") + return + } + + logger.logDeveloper("EmbraceNetworkLoggingService", "Domain: $domain") + + if (isIpAddress(domain)) { + logger.logDeveloper("EmbraceNetworkLoggingService", "Domain is an ip address") + val captureLimit = configService.networkBehavior.getNetworkCaptureLimit() + + if (ipAddressCount.getAndIncrement() < captureLimit) { + // only capture if the ipAddressCount has not exceeded defaultLimit + logger.logDeveloper("EmbraceNetworkLoggingService", "capturing network call") + sessionNetworkCalls[startTime] = networkCall + } else { + logger.logDeveloper("EmbraceNetworkLoggingService", "capture limit exceeded") + } + return + } + + val settings = domainSettings[domain] + if (settings == null) { + logger.logDeveloper("EmbraceNetworkLoggingService", "no domain settings") + sessionNetworkCalls[startTime] = networkCall + } else { + val suffix = settings.suffix + val limit = settings.limit + var count = callsPerDomain[suffix] + + if (count == null) { + count = DomainCount(1, limit) + } + + // Exclude if the network call exceeds the limit + if (count.requestCount < limit) { + sessionNetworkCalls[startTime] = networkCall + } else { + logger.logDeveloper("EmbraceNetworkLoggingService", "capture limit exceeded") + } + + // Track the number of calls for each domain (or configured suffix) + suffix?.let { + callsPerDomain[it] = DomainCount(count.requestCount + 1, limit) + logger.logDeveloper( + "EmbraceNetworkLoggingService", + "Call per domain $domain ${count.requestCount + 1}" + ) + } + } + } + + private fun storeSettings(url: String) { + try { + val mergedLimits = configService.networkBehavior.getNetworkCallLimitsPerDomain() + + val domain = getDomain(url) + if (domain == null) { + logger.logDeveloper("EmbraceNetworkLoggingService", "Domain not present") + return + } + if (domainSettings.containsKey(domain)) { + logger.logDeveloper("EmbraceNetworkLoggingService", "No settings for $domain") + return + } + + for ((key, value) in mergedLimits) { + if (domain.endsWith(key)) { + domainSettings[domain] = DomainSettings(value, key) + return + } + } + + val defaultLimit = configService.networkBehavior.getNetworkCaptureLimit() + domainSettings[domain] = DomainSettings(defaultLimit, domain) + } catch (ex: Exception) { + logger.logDebug("Failed to determine limits for URL: $url", ex) + } + } + + override fun cleanCollections() { + domainSettings.clear() + callsPerDomain.clear() + sessionNetworkCalls.clear() + // reset counters + ipAddressCount.set(0) + logger.logDeveloper("EmbraceNetworkLoggingService", "Collections cleaned") + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManager.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManager.java new file mode 100644 index 0000000000..c8bdd5d5c6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManager.java @@ -0,0 +1,165 @@ +package io.embrace.android.embracesdk.network.logging; + +import android.util.Base64; + +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; + +/** + * API to encrypt/decrypt data + */ +class NetworkCaptureEncryptionManager { + + private static final String UTF_8 = "UTF-8"; + private final String transformation = "RSA/ECB/PKCS1Padding"; + private static final int mEncryptionBlockSize = 245; + private static final int mDecryptionBlockSize = 256; + + + /** + * @return encrypted data in Base64 String or null if any error occur. + */ + @Nullable + public String encrypt(@NonNull String data, @NonNull String keyText) { + try { + Key publicKey = getKeyFromText(keyText); + if (publicKey != null) { + return encrypt(data, publicKey); + } else { + InternalStaticEmbraceLogger.logError("wrong public key"); + return null; + } + } catch (Exception e) { + InternalStaticEmbraceLogger.logError("data cannot be encrypted", e); + return null; + } + } + + /** + * @return encrypted data in Base64 String or null if any error occur. + */ + @Nullable + private String encrypt(@NonNull String data, @NonNull Key key) { + String result = ""; + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.ENCRYPT_MODE, key); + + byte[] plainData = data.getBytes(UTF_8); + byte[] decodedData = decodeWithBuffer(cipher, plainData, mEncryptionBlockSize); + + String encodedString = Base64.encodeToString(decodedData, Base64.DEFAULT); + result += encodedString; + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | + IllegalBlockSizeException | IOException e) { + InternalStaticEmbraceLogger.logError("data cannot be encrypted", e); + } + return result; + } + + /** + * @param data Base64 encrypted data. + * @return decrypted data or null if any error occur + */ + @Nullable + public String decrypt(@NonNull String data, @NonNull Key key) { + String result = null; + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.DECRYPT_MODE, key); + + byte[] decodedData; + byte[] encryptedData = Base64.decode(data, Base64.DEFAULT); + decodedData = decodeWithBuffer(cipher, encryptedData, mDecryptionBlockSize); + + result = new String(decodedData, UTF_8); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | + BadPaddingException | IllegalBlockSizeException | IOException e) { + InternalStaticEmbraceLogger.logError("data cannot be encrypted", e); + } + return result; + } + + private byte[] decodeWithBuffer(@NonNull Cipher cipher, @NonNull byte[] plainData, int bufferLength) + throws IllegalBlockSizeException, BadPaddingException { + // string initialize 2 buffers. + // scrambled will hold intermediate results + byte[] scrambled; + + // toReturn will hold the total result + byte[] toReturn = new byte[0]; + + // holds the bytes that have to be modified in one step + byte[] buffer = new byte[(plainData.length > bufferLength ? bufferLength : plainData.length)]; + + for (int i = 0; i < plainData.length; i++) { + if ((i > 0) && (i % bufferLength == 0)) { + //execute the operation + scrambled = cipher.doFinal(buffer); + // add the result to our total result. + toReturn = append(toReturn, scrambled); + // here we calculate the bufferLength of the next buffer required + int newLength = bufferLength; + + // if newLength would be longer than remaining bytes in the bytes array we shorten it. + if (i + bufferLength > plainData.length) { + newLength = plainData.length - i; + } + // clean the buffer array + buffer = new byte[newLength]; + } + // copy byte into our buffer. + buffer[i % bufferLength] = plainData[i]; + } + + // this step is needed if we had a trailing buffer. should only happen when encrypting. + // example: we encrypt 110 bytes. 100 bytes per run means we "forgot" the last 10 bytes. they are in the buffer array + scrambled = cipher.doFinal(buffer); + + // final step before we can return the modified data. + toReturn = append(toReturn, scrambled); + return toReturn; + } + + private byte[] append(byte[] prefix, byte[] suffix) { + byte[] toReturn = new byte[prefix.length + suffix.length]; + for (int i = 0; i < prefix.length; i++) { + toReturn[i] = prefix[i]; + } + for (int i = 0; i < suffix.length; i++) { + toReturn[i + prefix.length] = suffix[i]; + } + return toReturn; + } + + private Key getKeyFromText(String keyText) { + + try { + X509EncodedKeySpec encodedKeySpec = new X509EncodedKeySpec(Base64.decode(keyText, Base64.DEFAULT)); + return KeyFactory.getInstance("RSA").generatePublic(encodedKeySpec); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + + return null; + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureService.kt new file mode 100644 index 0000000000..a2b3a4812d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureService.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.network.logging + +import io.embrace.android.embracesdk.config.remote.NetworkCaptureRuleRemoteConfig +import io.embrace.android.embracesdk.network.http.NetworkCaptureData + +internal interface NetworkCaptureService { + + /** + * Returns the network capture rule applicable to the URL and the method given. + * + * @param url the network URL + * @param method the network URL + * @return the network rule to apply, or null if it is no rule that match the criteria. + */ + fun getNetworkCaptureRules(url: String, method: String): Set + + /** + * Logs the network captured data if this match the rule criteria. + */ + fun logNetworkCapturedData( + url: String, + httpMethod: String, + statusCode: Int, + startTime: Long, + endTime: Long, + networkCaptureData: NetworkCaptureData?, + errorMessage: String? = null + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt new file mode 100644 index 0000000000..0ce9e192e4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt @@ -0,0 +1,73 @@ +package io.embrace.android.embracesdk.network.logging + +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.payload.NetworkSessionV2 + +/** + * Logs network calls made by the application. The Embrace SDK intercepts the calls and reports + * them to the API. + */ +internal interface NetworkLoggingService { + + /** + * Get the calls and counts of network calls (which exceed the limit) within the specified time + * range. + * + * @param startTime the start time + * @param lastKnownTime the end time + * @return the network calls for the given session + */ + fun getNetworkCallsForSession(startTime: Long, lastKnownTime: Long): NetworkSessionV2 + + /** + * Logs a HTTP network call. + * + * @param url the URL being called + * @param httpMethod the HTTP method + * @param statusCode the status code from the response + * @param startTime the start time of the request + * @param endTime the end time of the request + * @param bytesSent the number of bytes sent + * @param bytesReceived the number of bytes received + * @param traceId optional trace ID that can be used to trace a particular request + * @param w3cTraceparent optional W3C-compliant traceparent representing the network call that is being recorded + * @param networkCaptureData the additional data captured if network body capture is enabled for the URL + */ + fun logNetworkCall( + url: String, + httpMethod: String, + statusCode: Int, + startTime: Long, + endTime: Long, + bytesSent: Long, + bytesReceived: Long, + traceId: String?, + w3cTraceparent: String?, + networkCaptureData: NetworkCaptureData? + ) + + /** + * Logs an exception which occurred when attempting to make a network call. + * + * @param url the URL being called + * @param httpMethod the HTTP method + * @param startTime the start time of the request + * @param endTime the end time of the request + * @param errorType the type of error being thrown + * @param errorMessage the error message + * @param traceId optional trace ID that can be used to trace a particular request + * @param w3cTraceparent optional W3C-compliant traceparent representing the network call that is being recorded + * @param networkCaptureData the additional data captured if network body capture is enabled for the URL + */ + fun logNetworkError( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + errorType: String?, + errorMessage: String?, + traceId: String?, + w3cTraceparent: String?, + networkCaptureData: NetworkCaptureData? + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleBreadcrumb.kt new file mode 100644 index 0000000000..1126f42d5b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleBreadcrumb.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * The data for the activity lifecycle breadcrumb. Note that this does not have the same structure + * as the Breadcrumb.java interface. + */ +internal data class ActivityLifecycleBreadcrumb( + + @Transient + internal val activity: String?, + + @SerializedName("s") + internal val state: ActivityLifecycleState, + + @SerializedName("st") + internal val start: Long?, + + @SerializedName("b") + internal var bundlePresent: Boolean? = false, + + @SerializedName("en") + internal var end: Long? = -1, +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleData.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleData.kt new file mode 100644 index 0000000000..fa7e2cdc32 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleData.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class ActivityLifecycleData( + @SerializedName("a") + internal val activity: String?, + + @SerializedName("d") + internal val data: List? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleState.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleState.kt new file mode 100644 index 0000000000..8e5dc892af --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ActivityLifecycleState.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.payload + +/** + * The possible Activity lifecycle states that we care about capturing + */ +internal enum class ActivityLifecycleState { + ON_CREATE, + ON_START, + ON_RESUME, + ON_PAUSE, + ON_STOP, + ON_DESTROY, + ON_SAVE_INSTANCE_STATE, +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrInterval.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrInterval.kt new file mode 100644 index 0000000000..0fcb408fe0 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrInterval.kt @@ -0,0 +1,105 @@ +package io.embrace.android.embracesdk.payload + +import androidx.annotation.CheckResult +import androidx.annotation.VisibleForTesting +import com.google.gson.annotations.SerializedName + +/** + * Intervals during which the UI thread was blocked for more than 1 second, which + * determines that the application is not responding (ANR). + */ +internal data class AnrInterval @JvmOverloads constructor( + + /** + * The time at which the application stopped responding. + */ + @SerializedName("st") + val startTime: Long, + + /** + * The last time the thread was alive. + */ + @SerializedName("lk") + @get:VisibleForTesting + val lastKnownTime: Long? = null, + + /** + * The time the application started responding. + */ + @SerializedName("en") + @get:VisibleForTesting + val endTime: Long? = null, + + /** + * The component of the application which stopped responding. + */ + @SerializedName("v") + @get:VisibleForTesting + val type: Type = Type.UI, + + /** + * The captured stacktraces of the anr interval. + */ + @SerializedName("se") + @get:VisibleForTesting + val anrSampleList: AnrSampleList? = null, + + /** + * The status code of the ANR interval. + */ + @SerializedName("c") + val code: Int? = CODE_DEFAULT +) { + /** + * The type of thread not responding. Currently only the UI thread is monitored. + */ + internal enum class Type { + @SerializedName("ui") + UI + } + + /** + * Retrieves the ANR sample count associated with this interval, or 0 if the samples have been + * redacted. + */ + fun size(): Int = anrSampleList?.size() ?: 0 + + /** + * Calculates the duration of the interval, returning -1 if this is unknown. + */ + fun duration(): Long { + return when (val end = endTime ?: lastKnownTime) { + null -> -1 + else -> end - startTime + } + } + + /** + * Performs a copy of the AnrInterval that ensures the [anrSampleList] is a new object. Note: + * that this does not copy all the way down the object tree. + */ + fun deepCopy(): AnrInterval { + val copy = when (val original = anrSampleList) { + null -> null + else -> original.copy(samples = original.samples.toMutableList()) + } + return AnrInterval( + startTime, + lastKnownTime, + endTime, + type, + copy, + code + ) + } + + @CheckResult + fun clearSamples(): AnrInterval = copy(anrSampleList = null, code = CODE_SAMPLES_CLEARED) + + fun hasSamples(): Boolean = code != CODE_SAMPLES_CLEARED + + companion object { + internal const val CODE_DEFAULT = 0 + internal const val CODE_SAMPLES_CLEARED = 1 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSample.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSample.kt new file mode 100644 index 0000000000..63f083e611 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSample.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Holds thread data taken during an [AnrInterval]. + */ +internal data class AnrSample( + + /** + * The timestamp in milliseconds at which this sample was captured + */ + @SerializedName("ts") + val timestamp: Long, + + /** + * All the information for threads that were captured during an ANR sample + */ + val threads: List?, + + /** + * The overhead in milliseconds associated with capturing thread traces for this sample + */ + @SerializedName("o") + val sampleOverheadMs: Long?, + + /** + * The status code of the ANR sample. + */ + @SerializedName("c") + val code: Int? = CODE_DEFAULT +) { + + companion object { + internal const val CODE_DEFAULT = 0 + internal const val CODE_SAMPLE_LIMIT_REACHED = 1 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSampleList.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSampleList.kt new file mode 100644 index 0000000000..078673e463 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AnrSampleList.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Holds a list of [AnrSample] objects. + */ +internal data class AnrSampleList( + + /** + * List of samples. + */ + @SerializedName("ticks") + val samples: List +) { + + /** + * Retrieves the size of the list. + */ + fun size(): Int = samples.size +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppExitInfoData.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppExitInfoData.kt new file mode 100644 index 0000000000..2d7faba227 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppExitInfoData.kt @@ -0,0 +1,45 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class AppExitInfoData( + @SerializedName("sid") + internal val sessionId: String?, + + @SerializedName("side") + internal val sessionIdError: String?, + + // the importance of the process that it used to have before the death. + @SerializedName("im") + internal val importance: Int?, + + // Last proportional set size of the memory that the process had used in Bytes. + @SerializedName("pss") + internal val pss: Long?, + + @SerializedName("rs") + internal val reason: Int?, + + // Last resident set size of the memory that the process had used in Bytes. + @SerializedName("rss") + internal val rss: Long?, + + // The exit status argument of exit() if the application calls it, + // or the signal number if the application is signaled. + @SerializedName("st") + internal val status: Int?, + + @SerializedName("ts") + internal val timestamp: Long?, + + // file with ANR/CRASH traces compressed as string + @SerializedName("blob") + internal val trace: String?, + + @SerializedName("ds") + internal val description: String?, + + // Error or Exception if the traces couldn't be collected + @SerializedName("trs") + internal val traceStatus: String? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppInfo.kt new file mode 100644 index 0000000000..5cee2eabc4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/AppInfo.kt @@ -0,0 +1,147 @@ +package io.embrace.android.embracesdk.payload + +import androidx.annotation.VisibleForTesting +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.internal.utils.MessageUtils + +internal data class AppInfo( + /** + * The version of the app which has embedded the Embrace SDK. + */ + @SerializedName("v") + val appVersion: String? = null, + /** + * The framework used by the app. + */ + @SerializedName("f") + val appFramework: Int? = null, + + /** + * A unique ID for the build which is generated at build time. This is written to a JSON file in + * the build directory and read by {@link BuildInfo}. + */ + @SerializedName("bi") + val buildId: String? = null, + + /** + * The build type name. This is written to a JSON file in the build directory and read by + * {@link BuildInfo}. + */ + @SerializedName("bt") + val buildType: String? = null, + + /** + * The flavor name. This is written to a JSON file in the build directory and read by + * {@link BuildInfo}. + */ + @SerializedName("fl") + val buildFlavor: String? = null, + + /** + * The name of the environment, i.e. dev or prod, determined by whether this is a debug build. + */ + @SerializedName("e") + val environment: String? = null, + + /** + * Whether the app was updated since the previous launch. + */ + @SerializedName("vu") + val appUpdated: Boolean? = null, + + /** + * Whether the app was updated since the previous launch. + */ + @SerializedName("vul") + val appUpdatedThisLaunch: Boolean? = null, + + /** + * The app bundle version. + */ + @SerializedName("bv") + val bundleVersion: String? = null, + + /** + * Whether the OS was updated since the last launch. + */ + @SerializedName("ou") + val osUpdated: Boolean? = null, + + /** + * Whether the OS was updated since the last launch. + */ + @SerializedName("oul") + val osUpdatedThisLaunch: Boolean? = null, + + /** + * The version number of the Embrace SDK. + */ + @SerializedName("sdk") + val sdkVersion: String? = null, + + /** + * The simple version number of the Embrace SDK. + */ + @SerializedName("sdc") + val sdkSimpleVersion: String? = null, + + /** + * The react native bundle hashed. + */ + @SerializedName("rn") + val reactNativeBundleId: String? = null, + + /** + * The java script patch number. + */ + @SerializedName("jsp") + val javaScriptPatchNumber: String? = null, + + /** + * The react native version number. + */ + @SerializedName("rnv") + val reactNativeVersion: String? = null, + + /** + * The version number of the platform (e.g. Unity 2021) + */ + @SerializedName("unv") + @get:VisibleForTesting + val hostedPlatformVersion: String? = null, + + /** + * The unity build id number. + */ + @SerializedName("ubg") + val buildGuid: String? = null, + + /** + * The version number of the hosted SDK (e.g. Embrace Unity 1.7.0) + */ + @SerializedName("usv") + @get:VisibleForTesting + val hostedSdkVersion: String? = null, +) { + fun toJson(): String { + return "{\"v\": " + MessageUtils.withNull(appVersion) + + ",\"f\": " + appFramework + + ",\"bi\":" + MessageUtils.withNull(buildId) + + ",\"bt\":" + MessageUtils.withNull(buildType) + + ",\"fl\":" + MessageUtils.withNull(buildFlavor) + + ",\"e\":" + MessageUtils.withNull(environment) + + ",\"vu\":" + MessageUtils.boolToStr(appUpdated) + + ",\"vul\":" + MessageUtils.boolToStr(appUpdatedThisLaunch) + + ",\"bv\":" + MessageUtils.withNull(bundleVersion) + + ",\"ou\":" + MessageUtils.boolToStr(osUpdated) + + ",\"oul\":" + MessageUtils.boolToStr(osUpdatedThisLaunch) + + ",\"sdk\":" + MessageUtils.withNull(sdkVersion) + + ",\"sdc\":" + MessageUtils.withNull(sdkSimpleVersion) + + ",\"rn\":" + MessageUtils.withNull(reactNativeBundleId) + + ",\"jsp\":" + MessageUtils.withNull(javaScriptPatchNumber) + + ",\"rnv\":" + MessageUtils.withNull(reactNativeVersion) + + ",\"unv\":" + MessageUtils.withNull(hostedPlatformVersion) + + ",\"ubg\":" + MessageUtils.withNull(buildGuid) + + ",\"usv\":" + MessageUtils.withNull(hostedSdkVersion) + "}" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivity.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivity.kt new file mode 100644 index 0000000000..0aa892bf28 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivity.kt @@ -0,0 +1,174 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Represents a particular user's session within the app. + */ +internal data class BackgroundActivity( + + /** + * A unique ID which identifies the session. + */ + @SerializedName("id") + val sessionId: String, + + /** + * The time that the session started. + */ + @SerializedName("st") + val startTime: Long?, + + /** + * Application state for this session (foreground or background) + */ + @SerializedName("as") + val appState: String?, + + /** + * The time that the session ended. + */ + @SerializedName("et") + val endTime: Long? = null, + + /** + * The ordinal of the session, starting from 1. + */ + @SerializedName("sn") + val number: Int? = null, + + /** + * Type of the session message (start or end) + */ + @SerializedName("ty") + val messageType: String? = null, + + @SerializedName("ht") + val lastHeartbeatTime: Long? = null, + + @SerializedName("ls") + val lastState: String? = null, + + @SerializedName("ba") + val startingBatteryLevel: Double? = null, + + @SerializedName("cs") + val isColdStart: Boolean? = null, + + @SerializedName("ss") + val eventIds: List? = null, + + @SerializedName("il") + val infoLogIds: List? = null, + + @SerializedName("wl") + val warningLogIds: List? = null, + + @SerializedName("el") + val errorLogIds: List? = null, + + @SerializedName("lic") + val infoLogsAttemptedToSend: Int? = null, + + @SerializedName("lwc") + val warnLogsAttemptedToSend: Int? = null, + + @SerializedName("lec") + val errorLogsAttemptedToSend: Int? = null, + + @SerializedName("e") + val exceptionError: ExceptionError? = null, + + @SerializedName("ri") + val crashReportId: String? = null, + + @SerializedName("em") + val endType: LifeEventType? = null, + + @SerializedName("sm") + val startType: LifeEventType? = null, + + @SerializedName("sp") + val properties: Map? = null, + + @SerializedName("ue") + val unhandledExceptions: Int? = null, + + @Transient + val user: UserInfo? = null +) { + + /** + * Enum to discriminate the different ways a background session can start / end + */ + internal enum class LifeEventType { + @SerializedName("bs") + BKGND_STATE, + + @SerializedName("bm") + BKGND_MANUAL, + + @SerializedName("bt") + BKGND_TIME, + + @SerializedName("be") + BKGND_SIZE + } + + companion object { + + @JvmStatic + fun createStartMessage( + embUuid: String, + startTime: Long, + coldStart: Boolean, + startType: LifeEventType, + applicationState: String, + userInfo: UserInfo? + ) = BackgroundActivity( + sessionId = embUuid, + startTime = startTime, + appState = applicationState, + isColdStart = coldStart, + startType = startType, + user = userInfo + ) + + @JvmStatic + @Suppress("LongParameterList") + fun createStopMessage( + original: BackgroundActivity, + applicationState: String, + messageType: String, + endTime: Long?, + eventIdsForSession: List, + infoLogIds: List, + warningLogIds: List, + errorLogIds: List, + infoLogsAttemptedToSend: Int, + warnLogsAttemptedToSend: Int, + errorLogsAttemptedToSend: Int, + currentExceptionError: ExceptionError?, + lastHeartbeatTime: Long, + endType: LifeEventType?, + unhandledExceptionsSent: Int, + crashId: String? + ) = original.copy( + appState = applicationState, + messageType = messageType, + endTime = endTime, + eventIds = eventIdsForSession, + infoLogIds = infoLogIds, + warningLogIds = warningLogIds, + errorLogIds = errorLogIds, + infoLogsAttemptedToSend = infoLogsAttemptedToSend, + warnLogsAttemptedToSend = warnLogsAttemptedToSend, + errorLogsAttemptedToSend = errorLogsAttemptedToSend, + exceptionError = currentExceptionError, + lastHeartbeatTime = lastHeartbeatTime, + endType = endType, + unhandledExceptions = unhandledExceptionsSent, + crashReportId = crashId + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessage.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessage.kt new file mode 100644 index 0000000000..0460fd232f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessage.kt @@ -0,0 +1,54 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData + +/** + * The session message, containing the session itself, as well as performance information about the + * device which occurred during the session. + */ +internal data class BackgroundActivityMessage @JvmOverloads internal constructor( + + /** + * The session information. + */ + @SerializedName("s") + val backgroundActivity: BackgroundActivity, + + /** + * The user information. + */ + @SerializedName("u") + val userInfo: UserInfo?, + + /** + * The app information. + */ + @SerializedName("a") + val appInfo: AppInfo, + + /** + * The device information. + */ + @SerializedName("d") + val deviceInfo: DeviceInfo, + + /** + * The device's performance info, such as power, cpu, network. + */ + @SerializedName("p") + val performanceInfo: PerformanceInfo, + + /** + * Breadcrumbs which occurred as part of this session. + */ + @SerializedName("br") + val breadcrumbs: Breadcrumbs, + + @SerializedName("spans") + val spans: List?, + + @SerializedName("v") + val version: Int = ApiClient.MESSAGE_VERSION +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BetaFeatures.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BetaFeatures.kt new file mode 100644 index 0000000000..3fd5e384bc --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BetaFeatures.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * This part of the session payload contains data that is collected from beta features. + * + * Putting it in this class segregates it from the rest of the payload & makes it obvious + * where we should be querying information. Once the beta features are promoted to stable + * features we should move the functionality into a different location. + */ +internal data class BetaFeatures( + + @SerializedName("lb") + internal var activityLifecycleBreadcrumbs: List? = null, + + @SerializedName("ts") + internal var thermalStates: List? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobMessage.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobMessage.kt new file mode 100644 index 0000000000..b3d623a441 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobMessage.kt @@ -0,0 +1,24 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.comms.api.ApiClient + +internal data class BlobMessage( + @SerializedName("a") + val appInfo: AppInfo? = null, + + @SerializedName("bae") + val applicationExits: List = emptyList(), + + @SerializedName("d") + val deviceInfo: DeviceInfo? = null, + + @SerializedName("s") + val session: BlobSession? = null, + + @SerializedName("u") + val userInfo: UserInfo? = null, + + @SerializedName("v") + val version: Int = ApiClient.MESSAGE_VERSION +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobSession.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobSession.kt new file mode 100644 index 0000000000..3a9f8e6311 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/BlobSession.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class BlobSession( + @SerializedName("si") + val sessionId: String? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Breadcrumbs.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Breadcrumbs.kt new file mode 100644 index 0000000000..d538b99a02 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Breadcrumbs.kt @@ -0,0 +1,57 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Contains lists of [ViewBreadcrumb], [TapBreadcrumb], [CustomBreadcrumb], + * and [WebViewBreadcrumb] within a particular time window, created by the + * [EmbraceBreadcrumbService]. + * + * Breadcrumbs are used to track user journeys throughout the apps, such as transitions between + * screens or taps on particular UI elements. A developer can create a [CustomBreadcrumb] if + * they would like to label some particular event or interaction within their app on the timeline. + */ +internal data class Breadcrumbs( + + /** + * List of breadcrumbs which relate to views. + */ + @SerializedName("vb") + val viewBreadcrumbs: List? = null, + + /** + * List of breadcrumbs which relate to screen taps. + */ + @SerializedName("tb") + val tapBreadcrumbs: List? = null, + + /** + * List of custom breadcrumbs defined by the developer. + */ + @SerializedName("cb") + val customBreadcrumbs: List? = null, + + /** + * List of webview breadcrumbs. + */ + @SerializedName("wv") + val webViewBreadcrumbs: List? = null, + + /** + * List of fragment (custom view) breadcrumbs. + */ + @SerializedName("cv") + val fragmentBreadcrumbs: List? = null, + + /** + * List of RN Action breadcrumbs. + */ + @SerializedName("rna") + val rnActionBreadcrumbs: List? = null, + + /** + * List of captured push notifications + */ + @SerializedName("pn") + val pushNotifications: List? = null, +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Crash.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Crash.kt new file mode 100644 index 0000000000..eeaf296d47 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Crash.kt @@ -0,0 +1,94 @@ +package io.embrace.android.embracesdk.payload + +import android.util.Base64 +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logError +import io.embrace.android.embracesdk.payload.ExceptionInfo.Companion.ofThrowable +import io.embrace.android.embracesdk.payload.ThreadInfo.Companion.ofThread + +internal data class Crash( + @SerializedName("id") + @JvmField + val crashId: String, + + @SerializedName("ex") + val exceptions: List? = null, + + @SerializedName("rep_js") + val jsExceptions: List? = null, + + @SerializedName("th") + val threads: List? = null +) { + + companion object { + private val serializer = EmbraceSerializer() + + /** + * Creates a crash from a {@link Throwable}. Extracts each cause and converts it to + * {@link ExceptionInfo}. Optionally includes a {@link JsException}. + * + * @param throwable the throwable to parse + * @param jsException an optional JS exception that is associated with the crash + * @param crashId an optional crash unique id + * @return a crash + */ + fun ofThrowable( + throwable: Throwable?, + jsException: JsException?, + crashId: String = Uuid.getEmbUuid() + ): Crash { + return Crash( + crashId, + exceptionInfo(throwable), + jsExceptions(jsException), + threadsInfo() + ) + } + + /** + * @param ex the throwable to parse + * @return a list of [ExceptionInfo] elements of the throwable. + */ + @JvmStatic + private fun exceptionInfo(ex: Throwable?): List { + val result = mutableListOf() + var throwable: Throwable? = ex + while (throwable != null && throwable != throwable.cause) { + val exceptionInfo = ofThrowable(throwable) + result.add(0, exceptionInfo) + throwable = throwable.cause + } + return result.toList() + } + + /** + * @return a list of [ThreadInfo] elements of the current thread list. + */ + @JvmStatic + private fun threadsInfo(): List { + return Thread.getAllStackTraces().map { ofThread(it.key, it.value) } + } + + /** + * @param jsException the [JsException] coming from the React Native layer. + * @return a list of [String] representing the javascript stacktrace of the crash. + */ + @JvmStatic + private fun jsExceptions(jsException: JsException?): List? { + var jsExceptions: List? = null + if (jsException != null) { + try { + val jsonException = serializer.toJson(jsException, jsException.javaClass).toByteArray() + val encodedString = Base64.encodeToString(jsonException, Base64.NO_WRAP) + jsExceptions = listOf(encodedString) + } catch (ex: Exception) { + logError("Failed to parse javascript exception", ex, true) + } + } + return jsExceptions + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/CustomBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/CustomBreadcrumb.kt new file mode 100644 index 0000000000..e1e825c057 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/CustomBreadcrumb.kt @@ -0,0 +1,73 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.capture.crumbs.Breadcrumb + +/** + * Custom breadcrumbs can be created to reflect events into the user timeline. + * The max number of characters for this breadcrumb message is + * [CustomBreadcrumb.BREADCRUMB_MESSAGE_MAX_LENGTH] + */ +internal class CustomBreadcrumb( + message: String?, + + /** + * The timestamp at which the event occurred. + */ + @SerializedName("ts") private val timestamp: Long +) : Breadcrumb { + + /** + * Message for the custom breadcrumb event. + * If the message exceeds the [CustomBreadcrumb.BREADCRUMB_MESSAGE_MAX_LENGTH] characters + * it will be ellipsized. + */ + @SerializedName("m") + val message: String? + + init { + this.message = ellipsizeBreadcrumbMessage(message) + } + + override fun getStartTime(): Long = timestamp + + private fun ellipsizeBreadcrumbMessage(input: String?): String? { + return if (input == null || input.length < BREADCRUMB_MESSAGE_MAX_LENGTH) { + input + } else { + input.substring( + 0, + BREADCRUMB_MESSAGE_MAX_LENGTH - 3 + ) + "..." + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (javaClass != other?.javaClass) { + return false + } + + other as CustomBreadcrumb + + if (timestamp != other.timestamp) { + return false + } + if (message != other.message) { + return false + } + return true + } + + override fun hashCode(): Int { + var result = timestamp.hashCode() + result = 31 * result + (message?.hashCode() ?: 0) + return result + } + + companion object { + private const val BREADCRUMB_MESSAGE_MAX_LENGTH = 256 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DeviceInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DeviceInfo.kt new file mode 100644 index 0000000000..831e3a1cdc --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DeviceInfo.kt @@ -0,0 +1,71 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.internal.utils.MessageUtils + +internal data class DeviceInfo( + + @SerializedName("dm") + val manufacturer: String? = null, + + @SerializedName("do") + val model: String? = null, + + @SerializedName("da") + val architecture: String? = null, + + @SerializedName("jb") + val jailbroken: Boolean? = null, + + @SerializedName("lc") + val locale: String? = null, + + @SerializedName("ms") + val internalStorageTotalCapacity: Long? = null, + + @SerializedName("os") + val operatingSystemType: String? = null, + + @SerializedName("ov") + val operatingSystemVersion: String? = null, + + @SerializedName("oc") + val operatingSystemVersionCode: Int? = null, + + @SerializedName("sr") + val screenResolution: String? = null, + + @SerializedName("tz") + val timezoneDescription: String? = null, + + @SerializedName("up") + val uptime: Long? = null, + + @SerializedName("nc") + val cores: Int? = null, + + @SerializedName("pt") + val cpuName: String? = null, + + @SerializedName("gp") + private val egl: String? = null +) { + + fun toJson(): String { + return "{\"dm\": " + MessageUtils.withNull(manufacturer) + + ",\"do\": " + MessageUtils.withNull(model) + + ",\"da\":" + MessageUtils.withNull(architecture) + + ",\"jb\":" + MessageUtils.boolToStr(jailbroken) + + ",\"lc\":" + MessageUtils.withNull(locale) + + ",\"ms\":" + MessageUtils.withNull(internalStorageTotalCapacity) + + ",\"os\":" + MessageUtils.withNull(operatingSystemType) + + ",\"ov\":" + MessageUtils.withNull(operatingSystemVersion) + + ",\"oc\":" + MessageUtils.withNull(operatingSystemVersionCode) + + ",\"sr\":" + MessageUtils.withNull(screenResolution) + + ",\"tz\":" + MessageUtils.withNull(timezoneDescription) + + ",\"up\":" + MessageUtils.withNull(uptime) + + ",\"nc\":" + MessageUtils.withNull(cores) + + ",\"pt\":" + MessageUtils.withNull(cpuName) + + ",\"gp\":" + MessageUtils.withNull(egl) + "}" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DiskUsage.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DiskUsage.kt new file mode 100644 index 0000000000..06fd7dec2d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/DiskUsage.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Disk space used by the app and available memory on the device. + */ +internal data class DiskUsage( + + /** + * Amount of disk space consumed by the app in bytes. + */ + @SerializedName("as") + val appDiskUsage: Long?, + + /** + * Amount of disk space free on the device in bytes. + */ + @SerializedName("fs") + val deviceDiskFree: Long? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Event.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Event.kt new file mode 100644 index 0000000000..4a81d4f488 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Event.kt @@ -0,0 +1,79 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.EmbraceEvent.Type + +internal data class Event constructor( + @SerializedName("n") + @JvmField + val name: String? = null, + + @SerializedName("li") + @JvmField + val messageId: String? = null, + + @SerializedName("id") + @JvmField + val eventId: String? = null, + + @SerializedName("si") + @JvmField + val sessionId: String? = null, + + @SerializedName("t") + @JvmField + val type: Type? = null, + + @SerializedName("ts") + @JvmField + val timestamp: Long? = null, + + @SerializedName("th") + @JvmField + val lateThreshold: Long? = null, + + @SerializedName("sc") + @JvmField + val screenshotTaken: Boolean? = false, + + @SerializedName("du") + @JvmField + val duration: Long? = null, + + @SerializedName("st") + @JvmField + val appState: String? = null, + + @Transient + private val customProperties: Map? = null, + + @Transient + private val sessionProperties: Map? = null, + + @Transient + private val activeEventIdsList: List? = null, + + @SerializedName("et") + @JvmField + val logExceptionType: String? = null, + + @SerializedName("en") + val exceptionName: String? = null, + + @SerializedName("em") + val exceptionMessage: String? = null, + + @SerializedName("f") + @JvmField + val framework: Int? = null, +) { + + @SerializedName("pr") + val customPropertiesMap: Map? = customProperties?.toMutableMap() + + @SerializedName("sp") + val sessionPropertiesMap: Map? = sessionProperties?.toMutableMap() + + @Transient + val activeEventIds: List? = activeEventIdsList?.toMutableList() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/EventMessage.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/EventMessage.kt new file mode 100644 index 0000000000..6f61387fff --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/EventMessage.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.comms.api.ApiClient + +internal data class EventMessage( + @SerializedName("et") + val event: Event, + + @SerializedName("cr") + val crash: Crash? = null, + + @SerializedName("d") + val deviceInfo: DeviceInfo? = null, + + @SerializedName("a") + val appInfo: AppInfo? = null, + + @SerializedName("u") + val userInfo: UserInfo? = null, + + @SerializedName("p") + val performanceInfo: PerformanceInfo? = null, + + @SerializedName("sk") + val stacktraces: Stacktraces? = null, + + @SerializedName("v") + val version: Int = ApiClient.MESSAGE_VERSION, + + @SerializedName("crn") + val nativeCrash: NativeCrash? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionError.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionError.kt new file mode 100644 index 0000000000..dd7a5f0ccb --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionError.kt @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.payload + +import androidx.annotation.VisibleForTesting +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.clock.Clock + +/** + * Describes an Exception Error with a count of occurrences and a list of exceptions (causes). + */ +internal data class ExceptionError(@Transient private val logStrictMode: Boolean) { + @SerializedName("c") + @VisibleForTesting + var occurrences = 0 + + @SerializedName("rep") + @VisibleForTesting + val exceptionErrors = mutableListOf() + + /** + * Add a new exception error info if exceptionError's size is below 20. + * For each exceptions, occurrences is incremented by 1. + * + * @param ex the exception error. + * @param appState (foreground or background). + */ + fun addException(ex: Throwable?, appState: String?, clock: Clock) { + occurrences++ + var exceptionsLimits = DEFAULT_EXCEPTION_ERROR_LIMIT + if (logStrictMode) { + exceptionsLimits = DEFAULT_EXCEPTION_ERROR_LIMIT_STRICT_MODE + } + if (exceptionErrors.size < exceptionsLimits) { + exceptionErrors.add( + ExceptionErrorInfo( + clock.now(), + appState, + getExceptionInfo(ex) + ) + ) + } + } + + private fun getExceptionInfo(ex: Throwable?): List { + val result = mutableListOf() + var throwable: Throwable? = ex + while (throwable != null && throwable != throwable.cause) { + val exceptionInfo = ExceptionInfo.ofThrowable(throwable) + result.add(0, exceptionInfo) + throwable = throwable.cause + } + return result + } +} + +/** + * The occurrences list limit. + */ +private const val DEFAULT_EXCEPTION_ERROR_LIMIT = 5 +private const val DEFAULT_EXCEPTION_ERROR_LIMIT_STRICT_MODE = 50 diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionErrorInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionErrorInfo.kt new file mode 100644 index 0000000000..9d23f5b645 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionErrorInfo.kt @@ -0,0 +1,26 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Describes a particular Exception error. Where an exception error has a cause, there will be an + * {@link ExceptionErrorInfo} for each nested cause. + */ +internal data class ExceptionErrorInfo( + + /** + * Timestamp when exception error happened. + */ + @SerializedName("ts") val timestamp: Long? = null, + + /** + * App state (foreground or background). + */ + @SerializedName("s") val state: String? = null, + + /** + * A list of exceptions. + */ + @SerializedName("ex") val exceptions: List? = null + +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionInfo.kt new file mode 100644 index 0000000000..f7f2045923 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ExceptionInfo.kt @@ -0,0 +1,78 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Describes a particular Java exception. Where an exception has a cause, there will be an + * [ExceptionInfo] for each nested cause. + */ +internal class ExceptionInfo internal constructor( + + /** + * The name of the class throwing the exception. + */ + @SerializedName("n") val name: String, + + /** + * The exception message. + */ + @SerializedName("m") val message: String?, + + lines: List +) { + + /** + * String representation of each line of the stack trace. + */ + @SerializedName("tt") + val lines: List = lines.take(STACK_FRAME_LIMIT) + + /** + * The original length of the stack trace. This will be null if it has not been truncated. + */ + @SerializedName("length") + val originalLength: Int? = lines.size.takeIf { it > STACK_FRAME_LIMIT } + + companion object { + + /** + * Maximum number of stackframes we are interested in serializing. + */ + private const val STACK_FRAME_LIMIT = 200 + + /** + * Creates a [ExceptionInfo] from a [Throwable], using the classname as the name, + * the exception message as the message, and each stacktrace element as each line. + * + * @param throwable the exception + * @return the stacktrace instance + */ + @JvmStatic + fun ofThrowable(throwable: Throwable): ExceptionInfo { + val name = throwable.javaClass.name + val message = throwable.message ?: "" + val lines = throwable.stackTrace.map(StackTraceElement::toString) + return ExceptionInfo(name, message, lines) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ExceptionInfo + + if (name != other.name) return false + if (message != other.message) return false + if (lines != other.lines) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + (message?.hashCode() ?: 0) + result = 31 * result + lines.hashCode() + return result + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/FragmentBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/FragmentBreadcrumb.kt new file mode 100644 index 0000000000..17dceed345 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/FragmentBreadcrumb.kt @@ -0,0 +1,24 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.capture.crumbs.Breadcrumb + +/** + * Breadcrumb that represents a fragment that was viewed. + */ +internal class FragmentBreadcrumb( + @SerializedName("n") + val name: String, + + @SerializedName("st") + var start: Long, + + @SerializedName("en") + var endTime: Long +) : Breadcrumb { + override fun getStartTime(): Long = start + + fun setStartTime(startTime: Long) { + start = startTime + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Interval.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Interval.kt new file mode 100644 index 0000000000..4989654f83 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Interval.kt @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Represents a value over a particular interval. This is used for: + * + * * Periods during which the device was charging + * * Periods during which the device was connected to Wifi, WAN, or no network + * + */ +internal data class Interval @JvmOverloads constructor( + @SerializedName("st") val startTime: Long, + @SerializedName("en") val endTime: Long, + @SerializedName("v") val value: String? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/JsException.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/JsException.kt new file mode 100644 index 0000000000..e6bf181cbc --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/JsException.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal class JsException( + @SerializedName("n") var name: String?, + @SerializedName("m") var message: String?, + @SerializedName("t") var type: String?, + @SerializedName("st") var stacktrace: String? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/MemoryWarning.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/MemoryWarning.kt new file mode 100644 index 0000000000..fa0e06019f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/MemoryWarning.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * An occasion where the device reported that the memory is running low, due to a trim memory + * event being called in [EmbraceActivityService]. + * + * See: [https://developer.android.com/reference/android/content/ComponentCallbacks2.html.onTrimMemory] +) */ +internal data class MemoryWarning( + + /** + * The timestamp at which the memory trim event occurred. + */ + @field:SerializedName("ts") val timestamp: Long +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrash.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrash.kt new file mode 100644 index 0000000000..76e8ff7c39 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrash.kt @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal class NativeCrash( + @SerializedName("id") val id: String?, + @SerializedName("m") val crashMessage: String?, + @SerializedName("sb") val symbols: Map?, + @SerializedName("er") val errors: List?, + @SerializedName("ue") val unwindError: Int?, + @SerializedName("ma") val map: String? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashData.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashData.kt new file mode 100644 index 0000000000..23e4b0308f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashData.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal class NativeCrashData( + @SerializedName("report_id") val nativeCrashId: String, + @SerializedName("sid") val sessionId: String, + @SerializedName("ts") val timestamp: Long, + @SerializedName("state") val appState: String?, + @SerializedName("meta") val metadata: NativeCrashMetadata?, + @SerializedName("ue") val unwindError: Int?, + @SerializedName("crash") private val crash: String?, + @SerializedName("symbols") var symbols: Map?, + @SerializedName("errors") var errors: List?, + @SerializedName("map") var map: String? +) { + + fun getCrash() = NativeCrash(nativeCrashId, crash, symbols, errors, unwindError, map) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashDataError.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashDataError.kt new file mode 100644 index 0000000000..bf803489ea --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashDataError.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal class NativeCrashDataError( + @SerializedName("n") val number: Int?, + @SerializedName("c") val context: Int? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashMetadata.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashMetadata.kt new file mode 100644 index 0000000000..94c36001c4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeCrashMetadata.kt @@ -0,0 +1,26 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.internal.utils.MessageUtils.withMap + +internal class NativeCrashMetadata( + @SerializedName("a") val appInfo: AppInfo, + @SerializedName("d") val deviceInfo: DeviceInfo, + @SerializedName("u") val userInfo: UserInfo, + @SerializedName("sp") val sessionProperties: Map? +) { + + fun toJson(): String { + val sb = StringBuilder() + sb.append("{\"a\":") + sb.append(appInfo.toJson()) + sb.append(",\"d\":") + sb.append(deviceInfo.toJson()) + sb.append(",\"u\":") + sb.append(userInfo.toJson()) + sb.append(",\"sp\":") + sb.append(withMap(sessionProperties)) + sb.append("}") + return sb.toString() + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeSymbols.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeSymbols.kt new file mode 100644 index 0000000000..b1d138ad3a --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeSymbols.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal class NativeSymbols( + @SerializedName("symbols") + private val symbols: Map> +) { + + fun getSymbolByArchitecture(arch: String?): Map { + if (arch == null) { + return HashMap() + } + return when { + symbols.containsKey(arch) -> symbols[arch] + + // Uses arm-v7 symbols for arm64 if no symbols for amr64 found. + arch == ARM_64_NAME -> symbols[ARM_ABI_V7_NAME] + + // Uncommon 64 bits arch, uses x86 symbols for x86-64 if no symbols for x86-64 found. + arch == ARCH_X86_64_NAME -> symbols[ARCH_X86_NAME] + + else -> null + } ?: HashMap() + } + + companion object { + private const val ARM_ABI_V7_NAME = "armeabi-v7a" + private const val ARM_64_NAME = "arm64-v8a" + private const val ARCH_X86_NAME = "x86" + private const val ARCH_X86_64_NAME = "x86_64" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrInterval.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrInterval.kt new file mode 100644 index 0000000000..ebaeb18a86 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrInterval.kt @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig + +internal class NativeThreadAnrInterval( + + /** + * The JVM ID of the sampled thread + */ + @SerializedName("id") + internal val id: Long?, + + /** + * The JVM name of the sampled thread + */ + @SerializedName("n") + internal val name: String?, + + /** + * The priority of the sampled thread + */ + @SerializedName("p") + internal val priority: Int?, + + /** + * The offset in milliseconds that was used to take the native sample + */ + @SerializedName("os") + internal val sampleOffsetMs: Long?, + + /** + * The timestamp in milliseconds at which the monitored thread was first detected as blocked. + */ + @SerializedName("t") + internal val threadBlockedTimestamp: Long?, + + /** + * The stacktrace from the sampled thread. + */ + @SerializedName("ss") + internal val samples: MutableList?, + + state: ThreadState?, + unwinder: AnrRemoteConfig.Unwinder? +) { + + /** + * The stack unwinder used + */ + @SerializedName("uw") + internal val unwinder: Int? = unwinder?.code + + /** + * The JVM state of the sampled thread + */ + @SerializedName("s") + internal val state: Int? = state?.code +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrSample.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrSample.kt new file mode 100644 index 0000000000..6e7255b0ae --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrSample.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Holds data for a sample of a native stackframe. + * IMPORTANT: This class is referenced by stacktrace_sampler_jni.c. Move or rename both at the same time, or it will break. + */ +internal class NativeThreadAnrSample( + + /** + * A zero value indicates the sample was successful. A non-zero value indicates + * that something went wrong with the sample. Error codes match those defined in utilities.h. + * + * Depending on the error code, the stack might not be populated if the error condition is + * likely to increase the payload size. + */ + @SerializedName("r") + val result: Int?, + + /** + * The time in milliseconds since the thread was first detected as blocked + */ + @SerializedName("t") + val sampleTimestamp: Long?, + + /** + * How long the sample took in milliseconds. + */ + @SerializedName("d") + val sampleDurationMs: Long?, + + /** + * All the stackframes which have been captured during the current sample. + */ + @SerializedName("s") + val stackframes: List? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrStackframe.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrStackframe.kt new file mode 100644 index 0000000000..d7b48ebf97 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NativeThreadAnrStackframe.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Holds data for a sample of a native stacktrace. + * IMPORTANT: This class is referenced by stacktrace_sampler_jni.c. Move or rename both at the same time, or it will break. + */ +internal data class NativeThreadAnrStackframe( + + /** + * The program counter + */ + @SerializedName("pc") + internal val pc: String?, + + /** + * The hex load address of shared object. This information may not be available + * in which case the value will be 0x0. + */ + @SerializedName("l") + internal val soLoadAddr: String?, + + /** + * The absolute path of the shared object. This information may not be available + * in which case the string will be null. + */ + @SerializedName("p") + internal val soPath: String?, + + /** + * The result for unwinding this particular stackframe. Non-zero values indicate an error. + */ + @SerializedName("r") + internal val result: Int? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCallV2.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCallV2.kt new file mode 100644 index 0000000000..0dc91f71a0 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCallV2.kt @@ -0,0 +1,53 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class NetworkCallV2( + /** The URL being requested. */ + @SerializedName("url") + val url: String? = null, + + /** The HTTP method the network request corresponds to. */ + @SerializedName("x") + val httpMethod: String? = null, + + /** The HTTP response code. */ + @SerializedName("rc") + val responseCode: Int? = null, + + /** The number of bytes sent during the network request. */ + @SerializedName("bo") + val bytesSent: Long = 0, + + /** The number of bytes received during the network request. */ + @SerializedName("bi") + val bytesReceived: Long = 0, + + /** The start time of the request. */ + @SerializedName("st") + val startTime: Long = 0, + + /** The end time of the request. */ + @SerializedName("et") + val endTime: Long = 0, + + /** The duration of the network request. */ + @SerializedName("dur") + val duration: Long = 0, + + /** The trace ID that can be used to trace a particular request. */ + @SerializedName("t") + val traceId: String? = null, + + /** If an exception was thrown, the name of the class which caused the exception. */ + @SerializedName("ed") + val errorType: String? = null, + + /** If an exception was thrown, the exception message. */ + @SerializedName("de") + val errorMessage: String? = null, + + /** A Traceparent that is W3C compliant to be used to create a span for the this network request */ + @SerializedName("w3c_traceparent") + val w3cTraceparent: String? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCapturedCall.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCapturedCall.kt new file mode 100644 index 0000000000..abbca18fd4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkCapturedCall.kt @@ -0,0 +1,126 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import java.util.UUID + +internal data class NetworkCapturedCall( + /** + * The duration of the network request in milliseconds. + */ + @SerializedName("dur") + val duration: Long? = null, + + /** + * The end time of the request. + */ + @SerializedName("et") + val endTime: Long? = null, + + /** + * The HTTP method the network request corresponds to. + */ + @SerializedName("m") + val httpMethod: String? = null, + + /** + * The matched URL from the rule. + */ + @SerializedName("mu") + val matchedUrl: String? = null, + + /** + * UUID identifying the network request captured. + */ + @SerializedName("id") + val networkId: String = UUID.randomUUID().toString(), + + /** + * Request body. + */ + @SerializedName("qb") + val requestBody: String? = null, + + /** + * Captured request body size in bytes. + */ + @SerializedName("qi") + val requestBodySize: Int? = null, + + /** + * The query string for the request, if present. + */ + @SerializedName("qq") + val requestQuery: String? = null, + + /** + * A dictionary containing the HTTP query headers. + */ + @SerializedName("qh") + val requestQueryHeaders: Map? = null, + + /** + * Request body size in bytes. + */ + @SerializedName("qz") + val requestSize: Int? = null, + + /** + * Contents of the body in a network request. + */ + @SerializedName("sb") + val responseBody: String? = null, + + /** + * Captured response body size in bytes. + */ + @SerializedName("si") + val responseBodySize: Int? = null, + + /** + * A dictionary containing the HTTP response headers. + */ + @SerializedName("sh") + val responseHeaders: Map? = null, + + /** + * Response body size in bytes. + */ + @SerializedName("sz") + val responseSize: Int? = null, + + /** + * UUID identifying the network request captured. + */ + @SerializedName("sc") + val responseStatus: Int? = null, + + /** + * Session ID that the network request occurred during. + */ + @SerializedName("sid") + val sessionId: String? = null, + + /** + * The start time of the request. + */ + @SerializedName("st") + val startTime: Long? = null, + + /** + * The URL being requested. + */ + @SerializedName("url") + val url: String? = null, + + /** + * Error message in case the network call has failed. + */ + @SerializedName("em") + val errorMessage: String? = null, + + /** + * Encrypted data. + */ + @SerializedName("ne") + val encryptedPayload: String? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkEvent.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkEvent.kt new file mode 100644 index 0000000000..1cb31b99bf --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkEvent.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class NetworkEvent( + @SerializedName("app_id") + val appId: String, + + @SerializedName("a") + val appInfo: AppInfo, + + @SerializedName("device_id") + val deviceId: String, + + @SerializedName("id") + val eventId: String, + + @SerializedName("n") + val networkCaptureCall: NetworkCapturedCall, + + @SerializedName("ts") + val timestamp: String, + + @SerializedName("ip") + val ipAddress: String?, + + @SerializedName("si") + val sessionId: String? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkRequests.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkRequests.kt new file mode 100644 index 0000000000..9d124e3cbc --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkRequests.kt @@ -0,0 +1,7 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class NetworkRequests( + @SerializedName("v2") val networkSessionV2: NetworkSessionV2? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkSessionV2.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkSessionV2.kt new file mode 100644 index 0000000000..b3cc3f347b --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/NetworkSessionV2.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class NetworkSessionV2( + /** The list of network requests captured as part of the session. */ + @SerializedName("r") val requests: List, + /** Counts of network requests per domain, only for domains exceeding the capture limit. */ + @SerializedName("c") val requestCounts: Map +) { + /** + * Included in the payload when the network request capture limit has been exceeded for a + * particular domain. Specifies the limit, and the total count. + */ + internal data class DomainCount( + /** The total count of network calls for the given domain. */ + val requestCount: Int, + /** The configured request capture limit for the given domain. */ + val captureLimit: Int + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Orientation.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Orientation.kt new file mode 100644 index 0000000000..8ed2ab47e8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Orientation.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk.payload + +import android.content.res.Configuration +import com.google.gson.annotations.SerializedName + +internal data class Orientation( + @SerializedName("o") val orientation: String, + @SerializedName("ts") val timestamp: Long +) { + + constructor(orientation: Int, timestamp: Long) : this( + if (orientation == Configuration.ORIENTATION_LANDSCAPE) "l" else "p", + timestamp + ) + + val internalOrientation: Int + get() = if (orientation == "l") Configuration.ORIENTATION_LANDSCAPE else Configuration.ORIENTATION_PORTRAIT +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PerformanceInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PerformanceInfo.kt new file mode 100644 index 0000000000..af66bc7add --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PerformanceInfo.kt @@ -0,0 +1,78 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorStateInfo + +/** + * Describes information about how the device is performing. + */ +internal data class PerformanceInfo( + + /** + * Current disk space usage of the app, and free space on the device. + */ + @SerializedName("ds") + val diskUsage: DiskUsage? = null, + + /** + * Occasions where the device reported that the memory is running low. + */ + @SerializedName("mw") + val memoryWarnings: List? = null, + + /** + * Periods during which the device was connected to WIFI, WAN, or no network. + */ + @SerializedName("ns") + val networkInterfaceIntervals: List? = null, + + /** + * Periods during which the application was not responding (UI thread blocked for > 1 sec). + */ + @SerializedName("anr") + val anrIntervals: List? = null, + + /** + * Periods during which the application was not responding (UI thread blocked for > 1 sec), + * detected by the OS, not Embrace. This is what we call ANR Process Errors. + */ + @SerializedName("anr_pe") + val anrProcessErrors: List? = null, + + /** + * Timestamps where Google ANRs were triggered. + */ + @SerializedName("ga") + val googleAnrTimestamps: List? = null, + + /** + * ApplicationExitInfo + */ + @SerializedName("aei") + val appExitInfoData: List? = null, + + /** + * Native thread ANR samples + */ + @SerializedName("nst") + val nativeThreadAnrIntervals: List? = null, + + /** + * Periods of save power mode + * lp refers "low power" + */ + @SerializedName("lp") + val powerSaveModeIntervals: List? = null, + + /** + * Network requests that happened during the session + */ + @SerializedName("nr") + val networkRequests: NetworkRequests? = null, + + /** + * StrictMode violations captured during the session + */ + @SerializedName("v") + val strictmodeViolations: List? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PowerModeInterval.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PowerModeInterval.kt new file mode 100644 index 0000000000..1648c777b4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PowerModeInterval.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Represents a value over a particular interval. This is used for: + * + * * Periods during which the device was in power save mode + * + */ +internal data class PowerModeInterval @JvmOverloads constructor( + @SerializedName("st") val startTime: Long, + @SerializedName("en") val endTime: Long? = null +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PushNotificationBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PushNotificationBreadcrumb.kt new file mode 100644 index 0000000000..688d6fe487 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/PushNotificationBreadcrumb.kt @@ -0,0 +1,53 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.capture.crumbs.Breadcrumb + +internal data class PushNotificationBreadcrumb( + @SerializedName("ti") + val title: String?, + + @SerializedName("bd") + val body: String?, + + @SerializedName("tp") + val from: String?, + + @SerializedName("id") + internal val id: String?, + + @SerializedName("pt") + val priority: Int?, + + @SerializedName("te") + val type: String?, + + @SerializedName("ts") + private val timestamp: Long + +) : Breadcrumb { + + internal enum class NotificationType(val type: String) { + NOTIFICATION("notif"), + DATA("data"), + + // this is a notification + data + NOTIFICATION_AND_DATA("notif-data"), + UNKNOWN("unknown"); + + companion object Builder { + fun notificationTypeFor(hasData: Boolean, hasNotification: Boolean): NotificationType { + return when { + hasData && hasNotification -> NOTIFICATION_AND_DATA + hasData && !hasNotification -> DATA + !hasData && hasNotification -> NOTIFICATION + else -> UNKNOWN + } + } + } + } + + override fun getStartTime(): Long { + return timestamp + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumb.kt new file mode 100644 index 0000000000..13fc376085 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumb.kt @@ -0,0 +1,67 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.capture.crumbs.Breadcrumb +import java.util.Arrays + +/** + * Breadcrumb that represents the dispatched actions from your state managment. + */ +internal data class RnActionBreadcrumb( + + /** + * The action name + */ + @SerializedName("n") val name: String, + + /** + * The timestamp at which the action started. + */ + @SerializedName("st") private val startTime: Long, + + /** + * The timestamp at which the action ended. + */ + @SerializedName("en") val endTime: Long, + + /** + * This object is for extra properties / data that was not cover + * with the already defined properties + */ + @SerializedName("p") + val properties: Map?, + + /** + * The timestamp at which the action ended. + */ + @SerializedName("pz") var bytesSent: Int, + + /** + * The output message SUCCESS | FAIL | INCOMPLETE + */ + @SerializedName("o") val output: String +) : Breadcrumb { + + internal enum class RnOutputType { + SUCCESS, FAIL, INCOMPLETE + } + + override fun getStartTime(): Long = startTime + + companion object { + + fun getValidRnBreadcrumbOutputName(): String = Arrays.toString(RnOutputType.values()) + + /** + * Method that validate the output is valid + */ + fun validateRnBreadcrumbOutputName(output: String): Boolean { + for (rnOutput in RnOutputType.values()) { + if (rnOutput.name.equals(output, ignoreCase = true)) { + return true + } + } + return false + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Session.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Session.kt new file mode 100644 index 0000000000..261589c1fe --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Session.kt @@ -0,0 +1,168 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.InternalApi +import io.embrace.android.embracesdk.session.EmbraceSessionService +import io.embrace.android.embracesdk.session.MESSAGE_TYPE_START + +/** + * Represents a particular user's session within the app. + */ +@InternalApi +internal data class Session @JvmOverloads internal constructor( + + /** + * A unique ID which identifies the session. + */ + @SerializedName("id") + val sessionId: String, + + /** + * The time that the session started. + */ + @SerializedName("st") + val startTime: Long, + + /** + * The ordinal of the session, starting from 1. + */ + @SerializedName("sn") + val number: Int, + + /** + * Type of the session message (start or end) + */ + @SerializedName("ty") + val messageType: String, + + /** + * Application state for this session (foreground or background) + */ + @SerializedName("as") + val appState: String, + + @SerializedName("cs") + val isColdStart: Boolean, + + /** + * The time that the session ended. + */ + @SerializedName("et") + val endTime: Long? = null, + + @SerializedName("ht") + val lastHeartbeatTime: Long? = null, + + @SerializedName("tt") + val terminationTime: Long? = null, + + @SerializedName("ce") + val isEndedCleanly: Boolean? = null, + + @SerializedName("tr") + val isReceivedTermination: Boolean? = null, + + @SerializedName("ss") + val eventIds: List? = null, + + @SerializedName("il") + val infoLogIds: List? = null, + + @SerializedName("wl") + val warningLogIds: List? = null, + + @SerializedName("el") + val errorLogIds: List? = null, + + @SerializedName("nc") + val networkLogIds: List? = null, + + @SerializedName("lic") + val infoLogsAttemptedToSend: Int? = null, + + @SerializedName("lwc") + val warnLogsAttemptedToSend: Int? = null, + + @SerializedName("lec") + val errorLogsAttemptedToSend: Int? = null, + + @SerializedName("e") + val exceptionError: ExceptionError? = null, + + @SerializedName("ri") + val crashReportId: String? = null, + + @SerializedName("em") + val endType: SessionLifeEventType? = null, + + @SerializedName("sm") + val startType: SessionLifeEventType? = null, + + @SerializedName("oc") + val orientations: List? = null, + + @SerializedName("sp") + val properties: Map? = null, + + @SerializedName("sd") + val startupDuration: Long? = null, + + @SerializedName("sdt") + val startupThreshold: Long? = null, + + @SerializedName("si") + val sdkStartupDuration: Long? = null, + + @SerializedName("ue") + val unhandledExceptions: Int? = null, + + /** + * Beta feature data that was captured during this session + */ + @SerializedName("bf") + val betaFeatures: BetaFeatures? = null, + + @SerializedName("sb") + val symbols: Map? = null, + + @SerializedName("wvi_beta") + val webViewInfo: List? = null, + + @Transient + val user: UserInfo? = null +) { + + /** + * Enum to discriminate the different ways a session can start / end + */ + enum class SessionLifeEventType { + @SerializedName("s") + STATE, @SerializedName("m") + MANUAL, @SerializedName("t") + TIMED + } + + companion object { + + @JvmStatic + fun buildStartSession( + id: String, + coldStart: Boolean, + startType: SessionLifeEventType, + startTime: Long, + sessionNumber: Int, + userInfo: UserInfo?, + sessionProperties: Map + ): Session = Session( + sessionId = id, + startTime = startTime, + number = sessionNumber, + appState = EmbraceSessionService.APPLICATION_STATE_FOREGROUND, + isColdStart = coldStart, + startType = startType, + properties = sessionProperties, + messageType = MESSAGE_TYPE_START, + user = userInfo + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/SessionMessage.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/SessionMessage.kt new file mode 100644 index 0000000000..c5ca25b7e6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/SessionMessage.kt @@ -0,0 +1,54 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData + +/** + * The session message, containing the session itself, as well as performance information about the + * device which occurred during the session. + */ +internal data class SessionMessage @JvmOverloads internal constructor( + + /** + * The session information. + */ + @SerializedName("s") + val session: Session, + + /** + * The user information. + */ + @SerializedName("u") + val userInfo: UserInfo? = null, + + /** + * The app information. + */ + @SerializedName("a") + val appInfo: AppInfo? = null, + + /** + * The device information. + */ + @SerializedName("d") + val deviceInfo: DeviceInfo? = null, + + /** + * The device's performance info, such as power, cpu, network. + */ + @SerializedName("p") + val performanceInfo: PerformanceInfo? = null, + + /** + * Breadcrumbs which occurred as part of this session. + */ + @SerializedName("br") + val breadcrumbs: Breadcrumbs? = null, + + @SerializedName("spans") + val spans: List? = null, + + @SerializedName("v") + val version: Int = ApiClient.MESSAGE_VERSION +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Stacktraces.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Stacktraces.kt new file mode 100644 index 0000000000..63a4338027 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/Stacktraces.kt @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.payload + +import androidx.annotation.VisibleForTesting +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.Embrace.AppFramework +import io.embrace.android.embracesdk.Embrace.AppFramework.FLUTTER +import io.embrace.android.embracesdk.Embrace.AppFramework.REACT_NATIVE +import io.embrace.android.embracesdk.Embrace.AppFramework.UNITY + +internal class Stacktraces @JvmOverloads constructor( + stacktraces: List?, + customStacktrace: String?, + framework: AppFramework, + + @SerializedName("c") + @get:VisibleForTesting + val context: String? = null, + + @SerializedName("l") + @get:VisibleForTesting + val library: String? = null +) { + + @SerializedName("tt") + @VisibleForTesting + val jvmStacktrace: List? + + @SerializedName("jsk") + @VisibleForTesting + val javascriptStacktrace: String? + + @SerializedName("u") + @VisibleForTesting + val unityStacktrace: String? + + @SerializedName("f") + @VisibleForTesting + val flutterStacktrace: String? + + init { + javascriptStacktrace = when (framework) { + REACT_NATIVE -> customStacktrace + else -> null + } + unityStacktrace = when (framework) { + UNITY -> customStacktrace + else -> null + } + flutterStacktrace = when (framework) { + FLUTTER -> customStacktrace + else -> null + } + + this.jvmStacktrace = when (customStacktrace) { + null -> stacktraces + else -> null + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/StrictModeViolation.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/StrictModeViolation.kt new file mode 100644 index 0000000000..c8b82cf4ae --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/StrictModeViolation.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Represents information about a StrictMode violation that was captured on the device + */ +internal data class StrictModeViolation( + + /** + * Information about the StrictMode violation + */ + @SerializedName("n") val exceptionInfo: ExceptionInfo, + + /** + * Timestamp in milliseconds at which the strictmode violation was captured + */ + @SerializedName("ts") val timestamp: Long? +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/TapBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/TapBreadcrumb.kt new file mode 100644 index 0000000000..e9e9b1f45d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/TapBreadcrumb.kt @@ -0,0 +1,57 @@ +package io.embrace.android.embracesdk.payload + +import android.util.Pair +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.capture.crumbs.Breadcrumb +import io.embrace.android.embracesdk.payload.TapBreadcrumb.TapBreadcrumbType + +/** + * Breadcrumbs that represent tap events. + */ +internal class TapBreadcrumb( + point: Pair?, + + /** + * Name of the tapped element. + */ + @SerializedName("tt") + val tappedElementName: String?, + + /** + * The timestamp at which the event occurred. + */ + @SerializedName("ts") + private val timestamp: Long, + + /** + * Type of TapBreadcrumb that categorizes the kind interaction, based on + * [TapBreadcrumbType] types. + */ + @SerializedName("t") + val type: TapBreadcrumbType? +) : Breadcrumb { + + /** + * Screen position (coordinates) of the tapped element. + */ + @SerializedName("tl") + var location: String? = null + + init { + location = if (point != null) { + val first = point.first?.toInt()?.toFloat() ?: 0.0f + val second = point.second?.toInt()?.toFloat() ?: 0.0f + first.toInt().toString() + "," + second.toInt() + } else { + "0,0" + } + } + + override fun getStartTime(): Long = timestamp + + internal enum class TapBreadcrumbType { + @SerializedName("s") + TAP, @SerializedName("l") + LONG_PRESS + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThermalState.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThermalState.kt new file mode 100644 index 0000000000..7c9ed92ab1 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThermalState.kt @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class ThermalState( + + @SerializedName("t") + internal val timestamp: Long, + + @SerializedName("s") + internal val status: Int +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadInfo.kt new file mode 100644 index 0000000000..1a9253114e --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadInfo.kt @@ -0,0 +1,70 @@ +package io.embrace.android.embracesdk.payload + +import androidx.annotation.VisibleForTesting +import com.google.gson.annotations.SerializedName + +/** + * Represents thread information at a given point in time. + */ +internal data class ThreadInfo @VisibleForTesting internal constructor( + + /** + * The thread ID + */ + val threadId: Long, + + /** + * Thread state when the ANR is happening [Thread.State] + */ + val state: Thread.State?, + + /** + * The name of the thread. + */ + @SerializedName("n") + val name: String?, + + /** + * The priority of the thread + */ + @SerializedName("p") + val priority: Int, + + /** + * String representation of each line of the stack trace. + */ + @SerializedName("tt") + val lines: List? +) { + + companion object { + + /** + * Creates a [ThreadInfo] from the [Thread], [StackTraceElement][] pair, + * using the thread name and priority, and each stacktrace element as each line with a limited length. + * + * @param thread the exception + * @param maxStacktraceSize the maximum lines of a stacktrace + * @return the stacktrace instance + */ + /** + * Creates a [ThreadInfo] from the [Thread], [StackTraceElement][] pair, + * using the thread name and priority, and each stacktrace element as each line. + * + * @param thread the exception + * @return the stacktrace instance + */ + @JvmStatic + @JvmOverloads + fun ofThread( + thread: Thread, + stackTraceElements: Array, + maxStacktraceSize: Int = Integer.MAX_VALUE + ): ThreadInfo { + val name = thread.name + val priority = thread.priority + val lines = stackTraceElements.take(maxStacktraceSize).map(StackTraceElement::toString) + return ThreadInfo(thread.id, thread.state, name, priority, lines) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadState.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadState.kt new file mode 100644 index 0000000000..317904a8d9 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ThreadState.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk.payload + +import java.lang.Thread.State + +internal fun mapThreadState(state: State) = + when (state) { + State.NEW -> ThreadState.NEW + State.RUNNABLE -> ThreadState.RUNNABLE + State.BLOCKED -> ThreadState.BLOCKED + State.WAITING -> ThreadState.WAITING + State.TIMED_WAITING -> ThreadState.TIMED_WAITING + State.TERMINATED -> ThreadState.TERMINATED + } + +internal enum class ThreadState(internal val code: Int) { + NEW(0), + RUNNABLE(1), + BLOCKED(2), + WAITING(3), + TIMED_WAITING(4), + TERMINATED(5); +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/UserInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/UserInfo.kt new file mode 100644 index 0000000000..3a86506166 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/UserInfo.kt @@ -0,0 +1,69 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.internal.utils.MessageUtils +import io.embrace.android.embracesdk.prefs.PreferencesService + +/** + * Information about the user of the app, provided by the developer performing the integration. + */ +internal data class UserInfo( + + @SerializedName("id") + var userId: String? = null, + + @SerializedName("em") + var email: String? = null, + + @SerializedName("un") + var username: String? = null, + + @SerializedName("per") + var personas: Set? = null +) { + + fun toJson(): String { + return "{\"id\": " + MessageUtils.withNull(userId) + + ",\"em\": " + MessageUtils.withNull(email) + + ",\"un\":" + MessageUtils.withNull(username) + + ",\"per\":" + MessageUtils.withSet(personas) + "}" + } + + companion object { + const val PERSONA_NEW_USER = "new_user" + const val PERSONA_POWER_USER = "power_user" + const val PERSONA_LOGGED_IN = "logged_in" + const val PERSONA_VIP = "vip" + const val PERSONA_CREATOR = "content_creator" + const val PERSONA_TESTER = "tester" + const val PERSONA_PAYER = "payer" + const val PERSONA_FIRST_DAY_USER = "first_day" + + /** + * Creates an instance of [UserInfo] from the cache. + * + * @param preferencesService the preferences service + * @return user info created from the cache and configuration + */ + @JvmStatic + fun ofStored(preferencesService: PreferencesService): UserInfo { + val id = preferencesService.userIdentifier + val name = preferencesService.username + val email = preferencesService.userEmailAddress + val personas: MutableSet = HashSet() + preferencesService.userPersonas?.let(personas::addAll) + @Suppress("DEPRECATION") // still need to store it, event thought it's deprecated.. + preferencesService.customPersonas?.let(personas::addAll) + + personas.remove(PERSONA_PAYER) + if (preferencesService.userPayer) { + personas.add(PERSONA_PAYER) + } + personas.remove(PERSONA_FIRST_DAY_USER) + if (preferencesService.isUsersFirstDay()) { + personas.add(PERSONA_FIRST_DAY_USER) + } + return UserInfo(id, email, name, personas) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ViewBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ViewBreadcrumb.kt new file mode 100644 index 0000000000..88d462c1ab --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/ViewBreadcrumb.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.capture.crumbs.Breadcrumb + +/** + * Breadcrumb that represents the display event for a View. + */ +internal class ViewBreadcrumb( + /** + * The screen name for the view breadcrumb. + */ + screen: String?, + + /** + * The timestamp at which the view started. + */ + @SerializedName("st") + val start: Long?, + + /** + * The timestamp at which the view ended. + */ + @SerializedName("en") + var end: Long? = null +) : Breadcrumb { + + @SerializedName("vn") + val screen: String + + init { + this.screen = screen ?: FALLBACK_SCREEN_NAME + } + + override fun getStartTime(): Long = start ?: 0 + + companion object { + private const val FALLBACK_SCREEN_NAME = "Unknown screen" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewBreadcrumb.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewBreadcrumb.kt new file mode 100644 index 0000000000..895e131ca2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewBreadcrumb.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.capture.crumbs.Breadcrumb + +/** + * Breadcrumb that represents the onPageStarted event for a WebView. + */ +internal class WebViewBreadcrumb( + @SerializedName("u") + val url: String, + + @SerializedName("st") + private val startTime: Long +) : Breadcrumb { + override fun getStartTime(): Long = startTime +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewInfo.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewInfo.kt new file mode 100644 index 0000000000..1f401921ff --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebViewInfo.kt @@ -0,0 +1,20 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +internal data class WebViewInfo( + @SerializedName("t") + val tag: String, + + @SerializedName("vt") + val webVitals: MutableList = mutableListOf(), + + @SerializedName("u") + val url: String, + + @SerializedName("ts") + val startTime: Long, + + @Transient + val webVitalMap: MutableMap = hashMapOf() +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVital.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVital.kt new file mode 100644 index 0000000000..25499fde5f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVital.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.annotations.SerializedName + +/** + * Web Vitals are a set of performance metrics that measure and report on the speed and quality of web pages. + */ +internal data class WebVital( + + @SerializedName("t") + val type: WebVitalType, + + @SerializedName("n") + val name: String, + + @SerializedName("st") + val startTime: Long, + + @SerializedName("d") + val duration: Long, + + @SerializedName("p") + val properties: Map, + + @SerializedName("s") + val score: Double +) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVitalType.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVitalType.kt new file mode 100644 index 0000000000..b5b08262fa --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/payload/WebVitalType.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.payload + +/** + * Web Core Vital type. + * + * FID = First Input Delay: Measures the delay between a user's interaction (such as tapping a button) and the browser's response. + * LCP = Largest Contentful Paint: Measures the time it takes for the largest content element to become visible to the user. + * CLS = Cumulative Layout Shift: Assesses the visual stability of the page by measuring unexpected layout shifts during loading. + * FCP = First Contentful Paint: Indicates the time it takes for the first content element to appear on the screen. + * + */ +internal enum class WebVitalType { + FID, LCP, CLS, FCP +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesService.kt new file mode 100644 index 0000000000..4d65216cfe --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesService.kt @@ -0,0 +1,360 @@ +package io.embrace.android.embracesdk.prefs + +import android.content.SharedPreferences +import com.google.gson.reflect.TypeToken +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.internal.utils.Uuid.getEmbUuid +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.session.ActivityListener +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future + +internal class EmbracePreferencesService( + registrationExecutorService: ExecutorService, + lazyPrefs: Lazy, + private val clock: Clock, + private val serializer: EmbraceSerializer +) : PreferencesService, ActivityListener { + + private val preferences: Future + private val registrationExecutorService: ExecutorService + private val lazyPrefs: Lazy + + init { + this.registrationExecutorService = registrationExecutorService + this.lazyPrefs = lazyPrefs + + // We get SharedPreferences on a background thread because it loads data from disk + // and can block. When client code needs to set/get a preference, getSharedPrefs() will + // block if necessary with Future.get(). Eagerly offloading buys us more time + // for SharedPreferences to load the File and reduces the likelihood of blocking + // when invoked by client code. + preferences = registrationExecutorService.submit(lazyPrefs::value) + alterStartupStatus(SDK_STARTUP_IN_PROGRESS) + } + + override fun applicationStartupComplete() = alterStartupStatus(SDK_STARTUP_COMPLETED) + + private fun alterStartupStatus(status: String) { + registrationExecutorService.submit( + Callable { + logDeveloper("EmbracePreferencesService", "Startup key: $status") + prefs.setStringPreference(SDK_STARTUP_STATUS_KEY, status) + null + } + ) + } + + // fallback from this very unlikely case by just loading on the main thread + private val prefs: SharedPreferences + get() = try { + preferences.get() + } catch (exc: Throwable) { + // fallback from this very unlikely case by just loading on the main thread + lazyPrefs.value + } + + private fun SharedPreferences.getStringPreference(key: String): String? { + return getString(key, null) + } + + private fun SharedPreferences.setStringPreference(key: String, value: String?) { + logDeveloper("EmbracePreferencesService", "Set $key: ${value ?: ""}") + val editor = edit() + editor.putString(key, value) + editor.apply() + } + + private fun SharedPreferences.getLongPreference(key: String): Long? { + val defaultValue: Long = -1L + return when (val value = getLong(key, defaultValue)) { + defaultValue -> null + else -> value + } + } + + private fun SharedPreferences.setLongPreference(key: String, value: Long?) { + logDeveloper("EmbracePreferencesService", "Set $key: ${value ?: ""}") + + if (value != null) { + val editor = edit() + editor.putLong(key, value) + editor.apply() + } + } + + private fun SharedPreferences.getIntegerPreference(key: String): Int? { + val defaultValue: Int = -1 + return when (val value = getInt(key, defaultValue)) { + defaultValue -> null + else -> value + } + } + + private fun SharedPreferences.setIntegerPreference(key: String, value: Int) { + logDeveloper("EmbracePreferencesService", "Set $key: $value") + val editor = edit() + editor.putInt(key, value) + editor.apply() + } + + private fun SharedPreferences.getBooleanPreference( + key: String, + defaultValue: Boolean + ): Boolean { + return getBoolean(key, defaultValue) + } + + private fun SharedPreferences.setBooleanPreference( + key: String, + value: Boolean? + ) { + logDeveloper("EmbracePreferencesService", "Set $key: ${value ?: ""}") + if (value != null) { + val editor = edit() + editor.putBoolean(key, value) + editor.apply() + } + } + + private fun SharedPreferences.setArrayPreference( + key: String, + value: Set? + ) { + logDeveloper("EmbracePreferencesService", "Set $key: ${value ?: ""}") + val editor = edit() + editor.putStringSet(key, value) + editor.apply() + } + + private fun SharedPreferences.getArrayPreference(key: String): Set? { + return getStringSet(key, null) + } + + private fun SharedPreferences.setMapPreference( + key: String, + value: Map? + ) { + logDeveloper("EmbracePreferencesService", "Set $key: ${value ?: ""}") + val editor = edit() + val mapString = when (value) { + null -> null + else -> serializer.toJson(value) + } + editor.putString(key, mapString) + editor.apply() + } + + private fun SharedPreferences.getMapPreference( + key: String + ): Map? { + val mapString = getString(key, null) ?: return null + val type = object : TypeToken?>() {}.type + return serializer.fromJson>(mapString, type) + } + + override var appVersion: String? + get() = prefs.getStringPreference(PREVIOUS_APP_VERSION_KEY) + set(value) = prefs.setStringPreference(PREVIOUS_APP_VERSION_KEY, value) + + override var osVersion: String? + get() = prefs.getStringPreference(PREVIOUS_OS_VERSION_KEY) + set(value) = prefs.setStringPreference(PREVIOUS_OS_VERSION_KEY, value) + + override var installDate: Long? + get() = prefs.getLongPreference(INSTALL_DATE_KEY) + set(value) = prefs.setLongPreference(INSTALL_DATE_KEY, value) + + override var deviceIdentifier: String + get() { + val deviceId = prefs.getStringPreference(DEVICE_IDENTIFIER_KEY) + if (deviceId != null) { + return deviceId + } + val newId = getEmbUuid() + logDeveloper( + "EmbracePreferencesService", + "Device ID is null, created new one: $newId" + ) + deviceIdentifier = newId + return newId + } + set(value) = prefs.setStringPreference(DEVICE_IDENTIFIER_KEY, value) + + override val sdkStartupStatus: String? + get() = prefs.getStringPreference(SDK_STARTUP_STATUS_KEY) + + override var sdkDisabled: Boolean + get() = prefs.getBooleanPreference(SDK_DISABLED_KEY, false) + set(value) = prefs.setBooleanPreference(SDK_DISABLED_KEY, value) + + override var userPayer: Boolean + get() = prefs.getBooleanPreference(USER_IS_PAYER_KEY, false) + set(value) = prefs.setBooleanPreference(USER_IS_PAYER_KEY, value) + + override var userIdentifier: String? + get() = prefs.getStringPreference(USER_IDENTIFIER_KEY) + set(value) = prefs.setStringPreference(USER_IDENTIFIER_KEY, value) + + override var userEmailAddress: String? + get() = prefs.getStringPreference(USER_EMAIL_ADDRESS_KEY) + set(value) = prefs.setStringPreference(USER_EMAIL_ADDRESS_KEY, value) + + override var userPersonas: Set? + get() = prefs.getArrayPreference(USER_PERSONAS_KEY) + set(value) = prefs.setArrayPreference(USER_PERSONAS_KEY, value) + + override var permanentSessionProperties: Map? + get() = prefs.getMapPreference(SESSION_PROPERTIES_KEY) + set(value) = prefs.setMapPreference(SESSION_PROPERTIES_KEY, value) + + @Deprecated("") + override val customPersonas: Set? + get() = prefs.getArrayPreference(CUSTOM_PERSONAS_KEY) + + override var username: String? + get() = prefs.getStringPreference(USER_USERNAME_KEY) + set(value) = prefs.setStringPreference(USER_USERNAME_KEY, value) + + override var lastConfigFetchDate: Long? + get() = prefs.getLongPreference(SDK_CONFIG_FETCHED_TIMESTAMP) + set(value) = prefs.setLongPreference(SDK_CONFIG_FETCHED_TIMESTAMP, value) + + override var userMessageNeedsRetry: Boolean + get() = prefs.getBooleanPreference(LAST_USER_MESSAGE_FAILED_KEY, false) + set(value) = prefs.setBooleanPreference(LAST_USER_MESSAGE_FAILED_KEY, value) + + override var sessionNumber: Int + get() = prefs.getIntegerPreference(LAST_SESSION_NUMBER_KEY) ?: 0 + set(value) = prefs.setIntegerPreference(LAST_SESSION_NUMBER_KEY, value) + + override var javaScriptBundleURL: String? + get() = prefs.getStringPreference(JAVA_SCRIPT_BUNDLE_URL_KEY) + set(value) = prefs.setStringPreference(JAVA_SCRIPT_BUNDLE_URL_KEY, value) + + override var rnSdkVersion: String? + get() = prefs.getStringPreference(REACT_NATIVE_SDK_VERSION_KEY) + set(value) = prefs.setStringPreference(REACT_NATIVE_SDK_VERSION_KEY, value) + + override var javaScriptPatchNumber: String? + get() = prefs.getStringPreference(JAVA_SCRIPT_PATCH_NUMBER_KEY) + set(value) = prefs.setStringPreference(JAVA_SCRIPT_PATCH_NUMBER_KEY, value) + + override var reactNativeVersionNumber: String? + get() = prefs.getStringPreference(REACT_NATIVE_VERSION_KEY) + set(value) = prefs.setStringPreference(REACT_NATIVE_VERSION_KEY, value) + + override var unityVersionNumber: String? + get() = prefs.getStringPreference(UNITY_VERSION_NUMBER_KEY) + set(value) = prefs.setStringPreference(UNITY_VERSION_NUMBER_KEY, value) + + override var unityBuildIdNumber: String? + get() = prefs.getStringPreference(UNITY_BUILD_ID_NUMBER_KEY) + set(value) = prefs.setStringPreference(UNITY_BUILD_ID_NUMBER_KEY, value) + + override var unitySdkVersionNumber: String? + get() = prefs.getStringPreference(UNITY_SDK_VERSION_NUMBER_KEY) + set(value) = prefs.setStringPreference(UNITY_SDK_VERSION_NUMBER_KEY, value) + + override var dartSdkVersion: String? + get() = prefs.getStringPreference(DART_SDK_VERSION_KEY) + set(value) = prefs.setStringPreference(DART_SDK_VERSION_KEY, value) + + override var embraceFlutterSdkVersion: String? + get() = prefs.getStringPreference(EMBRACE_FLUTTER_SDK_VERSION_KEY) + set(value) = prefs.setStringPreference(EMBRACE_FLUTTER_SDK_VERSION_KEY, value) + + override var jailbroken: Boolean? + get() = when { + !prefs.contains(IS_JAILBROKEN_KEY) -> null + else -> prefs.getBooleanPreference( + IS_JAILBROKEN_KEY, + false + ) + } + set(value) = prefs.setBooleanPreference(IS_JAILBROKEN_KEY, value) + + override var screenResolution: String? + get() = prefs.getStringPreference(SCREEN_RESOLUTION_KEY) + set(value) = prefs.setStringPreference(SCREEN_RESOLUTION_KEY, value) + + override var backgroundActivityEnabled: Boolean + get() = prefs.getBooleanPreference(BACKGROUND_ACTIVITY_ENABLED_KEY, false) + set(value) = prefs.setBooleanPreference(BACKGROUND_ACTIVITY_ENABLED_KEY, value) + + override var applicationExitInfoHistory: Set? + get() = prefs.getStringSet(AEI_HASH_CODES, null) + set(value) = prefs.setArrayPreference(AEI_HASH_CODES, value) + + override var cpuName: String? + get() = prefs.getStringPreference(CPU_NAME_KEY) + set(value) = prefs.setStringPreference(CPU_NAME_KEY, value) + + override var egl: String? + get() = prefs.getStringPreference(EGL_KEY) + set(value) = prefs.setStringPreference(EGL_KEY, value) + + override fun isUsersFirstDay(): Boolean { + val installDate = installDate + return installDate != null && clock.now() - installDate <= PreferencesService.DAY_IN_MS + } + + override fun isNetworkCaptureRuleOver(id: String): Boolean { + return getNetworkCaptureRuleRemainingCount(id) <= 0 + } + + override fun decreaseNetworkCaptureRuleRemainingCount(id: String, maxCount: Int) { + prefs.setIntegerPreference( + NETWORK_CAPTURE_RULE_PREFIX_KEY + id, + getNetworkCaptureRuleRemainingCount(id, maxCount) - 1 + ) + } + + private fun getNetworkCaptureRuleRemainingCount(id: String): Int { + return getNetworkCaptureRuleRemainingCount(id, 1) + } + + private fun getNetworkCaptureRuleRemainingCount(id: String, maxCount: Int): Int { + val value = prefs.getIntegerPreference(NETWORK_CAPTURE_RULE_PREFIX_KEY + id) + return value ?: maxCount + } + + companion object { + const val SDK_STARTUP_IN_PROGRESS = "startup_entered" + const val SDK_STARTUP_COMPLETED = "startup_completed" + private const val SDK_STARTUP_STATUS_KEY = "io.embrace.sdkstartup" + private const val DEVICE_IDENTIFIER_KEY = "io.embrace.deviceid" + private const val PREVIOUS_APP_VERSION_KEY = "io.embrace.lastappversion" + private const val PREVIOUS_OS_VERSION_KEY = "io.embrace.lastosversion" + private const val INSTALL_DATE_KEY = "io.embrace.installtimestamp" + private const val USER_IDENTIFIER_KEY = "io.embrace.userid" + private const val USER_EMAIL_ADDRESS_KEY = "io.embrace.useremail" + private const val USER_USERNAME_KEY = "io.embrace.username" + private const val USER_IS_PAYER_KEY = "io.embrace.userispayer" + private const val USER_PERSONAS_KEY = "io.embrace.userpersonas" + private const val CUSTOM_PERSONAS_KEY = "io.embrace.custompersonas" + private const val LAST_USER_MESSAGE_FAILED_KEY = "io.embrace.userupdatefailed" + private const val LAST_SESSION_NUMBER_KEY = "io.embrace.sessionnumber" + private const val JAVA_SCRIPT_BUNDLE_URL_KEY = "io.embrace.jsbundle.url" + private const val JAVA_SCRIPT_PATCH_NUMBER_KEY = "io.embrace.javascript.patch" + private const val REACT_NATIVE_VERSION_KEY = "io.embrace.reactnative.version" + private const val REACT_NATIVE_SDK_VERSION_KEY = "io.embrace.reactnative.sdk.version" + private const val SESSION_PROPERTIES_KEY = "io.embrace.session.properties" + private const val UNITY_VERSION_NUMBER_KEY = "io.embrace.unity.version" + private const val UNITY_BUILD_ID_NUMBER_KEY = "io.embrace.unity.build.id" + private const val UNITY_SDK_VERSION_NUMBER_KEY = "io.embrace.unity.sdk.version" + private const val DART_SDK_VERSION_KEY = "io.embrace.dart.sdk.version" + private const val EMBRACE_FLUTTER_SDK_VERSION_KEY = "io.embrace.flutter.sdk.version" + private const val IS_JAILBROKEN_KEY = "io.embrace.is_jailbroken" + private const val SCREEN_RESOLUTION_KEY = "io.embrace.screen.resolution" + private const val BACKGROUND_ACTIVITY_ENABLED_KEY = "io.embrace.bgactivitycapture" + private const val NETWORK_CAPTURE_RULE_PREFIX_KEY = "io.embrace.networkcapturerule" + private const val SDK_DISABLED_KEY = "io.embrace.disabled" + private const val SDK_CONFIG_FETCHED_TIMESTAMP = "io.embrace.sdkfetchedtimestamp" + private const val AEI_HASH_CODES = "io.embrace.aeiHashCode" + private const val CPU_NAME_KEY = "io.embrace.cpuName" + private const val EGL_KEY = "io.embrace.egl" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/PreferencesService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/PreferencesService.kt new file mode 100644 index 0000000000..74c9c877bd --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/prefs/PreferencesService.kt @@ -0,0 +1,186 @@ +package io.embrace.android.embracesdk.prefs + +internal interface PreferencesService { + + /** + * The last registered Host App version name + */ + var appVersion: String? + + /** + * The last registered OS Version + */ + var osVersion: String? + + /** + * The app install date in ms + */ + var installDate: Long? + + /** + * The unique identifier for this device. + */ + var deviceIdentifier: String + + /** + * The last SDK startup status registered. + */ + val sdkStartupStatus: String? + + /** + * If the sdk is disabled + */ + var sdkDisabled: Boolean + + /** + * If the user is payer + */ + var userPayer: Boolean + + /** + * User unique identifier + */ + var userIdentifier: String? + + /** + * User email address + */ + var userEmailAddress: String? + + /** + * Personas for the user + */ + var userPersonas: Set? + + /** + * All permanent session properties + */ + var permanentSessionProperties: Map? + + /** + * No longer used, will be removed in a future version. + * + * Method is still present to ensure that during any upgrades to SDK3, any custom + * personas are merged with the user personas list. + * + * @return custom personas + */ + @Deprecated("") + val customPersonas: Set? + + /** + * Username for the user + */ + var username: String? + + /** + * The last time config was fetched from the server + */ + var lastConfigFetchDate: Long? + + /** + * If the user message needs to retry send + */ + var userMessageNeedsRetry: Boolean + + /** + * Last session number. Increments by one. + */ + var sessionNumber: Int + + /** + * Last javaScript bundle string url. + */ + var javaScriptBundleURL: String? + + /** + * Embrace sdk version. + */ + var rnSdkVersion: String? + + /** + * Last javaScript patch string number. + */ + var javaScriptPatchNumber: String? + + /** + * Last react native version. + */ + var reactNativeVersionNumber: String? + + /** + * Last Unity version. + */ + var unityVersionNumber: String? + + /** + * Last Unity Build ID + */ + var unityBuildIdNumber: String? + + /** + * Last Unity SDK version + */ + var unitySdkVersionNumber: String? + + /** + * Last Flutter SDK version + */ + var embraceFlutterSdkVersion: String? + + /** + * Last Dart SDK version + */ + var dartSdkVersion: String? + + /** + * If the device is a rooted device. + */ + var jailbroken: Boolean? + + /** + * The device's screen resolution. + */ + var screenResolution: String? + + /** + * The device's cpu name. + */ + var cpuName: String? + + /** + * The device's egl. + */ + var egl: String? + + /** + * If background activity capture is enabled + */ + var backgroundActivityEnabled: Boolean + + /** + * Set of hashcodes derived from ApplicationExitInfo objects + */ + var applicationExitInfoHistory: Set? + + /** + * Whether or not the app was installed within the last 24 hours. + * + * @return true if it is the user's first day, false otherwise + */ + fun isUsersFirstDay(): Boolean + + /** + * Ssuffix to compose the key to get the stored value + */ + fun isNetworkCaptureRuleOver(id: String): Boolean + + /** + * Suffix to compose the key to get the stored value + */ + fun decreaseNetworkCaptureRuleRemainingCount(id: String, maxCount: Int) + + companion object { + const val DAY_IN_MS = 60 * 60 * 24 * 1000L + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/registry/ServiceRegistry.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/registry/ServiceRegistry.kt new file mode 100644 index 0000000000..b85eff0e09 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/registry/ServiceRegistry.kt @@ -0,0 +1,82 @@ +package io.embrace.android.embracesdk.registry + +import io.embrace.android.embracesdk.config.ConfigListener +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import io.embrace.android.embracesdk.session.MemoryCleanerService +import java.io.Closeable +import java.util.concurrent.atomic.AtomicBoolean + +/** + * An object that holds all of the services that are registered with the SDK. This makes it simpler + * to remember to set callbacks & close resources when creating a new service. + */ +internal class ServiceRegistry( + private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger +) : Closeable { + + private val registry = mutableListOf() + private var initialized = AtomicBoolean(false) + + // lazy init avoids type checks at startup until absolutely necessary. + // once these variables are initialized, no further services should be registered. + val closeables by lazy { registry.filterIsInstance() } + val configListeners by lazy { registry.filterIsInstance() } + val memoryCleanerListeners by lazy { registry.filterIsInstance() } + val activityListeners by lazy { registry.filterIsInstance() } + + fun registerServices(vararg services: Any?) { + services.forEach(::registerService) + } + + fun registerService(service: Any?) { + if (initialized.get()) { + error("Cannot register a service - already initialized.") + } + if (service == null) { + return + } + registry.add(service) + } + + fun closeRegistration() { + initialized.set(true) + } + + fun registerActivityListeners(activityService: ActivityService) = activityListeners.forEachSafe( + "Failed to register activity listener", + activityService::addListener + ) + + fun registerMemoryCleanerListeners(memoryCleanerService: MemoryCleanerService) = + memoryCleanerListeners.forEachSafe( + "Failed to register memory cleaner listener", + memoryCleanerService::addListener + ) + + /** + * Register all of the services in the registry that implement ConfigListener. + */ + fun registerConfigListeners(configService: ConfigService) = configListeners.forEachSafe( + "Failed to register config listener", + configService::addListener + ) + + // close all of the services in one go. this prevents someone creating a Closeable service + // but forgetting to close it. + override fun close() = closeables.forEachSafe("Failed to close service", Closeable::close) + + private fun List.forEachSafe(msg: String, action: (t: T) -> Unit) { + this.forEach { + try { + action(it) + } catch (exc: Throwable) { + logger.logError(msg, exc, true) + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationChecker.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationChecker.kt new file mode 100644 index 0000000000..bbc72fb306 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationChecker.kt @@ -0,0 +1,81 @@ +package io.embrace.android.embracesdk.samples + +import android.app.Activity +import com.google.gson.Gson +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import java.io.File +import java.io.FileNotFoundException + +internal class AutomaticVerificationChecker { + private val fileName = "emb_marker_file.txt" + private val verificationResult = VerificationResult() + private lateinit var file: File + private var gson = Gson() + + /** + * Returns true if the file was created, false if it already existed + */ + fun createFile(activity: Activity): Boolean { + val directory = activity.cacheDir.absolutePath + file = File("$directory/$fileName") + + return generateMarkerFile() + } + + /** + * Verifies if the marker file exists. It is used to determine whether to run the verification or no. + * If marker file does not exist, then we have to run the automatic verification, + * on the other hand, if the marker file exists, + * it means that the verification was executed before and it shouldn't run again + * + * @return true if marker file does not exist, otherwise returns false + */ + private fun generateMarkerFile(): Boolean { + var result = false + if (!file.exists()) { + result = file.createNewFile() + } + + return result + } + + fun deleteFile() { + if (file.exists() && !file.isDirectory) { + file.delete() + } + } + + /** + * The verification is correct if the file doesn't have any exception written. + * This could be called before the file is initialized, in that case it returns null. + */ + fun isVerificationCorrect(): Boolean? { + try { + if (::file.isInitialized) { // we should rethink this flow to avoid having this verification + val fileContent = file.readText() + return if (fileContent.isEmpty()) { + true + } else { + gson.fromJson(fileContent, VerificationResult::class.java).exceptions.isEmpty() + } + } + } catch (e: FileNotFoundException) { + InternalStaticEmbraceLogger.logger.logError("cannot open file", e) + } + return null + } + + fun addException(e: Throwable) { + verificationResult.exceptions.add(e) + file.writeText(gson.toJson(verificationResult).toString()) + } + + fun getExceptions(): List { + val fileContent = file.readText() + return if (fileContent.isBlank()) { + emptyList() + } else { + gson.fromJson(fileContent, VerificationResult::class.java).exceptions + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationExceptionHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationExceptionHandler.kt new file mode 100644 index 0000000000..48d91c0ec2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/AutomaticVerificationExceptionHandler.kt @@ -0,0 +1,26 @@ +package io.embrace.android.embracesdk.samples + +import io.embrace.android.embracesdk.EmbraceAutomaticVerification +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger + +/** + * Exception Handler that verifies if a VerifyIntegrationException was received, + * in order to execute restartAppFromPendingIntent + */ +internal class AutomaticVerificationExceptionHandler constructor( + private val defaultHandler: Thread.UncaughtExceptionHandler? +) : + + Thread.UncaughtExceptionHandler { + + override fun uncaughtException(thread: Thread, exception: Throwable) { + if (exception.cause?.cause?.javaClass == VerifyIntegrationException::class.java) { + EmbraceAutomaticVerification.instance.restartAppFromPendingIntent() + } + InternalStaticEmbraceLogger.logDebug( + "Finished handling exception. Delegating to default handler.", + exception + ) + defaultHandler?.uncaughtException(thread, exception) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/ComparableVersion.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/ComparableVersion.java new file mode 100644 index 0000000000..9e28214596 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/ComparableVersion.java @@ -0,0 +1,404 @@ +package io.embrace.android.embracesdk.samples; + +/* + Licensed under the Apache License,Version2.0(the"License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing,software + distributed under the License is distributed on an"AS IS"BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import androidx.annotation.NonNull; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Stack; + + +/** + * ComparableVersion.java was extracted from the Maven Versioning artifact. + * (org.apache.maven.artifact.versioning) + *

+ * Generic implementation of version comparison. + *

+ * Features: + *

+ * mixing of '-' (hyphen) and '.' (dot) separators, + *

+ * transition between characters and digits also constitutes a separator: + * 1.0alpha1 => [1, 0, alpha, 1] + *

+ * Unlimited number of version components, + *

+ * Version components in the text can be digits or strings, + *

+ * strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. + * Well-known qualifiers (case insensitive) are: + * - alpha or a + * - beta or b + * - milestone or m + * - rc or cr + * - snapshot + * - (the empty string) or ga or final + * - sp + *

+ * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), + *

+ * a hyphen usually precedes a qualifier, and is always less important than something preceded with a dot. + * + * @author Kenney Westerhof + * @author Hervé Boutemy + * @see "Versioning" on Maven Wiki + */ + +class ComparableVersion implements Comparable { + private String value; + + private String canonical; + + private ListItem items; + + private interface Item { + int INTEGER_ITEM = 0; + int STRING_ITEM = 1; + int LIST_ITEM = 2; + + int compareTo(Item item); + + int getType(); + + boolean isNull(); + } + + /** + * Represents a numeric item in the version item list. + */ + private static class IntegerItem implements Item { + private static final BigInteger BIG_INTEGER_ZERO = new BigInteger("0"); + + private final BigInteger value; + + public static final IntegerItem ZERO = new IntegerItem(); + + private IntegerItem() { + this.value = BIG_INTEGER_ZERO; + } + + IntegerItem(String str) { + this.value = new BigInteger(str); + } + + public int getType() { + return INTEGER_ITEM; + } + + public boolean isNull() { + return BIG_INTEGER_ZERO.equals(value); + } + + public int compareTo(Item item) { + if (item == null) { + return BIG_INTEGER_ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INTEGER_ITEM: + return value.compareTo(((IntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value.toString(); + } + } + + /** + * Represents a string in the version item list, usually a qualifier. + */ + private static class StringItem + implements Item { + private static final List QUALIFIERS = + Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp"); + + private static final Properties ALIASES = new Properties(); + + static { + ALIASES.put("ga", ""); + ALIASES.put("final", ""); + ALIASES.put("cr", "rc"); + } + + /** + * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes + * the version older than one without a qualifier, or more recent. + */ + private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf("")); + + private String value; + + StringItem(String value, boolean followedByDigit) { + if (followedByDigit && value.length() == 1) { + // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + default: + } + } + this.value = ALIASES.getProperty(value, value); + } + + public int getType() { + return STRING_ITEM; + } + + public boolean isNull() { + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); + } + + /** + * Returns a comparable value for a qualifier. + *

+ * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical + * ordering. + *

+ * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1 + * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character, + * so this is still fast. If more characters are needed then it requires a lexical sort anyway. + * + * @return an equivalent value that can be used with lexical comparison + */ + public static String comparableQualifier(String qualifier) { + int i = QUALIFIERS.indexOf(qualifier); + + return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); + } + + public int compareTo(Item item) { + if (item == null) { + // 1-rc < 1, 1-ga > 1 + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value; + } + } + + /** + * Represents a version list item. This class is used both for the global item list and for sub-lists (which start + * with '-(number)' in the version specification). + */ + protected static class ListItem extends ArrayList implements Item { + public int getType() { + return LIST_ITEM; + } + + public boolean isNull() { + return (size() == 0); + } + + void normalize() { + for (int i = size() - 1; i >= 0; i--) { + Item lastItem = (Item) get(i); + + if (lastItem.isNull()) { + // remove null trailing items: 0, "", empty list + remove(i); + } else if (!(lastItem instanceof ListItem)) { + break; + } + } + } + + public int compareTo(Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + Item first = (Item) get(0); + return first.compareTo(null); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + Item l = left.hasNext() ? (Item) left.next() : null; + Item r = right.hasNext() ? (Item) right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + StringBuilder buffer = new StringBuilder(); + for (Object item : this) { + if (buffer.length() > 0) { + buffer.append((item instanceof ListItem) ? '-' : '.'); + } + buffer.append(item); + } + return buffer.toString(); + } + } + + public ComparableVersion(@NonNull String version) { + parseVersion(version); + } + + /** + * Expected format MAJOR.MINOR.PATCH-qualifier + */ + @SuppressWarnings("checkstyle:innerassignment") + public final void parseVersion(@NonNull String version) { + this.value = version; + + items = new ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ListItem list = items; + + Stack stack = new Stack<>(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if (c == '-') { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + + list.add(list = new ListItem()); + stack.push(list); + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + list.add(new StringItem(version.substring(startIndex, i), true)); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(true, version.substring(startIndex, i))); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + list.add(parseItem(isDigit, version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ListItem) stack.pop(); + list.normalize(); + } + + canonical = items.toString(); + } + + private static Item parseItem(boolean isDigit, String buf) { + return isDigit ? new IntegerItem(buf) : new StringItem(buf, false); + } + + + public int compareTo(@NonNull ComparableVersion o) { + return items.compareTo(o.items); + } + + public String toString() { + return value; + } + + public boolean equals(Object o) { + return (o instanceof ComparableVersion) && canonical.equals(((ComparableVersion) o).canonical); + } + + public int hashCode() { + return canonical.hashCode(); + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/CrashSamplesNdkDelegate.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/CrashSamplesNdkDelegate.kt new file mode 100644 index 0000000000..8b1ae00ebc --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/CrashSamplesNdkDelegate.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.samples + +internal interface CrashSamplesNdkDelegate { + fun sigIllegalInstruction() + fun throwException() + fun sigAbort() + fun sigfpe() + fun sigsegv() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamples.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamples.kt new file mode 100644 index 0000000000..17df259a65 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamples.kt @@ -0,0 +1,132 @@ +package io.embrace.android.embracesdk.samples + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger + +/** + * Encapsulates the logic to trigger different type of crashes for testing purpose. + * It is recommended to implement every method call via a button press once the app has loaded. + * After a crash sent, the app should be restarted in order to see the error in the dashboard. + */ +internal object EmbraceCrashSamples { + + private val logger = InternalEmbraceLogger() + + @VisibleForTesting + val ndkCrashSamplesNdkDelegate = EmbraceCrashSamplesNdkDelegateImpl() + + /** + * Verifies if Embrace is initialized + */ + @VisibleForTesting + fun isSdkStarted() { + if (!Embrace.getInstance().isStarted) { + val e = EmbraceSampleCodeException( + "Embrace SDK not initialized. Please ensure you have included " + + "Embrace.getInstance().start(this) in Application#onCreate()\n" + + "and then trigger these crash samples via a button press once the app has loaded." + ) + logger.logError("Embrace SDK is not initialized", e) + throw e + } + } + + /** + * verifies if ANR detection is enabled + */ + @VisibleForTesting + fun checkAnrDetectionEnabled() { + if (Embrace.getInstance().configService?.anrBehavior?.isAnrCaptureEnabled() == false) { + val e = EmbraceSampleCodeException( + "ANR capture disabled - you need to enable it to test Embrace's ANR functionality:\n" + + " - add [\"anr\":{\"pct_enabled\": 100 }]" + + " inside the configuration file to enable ANR detection" + ) + logger.logError("ANR detection disabled", e) + throw e + } + } + + /** + * Throws a custom JVM exception: EmbraceCrashException + */ + fun throwJvmException() { + isSdkStarted() + throw EmbraceSampleCodeException("Custom JVM Exception") + } + + /** + * Block the app's main thread for 4 seconds. + * Embrace detects this and samples the main thread stacktraces, so you can better debug ANRs. + */ + fun blockMainThreadForShortInterval() { + isSdkStarted() + checkAnrDetectionEnabled() + try { + Thread.sleep(SHORT_ANR_4_SEC) + } catch (e: InterruptedException) { + logger.logError("Short ANR failed", e) + } + } + + /** + * Force a long ANR that lasts 30 seconds + */ + fun triggerLongAnr() { + isSdkStarted() + checkAnrDetectionEnabled() + val start = System.currentTimeMillis() + while (true) { + if (System.currentTimeMillis() - start >= LONG_ANR_LENGTH) { + break + } + } + } + + /** + * verifies if NDK detection is enabled + */ + @VisibleForTesting + fun checkNdkDetectionEnabled() { + // First verifies is Embrace SDK is initialized + isSdkStarted() + + if (Embrace.getInstance().configService?.autoDataCaptureBehavior?.isNdkEnabled() != true) { + val e = EmbraceSampleCodeException( + "NDK crash capture is disabled - you need to enable it to test Embrace's NDK functionality" + + " - To enable it, add [\"ndk_enabled\": true] inside the configuration file" + ) + logger.logError("NDK detection disabled", e) + throw e + } + } + + fun triggerNdkSigIllegalInstruction() { + checkNdkDetectionEnabled() + ndkCrashSamplesNdkDelegate.sigIllegalInstruction() + } + + fun triggerNdkThrowCppException() { + checkNdkDetectionEnabled() + ndkCrashSamplesNdkDelegate.throwException() + } + + fun triggerNdkSigAbort() { + checkNdkDetectionEnabled() + ndkCrashSamplesNdkDelegate.sigAbort() + } + + fun triggerNdkSigfpe() { + checkNdkDetectionEnabled() + ndkCrashSamplesNdkDelegate.sigfpe() + } + + fun triggerNdkSigsegv() { + checkNdkDetectionEnabled() + ndkCrashSamplesNdkDelegate.sigsegv() + } + + private const val LONG_ANR_LENGTH = 30000 + private const val SHORT_ANR_4_SEC = 4000L +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesNdkDelegateImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesNdkDelegateImpl.kt new file mode 100644 index 0000000000..1c95ba8e44 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesNdkDelegateImpl.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.samples + +internal class EmbraceCrashSamplesNdkDelegateImpl : CrashSamplesNdkDelegate { + external override fun sigIllegalInstruction() + external override fun throwException() + external override fun sigAbort() + external override fun sigfpe() + external override fun sigsegv() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceSampleCodeException.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceSampleCodeException.kt new file mode 100644 index 0000000000..a366bc2765 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/EmbraceSampleCodeException.kt @@ -0,0 +1,3 @@ +package io.embrace.android.embracesdk.samples + +internal class EmbraceSampleCodeException(val msg: String) : Exception(msg) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationActions.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationActions.kt new file mode 100644 index 0000000000..e510537593 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationActions.kt @@ -0,0 +1,208 @@ +package io.embrace.android.embracesdk.samples + +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.EmbraceAutomaticVerification +import io.embrace.android.embracesdk.Severity +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import org.json.JSONObject +import java.io.DataOutputStream +import java.net.HttpURLConnection +import java.net.URL + +/** + * Execute actions to verify the following features: + * - Log a Breadcrumb + * - Set user data + * - Add info, warning and error logs + * - Start and end a moment + * - Executes a GET request + * - Add the trace id to the request (default or the one specified in the local config) + * - Check the current and the latest SDK version + * - Execute a POST request + * - Execute a bad request + * - Trigger an ANR + * - Throw an Exception + */ +internal class VerificationActions( + private val embraceInstance: Embrace, + private val automaticVerificationChecker: AutomaticVerificationChecker +) { + + companion object { + private const val THROW_EXCEPTION_DELAY_MILLIS = 100L + private const val ANR_DURATION_MILLIS = 2000L + private const val MOMENT_DURATION_MILLIS = 3000L + + private const val networkingGetUrl = + "https://dash-api.embrace.io/external/sdk/android/version" + private const val networkingPostUrl = "https://httpbin.org/post" + private const val networkingWrongUrl = "https://httpbin.org/deaasd/ASdasdkjl" + private const val networkingPostBody = "{\"key_one\":\"value_one\"}" + private const val embraceChangelogLink = "https://embrace.io/docs/android/changelog/" + } + + private val handler = Handler(Looper.getMainLooper()) + + private val actionsToVerify = listOf( + Pair({ setUserData() }, "Set user data"), + Pair({ executeLogsActions() }, "Log messages"), + Pair({ executeMoment() }, "Trigger moment"), + Pair({ executeNetworkHttpGETRequest() }, "Executing network request: GET"), + Pair({ executeNetworkHttpPOSTRequest() }, "Executing network request: POST"), + Pair( + { executeNetworkHttpWrongRequest() }, + "Executing network request: testing a wrong url" + ), + Pair({ triggerAnr() }, "Causing an ANR, the application will be tilt"), + Pair({ throwAnException() }, "Throwing an Exception! 💣") + ) + private var currentStep = 0 + private val totalSteps = actionsToVerify.size + + private val sampleProperties = mapOf( + "String" to "Test String", + "LongString" to "This value will be trimmed Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo " + + "consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum. " + + "In culpa qui officia deserunt mollit anim id est laborum.", + "Float" to 1.0f, + "Nested Properties" to mapOf("a" to "b", "c" to "d") + ) + + /** + * Execute actions to verify the following features: + * - Log a Breadcrumb + * - Set user data + * - Add info, warning and error logs + * - Start and end a moment + * - Executes a GET request + * - Check the current and the latest SDK version + * - Execute a POST request + * - Execute a bad request + * - Trigger an ANR + * - Throw an Exception + */ + fun runActions() { + InternalStaticEmbraceLogger.logger.logInfo("${EmbraceAutomaticVerification.TAG} Starting Verification...") + embraceInstance.addBreadcrumb("This is a breadcrumb") + actionsToVerify.forEach { + verifyAction(it.first, it.second) + } + } + + private fun verifyAction(action: () -> Unit, message: String) { + currentStep++ + try { + InternalStaticEmbraceLogger.logger.logInfo( + "${EmbraceAutomaticVerification.TAG} ✓ Step $currentStep/$totalSteps: $message" + ) + action.invoke() + } catch (e: Throwable) { + InternalStaticEmbraceLogger.logger.logError( + "${EmbraceAutomaticVerification.TAG} -- $message ERROR ${e.localizedMessage}" + ) + automaticVerificationChecker.addException(e) + } + } + + private fun setUserData() { + val identifier = "1234567890" + val username = "Mr. Automated User" + val email = "automated@embrace.io" + + embraceInstance.setUserIdentifier(identifier) + embraceInstance.setUsername(username) + embraceInstance.setUserEmail(email) + embraceInstance.setUserAsPayer() + embraceInstance.addUserPersona("userPersona") + } + + private fun executeLogsActions() { + embraceInstance.logMessage("test info", Severity.INFO, sampleProperties) + embraceInstance.logMessage("test warn", Severity.WARNING, sampleProperties) + embraceInstance.logException( + Throwable("Sample throwable"), + Severity.ERROR, + sampleProperties, + "test error" + ) + } + + @VisibleForTesting + private fun executeMoment() { + val momentName = "Verify Integration Moment" + val momentIdentifier = "Verify Integration identifier" + embraceInstance.startMoment(momentName, momentIdentifier, sampleProperties) + handler.postDelayed({ + embraceInstance.endMoment(momentName, momentIdentifier) + }, MOMENT_DURATION_MILLIS) + } + + @VisibleForTesting + fun executeNetworkHttpGETRequest() { + val connection = URL(networkingGetUrl).openConnection() as HttpURLConnection + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded") + connection.setRequestProperty( + embraceInstance.traceIdHeader, + "traceId : ${embraceInstance.traceIdHeader}" + ) + + val data = connection.inputStream.bufferedReader().readText() + + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw VerifyIntegrationException("RESPONSE CODE IS ${connection.responseCode}") + } + + checkEmbraceSDKVersion(JSONObject(data).getString("value")) + } + + private fun checkEmbraceSDKVersion(latestEmbraceVersion: String) { + val currentVersion = BuildConfig.VERSION_NAME + + if (ComparableVersion(currentVersion) < ComparableVersion(latestEmbraceVersion)) { + InternalStaticEmbraceLogger.logger.logWarning( + "${EmbraceAutomaticVerification.TAG} Note that there is a newer version of Embrace available 🙌! " + + "You can read the changelog for $latestEmbraceVersion here: $embraceChangelogLink" + ) + } + } + + @VisibleForTesting + private fun executeNetworkHttpPOSTRequest() { + val connection = URL(networkingPostUrl).openConnection() as HttpURLConnection + connection.doOutput = true + DataOutputStream(connection.outputStream).use { it.writeBytes(networkingPostBody) } + + val result = connection.responseCode + + if (result != HttpURLConnection.HTTP_OK) { + throw VerifyIntegrationException("RESPONSE CODE IS $result") + } + } + + @VisibleForTesting + private fun executeNetworkHttpWrongRequest() { + val connection = URL(networkingWrongUrl).openConnection() as HttpURLConnection + val result = connection.responseCode + if (result != HttpURLConnection.HTTP_NOT_FOUND) { + throw VerifyIntegrationException("RESPONSE CODE IS $result") + } + } + + private fun triggerAnr() { + handler.post { Thread.sleep(ANR_DURATION_MILLIS) } + InternalStaticEmbraceLogger.logger.logInfo("${EmbraceAutomaticVerification.TAG} ANR Finished") + } + + @VisibleForTesting + private fun throwAnException() { + handler.postDelayed({ + throw VerifyIntegrationException("Forced Exception to verify integration") + }, THROW_EXCEPTION_DELAY_MILLIS) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationResult.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationResult.kt new file mode 100644 index 0000000000..8e6e3eaccb --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerificationResult.kt @@ -0,0 +1,5 @@ +package io.embrace.android.embracesdk.samples + +internal class VerificationResult { + val exceptions = mutableListOf() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerifyIntegrationException.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerifyIntegrationException.kt new file mode 100644 index 0000000000..f1a59e0f77 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/samples/VerifyIntegrationException.kt @@ -0,0 +1,3 @@ +package io.embrace.android.embracesdk.samples + +internal class VerifyIntegrationException(message: String) : Exception(message) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityListener.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityListener.kt new file mode 100644 index 0000000000..0b20e10d31 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityListener.kt @@ -0,0 +1,50 @@ +package io.embrace.android.embracesdk.session + +import android.app.Activity +import android.os.Bundle + +/** + * Listener implemented by observers of the [ActivityService]. + */ +internal interface ActivityListener { + + /** + * Triggered when the app enters the background. + */ + fun onBackground(timestamp: Long) {} + + /** + * Triggered when the application is resumed. + * + * @param coldStart whether this is a cold start + * @param startupTime the timestamp at which the application started + */ + fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) {} + + /** + * Triggered when the application has completed startup; + */ + fun applicationStartupComplete() {} + + /** + * Triggered when an activity is opened. + * + * @param activity details of the activity + */ + fun onView(activity: Activity) {} + + /** + * Triggered when an activity is closed. + * + * @param activity details of the activity + */ + fun onViewClose(activity: Activity) {} + + /** + * Triggered when an activity is created. + * + * @param activity the activity + * @param bundle the bundle + */ + fun onActivityCreated(activity: Activity, bundle: Bundle?) {} +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityService.kt new file mode 100644 index 0000000000..0b74bd8141 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/ActivityService.kt @@ -0,0 +1,50 @@ +package io.embrace.android.embracesdk.session + +import android.app.Activity +import android.app.Application +import android.content.ComponentCallbacks2 +import androidx.lifecycle.LifecycleObserver +import java.io.Closeable + +/** + * Service which handles Android activity lifecycle callbacks. + */ +internal interface ActivityService : + ComponentCallbacks2, + LifecycleObserver, + Application.ActivityLifecycleCallbacks, + Closeable { + + /** + * Whether the application is in the background. + * + * @return true if the application is in the background, false otherwise + */ + val isInBackground: Boolean + + /** + * Gets the activity which is currently in the foreground. + * + * @return an optional of the activity currently in the foreground + */ + val foregroundActivity: Activity? + + /** + * Adds an observer of the application's lifecycle activity events. + * + * @param listener the observer to register + */ + fun addListener(listener: ActivityListener) + + /** + * This function should be automatically invoked when the process lifecycle + * enters the foreground. You should not call this directly. + */ + fun onForeground() + + /** + * This function should be automatically invoked when the process lifecycle + * enters the background. You should not call this directly. + */ + fun onBackground() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/BackgroundActivityService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/BackgroundActivityService.kt new file mode 100644 index 0000000000..90dc080fa8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/BackgroundActivityService.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk.session + +/** + * Service that captures and sends information when the app is in background + */ +internal interface BackgroundActivityService { + + /** + * Stops the current background activity session and sends the session message to the backend + */ + fun sendBackgroundActivity() + + /** + * Handles an uncaught exception, ending the session and saving the activity to disk. + */ + fun handleCrash(crashId: String) + + /** + * Save the current background activity to disk + */ + fun save() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceActivityService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceActivityService.kt new file mode 100644 index 0000000000..0d92ab0015 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceActivityService.kt @@ -0,0 +1,303 @@ +package io.embrace.android.embracesdk.session + +import android.app.Activity +import android.app.Application +import android.content.ComponentCallbacks2 +import android.content.res.Configuration +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.ProcessLifecycleOwner +import io.embrace.android.embracesdk.annotation.StartupActivity +import io.embrace.android.embracesdk.capture.memory.MemoryService +import io.embrace.android.embracesdk.capture.orientation.OrientationService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.utils.ThreadUtils +import io.embrace.android.embracesdk.utils.stream +import java.lang.ref.WeakReference +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Service tracking the app's current activity and background state, and dispatching events to other + * services as required. + * + * See activity lifecycle documentation + * [here](https://developer.android.com/guide/components/activities/activity-lifecycle). + * + * We only need to track the `onForeground` and `onActivityPaused` lifecycle hooks for background + * detection, as these are the only two which are consistently fired in all scenarios. + */ +internal class EmbraceActivityService( + + /** + * The application. + */ + private val application: Application, + + /** + * The orientation service. + */ + private val orientationService: OrientationService?, + private val clock: Clock +) : ActivityService { + + /** + * The memory service, it's provided on the instantiation of the service. + */ + private var memoryService: MemoryService? = null + + /** + * List of listeners that subscribe to activity events. + */ + @VisibleForTesting + val listeners = CopyOnWriteArrayList() + + /** + * The currently active activity. + */ + @Volatile + private var currentActivity = WeakReference(null) + + /** + * States if the activity foreground phase comes from a cold start or not. + * Checked every time an activity executes a foreground phase. + */ + @Volatile + private var coldStart = true + + /** + * States the initialization time of the EmbraceActivityService, inferring it is initialized + * from the [Embrace.start] method. + */ + private val startTime: Long = clock.now() + + /** + * Returns if the app's in background or not. + */ + @Volatile + override var isInBackground = true + private set + + init { + application.registerActivityLifecycleCallbacks(this) + application.applicationContext.registerComponentCallbacks(this) + // add lifecycle observer on main thread to avoid IllegalStateExceptions with + // androidx.lifecycle + ThreadUtils.runOnMainThread( + Runnable { + ProcessLifecycleOwner.get().lifecycle + .addObserver(this@EmbraceActivityService) + } + ) + } + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + logDeveloper("EmbraceActivityService", "Activity created: " + getActivityName(activity)) + updateStateWithActivity(activity) + updateOrientationWithActivity(activity) + stream(listeners) { listener: ActivityListener -> + try { + listener.onActivityCreated(activity, bundle) + } catch (ex: Exception) { + logDebug(ERROR_FAILED_TO_NOTIFY, ex) + } + } + } + + override fun onActivityStarted(activity: Activity) { + logDeveloper("EmbraceActivityService", "Activity started: " + getActivityName(activity)) + updateStateWithActivity(activity) + stream(listeners) { listener: ActivityListener -> + try { + listener.onView(activity) + } catch (ex: Exception) { + logDebug(ERROR_FAILED_TO_NOTIFY, ex) + } + } + } + + override fun onActivityResumed(activity: Activity) { + logDeveloper("EmbraceActivityService", "Activity resumed: " + getActivityName(activity)) + if (!activity.javaClass.isAnnotationPresent(StartupActivity::class.java)) { + // If the activity coming to foreground doesn't have the StartupActivity annotation + // the the SDK will finalize any pending startup moment. + logDeveloper("EmbraceActivityService", "Activity resumed: " + getActivityName(activity)) + stream(listeners) { listener: ActivityListener -> + try { + listener.applicationStartupComplete() + } catch (ex: Exception) { + logDebug(ERROR_FAILED_TO_NOTIFY, ex) + } + } + } else { + logDeveloper( + "EmbraceActivityService", + getActivityName(activity) + " is @StartupActivity" + ) + } + } + + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) { + logDeveloper("EmbraceActivityService", "Activity stopped: " + getActivityName(activity)) + stream(listeners) { listener: ActivityListener -> + try { + listener.onViewClose(activity) + } catch (ex: Exception) { + logDebug(ERROR_FAILED_TO_NOTIFY, ex) + } + } + } + + override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + + /** + * This method will update the current activity for further checking. + * + * @param activity the activity involved in the state change. + */ + @VisibleForTesting + @Synchronized + fun updateStateWithActivity(activity: Activity?) { + logDeveloper("EmbraceActivityService", "Current activity: " + getActivityName(activity)) + currentActivity = WeakReference(activity) + } + + /** + * This method will update the current activity orientation. + * + * @param activity the activity involved in the tracking orientation process. + */ + private fun updateOrientationWithActivity(activity: Activity) { + if (orientationService != null) { + try { + logDeveloper( + "EmbraceActivityService", + "Updated orientation: " + activity.resources.configuration.orientation + ) + orientationService.onOrientationChanged(activity.resources.configuration.orientation) + } catch (ex: Exception) { + logDebug("Failed to register an orientation change", ex) + } + } + } + + /** + * This method will be called by the ProcessLifecycleOwner when the main app process calls + * ON START. + */ + @OnLifecycleEvent(Lifecycle.Event.ON_START) + override fun onForeground() { + logDebug("AppState: App entered foreground.") + isInBackground = false + val timestamp = clock.now() + stream(listeners) { listener: ActivityListener -> + try { + listener.onForeground(coldStart, startTime, timestamp) + } catch (ex: Exception) { + logDebug(ERROR_FAILED_TO_NOTIFY, ex) + } + } + coldStart = false + } + + /** + * This method will be called by the ProcessLifecycleOwner when the main app process calls + * ON STOP. + */ + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + override fun onBackground() { + logDebug("AppState: App entered background") + updateStateWithActivity(null) + isInBackground = true + val timestamp = clock.now() + stream(listeners) { listener: ActivityListener -> + try { + InternalStaticEmbraceLogger.logger.logWarning("onBackground() listener: $listener") + listener.onBackground(timestamp) + } catch (ex: Exception) { + logDebug(ERROR_FAILED_TO_NOTIFY, ex) + } + } + } + + /** + * Called when the OS has determined that it is a good time for a process to trim unneeded + * memory. + * + * @param trimLevel the context of the trim, giving a hint of the amount of trimming. + */ + override fun onTrimMemory(trimLevel: Int) { + logDeveloper("EmbraceActivityService", "onTrimMemory(). TrimLevel: $trimLevel") + if (trimLevel == ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) { + try { + memoryService?.onMemoryWarning() + } catch (ex: Exception) { + logDebug("Failed to handle onTrimMemory (low memory) event", ex) + } + } + } + + fun setMemoryService(memoryService: MemoryService?) { + this.memoryService = memoryService + } + + override fun onConfigurationChanged(configuration: Configuration) {} + override fun onLowMemory() {} + + /** + * Returns the current activity instance + */ + override val foregroundActivity: Activity? + get() { + val foregroundActivity = currentActivity.get() + if (foregroundActivity == null || foregroundActivity.isFinishing) { + logDeveloper("EmbraceActivityService", "Foreground activity not present") + return null + } + logDeveloper( + "EmbraceActivityService", + "Foreground activity name: " + getActivityName(foregroundActivity) + ) + return foregroundActivity + } + + override fun addListener(listener: ActivityListener) { + // assumption: we always need to run the Session service first, then everything else, + // because otherwise the session envelope will not be created. The ActivityListener + // could use separating from session handling, but that's a bigger change. + val priority = listener is SessionService + if (!listeners.contains(listener)) { + if (priority) { + listeners.add(0, listener) + } else { + listeners.addIfAbsent(listener) + } + } + } + + override fun close() { + try { + logDebug("Shutting down EmbraceActivityService") + application.applicationContext.unregisterComponentCallbacks(this) + application.unregisterActivityLifecycleCallbacks(this) + listeners.clear() + } catch (ex: Exception) { + logDebug("Error when closing EmbraceActivityService", ex) + } + } + + private fun getActivityName(activity: Activity?): String { + return activity?.localClassName ?: "Null" + } + + companion object { + private const val ERROR_FAILED_TO_NOTIFY = + "Failed to notify EmbraceActivityService listener" + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityService.kt new file mode 100644 index 0000000000..df085cf17c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityService.kt @@ -0,0 +1,369 @@ +package io.embrace.android.embracesdk.session + +import android.app.Activity +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.config.ConfigListener +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.internal.spans.EmbraceAttributes +import io.embrace.android.embracesdk.internal.spans.SpansService +import io.embrace.android.embracesdk.internal.utils.Uuid.getEmbUuid +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.BackgroundActivity +import io.embrace.android.embracesdk.payload.BackgroundActivity.Companion.createStartMessage +import io.embrace.android.embracesdk.payload.BackgroundActivity.Companion.createStopMessage +import io.embrace.android.embracesdk.payload.BackgroundActivity.LifeEventType +import io.embrace.android.embracesdk.payload.BackgroundActivityMessage +import io.embrace.android.embracesdk.utils.submitSafe +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.atomic.AtomicInteger + +internal class EmbraceBackgroundActivityService( + private val performanceInfoService: PerformanceInfoService, + private val metadataService: MetadataService, + private val breadcrumbService: BreadcrumbService, + activityService: ActivityService, + private val eventService: EventService, + private val remoteLogger: EmbraceRemoteLogger, + private val userService: UserService, + private val exceptionService: EmbraceInternalErrorService, + private val deliveryService: DeliveryService, + private val configService: ConfigService, + private val ndkService: NdkService, + /** + * Embrace service dependencies of the background activity session service. + */ + private val clock: Clock, + private val spansService: SpansService, + private val executorServiceSupplier: Lazy +) : BackgroundActivityService, ActivityListener, ConfigListener { + + @get:Synchronized + private val cacheExecutorService: ExecutorService by lazy { executorServiceSupplier.value } + private var lastSaved: Long = 0 + private var willBeSaved = false + + /** + * The active background activity session. + */ + @VisibleForTesting + @Volatile + var backgroundActivity: BackgroundActivity? = null + private val manualBkgSessionsSent = AtomicInteger(0) + + @VisibleForTesting + var lastSendAttempt: Long + private var isEnabled = true + + init { + activityService.addListener(this) + lastSendAttempt = clock.now() + configService.addListener(this) + if (activityService.isInBackground) { + // start background activity capture from a cold start + startBackgroundActivityCapture(clock.now(), true, LifeEventType.BKGND_STATE) + } + } + + override fun sendBackgroundActivity() { + if (!isEnabled || !verifyManualSendThresholds()) { + return + } + val now = clock.now() + val backgroundActivityMessage = + stopBackgroundActivityCapture(now, LifeEventType.BKGND_MANUAL, null) + // start a new background activity session + startBackgroundActivityCapture(clock.now(), false, LifeEventType.BKGND_MANUAL) + if (backgroundActivityMessage != null) { + deliveryService.sendBackgroundActivity(backgroundActivityMessage) + } + } + + override fun handleCrash(crashId: String) { + if (isEnabled && backgroundActivity != null) { + val now = clock.now() + val backgroundActivityMessage = + stopBackgroundActivityCapture(now, LifeEventType.BKGND_STATE, crashId) + if (backgroundActivityMessage != null) { + deliveryService.saveBackgroundActivity(backgroundActivityMessage) + } + startBackgroundActivityCapture(clock.now(), false, LifeEventType.BKGND_STATE) + } + } + + override fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) { + if (isEnabled) { + val backgroundActivityMessage = + stopBackgroundActivityCapture(timestamp - 1, LifeEventType.BKGND_STATE, null) + if (backgroundActivityMessage != null) { + deliveryService.saveBackgroundActivity(backgroundActivityMessage) + } + deliveryService.sendBackgroundActivities() + } + } + + override fun onBackground(timestamp: Long) { + if (isEnabled) { + startBackgroundActivityCapture(timestamp + 1, false, LifeEventType.BKGND_STATE) + } + } + + override fun onConfigChange(configService: ConfigService) { + if (isEnabled && !configService.isBackgroundActivityCaptureEnabled()) { + disableService() + } else if (!isEnabled && configService.isBackgroundActivityCaptureEnabled()) { + enableService() + } + } + + /** + * Save the background activity to disk + */ + override fun save() { + if (isEnabled && backgroundActivity != null) { + if (clock.now() - lastSaved > MIN_INTERVAL_BETWEEN_SAVES) { + saveNow() + } else if (!willBeSaved) { + willBeSaved = true + saveLater() + } + } + } + + private fun saveNow() { + cacheExecutorService.submitSafe( + Callable { + cacheBackgroundActivity() + null + } + ) + willBeSaved = false + } + + private fun saveLater() { + val handler = Handler(Looper.getMainLooper()) + handler.postDelayed(Runnable { saveNow() }, MIN_INTERVAL_BETWEEN_SAVES) + } + + private fun disableService() { + isEnabled = false + } + + private fun enableService() { + isEnabled = true + } + + /** + * Start the background activity capture by starting the cache service and creating the background + * session. + * + * @param coldStart defines if the action comes from an application cold start or not + * @param startType defines which is the lifecycle of the session + */ + private fun startBackgroundActivityCapture( + startTime: Long, + coldStart: Boolean, + startType: LifeEventType + ) { + val activity = createStartMessage( + getEmbUuid(), + startTime, + coldStart, + startType, + APPLICATION_STATE_BACKGROUND, + userService.loadUserInfoFromDisk() + ) + backgroundActivity = activity + metadataService.setActiveSessionId(activity.sessionId) + if (configService.autoDataCaptureBehavior.isNdkEnabled()) { + ndkService.updateSessionId(activity.sessionId) + } + saveNow() + } + + /** + * Stop the background activity capture by stopping the cache service and putting the background + * session to its final state with all the data collected up to the current point. + * Build the next background message and attempt to send it. + * + * @param endType defines what kind of event ended the background activity capture + */ + @Synchronized + private fun stopBackgroundActivityCapture( + endTime: Long, + endType: LifeEventType, + crashId: String? + ): BackgroundActivityMessage? { + val activity = backgroundActivity + if (activity == null) { + InternalStaticEmbraceLogger.logError("No background activity to report") + return null + } + val startTime = activity.startTime ?: 0 + val sendBackgroundActivity = createStopMessage( + activity, + APPLICATION_STATE_BACKGROUND, + MESSAGE_TYPE_END, + endTime, + eventService.findEventIdsForSession(startTime, endTime), + remoteLogger.findInfoLogIds(startTime, endTime), + remoteLogger.findWarningLogIds(startTime, endTime), + remoteLogger.findErrorLogIds(startTime, endTime), + remoteLogger.getInfoLogsAttemptedToSend(), + remoteLogger.getWarnLogsAttemptedToSend(), + remoteLogger.getErrorLogsAttemptedToSend(), + exceptionService.currentExceptionError, + endTime, + endType, + remoteLogger.getUnhandledExceptionsSent(), + crashId + ) + backgroundActivity = null + return buildBackgroundActivityMessage(sendBackgroundActivity, true) + } + + /** + * Verify if the amount of background activities captured reach the limit or if the last send + * attempt was less than 5 sec ago. + * + * @return false if the verify failed, true otherwise + */ + private fun verifyManualSendThresholds(): Boolean { + val behavior = configService.backgroundActivityBehavior + val manualBackgroundActivityLimit = behavior.getManualBackgroundActivityLimit() + val minBackgroundActivityDuration = behavior.getMinBackgroundActivityDuration() + if (manualBkgSessionsSent.getAndIncrement() >= manualBackgroundActivityLimit) { + InternalStaticEmbraceLogger.logWarning( + "Warning, failed to send background activity. " + + "The amount of background activity that can be sent reached the limit.." + ) + return false + } + if (lastSendAttempt < minBackgroundActivityDuration) { + InternalStaticEmbraceLogger.logWarning( + "Warning, failed to send background activity. The last attempt " + + "to send background activity was less than 5 seconds ago." + ) + return false + } + return true + } + + /** + * Create the background session message with the current state of the background activity. + * + * @param backgroundActivity the current state of a background activity + * @param isBackgroundActivityEnd true if the message is being built for the termination of the background activity + * @return a background activity message for backend + */ + private fun buildBackgroundActivityMessage( + backgroundActivity: BackgroundActivity?, + isBackgroundActivityEnd: Boolean + ): BackgroundActivityMessage? { + if (backgroundActivity != null) { + val startTime = backgroundActivity.startTime ?: 0L + val endTime = backgroundActivity.endTime ?: clock.now() + val isCrash = backgroundActivity.crashReportId != null + return BackgroundActivityMessage( + backgroundActivity, + backgroundActivity.user, + metadataService.getAppInfo(), + metadataService.getDeviceInfo(), + performanceInfoService.getSessionPerformanceInfo( + startTime, + endTime, + java.lang.Boolean.TRUE == backgroundActivity.isColdStart, + null + ), + breadcrumbService.flushBreadcrumbs(), + if (isBackgroundActivityEnd) { + spansService.flushSpans( + if (isCrash) { + EmbraceAttributes.AppTerminationCause.CRASH + } else { + null + } + ) + } else { + spansService.completedSpans() + } + ) + } + return null + } + + /** + * Cache the activity, with performance information generated up to the current point. + */ + private fun cacheBackgroundActivity() { + try { + val activity = backgroundActivity + if (activity != null) { + lastSaved = clock.now() + val startTime = activity.startTime ?: 0L + val endTime = activity.endTime ?: clock.now() + val cachedActivity = createStopMessage( + activity, + APPLICATION_STATE_BACKGROUND, + MESSAGE_TYPE_END, + null, + eventService.findEventIdsForSession(startTime, endTime), + remoteLogger.findInfoLogIds(startTime, endTime), + remoteLogger.findWarningLogIds(startTime, endTime), + remoteLogger.findErrorLogIds(startTime, endTime), + remoteLogger.getInfoLogsAttemptedToSend(), + remoteLogger.getWarnLogsAttemptedToSend(), + remoteLogger.getErrorLogsAttemptedToSend(), + exceptionService.currentExceptionError, + clock.now(), + null, + remoteLogger.getUnhandledExceptionsSent(), + null + ) + val message = buildBackgroundActivityMessage(cachedActivity, false) + if (message == null) { + InternalStaticEmbraceLogger.logDebug("Failed to cache background activity message.") + return + } + deliveryService.saveBackgroundActivity(message) + } + } catch (ex: Exception) { + InternalStaticEmbraceLogger.logDebug("Error while caching active session", ex) + } + } + + override fun applicationStartupComplete() {} + override fun onView(activity: Activity) {} + override fun onViewClose(activity: Activity) {} + override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} + + companion object { + /** + * Signals to the API that this is a background session. + */ + private const val APPLICATION_STATE_BACKGROUND = "background" + + /** + * Signals to the API the end of a session. + */ + private const val MESSAGE_TYPE_END = "en" + + /** + * Minimum time between writes of the background activity to disk + */ + private const val MIN_INTERVAL_BETWEEN_SAVES: Long = 5000 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerService.kt new file mode 100644 index 0000000000..adef8748c4 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerService.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.session + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.utils.stream +import java.util.concurrent.CopyOnWriteArrayList + +internal class EmbraceMemoryCleanerService : MemoryCleanerService { + + /** + * List of listeners that subscribe to clean services collections. + */ + @VisibleForTesting + val listeners = CopyOnWriteArrayList() + + override fun cleanServicesCollections( + exceptionService: EmbraceInternalErrorService + ) { + logDeveloper("EmbraceMemoryCleanerService", "Clean services collections") + + stream(listeners) { listener: MemoryCleanerListener -> + try { + listener.cleanCollections() + } catch (ex: Exception) { + logDebug("Failed to clean collections on service listener", ex) + } + } + exceptionService.resetExceptionErrorObject() + } + + override fun addListener(listener: MemoryCleanerListener) { + listeners.addIfAbsent(listener) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionProperties.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionProperties.kt new file mode 100644 index 0000000000..e36cfbf188 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionProperties.kt @@ -0,0 +1,116 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.PreferencesService + +internal class EmbraceSessionProperties( + private val preferencesService: PreferencesService, + private val logger: InternalEmbraceLogger, + private val configService: ConfigService +) { + private val temporary: MutableMap = HashMap() + private var permanent: MutableMap + + init { + // TODO: this blocks on the preferences being successfully read from this. Are we cool with this? + val existingPermanent: Map? = preferencesService.permanentSessionProperties + permanent = existingPermanent?.let(::HashMap) ?: HashMap() + } + + private fun haveKey(key: String): Boolean { + return permanent.containsKey(key) || temporary.containsKey(key) + } + + private fun isValidKey(key: String?): Boolean { + if (key == null) { + logger.logError("Session property key cannot be null") + return false + } + if (key == "") { + logger.logError("Session property key cannot be empty string") + return false + } + return true + } + + private fun isValidValue(key: String?): Boolean { + if (key == null) { + logger.logError("Session property value cannot be null") + return false + } + return true + } + + private fun enforceLength(value: String, maxLength: Int): String { + if (value.length <= maxLength) { + return value + } + val endChars = "..." + return value.substring(0, maxLength - endChars.length) + endChars + } + + @Synchronized + fun add(key: String, value: String, isPermanent: Boolean): Boolean { + if (!isValidKey(key)) { + return false + } + val sanitizedKey = enforceLength(key, SESSION_PROPERTY_KEY_LIMIT) + if (!isValidValue(value)) { + return false + } + val sanitizedValue = enforceLength(value, SESSION_PROPERTY_VALUE_LIMIT) + val maxSessionProperties = configService.sessionBehavior.getMaxSessionProperties() + if (size() > maxSessionProperties || size() == maxSessionProperties && !haveKey(sanitizedKey)) { + logger.logError("Session property count is at its limit. Rejecting.") + return false + } + + // add to selected destination, deleting the key if it exists in the other destination + if (isPermanent) { + permanent[sanitizedKey] = sanitizedValue + temporary.remove(sanitizedKey) + preferencesService.permanentSessionProperties = permanent + } else { + // only save the permanent values if the key existed in the permanent map + if (permanent.remove(sanitizedKey) != null) { + preferencesService.permanentSessionProperties = permanent + } + temporary[sanitizedKey] = sanitizedValue + } + return true + } + + @Synchronized + fun remove(key: String): Boolean { + if (!isValidKey(key)) { + return false + } + val sanitizedKey = enforceLength(key, SESSION_PROPERTY_KEY_LIMIT) + var existed = false + if (temporary.remove(sanitizedKey) != null) { + existed = true + } + if (permanent.remove(sanitizedKey) != null) { + preferencesService.permanentSessionProperties = permanent + existed = true + } + return existed + } + + @Synchronized + fun get(): Map = permanent.plus(temporary) + + private fun size(): Int = permanent.size + temporary.size + + fun clearTemporary() = temporary.clear() + + companion object { + + /** + * The maximum number of properties that can be attached to a session + */ + private const val SESSION_PROPERTY_KEY_LIMIT = 128 + private const val SESSION_PROPERTY_VALUE_LIMIT = 1024 + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionService.kt new file mode 100644 index 0000000000..8ee0aab1d7 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/EmbraceSessionService.kt @@ -0,0 +1,250 @@ +package io.embrace.android.embracesdk.session + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.internal.spans.EmbraceAttributes +import io.embrace.android.embracesdk.internal.spans.SpansService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.Session + +internal class EmbraceSessionService( + private val activityService: ActivityService, + private val ndkService: NdkService, + private val sessionProperties: EmbraceSessionProperties, + private val logger: InternalEmbraceLogger, + private val sessionHandler: SessionHandler, + private val deliveryService: DeliveryService, + private val isNdkEnabled: Boolean, + private val clock: Clock, + private val spansService: SpansService +) : SessionService, ActivityListener { + + companion object { + private const val TAG = "EmbraceSessionService" + + /** + * Signals to the API that the application was in the foreground. + */ + const val APPLICATION_STATE_FOREGROUND = "foreground" + + /** + * The minimum threshold for how long a session must last. Package-private for test accessibility + */ + const val minSessionTime = 5000L + + /** + * Session caching interval in seconds. + */ + const val SESSION_CACHING_INTERVAL = 2 + } + + /** + * Synchronization lock. + */ + private val lock = Any() + + /** + * SDK startup time. Only set for cold start sessions. + */ + private var sdkStartupDuration: Long = 0 + + /** + * The currently active session. + */ + @Volatile + private var activeSession: Session? = null + + init { + if (!this.activityService.isInBackground) { + // If the app goes to foreground before the SDK finishes its startup, + // the session service will not be registered to the activity listener and will not + // start the cold session. + // If so, force a cold session start. + logger.logDeveloper(TAG, "Forcing cold start") + startStateSession(true, clock.now()) + } + + // Send any sessions that were cached and not yet sent. + deliveryService.sendCachedSessions(isNdkEnabled, ndkService, activeSession?.sessionId) + } + + /** + * record the time taken to initialize the SDK + * + * @param sdkStartupDuration the time taken to initialize the SDK in milliseconds + */ + fun setSdkStartupDuration(sdkStartupDuration: Long) { + logger.logDeveloper(TAG, "Setting startup duration: $sdkStartupDuration") + this.sdkStartupDuration = sdkStartupDuration + } + + override fun startSession(coldStart: Boolean, startType: Session.SessionLifeEventType, startTime: Long) { + val automaticSessionCloserCallback = Runnable { + try { + synchronized(lock) { + logger.logInfo("Automatic session closing triggered.") + triggerStatelessSessionEnd(Session.SessionLifeEventType.TIMED) + } + } catch (ex: Exception) { + logger.logError("Error while trying to close the session automatically", ex) + } + } + + val sessionMessage = sessionHandler.onSessionStarted( + coldStart, + startType, + startTime, + sessionProperties, + automaticSessionCloserCallback, + this::onPeriodicCacheActiveSession + ) + + if (sessionMessage != null) { + logger.logDeveloper(TAG, "Session Message is created") + activeSession = sessionMessage.session + logger.logDeveloper(TAG, "Active session: " + activeSession?.sessionId) + } else { + logger.logDeveloper(TAG, "Session Message is NULL") + } + } + + override fun handleCrash(crashId: String) { + logger.logDeveloper(TAG, "Attempt to handle crash id: $crashId") + + activeSession?.also { + synchronized(lock) { + sessionHandler.onCrash( + it, + crashId, + sessionProperties, + sdkStartupDuration, + spansService.flushSpans(EmbraceAttributes.AppTerminationCause.CRASH) + ) + } + } ?: logger.logDeveloper(TAG, "Active session is NULL") + } + + /** + * Caches the session, with performance information generated up to the current point. + */ + @VisibleForTesting + fun onPeriodicCacheActiveSession() { + try { + synchronized(lock) { + val activeSessionInfo = sessionHandler.getActiveSessionEndMessage( + activeSession, + sessionProperties, + sdkStartupDuration, + spansService.completedSpans() + ) + activeSessionInfo?.let { + deliveryService.saveSession(it) + } + } + } catch (ex: Exception) { + logger.logDebug("Error while caching active session", ex) + } + } + + override fun onForeground(coldStart: Boolean, startupTime: Long, timestamp: Long) { + logger.logDeveloper(TAG, "OnForeground. Starting session.") + startStateSession(coldStart, timestamp) + } + + private fun startStateSession(coldStart: Boolean, endTime: Long) { + logger.logDeveloper(TAG, "Start state session. Is cold start: $coldStart") + synchronized(lock) { + startSession(coldStart, Session.SessionLifeEventType.STATE, endTime) + } + } + + override fun onBackground(timestamp: Long) { + logger.logDeveloper(TAG, "OnBackground. Ending session.") + endSession(Session.SessionLifeEventType.STATE, timestamp) + } + + /** + * It will try to end session. Note that it will either be for MANUAL or TIMED types. + * + * @param endType the origin of the event that ends the session. + */ + override fun triggerStatelessSessionEnd(endType: Session.SessionLifeEventType) { + if (Session.SessionLifeEventType.STATE == endType) { + logger.logWarning( + "triggerStatelessSessionEnd is not allowed to be called for SessionLifeEventType=$endType" + ) + return + } + + // Ends active session. + endSession(endType, clock.now()) + + // Starts a new session. + if (!activityService.isInBackground) { + logger.logDeveloper(TAG, "Activity is not in background, starting session.") + startSession(false, endType, clock.now()) + } else { + logger.logDeveloper(TAG, "Activity in background, not starting session.") + } + logger.logInfo("Session successfully closed.") + } + + /** + * This will trigger all necessary events to end the current session and send it to the server. + * + * @param endType the origin of the event that ends the session. + */ + @Synchronized + private fun endSession(endType: Session.SessionLifeEventType, endTime: Long) { + logger.logDebug("Will try to end session.") + sessionHandler.onSessionEnded( + endType, + activeSession, + sessionProperties, + sdkStartupDuration, + endTime, + spansService.flushSpans() + ) + + // clear active session + activeSession = null + logger.logDeveloper(TAG, "Active session cleared") + } + + override fun close() { + logger.logInfo("Shutting down EmbraceSessionService") + sessionHandler.close() + } + + fun getActiveSession(): Session? { + return activeSession + } + + override fun addProperty(key: String, value: String, permanent: Boolean): Boolean { + logger.logDeveloper(TAG, "Add Property: $key - $value") + val added = sessionProperties.add(key, value, permanent) + if (added) { + logger.logDeveloper(TAG, "Session properties updated") + ndkService.onSessionPropertiesUpdate(sessionProperties.get()) + } else { + logger.logDeveloper(TAG, "Cannot add property: $key") + } + return added + } + + override fun removeProperty(key: String): Boolean { + logger.logDeveloper(TAG, "Remove Property: $key") + val removed = sessionProperties.remove(key) + if (removed) { + logger.logDeveloper(TAG, "Session properties updated") + ndkService.onSessionPropertiesUpdate(sessionProperties.get()) + } else { + logger.logDeveloper(TAG, "Cannot remove property: $key") + } + return removed + } + + override fun getProperties(): Map = sessionProperties.get() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerListener.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerListener.kt new file mode 100644 index 0000000000..3d6661bdce --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerListener.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.session + +internal interface MemoryCleanerListener { + + /** + * Clean collections in memory when a session ends occurs. + */ + fun cleanCollections() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerService.kt new file mode 100644 index 0000000000..0003af75b6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/MemoryCleanerService.kt @@ -0,0 +1,20 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService + +internal interface MemoryCleanerService { + + /** + * Adds an observer of the end session event. + * + * @param listener the observer to register + */ + fun addListener(listener: MemoryCleanerListener) + + /** + * Flush collections from each service which has collections in memory. + */ + fun cleanServicesCollections( + exceptionService: EmbraceInternalErrorService + ) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt new file mode 100644 index 0000000000..c60cc711ef --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt @@ -0,0 +1,613 @@ +package io.embrace.android.embracesdk.session + +import androidx.annotation.VisibleForTesting +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerService +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbService +import io.embrace.android.embracesdk.capture.crumbs.activity.ActivityLifecycleBreadcrumbService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.thermalstate.ThermalStatusService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.capture.webview.WebViewService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.comms.delivery.SessionMessageState +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.internal.MessageType +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDeveloper +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.BetaFeatures +import io.embrace.android.embracesdk.payload.Session +import io.embrace.android.embracesdk.payload.Session.SessionLifeEventType +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.EmbraceSessionService.Companion.SESSION_CACHING_INTERVAL +import java.io.Closeable +import java.util.concurrent.ExecutorService +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +internal class SessionHandler( + private val logger: InternalEmbraceLogger, + private val configService: ConfigService, + private val preferencesService: PreferencesService, + private val userService: UserService, + private val networkConnectivityService: NetworkConnectivityService, + private val metadataService: MetadataService, + private val gatingService: GatingService, + private val breadcrumbService: BreadcrumbService, + private val activityService: ActivityService, + private val ndkService: NdkService, + private val eventService: EventService, + private val remoteLogger: EmbraceRemoteLogger, + private val exceptionService: EmbraceInternalErrorService, + private val performanceInfoService: PerformanceInfoService, + private val memoryCleanerService: MemoryCleanerService, + private val deliveryService: DeliveryService, + private val webViewService: WebViewService, + private val activityLifecycleBreadcrumbService: ActivityLifecycleBreadcrumbService?, + private val thermalStatusService: ThermalStatusService, + private val nativeThreadSamplerService: NativeThreadSamplerService?, + private val clock: Clock, + private val automaticSessionStopper: ScheduledExecutorService, + private val sessionPeriodicCacheExecutorService: ScheduledExecutorService, + private val sessionExecutorService: ExecutorService +) : Closeable { + + @VisibleForTesting + var scheduledFuture: ScheduledFuture<*>? = null + + /** + * It performs all corresponding operations in order to start a session. + */ + fun onSessionStarted( + coldStart: Boolean, + startType: SessionLifeEventType, + startTime: Long, + sessionProperties: EmbraceSessionProperties, + automaticSessionCloserCallback: Runnable, + cacheCallback: Runnable + ): SessionMessage? { + if (!isAllowedToStart()) { + logger.logDebug("Session not allowed to start.") + return null + } + + logDeveloper("SessionHandler", "Session Started") + val session = Session.buildStartSession( + Uuid.getEmbUuid(), + coldStart, + startType, + startTime, + incrementAndGetSessionNumber(), + userService.loadUserInfoFromDisk(), + sessionProperties.get() + ) + logDeveloper("SessionHandler", "SessionId = ${session.sessionId}") + + // Record the connection type at the start of the session. + networkConnectivityService.networkStatusOnSessionStarted(session.startTime) + + val sessionMessage = buildStartSessionMessage(session) + + metadataService.setActiveSessionId(session.sessionId) + + // sanitize start session message before send it to backend + val sanitizedSession = gatingService.gateSessionMessage(sessionMessage) + logger.logDebug("Start session successfully sanitized.") + + deliveryService.sendSession(sanitizedSession, SessionMessageState.START) + logger.logDebug("Start session successfully sent.") + + handleAutomaticSessionStopper(automaticSessionCloserCallback) + addFirstViewBreadcrumbForSession(startTime) + startPeriodicCaching(cacheCallback) + if (configService.autoDataCaptureBehavior.isNdkEnabled()) { + ndkService.updateSessionId(session.sessionId) + } + + return sessionMessage + } + + /** + * It performs all corresponding operations in order to end a session. + */ + fun onSessionEnded( + endType: SessionLifeEventType, + originSession: Session?, + sessionProperties: EmbraceSessionProperties, + sdkStartupDuration: Long, + endTime: Long, + completedSpans: List? = null + ) { + logger.logDebug("Will try to run end session full.") + if (configService.sessionBehavior.isAsyncEndEnabled()) { + sessionExecutorService.submit { + runEndSessionFull( + endType, + originSession, + sessionProperties, + sdkStartupDuration, + endTime, + completedSpans + ) + } + } else { + runEndSessionFull( + endType, + originSession, + sessionProperties, + sdkStartupDuration, + endTime, + completedSpans + ) + } + } + + /** + * Called when a regular crash happens. It will build a session message with associated crashId, + * and send it to our servers. + */ + fun onCrash( + originSession: Session, + crashId: String, + sessionProperties: EmbraceSessionProperties, + sdkStartupDuration: Long, + completedSpans: List? = null + ) { + logger.logDebug("Will try to run end session for crash.") + runEndSessionForCrash( + originSession, + crashId, + sessionProperties, + sdkStartupDuration, + completedSpans + ) + } + + /** + * Called when periodic cache update needs to be performed. + * It will update current session 's cache state. + * + * Note that the session message will not be sent to our servers. + */ + fun getActiveSessionEndMessage( + activeSession: Session?, + sessionProperties: EmbraceSessionProperties, + sdkStartupDuration: Long, + completedSpans: List? = null + ): SessionMessage? { + return activeSession?.let { + logger.logDebug("Will try to run end session for caching.") + runEndSessionForCaching( + activeSession, + sessionProperties, + sdkStartupDuration, + completedSpans + ) + } ?: kotlin.run { + logger.logDebug("Will no perform active session caching because there is no active session available.") + null + } + } + + override fun close() { + stopPeriodicSessionCaching() + } + + private fun stopPeriodicSessionCaching() { + logger.logDebug("Stopping session caching.") + scheduledFuture?.cancel(false) + } + + /** + * If maximum timeout session is set through config, then this method starts automatic session + * stopper job, so session timeouts at given time. + */ + private fun handleAutomaticSessionStopper(automaticSessionCloserCallback: Runnable) { + // If getMaxSessionSeconds is not null, schedule the session stopper. + val maxSessionSecondsAllowed = configService.sessionBehavior.getMaxSessionSecondsAllowed() + if (maxSessionSecondsAllowed != null) { + logger.logDebug("Will start automatic session stopper.") + startAutomaticSessionStopper( + automaticSessionCloserCallback, + maxSessionSecondsAllowed + ) + } else { + logger.logDebug("Maximum session timeout not set on config. Will not start automatic session stopper.") + } + } + + /** + * It determines if we are allowed to build an end session message. + */ + private fun isAllowedToEnd(endType: SessionLifeEventType, activeSession: Session?): Boolean { + if (activeSession == null) { + logger.logDebug("No active session found. Session is not allowed to end.") + return false + } + + return when (endType) { + SessionLifeEventType.STATE -> { + // state sessions are always allowed to be ended + logger.logDebug("Session is STATE, it is always allowed to end.") + true + } + SessionLifeEventType.MANUAL, SessionLifeEventType.TIMED -> { + logger.logDebug("Session is either MANUAL or TIMED.") + if (!configService.sessionBehavior.isSessionControlEnabled()) { + logger.logWarning( + "Session control disabled from remote configuration. " + + "Session is not allowed to end." + ) + false + } else if (endType == SessionLifeEventType.MANUAL && + ((clock.now() - activeSession.startTime) < EmbraceSessionService.minSessionTime) + ) { + // If less than 5 seconds, then the session cannot be finished manually. + logger.logError("The session has to be of at least 5 seconds to be ended manually.") + false + } else { + logger.logDebug("Session allowed to end.") + true + } + } + } + } + + @Suppress("ComplexMethod") + private fun buildEndSessionMessage( + originSession: Session, + endedCleanly: Boolean, + forceQuit: Boolean, + crashId: String?, + endType: SessionLifeEventType, + sessionProperties: EmbraceSessionProperties, + sdkStartupDuration: Long, + endTime: Long, + spans: List? = null + ): SessionMessage { + val startTime: Long = originSession.startTime + + // if it's a crash session, then add the stacktrace to the session payload + val crashReportId = when { + !crashId.isNullOrEmpty() -> crashId + else -> null + } + val terminationTime = when { + forceQuit -> endTime + else -> null + } + val receivedTermination = when { + forceQuit -> true + else -> null + } + // We don't set end time for force-quit, as the API interprets this to be a clean + // termination + val endTimeVal = when { + forceQuit -> null + else -> endTime + } + + val sdkStartDuration = when (originSession.isColdStart) { + true -> sdkStartupDuration + false -> null + } + + val startupEventInfo = eventService.getStartupMomentInfo() + + val startupDuration = when (originSession.isColdStart && startupEventInfo != null) { + true -> startupEventInfo.duration + false -> null + } + val startupThreshold = when (originSession.isColdStart && startupEventInfo != null) { + true -> startupEventInfo.threshold + false -> null + } + + val betaFeatures = when (configService.sdkModeBehavior.isBetaFeaturesEnabled()) { + false -> null + else -> BetaFeatures( + thermalStates = thermalStatusService.getCapturedData(), + activityLifecycleBreadcrumbs = activityLifecycleBreadcrumbService?.getCapturedData() + ) + } + + val endSession = originSession.copy( + isEndedCleanly = endedCleanly, + appState = EmbraceSessionService.APPLICATION_STATE_FOREGROUND, + messageType = MESSAGE_TYPE_END, + eventIds = eventService.findEventIdsForSession(startTime, endTime), + infoLogIds = remoteLogger.findInfoLogIds(startTime, endTime), + warningLogIds = remoteLogger.findWarningLogIds(startTime, endTime), + errorLogIds = remoteLogger.findErrorLogIds(startTime, endTime), + networkLogIds = remoteLogger.findNetworkLogIds(startTime, endTime), + infoLogsAttemptedToSend = remoteLogger.getInfoLogsAttemptedToSend(), + warnLogsAttemptedToSend = remoteLogger.getWarnLogsAttemptedToSend(), + errorLogsAttemptedToSend = remoteLogger.getErrorLogsAttemptedToSend(), + exceptionError = exceptionService.currentExceptionError, + lastHeartbeatTime = clock.now(), + properties = sessionProperties.get(), + endType = endType, + unhandledExceptions = remoteLogger.getUnhandledExceptionsSent(), + webViewInfo = webViewService.getCapturedData(), + crashReportId = crashReportId, + terminationTime = terminationTime, + isReceivedTermination = receivedTermination, + endTime = endTimeVal, + sdkStartupDuration = sdkStartDuration, + startupDuration = startupDuration, + startupThreshold = startupThreshold, + user = userService.getUserInfo(), + betaFeatures = betaFeatures, + symbols = nativeThreadSamplerService?.getNativeSymbols(), + + ) + + val performanceInfo = performanceInfoService.getSessionPerformanceInfo( + startTime, + endTime, + originSession.isColdStart, + originSession.isReceivedTermination + ) + + return SessionMessage( + session = endSession, + userInfo = endSession.user, + appInfo = metadataService.getAppInfo(), + deviceInfo = metadataService.getDeviceInfo(), + performanceInfo = performanceInfo.copy(), + breadcrumbs = breadcrumbService.getBreadcrumbs(startTime, endTime), + spans = spans + ) + } + + private fun buildStartSessionMessage(session: Session) = SessionMessage( + session = session, + appInfo = metadataService.getAppInfo(), + deviceInfo = metadataService.getDeviceInfo() + ) + + /** + * It builds an end active session message, it sanitizes it, it performs all types of memory cleaning, + * it updates cache and it sends it to our servers. + * It also stops periodic caching and automatic session stopper. + */ + private fun runEndSessionFull( + endType: SessionLifeEventType, + originSession: Session?, + sessionProperties: EmbraceSessionProperties, + sdkStartupDuration: Long, + endTime: Long, + completedSpans: List? + ) { + if (!isAllowedToEnd(endType, originSession)) { + logger.logDebug("Session not allowed to end.") + return + } + + stopPeriodicSessionCaching() + + if (!configService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.SESSION)) { + logger.logWarning("Session messages disabled. Ignoring all Sessions.") + return + } + + val fullEndSessionMessage = buildEndSessionMessage( + /* we are previously checking in allowSessionToEnd that originSession != null */ + originSession!!, + endedCleanly = true, + forceQuit = false, + null, + endType, + sessionProperties, + sdkStartupDuration, + endTime, + completedSpans + ) + + logger.logDeveloper("SessionHandler", "End session message=$fullEndSessionMessage") + + // Clean every collection of those services which have collections in memory. + memoryCleanerService.cleanServicesCollections(exceptionService) + metadataService.removeActiveSessionId(originSession.sessionId) + logger.logDebug("Services collections successfully cleaned.") + + // Sanitize session message + val sanitizedSessionMessage = gatingService.gateSessionMessage(fullEndSessionMessage) + logger.logDeveloper( + "SessionHandler", + "Sanitized End session message=$sanitizedSessionMessage" + ) + + deliveryService.sendSession(sanitizedSessionMessage, SessionMessageState.END) + + sessionProperties.clearTemporary() + logger.logDebug("Session properties successfully temporary cleared.") + } + + /** + * It builds an end active session message, it sanitizes it, it updates cache and it sends it to our servers synchronously. + * + * This is because when a crash happens, we do not have the ability to start a background + * thread because the JVM will soon kill the process. So we force the request to be performed + * in main thread. + * + * Note that this may cause ANRs. In the future we should come up with a better approach. + * + * Also note that we do not perform any memory cleaning because since the app is about to crash, + * we do not to waste time on those things. + */ + private fun runEndSessionForCrash( + originSession: Session, + crashId: String, + sessionProperties: EmbraceSessionProperties, + sdkStartupDuration: Long, + completedSpans: List? + ) { + if (!isAllowedToEnd(SessionLifeEventType.STATE, originSession)) { + logger.logDebug("Session not allowed to end.") + return + } + + val fullEndSessionMessage = buildEndSessionMessage( + originSession, + endedCleanly = false, + forceQuit = false, + crashId, + SessionLifeEventType.STATE, + sessionProperties, + sdkStartupDuration, + clock.now(), + completedSpans + ) + logger.logDeveloper("SessionHandler", "End session message=$fullEndSessionMessage") + + // Sanitize session message + val sanitizedSessionMessage = gatingService.gateSessionMessage(fullEndSessionMessage) + logger.logDeveloper( + "SessionHandler", + "Sanitized End session message=$sanitizedSessionMessage" + ) + + deliveryService.sendSession(sanitizedSessionMessage, SessionMessageState.END_WITH_CRASH) + } + + /** + * It builds an end active session message and it updates cache. + * + * Note that it does not send the session to our servers. + */ + private fun runEndSessionForCaching( + activeSession: Session, + sessionProperties: EmbraceSessionProperties, + sdkStartupDuration: Long, + completedSpans: List? + ): SessionMessage? { + if (!isAllowedToEnd(SessionLifeEventType.STATE, activeSession)) { + logger.logDebug("Session not allowed to end.") + return null + } + + val fullEndSessionMessage = buildEndSessionMessage( + activeSession, + endedCleanly = false, + forceQuit = true, + null, + SessionLifeEventType.STATE, + sessionProperties, + sdkStartupDuration, + clock.now(), + completedSpans + ) + logger.logDeveloper("SessionHandler", "End session message=$fullEndSessionMessage") + + return fullEndSessionMessage + } + + /** + * @return session number incremented by 1 + */ + private fun incrementAndGetSessionNumber(): Int { + val sessionNumber = preferencesService.sessionNumber + 1 + preferencesService.sessionNumber = sessionNumber + return sessionNumber + } + + /** + * It starts a background job that will schedule a callback to automatically end the session. + */ + private fun startAutomaticSessionStopper( + automaticSessionStopperCallback: Runnable, + maxSessionSeconds: Int + ) { + if (configService.sessionBehavior.isAsyncEndEnabled()) { + logger.logWarning( + "Can't close the session. Automatic session closing disabled " + + "since async session send is enabled." + ) + return + } + + try { + this.automaticSessionStopper.scheduleAtFixedRate( + automaticSessionStopperCallback, + maxSessionSeconds.toLong(), + maxSessionSeconds.toLong(), + TimeUnit.SECONDS + ) + logger.logDebug("Automatic session stopper successfully scheduled.") + } catch (e: RejectedExecutionException) { + // This happens if the executor has shutdown previous to the schedule call + logger.logError("Cannot schedule Automatic session stopper.", e) + } + } + + /** + * This function add the current view breadcrumb if the app comes from background to foreground + * or replace the first session view breadcrumb possibly created before the session ir order to + * have it in the session scope time. + */ + private fun addFirstViewBreadcrumbForSession(startTime: Long) { + val screen: String? = breadcrumbService.getLastViewBreadcrumbScreenName() + if (screen != null) { + breadcrumbService.replaceFirstSessionView(screen, startTime) + } else { + val foregroundActivity = activityService.foregroundActivity + if (foregroundActivity != null) { + breadcrumbService.forceLogView( + foregroundActivity.localClassName, + startTime + ) + } + } + } + + private fun isAllowedToStart(): Boolean { + return if (!configService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.SESSION)) { + logger.logWarning("Session messages disabled. Ignoring all sessions.") + false + } else { + logger.logDebug("Session is allowed to start.") + true + } + } + + /** + * It starts a background job that will schedule a callback to do periodic caching. + */ + private fun startPeriodicCaching(cacheCallback: Runnable) { + try { + scheduledFuture = this.sessionPeriodicCacheExecutorService.scheduleAtFixedRate( + cacheCallback, + 0, + SESSION_CACHING_INTERVAL.toLong(), + TimeUnit.SECONDS + ) + logger.logDebug("Periodic session cache successfully scheduled.") + } catch (e: RejectedExecutionException) { + // This happens if the executor has shutdown previous to the schedule call + logger.logError("Cannot schedule Periodic session cache.", e) + } + } +} + +/** + * Signals to the API the start of a session. + */ +internal const val MESSAGE_TYPE_START = "st" + +/** + * Signals to the API the end of a session. + */ +private const val MESSAGE_TYPE_END = "en" diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionMessageSerializer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionMessageSerializer.kt new file mode 100644 index 0000000000..1f51c83323 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionMessageSerializer.kt @@ -0,0 +1,95 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.Session +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.payload.UserInfo + +/** + * Serializes the session message to JSON in multiple parts. This allows nodes on the JSON tree + * to be cached as a string if they have not altered since the previous serialization attempt. + */ +internal class SessionMessageSerializer( + private val serializer: EmbraceSerializer +) : MemoryCleanerListener { + + private val jsonCache = mutableMapOf() + private var prevSession: SessionMessage? = null + + fun serialize(msg: SessionMessage): String { + synchronized(this) { + val json = StringBuilder() + json.append("{") + + val session = calculateJsonValue(msg, "s", Session::class.java) { it.session } + addJsonProperty("\"s\":", session, json) + + val userInfo = calculateJsonValue(msg, "u", UserInfo::class.java) { it.userInfo } + addJsonProperty("\"u\":", userInfo, json) + + val appInfo = calculateJsonValue(msg, "a", AppInfo::class.java) { it.appInfo } + addJsonProperty("\"a\":", appInfo, json) + + val deviceInfo = calculateJsonValue(msg, "d", DeviceInfo::class.java) { it.deviceInfo } + addJsonProperty("\"d\":", deviceInfo, json) + + val performanceInfo = + calculateJsonValue(msg, "p", PerformanceInfo::class.java) { it.performanceInfo } + addJsonProperty("\"p\":", performanceInfo, json) + + val breadcrumbs = + calculateJsonValue(msg, "br", Breadcrumbs::class.java) { it.breadcrumbs } + addJsonProperty("\"br\":", breadcrumbs, json) + + val spans = calculateJsonValue(msg, "spans", List::class.java) { it.spans } + addJsonProperty("\"spans\":", spans, json) + + json.append("\"v\":") + json.append(ApiClient.MESSAGE_VERSION) + json.append("}") + prevSession = msg + return json.toString() + } + } + + private fun addJsonProperty(key: String, value: String, json: StringBuilder) { + if (value != "null") { + json.append(key) + json.append(value) + json.append(",") + } + } + + private fun calculateJsonValue( + msg: SessionMessage, + key: String, + clz: Class, + fieldProvider: (sessionMessage: SessionMessage) -> T? + ): String { + return runCatching { + val newValue = fieldProvider(msg) ?: return "null" + val oldValue: T? = prevSession?.run { fieldProvider(this) } + val cache = jsonCache[key] + val isCacheValid = newValue == oldValue + return when { + cache != null && isCacheValid -> cache + else -> return serializer.toJson(newValue, clz).apply { + jsonCache[key] = this + } + } + }.getOrElse { + "null" + } + } + + override fun cleanCollections() { + synchronized(this) { + jsonCache.clear() + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionService.kt new file mode 100644 index 0000000000..822c367923 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionService.kt @@ -0,0 +1,60 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.payload.Session.SessionLifeEventType +import java.io.Closeable + +internal interface SessionService : Closeable { + + /** + * Starts a new session. + * + * @param coldStart whether this is a cold start of the application + * @param startType the origin of the event that starts the session. + */ + fun startSession(coldStart: Boolean, startType: SessionLifeEventType, startTime: Long) + + /** + * This is responsible for the current session to be cached, ended and sent to the server and + * immediately starting a new session after that. + * + * @param endType the origin of the event that ends the session. + */ + fun triggerStatelessSessionEnd(endType: SessionLifeEventType) + + /** + * Handles an uncaught exception, ending the session and saving the session to disk. + */ + fun handleCrash(crashId: String) + + /** + * Annotates the session with a new property. Use this to track permanent and ephemeral + * features of the session. A permanent property is added to all sessions submitted from this + * device, use this for properties such as work site, building, owner. A non-permanent property + * is added to only the currently active session. + * + * + * There is a maximum of 10 total properties in a session. + * + * @param key The key for this property, must be unique within session properties + * @param value The value to store for this property + * @param permanent If true the property is applied to all sessions going forward, persist + * through app launches. + * @return A boolean indicating whether the property was added or not + */ + fun addProperty(key: String, value: String, permanent: Boolean): Boolean + + /** + * Removes a property from the session. If that property was permanent then it is removed from + * all future sessions as well. + * + * @param key the key to be removed + */ + fun removeProperty(key: String): Boolean + + /** + * Get a read-only representation of the currently set session properties. You can query and + * read from this representation however setting values in this object will not modify the + * actual properties in the session. To modify session properties see addProperty. + */ + fun getProperties(): Map +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpan.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpan.kt new file mode 100644 index 0000000000..041ff5d065 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpan.kt @@ -0,0 +1,76 @@ +package io.embrace.android.embracesdk.spans + +import io.embrace.android.embracesdk.BetaApi + +/** + * Represents a Span that can be started and stopped with the appropriate [ErrorCode] if applicable. This wraps the OpenTelemetry Span + * by adding an additional layer for local validation + */ +@BetaApi +public interface EmbraceSpan { + /** + * ID of the Trace that this Span belongs to. The format adheres to the OpenTelemetry standard for Trace IDs + */ + public val traceId: String? + + /** + * ID of the Span. The format adheres to the OpenTelemetry standard for Span IDs + */ + public val spanId: String? + + /** + * Returns true if and only if this Span has been started and has not been stopped + */ + public val isRecording: Boolean + + /** + * The Span that is the parent of this Span. If this is null, it means this Span is the root of the Trace. + */ + public val parent: EmbraceSpan? + + /** + * Start recording of the Span. Returns true if this call triggered the start of the recording. Returns false if the Span has already + * been started or has been stopped. + */ + public fun start(): Boolean + + /** + * Stop the recording of the Span with no [ErrorCode], indicating a successful completion of the underlying operation. Returns true + * if this call triggered the stopping of the recording. Returns false if the Span has not been started or if has already been stopped. + */ + public fun stop(): Boolean + + /** + * Stop the recording of the Span with an [ErrorCode], a non-null value indicating an unsuccessful completion of the underlying + * operation with the given reason. Returns true if this call triggered the stopping of the recording. Returns false if the Span has + * not been started or if has already been stopped. + */ + public fun stop(errorCode: ErrorCode?): Boolean + + /** + * Add an [EmbraceSpanEvent] with the given [name] at the current time. Returns false if the Event was definitely not successfully + * added. Returns true if the validation at the Embrace level has passed and the call to add the Event at the OpenTelemetry level was + * successful. + */ + public fun addEvent( + name: String + ): Boolean + + /** + * Add an [EmbraceSpanEvent] with the given [name]. If [time] is null, the current time will be used. Optionally, the specific + * time of the event and a set of attributes can be passed in associated with the event. Returns false if the Event was definitely not + * successfully added. Returns true if the validation at the Embrace level has passed and the call to add the Event at the + * OpenTelemetry level was successful. + */ + public fun addEvent( + name: String, + time: Long?, + attributes: Map? + ): Boolean + + /** + * Add the given key-value pair as an Attribute to the Event. Returns false if the Attribute was definitely not added. Returns true + * if the validation at the Embrace Level has passed and the call to add the Attribute at the OpenTelemetry level was successful. + */ + public fun addAttribute(key: String, value: String): Boolean +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpanEvent.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpanEvent.kt new file mode 100644 index 0000000000..7533372cfb --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/EmbraceSpanEvent.kt @@ -0,0 +1,44 @@ +package io.embrace.android.embracesdk.spans + +import com.google.gson.annotations.SerializedName +import io.embrace.android.embracesdk.BetaApi + +/** + * Represents an Event in an [EmbraceSpan] + */ +@BetaApi +public data class EmbraceSpanEvent internal constructor( + /** + * The name of the event + */ + @SerializedName("name") + val name: String, + + /** + * The timestamp of the event in nanoseconds + */ + @SerializedName("time_unix_nano") + val timestampNanos: Long, + + /** + * The attributes of this event + */ + @SerializedName("attributes") + val attributes: Map +) { + public companion object { + internal const val MAX_EVENT_NAME_LENGTH = 100 + internal const val MAX_EVENT_ATTRIBUTE_COUNT = 10 + + public fun create(name: String, timestampNanos: Long, attributes: Map?): EmbraceSpanEvent? { + if (inputsValid(name, attributes)) { + return EmbraceSpanEvent(name = name, timestampNanos = timestampNanos, attributes = attributes ?: emptyMap()) + } + + return null + } + + internal fun inputsValid(name: String, attributes: Map?) = + name.length <= MAX_EVENT_NAME_LENGTH && (attributes == null || attributes.size <= MAX_EVENT_ATTRIBUTE_COUNT) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/ErrorCode.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/ErrorCode.kt new file mode 100644 index 0000000000..b301732553 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/ErrorCode.kt @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.spans + +import io.embrace.android.embracesdk.BetaApi +import io.embrace.android.embracesdk.internal.spans.EmbraceAttributes + +/** + * Attribute to categorize the broad reason a Span completed unsuccessfully. + */ +@BetaApi +public enum class ErrorCode : EmbraceAttributes.Attribute { + + /** + * An application failure caused the Span to terminate + */ + FAILURE, + + /** + * The operation tracked by the Span was terminated because the user abandoned and canceled it before it can complete successfully. + */ + USER_ABANDON, + + /** + * The reason for the unsuccessful termination is unknown + */ + UNKNOWN; + + override val canonicalName: String = "error_code" +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/TracingApi.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/TracingApi.kt new file mode 100644 index 0000000000..989a004f67 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/spans/TracingApi.kt @@ -0,0 +1,144 @@ +package io.embrace.android.embracesdk.spans + +import io.embrace.android.embracesdk.BetaApi + +/** + * The public API used to add traces to your application. Use [isTracingAvailable] to determine if the SDK is ready log traces. Note that + * [recordCompletedSpan] methods can still be invoked successfully before the [isTracingAvailable] returns true - the actual trace won't + * be recorded until the system is ready, but the SDK will buffer the call and record it once it is. The other tracing methods, however, + * will not work until [isTracingAvailable] returns true. + */ +@BetaApi +public interface TracingApi { + /** + * Returns true if the tracing API is fully initialized so that [createSpan] and [recordSpan] methods will work. This is different than + * what [Embrace.isEnabled] returned as the tracing service is initialized asynchronously shortly after the SDK is initialized. Until + * this returns true, the [recordCompletedSpan] method can be used as invocations to it will be buffered and replayed when tracing + * service is ready to be used. + */ + @BetaApi + public fun isTracingAvailable(): Boolean + /** + * Create an [EmbraceSpan] with the given name that will be the root span of a new trace. Returns null if the [EmbraceSpan] cannot + * be created given the current conditions of the SDK or an invalid name. + */ + @BetaApi + public fun createSpan( + name: String + ): EmbraceSpan? + + /** + * Create an [EmbraceSpan] with the given name and parent. Passing in a parent that is null result in a new trace with this + * [EmbraceSpan] as its root. Returns null if the [EmbraceSpan] cannot be created, e.g if the parent has not been started, + * the name is invalid, or some other factor due to the current conditions of the SDK. + */ + @BetaApi + public fun createSpan( + name: String, + parent: EmbraceSpan? + ): EmbraceSpan? + + /** + * Execute the given block of code and record a new trace around it. If the span cannot be created, the block of code will still run and + * return correctly. If an exception or error is thrown inside the block, the span will end at the point of the throw and the + * [Throwable] will be rethrown. + */ + @BetaApi + public fun recordSpan( + name: String, + code: () -> T + ): T + + /** + * Execute the given block of code and record a new span around it with the given parent. Passing in a parent that is null will result + * in a new trace with the new span as its root. If the span cannot be created, the block of code will still run and + * return correctly. If an exception or error is thrown inside the block, the span will end at the point of the throw and the + * [Throwable] will be rethrown. + */ + @BetaApi + public fun recordSpan( + name: String, + parent: EmbraceSpan?, + code: () -> T + ): T + + /** + * Record a span with the given name as well as start and end times, which will be the root span of a new trace. + */ + @BetaApi + public fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long + ): Boolean + + /** + * Record a span with the given name, error code, as well as start and end times, which will be the root span of a new trace. A + * non-null [ErrorCode] can be passed in to denote the operation the span represents was ended unsuccessfully under the stated + * circumstances. + */ + @BetaApi + public fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + errorCode: ErrorCode? + ): Boolean + + /** + * Record a span with the given name, parent, as well as start and end times. Passing in a parent that is null will result + * in a new trace with the new span as its root. + */ + @BetaApi + public fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + parent: EmbraceSpan? + ): Boolean + + /** + * Record a span with the given name, parent, error code, as well as start and end times. Passing in a parent that is null will result + * in a new trace with the new span as its root. A non-null [ErrorCode] can be passed in to denote the operation the span represents + * was ended unsuccessfully under the stated circumstances. + */ + @BetaApi + public fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + errorCode: ErrorCode?, + parent: EmbraceSpan?, + ): Boolean + + /** + * Record a span with the given name as well as start and end times, which will be the root span of a new trace. You can also pass in + * a [Map] with [String] keys and values to be used as the attributes of the recorded span, or a [List] of [EmbraceSpanEvent] to be + * used as the events of the recorded span. + */ + @BetaApi + public fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + attributes: Map?, + events: List? + ): Boolean + + /** + * Record a span with the given name, error code, parent, as well as start and end times. Passing in a parent that is null will result + * in a new trace with the new span as its root. A non-null [ErrorCode] can be passed in to denote the operation the span represents + * was ended unsuccessfully under the stated circumstances. You can also pass in a [Map] with [String] keys and values to be used as + * the attributes of the recorded span, or a [List] of [EmbraceSpanEvent] to be used as the events of the recorded span. + */ + @BetaApi + public fun recordCompletedSpan( + name: String, + startTimeNanos: Long, + endTimeNanos: Long, + errorCode: ErrorCode?, + parent: EmbraceSpan?, + attributes: Map?, + events: List? + ): Boolean +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Consumer.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Consumer.kt new file mode 100644 index 0000000000..fccaec4c30 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Consumer.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.utils + +/** + * Backwards compatible implementation of a Java Consumer. + */ +internal fun interface Consumer { + + fun accept(s: S, t: T) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ExecutorServiceExtensions.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ExecutorServiceExtensions.kt new file mode 100644 index 0000000000..2c485564b0 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ExecutorServiceExtensions.kt @@ -0,0 +1,48 @@ +package io.embrace.android.embracesdk.utils + +import io.embrace.android.embracesdk.InternalApi +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.RejectedExecutionException + +/** + * Submit a [Callable] such that if the submit fails due to a [RejectedExecutionException], it simply does nothing + */ +@InternalApi +internal fun ExecutorService.submitSafe(task: Callable): Future? { + return try { + submit(task) + } catch (e: RejectedExecutionException) { + null + } +} + +/** + * Eagerly loads the value returned by a Lazy property on a background thread. + * + * When the Lazy property is accessed for the first time, the value is returned if it is already loaded, or the + * main thread is blocked until it finishes loading. + */ +@InternalApi +internal fun ExecutorService.eagerLazyLoad(task: Callable): Lazy { + val future = submitSafe(task) + return lazy { + if (future != null) { + try { + return@lazy future.get() + } catch (e: Exception) { + return@lazy getCallableValue(task) + } + } + getCallableValue(task) + } +} + +private fun getCallableValue(task: Callable): T { + return try { + task.call() + } catch (e: Exception) { + throw IllegalStateException("Failed to load property.", e) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Function.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Function.kt new file mode 100644 index 0000000000..2de3d1a450 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/Function.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.utils + +/** + * Backwards compatible implementation of a Java Function. + */ +internal fun interface Function { + + fun apply(t: T): R +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ListExtensions.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ListExtensions.kt new file mode 100644 index 0000000000..220d39f24f --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ListExtensions.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.utils + +/** + * Safe-wrapper around list's subscript. + * Returns the element at the specified index. + * Returns null if the index is out of bounds, instead of the out-of-bounds exception. + */ +internal fun List.at(index: Int): T? { + return if (index >= 0 && index < count()) { + this[index] + } else { + null + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt new file mode 100644 index 0000000000..1df11d48f8 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt @@ -0,0 +1,108 @@ +package io.embrace.android.embracesdk.utils + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logWarning +import java.net.MalformedURLException +import java.net.URL +import java.nio.charset.Charset +import java.util.regex.Pattern + +internal object NetworkUtils { + + private const val TRACE_ID_MAXIMUM_ALLOWED_LENGTH = 64 + private const val DNS_PATTERN = + "([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,63}[a-zA-Z0-9])?)(\\.[a-zA-Z]{1,63})(\\.[a-zA-Z]{1,2})?$" + private const val IPV4_PATTERN = + "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" + private const val IPV6_PATTERN = "(([a-fA-F0-9]{1,4}|):){1,7}([a-fA-F0-9]{1,4}|:)" + + private val IpAddrPattern = Pattern.compile("$IPV4_PATTERN|$IPV6_PATTERN") + private val DomainPattern = Pattern.compile("$DNS_PATTERN|$IPV4_PATTERN|$IPV6_PATTERN") + + @JvmStatic + fun getValidTraceId(traceId: String?): String? { + if (traceId == null) { + logDebug("Ignoring null traceId") + return null + } + + if (!Charset.forName("US-ASCII").newEncoder().canEncode(traceId)) { + logDebug("Relative path must not contain unicode characters. Relative path $traceId will be ignored.") + return null + } + + return if (traceId.length > TRACE_ID_MAXIMUM_ALLOWED_LENGTH) { + logWarning("Truncating traceId to ${traceId.length} characters") + traceId.substring(0, TRACE_ID_MAXIMUM_ALLOWED_LENGTH) + } else { + traceId + } + } + + /** + * Gets the host of a URL. + * + * @param originalUrl the URL + * @return the hostname or IP address + */ + @JvmStatic + fun getDomain(originalUrl: String): String? { + // This is necessary for the "new URL(url)" logic. + val url = if (!originalUrl.startsWith("http")) "http://$originalUrl" else originalUrl + + val matcher = try { + DomainPattern.matcher(URL(url).host) + } catch (ignored: MalformedURLException) { + DomainPattern.matcher(url) + } + + return when { + matcher.find() -> matcher.group(0) + else -> null + } + } + + /** + * Tests whether a hostname is an IP address + * + * @param domain the hostname to test + * @return true if the domain is an IP address, false otherwise + */ + @JvmStatic + fun isIpAddress(domain: String?) = + if (domain == null) false else IpAddrPattern.matcher(domain).find() + + /** + * Strips off the query string and hash fragment from a URL. + * + * @param url the URL to parse + * @return the URL with the hash fragment and query string parameters removed + */ + @JvmStatic + fun stripUrl(url: String?): String? { + if (url == null) { + return null + } + + val pathPos: Int = url.lastIndexOf('/') + val suffix: String = if (pathPos < 0) url else url.substring(pathPos) + + val queryPos = suffix.indexOf('?') + val fragmentPos = suffix.indexOf('#') + + val queryPosResult = if (queryPos < 0) Int.MAX_VALUE else queryPos + val fragmentPosResult = if (fragmentPos < 0) Int.MAX_VALUE else fragmentPos + + val terminalPos = queryPosResult.coerceAtMost(fragmentPosResult) + + return url.substring( + 0, + (if (pathPos < 0) 0 else pathPos) + suffix.length.coerceAtMost(terminalPos) + ) + } + + @JvmStatic + fun isNetworkSpanForwardingEnabled(configService: ConfigService?): Boolean = + configService?.networkSpanForwardingBehavior?.isNetworkSpanForwardingEnabled() ?: false +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/PropertyUtils.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/PropertyUtils.kt new file mode 100644 index 0000000000..1457a0b6b2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/PropertyUtils.kt @@ -0,0 +1,71 @@ +package io.embrace.android.embracesdk.utils + +import android.os.Parcelable +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logWarning +import java.io.Serializable +import java.util.AbstractMap.SimpleEntry + +/** + * Utility to for sanitizing user-supplied properties. + */ +internal object PropertyUtils { + + const val MAX_PROPERTY_SIZE = 10 + + /** + * This method will normalize the map by applying the following rules: + * + * - Null key registries will be discarded. + * - Null value registries will be renamed to null as a String. + * - Cap the properties map to a maximum of [PropertyUtils.MAX_PROPERTY_SIZE] properties. + * + * @param properties properties to be normalized. + * @return a normalized Map of the provided properties. + */ + @Suppress("UNCHECKED_CAST") + @JvmStatic + fun sanitizeProperties(properties: Map?): Map { + if (properties == null) { + return HashMap() + } + if (properties.size > MAX_PROPERTY_SIZE) { + val msg = + "The maximum number of properties is " + MAX_PROPERTY_SIZE + ", the rest will be ignored." + logWarning(msg) + } + val sanitizedEntries = properties.entries + .mapNotNull { + when { + it.key != null -> it as Map.Entry + else -> null + } + } + .take(MAX_PROPERTY_SIZE) + .map(::mapNullValue) + + val map: MutableMap = HashMap() + sanitizedEntries.forEach { (key, value) -> + if (key != null && value != null) { + map[key] = value + } + } + return map + } + + private fun mapNullValue(entry: Map.Entry): Map.Entry { + return SimpleEntry(entry.key, checkIfSerializable(entry.key, entry.value)) + } + + private fun checkIfSerializable(key: String, value: Any?): Any { + if (value == null) { + return "null" + } + if (!(value is Parcelable || value is Serializable)) { + val msg = + "The property with key $key has an entry that cannot be serialized. It will be ignored." + logWarning(msg) + return "not serializable" + } + return value + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/StreamUtils.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/StreamUtils.kt new file mode 100644 index 0000000000..1c703a4bb6 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/StreamUtils.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk.utils + +/** + * Backwards compatibility streaming for Java, implemented via Kotlin. + */ +internal inline fun stream( + collection: Collection, + function: (T) -> Unit +) = collection.toList().forEach(function) + +/** + * Backwards compatibility filtering for Java, implemented via Kotlin. + */ +internal inline fun filter( + collection: Collection, + function: (T) -> Boolean +) = collection.toList().filter(function) + +/** + * Backwards compatibility mapping for Java, implemented via Kotlin. + */ +internal inline fun map( + collection: Collection, + function: (T) -> R +) = collection.toList().map(function) diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ThreadUtils.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ThreadUtils.kt new file mode 100644 index 0000000000..ca116b0289 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/ThreadUtils.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.utils + +import android.os.Handler +import android.os.Looper +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logError + +internal object ThreadUtils { + + private val mainLooper = Looper.getMainLooper() + private val mainThread = mainLooper.thread + + fun runOnMainThread(runnable: Runnable) { + val wrappedRunnable = Runnable { + try { + runnable.run() + } catch (ex: Exception) { + logError("Failed to run wrapped runnable on Main thread.", ex) + } + } + if (Thread.currentThread() !== mainThread) { + val mainHandler = Handler(mainLooper) + mainHandler.post(wrappedRunnable) + } else { + wrappedRunnable.run() + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/VersionChecker.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/VersionChecker.kt new file mode 100644 index 0000000000..506e405989 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/VersionChecker.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.utils + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast + +internal fun interface VersionChecker { + @ChecksSdkIntAtLeast(parameter = 0) + fun isAtLeast(min: Int): Boolean +} + +internal object BuildVersionChecker : VersionChecker { + @ChecksSdkIntAtLeast(parameter = 0) + override fun isAtLeast(min: Int) = Build.VERSION.SDK_INT >= min +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/Unchecked.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/Unchecked.kt new file mode 100644 index 0000000000..11ed79128d --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/Unchecked.kt @@ -0,0 +1,92 @@ +package io.embrace.android.embracesdk.utils.exceptions + +import io.embrace.android.embracesdk.utils.exceptions.function.CheckedSupplier +import java.lang.reflect.InvocationTargetException + +/** + * Static utility methods that convert checked exceptions to unchecked. + * + * Two `wrap()` methods are provided that can wrap an arbitrary piece of logic + * and convert checked exceptions to unchecked. + * + * A number of other methods are provided that allow a lambda block to be decorated + * to avoid handling checked exceptions. + * For example, the method [java.io.File.getCanonicalFile] throws an [java.io.IOException] + * which can be handled as follows: + * ``` + * stream.map(Unchecked.function(file -> file.getCanonicalFile()) + * ``` + * + * Each method accepts a functional interface that is defined to throw [Throwable]. + * Catching `Throwable` means that any method can be wrapped. + * Any `InvocationTargetException` is extracted and processed recursively. + * Any [Error] or [RuntimeException] is re-thrown without alteration. + * Any other exception is wrapped in a [RuntimeException]. + */ +internal object Unchecked { + + /** + * Wraps a block of code, converting checked exceptions to unchecked. + * ``` + * Unchecked.wrap(() -> { + * // any code that throws a checked exception + * } + * ``` + * + * If a checked exception is thrown it is converted to a [RuntimeException]. + * + * @param the type of the result + * @param block the code block to wrap + * @return the result of invoking the block + * @throws RuntimeException if an exception occurs + */ + @JvmStatic + fun wrap(block: CheckedSupplier): T { + return try { + block.get() + } catch (ex: Throwable) { + throw propagate(ex) + } + } + + fun wrap(block: () -> T): T = wrap(object : CheckedSupplier { + override fun get(): T = block() + }) + + /** + * Propagates `throwable` as-is if possible, or by wrapping in a `RuntimeException` if not. + * + * * If `throwable` is an `InvocationTargetException` the cause is extracted and processed recursively. + * * If `throwable` is an `InterruptedException` then the thread is interrupted + * and a `RuntimeException` is thrown. + * * If `throwable` is an `Error` or `RuntimeException` it is propagated as-is. + * * Otherwise `throwable` is wrapped in a `RuntimeException` and thrown. + * + * This method always throws an exception. The return type is a convenience to satisfy the type system + * when the enclosing method returns a value. For example: + * ``` + * T foo() { + * try { + * return methodWithCheckedException(); + * } catch (Exception e) { + * throw Unchecked.propagate(e); + * } + * } + * ``` + * + * @param throwable the `Throwable` to propagate + * @return nothing; this method always throws an exception + */ + @JvmStatic + fun propagate(throwable: Throwable?): RuntimeException { + if (throwable is InvocationTargetException) { + throw propagate(throwable.cause) + } else { + if (throwable is InterruptedException) { + Thread.currentThread().interrupt() + } + @Suppress("TooGenericExceptionThrown") // maintain backwards compat. + throw RuntimeException(throwable) + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/function/CheckedSupplier.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/function/CheckedSupplier.kt new file mode 100644 index 0000000000..267e5648c2 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/exceptions/function/CheckedSupplier.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.utils.exceptions.function + +internal interface CheckedSupplier { + + /** + * Gets a result. + * + * @return a result + * @throws Throwable if an error occurs + */ + @Throws(Throwable::class) + fun get(): R +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModule.kt new file mode 100644 index 0000000000..a08f90d6af --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModule.kt @@ -0,0 +1,46 @@ +package io.embrace.android.embracesdk.worker + +import java.io.Closeable +import java.util.concurrent.ExecutorService +import java.util.concurrent.ScheduledExecutorService + +/** + * A set of shared executors to be used throughout the SDK + */ +internal interface WorkerThreadModule : Closeable { + + /** + * Return the [ExecutorService] given the [executorName] + */ + fun backgroundExecutor(executorName: ExecutorName): ExecutorService + + /** + * Return the [ScheduledExecutorService] given the [executorName] + */ + fun scheduledExecutor(executorName: ExecutorName): ScheduledExecutorService + + /** + * This should only be invoked when the SDK is shutting down. Closing all the worker threads in production means the + * SDK will not be functional afterwards. + */ + override fun close() +} + +/** + * The key used to reference a specific shared [ExecutorService] or the [ScheduledExecutorService] that uses it + */ +internal enum class ExecutorName(internal val threadName: String) { + BACKGROUND_REGISTRATION("background-reg"), + SCHEDULED_REGISTRATION("scheduled-reg"), + CACHED_SESSIONS("cached-sessions"), + SEND_SESSIONS("send-sessions"), + DELIVERY_CACHE("delivery-cache"), + API_RETRY("api-retry"), + NATIVE_CRASH_CLEANER("native-crash-cleaner"), + NATIVE_STARTUP("native-startup"), + SESSION_CACHE_EXECUTOR("session-cache"), + REMOTE_LOGGING("remote-logging"), + SESSION("session"), + SESSION_CLOSER("session-closer"), + SESSION_CACHING("session-caching"), +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImpl.kt new file mode 100644 index 0000000000..61b4433076 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImpl.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.worker + +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ThreadFactory + +// This lint error seems spurious as it only flags methods annotated with @JvmStatic even though the accessor is generated regardless +// for lazily initialized members +internal class WorkerThreadModuleImpl : WorkerThreadModule { + + private val executors: MutableMap = ConcurrentHashMap() + + private fun fetchExecutor(executorName: ExecutorName): ScheduledExecutorService { + return executors.getOrPut(executorName) { + Executors.newSingleThreadScheduledExecutor(createThreadFactory(executorName.threadName)) + } + } + + private fun createThreadFactory(name: String): ThreadFactory = ThreadFactory { runnable: Runnable -> + Executors.defaultThreadFactory().newThread(runnable).apply { + this.name = "emb-$name" + } + } + + override fun backgroundExecutor(executorName: ExecutorName): ExecutorService = + fetchExecutor(executorName) + + override fun scheduledExecutor(executorName: ExecutorName): ScheduledExecutorService = + fetchExecutor(executorName) + + override fun close() { + executors.values.forEach(ScheduledExecutorService::shutdown) + } +} diff --git a/embrace-android-sdk/src/main/res/values/strings.xml b/embrace-android-sdk/src/main/res/values/strings.xml new file mode 100644 index 0000000000..af0e27cc6b --- /dev/null +++ b/embrace-android-sdk/src/main/res/values/strings.xml @@ -0,0 +1,16 @@ + + GOT IT + SEND ERROR LOG + CLOSE + Success! Open your dashboard + Almost done. Please check the Embrace dashboard to confirm that the information was received. + \n\nIf you’re experiencing any issues, reach out to support@embrace.io or your Embrace contact.\n + "Automatic verification has started. The app will relaunch shortly." + Hmm… Something went wrong + Please try again. If you continue to experience issues, + reach out to support@embrace.io or your Embrace contact + The verification cannot be started. Reach out to support@embrace.io or your Embrace contact + The Embrace SDK was not notified of lifecycle events, and will not report sessions. + The Embrace SDK cannot report sessions because ProcessLifecycleInitializer was not called. + Embrace SDK has found the following issues:\n[X].\nPlease reach out to support@embrace.io or your Embrace contact. + diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleListTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleListTest.kt new file mode 100644 index 0000000000..79b7cbfbae --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleListTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.payload.AnrSample +import io.embrace.android.embracesdk.payload.AnrSampleList +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class AnrSampleListTest { + + @Test + fun testIsEmpty() { + val stacktraces = AnrSampleList(emptyList()) + assertEquals(0, stacktraces.size()) + + val stacktraces2 = AnrSampleList( + listOf( + AnrSample(0, emptyList(), 0), + AnrSample(1, emptyList(), 0), + AnrSample(2, emptyList(), 0) + ) + ) + assertEquals(3, stacktraces2.size()) + val expected = listOf( + AnrSample(0, emptyList(), 0), + AnrSample(1, emptyList(), 0), + AnrSample(2, emptyList(), 0) + ) + assertEquals(expected, stacktraces2.samples) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleTest.kt new file mode 100644 index 0000000000..cc68c3ff79 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AnrSampleTest.kt @@ -0,0 +1,44 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.AnrSample +import io.embrace.android.embracesdk.payload.ThreadInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class AnrSampleTest { + + private val threadInfo = ThreadInfo( + 13, Thread.State.RUNNABLE, "my-thread", 5, + listOf( + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ) + ) + + @Test + fun testAnrTickSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("anr_tick_expected.json") + .filter { !it.isWhitespace() } + + val obj = AnrSample(156098234092, listOf(threadInfo), 2) + val observed = Gson().toJson(obj) + assertEquals(expectedInfo, observed) + } + + @Test + fun testAnrTickDeserialization() { + val json = ResourceReader.readResourceAsText("anr_tick_expected.json") + val obj = Gson().fromJson(json, AnrSample::class.java) + assertEquals(2L, obj.sampleOverheadMs) + assertEquals(156098234092, obj.timestamp) + assertEquals(listOf(threadInfo), obj.threads) + } + + @Test + fun testThreadInfoEmptyObject() { + val threadInfo = Gson().fromJson("{}", AnrSample::class.java) + assertNotNull(threadInfo) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ApiUrlBuilderTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ApiUrlBuilderTest.kt new file mode 100644 index 0000000000..ca4ac9e034 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ApiUrlBuilderTest.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.comms.api.ApiUrlBuilder +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class ApiUrlBuilderTest { + + private val configService = FakeConfigService() + private val metadataService = FakeAndroidMetadataService() + + @Test + fun testUrls() { + val builder = ApiUrlBuilder( + configService = configService, + metadataService = metadataService, + enableIntegrationTesting = false, + isDebug = false + ) + assertEquals( + "https://a-o0o0o.config.emb-api.com/v2/config?appId=o0o0o&osVersion=0.0.0" + + "&appVersion=1.0.0&deviceId=07D85B44E4E245F4A30E559BFC0D07FF", + builder.getConfigUrl() + ) + assertEquals( + "https://a-o0o0o.data.emb-api.com/v1/log/suffix", + builder.getEmbraceUrlWithSuffix("suffix") + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AppInfoTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AppInfoTest.kt new file mode 100644 index 0000000000..ac0e9e86a7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/AppInfoTest.kt @@ -0,0 +1,69 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.AppInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class AppInfoTest { + + private val info = AppInfo( + appVersion = "1.0", + appFramework = Embrace.AppFramework.NATIVE.value, + buildId = "1234", + buildType = "release", + buildFlavor = "demo", + environment = "prod", + appUpdated = false, + appUpdatedThisLaunch = false, + bundleVersion = "5ac7fe", + osUpdated = false, + osUpdatedThisLaunch = false, + sdkSimpleVersion = "5.10.0", + sdkVersion = "5.11.0", + reactNativeBundleId = "fba09c9f", + javaScriptPatchNumber = "53", + reactNativeVersion = "0.69.2", + buildGuid = "5092abc", + hostedPlatformVersion = "2019", + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("app_info_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("app_info_expected.json") + val obj = Gson().fromJson(json, AppInfo::class.java) + assertEquals("1.0", obj.appVersion) + assertEquals(Embrace.AppFramework.NATIVE.value, obj.appFramework) + assertEquals("1234", obj.buildId) + assertEquals("release", obj.buildType) + assertEquals("demo", obj.buildFlavor) + assertEquals("prod", obj.environment) + assertFalse(obj.appUpdated!!) + assertFalse(obj.appUpdatedThisLaunch!!) + assertEquals("5ac7fe", obj.bundleVersion) + assertFalse(obj.osUpdated!!) + assertFalse(obj.osUpdatedThisLaunch!!) + assertEquals("5.10.0", obj.sdkSimpleVersion) + assertEquals("5.11.0", obj.sdkVersion) + assertEquals("fba09c9f", obj.reactNativeBundleId) + assertEquals("53", obj.javaScriptPatchNumber) + assertEquals("0.69.2", obj.reactNativeVersion) + assertEquals("5092abc", obj.buildGuid) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", AppInfo::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/BetaFeaturesTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/BetaFeaturesTest.kt new file mode 100644 index 0000000000..a0938e52f2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/BetaFeaturesTest.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.payload.BetaFeatures +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class BetaFeaturesTest { + + @Test + fun `test beta features not included by default`() { + assertNull(fakeSession().betaFeatures) + } + + @Test + fun `test beta features settable via builder`() { + val be = BetaFeatures() + val msg = fakeSession().copy(betaFeatures = BetaFeatures()) + assertEquals(be, msg.betaFeatures) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/CacheableValueTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/CacheableValueTest.kt new file mode 100644 index 0000000000..f971cdb597 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/CacheableValueTest.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.internal.CacheableValue +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class CacheableValueTest { + + @Test + fun testCaching() { + var value = "test" + val cache = CacheableValue { value } + assertEquals("test", cache.value { "test" }) + assertEquals("test", cache.value { throw IllegalStateException() }) + assertEquals("test", cache.value { throw IllegalStateException() }) + + value = "foo" + assertEquals("another", cache.value { "another" }) + } + + @Test + fun testHashcode() { + var value = -1 + val cache = CacheableValue { value } + assertEquals(5, cache.value { 5 }) + assertEquals(5, cache.value { throw IllegalStateException() }) + assertEquals(5, cache.value { throw IllegalStateException() }) + + value = 79 + assertEquals(22, cache.value { 22 }) + } + + @Test(expected = IllegalStateException::class) + fun testNullNotSupported() { + val cache = CacheableValue { "test" } + assertEquals("test", cache.value { null }) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ConfigRoundTripTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ConfigRoundTripTest.kt new file mode 100644 index 0000000000..9bd0ee41bb --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ConfigRoundTripTest.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class ConfigRoundTripTest { + + /** + * Verifies that Config can be serialized then deserialized. If this fails then Config + * can't be read from the cache service. + */ + @Test + fun testConfigRoundTrip() { + val gson = Gson() + val cfg = RemoteConfig() + val json = gson.toJson(cfg) + val observed = gson.fromJson(json, RemoteConfig::class.java) + assertNotNull(observed) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeliveryCacheManagerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeliveryCacheManagerTest.kt new file mode 100644 index 0000000000..a961ee2c8f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeliveryCacheManagerTest.kt @@ -0,0 +1,343 @@ +package io.embrace.android.embracesdk + +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.comms.api.ApiRequest +import io.embrace.android.embracesdk.comms.api.EmbraceUrl +import io.embrace.android.embracesdk.comms.delivery.CacheService +import io.embrace.android.embracesdk.comms.delivery.DeliveryCacheManager +import io.embrace.android.embracesdk.comms.delivery.DeliveryFailedApiCall +import io.embrace.android.embracesdk.comms.delivery.DeliveryFailedApiCalls +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.nio.charset.Charset + +internal class DeliveryCacheManagerTest { + + private val prefix = "last_session" + private val serializer = EmbraceSerializer() + private val executor = MoreExecutors.newDirectExecutorService() + private lateinit var deliveryCacheManager: DeliveryCacheManager + private lateinit var cacheService: CacheService + private lateinit var memoryCleanerService: MemoryCleanerService + private lateinit var fakeClock: FakeClock + + companion object { + private const val clockInit = 1663800000000 + private lateinit var logger: InternalEmbraceLogger + + @BeforeClass + @JvmStatic + fun beforeClass() { + logger = InternalEmbraceLogger() + } + } + + @Before + fun before() { + cacheService = spyk(TestCacheService()) + memoryCleanerService = mockk(relaxed = true) + fakeClock = FakeClock(clockInit) + initializeSessionCacheManager() + } + + @After + fun after() { + clearAllMocks(answers = false) + } + + private fun initializeSessionCacheManager() { + deliveryCacheManager = DeliveryCacheManager( + cacheService, + executor, + logger, + fakeClock, + EmbraceSerializer() + ) + } + + @Test + fun `cache current session successfully`() { + val sessionMessage = createSessionMessage("test_cache") + val expectedBytes = + EmbraceSerializer().bytesFromPayload(sessionMessage, SessionMessage::class.java) + + val serialized = deliveryCacheManager.saveSession(sessionMessage) + + assertArrayEquals(expectedBytes, serialized) + + verify { + cacheService.cacheBytes( + "$prefix.$clockInit.test_cache.json", + expectedBytes + ) + } + + assertSessionsMatch(sessionMessage, deliveryCacheManager.loadSession("test_cache")!!) + + val expectedByteArray = + serializer.bytesFromPayload(sessionMessage, SessionMessage::class.java) + assertArrayEquals(expectedByteArray, deliveryCacheManager.loadSessionBytes("test_cache")!!) + } + + @Test + fun `session not found in cache`() { + assertNull(deliveryCacheManager.loadSession("not_found_session")) + assertNull(deliveryCacheManager.loadSessionBytes("not_found_session")) + } + + @Test + fun `manager returns null if cache service throws an exception`() { + every { cacheService.loadObject(any(), SessionMessage::class.java) } throws Exception() + + deliveryCacheManager.saveSession(createSessionMessage("exception_session")) + assertNull(deliveryCacheManager.loadSession("exception_session")) + + every { + cacheService.loadObject( + any(), + SessionMessage::class.java + ) + } answers { callOriginal() } + } + + @Test + fun `return serialized current session even if cache fails`() { + every { cacheService.cacheBytes(any(), any()) } throws Exception() + + val sessionMessage = createSessionMessage("test_cache_fails") + val expectedBytes = checkNotNull( + EmbraceSerializer().bytesFromPayload( + sessionMessage, + SessionMessage::class.java + ) + ) + + val serialized = checkNotNull(deliveryCacheManager.saveSession(sessionMessage)) + + val charset = Charset.defaultCharset() + val expectedStr = String(expectedBytes, charset) + val observedStr = String(serialized) + assertEquals(expectedStr, observedStr) + + every { cacheService.cacheBytes(any(), any()) } answers { callOriginal() } + } + + @Test + fun `remove cached session successfully`() { + assertNull(deliveryCacheManager.loadSession("test_remove")) + + val session = createSessionMessage("test_remove") + deliveryCacheManager.saveSession(session) + + val cachedSession = deliveryCacheManager.loadSession("test_remove") + assertNotNull(cachedSession) + assertSessionsMatch(session, cachedSession!!) + + deliveryCacheManager.deleteSession("test_remove") + + verify(exactly = 1) { cacheService.deleteFile("$prefix.$clockInit.test_remove.json") } + } + + @Test + fun `if an exception is thrown, then remove cache session should not fail`() { + every { cacheService.deleteFile(any()) } throws Exception() + + deliveryCacheManager.saveSession(createSessionMessage("test_delete_exception")) + deliveryCacheManager.deleteSession("test_delete_exception") + + verify { + cacheService.deleteFile( + getCachedSessionName( + "test_delete_exception", + 1663800000000 + ) + ) + } + + every { cacheService.deleteFile(any()) } answers { callOriginal() } + } + + @Test + fun `read cached sessions`() { + cacheService.cacheBytes( + getCachedSessionName("session1", clockInit - 300000), + "{ cached_session }".toByteArray() + ) + cacheService.cacheBytes( + getCachedSessionName("session2", clockInit - 360000), + "{ cached_session }".toByteArray() + ) + cacheService.cacheBytes( + getCachedSessionName("session3", clockInit - 420000), + "{ cached_session }".toByteArray() + ) + + assertEquals( + setOf("session1", "session2", "session3"), + deliveryCacheManager.getAllCachedSessionIds().toSet() + ) + } + + @Test + fun `malformed file names do not trigger an exception`() { + cacheService.cacheBytes("$prefix.session1.json", "{ cached_session }".toByteArray()) + cacheService.cacheBytes("$prefix.$clockInit.json", "{ cached_session }".toByteArray()) + cacheService.cacheBytes("$prefix..json", "{ cached_session }".toByteArray()) + cacheService.cacheBytes( + "$prefix.session1.$clockInit.json", + "{ cached_session }".toByteArray() + ) + + assertTrue(deliveryCacheManager.getAllCachedSessionIds().isEmpty()) + } + + @Test + fun `amount of cached sessions in file is limited`() { + repeat(100) { i -> + deliveryCacheManager.saveSession(createSessionMessage("test$i")) + fakeClock.tick() + } + for (i in 0..99) { + verify(exactly = 1) { + cacheService.cacheBytes( + eq( + getCachedSessionName( + "test$i", + clockInit + i + ) + ), + any() + ) + } + } + for (i in 0..(99 - DeliveryCacheManager.MAX_SESSIONS_CACHED)) { + verify(exactly = 1) { cacheService.deleteFile("$prefix.${clockInit + i}.test$i.json") } + } + + val cachedSessions = deliveryCacheManager.getAllCachedSessionIds() + assertEquals(DeliveryCacheManager.MAX_SESSIONS_CACHED, cachedSessions.size) + for (i in (100 - DeliveryCacheManager.MAX_SESSIONS_CACHED)..99) { + assertTrue(cachedSessions.contains("test$i")) + } + } + + @Test + fun `check for a session saved in previous versions of the SDK`() { + val session = createSessionMessage("previous_sdk_session") + cacheService.cacheObject("last_session.json", session, SessionMessage::class.java) + + initializeSessionCacheManager() + + val allSessions = deliveryCacheManager.getAllCachedSessionIds() + assertEquals(1, allSessions.size) + assertSessionsMatch(session, deliveryCacheManager.loadSession(allSessions[0])!!) + + verify { cacheService.deleteFile("last_session.json") } + } + + @Test + fun `save and load payloads`() { + val payload = "{ json payload }".toByteArray() + val cacheName = deliveryCacheManager.savePayload(payload) + + verify { cacheService.cacheBytes(cacheName, payload) } + + assertArrayEquals(payload, deliveryCacheManager.loadPayload(cacheName)) + } + + @Test + fun `save and load failed api calls`() { + val failedCalls = DeliveryFailedApiCalls() + val request1 = ApiRequest( + url = EmbraceUrl.getUrl("http://test.url"), + httpMethod = HttpMethod.POST, + appId = "test_app_id_1", + deviceId = "test_device_id", + eventId = "request_1", + contentEncoding = "gzip" + ) + val failedApiCall1 = DeliveryFailedApiCall(request1, "payload_1.json") + failedCalls.add(failedApiCall1) + + val request2 = ApiRequest( + url = EmbraceUrl.getUrl("http://test.url"), + httpMethod = HttpMethod.POST, + appId = "test_app_id", + deviceId = "test_device_id", + eventId = "request_2", + contentEncoding = "gzip" + ) + + val failedApiCall2 = DeliveryFailedApiCall(request2, "payload_2.json") + failedCalls.add(failedApiCall2) + + val request3 = ApiRequest( + url = EmbraceUrl.getUrl("http://test.url"), + httpMethod = HttpMethod.POST, + appId = "test_app_id", + deviceId = "test_device_id", + eventId = "request_3", + contentEncoding = "gzip" + ) + val failedApiCall3 = DeliveryFailedApiCall(request3, "payload_3.json") + failedCalls.add(failedApiCall3) + + deliveryCacheManager.saveFailedApiCalls(failedCalls) + val cachedCalls = deliveryCacheManager.loadFailedApiCalls() + + assertEquals(3, cachedCalls.size) + assertEquals( + listOf("request_1", "request_2", "request_3"), + cachedCalls.map { failedCall -> failedCall.apiRequest.eventId } + ) + assertEquals( + listOf("payload_1.json", "payload_2.json", "payload_3.json"), + cachedCalls.map { failedCall -> failedCall.cachedPayload } + ) + } + + @Test + fun `load empty set of delivery calls if non cached`() { + val failedCalls = deliveryCacheManager.loadFailedApiCalls() + assertTrue(failedCalls.isEmpty()) + } + + private fun assertSessionsMatch(session1: SessionMessage, session2: SessionMessage) { + // SessionMessage does not implement equals, so we have to serialize to compare + assertEquals( + String(serializer.bytesFromPayload(session1, SessionMessage::class.java)!!), + String(serializer.bytesFromPayload(session2, SessionMessage::class.java)!!) + ) + } + + private fun createSessionMessage(sessionId: String): SessionMessage { + val session = fakeSession().copy( + sessionId = sessionId, + startTime = fakeClock.now() + ) + return SessionMessage(session) + } + + private fun getCachedSessionName(sessionId: String, timestamp: Long): String { + return DeliveryCacheManager.CachedSession(sessionId, timestamp).filename + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeviceInfoTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeviceInfoTest.kt new file mode 100644 index 0000000000..83b9b5b18e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DeviceInfoTest.kt @@ -0,0 +1,56 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.DeviceInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class DeviceInfoTest { + + private val info = DeviceInfo( + "samsung", + "S20", "armeabi", false, + "en-US", + 150982302, + "android", + "10.2.1", + 29, + "1080x720", + "GMT+1", + 150923, + 8 + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("device_info_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("device_info_expected.json") + val obj = Gson().fromJson(json, DeviceInfo::class.java) + assertEquals("samsung", obj.manufacturer) + assertEquals("S20", obj.model) + assertEquals("armeabi", obj.architecture) + assertEquals(false, obj.jailbroken) + assertEquals("en-US", obj.locale) + assertEquals(150982302L, obj.internalStorageTotalCapacity) + assertEquals("android", obj.operatingSystemType) + assertEquals("10.2.1", obj.operatingSystemVersion) + assertEquals(29, obj.operatingSystemVersionCode) + assertEquals("1080x720", obj.screenResolution) + assertEquals("GMT+1", obj.timezoneDescription) + assertEquals(8, obj.cores) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", DeviceInfo::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DiskUsageTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DiskUsageTest.kt new file mode 100644 index 0000000000..4e39c734c2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/DiskUsageTest.kt @@ -0,0 +1,37 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.DiskUsage +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class DiskUsageTest { + + private val info = DiskUsage( + 150982302, + 150923, + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("disk_usage_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("disk_usage_expected.json") + val obj = Gson().fromJson(json, DiskUsage::class.java) + assertEquals(150982302L, obj.appDiskUsage) + assertEquals(150923L, obj.deviceDiskFree) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", DiskUsage::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceActivityLifecycleBreadcrumbServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceActivityLifecycleBreadcrumbServiceTest.kt new file mode 100644 index 0000000000..f81d30d26e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceActivityLifecycleBreadcrumbServiceTest.kt @@ -0,0 +1,143 @@ +package io.embrace.android.embracesdk + +import android.app.Activity +import android.os.Bundle +import io.embrace.android.embracesdk.capture.crumbs.activity.EmbraceActivityLifecycleBreadcrumbService +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeSdkModeBehavior +import io.embrace.android.embracesdk.payload.ActivityLifecycleState +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.atomic.AtomicLong + +internal class EmbraceActivityLifecycleBreadcrumbServiceTest { + + private val configService = FakeConfigService( + sdkModeBehavior = fakeSdkModeBehavior( + isDebug = true + ) + ) + + @Test + fun `test breadcrumb collection`() { + val activity = mockk() + val collector = EmbraceActivityLifecycleBreadcrumbService(configService) { 1600000000L } + assertEquals(0, collector.getCapturedData().size) + + with(collector) { + onActivityPreCreated(activity, null) + onActivityPostCreated(activity, null) + onActivityPreStarted(activity) + onActivityPostStarted(activity) + onActivityPreResumed(activity) + onActivityPostResumed(activity) + onActivityPrePaused(activity) + onActivityPostPaused(activity) + onActivityPreStopped(activity) + onActivityPostStopped(activity) + onActivityPreDestroyed(activity) + onActivityPostDestroyed(activity) + } + + // check top-level data + val data = collector.getCapturedData() + val obj = data.single() + assertEquals("Activity", obj.activity) + checkNotNull(obj.data) + assertEquals(6, obj.data.size) + + // check crumbs + obj.data.forEachIndexed { index, crumb -> + assertEquals("Activity", crumb.activity) + assertEquals(1600000000L, crumb.start) + assertEquals(1600000000L, crumb.end) + assertEquals(false, crumb.bundlePresent) + val expectedState = ActivityLifecycleState.values()[index] + assertEquals(expectedState, crumb.state) + } + } + + @Test + fun `test saved state check`() { + val activity = mockk() + val bundle = mockk() + val collector = EmbraceActivityLifecycleBreadcrumbService(configService) { 0 } + collector.onActivityPreCreated(activity, bundle) + collector.onActivityPostCreated(activity, bundle) + + val obj = collector.getCapturedData() + val crumb = checkNotNull(obj.single().data).single() + assertEquals(true, crumb.bundlePresent) + } + + @Test + fun `test exceeding breadcrumb limit drops oldest`() { + val activity = mockk() + val tick = AtomicLong(-1) + val collector = + EmbraceActivityLifecycleBreadcrumbService(configService) { tick.incrementAndGet() } + + repeat(80) { + collector.onActivityPreResumed(activity) + collector.onActivityPostResumed(activity) + collector.onActivityPrePaused(activity) + collector.onActivityPostPaused(activity) + } + val obj = collector.getCapturedData() + val crumbs = checkNotNull(obj.single().data) + assertEquals(80, crumbs.size) + assertEquals(160L, crumbs.first().start) + assertEquals(318L, crumbs.last().start) + } + + @Test + fun `test breadcrumbs captured`() { + val activity = mockk() + val configService = FakeConfigService( + sdkModeBehavior = fakeSdkModeBehavior( + isDebug = true + ) + ) + + val collector = EmbraceActivityLifecycleBreadcrumbService(configService) { 1600000000L } + collector.onActivityPreStarted(activity) + collector.onActivityPostStarted(activity) + + val capturedData = collector.getCapturedData() + val obj = capturedData.single() + assertEquals("Activity", obj.activity) + + val crumb = checkNotNull(obj.data).single() + assertEquals(1600000000L, crumb.start) + assertEquals(1600000000L, crumb.end) + assertEquals(false, crumb.bundlePresent) + assertEquals(ActivityLifecycleState.ON_START, crumb.state) + } + + @Test + fun `test sending disabled`() { + val activity = mockk() + val configService = FakeConfigService( + sdkModeBehavior = fakeSdkModeBehavior( + remoteCfg = { RemoteConfig(pctBetaFeaturesEnabled = 0.0f) } + ) + ) + val collector = EmbraceActivityLifecycleBreadcrumbService(configService) { 1600000000L } + collector.onActivityPreStarted(activity) + collector.onActivityPostStarted(activity) + assertEquals(0, collector.getCapturedData().size) + } + + @Test + fun testCleanCollections() { + val activity = mockk() + val service = EmbraceActivityLifecycleBreadcrumbService(configService) { 1600000000L } + service.onActivityPreStarted(activity) + assertEquals(1, service.getCapturedData().size) + + service.cleanCollections() + assertEquals(0, service.getCapturedData().size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceRule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceRule.kt new file mode 100644 index 0000000000..37220e3f92 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceRule.kt @@ -0,0 +1,92 @@ +package io.embrace.android.embracesdk + +import android.os.Looper +import io.embrace.android.embracesdk.anr.EmbraceAnrService +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorSampler +import io.embrace.android.embracesdk.anr.detection.BlockedThreadDetector +import io.embrace.android.embracesdk.anr.detection.LivenessCheckScheduler +import io.embrace.android.embracesdk.anr.detection.TargetThreadHandler +import io.embrace.android.embracesdk.anr.detection.ThreadMonitoringState +import io.embrace.android.embracesdk.anr.sigquit.SigquitDetectionService +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.every +import io.mockk.mockk +import org.junit.rules.ExternalResource +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.atomic.AtomicReference + +/** + * A [org.junit.Rule] that creates an [EmbraceAnrService] suitable for use in tests using mostly real sub-components including: + * - [TargetThreadHandler] + * - [BlockedThreadDetector] + * - [LivenessCheckScheduler] + */ +internal class EmbraceAnrServiceRule( + val clock: FakeClock = FakeClock(), + private val scheduledExecutorSupplier: () -> T +) : ExternalResource() { + val logger = InternalEmbraceLogger() + val mockSigquitDetectionService: SigquitDetectionService = mockk(relaxed = true) + val mockAnrProcessErrorSampler: AnrProcessErrorSampler = mockk(relaxed = true) + + lateinit var fakeConfigService: FakeConfigService + lateinit var anrService: EmbraceAnrService + lateinit var livenessCheckScheduler: LivenessCheckScheduler + lateinit var state: ThreadMonitoringState + lateinit var blockedThreadDetector: BlockedThreadDetector + lateinit var cfg: AnrRemoteConfig + lateinit var anrExecutorService: T + lateinit var targetThreadHandler: TargetThreadHandler + lateinit var anrMonitorThread: AtomicReference + + override fun before() { + clock.setCurrentTime(0) + val mockLooper: Looper = mockk(relaxed = true) + every { mockLooper.thread } returns Thread.currentThread() + cfg = AnrRemoteConfig() + anrMonitorThread = AtomicReference(Thread.currentThread()) + fakeConfigService = FakeConfigService(anrBehavior = fakeAnrBehavior { cfg }) + anrExecutorService = scheduledExecutorSupplier.invoke() + state = ThreadMonitoringState(clock) + targetThreadHandler = TargetThreadHandler( + looper = mockLooper, + anrExecutorService = anrExecutorService, + anrMonitorThread = anrMonitorThread, + configService = fakeConfigService, + clock = clock + ) + blockedThreadDetector = BlockedThreadDetector( + configService = fakeConfigService, + clock = clock, + state = state, + targetThread = Thread.currentThread(), + anrMonitorThread = anrMonitorThread + ) + livenessCheckScheduler = LivenessCheckScheduler( + configService = fakeConfigService, + anrExecutor = anrExecutorService, + clock = clock, + state = state, + targetThreadHandler = targetThreadHandler, + blockedThreadDetector = blockedThreadDetector, + logger = logger, + anrMonitorThread = anrMonitorThread + ) + anrService = EmbraceAnrService( + configService = fakeConfigService, + looper = mockLooper, + logger = logger, + sigquitDetectionService = mockSigquitDetectionService, + livenessCheckScheduler = livenessCheckScheduler, + anrExecutorService = anrExecutorService, + state = state, + anrProcessErrorSampler = mockAnrProcessErrorSampler, + clock = clock, + anrMonitorThread = anrMonitorThread + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTest.kt new file mode 100644 index 0000000000..d345f75eb8 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTest.kt @@ -0,0 +1,505 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.anr.BlockedThreadListener +import io.embrace.android.embracesdk.anr.EmbraceAnrService +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorStateInfo +import io.embrace.android.embracesdk.concurrency.SingleThreadTestScheduledExecutor +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.internal.WrongThreadException +import io.embrace.android.embracesdk.payload.AnrInterval +import io.embrace.android.embracesdk.payload.AnrSample +import io.embrace.android.embracesdk.payload.AnrSampleList +import io.mockk.every +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.lang.Thread.currentThread +import java.util.concurrent.TimeUnit + +internal class EmbraceAnrServiceTest { + private lateinit var anrExecutorService: SingleThreadTestScheduledExecutor + + @Rule + @JvmField + val rule = EmbraceAnrServiceRule( + scheduledExecutorSupplier = { + anrExecutorService = SingleThreadTestScheduledExecutor() + anrExecutorService + } + ) + + @Before + fun setUp() { + anrExecutorService.reset() + anrExecutorService.submit { rule.anrMonitorThread.set(currentThread()) } + .get(1L, TimeUnit.SECONDS) + } + + @After + fun tearDown() { + anrExecutorService.shutdown() + anrExecutorService.awaitTermination(1L, TimeUnit.SECONDS) + assertFalse(anrExecutorService.executing.get()) + val lastThrowable = anrExecutorService.lastThrowable() + val lastThrowableDescription = lastThrowable?.message + assertTrue( + "The last throwable was a WrongThreadException with the message: $lastThrowableDescription", + lastThrowable == null || lastThrowable !is WrongThreadException + ) + } + + @Test + fun testFinishInitialization() { + with(rule) { + val configService = FakeConfigService() + anrExecutorService.submit { + anrService.finishInitialization( + configService + ) + }.get(1L, TimeUnit.SECONDS) + // verify the config service was changed from the bootstrapped early version + assertNotSame(this.fakeConfigService, configService) + } + } + + @Test + fun testGetAnrProcessErrors() { + with(rule) { + val startTime = 10L + every { mockAnrProcessErrorSampler.getAnrProcessErrors(startTime) } returns listOf( + anrProcessErrorStateInfo + ) + + val anrErrors = anrService.getAnrProcessErrors(startTime) + + assertTrue(anrErrors.size == 1) + assertEquals(anrProcessErrorStateInfo, anrErrors.get(0)) + } + } + + @Test + fun testListener() { + with(rule) { + val listener = FakeBlockedThreadListener() + anrService.addBlockedThreadListener(listener) + assertEquals(anrService, blockedThreadDetector.listener) + assertTrue(anrService.listeners.contains(listener)) + assertTrue(anrService.listeners.contains(mockAnrProcessErrorSampler)) + } + } + + @Test + fun testColdStartIgnored() { + with(rule) { + // cold starts should always be ignored + state.lastTargetThreadResponseMs = 1 + anrService.onForeground(true, 0L, 0L) + + // assert no ANR interval was added + assertEquals(0, anrService.stacktraceSampler.anrIntervals.size) + } + } + + @Test + fun testCleanCollections() { + with(rule) { + // assert the ANR interval was added + anrService.stacktraceSampler.anrIntervals.add(AnrInterval(0)) + assertEquals(1, anrService.stacktraceSampler.anrIntervals.size) + + // the ANR interval should be removed here + anrService.cleanCollections() + anrExecutorService.shutdownNow() + anrExecutorService.awaitTermination(1, TimeUnit.SECONDS) + assertEquals(0, anrService.stacktraceSampler.anrIntervals.size) + } + } + + @Test + fun testGetIntervals() { + with(rule) { + populateAnrIntervals(anrService) + + val anrIntervals = anrService.getCapturedData() + assertEquals(5, anrIntervals.size) + assertEquals(14000000L, anrIntervals[0].startTime) + assertEquals(15000000L, anrIntervals[1].startTime) + assertEquals(15000500L, anrIntervals[2].startTime) + assertEquals(15001000L, anrIntervals[3].startTime) + assertEquals(16000000L, anrIntervals[4].startTime) + } + } + + @Test + fun testGetIntervalsAnrInProgress() { + with(rule) { + clock.setCurrentTime(500) + blockedThreadDetector.listener = anrService + state.anrInProgress = true + + // assert only one anr interval was added from the anrInProgress flag + val anrIntervals = anrService.getCapturedData() + val interval = anrIntervals.single() + assertEquals(0, interval.startTime) + assertNull(interval.endTime) + assertEquals(500L, interval.lastKnownTime) + assertEquals(AnrInterval.Type.UI, interval.type) + assertNotNull(interval.anrSampleList) + } + } + + @Test + fun testGetIntervalsCrashInProgress() { + with(rule) { + clock.setCurrentTime(500) + blockedThreadDetector.listener = anrService + state.anrInProgress = true + + // assert only one anr interval was added from the anrInProgress flag + val anrIntervals = anrService.getCapturedData() + assertEquals(1, anrIntervals.size) + } + } + + @Test + fun testGetIntervalsWithStacktraces() { + with(rule) { + // create an ANR service with one stacktrace + clock.setCurrentTime(15020000L) + + blockedThreadDetector.listener = anrService + state.anrInProgress = true + state.lastTargetThreadResponseMs = 15000000L + anrService.processAnrTick(clock.now()) + assertEquals(1, anrService.stacktraceSampler.size()) + + // assert only one anr interval was added from the anrInProgress flag + val anrIntervals = anrService.getCapturedData() + val interval = anrIntervals.single() + assertEquals(15000000L, interval.startTime) + assertNull(interval.endTime) + assertEquals(15020000L, interval.lastKnownTime) + assertEquals(AnrInterval.Type.UI, interval.type) + + val stacktraces = interval.anrSampleList + assertNotNull(stacktraces) + val tick = checkNotNull(stacktraces?.samples?.single()) + assertEquals(15020000, tick.timestamp) + assertNotNull(tick.threads?.single()) + } + } + + @Test + fun testAnrIntervalStartAndEndTimes() { + executeSynchronouslyOnCurrentThread { + with(rule) { + val anrStartTs = 15020000L + val anrInProgressTs = 15021500L + val anrEndTs = 15023000L + clock.setCurrentTime(anrStartTs) + + blockedThreadDetector.updateAnrTracking(anrStartTs) + state.lastTargetThreadResponseMs = anrStartTs + blockedThreadDetector.onTargetThreadResponse(anrStartTs) + assertFalse(state.anrInProgress) + + blockedThreadDetector.updateAnrTracking( + anrInProgressTs + ) + assertTrue(state.anrInProgress) + + state.lastTargetThreadResponseMs = anrEndTs + blockedThreadDetector.onTargetThreadResponse(anrEndTs) + assertFalse(state.anrInProgress) + + val intervals = anrService.getCapturedData() + assertEquals(1, intervals.size) + val interval = intervals[0] + assertEquals(anrStartTs, interval.startTime) + assertEquals(anrEndTs, interval.endTime) + assertNull(interval.lastKnownTime) + } + } + } + + @Test + fun `test ANR state is reset when onForeground is executed to prevent false positive ANR`() { + val anrStartTs = 15020000L + val anrInProgressTs = 15020500L + val anrEndTs = 15023000L + with(rule) { + clock.setCurrentTime(anrStartTs) + anrService.onForeground(false, anrStartTs, anrInProgressTs) + anrService.onBackground(anrEndTs) + clock.setCurrentTime(anrEndTs) + anrService.onForeground(false, anrEndTs, anrEndTs) + // Since Looper is a mock, we execute this operation to + // ensure onMainThreadUnblocked runs and lastTargetThreadResponseMs gets updated + targetThreadHandler.onIdleThread() + val intervals = anrService.getCapturedData() + assertEquals(0, intervals.size) + } + } + + @Test + fun `test timestamps are updated if onMainThreadUnblocked runs before onMonitorThreadHeartbeat to prevent false positive ANR`() { + val anrStartTs = 15020000L + val anrInProgressTs = 15020500L + val anrEndTs = 15023000L + + with(rule) { + clock.setCurrentTime(anrStartTs) + anrService.onForeground(false, anrStartTs, anrInProgressTs) + anrService.onBackground(anrEndTs) + clock.setCurrentTime(anrEndTs) + anrService.onForeground(false, anrEndTs, anrEndTs) + targetThreadHandler.onIdleThread() + val intervals = anrService.getCapturedData() + assertEquals(0, intervals.size) + } + } + + @Test + fun testAnrCaptureLimit() { + executeSynchronouslyOnCurrentThread { + // create an ANR service with one stacktrace + with(rule) { + clock.setCurrentTime(15020000L) + val defaultLimit = 80 + val extra = 10 + val count = defaultLimit + extra + + repeat(count) { + anrService.onThreadBlockedInterval(currentThread(), clock.now()) + } + anrService.onThreadUnblocked(currentThread(), clock.now()) + + val sampler = anrService.stacktraceSampler + assertEquals(1, sampler.anrIntervals.size) + + val interval = checkNotNull(sampler.anrIntervals.first()) + val samples = checkNotNull(interval.anrSampleList).samples + assertEquals(count, samples.size) + + // after the default limit, samples are dropped samples are still serialized + assertEquals( + extra, + samples.count { sample -> + sample.threads == null && sample.code == AnrSample.CODE_SAMPLE_LIMIT_REACHED + } + ) + assertEquals( + defaultLimit, + samples.count { sample -> + sample.threads != null && sample.code == AnrSample.CODE_DEFAULT + } + ) + } + } + } + + @Test + fun testProcessAnrTickDisabled() { + with(rule) { + // create an ANR service with config that disables ANR capture + cfg = cfg.copy(pctEnabled = 0) + clock.setCurrentTime(15020000L) + anrService.processAnrTick(clock.now()) + assertEquals(0, anrService.stacktraceSampler.size()) + + // assert no anr intervals were added + val anrIntervals = anrService.getCapturedData() + assertTrue(anrIntervals.isEmpty()) + } + } + + @Test + fun testReachedAnrCaptureLimit() { + with(rule) { + cfg = cfg.copy(anrPerSession = 3) + val state = anrService.stacktraceSampler + assertFalse(state.reachedAnrStacktraceCaptureLimit()) + + state.anrIntervals.add(AnrInterval(0, anrSampleList = AnrSampleList(listOf()))) + state.anrIntervals.add(AnrInterval(0, anrSampleList = AnrSampleList(listOf()))) + state.anrIntervals.add(AnrInterval(0, anrSampleList = AnrSampleList(listOf()))) + assertFalse(state.reachedAnrStacktraceCaptureLimit()) + + state.anrIntervals.add(AnrInterval(0, anrSampleList = AnrSampleList(listOf()))) + assertTrue(state.reachedAnrStacktraceCaptureLimit()) + } + } + + @Test + fun testBelowAnrDurationThreshold() { + executeSynchronouslyOnCurrentThread { + with(rule) { + // if the lastTimeThreadUnblocked is zero this should never be true + state.lastTargetThreadResponseMs = 0 + state.lastMonitorThreadResponseMs = 0 + assertFalse(blockedThreadDetector.isAnrDurationThresholdExceeded(700)) + assertFalse(blockedThreadDetector.isAnrDurationThresholdExceeded(70000)) + } + } + } + + @Test + fun testAboveAnrDurationThreshold() { + executeSynchronouslyOnCurrentThread { + with(rule) { + // if the lastTimeThreadUnblocked is above the threshold return true + val now = 100L + state.lastTargetThreadResponseMs = now + state.lastMonitorThreadResponseMs = now + assertFalse(blockedThreadDetector.isAnrDurationThresholdExceeded(now + 500)) + assertFalse(blockedThreadDetector.isAnrDurationThresholdExceeded(now + 1000)) + assertTrue(blockedThreadDetector.isAnrDurationThresholdExceeded(now + 1001)) + assertTrue(blockedThreadDetector.isAnrDurationThresholdExceeded(now + 10000)) + } + } + } + + @Test + fun `finishing initialization adds a config service listener when google anr capture is disabled`() { + with(rule) { + // given anr capture is disabled + cfg = cfg.copy(googlePctEnabled = 0) + // when finishing initialization + anrExecutorService.submit { + anrService.finishInitialization( + fakeConfigService + ) + }.get(1L, TimeUnit.SECONDS) + + // a listener is added to config service + verify(exactly = 1) { mockSigquitDetectionService.configService = any() } + } + } + + @Test + fun testMonitorThreadTimeout() { + executeSynchronouslyOnCurrentThread { + with(rule) { + // if the last response times greatly exceed the capture threshold it indicates the + // process has been cached. We need to avoid a false positive in this case. + + val startTime = 150000000L + val endTime = 150150239L + clock.setCurrentTime(startTime) + + state.lastTargetThreadResponseMs = 0 + state.lastMonitorThreadResponseMs = startTime + clock.setCurrentTime(endTime) + + // timestamp not updated if ANR threshold is not met + assertTrue(blockedThreadDetector.isAnrDurationThresholdExceeded(startTime + 500)) + + assertEquals(0, state.lastTargetThreadResponseMs) + assertEquals(startTime, state.lastMonitorThreadResponseMs) + + // timestamp not updated if ANR threshold is met + state.lastTargetThreadResponseMs = 0 + assertTrue(blockedThreadDetector.isAnrDurationThresholdExceeded(startTime + 5000)) + + assertEquals(0, state.lastTargetThreadResponseMs) + assertEquals(startTime, state.lastMonitorThreadResponseMs) + + // timestamp only updated if cached process threshold is reached + assertFalse(blockedThreadDetector.isAnrDurationThresholdExceeded(startTime + 60001)) + + assertEquals(endTime, state.lastTargetThreadResponseMs) + assertEquals(endTime, state.lastMonitorThreadResponseMs) + } + } + } + + @Test + fun `check ANR recovery`() { + with(rule) { + clock.setCurrentTime(100000L) + targetThreadHandler.onIdleThread() + clock.tick(2000L) + anrExecutorService.submit { blockedThreadDetector.updateAnrTracking(clock.now()) } + .get(1L, TimeUnit.SECONDS) + targetThreadHandler.onIdleThread() + assertFalse(state.anrInProgress) + } + } + + @Test + fun `test forceAnrTrackingStopOnCrash stops ANR tracking but samples can still be retrieved`() { + with(rule) { + clock.setCurrentTime(14000000L) + cfg = cfg.copy(pctBgEnabled = 100) + anrService.onForeground(true, clock.now(), clock.now()) + anrExecutorService.submit { + assertTrue(state.started.get()) + } + populateAnrIntervals(anrService) + anrService.forceAnrTrackingStopOnCrash() + val anrIntervals = anrService.getCapturedData() + assertEquals(5, anrIntervals.size) + assertFalse(state.started.get()) + } + } + + private fun populateAnrIntervals(anrService: EmbraceAnrService) { + val state = anrService.stacktraceSampler + state.anrIntervals.add(AnrInterval(startTime = 14000000L)) + state.anrIntervals.add(AnrInterval(startTime = 15000000L)) + state.anrIntervals.add(AnrInterval(startTime = 15000500L)) + state.anrIntervals.add(AnrInterval(startTime = 15001000L)) + state.anrIntervals.add(AnrInterval(startTime = 16000000L)) + } + + /** + * Some tests require running functions meant to be run in the ANR monitoring thread synchronously on the current thread to simulate + * conditions that are to be tested. This allows the [enforceThread] to not fail by temporarily switching out the thread to be + * compared against. + */ + private fun executeSynchronouslyOnCurrentThread(action: () -> Unit) { + with(rule) { + synchronized(anrMonitorThread) { + val previousAnrMonitoringThread = anrMonitorThread.get() + anrMonitorThread.set(currentThread()) + action() + anrMonitorThread.set(previousAnrMonitoringThread) + } + } + } + + class FakeBlockedThreadListener : BlockedThreadListener { + var blockedCount = 0 + var unblockedCount = 0 + var intervalCount = 0 + + override fun onThreadBlocked(thread: Thread, timestamp: Long) { + blockedCount++ + } + + override fun onThreadBlockedInterval(thread: Thread, timestamp: Long) { + intervalCount++ + } + + override fun onThreadUnblocked(thread: Thread, timestamp: Long) { + unblockedCount++ + } + } + + companion object { + val anrProcessErrorStateInfo = AnrProcessErrorStateInfo( + "tag", + "shortMsg", + "longMsg", + "stacktrace" + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTimingTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTimingTest.kt new file mode 100644 index 0000000000..cefe312aaa --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAnrServiceTimingTest.kt @@ -0,0 +1,55 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.fakes.FakeClock +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.lang.Thread.currentThread +import java.util.concurrent.atomic.AtomicReference + +/** + * Tests for the [EmbraceAnrService] that verifies behaviour when a specific order of events happen + */ +internal class EmbraceAnrServiceTimingTest { + + private val clock = FakeClock() + + @Rule + @JvmField + val rule = EmbraceAnrServiceRule( + clock = clock, + scheduledExecutorSupplier = { BlockingScheduledExecutorService(fakeClock = clock) } + ) + + private lateinit var anrExecutorService: BlockingScheduledExecutorService + + @Before + fun setUp() { + anrExecutorService = checkNotNull(rule.anrExecutorService) + anrExecutorService.execute { + rule.anrMonitorThread = AtomicReference(currentThread()) + } + anrExecutorService.runCurrentlyBlocked() + } + + @Test + fun `check ANR recovery`() { + with(rule) { + clock.setCurrentTime(100000L) + anrService.finishInitialization(fakeConfigService) + anrExecutorService.runCurrentlyBlocked() + targetThreadHandler.onIdleThread() + anrExecutorService.runCurrentlyBlocked() + repeat(20) { + anrExecutorService.moveForwardAndRunBlocked(100L) + } + assertTrue(state.anrInProgress) + targetThreadHandler.onIdleThread() + anrExecutorService.runCurrentlyBlocked() + assertFalse(state.anrInProgress) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceApplicationExitInfoServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceApplicationExitInfoServiceTest.kt new file mode 100644 index 0000000000..bd97797b92 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceApplicationExitInfoServiceTest.kt @@ -0,0 +1,407 @@ +package io.embrace.android.embracesdk + +import android.app.ActivityManager +import android.app.ApplicationExitInfo +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.capture.aei.EmbraceApplicationExitInfoService +import io.embrace.android.embracesdk.config.remote.AppExitInfoConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.fakes.fakeAppExitInfoBehavior +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.IOException + +private const val TIMESTAMP = 15000000000L +private const val PID = 6952 +private const val IMPORTANCE = 125 +private const val PSS = 1509123409L +private const val RSS = 1123409L +private const val REASON = 4 +private const val STATUS = 1 +private const val DESCRIPTION = "testDescription" +private const val TRACE = "testInputStream" +private const val SESSION_ID = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" + +internal class EmbraceApplicationExitInfoServiceTest { + + private lateinit var applicationExitInfoService: EmbraceApplicationExitInfoService + + private val executorService = MoreExecutors.newDirectExecutorService() + + private var appExitInfoConfig = AppExitInfoConfig(pctAeiCaptureEnabled = 100.0f) + private val configService = FakeConfigService( + appExitInfoBehavior = fakeAppExitInfoBehavior { + RemoteConfig(appExitInfoConfig = appExitInfoConfig) + } + ) + + private val deliveryService = FakeDeliveryService() + private val preferenceService = FakePreferenceService() + + private val mockActivityManager: ActivityManager = mockk { + every { getHistoricalProcessExitReasons(any(), any(), any()) } returns emptyList() + } + + private val mockAppExitInfo = mockk(relaxed = true) { + every { timestamp } returns TIMESTAMP + every { pid } returns PID + every { processStateSummary } returns SESSION_ID.toByteArray() + every { importance } returns IMPORTANCE + every { pss } returns PSS + every { reason } returns REASON + every { rss } returns RSS + every { status } returns STATUS + every { description } returns DESCRIPTION + every { traceInputStream } returns TRACE.byteInputStream() + } + + companion object { + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + private fun startApplicationExitInfoService() { + applicationExitInfoService = EmbraceApplicationExitInfoService( + executorService, + configService, + mockActivityManager, + preferenceService, + deliveryService + ) + } + + @Test + fun `AEI data capture happy path`() { + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + + startApplicationExitInfoService() + + // when getCapturedData is called + with(getLastAeiRequest()) { + assertEquals(TIMESTAMP, timestamp) + assertEquals(SESSION_ID, sessionId) + assertEquals(IMPORTANCE, importance) + assertEquals(PSS, pss) + assertEquals(RSS, rss) + assertEquals(STATUS, status) + assertEquals(DESCRIPTION, description) + assertEquals(TRACE, trace) + assertEquals("", sessionIdError) + assertNull(traceStatus) + } + } + + @Test + fun `service should stop execution if remote config changed to disabled`() { + // given a service starts with config enabled + appExitInfoConfig = AppExitInfoConfig(pctAeiCaptureEnabled = 100.0f) + startApplicationExitInfoService() + + // when config changes to disabled + appExitInfoConfig = AppExitInfoConfig(pctAeiCaptureEnabled = 0.0f) + applicationExitInfoService.onConfigChange(configService) + + // then background execution should stop + assertNull(applicationExitInfoService.backgroundExecution) + } + + @Test + fun `service should start execution if remote config changed to enabled`() { + // given a service starts with config disabled + appExitInfoConfig = AppExitInfoConfig(pctAeiCaptureEnabled = 0.0f) + startApplicationExitInfoService() + + // when config changes to enabled + appExitInfoConfig = AppExitInfoConfig(pctAeiCaptureEnabled = 100.0f) + applicationExitInfoService.onConfigChange(configService) + + // then background execution should start + assertNotNull(applicationExitInfoService.backgroundExecution) + } + + @Test + fun `getCapturedData should return an empty list when getHistoricalProcessExitInfo returns an empty list`() { + // given getHistoricalProcessExitReasons returns an empty list + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns emptyList() + startApplicationExitInfoService() + + // when getCapturedData is called + val capturedData = applicationExitInfoService.getCapturedData() + + // then an empty list should be returned + assertTrue(capturedData.isEmpty()) + } + + @Test + fun `getHistoricalProcessExitInfo should truncate to 32 entries`() { + // given getHistoricalProcessExitReasons returns more than 32 entries + val appExitInfoListWithMoreThan32Entries = mutableListOf() + repeat(33) { + appExitInfoListWithMoreThan32Entries.add(mockAppExitInfo) + } + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns appExitInfoListWithMoreThan32Entries + + startApplicationExitInfoService() + + // when getCapturedData is called + val capturedData = applicationExitInfoService.getCapturedData() + + // then captured data should only have 32 entries + assertEquals(32, capturedData.size) + } + + @Test + fun `getUnsentExitReasons should not return AEI that have already been sent`() { + // given getHistoricalProcessExitReasons returns 3 entries, but there are 2 that have already been sent + val appExitInfo1 = mockk(relaxed = true) { + every { timestamp } returns 1L + every { pid } returns STATUS + } + val appExitInfo2 = mockk(relaxed = true) { + every { timestamp } returns 2L + every { pid } returns 2 + } + val appExitInfo3 = mockk(relaxed = true) { + every { timestamp } returns 3L + every { pid } returns 3 + } + + val appExitInfo1Hash = "${appExitInfo1.timestamp}_${appExitInfo1.pid}" + val appExitInfo2Hash = "${appExitInfo2.timestamp}_${appExitInfo2.pid}" + + every { mockActivityManager.getHistoricalProcessExitReasons(any(), any(), any()) } returns + listOf(appExitInfo1, appExitInfo2, appExitInfo3) + + preferenceService.applicationExitInfoHistory = setOf( + appExitInfo1Hash, + appExitInfo2Hash + ) + + startApplicationExitInfoService() + + // when getCapturedData is called + val capturedData = applicationExitInfoService.getCapturedData() + + // then captured data should only have applicationExitInfo3 + assertEquals(3L, capturedData[0].timestamp) + assertEquals(STATUS, capturedData.size) + } + + @Test + fun `getCapturedData gets AEI without traces`() { + // given an AEI with a trace + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + startApplicationExitInfoService() + + // when getCapturedData is called + val capturedData = applicationExitInfoService.getCapturedData() + + // then it shouldn't have a trace, and should retrieve the correct data + assertEquals(SESSION_ID, capturedData[0].sessionId) + assertEquals(null, capturedData[0].trace) + assertEquals(null, capturedData[0].traceStatus) + assertEquals("", capturedData[0].sessionIdError) + assertEquals(STATUS, capturedData.size) + } + + @Test + fun `invalid session id should show up in ApplicationExitInfoData sessionIdError`() { + // given an AEI with an invalid session ID + val invalidSessionId = "_ 1NV@lid" + every { mockAppExitInfo.processStateSummary } returns invalidSessionId.toByteArray() + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + startApplicationExitInfoService() + + // when getCapturedData is called + val capturedData = applicationExitInfoService.getCapturedData() + + // then the invalid session ID message should be added to the sessionIdError + assertEquals("invalid session ID: $invalidSessionId", capturedData[0].sessionIdError) + assertEquals(invalidSessionId, capturedData[0].sessionId) + } + + @Test + fun `null traces won't be sent to the blob endpoint`() { + // given an application exit info with a null trace + every { mockAppExitInfo.traceInputStream } returns null + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + + // when the service is started + startApplicationExitInfoService() + + // then no null traces should be sent + assertTrue(deliveryService.appExitInfoRequests.isEmpty()) + } + + @Test + fun `OOM while reading trace`() { + // given an OOM happens when reading a trace + every { mockAppExitInfo.traceInputStream } throws OutOfMemoryError("Ouch") + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + + // when the service is started + startApplicationExitInfoService() + + // then a null trace should be sent + val payload = checkNotNull(getLastAeiRequest()) + assertNull(payload.trace) + assertEquals("oom: Ouch", payload.traceStatus) + } + + @Test + fun `IOException while reading trace`() { + // given an IO exception happens when reading a trace + every { mockAppExitInfo.traceInputStream } throws IOException("Ouch") + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + + // when the service is started + startApplicationExitInfoService() + + // then a null trace should be sent + val payload = getLastAeiRequest() + assertNull(payload.trace) + assertEquals("ioexception: Ouch", payload.traceStatus) + } + + @Test + fun `other error while reading trace`() { + val errorMessage = "Please turn your computer screen back on." + // given an IO exception happens when reading a trace + every { mockAppExitInfo.traceInputStream } throws IllegalMonitorStateException(errorMessage) + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + + // when the service is started + startApplicationExitInfoService() + + // then a null trace should be sent + val payload = getLastAeiRequest() + assertNull(payload.trace) + assertEquals("error: $errorMessage", payload.traceStatus) + } + + @Test + fun `Truncate trace if it exceeds limit`() { + // given a trace that exceeds the limit + every { mockAppExitInfo.traceInputStream } returns "a".repeat(500).byteInputStream() + + appExitInfoConfig = + AppExitInfoConfig(pctAeiCaptureEnabled = 100.0f, appExitInfoTracesLimit = 100) + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns listOf(mockAppExitInfo) + + // when the service is started + startApplicationExitInfoService() + + // then a null trace should be sent + val payload = getLastAeiRequest() + assertEquals("a".repeat(100), payload.trace) + } + + @Test + fun testActivityManagerException() { + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } throws NullPointerException() + + // when the service is started + startApplicationExitInfoService() + + // then a null trace should be sent + assertTrue(deliveryService.appExitInfoRequests.isEmpty()) + } + + @Test + fun `one object sent per payload`() { + val entries = (0..32).map { mockAppExitInfo } + every { + mockActivityManager.getHistoricalProcessExitReasons( + any(), + any(), + any() + ) + } returns entries + + startApplicationExitInfoService() + + // each AEI object with a trace should be sent in a separate payload + val payloads = checkNotNull(deliveryService.appExitInfoRequests) + assertEquals(32, payloads.size) + } + + private fun getLastAeiRequest() = deliveryService.appExitInfoRequests.single().single() +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAutomaticVerificationTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAutomaticVerificationTest.kt new file mode 100644 index 0000000000..e346e67984 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceAutomaticVerificationTest.kt @@ -0,0 +1,115 @@ +package io.embrace.android.embracesdk + +import android.app.Activity +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.runs +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.io.IOException +import java.util.concurrent.ExecutorService +import java.util.concurrent.ScheduledExecutorService + +internal class EmbraceAutomaticVerificationTest { + + companion object { + private lateinit var embraceSamples: EmbraceAutomaticVerification + private val activity: Activity = mockk(relaxed = true) + private val scheduledExecutorService: ScheduledExecutorService = mockk(relaxed = true) + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(ScheduledExecutorService::class) + mockkStatic(ExecutorService::class) + mockkStatic(EmbraceImpl::class) + mockkStatic(Embrace::class) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @Before + fun setup() { + every { Embrace.getImpl() } returns mockk(relaxed = true) + embraceSamples = EmbraceAutomaticVerification(scheduledExecutorService) + } + + @After + fun after() { + clearAllMocks( + answers = false, + staticMocks = false, + objectMocks = false + ) + } + + @Test + fun `test runEndSession`() { + with(embraceSamples) { + every { Embrace.getInstance().endSession() } just runs + runEndSession() + verify { Embrace.getInstance().endSession() } + } + } + + @Test + fun `test startVerification that captures IOException`() { + with(embraceSamples) { + automaticVerificationChecker = mockk(relaxed = true) + activityService = FakeActivityService(foregroundActivity = activity) + verificationActions = mockk(relaxed = true) + every { automaticVerificationChecker.createFile(activity) } throws IOException("ERROR") + + startVerification() + + verify(exactly = 0) { verificationActions.runActions() } + } + } + + @Test + fun `test startVerification does not run verification steps if marker file exists`() { + with(embraceSamples) { + automaticVerificationChecker = mockk(relaxed = true) + activityService = FakeActivityService(foregroundActivity = activity) + verificationActions = mockk(relaxed = true) + every { automaticVerificationChecker.createFile(activity) } returns false + + startVerification() + + verify(exactly = 0) { + verificationActions.runActions() + } + } + } + + @Test + fun `test startVerification runs verification steps if marker file does not exist`() { + with(embraceSamples) { + automaticVerificationChecker = mockk(relaxed = true) + activityService = FakeActivityService(foregroundActivity = activity) + verificationActions = mockk(relaxed = true) + every { automaticVerificationChecker.createFile(any() as Activity) } returns true + every { verificationActions.runActions() } just runs + + startVerification() + + verify(exactly = 1) { + verificationActions.runActions() + } + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCacheServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCacheServiceTest.kt new file mode 100644 index 0000000000..e76bf6becc --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCacheServiceTest.kt @@ -0,0 +1,223 @@ +package io.embrace.android.embracesdk + +import android.content.Context +import io.embrace.android.embracesdk.comms.api.ApiRequest +import io.embrace.android.embracesdk.comms.api.EmbraceUrl +import io.embrace.android.embracesdk.comms.delivery.CacheService +import io.embrace.android.embracesdk.comms.delivery.DeliveryFailedApiCall +import io.embrace.android.embracesdk.comms.delivery.DeliveryFailedApiCalls +import io.embrace.android.embracesdk.comms.delivery.EmbraceCacheService +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.File +import java.nio.file.Files + +internal class EmbraceCacheServiceTest { + + private lateinit var context: Context + private lateinit var service: CacheService + private lateinit var dir: File + + @Before + fun setUp() { + context = mockk() + + dir = Files.createTempDirectory("tmpDirPrefix").toFile() + every { context.cacheDir } returns dir + service = EmbraceCacheService( + context, + EmbraceSerializer(), + InternalEmbraceLogger() + ) + + // always assert that nothing is in the dir + assertTrue(checkNotNull(dir.listFiles()).isEmpty()) + } + + @Test + fun `test cacheBytes and loadBytes`() { + val myBytes = "{ \"payload\": \"test_payload\"}".toByteArray() + service.cacheBytes(CUSTOM_OBJECT_1_FILE_NAME, myBytes) + val children = checkNotNull(dir.listFiles()) + val file = children.single() + assertEquals("emb_$CUSTOM_OBJECT_1_FILE_NAME", file.name) + + val loadedObject = service.loadBytes(CUSTOM_OBJECT_1_FILE_NAME) + assertArrayEquals(myBytes, loadedObject) + } + + @Test + fun `test loadBytes with non-existent file returns empty optional`() { + val loadedBytes = service.loadBytes(CUSTOM_OBJECT_1_FILE_NAME) + assertNull(loadedBytes) + } + + @Test + fun `test cacheBytes with non-writable file does not throw exception`() { + val cacheFile = File(dir, "emb_$CUSTOM_OBJECT_1_FILE_NAME") + cacheFile.writeText("locked file") + cacheFile.setReadOnly() + + val myBytes = "{ \"payload\": \"test_payload\"}".toByteArray() + service.cacheBytes(CUSTOM_OBJECT_1_FILE_NAME, myBytes) + + val loadedBytes = service.loadBytes(CUSTOM_OBJECT_1_FILE_NAME) + assertNotNull(loadedBytes) + assertArrayEquals("locked file".toByteArray(), loadedBytes) + cacheFile.delete() + } + + @Test + fun `test cacheObject and loadObject`() { + val myObject = CustomObject(CUSTOM_OBJECT_1_FILE_NAME) + service.cacheObject(CUSTOM_OBJECT_1_FILE_NAME, myObject, CustomObject::class.java) + val children = checkNotNull(dir.listFiles()) + val file = children.single() + assertEquals("emb_$CUSTOM_OBJECT_1_FILE_NAME", file.name) + + val loadedObject = service.loadObject(CUSTOM_OBJECT_1_FILE_NAME, CustomObject::class.java) + assertEquals(myObject, checkNotNull(loadedObject)) + } + + @Test + fun `test loadObject with non-existent file returns empty optional`() { + val loadedObject = service.loadObject(CUSTOM_OBJECT_1_FILE_NAME, CustomObject::class.java) + assertNull(loadedObject) + } + + @Test + fun `test loadObject with malformed file returns empty optional`() { + val myObject1 = CustomObject(CUSTOM_OBJECT_1_FILE_NAME) + service.cacheObject(CUSTOM_OBJECT_1_FILE_NAME, myObject1, CustomObject::class.java) + + val children = checkNotNull(dir.listFiles()) + val file = children.single() + file.writeText("malformed content") + + val loadedObject = service.loadObject(CUSTOM_OBJECT_1_FILE_NAME, CustomObject::class.java) + assertNull(loadedObject) + } + + @Test + fun `test deleteObject returns true and deletes the file correctly`() { + val myObject = CustomObject(CUSTOM_OBJECT_1_FILE_NAME) + service.cacheObject(CUSTOM_OBJECT_1_FILE_NAME, myObject, CustomObject::class.java) + + val deleted = service.deleteObject(CUSTOM_OBJECT_1_FILE_NAME) + val children = checkNotNull(dir.listFiles()) + + assertTrue(deleted) + assertEquals(0, children.size) + } + + @Test + fun `test deleteObject with non-existent file returns false`() { + val deleted = service.deleteObject(CUSTOM_OBJECT_1_FILE_NAME) + val children = checkNotNull(dir.listFiles()) + + assertFalse(deleted) + assertEquals(0, children.size) + } + + @Test + fun `test deleteObjectsByRegex`() { + val myObject1 = CustomObject(CUSTOM_OBJECT_1_FILE_NAME) + val myObject2 = CustomObject(CUSTOM_OBJECT_2_FILE_NAME) + val myObject3 = CustomObject(CUSTOM_OBJECT_3_FILE_NAME) + service.cacheObject(CUSTOM_OBJECT_1_FILE_NAME, myObject1, CustomObject::class.java) + service.cacheObject(CUSTOM_OBJECT_2_FILE_NAME, myObject2, CustomObject::class.java) + service.cacheObject(CUSTOM_OBJECT_3_FILE_NAME, myObject3, CustomObject::class.java) + + val deleted = service.deleteObjectsByRegex(".*object.*") + val children = checkNotNull(dir.listFiles()) + + assertTrue(deleted) + assertEquals(1, children.size) + } + + @Test + fun `test deleteObjectsByRegex with listFiles() = null returns false`() { + // In order to force File.listFiles() to return null, we make the File not to be a directory + val myDir = File("no_directory_file") + myDir.createNewFile() + every { context.cacheDir } returns myDir + + val deleted = service.deleteObjectsByRegex(".*object.*") + assertFalse(deleted) + myDir.delete() + } + + @Test + fun `test moveObject with existent source`() { + val myObject = CustomObject(CUSTOM_OBJECT_1_FILE_NAME) + service.cacheObject(CUSTOM_OBJECT_1_FILE_NAME, myObject, CustomObject::class.java) + + val moved = service.moveObject(CUSTOM_OBJECT_1_FILE_NAME, CUSTOM_OBJECT_2_FILE_NAME) + val children = checkNotNull(dir.listFiles()) + val file = children.single() + + assertTrue(moved) + assertEquals("emb_$CUSTOM_OBJECT_2_FILE_NAME", file.name) + } + + @Test + fun `test moveObject with non-existent source`() { + val moved = service.moveObject(CUSTOM_OBJECT_1_FILE_NAME, CUSTOM_OBJECT_2_FILE_NAME) + val children = checkNotNull(dir.listFiles()) + + assertFalse(moved) + assertEquals(0, children.size) + } + + @Test + fun `test DeliveryFailedApiCalls can be cached`() { + val apiRequest = ApiRequest( + httpMethod = HttpMethod.GET, + url = EmbraceUrl.getUrl("http://fake.url") + ) + val failedApiCalls = DeliveryFailedApiCalls() + failedApiCalls.add(DeliveryFailedApiCall(apiRequest, "payload_id")) + + val cacheKey = "test_failed_calls_cache" + service.cacheObject( + cacheKey, + failedApiCalls, + DeliveryFailedApiCalls::class.java + ) + val cachedFailedCalls = + service.loadObject(cacheKey, DeliveryFailedApiCalls::class.java) + + checkNotNull(cachedFailedCalls) + assertFalse(cachedFailedCalls.isEmpty()) + val cachedApiRequest = cachedFailedCalls.poll()?.apiRequest + assertNotNull(cachedApiRequest) + assertEquals(apiRequest.contentType, cachedApiRequest?.contentType) + assertEquals(apiRequest.userAgent, cachedApiRequest?.userAgent) + assertEquals(apiRequest.contentEncoding, cachedApiRequest?.contentEncoding) + assertEquals(apiRequest.accept, cachedApiRequest?.accept) + assertEquals(apiRequest.acceptEncoding, cachedApiRequest?.acceptEncoding) + assertEquals(apiRequest.appId, cachedApiRequest?.appId) + assertEquals(apiRequest.deviceId, cachedApiRequest?.deviceId) + assertEquals(apiRequest.eventId, cachedApiRequest?.eventId) + assertEquals(apiRequest.logId, cachedApiRequest?.logId) + assertEquals(apiRequest.url.toString(), cachedApiRequest?.url.toString()) + assertEquals(apiRequest.httpMethod, cachedApiRequest?.httpMethod) + } +} + +internal data class CustomObject(val name: String) + +internal const val CUSTOM_OBJECT_1_FILE_NAME = "custom_object_1.json" +internal const val CUSTOM_OBJECT_2_FILE_NAME = "custom_object_2.json" +internal const val CUSTOM_OBJECT_3_FILE_NAME = "custom_3.json" diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceConfigServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceConfigServiceTest.kt new file mode 100644 index 0000000000..413f1eddb5 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceConfigServiceTest.kt @@ -0,0 +1,316 @@ +package io.embrace.android.embracesdk + +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.comms.api.ApiService +import io.embrace.android.embracesdk.comms.api.CachedConfig +import io.embrace.android.embracesdk.comms.delivery.CacheService +import io.embrace.android.embracesdk.config.ConfigListener +import io.embrace.android.embracesdk.config.EmbraceConfigService +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.ActivityService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.robolectric.android.util.concurrent.PausedExecutorService +import java.util.concurrent.ExecutorService + +internal class EmbraceConfigServiceTest { + + private lateinit var fakePreferenceService: PreferencesService + private lateinit var service: EmbraceConfigService + private lateinit var executorService: ExecutorService + + companion object { + private lateinit var localConfig: LocalConfig + private lateinit var mockConfig: RemoteConfig + private lateinit var mockApiService: ApiService + private lateinit var activityService: ActivityService + private lateinit var mockCacheService: CacheService + private lateinit var logger: InternalEmbraceLogger + private lateinit var fakeClock: FakeClock + private lateinit var mockConfigListener: ConfigListener + private lateinit var fakeCachedConfig: RemoteConfig + + /** + * Setup before all tests get executed. Create mocks here. + */ + @BeforeClass + @JvmStatic + fun setupBeforeAll() { + mockkStatic(RemoteConfig::class) + localConfig = createLocalConfig() + mockConfig = RemoteConfig() + mockApiService = mockk() + activityService = FakeActivityService() + mockCacheService = mockk(relaxed = true) + fakeClock = FakeClock() + logger = InternalEmbraceLogger() + mockConfigListener = mockk() + fakeCachedConfig = RemoteConfig( // alter config to trigger listener + anrConfig = AnrRemoteConfig(pctIdleHandlerEnabled = 59f) + ) + } + + fun createLocalConfig(action: () -> SdkLocalConfig = { SdkLocalConfig() }): LocalConfig { + return LocalConfig("abcde", false, action()) + } + + /** + * Setup after all tests get executed. Un-mock all here. + */ + @AfterClass + @JvmStatic + fun tearDownAfterAll() { + unmockkAll() + } + } + + /** + * Setup before each test. + */ + @Before + fun setup() { + fakeClock.setCurrentTime(1000000000000) + every { mockApiService.getConfig() } returns mockConfig + fakePreferenceService = FakePreferenceService(deviceIdentifier = "07D85B44E4E245F4A30E559BFC0D07FF") + every { + mockCacheService.loadObject("config.json", RemoteConfig::class.java) + } returns fakeCachedConfig + executorService = MoreExecutors.newDirectExecutorService() + service = createService(executorService = executorService) + } + + /** + * Setup after each test. Clean mocks content. + */ + @After + fun tearDown() { + clearAllMocks() + service.close() + executorService.shutdown() + } + + @Suppress("DEPRECATION") + @Test + fun `test legacy normalized DeviceId`() { + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC0D0700" + assertEquals(0.0, service.thresholdCheck.getNormalizedDeviceId().toDouble(), 0.01) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC0D07FF" + assertEquals(100.0, service.thresholdCheck.getNormalizedDeviceId().toDouble(), 0.01) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC0D0739" + assertEquals(22.35, service.thresholdCheck.getNormalizedDeviceId().toDouble(), 0.01) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC0D07D9" + assertEquals(85.09, service.thresholdCheck.getNormalizedDeviceId().toDouble(), 0.01) + } + + @Test + fun `test new normalized DeviceId`() { + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC000000" + assertEquals(0.0, service.thresholdCheck.getNormalizedLargeDeviceId().toDouble(), 0.01) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFCFFFFFF" + assertEquals(100.0, service.thresholdCheck.getNormalizedLargeDeviceId().toDouble(), 0.01) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC0D0739" + assertEquals(5.08, service.thresholdCheck.getNormalizedLargeDeviceId().toDouble(), 0.01) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFCED0739" + assertEquals(92.58, service.thresholdCheck.getNormalizedLargeDeviceId().toDouble(), 0.01) + } + + @Test + fun `test isBehaviourEnabled`() { + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC000000" + assertFalse(service.thresholdCheck.isBehaviorEnabled(0.0f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(0.1f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(100.0f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(99.9f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(34.9f)) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFCFFFFFF" + assertFalse(service.thresholdCheck.isBehaviorEnabled(99.9f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(100.0f)) + + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFC0D0739" + assertFalse(service.thresholdCheck.isBehaviorEnabled(0.0f)) + assertFalse(service.thresholdCheck.isBehaviorEnabled(2.0f)) + assertFalse(service.thresholdCheck.isBehaviorEnabled(5.0f)) + assertFalse(service.thresholdCheck.isBehaviorEnabled(5.07f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(5.09f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(47.92f)) + assertTrue(service.thresholdCheck.isBehaviorEnabled(100.0f)) + } + + @Test + fun `test isBehaviourEnabled with bad input`() { + fakePreferenceService.deviceIdentifier = "07D85B44E4E245F4A30E559BFCFFFFFF" + assertFalse(service.thresholdCheck.isBehaviorEnabled(1000f)) + assertFalse(service.thresholdCheck.isBehaviorEnabled(-1000f)) + } + + @Test + fun `test config exists in cache and is loaded correctly`() { + assertTrue(service.anrBehavior.shouldCaptureMainThreadOnly()) + + val obj = RemoteConfig(anrConfig = AnrRemoteConfig(mainThreadOnly = false)) + every { mockApiService.getCachedConfig() } returns CachedConfig(obj, null) + service.loadConfigFromCache() + + // config was updated + assertFalse(service.anrBehavior.shouldCaptureMainThreadOnly()) + } + + @Test + fun `test config does not exist in cache, so it's not loaded`() { + assertTrue(service.anrBehavior.shouldCaptureMainThreadOnly()) + every { mockApiService.getCachedConfig() } returns CachedConfig(null, null) + service.loadConfigFromCache() + + // config was not updated + assertTrue(service.anrBehavior.shouldCaptureMainThreadOnly()) + } + + @Test + fun `test service constructor reads cached config`() { + val obj = RemoteConfig(anrConfig = AnrRemoteConfig(mainThreadOnly = false)) + every { mockApiService.getConfig() } returns null + every { mockApiService.getCachedConfig() } returns CachedConfig(obj, null) + service = createService(executorService) + + // config was updated + assertFalse(service.anrBehavior.shouldCaptureMainThreadOnly()) + } + + /** + * Test that calling getConfig() notifies the listener. + * As we are using a DirectExecutor this method will run synchronously and + * return the updated config. + * In a real situation, the async refresh would be triggered and the config returned would be the previous one. + */ + @Test + fun `test getConfig() notifies a listener`() { + // advance the clock so it's safe to retry config refresh + fakeClock.tick(1000000000000) + + // return a different object from default so listener triggers + val newConfig = RemoteConfig(anrConfig = AnrRemoteConfig(pctBgEnabled = 59)) + every { mockApiService.getConfig() } returns newConfig + fakePreferenceService.sdkDisabled = false + service.addListener(mockConfigListener) + + // call an arbitrary function to trigger a config refresh + service.anrBehavior.shouldCaptureMainThreadOnly() + verify(exactly = 1) { mockConfigListener.onConfigChange(service) } + } + + /** + * Test that calling getConfig() refreshes the config and notify the listener + * As we are using a DirectExecutor this method will run synchronously and + * return the updated config. + * In a real situation, the async refresh would be triggered and the config returned would be the previous one. + */ + @Test + fun `test onForeground() refreshes the config`() { + // advance the clock so it's safe to retry config refresh + fakeClock.tick(1000000000000) + val newConfig = RemoteConfig(anrConfig = AnrRemoteConfig()) + every { mockApiService.getConfig() } returns newConfig + fakePreferenceService.sdkDisabled = false + service.addListener(mockConfigListener) + + service.onForeground(true, 1000L, 1100L) + + verify(exactly = 1) { mockConfigListener.onConfigChange(service) } + } + + @Test + fun `test onForeground() with sdk started and config sdkDisabled=true stops the SDK`() { + mockkObject(Embrace.getImpl()) + every { Embrace.getImpl().isStarted } returns true + fakePreferenceService.sdkDisabled = true + + service.onForeground(true, 1000L, 1100L) + + verify(exactly = 1) { Embrace.getImpl().stop() } + } + + @Test + fun `test isSdkDisabled returns true`() { + fakePreferenceService.sdkDisabled = true + assertTrue(service.isSdkDisabled()) + } + + @Test + fun `test isSdkDisabled returns false`() { + fakePreferenceService.sdkDisabled = false + assertFalse(service.isSdkDisabled()) + } + + @Test + fun `Update listeners when cached config is loaded`() { + // Use ExecutorService that requires tasks to be explicitly run. This allows us to simulate the case + // when the loading from the cache doesn't run before the config is read. + + val pausableExecutorService = PausedExecutorService() + + every { mockApiService.getCachedConfig() } returns CachedConfig(null, null) + + // Create a new instance of the ConfigService where the value of the config is what it is when the config + // variable is initialized, before the cached version is loaded. + val configService = createService(pausableExecutorService) + assertFalse(configService.hasValidRemoteConfig()) + + configService.addListener(ConfigListener { }) + + // call arbitrary function to trigger config refresh + configService.anrBehavior.shouldCaptureMainThreadOnly() + + // Only run the task from the executor that loads the cached config to the ConfigService so the call to fetch + // a new config from the server isn't run + pausableExecutorService.runNext() + assertFalse(configService.hasValidRemoteConfig()) + + // fetch config from the server + pausableExecutorService.runNext() + assertTrue(configService.hasValidRemoteConfig()) + } + + /** + * Create a new instance of the [EmbraceConfigService] using the passed in [executorService] to run + * tasks for its internal [ExecutorService] + */ + private fun createService(executorService: ExecutorService): EmbraceConfigService = + EmbraceConfigService( + localConfig, + { mockApiService }, + fakePreferenceService, + fakeClock, + logger, + executorService, + false, + { Embrace.getImpl().stop() } + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCrashServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCrashServiceTest.kt new file mode 100644 index 0000000000..9c503ea5dd --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceCrashServiceTest.kt @@ -0,0 +1,224 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.capture.crash.EmbraceCrashService +import io.embrace.android.embracesdk.capture.crash.EmbraceUncaughtExceptionHandler +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.comms.delivery.EmbraceDeliveryService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.CrashHandlerLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import io.embrace.android.embracesdk.fakes.fakeSessionBehavior +import io.embrace.android.embracesdk.gating.EmbraceGatingService +import io.embrace.android.embracesdk.internal.crash.CrashFileMarker +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.Crash +import io.embrace.android.embracesdk.payload.ExceptionInfo +import io.embrace.android.embracesdk.payload.JsException +import io.embrace.android.embracesdk.payload.ThreadInfo +import io.embrace.android.embracesdk.session.SessionService +import io.embrace.android.embracesdk.utils.at +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class EmbraceCrashServiceTest { + + private lateinit var embraceCrashService: EmbraceCrashService + private lateinit var sessionService: SessionService + private lateinit var metadataService: FakeAndroidMetadataService + private lateinit var deliveryService: EmbraceDeliveryService + private lateinit var userService: UserService + private lateinit var eventService: EventService + private lateinit var anrService: AnrService + private lateinit var ndkService: NdkService + private lateinit var configService: ConfigService + + private lateinit var crash: Crash + private lateinit var localJsException: JsException + private lateinit var crashMarker: CrashFileMarker + private val testException = RuntimeException("Test exception") + private val fakeClock = FakeClock(1000L) + + @Before + fun setup() { + mockkStatic(Crash::class) + mockkObject(Crash.Companion) + + sessionService = mockk(relaxed = true) + metadataService = FakeAndroidMetadataService() + deliveryService = mockk(relaxUnitFun = true) + userService = mockk(relaxed = true) + eventService = mockk(relaxed = true) + anrService = mockk(relaxUnitFun = true) + ndkService = mockk() + crashMarker = mockk(relaxUnitFun = true) + + localJsException = JsException("jsException", "Error", "Error", "") + crash = Crash.ofThrowable(testException, localJsException) + } + + private fun setupForHandleCrash(crashHandlerEnabled: Boolean) { + configService = FakeConfigService( + autoDataCaptureBehavior = fakeAutoDataCaptureBehavior( + localCfg = { + LocalConfig( + "", + false, + SdkLocalConfig(crashHandler = CrashHandlerLocalConfig(crashHandlerEnabled)) + ) + } + ) + ) + + val gatingService = EmbraceGatingService( + mockk(relaxed = true) { + every { sessionBehavior } returns fakeSessionBehavior() + } + ) + + embraceCrashService = EmbraceCrashService( + configService, + sessionService, + metadataService, + deliveryService, + userService, + eventService, + anrService, + ndkService, + gatingService, + null, + crashMarker, + fakeClock + ) + + metadataService.setAppForeground() + every { Crash.ofThrowable(any(), any(), any()) } returns crash + } + + @Test + fun `test ApiClient and SessionService are called when handleCrash is called with JSException`() { + setupForHandleCrash(true) + every { ndkService.getUnityCrashId() } returns null + embraceCrashService.handleCrash(Thread.currentThread(), testException) + + verify { Crash.ofThrowable(testException, localJsException, any()) } + verify { anrService.forceAnrTrackingStopOnCrash() } + + verify { deliveryService.sendCrash(any()) } + verify { sessionService.handleCrash(crash.crashId) } + + /* + * Verify mainCrashHandled is true after the first execution + * by testing that a second execution of handleCrash wont run anything + */ + embraceCrashService.handleCrash(Thread.currentThread(), testException) + verify(exactly = 1) { anrService.forceAnrTrackingStopOnCrash() } + verify(exactly = 1) { deliveryService.sendCrash(any()) } + verify(exactly = 1) { sessionService.handleCrash(crash.crashId) } + } + + @Test + fun `test ApiClient and SessionService are called when handleCrash is called with unityId`() { + crash = Crash.ofThrowable(testException, localJsException, "Unity123") + setupForHandleCrash(false) + every { ndkService.getUnityCrashId() } returns "Unity123" + + embraceCrashService.handleCrash(Thread.currentThread(), testException) + + verify { Crash.ofThrowable(testException, localJsException, "Unity123") } + verify { anrService.forceAnrTrackingStopOnCrash() } + verify { deliveryService.sendCrash(any()) } + verify { sessionService.handleCrash(crash.crashId) } + } + + @Test + fun `test handleCrash calls mark() method when capture_last_run config is enabled`() { + crash = Crash.ofThrowable(testException, localJsException, "Unity123") + setupForHandleCrash(false) + every { ndkService.getUnityCrashId() } returns null + + embraceCrashService.handleCrash(Thread.currentThread(), testException) + + verify(exactly = 1) { crashMarker.mark() } + } + + @Test + fun `test exception handler is registered with config option enabled`() { + setupForHandleCrash(true) + assert(Thread.getDefaultUncaughtExceptionHandler() is EmbraceUncaughtExceptionHandler) + } + + @Test + fun `test exception handler is not registered with config option disabled`() { + setupForHandleCrash(false) + assert(Thread.getDefaultUncaughtExceptionHandler() !is EmbraceUncaughtExceptionHandler) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun testSerialization() { + val crash = Crash( + "123", + listOf( + ExceptionInfo( + "java.lang.RuntimeException", + "ExceptionMessage", + listOf("stacktrace.line") + ) + ), + listOf("js_exception"), + listOf( + ThreadInfo( + 123, + Thread.State.RUNNABLE, + "ReferenceHandler", + 1, + listOf("stacktrace.line.thread") + ) + ) + ) + val expectedInfo = ResourceReader.readResourceAsText("crash_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(crash) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("crash_expected.json") + val obj = Gson().fromJson(json, Crash::class.java) + + assertEquals("123", obj.crashId) + + assertEquals("java.lang.RuntimeException", obj?.exceptions?.at(0)?.name) + assertEquals("ExceptionMessage", obj?.exceptions?.at(0)?.message) + assertEquals("stacktrace.line", obj?.exceptions?.at(0)?.lines?.at(0)) + + assertEquals("js_exception", obj?.jsExceptions?.at(0)) + + assertEquals(123L, obj?.threads?.at(0)?.threadId) + assertEquals(Thread.State.RUNNABLE, obj?.threads?.at(0)?.state) + assertEquals("ReferenceHandler", obj?.threads?.at(0)?.name) + assertEquals(1, obj?.threads?.at(0)?.priority) + assertEquals("stacktrace.line.thread", obj?.threads?.at(0)?.lines?.at(0)) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceEventTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceEventTest.kt new file mode 100644 index 0000000000..5e75bd1a79 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceEventTest.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.payload.Event +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class EmbraceEventTest { + + private val event = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.CRASH + ) + + private val eventComplete = Event( + eventId = "eventId", + sessionId = "sessionId", + messageId = "messageId", + name = "test", + timestamp = 1111L, + type = EmbraceEvent.Type.WARNING_LOG, + logExceptionType = LogExceptionType.NONE.value, + screenshotTaken = false, + appState = "active", + customProperties = mapOf("Float" to 1, "String" to "TestString"), + sessionProperties = mapOf() + ) + + @Test + fun testMandatoryValues() { + assertNotNull(event.eventId) + assertNotNull(event.timestamp) + assertNotNull(event.type) + } + + @Test + fun testSerialization() { + val data = ResourceReader.readResourceAsText("event_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(eventComplete) + assertEquals(data, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("event_expected.json") + val obj = Gson().fromJson(json, Event::class.java) + assertEquals("eventId", obj.eventId) + assertEquals("sessionId", obj.sessionId) + assertEquals("messageId", obj.messageId) + assertEquals("test", obj.name) + assertEquals(1111L, obj.timestamp) + assertEquals(EmbraceEvent.Type.WARNING_LOG, obj.type) + assertEquals(LogExceptionType.NONE.value, obj.logExceptionType) + assertEquals(false, obj.screenshotTaken) + assertEquals("active", obj.appState) + assertEquals(mapOf("Float" to 1.0, "String" to "TestString"), obj.customPropertiesMap) + assertEquals(mapOf(), obj.sessionPropertiesMap) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceGatingServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceGatingServiceTest.kt new file mode 100644 index 0000000000..60bcd2bf3f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceGatingServiceTest.kt @@ -0,0 +1,486 @@ +package io.embrace.android.embracesdk + +import android.util.Pair +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.SessionBehavior +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SessionRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.fakes.fakeSessionBehavior +import io.embrace.android.embracesdk.gating.EmbraceGatingService +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_CUSTOM +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_CUSTOM_VIEWS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_TAPS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_VIEWS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.BREADCRUMBS_WEB_VIEWS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.FULL_SESSION_CRASHES +import io.embrace.android.embracesdk.gating.SessionGatingKeys.FULL_SESSION_ERROR_LOGS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.LOGS_INFO +import io.embrace.android.embracesdk.gating.SessionGatingKeys.LOGS_WARN +import io.embrace.android.embracesdk.gating.SessionGatingKeys.LOG_PROPERTIES +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_ANR +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_CONNECTIVITY +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_CPU +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_CURRENT_DISK_USAGE +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_LOW_MEMORY +import io.embrace.android.embracesdk.gating.SessionGatingKeys.PERFORMANCE_NETWORK +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_MOMENTS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_ORIENTATIONS +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_PROPERTIES +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_USER_TERMINATION +import io.embrace.android.embracesdk.gating.SessionGatingKeys.STARTUP_MOMENT +import io.embrace.android.embracesdk.gating.SessionGatingKeys.USER_PERSONAS +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.CustomBreadcrumb +import io.embrace.android.embracesdk.payload.DiskUsage +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.FragmentBreadcrumb +import io.embrace.android.embracesdk.payload.NetworkRequests +import io.embrace.android.embracesdk.payload.NetworkSessionV2 +import io.embrace.android.embracesdk.payload.Orientation +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.payload.TapBreadcrumb +import io.embrace.android.embracesdk.payload.UserInfo +import io.embrace.android.embracesdk.payload.WebViewBreadcrumb +import io.embrace.android.embracesdk.utils.at +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class EmbraceGatingServiceTest { + + private lateinit var localConfig: LocalConfig + private lateinit var gatingService: EmbraceGatingService + private lateinit var configService: ConfigService + private lateinit var internalEmbraceLogger: InternalEmbraceLogger + + private val enabledComponentsFull = setOf( + BREADCRUMBS_TAPS, + BREADCRUMBS_WEB_VIEWS, + BREADCRUMBS_CUSTOM, + BREADCRUMBS_VIEWS, + BREADCRUMBS_CUSTOM_VIEWS, + SESSION_PROPERTIES, + SESSION_USER_TERMINATION, + SESSION_ORIENTATIONS, + SESSION_MOMENTS, + STARTUP_MOMENT, + PERFORMANCE_CPU, + PERFORMANCE_NETWORK, + PERFORMANCE_LOW_MEMORY, + PERFORMANCE_ANR, + PERFORMANCE_CONNECTIVITY, + PERFORMANCE_CURRENT_DISK_USAGE, + USER_PERSONAS, + LOG_PROPERTIES, + LOGS_INFO, + LOGS_WARN + ) + + private lateinit var sessionBehavior: SessionBehavior + private var cfg: RemoteConfig? = RemoteConfig() + + @Before + fun setUp() { + localConfig = LocalConfig("default test app Id", false, SdkLocalConfig()) + sessionBehavior = fakeSessionBehavior { cfg } + configService = FakeConfigService(sessionBehavior = fakeSessionBehavior { cfg }) + internalEmbraceLogger = InternalEmbraceLogger() + gatingService = EmbraceGatingService( + configService + ) + } + + @Test + fun `test gating feature disabled by default for sessions`() { + val sessionMessage = SessionMessage(fakeSession().copy(properties = emptyMap())) + val result = gatingService.gateSessionMessage(sessionMessage) + + // result shouldn't be sanitized. + assertNotNull(result.session.properties) + } + + @Test + fun `test error logs not empty`() { + cfg = buildCustomRemoteConfig( + setOf(), + setOf(FULL_SESSION_ERROR_LOGS) + ) + + val sessionMessage = SessionMessage( + fakeSession().copy( + errorLogIds = listOf("id1"), + properties = emptyMap() + ) + ) + + val result = gatingService.gateSessionMessage(sessionMessage) + + // result shouldn't be sanitized. + assertEquals("id1", result.session.errorLogIds?.at(0)) + assertNotNull(result.session.properties) + } + + @Test + fun `test crashReportId not null`() { + cfg = buildCustomRemoteConfig( + setOf(), + setOf(FULL_SESSION_ERROR_LOGS) + ) + + val sessionMessage = SessionMessage( + fakeSession().copy( + crashReportId = "crashReportId", + properties = emptyMap() + ) + ) + + val result = gatingService.gateSessionMessage(sessionMessage) + + // result shouldn't be sanitized. + assertEquals("crashReportId", result.session.crashReportId) + assertNotNull(result.session.properties) + } + + @Test + fun `test gating feature from local and remote config`() { + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"components\": [" + + "\"br_tb\"," + + "\"br_vb\"," + + "\"br_cv\"," + + "\"br_wv\"," + + "\"br_cb\"," + + "\"log_pr\"," + + "\"s_props\"," + + "\"s_oc\"," + + "\"s_tr\"," + + "\"ur_per\"," + + "\"pr_anr\"," + + "\"pr_ns\"," + + "\"pr_nr\"," + + "\"pr_cp\"," + + "\"pr_mw\"," + + "\"pr_ds\"," + + "\"log_in\"," + + "\"log_war\"," + + "\"s_mts\"," + + "\"mts_st\"" + + "]" + + "}}", + EmbraceSerializer() + ) + + cfg = buildCustomRemoteConfig( + enabledComponentsFull, + null + ) + + gatingService = EmbraceGatingService( + configService + ) + } + + @Test + fun `test full session events config`() { + cfg = buildCustomRemoteConfig( + setOf(), + setOf(FULL_SESSION_CRASHES, FULL_SESSION_ERROR_LOGS) + ) + + assertTrue(sessionBehavior.shouldSendFullForCrash()) + assertTrue(sessionBehavior.shouldSendFullForErrorLog()) + } + + @Test + fun `test gate breadcrumbs from remote config`() { + val tapBreadcrumb = TapBreadcrumb( + Pair(0.0f, 0.0f), + "", + 0, + TapBreadcrumb.TapBreadcrumbType.TAP + ) + val webViewBreadcrumb = WebViewBreadcrumb("web url", 0) + val customBreadcrumb = CustomBreadcrumb("custom breadcrumb", 0) + val fragmentBreadcrumb = FragmentBreadcrumb("custom breadcrumb", 0, 1) + + val breadcrumbs = Breadcrumbs( + tapBreadcrumbs = listOf(tapBreadcrumb), + customBreadcrumbs = listOf(customBreadcrumb), + webViewBreadcrumbs = listOf(webViewBreadcrumb), + fragmentBreadcrumbs = listOf(fragmentBreadcrumb) + ) + + val message = SessionMessage( + session = fakeSession(), + userInfo = UserInfo(), + performanceInfo = PerformanceInfo(), + breadcrumbs = breadcrumbs + ) + + cfg = + buildCustomRemoteConfig(setOf(BREADCRUMBS_TAPS, BREADCRUMBS_WEB_VIEWS), null) + + val sanitizedMessage = gatingService.gateSessionMessage(message) + + val crumbs = checkNotNull(sanitizedMessage.breadcrumbs) + assertNotNull(crumbs.tapBreadcrumbs) + assertNotNull(crumbs.webViewBreadcrumbs) + assertNull(crumbs.fragmentBreadcrumbs) + assertNull(crumbs.customBreadcrumbs) + } + + @Test + fun `test gate session properties for Session`() { + val session = fakeSession().copy( + properties = mapOf("key" to "value") + ) + cfg = buildCustomRemoteConfig(setOf(SESSION_PROPERTIES), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + assertNotNull(sanitizedMessage.session.properties) + } + + @Test + fun `test gate tracked orientations for Session`() { + val session = fakeSession().copy( + orientations = listOf(Orientation(1, 123123123)) + ) + cfg = buildCustomRemoteConfig(setOf(SESSION_ORIENTATIONS), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + assertNotNull(sanitizedMessage.session.orientations) + } + + @Test + fun `test gate user termination for Session`() { + val session = fakeSession().copy( + terminationTime = 123123123, + isReceivedTermination = true + ) + cfg = buildCustomRemoteConfig(setOf(SESSION_USER_TERMINATION), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + assertNotNull(sanitizedMessage.session.terminationTime) + assertTrue(checkNotNull(sanitizedMessage.session.isReceivedTermination)) + } + + @Test + fun `test do not gate startup moment for Session`() { + val session = fakeSession().copy( + startupDuration = 123123123, + startupThreshold = 321321321 + ) + cfg = buildCustomRemoteConfig(setOf(STARTUP_MOMENT), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + assertNotNull(sanitizedMessage.session.startupDuration) + assertNotNull(sanitizedMessage.session.startupThreshold) + } + + @Test + fun `test gate startup moment for Session`() { + val session = fakeSession().copy( + startupDuration = 123123123, + startupThreshold = 321321321 + ) + cfg = buildCustomRemoteConfig(setOf(), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + assertNull(sanitizedMessage.session.startupDuration) + assertNull(sanitizedMessage.session.startupThreshold) + } + + @Test + fun `test do not gate logs for Session`() { + val session = fakeSession().copy( + infoLogIds = listOf("INFO-LOG"), + infoLogsAttemptedToSend = 1, + warningLogIds = listOf("WARNING-LOG"), + warnLogsAttemptedToSend = 1 + ) + cfg = buildCustomRemoteConfig(setOf(LOGS_WARN, LOGS_INFO), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + val infoIds = checkNotNull(sanitizedMessage.session.infoLogIds) + assertTrue(sanitizedMessage.session.infoLogsAttemptedToSend == 1) + assertTrue(infoIds.contains("INFO-LOG")) + val warnIds = checkNotNull(sanitizedMessage.session.warningLogIds) + assertTrue(sanitizedMessage.session.warnLogsAttemptedToSend == 1) + assertTrue(warnIds.contains("WARNING-LOG")) + } + + @Test + fun `test gate logs for Session`() { + val session = fakeSession().copy( + infoLogIds = listOf("INFO-LOG"), + infoLogsAttemptedToSend = 1, + warningLogIds = listOf("WARNING-LOG"), + warnLogsAttemptedToSend = 1 + ) + cfg = buildCustomRemoteConfig(setOf(), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + assertNull(sanitizedMessage.session.infoLogIds) + assertFalse(sanitizedMessage.session.infoLogsAttemptedToSend == 1) + assertNull(sanitizedMessage.session.warningLogIds) + assertFalse(sanitizedMessage.session.warnLogsAttemptedToSend == 1) + } + + @Test + fun `test do not gate moment event for Session`() { + val session = fakeSession().copy(eventIds = listOf("MOMENT-ID")) + cfg = buildCustomRemoteConfig(setOf(SESSION_MOMENTS), null) + + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + val ids = checkNotNull(sanitizedMessage.session.eventIds) + assertTrue(ids.contains("MOMENT-ID")) + } + + @Test + fun `test gate moment event for Session`() { + val session = fakeSession().copy(eventIds = listOf("MOMENT-ID")) + cfg = buildCustomRemoteConfig(setOf(), null) + val sessionMessage = SessionMessage(session) + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + assertNull(sanitizedMessage.session.eventIds) + } + + @Test + fun `test gate user personas for Event`() { + val userInfo = UserInfo(personas = setOf("persona")) + + val eventMessage = EventMessage( + event = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.INFO_LOG + ), + userInfo = userInfo, + performanceInfo = PerformanceInfo() + ) + + cfg = buildCustomRemoteConfig(setOf(USER_PERSONAS), null) + + val sanitizedMessage = gatingService.gateEventMessage(eventMessage) + + assertNotNull(sanitizedMessage.userInfo?.personas) + } + + @Test + fun `test gate user personas for Session`() { + val userInfo = UserInfo(personas = setOf("persona")) + + val sessionMessage = SessionMessage( + session = fakeSession(), + userInfo = userInfo, + performanceInfo = PerformanceInfo() + ) + + cfg = buildCustomRemoteConfig(setOf(USER_PERSONAS), null) + + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + assertNotNull(sanitizedMessage.userInfo?.personas) + } + + @Test + fun `test gate performance info for Session`() { + val performanceInfo = PerformanceInfo( + anrIntervals = listOf(), + memoryWarnings = listOf(), + networkInterfaceIntervals = listOf(), + diskUsage = DiskUsage(100, null) + ) + + val sessionPerformanceInfo = performanceInfo.copy( + networkRequests = + NetworkRequests( + NetworkSessionV2( + listOf(), + mapOf() + ) + ) + ) + + val sessionMessage = SessionMessage( + session = fakeSession(), + performanceInfo = sessionPerformanceInfo + ) + + cfg = buildCustomRemoteConfig( + setOf( + PERFORMANCE_ANR, + PERFORMANCE_CONNECTIVITY, + PERFORMANCE_CPU, + PERFORMANCE_NETWORK, + PERFORMANCE_CURRENT_DISK_USAGE, + PERFORMANCE_LOW_MEMORY + ), + null + ) + + val sanitizedMessage = gatingService.gateSessionMessage(sessionMessage) + + val perfInfo = checkNotNull(sanitizedMessage.performanceInfo) + assertNotNull(perfInfo.anrIntervals) + assertNotNull(perfInfo.diskUsage) + assertNotNull(perfInfo.memoryWarnings) + assertNotNull(perfInfo.networkInterfaceIntervals) + assertNotNull(perfInfo.networkRequests) + } + + @Test + fun `test public methods`() { + cfg = buildCustomRemoteConfig( + setOf(), + setOf() + ) + + // the enabled components are empty, so it should gate everything + assertTrue(sessionBehavior.shouldGateMoment()) + assertTrue(sessionBehavior.shouldGateInfoLog()) + assertTrue(sessionBehavior.shouldGateWarnLog()) + assertTrue(sessionBehavior.shouldGateStartupMoment()) + } + + private fun buildCustomRemoteConfig(components: Set?, fullSessionEvents: Set?) = + RemoteConfig( + sessionConfig = SessionRemoteConfig( + true, + false, + components, + fullSessionEvents + ) + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt new file mode 100644 index 0000000000..006c2a17a5 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt @@ -0,0 +1,300 @@ +package io.embrace.android.embracesdk + +import android.net.Uri +import android.webkit.URLUtil +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +internal class EmbraceInternalInterfaceImplTest { + + private lateinit var impl: EmbraceInternalInterfaceImpl + private lateinit var embrace: EmbraceImpl + + @Before + fun setUp() { + embrace = mockk(relaxed = true) + impl = EmbraceInternalInterfaceImpl(embrace) + } + + @Test + fun testLogInfo() { + impl.logInfo("", emptyMap()) + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.INFO_LOG, + "", + emptyMap(), + null, + null, + LogExceptionType.NONE, + null, + null + ) + } + } + + @Test + fun testLogWarning() { + impl.logWarning("", emptyMap(), null) + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.WARNING_LOG, + "", + emptyMap(), + null, + null, + LogExceptionType.NONE, + null, + null + ) + } + } + + @Test + fun testLogError() { + impl.logError("", emptyMap(), null, false) + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "", + emptyMap(), + null, + null, + LogExceptionType.NONE, + null, + null + ) + } + } + + @Test + fun testLogHandledException() { + val exception = Throwable("handled exception") + impl.logHandledException(exception, LogType.ERROR, emptyMap(), null) + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "handled exception", + emptyMap(), + exception.stackTrace, + null, + LogExceptionType.NONE, + null, + null + ) + } + } + + @Test + fun testAddBreadcrumb() { + impl.addBreadcrumb("") + verify(exactly = 1) { embrace.addBreadcrumb("") } + } + + @Test + fun testGetDeviceId() { + every { embrace.deviceId } returns "test" + assertEquals("test", impl.deviceId) + } + + @Test + fun testSetUserIdentifier() { + impl.setUserIdentifier("") + verify(exactly = 1) { embrace.setUserIdentifier("") } + } + + @Test + fun testClearUserIdentifier() { + impl.clearUserIdentifier() + verify(exactly = 1) { embrace.clearUserIdentifier() } + } + + @Test + fun testSetUsername() { + impl.setUsername("") + verify(exactly = 1) { embrace.setUsername("") } + } + + @Test + fun testClearUsername() { + impl.clearUsername() + verify(exactly = 1) { embrace.clearUsername() } + } + + @Test + fun testSetUserEmail() { + impl.setUserEmail("") + verify(exactly = 1) { embrace.setUserEmail("") } + } + + @Test + fun testClearUserEmail() { + impl.clearUserEmail() + verify(exactly = 1) { embrace.clearUserEmail() } + } + + @Test + fun testSetUserAsPayer() { + impl.setUserAsPayer() + verify(exactly = 1) { embrace.setUserAsPayer() } + } + + @Test + fun testClearUserAsPayer() { + impl.clearUserAsPayer() + verify(exactly = 1) { embrace.clearUserAsPayer() } + } + + @Test + fun testAddUserPersona() { + impl.addUserPersona("") + verify(exactly = 1) { embrace.addUserPersona("") } + } + + @Test + fun testClearUserPersona() { + impl.clearUserPersona("") + verify(exactly = 1) { embrace.clearUserPersona("") } + } + + @Test + fun testClearAllUserPersonas() { + impl.clearAllUserPersonas() + verify(exactly = 1) { embrace.clearAllUserPersonas() } + } + + @Test + fun testAddSessionProperty() { + impl.addSessionProperty("key", "value", true) + verify(exactly = 1) { embrace.addSessionProperty("key", "value", true) } + } + + @Test + fun testRemoveSessionProperty() { + impl.removeSessionProperty("key") + verify(exactly = 1) { embrace.removeSessionProperty("key") } + } + + @Test + fun testGetSessionProperties() { + every { embrace.sessionProperties } returns mapOf() + assertEquals(mapOf(), impl.sessionProperties) + } + + @Test + fun testStartMoment() { + impl.startMoment("name", "id", mapOf()) + verify(exactly = 1) { embrace.startMoment("name", "id", mapOf()) } + } + + @Test + fun testEndMoment() { + impl.endMoment("name", "id", mapOf()) + verify(exactly = 1) { embrace.endMoment("name", "id", mapOf()) } + } + + @Test + fun testStartView() { + impl.startView("") + verify(exactly = 1) { embrace.startView("") } + } + + @Test + fun testEndView() { + impl.endView("") + verify(exactly = 1) { embrace.endView("") } + } + + @Test + fun testEndAppStartup() { + impl.endAppStartup(emptyMap()) + verify(exactly = 1) { embrace.endAppStartup(emptyMap()) } + } + + @Test + fun testLogInternalError() { + impl.logInternalError("msg", "details") + verify(exactly = 1) { embrace.logInternalError("msg", "details") } + } + + @Test + fun testEndSession() { + impl.endSession(true) + verify(exactly = 1) { embrace.endSession(true) } + } + + @Test + fun testCompletedNetworkRequest() { + mockkStatic(Uri::class) + mockkStatic(URLUtil::class) + every { Uri.parse("https://google.com") } returns mockk(relaxed = true) + every { URLUtil.isHttpsUrl("https://google.com") } returns true + impl.recordCompletedNetworkRequest( + "https://google.com", + "get", + 15092342340, + 15092342799, + 140, + 2509, + 200, + null, + null + ) + val captor = slot() + verify(exactly = 1) { + embrace.recordNetworkRequest(capture(captor)) + } + + val request = captor.captured + assertEquals("https://google.com", request.url) + assertEquals(HttpMethod.GET.name, request.httpMethod) + assertEquals(15092342340L, request.startTime) + assertEquals(15092342799L, request.endTime) + assertEquals(140L, request.bytesSent) + assertEquals(2509L, request.bytesReceived) + assertEquals(200, request.responseCode) + assertNull(request.error) + assertNull(request.networkCaptureData) + } + + @Test + fun testIncompleteNetworkRequest() { + mockkStatic(Uri::class) + mockkStatic(URLUtil::class) + every { Uri.parse("https://google.com") } returns mockk(relaxed = true) + every { URLUtil.isHttpsUrl("https://google.com") } returns true + + val exc = RuntimeException("Whoops") + impl.recordIncompleteNetworkRequest( + "https://google.com", + "get", + 15092342340L, + 15092342799L, + exc, + "id-123", + null + ) + val captor = slot() + verify(exactly = 1) { + embrace.recordNetworkRequest(capture(captor)) + } + + val request = captor.captured + assertEquals("https://google.com", request.url) + assertEquals(HttpMethod.GET.name, request.httpMethod) + assertEquals(15092342340L, request.startTime) + assertEquals(15092342799L, request.endTime) + assertNull(request.error) + assertEquals("id-123", request.traceId) + assertNull(request.networkCaptureData) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceMemoryServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceMemoryServiceTest.kt new file mode 100644 index 0000000000..90d1083e2b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceMemoryServiceTest.kt @@ -0,0 +1,77 @@ +package io.embrace.android.embracesdk + +import android.app.ActivityManager +import io.embrace.android.embracesdk.capture.memory.EmbraceMemoryService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test + +internal class EmbraceMemoryServiceTest { + + private lateinit var embraceMemoryService: EmbraceMemoryService + private var activityManager: ActivityManager? = null + private var memoryCleanerService: MemoryCleanerService? = null + private val fakeClock = FakeClock() + + @Before + fun setUp() { + activityManager = mockk(relaxUnitFun = true) + memoryCleanerService = mockk(relaxUnitFun = true) + fakeClock.setCurrentTime(100L) + embraceMemoryService = EmbraceMemoryService(fakeClock) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `test memory service initialization throws an exception if activityManager is null`() { + memoryCleanerService = null + activityManager = null + assertThrows( + IllegalStateException::class.java + ) { + checkNotNull(memoryCleanerService) { "memoryCleanerService must not be null" } + } + } + + @Test + fun `test memory service initialization throws an exception if memoryCleanerService is null`() { + activityManager = null + assertThrows( + IllegalStateException::class.java + ) { + checkNotNull(activityManager) { "activityManager must not be null" } + } + } + + @Test + fun `onMemoryWarning populates memoryTimestamps if the offset is less than 100`() { + with(embraceMemoryService) { + repeat(100) { + onMemoryWarning() + fakeClock.tick() + } + val result = this.getCapturedData() + assertEquals(result.size, 100) + onMemoryWarning() + assertEquals(result.size, 100) + } + } + + @Test + fun testCleanCollections() { + embraceMemoryService.onMemoryWarning() + assertEquals(1, embraceMemoryService.getCapturedData().size) + embraceMemoryService.cleanCollections() + assertEquals(0, embraceMemoryService.getCapturedData().size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNativeThreadSamplerServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNativeThreadSamplerServiceTest.kt new file mode 100644 index 0000000000..503878a033 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNativeThreadSamplerServiceTest.kt @@ -0,0 +1,441 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.anr.ndk.EmbraceNativeThreadSamplerService +import io.embrace.android.embracesdk.anr.ndk.isUnityMainThread +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.AnrBehavior +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeDeviceArchitecture +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.NativeThreadAnrInterval +import io.embrace.android.embracesdk.payload.NativeThreadAnrSample +import io.embrace.android.embracesdk.payload.NativeThreadAnrStackframe +import io.embrace.android.embracesdk.payload.mapThreadState +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.Random +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory + +internal class EmbraceNativeThreadSamplerServiceTest { + + private lateinit var sampler: EmbraceNativeThreadSamplerService + private lateinit var configService: ConfigService + private lateinit var delegate: EmbraceNativeThreadSamplerService.NdkDelegate + private lateinit var random: Random + private lateinit var executorService: BlockingScheduledExecutorService + private val obj = NativeThreadAnrInterval(null, null, null, null, null, null, null, null) + private val testSample = NativeThreadAnrSample(null, null, null, null) + private lateinit var anrBehavior: AnrBehavior + private lateinit var cfg: AnrRemoteConfig + + @Before + fun setUp() { + cfg = AnrRemoteConfig(pctNativeThreadAnrSamplingEnabled = 100f) + anrBehavior = fakeAnrBehavior { cfg } + configService = FakeConfigService(anrBehavior = anrBehavior) + delegate = mockk(relaxed = true) + val logger = InternalEmbraceLogger() + random = mockk(relaxed = true) + executorService = BlockingScheduledExecutorService() + sampler = + EmbraceNativeThreadSamplerService( + configService, + lazy { emptyMap() }, + random, + logger, + delegate, + executorService, + FakeDeviceArchitecture() + ) + every { random.nextInt(any()) } returns 0 + } + + @Test + fun testIsNotUnityMainThread() { + assertFalse(sampler.setupNativeSampler()) + assertFalse(isUnityMainThread()) + verify(exactly = 1) { delegate.setupNativeThreadSampler(true) } + } + + @Test + fun testIsUnityMainThread() { + val threadFactory = ThreadFactory { runnable -> + Executors.defaultThreadFactory().newThread(runnable::run).apply { + name = "UnityMain" + } + } + val future = Executors.newSingleThreadExecutor(threadFactory).submit { + assertTrue(isUnityMainThread()) + assertFalse(sampler.setupNativeSampler()) + verify(exactly = 1) { delegate.setupNativeThreadSampler(true) } + } + future.get() + } + + @Test + fun testSessionEndDisabledSampling() { + cfg = cfg.copy(pctNativeThreadAnrSamplingEnabled = 0f) + sampler.intervals = mutableListOf(obj) + simulateUnityThreadSample() + assertNull(sampler.getCapturedIntervals(false)) + } + + @Test + fun testSessionEndListener() { + sampler.onThreadBlocked(Thread.currentThread(), 0) + while (!sampler.sampling) { + sampler.onThreadBlockedInterval(Thread.currentThread(), 0) + } + sampler.intervals.add( + NativeThreadAnrInterval( + null, + null, + null, + null, + null, + mutableListOf(), + null, + null + ) + ) + every { delegate.finishSampling() } returns listOf(testSample) + + val nativeThreadAnrIntervals = sampler.getCapturedIntervals(false) + val interval = nativeThreadAnrIntervals?.single() + val trace = checkNotNull(interval?.samples) + assertTrue(trace.isNotEmpty()) + } + + /** + * If an ANR interval occurred but no unity sample was created, then don't report + * anything in the session + */ + @Test + fun testSessionWithoutUsefulSamples() { + // don't include a stacktrace in the sample. + sampler.onThreadBlocked(Thread.currentThread(), 0) + assertNull(sampler.getCapturedIntervals(false)) + } + + @Test + fun testEmptySamplesFilteredOut() { + // include a stacktrace in the first sample + simulateUnityThreadSample() + + // don't include a stacktrace in subsequent samples + sampler.onThreadBlocked(Thread.currentThread(), 0) + + val nativeThreadAnrIntervals = sampler.getCapturedIntervals(false) + val interval = nativeThreadAnrIntervals?.single() + val trace = checkNotNull(interval?.samples) + assertTrue(trace.isNotEmpty()) + } + + @Test + fun testMaxCaptureLimitsExceeded() { + val sampleCount = 20 + val testSamples = (0 until sampleCount).map { testSample } + every { delegate.finishSampling() } returns testSamples + + // simulate capturing a ridiculous number of ANR samples + repeat(100) { count -> + val timestamp = count.toLong() + sampler.onThreadBlocked(Thread.currentThread(), timestamp) + sampler.factor = 1 + + // ANR takes 20s to complete each time, producing 20 samples + val thread = Thread.currentThread() + sampler.onThreadBlockedInterval(thread, timestamp) + sampler.onThreadUnblocked(thread, timestamp) + executorService.runAllSubmittedTasks() + } + + // finish all pending scheduled jobs + executorService.runAllSubmittedTasks() + + // verify data was captured up to the default limit of 5 + val nativeThreadAnrIntervals = sampler.getCapturedIntervals(false) + + // verify same data was included in the session + val intervals = checkNotNull(nativeThreadAnrIntervals) + + assertEquals( + 5, + intervals.size + ) + assertTrue( + intervals.all { + val sample = checkNotNull(it.samples) + sample.size == sampleCount + } + ) + } + + @Test + fun testIgnoreAllowlist() { + val thread = Thread.currentThread() + assertTrue(sampler.containsAllowedStackframes(anrBehavior, thread.stackTrace)) + assertTrue( + sampler.containsAllowedStackframes( + anrBehavior, + thread.stackTrace + ) + ) + } + + @Test + fun testRespectsAllowlist() { + cfg = AnrRemoteConfig( + nativeThreadAnrSamplingAllowlist = listOf( + AnrRemoteConfig.AllowedNdkSampleMethod( + "com.unity3d.player.UnityPlayer", + "pauseUnity" + ), + AnrRemoteConfig.AllowedNdkSampleMethod("io.example.CustomClz", "customMethod") + ), + ignoreNativeThreadAnrSamplingAllowlist = false + ) + + val unityPlayer = + StackTraceElement("com.unity3d.player.UnityPlayer", "pauseUnity", null, -1) + val fail1 = StackTraceElement("com.unity3d.player.UnityPlayer", "foo", null, -1) + val fail2 = StackTraceElement("io.example.CustomClz", "pauseUnity", null, -1) + val fail3 = StackTraceElement("java.lang.String", "toString", null, -1) + assertTrue(sampler.containsAllowedStackframes(anrBehavior, arrayOf(unityPlayer))) + assertFalse(sampler.containsAllowedStackframes(anrBehavior, arrayOf(fail1))) + assertFalse(sampler.containsAllowedStackframes(anrBehavior, arrayOf(fail2))) + assertFalse(sampler.containsAllowedStackframes(anrBehavior, arrayOf(fail3))) + } + + @Test + fun testThreadBlockedAllowListTrue() { + assertEquals(-1, sampler.count) + assertEquals(-1, sampler.factor) + assertTrue(sampler.ignored) + + val currentThread = Thread.currentThread() + sampler.onThreadBlocked(currentThread, 1500000) + sampler.onThreadBlockedInterval(currentThread, 1500000) + verify(exactly = 1) { delegate.startSampling(0, 500) } + assertEquals(1, sampler.count) + assertEquals(5, sampler.factor) + assertFalse(sampler.ignored) + + with(sampler.currentInterval) { + checkNotNull(this) + assertEquals(0L, sampleOffsetMs) + assertEquals(1500000L, threadBlockedTimestamp) + assertEquals(currentThread.id, id) + assertEquals(currentThread.name, name) + assertEquals(mapThreadState(currentThread.state).code, state) + assertEquals(currentThread.priority, priority) + } + } + + @Test + fun testThreadBlockedAllowListFalse() { + cfg = cfg.copy( + ignoreNativeThreadAnrSamplingAllowlist = false + ) + assertEquals(-1, sampler.count) + assertEquals(-1, sampler.factor) + assertTrue(sampler.ignored) + + sampler.onThreadBlocked(Thread.currentThread(), 0) + verify(exactly = 0) { delegate.startSampling(any(), any()) } + assertEquals(-1, sampler.count) + assertEquals(-1, sampler.factor) + assertTrue(sampler.ignored) + } + + @Test + fun testThreadBlockedDisabledSampling() { + cfg = cfg.copy( + pctNativeThreadAnrSamplingEnabled = 0f + ) + + sampler.onThreadBlocked(Thread.currentThread(), 0) + verify(exactly = 0) { delegate.startSampling(any(), any()) } + assertEquals(-1, sampler.count) + assertEquals(-1, sampler.factor) + assertTrue(sampler.ignored) + } + + @Test + fun testOnIntervalIgnoredAllowlist() { + cfg = cfg.copy( + ignoreNativeThreadAnrSamplingAllowlist = false + ) + sampler.onThreadBlocked(Thread.currentThread(), 0) + + repeat(10) { + sampler.onThreadBlockedInterval(Thread.currentThread(), 0) + } + verify(exactly = 0) { delegate.startSampling(0, 500L) } + } + + @Test + fun testOnIntervalDisabledSampling() { + cfg = cfg.copy( + pctNativeThreadAnrSamplingEnabled = 0f + ) + + sampler.onThreadBlocked(Thread.currentThread(), 0) + repeat(10) { + assertEquals(-1, sampler.count) + sampler.onThreadBlockedInterval(Thread.currentThread(), 0) + } + verify(exactly = 0) { delegate.startSampling(any(), any()) } + assertEquals(-1, sampler.count) + } + + @Test + fun testOnIntervalSampled() { + sampler.onThreadBlocked(Thread.currentThread(), 0) + + repeat(6) { k -> + assertEquals(k, sampler.count) + sampler.onThreadBlockedInterval(Thread.currentThread(), 0) + } + verify(exactly = 1) { delegate.startSampling(0, 500L) } + assertEquals(1, sampler.count) + } + + @Test + fun testNonZeroOffset() { + val shift = 3 + every { random.nextInt(any()) } returns shift + sampler.onThreadBlocked(Thread.currentThread(), 0) + val startPos = 2 + assertEquals(startPos, sampler.count) + + repeat(4) { k -> + assertEquals(startPos + k, sampler.count) + sampler.onThreadBlockedInterval(Thread.currentThread(), 0) + } + verify(exactly = 1) { delegate.startSampling(0, 500L) } + assertEquals(1, sampler.count) + } + + @Test + fun testCleanSamples() { + val ref = mutableListOf() + sampler.intervals = ref + sampler.cleanCollections() + assertNotSame(sampler.intervals, ref) + } + + @Test + fun testTickRecorded() { + val shift = 5 + every { delegate.finishSampling() } returns listOf(testSample) + every { random.nextInt(any()) } returns shift + + val thread = Thread.currentThread() + sampler.onThreadBlocked(thread, 14993000L) + + repeat(1) { + sampler.onThreadBlockedInterval(thread, 14999000L) + } + + sampler.onThreadUnblocked(thread, 15000000L) + executorService.runAllSubmittedTasks() + + // verify info recorded. + val tick = sampler.intervals.single() + assertEquals(500L, tick.sampleOffsetMs) + assertEquals(14993000L, tick.threadBlockedTimestamp) + assertEquals(thread.id, tick.id) + assertEquals(thread.name, tick.name) + assertEquals(mapThreadState(thread.state).code, tick.state) + assertEquals(thread.priority, tick.priority) + assertEquals(AnrRemoteConfig.Unwinder.LIBUNWIND.code, tick.unwinder) + + val obj = checkNotNull(tick.samples?.single()) + assertEquals(testSample, obj) + } + + @Test + fun testDupeStacktraceCollection() { + val thread = Thread.currentThread() + sampler.onThreadBlocked(thread, 14993000L) + + // create the first sample + val frame1 = NativeThreadAnrStackframe( + "0x5823409a", + "0x00340f204", + "/data/foo.so", + 11 + ) + val frame2 = NativeThreadAnrStackframe( + "0x2823449a", + "0x10320f520", + "/data/bar.so", + 9 + ) + every { delegate.finishSampling() } returns listOf( + NativeThreadAnrSample( + 0, + 0, + 0, + listOf(frame1) + ), + NativeThreadAnrSample( + 0, + 0, + 0, + listOf(frame1) + ), + NativeThreadAnrSample( + 0, + 0, + 0, + listOf(frame2) + ) + ) + sampler.onThreadBlockedInterval(thread, 14993000L) + sampler.onThreadUnblocked(thread, 14993000L) + executorService.runAllSubmittedTasks() + + // assert the frames were recorded + val currentSample = checkNotNull(sampler.currentInterval) + val traces = checkNotNull(currentSample.samples) + assertEquals(3, traces.size) + assertEquals(listOf(frame1), traces[0].stackframes) + assertEquals(listOf(frame1), traces[1].stackframes) + assertEquals(listOf(frame2), traces[2].stackframes) + } + + @Test + fun testMaxIntervalLimit() { + every { delegate.finishSampling() } returns listOf(testSample) + + val factor = checkNotNull(configService.anrBehavior.getNativeThreadAnrSamplingFactor()) + val thread = Thread.currentThread() + + repeat(10) { + sampler.onThreadBlocked(thread, 14993000L) + sampler.count = factor + sampler.onThreadBlockedInterval(thread, 14993000L) + sampler.onThreadUnblocked(thread, 14993000L) + } + assertEquals(5, sampler.intervals.size) + } + + private fun simulateUnityThreadSample() { + sampler.onThreadBlocked(Thread.currentThread(), 0) + sampler.currentInterval?.samples?.add(testSample) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNetworkConnectivityServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNetworkConnectivityServiceTest.kt new file mode 100644 index 0000000000..b439e5b9e0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceNetworkConnectivityServiceTest.kt @@ -0,0 +1,259 @@ +package io.embrace.android.embracesdk + +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.capture.connectivity.EmbraceNetworkConnectivityService +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityListener +import io.embrace.android.embracesdk.comms.delivery.NetworkStatus +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ExecutorService + +@Suppress("DEPRECATION") +internal class EmbraceNetworkConnectivityServiceTest { + + private lateinit var service: EmbraceNetworkConnectivityService + + companion object { + private lateinit var mockContext: Context + private lateinit var mockCleanerService: MemoryCleanerService + private lateinit var logger: InternalEmbraceLogger + private lateinit var mockConnectivityManager: ConnectivityManager + private lateinit var executor: ExecutorService + private lateinit var fakeClock: FakeClock + + /** + * Setup before all tests get executed. Create mocks here. + */ + @BeforeClass + @JvmStatic + fun setupBeforeAll() { + mockContext = mockk(relaxUnitFun = true) + mockCleanerService = mockk(relaxUnitFun = true) + logger = InternalEmbraceLogger() + mockConnectivityManager = mockk() + fakeClock = FakeClock() + executor = MoreExecutors.newDirectExecutorService() + } + + /** + * Setup after all tests get executed. Un-mock all here. + */ + @AfterClass + @JvmStatic + fun tearDownAfterAll() { + unmockkAll() + executor.shutdown() + } + } + + /** + * Setup before each test. + */ + @Before + fun setup() { + every { mockContext.getSystemService(Context.CONNECTIVITY_SERVICE) } returns mockConnectivityManager + + service = EmbraceNetworkConnectivityService( + mockContext, + fakeClock, + executor, + logger, + mockConnectivityManager + ) + } + + /** + * Setup after each test. Clean mocks content. + */ + @After + fun tearDown() { + clearAllMocks() + } + + /** + * Asserts that a network connectivity broadcast receiver can be registered/unregistered + */ + @Test + @Throws(InterruptedException::class) + fun `test connectivity broadcast receiver can register and unregister`() { + verify { mockContext.registerReceiver(service, any()) } + service.close() + verify { mockContext.unregisterReceiver(service) } + } + + @Test + fun `test onReceive with no connection creates an interval`() { + val mockIntent = mockk() + val startTime = fakeClock.now() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns false + service.onReceive(mockContext, mockIntent) + fakeClock.tick(2000) + val endTime = fakeClock.now() + val intervals = service.getCapturedData() + + assertEquals(1, intervals.size) + assertEquals(intervals.single().value, "none") + } + + @Test + fun `test networkStatusOnSessionStarted with no connection creates an interval`() { + val startTime = fakeClock.now() + fakeClock.tick(2000) + val endTime = fakeClock.now() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns false + service.networkStatusOnSessionStarted(startTime) + + val intervals = service.getCapturedData() + + assertEquals(1, intervals.size) + assertEquals(intervals.single().startTime, startTime) + assertEquals(intervals.single().endTime, endTime) + assertEquals(intervals.single().value, "none") + } + + @Test + fun `test networkStatusOnSessionStarted with WIFI connection creates an interval`() { + val startTime = fakeClock.now() + fakeClock.tick(2000) + val endTime = fakeClock.now() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns true + every { mockConnectivityManager.activeNetworkInfo?.type } returns ConnectivityManager.TYPE_WIFI + service.networkStatusOnSessionStarted(startTime) + + val intervals = service.getCapturedData() + + assertEquals(1, intervals.size) + assertEquals(intervals.single().startTime, startTime) + assertEquals(intervals.single().endTime, endTime) + assertEquals(intervals.single().value, "wifi") + } + + @Test + fun `test networkStatusOnSessionStarted with WAN connection creates an interval`() { + val startTime = fakeClock.now() + fakeClock.tick(2000) + val endTime = fakeClock.now() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns true + every { mockConnectivityManager.activeNetworkInfo?.type } returns ConnectivityManager.TYPE_MOBILE + + service.networkStatusOnSessionStarted(startTime) + val intervals = service.getCapturedData() + + assertEquals(1, intervals.size) + assertEquals(intervals.single().startTime, startTime) + assertEquals(intervals.single().endTime, endTime) + assertEquals(intervals.single().value, "wan") + } + + @Test + fun `test cleanCollections and getCapturedData returns no intervals`() { + val startTime = fakeClock.now() + fakeClock.tick(2000) + val endTime = fakeClock.now() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns false + + service.networkStatusOnSessionStarted(startTime) + var intervals = service.getCapturedData() + + assertEquals(1, intervals.size) + + service.cleanCollections() + + intervals = service.getCapturedData() + assertEquals(0, intervals.size) + } + + @Test + fun `test listener get notified when connectivity status changes to WIFI`() { + // add the connectivity listener + val listener = mockk() + service.addNetworkConnectivityListener(listener) + + // call onReceive to emulate a connectivity status change + val mockIntent = mockk() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns true + every { mockConnectivityManager.activeNetworkInfo?.type } returns ConnectivityManager.TYPE_WIFI + service.onReceive(mockContext, mockIntent) + + verify(exactly = 1) { listener.onNetworkConnectivityStatusChanged(NetworkStatus.WIFI) } + } + + @Test + fun `test listener get notified when connectivity status changes to MOBILE`() { + // add the connectivity listener + val listener = mockk() + service.addNetworkConnectivityListener(listener) + + // call onReceive to emulate a connectivity status change + val mockIntent = mockk() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns true + every { mockConnectivityManager.activeNetworkInfo?.type } returns ConnectivityManager.TYPE_MOBILE + service.onReceive(mockContext, mockIntent) + + verify(exactly = 1) { listener.onNetworkConnectivityStatusChanged(NetworkStatus.WAN) } + } + + @Test + fun `test listener get notified when connectivity status changes to no connectivity`() { + // add the connectivity listener + val listener = mockk() + service.addNetworkConnectivityListener(listener) + + // call onReceive to emulate a connectivity status change + val mockIntent = mockk() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns false + service.onReceive(mockContext, mockIntent) + + verify(exactly = 1) { listener.onNetworkConnectivityStatusChanged(NetworkStatus.NOT_REACHABLE) } + } + + @Test + fun `test listener get notified when connectivity status changes and no info obtained`() { + // add the connectivity listener + val listener = mockk() + service.addNetworkConnectivityListener(listener) + + // call onReceive to emulate a connectivity status change + val mockIntent = mockk() + every { mockConnectivityManager.activeNetworkInfo } throws Exception("") + service.onReceive(mockContext, mockIntent) + + verify(exactly = 1) { listener.onNetworkConnectivityStatusChanged(NetworkStatus.UNKNOWN) } + } + + @Test + fun `test listener get notified when connectivity status changes and not notified when removed`() { + // add the connectivity listener + val listener = mockk() + service.addNetworkConnectivityListener(listener) + + // call onReceive to emulate a connectivity status change + val mockIntent = mockk() + every { mockConnectivityManager.activeNetworkInfo?.isConnected } returns true + every { mockConnectivityManager.activeNetworkInfo?.type } returns ConnectivityManager.TYPE_MOBILE + service.onReceive(mockContext, mockIntent) + + verify(exactly = 1) { listener.onNetworkConnectivityStatusChanged(NetworkStatus.WAN) } + + // remove listener and call onReceive again + service.removeNetworkConnectivityListener(listener) + service.onReceive(mockContext, mockIntent) + + verify(exactly = 1) { listener.onNetworkConnectivityStatusChanged(any()) } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceOrientationServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceOrientationServiceTest.kt new file mode 100644 index 0000000000..0ac7786057 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceOrientationServiceTest.kt @@ -0,0 +1,49 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.capture.orientation.EmbraceOrientationService +import io.embrace.android.embracesdk.clock.Clock +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class EmbraceOrientationServiceTest { + + lateinit var service: EmbraceOrientationService + private val clock = Clock { 1509234234L } + + @Before + fun setUp() { + service = EmbraceOrientationService(clock).apply { + onOrientationChanged(1) // portrait orientation + } + } + + @Test + fun testDataCapture() { + val orientation = service.getCapturedData().single() + assertEquals("p", orientation.orientation) + assertEquals(1, orientation.internalOrientation) + assertEquals(clock.now(), orientation.timestamp) + } + + @Test + fun testCleanCollections() { + assertEquals(1, service.getCapturedData().size) + service.cleanCollections() + assertEquals(0, service.getCapturedData().size) + } + + @Test + fun testMissingOrientation() { + assertEquals(1, service.getCapturedData().size) + service.onOrientationChanged(null) + assertEquals(1, service.getCapturedData().size) + } + + @Test + fun testMatchesLastOrientation() { + assertEquals(1, service.getCapturedData().size) + service.onOrientationChanged(1) + assertEquals(1, service.getCapturedData().size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePerformanceInfoServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePerformanceInfoServiceTest.kt new file mode 100644 index 0000000000..8758d23b92 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePerformanceInfoServiceTest.kt @@ -0,0 +1,110 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.anr.sigquit.GoogleAnrTimestampRepository +import io.embrace.android.embracesdk.capture.EmbracePerformanceInfoService +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeAnrService +import io.embrace.android.embracesdk.fakes.FakeApplicationExitInfoService +import io.embrace.android.embracesdk.fakes.FakeMemoryService +import io.embrace.android.embracesdk.fakes.FakeNetworkConnectivityService +import io.embrace.android.embracesdk.fakes.FakeNetworkLoggingService +import io.embrace.android.embracesdk.fakes.FakePowerSaveModeService +import io.embrace.android.embracesdk.fakes.FakeStrictModeService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.NetworkRequests +import io.embrace.android.embracesdk.payload.PerformanceInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +private const val SESSION_END_TIME_MS = 1609234092345 + +internal class EmbracePerformanceInfoServiceTest { + + private lateinit var service: EmbracePerformanceInfoService + private val anrService = FakeAnrService() + private val networkConnectivityService = FakeNetworkConnectivityService() + private val networkLoggingService = FakeNetworkLoggingService() + private val powerSaveModeService = FakePowerSaveModeService() + private val memoryService = FakeMemoryService() + private val metadataService = FakeAndroidMetadataService() + private val googleAnrTimestampRepository = GoogleAnrTimestampRepository(InternalEmbraceLogger()) + private val applicationExitInfoService = FakeApplicationExitInfoService() + private val strictModeService = FakeStrictModeService() + + @Before + fun setUp() { + service = EmbracePerformanceInfoService( + anrService, + networkConnectivityService, + networkLoggingService, + powerSaveModeService, + memoryService, + metadataService, + googleAnrTimestampRepository, + applicationExitInfoService, + strictModeService, + null + ) + googleAnrTimestampRepository.add(150209234099) + } + + @Test + fun testPerformanceInfo() { + val info = service.getPerformanceInfo(0, SESSION_END_TIME_MS, false) + assertBasicPerfInfoIncluded(info) + } + + @Test + fun testSessionPerformanceInfoNonColdStart() { + val info = service.getSessionPerformanceInfo(0, SESSION_END_TIME_MS, false, null) + assertBasicPerfInfoIncluded(info) + assertBasicSessionPerfInfoIncluded(info) + + // verify certain fields were not included in non-cold start + assertNull(info.appExitInfoData) + } + + @Test + fun testSessionPerformanceInfoColdStart() { + val info = service.getSessionPerformanceInfo(0, SESSION_END_TIME_MS, true, null) + assertBasicPerfInfoIncluded(info) + assertBasicSessionPerfInfoIncluded(info) + + assertValueCopied(anrService.data, info.anrIntervals) + assertValueCopied(applicationExitInfoService.data, info.appExitInfoData) + } + + private fun assertBasicPerfInfoIncluded(info: PerformanceInfo) { + assertValueCopied(metadataService.getDiskUsage(), info.diskUsage) + assertValueCopied(memoryService.data, info.memoryWarnings) + assertValueCopied(networkConnectivityService.data, info.networkInterfaceIntervals) + assertValueCopied(powerSaveModeService.data, info.powerSaveModeIntervals) + } + + private fun assertBasicSessionPerfInfoIncluded(info: PerformanceInfo) { + assertValueCopied(NetworkRequests(networkLoggingService.data), info.networkRequests) + assertValueCopied(anrService.data, info.anrIntervals) + assertValueCopied(anrService.anrProcessErrors, info.anrProcessErrors) + assertValueCopied( + googleAnrTimestampRepository.getGoogleAnrTimestamps(0, SESSION_END_TIME_MS), + info.googleAnrTimestamps + ) + assertValueCopied(strictModeService.data, info.strictmodeViolations) + } + + private fun assertValueCopied(expected: Any?, observed: Any?) { + checkNotNull(expected) + checkNotNull(observed) + assertEquals(expected, observed) + assertNotSame( + "An original reference was added to the PerformanceInfo object. This can lead to " + + "data corruption & thread safety issues. Please fix EmbracePerformanceInfoService so that " + + "it makes a proper defensive copy for this property.", + expected, + observed + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePowerSaveModeServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePowerSaveModeServiceTest.kt new file mode 100644 index 0000000000..3a6581236d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbracePowerSaveModeServiceTest.kt @@ -0,0 +1,182 @@ +package io.embrace.android.embracesdk + +import android.content.Context +import android.content.Intent +import android.os.PowerManager +import android.os.PowerManager.ACTION_POWER_SAVE_MODE_CHANGED +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.capture.powersave.EmbracePowerSaveModeService +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ExecutorService + +internal class EmbracePowerSaveModeServiceTest { + + private lateinit var service: EmbracePowerSaveModeService + + companion object { + private lateinit var mockContext: Context + private lateinit var mockIntent: Intent + private lateinit var mockCleanerService: MemoryCleanerService + private lateinit var executor: ExecutorService + private lateinit var fakeClock: FakeClock + private lateinit var powerManager: PowerManager + private lateinit var activityService: ActivityService + private lateinit var activityListener: ActivityListener + + /** + * Setup before all tests get executed. Create mocks here. + */ + @BeforeClass + @JvmStatic + fun setupBeforeAll() { + mockContext = mockk(relaxUnitFun = true) + mockIntent = mockk() + mockCleanerService = mockk(relaxUnitFun = true) + executor = MoreExecutors.newDirectExecutorService() + fakeClock = FakeClock() + powerManager = mockk() + activityService = FakeActivityService() + activityListener = mockk() + } + + /** + * Setup after all tests get executed. Un-mock all here. + */ + @AfterClass + @JvmStatic + fun tearDownAfterAll() { + unmockkAll() + executor.shutdown() + } + } + + /** + * Setup before each test. + */ + @Before + fun setup() { + service = EmbracePowerSaveModeService( + mockContext, + executor, + fakeClock, + powerManager + ) + } + + /** + * Setup after each test. Clean mocks content. + */ + @After + fun tearDown() { + clearAllMocks() + } + + @Test + fun `test onReceive adds a low battery interval correctly`() { + every { mockContext.getSystemService(Context.POWER_SERVICE) } returns powerManager + every { mockIntent.action } returns ACTION_POWER_SAVE_MODE_CHANGED + every { powerManager.isPowerSaveMode } returns true + fakeClock.setCurrentTime(111111L) + + service.onReceive(mockContext, mockIntent) + every { powerManager.isPowerSaveMode } returns false + + service.onReceive(mockContext, mockIntent) + val intervals = service.getCapturedData() + + assertEquals(1, intervals.size) + assertTrue(intervals[0].startTime != 0L) + assertTrue(intervals[0].endTime != null) + } + + @Test + fun `test onReceive start interval no end`() { + every { mockContext.getSystemService(Context.POWER_SERVICE) } returns powerManager + every { mockIntent.action } returns ACTION_POWER_SAVE_MODE_CHANGED + every { powerManager.isPowerSaveMode } returns true + fakeClock.setCurrentTime(111111L) + service.onReceive(mockContext, mockIntent) + + val intervals = service.getCapturedData() + assertEquals(1, intervals.size) + assertTrue(intervals[0].startTime != 0L) + assertTrue(intervals[0].endTime == null) + } + + @Test + fun `test start session on power save mode no end`() { + every { mockContext.getSystemService(Context.POWER_SERVICE) } returns powerManager + every { powerManager.isPowerSaveMode } returns true + fakeClock.setCurrentTime(111111L) + val startTime = fakeClock.now() + service.onForeground(true, startTime, startTime) + + val intervals = service.getCapturedData() + assertEquals(1, intervals.size) + assertTrue(intervals[0].startTime != 0L) + assertTrue(intervals[0].endTime == null) + } + + @Test + fun `test start session on power save mode end saving mode in session`() { + every { mockContext.getSystemService(Context.POWER_SERVICE) } returns powerManager + every { powerManager.isPowerSaveMode } returns true + fakeClock.setCurrentTime(111111L) + val startTime = fakeClock.now() + service.onForeground(true, startTime, startTime) + + every { mockIntent.action } returns ACTION_POWER_SAVE_MODE_CHANGED + every { powerManager.isPowerSaveMode } returns false + service.onReceive(mockContext, mockIntent) + + val intervals = service.getCapturedData() + assertEquals(1, intervals.size) + assertTrue(intervals[0].startTime != 0L) + assertTrue(intervals[0].endTime != null) + } + + @Test + fun `test onReceive throws an exception`() { + every { mockIntent.action } throws Exception() + fakeClock.setCurrentTime(111111L) + assertThrows(Exception::class.java) { service.onReceive(mockContext, mockIntent) } + } + + @Test + @Throws(InterruptedException::class) + fun `test receiver can be registered and unregistered`() { + verify { mockContext.registerReceiver(service, any()) } + service.close() + verify { mockContext.unregisterReceiver(service) } + } + + @Test + fun testCleanCollections() { + every { mockContext.getSystemService(Context.POWER_SERVICE) } returns powerManager + every { powerManager.isPowerSaveMode } returns true + fakeClock.setCurrentTime(111111L) + val startTime = fakeClock.now() + service.onForeground(true, startTime, startTime) + + assertEquals(1, service.getCapturedData().size) + service.cleanCollections() + assertEquals(0, service.getCapturedData().size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceSessionPropertiesTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceSessionPropertiesTest.kt new file mode 100644 index 0000000000..821b4e7d47 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceSessionPropertiesTest.kt @@ -0,0 +1,273 @@ +@file:Suppress("DEPRECATION") + +package io.embrace.android.embracesdk + +import android.content.Context +import android.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeSessionBehavior +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.EmbracePreferencesService +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors + +@RunWith(AndroidJUnit4::class) +internal class EmbraceSessionPropertiesTest { + + companion object { + private val fakeClock = FakeClock() + private const val KEY_VALID = "abc" + private const val VALUE_VALID = "def" + } + + private lateinit var preferencesService: PreferencesService + private lateinit var sessionProperties: EmbraceSessionProperties + private lateinit var context: Context + private lateinit var logger: InternalEmbraceLogger + private lateinit var configService: ConfigService + private lateinit var config: RemoteConfig + + @Before + fun setUp() { + val executorService = Executors.newSingleThreadExecutor() + context = ApplicationProvider.getApplicationContext() + logger = InternalEmbraceLogger() + val prefs = lazy { PreferenceManager.getDefaultSharedPreferences(context) } + preferencesService = + EmbracePreferencesService(executorService, prefs, fakeClock, EmbraceSerializer()) + + config = RemoteConfig() + configService = FakeConfigService( + sessionBehavior = fakeSessionBehavior { + config + } + ) + sessionProperties = EmbraceSessionProperties( + preferencesService, + logger, + configService + ) + } + + @Test + fun `Add session property with no error when maxSessionProperties is absent`() { + assertTrue(sessionProperties.add(KEY_VALID, VALUE_VALID, false)) + assertEquals(1, sessionProperties.get().size.toLong()) + } + + @Test + fun addSessionProperty() { + assertTrue(sessionProperties.add(KEY_VALID, VALUE_VALID, false)) + assertEquals(1, sessionProperties.get().size.toLong()) + assertEquals(VALUE_VALID, sessionProperties.get()[KEY_VALID]) + + // temporary property should not have been persisted + val sessionProperties2 = EmbraceSessionProperties(preferencesService, logger, configService) + assertTrue(sessionProperties2.get().isEmpty()) + } + + @Test + fun addSessionPropertyInvalidKey() { + assertFalse(sessionProperties.add("", VALUE_VALID, false)) + assertTrue(sessionProperties.get().isEmpty()) + } + + @Test + fun addSessionPropertyInvalidValue() { + assertTrue(sessionProperties.get().isEmpty()) + } + + @Test + fun addSessionPropertyPermanent() { + assertTrue(sessionProperties.add(KEY_VALID, VALUE_VALID, true)) + assertEquals(1, sessionProperties.get().size.toLong()) + assertEquals(VALUE_VALID, sessionProperties.get()[KEY_VALID]) + + // permanent property should have been persisted + val sessionProperties2 = EmbraceSessionProperties(preferencesService, logger, configService) + assertEquals(1, sessionProperties2.get().size.toLong()) + assertEquals(VALUE_VALID, sessionProperties2.get()[KEY_VALID]) + + // /change property to be not permanent + assertTrue(sessionProperties.add(KEY_VALID, VALUE_VALID, false)) + + // permanent property should no longer have been persisted + val sessionProperties3 = EmbraceSessionProperties(preferencesService, logger, configService) + assertTrue(sessionProperties3.get().isEmpty()) + } + + @Test + fun addSessionPropertyKeyTooLong() { + val longKey = "a".repeat(129) + assertTrue(sessionProperties.add(longKey, VALUE_VALID, false)) + assertEquals(1, sessionProperties.get().size.toLong()) + val key = "a".repeat(125) + "..." + assertEquals(VALUE_VALID, sessionProperties.get()[key]) + } + + @Test + fun addSessionPropertyValueTooLong() { + val longValue = "a".repeat(1025) + assertTrue(sessionProperties.add(KEY_VALID, longValue, false)) + assertEquals(1, sessionProperties.get().size.toLong()) + val value = "a".repeat(1021) + "..." + assertEquals(value, sessionProperties.get()[KEY_VALID]) + } + + @Test + @Throws(InterruptedException::class) + fun addSessionPropertyFromMultipleThreads() { + val expected: MutableMap = HashMap() + val properties = ArrayList() + var key: String + for (i in 0 until MAX_SESSION_PROPERTIES_DEFAULT) { + key = "prop$i" + properties.add(key) + expected[key] = VALUE_VALID + } + val startSignal = CountDownLatch(1) + val doneSignal = CountDownLatch(properties.size) + for (property in properties) { + // start workers that will all add a fragment each + Thread(AddPropertyWorker(startSignal, doneSignal, sessionProperties, property)).start() + } + startSignal.countDown() + // wait for all the workers to finish + doneSignal.await() + assertEquals(properties.size.toLong(), sessionProperties.get().size.toLong()) + assertEquals(expected, sessionProperties.get()) + } + + internal class AddPropertyWorker( + private val startSignal: CountDownLatch, + private val doneSignal: CountDownLatch, + private val properties: EmbraceSessionProperties, + private val property: String + ) : Runnable { + override fun run() { + try { + startSignal.await() + assertTrue(properties.add(property, VALUE_VALID, false)) + doneSignal.countDown() + } catch (ex: InterruptedException) { + fail("worker thread died") + } + } + } + + @Test + fun addPropertyTooManyWithDefaultMax() { + var isPermanent = true + for (i in 0 until MAX_SESSION_PROPERTIES_DEFAULT) { + assertTrue(sessionProperties.add("prop$i", VALUE_VALID, isPermanent)) + // flip between permanent and temporary + isPermanent = !isPermanent + } + assertFalse( + "should not be able to add new key when limit is hit", + sessionProperties.add("propNew", VALUE_VALID, true) + ) + val otherValue = "other" + assertTrue( + "should be able to update key when properties are full", + sessionProperties.add("prop0", otherValue, true) + ) + assertEquals( + "property was updated", otherValue, + sessionProperties.get()["prop0"] + ) + assertTrue(sessionProperties.remove("prop0")) + assertTrue( + "can add key once one was deleted", + sessionProperties.add("prop11", VALUE_VALID, isPermanent) + ) + } + + @Test + fun addPropertyTooManyWithRemoteConfigMax() { + config = RemoteConfig(maxSessionProperties = MAX_SESSION_PROPERTIES_FROM_CONFIG) + var isPermanent = true + for (i in 0 until MAX_SESSION_PROPERTIES_FROM_CONFIG) { + assertTrue(sessionProperties.add("prop$i", VALUE_VALID, isPermanent)) + // flip between permanent and temporary + isPermanent = !isPermanent + } + assertFalse( + "should not be able to add new key when limit is hit", + sessionProperties.add("propNew", VALUE_VALID, true) + ) + val otherValue = "other" + assertTrue( + "should be able to update key when properties are full", + sessionProperties.add("prop0", otherValue, true) + ) + assertEquals( + "property was updated", + otherValue, + sessionProperties.get()["prop0"] + ) + assertTrue(sessionProperties.remove("prop0")) + assertTrue( + "can add key once one was deleted", + sessionProperties.add("prop11", VALUE_VALID, isPermanent) + ) + } + + @Test + fun removeSessionProperty() { + assertTrue(sessionProperties.add(KEY_VALID, VALUE_VALID, false)) + assertTrue(sessionProperties.remove(KEY_VALID)) + assertTrue(sessionProperties.get().isEmpty()) + } + + @Test + fun removeSessionPropertyPermanent() { + assertTrue(sessionProperties.add(KEY_VALID, VALUE_VALID, true)) + + // permanent property should have been persisted + val sessionProperties2 = EmbraceSessionProperties(preferencesService, logger, configService) + assertEquals(1, sessionProperties2.get().size.toLong()) + assertTrue(sessionProperties.remove(KEY_VALID)) + assertTrue(sessionProperties.get().isEmpty()) + + // permanent property should have been removed + val sessionProperties3 = EmbraceSessionProperties(preferencesService, logger, configService) + assertTrue(sessionProperties3.get().isEmpty()) + } + + @Test + fun removeSessionPropertyInvalidKey() { + assertFalse(sessionProperties.remove("")) + } + + @Test + fun removeSessionPropertyDoesNotExist() { + assertFalse(sessionProperties.remove(KEY_VALID)) + } + + @Test + fun removeSessionPropertyLongKey() { + val longKey = "a".repeat(129) + assertTrue(sessionProperties.add(longKey, VALUE_VALID, false)) + assertTrue(sessionProperties.remove(longKey)) + assertTrue(sessionProperties.get().isEmpty()) + } +} + +private const val MAX_SESSION_PROPERTIES_FROM_CONFIG = 5 +private const val MAX_SESSION_PROPERTIES_DEFAULT = 10 diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceTest.kt new file mode 100644 index 0000000000..8ee21d14f5 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceTest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk + +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame +import org.junit.Test + +/** + * Tests for the Embrace SDK public API. + */ +internal class EmbraceTest { + + /** + * Tests that the Embrace SDK instance is not null. + */ + @Test + fun instanceIsNotNull() { + assertNotNull(Embrace.getInstance()) + } + + /** + * Tests that the Embrace SDK returns a singleton and not a new object every time. + */ + @Test + fun instanceIsSameBetweenCalls() { + val instance1 = Embrace.getInstance() + val instance2 = Embrace.getInstance() + assertSame(instance1, instance2) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceThermalStatusServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceThermalStatusServiceTest.kt new file mode 100644 index 0000000000..5b7f5ce127 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceThermalStatusServiceTest.kt @@ -0,0 +1,58 @@ +package io.embrace.android.embracesdk + +import android.os.PowerManager +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.capture.thermalstate.EmbraceThermalStatusService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.ThermalState +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class EmbraceThermalStatusServiceTest { + + private lateinit var service: EmbraceThermalStatusService + + @Before + fun setUp() { + service = EmbraceThermalStatusService( + MoreExecutors.directExecutor(), + { 0 }, + InternalEmbraceLogger(), + mockk(relaxed = true) + ) + } + + @Test + fun onThermalStatusChanged() { + with(service) { + handleThermalStateChange(PowerManager.THERMAL_STATUS_NONE) + handleThermalStateChange(PowerManager.THERMAL_STATUS_SEVERE) + handleThermalStateChange(PowerManager.THERMAL_STATUS_CRITICAL) + } + val states = service.getCapturedData() + assertEquals(3, states.size) + assertEquals(ThermalState(0, PowerManager.THERMAL_STATUS_NONE), states[0]) + assertEquals(ThermalState(0, PowerManager.THERMAL_STATUS_SEVERE), states[1]) + assertEquals(ThermalState(0, PowerManager.THERMAL_STATUS_CRITICAL), states[2]) + } + + @Test + fun onLimitExceeded() { + repeat(250) { + service.handleThermalStateChange(PowerManager.THERMAL_STATUS_SEVERE) + } + + val states = service.getCapturedData() + assertEquals(100, states.size) + } + + @Test + fun testCleanCollections() { + service.handleThermalStateChange(PowerManager.THERMAL_STATUS_CRITICAL) + assertEquals(1, service.getCapturedData().size) + service.cleanCollections() + assertEquals(0, service.getCapturedData().size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUncaughtExceptionHandlerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUncaughtExceptionHandlerTest.kt new file mode 100644 index 0000000000..bf49b638b1 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUncaughtExceptionHandlerTest.kt @@ -0,0 +1,86 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.capture.crash.CrashService +import io.embrace.android.embracesdk.capture.crash.EmbraceUncaughtExceptionHandler +import io.embrace.android.embracesdk.fakes.FakeCrashService +import io.embrace.android.embracesdk.payload.JsException +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Tests that the [EmbraceUncaughtExceptionHandler]: + * + * * Does not permit null args + * * Delegates to the [CrashService] + * * Always delegates to the default [Thread.UncaughtExceptionHandler] + */ +internal class EmbraceUncaughtExceptionHandlerTest { + + /** + * Tests that [EmbraceUncaughtExceptionHandler] accepts a null arg. + * + * + * The [Thread.UncaughtExceptionHandler] is null by default. + */ + @Test + fun testNullArg1() { + EmbraceUncaughtExceptionHandler(null, FakeCrashService()) + } + + /** + * Tests that [EmbraceUncaughtExceptionHandler] correctly returns the crash to the + * [CrashService], and then delegates to the default [Thread.UncaughtExceptionHandler]. + */ + @Test + fun testExceptionHandler() { + val defaultHandler = TestUncaughtExceptionHandler() + val fakeCrashService = FakeCrashService() + val handler = EmbraceUncaughtExceptionHandler(defaultHandler, fakeCrashService) + + val testException = RuntimeException("Test exception") + handler.uncaughtException(Thread.currentThread(), testException) + val actual = fakeCrashService.exception + + // Test that the exception returned to the crash service matches + assertEquals(testException, actual) + + // Test that the exception was successfully delegated to the default handler + assertEquals(Thread.currentThread(), defaultHandler.thread) + assertEquals(testException, defaultHandler.throwable) + } + + /** + * Tests that where we throw an exception whilst processing the crash, we still delegate + * to the default [Thread.UncaughtExceptionHandler]. + */ + @Test + fun testCrashingExceptionHandler() { + val defaultHandler = TestUncaughtExceptionHandler() + val crashingCrashService = CrashingCrashService() + val handler = EmbraceUncaughtExceptionHandler(defaultHandler, crashingCrashService) + val testException = RuntimeException("Test exception") + handler.uncaughtException(Thread.currentThread(), testException) + + // Test that the exception was successfully delegated to the default handler + assertEquals(Thread.currentThread(), defaultHandler.thread) + assertEquals(testException, defaultHandler.throwable) + } + + internal class CrashingCrashService : CrashService { + override fun handleCrash(thread: Thread, exception: Throwable) { + throw RuntimeException("Test crash") + } + + override fun logUnhandledJsException(exception: JsException) {} + } + + internal class TestUncaughtExceptionHandler : Thread.UncaughtExceptionHandler { + var thread: Thread? = null + var throwable: Throwable? = null + + override fun uncaughtException(thread: Thread, throwable: Throwable) { + this.thread = thread + this.throwable = throwable + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUserServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUserServiceTest.kt new file mode 100644 index 0000000000..e933ead88b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceUserServiceTest.kt @@ -0,0 +1,198 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.capture.user.EmbraceUserService +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.UserInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * Tests that the persona regex filters personas correctly. + */ +internal class EmbraceUserServiceTest { + + private val extraPersonas = setOf( + "payer", + "first_day" + ) + + private val userPersonas = setOf( + "persona", + ) + + private val customPersonas = setOf( + "PERSONA1", + "1persona2", + "a", + "7", + "Persona_2", + "1Persona_3_" + ) + + private lateinit var service: EmbraceUserService + private lateinit var preferencesService: FakePreferenceService + private lateinit var logger: InternalEmbraceLogger + + @Before + fun setUp() { + logger = InternalEmbraceLogger() + preferencesService = FakePreferenceService() + } + + @Test + fun testUserInfoNotLoaded() { + mockNoUserInfo() + assertNotNull(service.info) + service.info.verifyNoUserInfo() + } + + @Test + fun testUserInfoLoaded() { + mockUserInfo() + assertNotNull(service.info) + service.info.verifyExpectedUserInfo() + } + + @Test + fun testUserInfoSessionCopy() { + mockUserInfo() + assertNotSame(service.info, service.getUserInfo()) + } + + @Test + fun testUserIdentifier() { + mockUserInfo() + + with(service) { + assertEquals("f0a923498c", info.userId) + setUserIdentifier("abc") + assertEquals("abc", info.userId) + service.clearUserIdentifier() + assertNull(info.userId) + } + } + + @Test + fun testUsername() { + mockUserInfo() + + with(service) { + assertEquals("Mr Test", info.username) + setUsername("Joe") + assertEquals("Joe", info.username) + service.clearUsername() + assertNull(info.username) + } + } + + @Test + fun testUserEmail() { + mockUserInfo() + + with(service) { + assertEquals("test@example.com", info.email) + setUserEmail("foo@test.com") + assertEquals("foo@test.com", info.email) + service.clearUserEmail() + assertNull(info.email) + } + } + + @Test + fun testUserAsPayer() { + mockUserInfo() + + with(service) { + assertTrue(info.personas!!.contains("payer")) + clearUserAsPayer() + assertFalse(info.personas!!.contains("payer")) + setUserAsPayer() + assertTrue(info.personas!!.contains("payer")) + } + } + + @Test + fun testClearAllUserInfo() { + mockUserInfo() + + with(service) { + info.verifyExpectedUserInfo() + service.clearAllUserInfo() + assertNull(info.email) + assertNull(info.userId) + assertNull(info.username) + assertEquals(extraPersonas, info.personas) + } + } + + @Test + fun testInvalidPersonaLogMsg() { + mockUserInfo() + val persona = "!@£$$%*(" + service.addUserPersona(persona) + assertFalse(service.info.personas!!.contains(persona)) + } + + @Test + fun testMaxPersonaLogMsg() { + mockNoUserInfo() + + repeat(11) { k -> + service.addUserPersona("Persona_$k") + } + val personas = checkNotNull(service.info.personas) + assertTrue(personas.contains("Persona_1")) + assertTrue(personas.contains("Persona_9")) + assertFalse(personas.contains("Persona_10")) + } + + private fun mockUserInfo() { + preferencesService.userEmailAddress = "test@example.com" + preferencesService.userIdentifier = "f0a923498c" + preferencesService.username = "Mr Test" + preferencesService.userPersonas = userPersonas + @Suppress("DEPRECATION") + preferencesService.customPersonas = customPersonas + preferencesService.userPayer = true + preferencesService.firstDay = true + + // load user info + service = EmbraceUserService(preferencesService, logger) + } + + private fun mockNoUserInfo() { + preferencesService.userEmailAddress = null + preferencesService.userIdentifier = null + preferencesService.username = null + preferencesService.userPersonas = null + @Suppress("DEPRECATION") + preferencesService.customPersonas = null + preferencesService.userPayer = false + preferencesService.firstDay = false + + // load user info + service = EmbraceUserService(preferencesService, logger) + } + + private fun UserInfo.verifyExpectedUserInfo() { + assertEquals("test@example.com", email) + assertEquals("f0a923498c", userId) + assertEquals("Mr Test", username) + val expectedPersonas = userPersonas.plus(customPersonas).plus(extraPersonas) + assertEquals(expectedPersonas, personas) + } + + private fun UserInfo.verifyNoUserInfo() { + assertNull(email) + assertNull(userId) + assertNull(username) + assertEquals(emptySet(), personas) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceWebViewServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceWebViewServiceTest.kt new file mode 100644 index 0000000000..bcaa8aa7ab --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceWebViewServiceTest.kt @@ -0,0 +1,177 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.capture.webview.EmbraceWebViewService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.WebViewVitals +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeWebViewVitalsBehavior +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.payload.WebVitalType +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.embrace.android.embracesdk.utils.at +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class EmbraceWebViewServiceTest { + + companion object { + private const val embraceKeyForConsoleLogs = "EMBRACE_METRIC" + } + + private val expectedCompleteData = + ResourceReader.readResourceAsText("expected_core_vital_script.json") + private val expectedCompleteData2 = + ResourceReader.readResourceAsText("expected_core_vital_script1.json") + + // same url and start time than expected_core_vital_script.json + private val expectedCompleteRepeatedData = + ResourceReader.readResourceAsText("expected_core_vital_script_repeated.json") + + // repeated elements in the same console message + private val repeatedElementsSameMessage = + ResourceReader.readResourceAsText("expected_core_vital_repeated_elements_script.json") + + private lateinit var configService: ConfigService + private lateinit var memoryCleanerService: MemoryCleanerService + private lateinit var embraceWebViewService: EmbraceWebViewService + private var cfg: RemoteConfig? = RemoteConfig() + + @Before + fun setup() { + memoryCleanerService = mockk() + every { memoryCleanerService.addListener(any()) } returns Unit + cfg = RemoteConfig(webViewVitals = WebViewVitals(100f, 50)) + configService = FakeConfigService(webViewVitalsBehavior = fakeWebViewVitalsBehavior { cfg }) + embraceWebViewService = EmbraceWebViewService(configService, EmbraceSerializer()) + } + + @Test + fun `test messages complete group by url and timestamp`() { + + embraceWebViewService.collectWebData("webView1", expectedCompleteData) + + assertEquals(1, embraceWebViewService.getCapturedData().size) + assertEquals(4, embraceWebViewService.getCapturedData().at(0)?.webVitals?.size) + } + + @Test + fun `test two complete groups by url and timestamp`() { + + embraceWebViewService.collectWebData("webView1", expectedCompleteData) + embraceWebViewService.collectWebData("webView1", expectedCompleteData2) + + assertEquals(2, embraceWebViewService.getCapturedData().size) + assertEquals(4, embraceWebViewService.getCapturedData().at(0)?.webVitals?.size) + assertEquals(4, embraceWebViewService.getCapturedData().at(1)?.webVitals?.size) + } + + @Test + fun `test two complete groups whit same url and timestamp keep correct CLS and LCP`() { + + embraceWebViewService.collectWebData("webView1", expectedCompleteData) + embraceWebViewService.collectWebData("webView1", expectedCompleteRepeatedData) + + assertEquals(1, embraceWebViewService.getCapturedData().size) + assertEquals(4, embraceWebViewService.getCapturedData().at(0)?.webVitals?.size) + + embraceWebViewService.getCapturedData().at(0)?.webVitals?.forEach { + when (it.type) { + WebVitalType.CLS -> { + assertEquals( + 20, + it.duration + ) // bigger duration from expectedCompleteRepeatedData + } + + WebVitalType.LCP -> { + assertEquals(2222, it.startTime) // bigger starttime from expectedCompleteData + } + } + } + } + + @Test + fun `test 3 groups 2 diff timestamps `() { + + embraceWebViewService.collectWebData("webView1", expectedCompleteData) + embraceWebViewService.collectWebData("webView1", expectedCompleteData2) + embraceWebViewService.collectWebData("webView1", expectedCompleteRepeatedData) + + assertEquals(2, embraceWebViewService.getCapturedData().size) + assertEquals(4, embraceWebViewService.getCapturedData().at(0)?.webVitals?.size) + assertEquals(4, embraceWebViewService.getCapturedData().at(1)?.webVitals?.size) + } + + @Test + fun `test repeated elements in one message`() { + + embraceWebViewService.collectWebData("webView1", repeatedElementsSameMessage) + + assertEquals(1, embraceWebViewService.getCapturedData().size) + assertEquals(4, embraceWebViewService.getCapturedData().at(0)?.webVitals?.size) + + embraceWebViewService.getCapturedData().at(0)?.webVitals?.forEach { + when (it.type) { + WebVitalType.CLS -> { + assertEquals( + 30, + it.duration + ) // bigger duration from expectedCompleteRepeatedData + } + + WebVitalType.LCP -> { + assertEquals(2222, it.startTime) // bigger starttime from expectedCompleteData + } + } + } + } + + @Test + fun `test limit collect web vital by maxVitals remote config`() { + cfg = RemoteConfig(webViewVitals = WebViewVitals(100f, 1)) + + embraceWebViewService.collectWebData("webViewMock", expectedCompleteData) + embraceWebViewService.collectWebData("webViewMock", expectedCompleteData2) + assertEquals(1, embraceWebViewService.getCapturedData().size) + + // same but bigger max vitals limit + cfg = RemoteConfig(webViewVitals = WebViewVitals(100f, 10)) + + embraceWebViewService.collectWebData("webViewMock", expectedCompleteData) + embraceWebViewService.collectWebData("webViewMock", expectedCompleteData2) + assertEquals(2, embraceWebViewService.getCapturedData().size) + } + + @Test + fun `test web vital is not collected if json exceeds max length`() { + val repeatTimes = 2000 / embraceKeyForConsoleLogs.length + val messageTooLong = + "$embraceKeyForConsoleLogs ".repeat(repeatTimes) + "1" // limit is 800 characters + + embraceWebViewService.collectWebData("webViewMock", messageTooLong) + assertEquals(0, embraceWebViewService.getCapturedData().size) + } + + @Test + fun `WebView console log is only collected if it has the Embrace key`() { + val dataWithoutKey = expectedCompleteData2.replace(embraceKeyForConsoleLogs, "") + + embraceWebViewService.collectWebData("webView1", expectedCompleteData) + embraceWebViewService.collectWebData("webView2", dataWithoutKey) + + assertEquals(1, embraceWebViewService.getCapturedData().size) + } + + @Test + fun testWebViewCleanCollections() { + embraceWebViewService.collectWebData("webView1", repeatedElementsSameMessage) + assertEquals(1, embraceWebViewService.getCapturedData().size) + + embraceWebViewService.cleanCollections() + assertEquals(0, embraceWebViewService.getCapturedData().size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EssentialServiceModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EssentialServiceModuleImplTest.kt new file mode 100644 index 0000000000..a1be84142b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EssentialServiceModuleImplTest.kt @@ -0,0 +1,77 @@ +package io.embrace.android.embracesdk + +import android.os.Looper +import io.embrace.android.embracesdk.capture.metadata.EmbraceMetadataService +import io.embrace.android.embracesdk.capture.orientation.NoOpOrientationService +import io.embrace.android.embracesdk.config.EmbraceConfigService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.injection.FakeAndroidServicesModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeSystemServiceModule +import io.embrace.android.embracesdk.gating.EmbraceGatingService +import io.embrace.android.embracesdk.injection.EssentialServiceModuleImpl +import io.embrace.android.embracesdk.injection.InitModuleImpl +import io.embrace.android.embracesdk.internal.BuildInfo +import io.embrace.android.embracesdk.session.EmbraceActivityService +import io.embrace.android.embracesdk.session.EmbraceMemoryCleanerService +import io.embrace.android.embracesdk.worker.WorkerThreadModuleImpl +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class EssentialServiceModuleImplTest { + + @Test + fun testDefaultImplementations() { + mockkStatic(Looper::class) + every { Looper.getMainLooper() } returns mockk(relaxed = true) + + val coreModule = FakeCoreModule() + val module = EssentialServiceModuleImpl( + initModule = InitModuleImpl(), + coreModule = coreModule, + workerThreadModule = WorkerThreadModuleImpl(), + systemServiceModule = FakeSystemServiceModule(), + androidServicesModule = FakeAndroidServicesModule(), + buildInfo = BuildInfo("", "", ""), + customAppId = "abcde", + enableIntegrationTesting = false, + configStopAction = {}, + configServiceProvider = { null } + ) + + assertTrue(module.memoryCleanerService is EmbraceMemoryCleanerService) + assertTrue(module.orientationService is NoOpOrientationService) + assertTrue(module.activityService is EmbraceActivityService) + assertTrue(module.metadataService is EmbraceMetadataService) + assertNotNull(module.urlBuilder) + assertNotNull(module.cache) + assertNotNull(module.apiClient) + assertNotNull(module.apiService) + assertTrue(module.configService is EmbraceConfigService) + assertTrue(module.gatingService is EmbraceGatingService) + } + + @Test + fun testConfigServiceProvider() { + val fakeConfigService = FakeConfigService() + val module = EssentialServiceModuleImpl( + initModule = InitModuleImpl(), + coreModule = FakeCoreModule(), + workerThreadModule = FakeWorkerThreadModule(), + systemServiceModule = FakeSystemServiceModule(), + androidServicesModule = FakeAndroidServicesModule(), + buildInfo = BuildInfo("", "", ""), + customAppId = null, + enableIntegrationTesting = false, + configStopAction = {}, + configServiceProvider = { fakeConfigService } + ) + + assertSame(fakeConfigService, module.configService) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerFacadeTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerFacadeTest.kt new file mode 100644 index 0000000000..415bb1ac9d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerFacadeTest.kt @@ -0,0 +1,91 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.gating.EventSanitizerFacade +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.UserInfo +import io.mockk.mockk +import org.junit.Assert +import org.junit.Test + +internal class EventSanitizerFacadeTest { + + private val event = Event( + eventId = "123", + timestamp = 100L, + duration = 1000L, + appState = "state", + type = EmbraceEvent.Type.INFO_LOG, + customProperties = mapOf("custom" to 123), + sessionProperties = mapOf("custom" to "custom"), + logExceptionType = LogExceptionType.NONE.value + ) + + private val userInfo = UserInfo( + personas = setOf("personas"), + email = "example@embrace.com" + ) + + private val performanceInfo = PerformanceInfo( + anrIntervals = mockk(relaxed = true), + networkInterfaceIntervals = mockk(), + memoryWarnings = mockk(), + diskUsage = mockk() + ) + + private val eventMessage = EventMessage( + event = event, + userInfo = userInfo, + appInfo = mockk(), + deviceInfo = mockk(), + performanceInfo = performanceInfo + ) + + private val enabledComponents = setOf( + SessionGatingKeys.SESSION_PROPERTIES, + SessionGatingKeys.LOG_PROPERTIES, + SessionGatingKeys.USER_PERSONAS, + SessionGatingKeys.PERFORMANCE_ANR, + SessionGatingKeys.PERFORMANCE_CURRENT_DISK_USAGE, + SessionGatingKeys.PERFORMANCE_CPU, + SessionGatingKeys.PERFORMANCE_CONNECTIVITY, + SessionGatingKeys.PERFORMANCE_LOW_MEMORY + ) + + @Test + fun `test if it keeps all event message components`() { + val sanitizedMessage = + EventSanitizerFacade(eventMessage, enabledComponents).getSanitizedMessage() + + Assert.assertNotNull(sanitizedMessage.event.customPropertiesMap) + Assert.assertNotNull(sanitizedMessage.event.sessionPropertiesMap) + Assert.assertNotNull(sanitizedMessage.userInfo!!.personas) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.anrIntervals) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.networkInterfaceIntervals) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.memoryWarnings) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.diskUsage) + + Assert.assertNotNull(sanitizedMessage.appInfo) + Assert.assertNotNull(sanitizedMessage.deviceInfo) + } + + @Test + fun `test if it sanitizes event message components`() { + // uses an empty set for enabled components + val eventSanitizer = EventSanitizerFacade(eventMessage, setOf()) + val sanitizedMessage = eventSanitizer.getSanitizedMessage() + + Assert.assertNull(sanitizedMessage.event.customPropertiesMap) + Assert.assertNull(sanitizedMessage.event.sessionPropertiesMap) + Assert.assertNull(sanitizedMessage.userInfo!!.personas) + Assert.assertNull(sanitizedMessage.performanceInfo?.anrIntervals) + Assert.assertNull(sanitizedMessage.performanceInfo?.networkInterfaceIntervals) + Assert.assertNull(sanitizedMessage.performanceInfo?.memoryWarnings) + Assert.assertNull(sanitizedMessage.performanceInfo?.diskUsage) + + Assert.assertNotNull(sanitizedMessage.appInfo) + Assert.assertNotNull(sanitizedMessage.deviceInfo) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerTest.kt new file mode 100644 index 0000000000..c92a59a2e3 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EventSanitizerTest.kt @@ -0,0 +1,132 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.gating.EventSanitizer +import io.embrace.android.embracesdk.gating.SessionGatingKeys.SESSION_PROPERTIES +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.payload.Event +import org.junit.Assert +import org.junit.Test + +internal class EventSanitizerTest { + + @Test + fun `test if a error-log event sanitize properties`() { + // enabled components doesn't contain LOG_PROPERTIES + val components = setOf() + + val errorLogEvent = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.ERROR_LOG, + customProperties = mapOf("custom" to 123) + ) + + val sanitizerError = EventSanitizer(errorLogEvent, components) + val resultLogError = sanitizerError.sanitize() + Assert.assertNull(resultLogError.customPropertiesMap) + } + + @Test + fun `test if a info-log event sanitize properties`() { + // enabled components doesn't contain LOG_PROPERTIES + val components = setOf() + + val infoLogEvent = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.INFO_LOG, + customProperties = mapOf("custom" to 123) + ) + + val sanitizerInfo = EventSanitizer(infoLogEvent, components) + val resultInfo = sanitizerInfo.sanitize() + Assert.assertNull(resultInfo.customPropertiesMap) + } + + @Test + fun `test if a warning-log event sanitize properties`() { + // enabled components doesn't contain LOG_PROPERTIES + val components = setOf() + + val warningLogEvent = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.WARNING_LOG, + customProperties = mapOf("custom" to 123) + ) + + val sanitizerWarning = EventSanitizer(warningLogEvent, components) + val resultWarning = sanitizerWarning.sanitize() + Assert.assertNull(resultWarning.customPropertiesMap) + } + + @Test + fun `test if a non-log event keeps custom properties`() { + // enabled components doesn't contain LOG_PROPERTIES + val components = setOf() + + val noLogEvent = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + customProperties = mapOf("custom" to 123), + type = EmbraceEvent.Type.START + ) + + val result = EventSanitizer(noLogEvent, components).sanitize() + Assert.assertNotNull(result.customPropertiesMap) + } + + @Test + fun `test if it sanitizes session properties`() { + // enabled components doesn't include SESSION_PROPERTIES + val components = setOf() + + val event = Event( + eventId = "123", + timestamp = 100L, + duration = 1000L, + appState = "state", + type = EmbraceEvent.Type.INFO_LOG, + customProperties = mapOf("custom" to 123), + sessionProperties = mapOf("custom" to "custom"), + logExceptionType = LogExceptionType.NONE.value + ) + + val sanitizer = EventSanitizer(event, components) + val result = sanitizer.sanitize() + + // Expected: Same event without sessionProperties + Assert.assertEquals("123", result.eventId) + Assert.assertEquals(1000L, result.duration) + Assert.assertEquals("state", result.appState) + Assert.assertEquals(EmbraceEvent.Type.INFO_LOG, result.type) + Assert.assertEquals(null, result.customPropertiesMap) + Assert.assertEquals(null, result.sessionPropertiesMap) + Assert.assertEquals(LogExceptionType.NONE.value, result.logExceptionType) + } + + @Test + fun `test if it keeps session properties`() { + val components = setOf(SESSION_PROPERTIES) + + val event = Event( + eventId = "123", + timestamp = 100L, + duration = 1000L, + appState = "state", + sessionProperties = mapOf("custom" to "custom"), + logExceptionType = LogExceptionType.NONE.value, + type = EmbraceEvent.Type.START + ) + + val sanitizer = EventSanitizer(event, components) + val result = sanitizer.sanitize() + + // Expected: Same event + Assert.assertEquals(event.eventId, result.eventId) + Assert.assertEquals(event.duration, result.duration) + Assert.assertEquals(event.appState, result.appState) + Assert.assertEquals(event.sessionPropertiesMap, result.sessionPropertiesMap) + Assert.assertEquals(event.logExceptionType, result.logExceptionType) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionErrorInfoTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionErrorInfoTest.kt new file mode 100644 index 0000000000..a981fa792d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionErrorInfoTest.kt @@ -0,0 +1,47 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.ExceptionErrorInfo +import io.embrace.android.embracesdk.payload.ExceptionInfo +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class ExceptionErrorInfoTest { + + private val info = ExceptionInfo( + "java.lang.IllegalStateException", + "Whoops!", + listOf( + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ) + ) + + @Test + fun testExceptionErrorInfoSerialization() { + val exceptionErrorInfo = ExceptionErrorInfo( + 0, "STATE", + listOf( + info, + ) + ) + + val expectedInfo = ResourceReader.readResourceAsText("exception_error_info_expected.json") + .filter { !it.isWhitespace() } + + val observed = Gson().toJson(exceptionErrorInfo) + assertEquals(expectedInfo, observed) + } + + @Test + fun testExceptionErrorInfoDeserialization() { + val json = ResourceReader.readResourceAsText("exception_error_info_expected.json") + val obj = Gson().fromJson(json, ExceptionErrorInfo::class.java) + assertEquals(0L, obj.timestamp) + assertEquals("STATE", obj.state) + val exceptionInfo = obj.exceptions?.get(0) + assertEquals(info.message, exceptionInfo?.message) + assertEquals(info.name, exceptionInfo?.name) + assertEquals(info.lines, exceptionInfo?.lines) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionInfoTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionInfoTest.kt new file mode 100644 index 0000000000..bfd8d317ec --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ExceptionInfoTest.kt @@ -0,0 +1,82 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.ExceptionInfo +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +internal class ExceptionInfoTest { + + private val info = ExceptionInfo( + "java.lang.IllegalStateException", + "Whoops!", + listOf( + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ) + ) + + @Test + fun testExceptionInfoSerialization() { + val data = ResourceReader.readResourceAsText("exception_info_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(data, observed) + } + + @Test + fun testExceptionInfoDeserialization() { + val json = ResourceReader.readResourceAsText("exception_info_expected.json") + val obj = Gson().fromJson(json, ExceptionInfo::class.java) + assertEquals("java.lang.IllegalStateException", obj.name) + assertEquals("Whoops!", obj.message) + assertEquals("java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", obj.lines[0]) + assertEquals( + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)", + obj.lines[1] + ) + } + + @Test + fun testExceptionInfoEmptyObject() { + val info = Gson().fromJson("{}", ExceptionInfo::class.java) + assertNotNull(info) + info.name + } + + @Test + fun testOfThrowable() { + val info = ExceptionInfo.ofThrowable( + mockk { + every { message } returns "UhOh." + every { stackTrace } returns arrayOf( + StackTraceElement("Foo", "bar", "Foo.kt", 5) + ) + } + ) + assertNotNull(info) + assertEquals("UhOh.", info.message) + assertEquals("java.lang.Throwable", info.name) + assertEquals("Foo.bar(Foo.kt:5)", info.lines.single()) + assertNull(info.originalLength) + } + + @Test + fun testMaxStacktraceLimit() { + val limit = 200 + val len = limit + 100 + val obj = ExceptionInfo( + "java.lang.IllegalStateException", + "Whoops!", + (0 until len).map { "line $it" } + ) + assertEquals(limit, obj.lines.size) + val expected = (0 until limit).map { "line $it" } + assertEquals(expected, obj.lines) + assertEquals(len, obj.originalLength) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBackgroundActivity.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBackgroundActivity.kt new file mode 100644 index 0000000000..57b04a2f89 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBackgroundActivity.kt @@ -0,0 +1,35 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.BackgroundActivity +import io.embrace.android.embracesdk.payload.BackgroundActivityMessage +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.CustomBreadcrumb +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.DiskUsage +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.UserInfo +import io.opentelemetry.api.trace.StatusCode + +internal fun fakeBackgroundActivity(): BackgroundActivityMessage { + val backgroundActivity = BackgroundActivity("fake-activity", 0, "") + val userInfo = UserInfo("fake-user-id") + val appInfo = AppInfo("fake-app-id") + val deviceInfo = DeviceInfo("fake-manufacturer") + val breadcrumbs = Breadcrumbs( + customBreadcrumbs = listOf(CustomBreadcrumb("fake-breadcrumb", 1)) + ) + val spans = listOf(EmbraceSpanData("fake-span-id", "", "", "", 0, 0, StatusCode.OK)) + val perfInfo = PerformanceInfo(DiskUsage(1, 2)) + + return BackgroundActivityMessage( + backgroundActivity, + userInfo, + appInfo, + deviceInfo, + perfInfo, + breadcrumbs, + spans + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBreadcrumbService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBreadcrumbService.kt new file mode 100644 index 0000000000..6437b56992 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeBreadcrumbService.kt @@ -0,0 +1,106 @@ +package io.embrace.android.embracesdk + +import android.util.Pair +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbService +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.CustomBreadcrumb +import io.embrace.android.embracesdk.payload.FragmentBreadcrumb +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb +import io.embrace.android.embracesdk.payload.RnActionBreadcrumb +import io.embrace.android.embracesdk.payload.TapBreadcrumb +import io.embrace.android.embracesdk.payload.ViewBreadcrumb +import io.embrace.android.embracesdk.payload.WebViewBreadcrumb + +internal class FakeBreadcrumbService : BreadcrumbService { + + override fun getViewBreadcrumbsForSession(start: Long, end: Long): List = + emptyList() + + override fun getTapBreadcrumbsForSession(start: Long, end: Long): List = + emptyList() + + override fun getCustomBreadcrumbsForSession(start: Long, end: Long): List = + emptyList() + + override fun getWebViewBreadcrumbsForSession(start: Long, end: Long): List = + emptyList() + + override fun getFragmentBreadcrumbsForSession( + startTime: Long, + endTime: Long + ): List = emptyList() + + override fun getRnActionBreadcrumbForSession( + startTime: Long, + endTime: Long + ): List = emptyList() + + override fun getPushNotificationsBreadcrumbsForSession( + startTime: Long, + endTime: Long + ): List = emptyList() + + override fun getBreadcrumbs(start: Long, end: Long): Breadcrumbs { + return Breadcrumbs() + } + + override fun flushBreadcrumbs(): Breadcrumbs { + return Breadcrumbs() + } + + override fun logView(screen: String?, timestamp: Long) { + } + + override fun forceLogView(screen: String?, timestamp: Long) { + } + + override fun replaceFirstSessionView(screen: String?, timestamp: Long) { + } + + override fun startView(name: String?): Boolean { + return false + } + + override fun endView(name: String?): Boolean { + return false + } + + override fun logTap( + point: Pair, + element: String, + timestamp: Long, + type: TapBreadcrumb.TapBreadcrumbType + ) { + } + + override fun logCustom(message: String, timestamp: Long) { + } + + override fun logRnAction( + name: String, + startTime: Long, + endTime: Long, + properties: Map, + bytesSent: Int, + output: String + ) { + } + + override fun logWebView(url: String?, startTime: Long) { + } + + override fun getLastViewBreadcrumbScreenName(): String? { + return null + } + + override fun logPushNotification( + title: String?, + body: String?, + topic: String?, + id: String?, + notificationPriority: Int?, + messageDeliveredPriority: Int, + type: PushNotificationBreadcrumb.NotificationType + ) { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeDeliveryService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeDeliveryService.kt new file mode 100644 index 0000000000..cbed732da4 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeDeliveryService.kt @@ -0,0 +1,90 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.comms.delivery.SessionMessageState +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.AppExitInfoData +import io.embrace.android.embracesdk.payload.BackgroundActivityMessage +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.NetworkEvent +import io.embrace.android.embracesdk.payload.SessionMessage + +/** + * A [DeliveryService] that records the last parameters used to invoke each method, and for the ones that need it, count the number of + * invocations. Please add additional tracking functionality as tests require them. + */ +internal class FakeDeliveryService : DeliveryService { + var lastSentNetworkCall: NetworkEvent? = null + var lastSentCrash: EventMessage? = null + var lastSentEvent: EventMessage? = null + val lastSentLogs: MutableList = mutableListOf() + var sendBackgroundActivitiesInvokedCount: Int = 0 + var lastSentBackgroundActivity: BackgroundActivityMessage? = null + var saveBackgroundActivityInvokedCount: Int = 0 + var lastSavedBackgroundActivity: BackgroundActivityMessage? = null + var lastEventSentAsync: EventMessage? = null + var eventSentAsyncInvokedCount: Int = 0 + var lastSavedCrash: EventMessage? = null + var lastSentCachedSession: String? = null + var lastSavedSession: SessionMessage? = null + val lastSentSessions: MutableList> = mutableListOf() + var appExitInfoRequests: MutableList> = mutableListOf() + + override fun saveSession(sessionMessage: SessionMessage) { + lastSavedSession = sessionMessage + } + + override fun sendSession(sessionMessage: SessionMessage, state: SessionMessageState) { + lastSentSessions.add(Pair(sessionMessage, state)) + } + + override fun sendCachedSessions(isNdkEnabled: Boolean, ndkService: NdkService, currentSession: String?) { + lastSentCachedSession = currentSession + } + + override fun saveCrash(crash: EventMessage) { + lastSavedCrash = crash + } + + override fun sendEventAsync(eventMessage: EventMessage) { + eventSentAsyncInvokedCount++ + lastEventSentAsync = eventMessage + } + + override fun saveBackgroundActivity(backgroundActivityMessage: BackgroundActivityMessage) { + saveBackgroundActivityInvokedCount++ + lastSavedBackgroundActivity = backgroundActivityMessage + } + + override fun sendBackgroundActivity(backgroundActivityMessage: BackgroundActivityMessage) { + lastSentBackgroundActivity = backgroundActivityMessage + } + + override fun sendBackgroundActivities() { + sendBackgroundActivitiesInvokedCount++ + } + + override fun sendLogs(eventMessage: EventMessage) { + lastSentLogs.add(eventMessage) + } + + override fun sendNetworkCall(networkEvent: NetworkEvent) { + lastSentNetworkCall = networkEvent + } + + override fun sendEvent(eventMessage: EventMessage) { + lastSentEvent = eventMessage + } + + override fun sendEventAndWait(eventMessage: EventMessage) { + lastSentEvent = eventMessage + } + + override fun sendCrash(crash: EventMessage) { + lastSentCrash = crash + } + + override fun sendAEIBlob(appExitInfoData: List) { + this.appExitInfoRequests.add(appExitInfoData) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeNdkService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeNdkService.kt new file mode 100644 index 0000000000..c4ab2f1270 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeNdkService.kt @@ -0,0 +1,34 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.NativeCrashData + +internal class FakeNdkService : NdkService { + override fun updateSessionId(newSessionId: String) { + TODO("Not yet implemented") + } + + override fun onSessionPropertiesUpdate(properties: Map) { + TODO("Not yet implemented") + } + + override fun onUserInfoUpdate() { + TODO("Not yet implemented") + } + + override fun getUnityCrashId(): String? { + TODO("Not yet implemented") + } + + override fun testCrash(isCpp: Boolean) { + TODO("Not yet implemented") + } + + override fun checkForNativeCrash(): NativeCrashData? { + TODO("Not yet implemented") + } + + override fun getSymbolsForCurrentArch(): Map? { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeSessionService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeSessionService.kt new file mode 100644 index 0000000000..816240127b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeSessionService.kt @@ -0,0 +1,39 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.payload.Session +import io.embrace.android.embracesdk.session.SessionService + +internal class FakeSessionService : SessionService { + + override fun startSession( + coldStart: Boolean, + startType: Session.SessionLifeEventType, + startTime: Long + ) { + TODO("Not yet implemented") + } + + override fun triggerStatelessSessionEnd(endType: Session.SessionLifeEventType) { + TODO("Not yet implemented") + } + + override fun handleCrash(crashId: String) { + TODO("Not yet implemented") + } + + override fun addProperty(key: String, value: String, permanent: Boolean): Boolean { + TODO("Not yet implemented") + } + + override fun removeProperty(key: String): Boolean { + TODO("Not yet implemented") + } + + override fun getProperties(): Map { + TODO("Not yet implemented") + } + + override fun close() { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeWorkerThreadModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeWorkerThreadModule.kt new file mode 100644 index 0000000000..a34864bfa4 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FakeWorkerThreadModule.kt @@ -0,0 +1,45 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.concurrency.BlockableExecutorService +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.worker.ExecutorName +import io.embrace.android.embracesdk.worker.WorkerThreadModule +import kotlin.reflect.KFunction1 + +/** + * Version of [WorkerThreadModule] used for tests that uses and exposes [BlockableExecutorService] and [BlockingScheduledExecutorService] + * so test writers can control the execution of jobs on these threads. + * + * Note: the providers must return the Blocking versions of the executor services that [WorkerThreadModule] relies on, so the overridden + * methods that provide access to those executor services can also explicitly return those types. Be aware of this when using this fake. + */ +internal class FakeWorkerThreadModule( + executorProvider: KFunction1 = ::BlockableExecutorService, + scheduledExecutorProvider: KFunction1 = ::BlockingScheduledExecutorService, + private val clock: FakeClock = FakeClock(), + private val blockingMode: Boolean = false +) : WorkerThreadModule { + private val executorServices = + ExecutorName.values().associateWith { + executorProvider(blockingMode) + } + + private val scheduledExecutorServices = + ExecutorName.values().associateWith { + scheduledExecutorProvider(clock) + } + + override fun backgroundExecutor(executorName: ExecutorName): BlockableExecutorService { + return checkNotNull(executorServices[executorName]) + } + + override fun scheduledExecutor(executorName: ExecutorName): BlockingScheduledExecutorService { + return checkNotNull(scheduledExecutorServices[executorName]) + } + + override fun close() { + executorServices.values.forEach { it.shutdown() } + scheduledExecutorServices.values.forEach { it.shutdown() } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImplTest.kt new file mode 100644 index 0000000000..4280301da7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImplTest.kt @@ -0,0 +1,112 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class FlutterInternalInterfaceImplTest { + + private lateinit var impl: FlutterInternalInterfaceImpl + private lateinit var embrace: EmbraceImpl + private lateinit var logger: InternalEmbraceLogger + private lateinit var metadataService: FakeAndroidMetadataService + + @Before + fun setUp() { + embrace = mockk(relaxed = true) + metadataService = FakeAndroidMetadataService() + logger = mockk(relaxed = true) + impl = FlutterInternalInterfaceImpl(embrace, mockk(), metadataService, logger) + } + + @Test + fun testSetFlutterSdkVersion() { + every { embrace.isStarted } returns true + impl.setEmbraceFlutterSdkVersion("2.12") + assertEquals("2.12", metadataService.fakeFlutterSdkVersion) + } + + @Test + fun testSetFlutterSdkVersionNotStarted() { + every { embrace.isStarted } returns false + impl.setEmbraceFlutterSdkVersion("2.12") + verify(exactly = 1) { + logger.logSDKNotInitialized(any()) + } + } + + @Test + fun testSetFlutterSdkVersionNull() { + every { embrace.isStarted } returns true + impl.setEmbraceFlutterSdkVersion(null) + assertEquals("fakeFlutterSdkVersion", metadataService.fakeFlutterSdkVersion) + } + + @Test + fun testSetDartSdkVersion() { + every { embrace.isStarted } returns true + impl.setDartVersion("2.12") + assertEquals("2.12", metadataService.fakeDartVersion) + } + + @Test + fun testSetDartVersionNotStarted() { + every { embrace.isStarted } returns false + impl.setDartVersion("2.12") + verify(exactly = 1) { + logger.logSDKNotInitialized(any()) + } + } + + @Test + fun testSetDartVersionNull() { + every { embrace.isStarted } returns true + impl.setDartVersion(null) + assertEquals("fakeDartVersion", metadataService.fakeDartVersion) + } + + @Test + fun testLogUnhandledDartException() { + every { embrace.isStarted } returns true + impl.logUnhandledDartException("stack", "exception name", "message", "ctx", "lib") + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "Dart error", + null, + null, + "stack", + LogExceptionType.UNHANDLED, + "ctx", + "lib", + "exception name", + "message" + ) + } + } + + @Test + fun testLogHandledDartException() { + every { embrace.isStarted } returns true + impl.logHandledDartException("stack", "exception name", "message", "ctx", "lib") + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "Dart error", + null, + null, + "stack", + LogExceptionType.HANDLED, + "ctx", + "lib", + "exception name", + "message" + ) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FragmentBreadcrumbTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FragmentBreadcrumbTest.kt new file mode 100644 index 0000000000..1ec3b355da --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/FragmentBreadcrumbTest.kt @@ -0,0 +1,39 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.FragmentBreadcrumb +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class FragmentBreadcrumbTest { + + private val info = FragmentBreadcrumb( + "test", + 1600000000, + 1600001000, + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("fragment_breadcrumb_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("fragment_breadcrumb_expected.json") + val obj = Gson().fromJson(json, FragmentBreadcrumb::class.java) + assertEquals("test", obj.name) + assertEquals(1600000000, obj.getStartTime()) + assertEquals(1600001000, obj.endTime) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", FragmentBreadcrumb::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt new file mode 100644 index 0000000000..d94f46a9fd --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.fakes.injection.FakeAndroidServicesModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeCrashModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class InternalInterfaceModuleImplTest { + + @Test + fun testModule() { + val module: InternalInterfaceModule = InternalInterfaceModuleImpl( + FakeCoreModule(), + FakeAndroidServicesModule(), + FakeEssentialServiceModule(), + EmbraceImpl(), + FakeCrashModule() + ) + + assertNotNull(module.flutterInternalInterface) + assertNotNull(module.unityInternalInterface) + assertNotNull(module.reactNativeInternalInterface) + assertNotNull(module.embraceInternalInterface) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/JsExceptionTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/JsExceptionTest.kt new file mode 100644 index 0000000000..24a9ea6708 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/JsExceptionTest.kt @@ -0,0 +1,42 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.JsException +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class JsExceptionTest { + + private val info = JsException( + "java.lang.IllegalStateException", + "Whoops!", + "JsError", + "foo(:20:21)" + ) + + @Test + fun testSerialization() { + val data = ResourceReader.readResourceAsText("js_exception_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(data, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("js_exception_expected.json") + val obj = Gson().fromJson(json, JsException::class.java) + assertEquals("java.lang.IllegalStateException", obj.name) + assertEquals("Whoops!", obj.message) + assertEquals("JsError", obj.type) + assertEquals("foo(:20:21)", obj.stacktrace) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", JsException::class.java) + assertNotNull(info) + info.name + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/LocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/LocalConfigTest.kt new file mode 100644 index 0000000000..9754b49bd1 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/LocalConfigTest.kt @@ -0,0 +1,523 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class LocalConfigTest { + + @Test + fun testEmptyConfig() { + val localConfig = LocalConfig.buildConfig("GrCPU", false, null, EmbraceSerializer()) + assertNotNull(localConfig) + } + + @Test + fun testAppOnlyConfig() { + val localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"app\": {\"report_disk_usage\": false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.app?.reportDiskUsage)) + } + + @Test + fun testBetaFunctionalityOnlyConfig() { + // disabled explicitly + var localConfig = + LocalConfig.buildConfig("GrCPU", false, "{\"beta_features_enabled\": false}", EmbraceSerializer()) + assertFalse(checkNotNull(localConfig.sdkConfig.betaFeaturesEnabled)) + + // enabled explicitly + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"beta_features_enabled\": true}", + EmbraceSerializer() + ) + assertTrue(checkNotNull(localConfig.sdkConfig.betaFeaturesEnabled)) + + // enabled by default + localConfig = LocalConfig.buildConfig("GrCPU", false, "{}", EmbraceSerializer()) + assertNull(localConfig.sdkConfig.betaFeaturesEnabled) + } + + @Test + fun testSigHandlerDetectionOnlyConfig() { + // disabled explicitly + var localConfig = + LocalConfig.buildConfig("GrCPU", false, "{\"sig_handler_detection\": false}", EmbraceSerializer()) + assertFalse(checkNotNull(localConfig.sdkConfig.sigHandlerDetection)) + + // enabled explicitly + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"sig_handler_detection\": true}", + EmbraceSerializer() + ) + assertTrue(checkNotNull(localConfig.sdkConfig.sigHandlerDetection)) + + // enabled by default + localConfig = LocalConfig.buildConfig("GrCPU", false, "{}", EmbraceSerializer()) + assertNull(localConfig.sdkConfig.sigHandlerDetection) + } + + @Test + fun testBaseUrlOnlyConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"base_urls\": {\"config\": \"custom_config\"}}", + EmbraceSerializer() + ) + assertEquals(localConfig.sdkConfig.baseUrls?.config, "custom_config") + localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"base_urls\": {\"data\": \"custom_data\"}}", + EmbraceSerializer() + ) + assertEquals(localConfig.sdkConfig.baseUrls?.data, "custom_data") + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"base_urls\": {\"data_dev\": \"custom_data_dev\"}}", + EmbraceSerializer() + ) + assertEquals( + localConfig.sdkConfig.baseUrls?.dataDev, + "custom_data_dev" + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"base_urls\": {\"images\": \"custom_images\"}}", + EmbraceSerializer() + ) + assertEquals(localConfig.sdkConfig.baseUrls?.images, "custom_images") + } + + @Test + fun testViewConfigOnlyConfig() { + var localConfig = LocalConfig.buildConfig("GrCPU", false, "{}", EmbraceSerializer()) + assertNull( + localConfig.sdkConfig.viewConfig?.enableAutomaticActivityCapture, + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"view_config\":{\"enable_automatic_activity_capture\":false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.viewConfig?.enableAutomaticActivityCapture)) + } + + @Test + fun testCrashHandlerOnlyConfig() { + var localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"crash_handler\": {\"enabled\": false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.crashHandler?.enabled)) + localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"crash_handler\": {\"ndk_enabled\": false}}", + EmbraceSerializer() + ) + assertFalse(localConfig.ndkEnabled) + } + + @Test + fun testSessionOnlyConfig() { + var localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"max_session_seconds\": 60}}", + EmbraceSerializer() + ) + assertEquals( + localConfig.sdkConfig.sessionConfig?.maxSessionSeconds, + 60 + ) + + // ignore max_session_seconds when it is too small + localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"max_session_seconds\": 59}}", + EmbraceSerializer() + ) + assertEquals( + 59, + localConfig.sdkConfig.sessionConfig?.maxSessionSeconds, + ) + + // max_session_seconds can be null + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"max_session_seconds\": null}}", + EmbraceSerializer() + ) + assertNull( + localConfig.sdkConfig.sessionConfig?.maxSessionSeconds, + ) + + // ignore max_session_seconds when it is too small + localConfig = + LocalConfig.buildConfig("GrCPU", false, "{\"session\": {\"async_end\": true}}", EmbraceSerializer()) + assertTrue(checkNotNull(localConfig.sdkConfig.sessionConfig?.asyncEnd)) + + // error_log_strict_mode is true + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"error_log_strict_mode\": true}}", + EmbraceSerializer() + ) + assertTrue( + checkNotNull(localConfig.sdkConfig.sessionConfig?.sessionEnableErrorLogStrictMode) + ) + + // receive a session component to restrict session messages + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"components\": [\"breadcrumbs_taps\"]}}", + EmbraceSerializer() + ) + assertTrue( + checkNotNull(localConfig.sdkConfig.sessionConfig?.sessionComponents) + .contains("breadcrumbs_taps") + ) + + // full session for component list is empty + localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"send_full_for\": []}}", + EmbraceSerializer() + ) + assertTrue( + checkNotNull(localConfig.sdkConfig.sessionConfig?.fullSessionEvents).isEmpty() + ) + + // receive a full session for component to restrict session messages + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"session\": {\"send_full_for\": [\"crashes\"]}}", + EmbraceSerializer() + ) + val sessionConfig = localConfig.sdkConfig.sessionConfig + assertFalse( + checkNotNull(sessionConfig?.fullSessionEvents?.isEmpty()) + ) + assertTrue( + checkNotNull(sessionConfig?.fullSessionEvents?.contains("crashes")) + ) + } + + @Test + fun testStartupMomentOnlyConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"startup_moment\": {\"automatically_end\": false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.startupMoment?.automaticallyEnd)) + } + + @Test + fun testTapsOnlyConfig() { + val localConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"taps\": {\"capture_coordinates\": false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.taps?.captureCoordinates)) + } + + @Test + fun testNetworkingOnlyConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"networking\": {\"capture_request_content_length\": true}}", + EmbraceSerializer() + ) + assertTrue( + checkNotNull(localConfig.sdkConfig.networking?.captureRequestContentLength) + ) + + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"networking\": {\"enable_native_monitoring\": true}}", + EmbraceSerializer() + ) + assertTrue(checkNotNull(localConfig.sdkConfig.networking?.enableNativeMonitoring)) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"networking\": {\"trace_id_header\": \"custom-value\"}}", + EmbraceSerializer() + ) + assertEquals( + checkNotNull(localConfig.sdkConfig.networking?.traceIdHeader), + "custom-value" + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"networking\": {\"disabled_url_patterns\": [\"a.b.c\", \"https://example.com\", \"https://example2.com/foo/123/bar\"]}}", + EmbraceSerializer() + ) + assertEquals( + 3, + localConfig.sdkConfig.networking?.disabledUrlPatterns?.size + ) + } + + @Test + fun testIntegrationModeEnabled() { + val localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"integration_mode\": true}", + EmbraceSerializer() + ) + assertTrue(checkNotNull(localConfig.sdkConfig.integrationModeEnabled)) + } + + @Test + fun testIntegrationModeDisabled() { + val localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"integration_mode\": false}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.integrationModeEnabled)) + } + + @Test + fun testWebviewCaptureOnlyConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"webview\": {\"enable\": false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.webViewConfig?.captureWebViews)) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"webview\": {\"capture_query_params\": false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.webViewConfig?.captureQueryParams)) + } + + @Test + fun testBackgroundActivityConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"background_activity\": {}}", + EmbraceSerializer() + ) + var backgroundActivityCfg = checkNotNull(localConfig.sdkConfig.backgroundActivityConfig) + assertNull( + backgroundActivityCfg.backgroundActivityCaptureEnabled + ) + assertNull( + backgroundActivityCfg.manualBackgroundActivityLimit + ) + assertNull( + backgroundActivityCfg.minBackgroundActivityDuration + ) + assertNull( + backgroundActivityCfg.maxCachedActivities + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"background_activity\": {\"capture_enabled\": true}}", + EmbraceSerializer() + ) + backgroundActivityCfg = checkNotNull(localConfig.sdkConfig.backgroundActivityConfig) + + assertTrue( + checkNotNull(backgroundActivityCfg.backgroundActivityCaptureEnabled) + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"background_activity\": {\"capture_enabled\": true, \"manual_background_activity_limit\": 50}}", + EmbraceSerializer() + ) + backgroundActivityCfg = checkNotNull(localConfig.sdkConfig.backgroundActivityConfig) + assertTrue( + checkNotNull(backgroundActivityCfg.backgroundActivityCaptureEnabled) + ) + assertEquals( + 50, + backgroundActivityCfg.manualBackgroundActivityLimit + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"background_activity\": {\"capture_enabled\": true, \"min_background_activity_duration\": 300}}", + EmbraceSerializer() + ) + backgroundActivityCfg = checkNotNull(localConfig.sdkConfig.backgroundActivityConfig) + assertTrue( + checkNotNull(backgroundActivityCfg.backgroundActivityCaptureEnabled) + ) + assertEquals( + 300L, + backgroundActivityCfg.minBackgroundActivityDuration + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"background_activity\": {\"capture_enabled\": true, \"max_cached_activities\": 50}}", + EmbraceSerializer() + ) + backgroundActivityCfg = checkNotNull(localConfig.sdkConfig.backgroundActivityConfig) + assertTrue( + checkNotNull(backgroundActivityCfg.backgroundActivityCaptureEnabled) + ) + assertEquals( + 50, + backgroundActivityCfg.maxCachedActivities + ) + } + + @Test + fun testComposeConfig() { + var localConfig = LocalConfig.buildConfig("GrCPU", false, "{}", EmbraceSerializer()) + assertNull( + localConfig.sdkConfig.composeConfig?.captureComposeOnClick, + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", + false, + "{\"compose\":{\"capture_compose_onclick\":false}}", + EmbraceSerializer() + ) + assertFalse(checkNotNull(localConfig.sdkConfig.composeConfig?.captureComposeOnClick)) + } + + @Test + fun testServiceEnablementMemoryServiceConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"memory_info\": false}}", + EmbraceSerializer() + ) + var cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + assertFalse( + checkNotNull(cfg.memoryServiceEnabled) + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"memory_info\": true}}", + EmbraceSerializer() + ) + cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + assertTrue( + checkNotNull(cfg.memoryServiceEnabled) + ) + } + + @Test + fun testServiceEnablementPowerSaveModeServiceConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"power_save_mode_info\": false}}", + EmbraceSerializer() + ) + var cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + assertFalse( + checkNotNull(cfg.powerSaveModeServiceEnabled) + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"power_save_mode_info\": true}}", + EmbraceSerializer() + ) + cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + assertTrue( + checkNotNull(cfg.powerSaveModeServiceEnabled) + ) + } + + @Test + fun testServiceEnablementNetworkConnectivityServiceConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"network_connectivity_info\": false}}", + EmbraceSerializer() + ) + var cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + + assertFalse( + checkNotNull(cfg.networkConnectivityServiceEnabled) + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"network_connectivity_info\": true}}", + EmbraceSerializer() + ) + cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + + assertTrue( + checkNotNull(cfg.networkConnectivityServiceEnabled) + ) + } + + @Test + fun testServiceEnablementAnrServiceConfig() { + var localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"anr_info\": false}}", + EmbraceSerializer() + ) + var cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + assertFalse( + checkNotNull(cfg.anrServiceEnabled) + ) + localConfig = LocalConfig.buildConfig( + "GrCPU", false, + "{\"automatic_data_capture\": { \"anr_info\": true}}", + EmbraceSerializer() + ) + cfg = checkNotNull(localConfig.sdkConfig.automaticDataCaptureConfig) + assertTrue( + checkNotNull(cfg.anrServiceEnabled) + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MemoryWarningTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MemoryWarningTest.kt new file mode 100644 index 0000000000..5321920293 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MemoryWarningTest.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.MemoryWarning +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class MemoryWarningTest { + + private val info = MemoryWarning(16098234098234) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("memory_warning_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("memory_warning_expected.json") + val obj = Gson().fromJson(json, MemoryWarning::class.java) + assertEquals(16098234098234, obj.timestamp) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", MemoryWarning::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MessageUtilsTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MessageUtilsTest.kt new file mode 100644 index 0000000000..7c767aa2a5 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/MessageUtilsTest.kt @@ -0,0 +1,94 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.internal.utils.MessageUtils.boolToStr +import io.embrace.android.embracesdk.internal.utils.MessageUtils.withMap +import io.embrace.android.embracesdk.internal.utils.MessageUtils.withNull +import io.embrace.android.embracesdk.internal.utils.MessageUtils.withSet +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class MessageUtilsTest { + + @Test + fun `true to string`() { + assertEquals("true", boolToStr(true)) + } + + @Test + fun `false to string`() { + assertEquals("false", boolToStr(false)) + } + + @Test + fun `withNull with a null integer`() { + assertEquals("null", withNull(null as Int?)) + } + + @Test + fun `withNull with a null string`() { + assertEquals("null", withNull(null as String?)) + } + + @Test + fun `withNull with a null long`() { + assertEquals("null", withNull(null as Long?)) + } + + @Test + fun `withNull with a long value`() { + assertEquals("\"10\"", withNull(10L)) + } + + @Test + fun `withNull with an int value`() { + assertEquals("\"10\"", withNull(10)) + } + + @Test + fun `withNull with a string value`() { + assertEquals("\"value\"", withNull("value")) + } + + @Test + fun `withSet with a null value`() { + assertEquals("[]", withSet(null)) + } + + @Test + fun `withSet with an empty value`() { + assertEquals("[]", withSet(setOf())) + } + + @Test + fun `withSet with a set of null values`() { + assertEquals("[null,\"null\"]", withSet(setOf(null, "null"))) + } + + @Test + fun `withSet with a set of values`() { + assertEquals("[\"10\",\"20\",\"stringValue\"]", withSet(setOf("10", "20", "stringValue"))) + } + + @Test + fun `withMap with a null value`() { + assertEquals("{}", withMap(null)) + } + + @Test + fun `withMap with an empty value`() { + assertEquals("{}", withMap(mapOf())) + } + + @Test + fun `withMap with a map of null values`() { + assertEquals("{null: null}", withMap(mapOf(null to null))) + } + + @Test + fun `withMap with a map of values`() { + assertEquals( + "{\"10\": \"20\",\"another\": \"value\"}", + withMap(mapOf("10" to "20", "another" to "value")) + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NativeThreadSamplerInstallerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NativeThreadSamplerInstallerTest.kt new file mode 100644 index 0000000000..ff03aecde6 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NativeThreadSamplerInstallerTest.kt @@ -0,0 +1,118 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.anr.ndk.EmbraceNativeThreadSamplerService +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerInstaller +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.embrace.android.embracesdk.session.SessionService +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +internal class NativeThreadSamplerInstallerTest { + + private lateinit var sampler: EmbraceNativeThreadSamplerService + private lateinit var configService: ConfigService + private lateinit var anrService: AnrService + private lateinit var sessionService: SessionService + private lateinit var delegate: EmbraceNativeThreadSamplerService.NdkDelegate + private lateinit var cfg: AnrRemoteConfig + + @Before + fun setUp() { + anrService = mockk(relaxed = true) + sessionService = mockk(relaxed = true) + delegate = mockk(relaxed = true) + sampler = mockk(relaxed = true) + + cfg = AnrRemoteConfig(pctNativeThreadAnrSamplingEnabled = 100f) + configService = FakeConfigService(anrBehavior = fakeAnrBehavior { cfg }) + } + + @Test + fun testInstallDisabled() { + val installer = NativeThreadSamplerInstaller() + installer.monitorCurrentThread(sampler, configService, anrService) + verify(exactly = 0) { delegate.setupNativeThreadSampler(false) } + } + + @Test + fun testInstallEnabledSuccess() { + val installer = NativeThreadSamplerInstaller() + every { sampler.setupNativeSampler() } returns true + every { sampler.monitorCurrentThread() } returns true + + repeat(5) { + installer.monitorCurrentThread(sampler, configService, anrService) + } + verify(exactly = 1) { sampler.monitorCurrentThread() } + verify(exactly = 1) { anrService.addBlockedThreadListener(sampler) } + } + + @Test + fun testInstallEnabledFailure() { + val installer = NativeThreadSamplerInstaller() + every { sampler.setupNativeSampler() } returns false + every { sampler.monitorCurrentThread() } returns false + sampler.setupNativeSampler() + + repeat(5) { + installer.monitorCurrentThread(sampler, configService, anrService) + } + verify(exactly = 5) { sampler.monitorCurrentThread() } + verify(exactly = 0) { anrService.addBlockedThreadListener(sampler) } + + // now do a successful install + every { sampler.setupNativeSampler() } returns true + every { sampler.monitorCurrentThread() } returns true + sampler.setupNativeSampler() + + repeat(5) { + installer.monitorCurrentThread(sampler, configService, anrService) + } + verify(exactly = 6) { sampler.monitorCurrentThread() } + verify(exactly = 1) { anrService.addBlockedThreadListener(sampler) } + } + + @Test + fun testConfigListener() { + val installer = NativeThreadSamplerInstaller() + every { sampler.setupNativeSampler() } returns true + every { sampler.monitorCurrentThread() } returns true + sampler.setupNativeSampler() + + repeat(5) { + installer.monitorCurrentThread(sampler, configService, anrService) + } + verify(exactly = 1) { sampler.monitorCurrentThread() } + verify(exactly = 1) { anrService.addBlockedThreadListener(sampler) } + } + + @Test + fun testInstallNewThread() { + val installer = NativeThreadSamplerInstaller() + every { sampler.setupNativeSampler() } returns true + every { sampler.monitorCurrentThread() } returns true + + installer.monitorCurrentThread(sampler, configService, anrService) + verify(exactly = 1) { sampler.monitorCurrentThread() } + verify(exactly = 1) { anrService.addBlockedThreadListener(sampler) } + assertEquals(installer.currentThread, Thread.currentThread()) + + val executor = Executors.newSingleThreadExecutor() + var executorThread: Thread? = null + executor.submit { + executorThread = Thread.currentThread() + installer.monitorCurrentThread(sampler, configService, anrService) + }.get(1, TimeUnit.SECONDS) + assertEquals(installer.currentThread, executorThread) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NetworkUtilsTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NetworkUtilsTest.kt new file mode 100644 index 0000000000..04b230967f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/NetworkUtilsTest.kt @@ -0,0 +1,150 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.utils.NetworkUtils +import io.embrace.android.embracesdk.utils.NetworkUtils.getDomain +import io.embrace.android.embracesdk.utils.NetworkUtils.isIpAddress +import io.embrace.android.embracesdk.utils.NetworkUtils.stripUrl +import org.junit.Assert +import org.junit.Test + +internal class NetworkUtilsTest { + + private val validIpAddresses = arrayOf( + "1.1.1.1", + "255.255.255.255", + "[::1]", + "[2001::]", + "[2001:4860:4860]:8888]", + "[2001:4860:4860:0:0:0:0]" + ) + + private val invalidIpAddresses = arrayOf( + "google.com", + "google.co.uk", + "foo.google.com", + "bar.foo.google.com", + "baz.bar.foo.google.com", + "baz.bar.foo.google.co.uk" + ) + + private val invalidURLs = arrayOf( + "google", + "http://google", + "http://google.", + "http://.google", + "http://google-.com", + "http://30.168.1.255.1", + "http://192.168.1.256", + "http://-1.2.3.4", + "http://3...3", + "http://127.1" + ) + + private val validURLs = arrayOf( + arrayOf("http://google.com", "google.com"), + arrayOf("http://google.co.uk", "google.co.uk"), + arrayOf("http://google.com/foo", "google.com"), + arrayOf("http://foo.google.com/foo", "google.com"), + arrayOf("http://bar.foo.google.com/foo", "google.com"), + arrayOf("http://baz.bar.foo.google.com/foo", "google.com"), + arrayOf("http://baz.bar.foo.google.co.uk/foo", "google.co.uk"), + arrayOf("http://1.1.1.1/foo", "1.1.1.1"), + arrayOf("http://1.1.1.1:8888/foo", "1.1.1.1"), + arrayOf("http://[::1]", "::1"), + arrayOf("http://[2001::]", "2001::"), + arrayOf("http://[2001:4860:4860]:8888]", "2001:4860:4860"), + arrayOf("http://[2001:4860:4860:0:0:0:0]", "2001:4860:4860:0:0:0:0") + ) + + private val urlsToStrip = arrayOf( + arrayOf("http://google.com", "http://google.com"), + arrayOf("http://foo.google.com/foo", "http://foo.google.com/foo"), + arrayOf("http://1.1.1.1/foo", "http://1.1.1.1/foo"), + arrayOf("http://1.1.1.1:8888/foo?param=1", "http://1.1.1.1:8888/foo"), + + arrayOf("http://foo.google.com/foo?color=blue&limit=100", "http://foo.google.com/foo"), + arrayOf("http://www.example.org/foo.html#bar", "http://www.example.org/foo.html"), + arrayOf( + "http://example.com/index.html#:words:some-context-for-a-(search-term)", + "http://example.com/index.html" + ), + ) + + @Test + fun testValidIPs() { + for (ip in validIpAddresses) { + Assert.assertTrue("$ip should be a valid IP", isIpAddress(ip)) + } + } + + @Test + fun testInvalidIps() { + for (ip in invalidIpAddresses) { + Assert.assertFalse("$ip should not be a valid IP", isIpAddress(ip)) + } + } + + @Test + fun testInvalidUrls() { + for (url in invalidURLs) { + Assert.assertNull( + "$url should not be a valid domain", + getDomain(url) + ) + } + } + + @Test + fun testValidUrls() { + for (pairs in validURLs) { + val url = pairs[0] + val expected = pairs[1] + val domain = getDomain(url) + + Assert.assertTrue("$url should contain a domain", domain != null) + + if (domain != null) { + Assert.assertEquals( + "Domain for " + url + " should be " + expected + " not " + domain, + domain, + expected + ) + } + } + } + + @Test + fun getValidTraceId() { + val validTraceId = "1-58406520-a006649127e371903a2de979" + + val traceIdMoreThanAllowedLength = + "34ec0b8ac9d65e91,34ec0b8ac9d65e9134ec0b8ac9d65e91,34ec0b8ac9d65e91" + + val traceIdMoreEqualAllowedLength = + "34ec0b8ac9d65e91,34ec0b8ac9d65e9134ec0b8ac9d65e91,34ec0b8ac9d65e" + + Assert.assertNull(NetworkUtils.getValidTraceId(null)) + + Assert.assertNull(NetworkUtils.getValidTraceId("\u00B6containUnicode")) + + Assert.assertEquals(validTraceId, NetworkUtils.getValidTraceId(validTraceId)) + + Assert.assertEquals( + traceIdMoreEqualAllowedLength, + NetworkUtils.getValidTraceId(traceIdMoreThanAllowedLength) + ) + } + + @Test + fun stripUrl() { + Assert.assertNull(stripUrl(null)) + + for (pairs in urlsToStrip) { + val url = pairs[0] + val expected = pairs[1] + val strippedUrl = stripUrl(url) + + Assert.assertEquals(expected, strippedUrl) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/OrientationTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/OrientationTest.kt new file mode 100644 index 0000000000..6b7d3c44c1 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/OrientationTest.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk + +import android.content.res.Configuration +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.Orientation +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class OrientationTest { + private val testOrientation = Orientation( + "p", + 12345678L + ) + + @Test + fun testSerialization() { + val data = ResourceReader.readResourceAsText("orientation_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(testOrientation) + assertEquals(data, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("orientation_expected.json") + val obj = Gson().fromJson(json, Orientation::class.java) + assertEquals("p", obj.orientation) + assertEquals(12345678L, obj.timestamp) + assertEquals(Configuration.ORIENTATION_PORTRAIT, obj.internalOrientation) + } + + @Test + fun testInternalOrientation() { + val portraitOrientation = Orientation("p", 1234L) + val landscapeOrientation = Orientation("l", 1234L) + + assertEquals(Configuration.ORIENTATION_PORTRAIT, portraitOrientation.internalOrientation) + assertEquals(Configuration.ORIENTATION_LANDSCAPE, landscapeOrientation.internalOrientation) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoSanitizerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoSanitizerTest.kt new file mode 100644 index 0000000000..6e59c0f092 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoSanitizerTest.kt @@ -0,0 +1,49 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.gating.PerformanceInfoSanitizer +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.mockk.mockk +import org.junit.Assert +import org.junit.Test + +internal class PerformanceInfoSanitizerTest { + + private val performanceInfo = PerformanceInfo( + anrIntervals = mockk(relaxed = true), + networkInterfaceIntervals = mockk(), + memoryWarnings = mockk(), + diskUsage = mockk() + ) + + @Test + fun `test if it keeps all performance info fields`() { + // enabled components contains everything about performance info + val components = setOf( + SessionGatingKeys.PERFORMANCE_ANR, + SessionGatingKeys.PERFORMANCE_CURRENT_DISK_USAGE, + SessionGatingKeys.PERFORMANCE_CPU, + SessionGatingKeys.PERFORMANCE_CONNECTIVITY, + SessionGatingKeys.PERFORMANCE_LOW_MEMORY + ) + + val result = PerformanceInfoSanitizer(performanceInfo, components).sanitize() + + Assert.assertNotNull(result?.anrIntervals) + Assert.assertNotNull(result?.networkInterfaceIntervals) + Assert.assertNotNull(result?.memoryWarnings) + Assert.assertNotNull(result?.diskUsage) + } + + @Test + fun `test if it sanitizes performance info`() { + val components = setOf() + + val result = PerformanceInfoSanitizer(performanceInfo, components).sanitize() + + Assert.assertNull(result?.anrIntervals) + Assert.assertNull(result?.networkInterfaceIntervals) + Assert.assertNull(result?.memoryWarnings) + Assert.assertNull(result?.diskUsage) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoTest.kt new file mode 100644 index 0000000000..25f0587ebc --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PerformanceInfoTest.kt @@ -0,0 +1,76 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.AnrInterval +import io.embrace.android.embracesdk.payload.AppExitInfoData +import io.embrace.android.embracesdk.payload.DiskUsage +import io.embrace.android.embracesdk.payload.Interval +import io.embrace.android.embracesdk.payload.MemoryWarning +import io.embrace.android.embracesdk.payload.NativeThreadAnrInterval +import io.embrace.android.embracesdk.payload.NetworkRequests +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.PowerModeInterval +import io.embrace.android.embracesdk.payload.StrictModeViolation +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class PerformanceInfoTest { + + private val diskUsage: DiskUsage = DiskUsage(10000000, 2000000) + private val networkRequests: NetworkRequests = mockk() + private val memoryWarnings: List = emptyList() + private val networkInterfaceIntervals: List = emptyList() + private val googleAnrTimestamps: List = emptyList() + private val anrIntervals: List = emptyList() + private val appExitInfoData: List = mockk(relaxed = true) + private val nativeThreadAnrIntervals: List = emptyList() + private val powerSaveModeIntervals: List = emptyList() + private val violations: List = emptyList() + + @Test + fun testPerfInfoSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("perf_info_expected.json") + .filter { !it.isWhitespace() } + + val observed = Gson().toJson(buildPerformanceInfo()) + assertEquals(expectedInfo, observed) + } + + @Test + fun testPerfInfoDeserialization() { + val json = ResourceReader.readResourceAsText("perf_info_expected.json") + val obj = Gson().fromJson(json, PerformanceInfo::class.java) + verifyFields(obj) + } + + @Test + fun testPerfInfoEmptyObject() { + val anrInterval = Gson().fromJson("{}", PerformanceInfo::class.java) + assertNotNull(anrInterval) + } + + private fun verifyFields(performanceInfo: PerformanceInfo) { + assertEquals(anrIntervals, performanceInfo.anrIntervals) + assertEquals(googleAnrTimestamps, performanceInfo.googleAnrTimestamps) + assertEquals(memoryWarnings, performanceInfo.memoryWarnings) + assertEquals(nativeThreadAnrIntervals, performanceInfo.nativeThreadAnrIntervals) + assertEquals(networkInterfaceIntervals, performanceInfo.networkInterfaceIntervals) + assertEquals(powerSaveModeIntervals, performanceInfo.powerSaveModeIntervals) + assertEquals(violations, performanceInfo.strictmodeViolations) + } + + private fun buildPerformanceInfo(): PerformanceInfo = PerformanceInfo( + anrIntervals = anrIntervals, + appExitInfoData = appExitInfoData, + diskUsage = diskUsage, + googleAnrTimestamps = googleAnrTimestamps, + memoryWarnings = memoryWarnings, + nativeThreadAnrIntervals = nativeThreadAnrIntervals, + networkInterfaceIntervals = networkInterfaceIntervals, + powerSaveModeIntervals = powerSaveModeIntervals, + networkRequests = networkRequests, + strictmodeViolations = violations + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PropertiesTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PropertiesTest.kt new file mode 100644 index 0000000000..3a2a51e169 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PropertiesTest.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.utils.PropertyUtils +import org.junit.Assert +import org.junit.Test + +internal class PropertiesTest { + + @Test + fun testPropertiesNormalization() { + val sourceMap: MutableMap = HashMap() + sourceMap[null] = "Null key" + sourceMap[""] = "Empty key" + sourceMap["EmptyValue"] = "" + sourceMap["NullValue"] = "" + for (i in 1..9) { + sourceMap["Key$i"] = "Value$i" + } + val resultMap = PropertyUtils.sanitizeProperties(sourceMap) + Assert.assertTrue( + "Unexpected normalized map size.", + resultMap.size <= PropertyUtils.MAX_PROPERTY_SIZE + ) + resultMap.entries.stream() + .peek { (key): Map.Entry -> + Assert.assertNotNull( + "Unexpected normalized map key: NULL.", + key + ) + } + .peek { (_, value): Map.Entry -> + Assert.assertNotNull( + "Unexpected normalized map value: NULL.", + value + ) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PushNotificationBreadcrumbTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PushNotificationBreadcrumbTest.kt new file mode 100644 index 0000000000..7e35f9881b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/PushNotificationBreadcrumbTest.kt @@ -0,0 +1,47 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class PushNotificationBreadcrumbTest { + + private val info = PushNotificationBreadcrumb( + "title", + "body", + "from", + "id", + 1, + "type", + 1600000000 + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("push_notification_breadcrumb_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("push_notification_breadcrumb_expected.json") + val obj = Gson().fromJson(json, PushNotificationBreadcrumb::class.java) + assertEquals("title", obj.title) + assertEquals("body", obj.body) + assertEquals("from", obj.from) + assertEquals("id", obj.id) + assertEquals(1, obj.priority) + assertEquals("type", obj.type) + assertEquals(1600000000, obj.getStartTime()) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", PushNotificationBreadcrumb::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImplTest.kt new file mode 100644 index 0000000000..5ffc69d48d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImplTest.kt @@ -0,0 +1,185 @@ +package io.embrace.android.embracesdk + +import android.content.Context +import io.embrace.android.embracesdk.Embrace.AppFramework.FLUTTER +import io.embrace.android.embracesdk.Embrace.AppFramework.REACT_NATIVE +import io.embrace.android.embracesdk.capture.crash.CrashService +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.JsException +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class ReactNativeInternalInterfaceImplTest { + + private lateinit var impl: ReactNativeInternalInterfaceImpl + private lateinit var embrace: EmbraceImpl + private lateinit var preferencesService: PreferencesService + private lateinit var crashService: CrashService + private lateinit var metadataService: FakeAndroidMetadataService + private lateinit var logger: InternalEmbraceLogger + private lateinit var context: Context + + @Before + fun setUp() { + embrace = mockk(relaxed = true) + preferencesService = FakePreferenceService() + crashService = mockk(relaxed = true) + metadataService = FakeAndroidMetadataService() + logger = mockk(relaxed = true) + context = mockk(relaxed = true) + impl = ReactNativeInternalInterfaceImpl( + embrace, + mockk(), + REACT_NATIVE, + preferencesService, + crashService, + metadataService, + logger + ) + } + + @Test + fun testSetJavaScriptPatchNumber() { + every { embrace.isStarted } returns true + impl.setJavaScriptPatchNumber("28.9.1") + assertEquals("28.9.1", preferencesService.javaScriptPatchNumber) + } + + @Test + fun testSetJavaScriptPatchNumberNotStarted() { + every { embrace.isStarted } returns false + impl.setJavaScriptPatchNumber("28.9.1") + verify(exactly = 1) { + logger.logSDKNotInitialized(any()) + } + } + + @Test + fun testSetJavaScriptPatchNumberNull() { + every { embrace.isStarted } returns true + preferencesService.javaScriptPatchNumber = "123" + impl.setJavaScriptPatchNumber(null) + assertEquals("123", preferencesService.javaScriptPatchNumber) + verify(exactly = 1) { + logger.logError(any()) + } + } + + @Test + fun testSetJavaScriptPatchNumberEmpty() { + every { embrace.isStarted } returns true + preferencesService.javaScriptPatchNumber = "123" + impl.setJavaScriptPatchNumber("") + assertEquals("123", preferencesService.javaScriptPatchNumber) + verify(exactly = 1) { + logger.logError(any()) + } + } + + @Test + fun testSetReactNativeVersionNumber() { + every { embrace.isStarted } returns true + impl.setReactNativeVersionNumber("0.69.1") + assertEquals("0.69.1", preferencesService.reactNativeVersionNumber) + } + + @Test + fun testSetReactNativeVersionNumberNotStarted() { + every { embrace.isStarted } returns false + impl.setReactNativeVersionNumber("0.69.1") + verify(exactly = 1) { + logger.logSDKNotInitialized(any()) + } + } + + @Test + fun testSetReactNativeVersionNumberNull() { + every { embrace.isStarted } returns true + preferencesService.reactNativeVersionNumber = "0.1" + impl.setReactNativeVersionNumber(null) + assertEquals("0.1", preferencesService.reactNativeVersionNumber) + verify(exactly = 1) { + logger.logError(any()) + } + } + + @Test + fun testSetReactNativeVersionNumberEmpty() { + every { embrace.isStarted } returns true + preferencesService.reactNativeVersionNumber = "0.1" + impl.setReactNativeVersionNumber("") + assertEquals("0.1", preferencesService.reactNativeVersionNumber) + verify(exactly = 1) { + logger.logError(any()) + } + } + + @Test + fun testSetJavaScriptBundleURL() { + every { embrace.isStarted } returns true + impl.setJavaScriptBundleUrl(context, "index.android.bundle") + assertEquals("index.android.bundle", metadataService.fakeReactNativeBundleId) + } + + @Test + fun testSetJavaScriptBundleURLNotStarted() { + every { embrace.isStarted } returns false + impl.setJavaScriptBundleUrl(context, "index.android.bundle") + verify(exactly = 1) { + logger.logSDKNotInitialized(any()) + } + } + + @Test + fun testSetJavaScriptBundleURLWrongFramework() { + impl = ReactNativeInternalInterfaceImpl( + embrace, + mockk(), + FLUTTER, + preferencesService, + crashService, + metadataService, + logger + ) + + every { embrace.isStarted } returns true + impl.setJavaScriptBundleUrl(context, "index.android.bundle") + verify(exactly = 1) { + logger.logError(any()) + } + } + + @Test + fun testLogUnhandledJsException() { + every { embrace.isStarted } returns true + impl.logUnhandledJsException("name", "message", "type", "stack") + + val captor = slot() + verify(exactly = 1) { + crashService.logUnhandledJsException(capture(captor)) + } + with(captor.captured) { + assertEquals("name", name) + assertEquals("message", message) + assertEquals("type", type) + assertEquals("stack", stacktrace) + } + } + + @Test + fun testLogUnhandledJsExceptionNotStarted() { + every { embrace.isStarted } returns false + impl.logUnhandledJsException("name", "message", "type", "stack") + verify(exactly = 1) { + logger.logSDKNotInitialized(any()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ResourceReader.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ResourceReader.kt new file mode 100644 index 0000000000..645cd830a1 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ResourceReader.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk + +import java.io.InputStream + +internal object ResourceReader { + fun readResource(name: String): InputStream { + val classLoader = checkNotNull(javaClass.classLoader) + return classLoader.getResourceAsStream(name) + ?: error("Could not find resource '$name'") + } + + fun readResourceAsText(name: String): String { + return readResource(name).bufferedReader().readText() + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionPerformanceInfoSanitizerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionPerformanceInfoSanitizerTest.kt new file mode 100644 index 0000000000..01275a3d06 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionPerformanceInfoSanitizerTest.kt @@ -0,0 +1,55 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.gating.PerformanceInfoSanitizer +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.mockk.mockk +import org.junit.Assert +import org.junit.Test + +internal class SessionPerformanceInfoSanitizerTest { + + private val sessionPerformanceInfo = PerformanceInfo( + anrIntervals = mockk(relaxed = true), + networkInterfaceIntervals = mockk(), + memoryWarnings = mockk(), + diskUsage = mockk(), + networkRequests = mockk() + ) + + @Test + fun `test if it keeps all performance info fields`() { + // enabled components contains everything about session performance info + val components = setOf( + SessionGatingKeys.PERFORMANCE_NETWORK, + + SessionGatingKeys.PERFORMANCE_ANR, + SessionGatingKeys.PERFORMANCE_CURRENT_DISK_USAGE, + SessionGatingKeys.PERFORMANCE_CPU, + SessionGatingKeys.PERFORMANCE_CONNECTIVITY, + SessionGatingKeys.PERFORMANCE_LOW_MEMORY + ) + + val result = PerformanceInfoSanitizer(sessionPerformanceInfo, components).sanitize() + + Assert.assertNotNull(result?.networkRequests) + + Assert.assertNotNull(result?.anrIntervals) + Assert.assertNotNull(result?.networkInterfaceIntervals) + Assert.assertNotNull(result?.diskUsage) + } + + @Test + fun `test if it sanitizes performance info`() { + val components = setOf() + + val result = PerformanceInfoSanitizer(sessionPerformanceInfo, components).sanitize() + + Assert.assertNull(result?.networkRequests) + + Assert.assertNull(result?.anrIntervals) + Assert.assertNull(result?.networkInterfaceIntervals) + Assert.assertNull(result?.memoryWarnings) + Assert.assertNull(result?.diskUsage) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionRemoteConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionRemoteConfigTest.kt new file mode 100644 index 0000000000..2389b90021 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionRemoteConfigTest.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.config.remote.SessionRemoteConfig +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class SessionRemoteConfigTest { + + @Test + fun testDefaults() { + val cfg = SessionRemoteConfig(false, false, null, null) + assertFalse(checkNotNull(cfg.isEnabled)) + assertFalse(checkNotNull(cfg.endAsync)) + assertNull(cfg.sessionComponents) + assertNull(cfg.fullSessionEvents) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerFacadeTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerFacadeTest.kt new file mode 100644 index 0000000000..4c86297ccb --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerFacadeTest.kt @@ -0,0 +1,159 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.gating.SessionSanitizerFacade +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.Orientation +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.payload.UserInfo +import io.mockk.mockk +import org.junit.Assert +import org.junit.Test + +internal class SessionSanitizerFacadeTest { + + private val breadcrumbs = Breadcrumbs( + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + ) + + private val sessionPerformanceInfo = PerformanceInfo( + anrIntervals = mockk(relaxed = true), + networkInterfaceIntervals = mockk(), + memoryWarnings = mockk(), + diskUsage = mockk(), + networkRequests = mockk() + ) + + private val userInfo = UserInfo( + personas = setOf("personas"), + email = "example@embrace.com" + ) + + private val session = fakeSession().copy( + properties = mapOf("example" to "example"), + orientations = listOf(Orientation(0, 0L)), + terminationTime = 100L, + isReceivedTermination = false, + infoLogIds = listOf("infoLog"), + infoLogsAttemptedToSend = 1, + warningLogIds = listOf("warningLog"), + warnLogsAttemptedToSend = 1, + eventIds = listOf("eventId"), + startupDuration = 100L, + startupThreshold = 500L + ) + + private val sessionMessage = SessionMessage( + session = session, + userInfo = userInfo, + appInfo = AppInfo(), + deviceInfo = DeviceInfo(), + performanceInfo = sessionPerformanceInfo, + breadcrumbs = breadcrumbs + ) + + private val enabledComponents = setOf( + SessionGatingKeys.BREADCRUMBS_TAPS, + SessionGatingKeys.BREADCRUMBS_VIEWS, + SessionGatingKeys.BREADCRUMBS_CUSTOM_VIEWS, + SessionGatingKeys.BREADCRUMBS_WEB_VIEWS, + SessionGatingKeys.BREADCRUMBS_CUSTOM, + SessionGatingKeys.USER_PERSONAS, + SessionGatingKeys.SESSION_PROPERTIES, + SessionGatingKeys.SESSION_ORIENTATIONS, + SessionGatingKeys.SESSION_USER_TERMINATION, + SessionGatingKeys.SESSION_MOMENTS, + SessionGatingKeys.LOGS_INFO, + SessionGatingKeys.LOGS_WARN, + SessionGatingKeys.STARTUP_MOMENT, + SessionGatingKeys.PERFORMANCE_NETWORK, + SessionGatingKeys.PERFORMANCE_ANR, + SessionGatingKeys.PERFORMANCE_CURRENT_DISK_USAGE, + SessionGatingKeys.PERFORMANCE_CPU, + SessionGatingKeys.PERFORMANCE_CONNECTIVITY, + SessionGatingKeys.PERFORMANCE_LOW_MEMORY + ) + + @Test + fun `test if it keeps all event message components`() { + val sanitizedMessage = + SessionSanitizerFacade(sessionMessage, enabledComponents).getSanitizedMessage() + + val crumbs = checkNotNull(sanitizedMessage.breadcrumbs) + Assert.assertNotNull(crumbs.customBreadcrumbs) + Assert.assertNotNull(crumbs.viewBreadcrumbs) + Assert.assertNotNull(crumbs.fragmentBreadcrumbs) + Assert.assertNotNull(crumbs.tapBreadcrumbs) + Assert.assertNotNull(crumbs.webViewBreadcrumbs) + + Assert.assertNotNull(sanitizedMessage.userInfo?.personas) + + Assert.assertNotNull(sanitizedMessage.session.properties) + Assert.assertNotNull(sanitizedMessage.session.orientations) + Assert.assertNotNull(sanitizedMessage.session.terminationTime) + Assert.assertNotNull(sanitizedMessage.session.isReceivedTermination) + Assert.assertNotNull(sanitizedMessage.session.infoLogIds) + Assert.assertNotNull(sanitizedMessage.session.infoLogsAttemptedToSend) + Assert.assertNotNull(sanitizedMessage.session.warningLogIds) + Assert.assertNotNull(sanitizedMessage.session.warnLogsAttemptedToSend) + Assert.assertNotNull(sanitizedMessage.session.eventIds) + Assert.assertNotNull(sanitizedMessage.session.startupDuration) + Assert.assertNotNull(sanitizedMessage.session.startupThreshold) + + Assert.assertNotNull(sanitizedMessage.performanceInfo?.networkRequests) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.anrIntervals) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.networkInterfaceIntervals) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.memoryWarnings) + Assert.assertNotNull(sanitizedMessage.performanceInfo?.diskUsage) + + Assert.assertNotNull(sanitizedMessage.appInfo) + Assert.assertNotNull(sanitizedMessage.deviceInfo) + } + + @Test + fun `test if it sanitizes event message components`() { + // uses an empty set for enabled components + val sanitizedMessage = + SessionSanitizerFacade(sessionMessage, setOf()).getSanitizedMessage() + + val crumbs = checkNotNull(sanitizedMessage.breadcrumbs) + Assert.assertNull(crumbs.customBreadcrumbs) + Assert.assertNull(crumbs.viewBreadcrumbs) + Assert.assertNull(crumbs.fragmentBreadcrumbs) + Assert.assertNull(crumbs.tapBreadcrumbs) + Assert.assertNull(crumbs.webViewBreadcrumbs) + + Assert.assertNull(sanitizedMessage.userInfo?.personas) + + Assert.assertNull(sanitizedMessage.session.properties) + Assert.assertNull(sanitizedMessage.session.orientations) + Assert.assertNull(sanitizedMessage.session.terminationTime) + Assert.assertNull(sanitizedMessage.session.isReceivedTermination) + Assert.assertNull(sanitizedMessage.session.infoLogIds) + Assert.assertNull(sanitizedMessage.session.infoLogsAttemptedToSend) + Assert.assertNull(sanitizedMessage.session.warningLogIds) + Assert.assertNull(sanitizedMessage.session.warnLogsAttemptedToSend) + Assert.assertNull(sanitizedMessage.session.eventIds) + Assert.assertNull(sanitizedMessage.session.startupDuration) + Assert.assertNull(sanitizedMessage.session.startupThreshold) + + Assert.assertNull(sanitizedMessage.performanceInfo?.networkRequests) + Assert.assertNull(sanitizedMessage.performanceInfo?.anrIntervals) + Assert.assertNull(sanitizedMessage.performanceInfo?.networkInterfaceIntervals) + Assert.assertNull(sanitizedMessage.performanceInfo?.memoryWarnings) + Assert.assertNull(sanitizedMessage.performanceInfo?.diskUsage) + + Assert.assertNotNull(sanitizedMessage.appInfo) + Assert.assertNotNull(sanitizedMessage.deviceInfo) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerTest.kt new file mode 100644 index 0000000000..f93942c48f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionSanitizerTest.kt @@ -0,0 +1,74 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.gating.SessionSanitizer +import io.embrace.android.embracesdk.payload.Orientation +import org.junit.Assert +import org.junit.Test + +internal class SessionSanitizerTest { + + private val session = fakeSession().copy( + properties = mapOf("example" to "example"), + orientations = listOf(Orientation(0, 0L)), + terminationTime = 100L, + isReceivedTermination = false, + infoLogIds = listOf("infoLog"), + infoLogsAttemptedToSend = 1, + warningLogIds = listOf("warningLog"), + warnLogsAttemptedToSend = 1, + eventIds = listOf("eventId"), + startupDuration = 100L, + startupThreshold = 500L + ) + + @Test + fun `test if it keeps all session info`() { + // enabled components contains everything about session + val components = setOf( + SessionGatingKeys.SESSION_PROPERTIES, + SessionGatingKeys.SESSION_ORIENTATIONS, + SessionGatingKeys.SESSION_USER_TERMINATION, + SessionGatingKeys.SESSION_MOMENTS, + SessionGatingKeys.LOGS_INFO, + SessionGatingKeys.LOGS_WARN, + SessionGatingKeys.STARTUP_MOMENT + ) + + val result = SessionSanitizer(session, components).sanitize() + + Assert.assertNotNull(result.properties) + Assert.assertNotNull(result.orientations) + Assert.assertNotNull(result.terminationTime) + Assert.assertNotNull(result.isReceivedTermination) + Assert.assertNotNull(result.infoLogIds) + Assert.assertNotNull(result.infoLogsAttemptedToSend) + Assert.assertNotNull(result.warningLogIds) + Assert.assertNotNull(result.warnLogsAttemptedToSend) + Assert.assertNotNull(result.eventIds) + Assert.assertNotNull(result.startupDuration) + Assert.assertNotNull(result.startupThreshold) + Assert.assertNull(result.betaFeatures) + } + + @Test + fun `test if it sanitizes session info`() { + val components = setOf() + + val result = SessionSanitizer(session, components).sanitize() + + Assert.assertNull(result.properties) + Assert.assertNull(result.orientations) + Assert.assertNull(result.terminationTime) + Assert.assertNull(result.isReceivedTermination) + Assert.assertNull(result.infoLogIds) + Assert.assertNull(result.infoLogsAttemptedToSend) + Assert.assertNull(result.warningLogIds) + Assert.assertNull(result.warnLogsAttemptedToSend) + Assert.assertNull(result.eventIds) + Assert.assertNull(result.startupDuration) + Assert.assertNull(result.startupThreshold) + Assert.assertNull(result.betaFeatures) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionStacktraceSampleJsonTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionStacktraceSampleJsonTest.kt new file mode 100644 index 0000000000..5e71146106 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionStacktraceSampleJsonTest.kt @@ -0,0 +1,104 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.payload.NativeThreadAnrInterval +import io.embrace.android.embracesdk.payload.NativeThreadAnrSample +import io.embrace.android.embracesdk.payload.NativeThreadAnrStackframe +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.ThreadState +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Verifies the stacktrace sample is included in the session JSON when set + */ +internal class SessionStacktraceSampleJsonTest { + + @Test + fun testSymbolSerialization() { + val session = fakeSession().copy(symbols = mapOf("foo" to "bar")) + val root = Gson().toJsonTree(session).asJsonObject + + // assert symbols included + val symbols = root.getAsJsonObject("sb") + assertEquals("bar", symbols["foo"].asString) + } + + @Test + fun testSerialization() { + val fixture = generateNativeSampleTick() + val session = PerformanceInfo( + nativeThreadAnrIntervals = listOf(fixture) + ) + val root = Gson().toJsonTree(session).asJsonObject + + // assert ticks included + val ticks = root.getAsJsonArray("nst") + val tick = ticks.single() as JsonObject + + // assert tick info serialized + verifyTickInfoJson(tick) + + // assert stacktrace sample serialized + verifyStacktraceSampleJson(tick.getAsJsonArray("ss")) + } + + private fun generateNativeSampleTick(): NativeThreadAnrInterval { + val obj = NativeThreadAnrSample( + 2, + 15002000, + 2, + listOf( + NativeThreadAnrStackframe( + "0x5092afb9", + "0x00274fc1", + "/data/foo/libtest.so", + 5 + ) + ) + ) + return NativeThreadAnrInterval( + 25, + "UnityMain", + 5, + 100, + 15000000, + mutableListOf(obj), + ThreadState.RUNNABLE, + AnrRemoteConfig.Unwinder.LIBUNWIND + ) + } + + private fun verifyTickInfoJson(node: JsonObject) { + assertEquals(8, node.size()) + assertEquals(100L, node.get("os").asLong) + assertEquals(15000000L, node.get("t").asLong) + assertEquals(25, node.get("id").asInt) + assertEquals("UnityMain", node.get("n").asString) + assertEquals(ThreadState.RUNNABLE.code, node.get("s").asInt) + assertEquals(AnrRemoteConfig.Unwinder.LIBUNWIND.code, node.get("uw").asInt) + assertEquals(5, node.get("p").asInt) + } + + private fun verifyStacktraceSampleJson(array: JsonArray) { + val node = array.single() as JsonObject + assertEquals(4, node.size()) + assertEquals(2, node.get("r").asInt) + assertEquals(15002000L, node.get("t").asLong) + assertEquals(2, node.get("d").asInt) + + val frames = node.get("s").asJsonArray + assertEquals(1, frames.size()) + + // assert stackframe serialized + val frame = frames.get(0).asJsonObject + assertEquals("0x5092afb9", frame.get("pc").asString) + assertEquals("0x00274fc1", frame.get("l").asString) + assertEquals("/data/foo/libtest.so", frame.get("p").asString) + assertEquals(5, frame.get("r").asInt) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionTest.kt new file mode 100644 index 0000000000..21b80367aa --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/SessionTest.kt @@ -0,0 +1,115 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.BetaFeatures +import io.embrace.android.embracesdk.payload.ExceptionError +import io.embrace.android.embracesdk.payload.Orientation +import io.embrace.android.embracesdk.payload.Session +import io.embrace.android.embracesdk.payload.Session.SessionLifeEventType +import io.embrace.android.embracesdk.payload.UserInfo +import io.embrace.android.embracesdk.payload.WebViewInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class SessionTest { + + private val info = Session( + sessionId = "fake-session-id", + startTime = 123456789L, + endTime = 987654321L, + number = 5, + appState = "foreground", + lastHeartbeatTime = 123456780L, + isEndedCleanly = true, + isReceivedTermination = true, + isColdStart = true, + messageType = "fake-message-type", + terminationTime = 16090292309L, + eventIds = listOf("fake-event-id"), + infoLogIds = listOf("fake-info-id"), + warningLogIds = listOf("fake-warn-id"), + errorLogIds = listOf("fake-err-id"), + networkLogIds = listOf("fake-network-id"), + infoLogsAttemptedToSend = 1, + warnLogsAttemptedToSend = 2, + errorLogsAttemptedToSend = 3, + crashReportId = "fake-crash-id", + endType = SessionLifeEventType.STATE, + startType = SessionLifeEventType.STATE, + startupDuration = 1223, + startupThreshold = 5000, + sdkStartupDuration = 109, + unhandledExceptions = 1, + user = UserInfo("fake-user-id", "fake-user-name"), + exceptionError = ExceptionError(false), + orientations = listOf(Orientation(1, 16092342200)), + properties = mapOf("fake-key" to "fake-value"), + symbols = mapOf("fake-native-key" to "fake-native-value"), + betaFeatures = BetaFeatures(), + webViewInfo = listOf( + WebViewInfo( + "fake-webview-id", + url = "fake-url", + startTime = 16090292309L + ) + ) + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("session_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("session_expected.json") + val obj = Gson().fromJson(json, Session::class.java) + assertNotNull(obj) + + with(obj) { + assertEquals("fake-session-id", sessionId) + assertEquals(123456789L, startTime) + assertEquals(987654321L, endTime) + assertEquals(5, number) + assertEquals("foreground", appState) + assertEquals("fake-message-type", messageType) + assertEquals(16090292309L, terminationTime) + assertEquals(123456780L, lastHeartbeatTime) + assertTrue(checkNotNull(isEndedCleanly)) + assertTrue(checkNotNull(isReceivedTermination)) + assertTrue(isColdStart) + assertEquals(listOf("fake-event-id"), eventIds) + assertEquals(listOf("fake-info-id"), infoLogIds) + assertEquals(listOf("fake-warn-id"), warningLogIds) + assertEquals(listOf("fake-err-id"), errorLogIds) + assertEquals(listOf("fake-network-id"), networkLogIds) + assertEquals(1, infoLogsAttemptedToSend) + assertEquals(2, warnLogsAttemptedToSend) + assertEquals(3, errorLogsAttemptedToSend) + assertEquals("fake-crash-id", crashReportId) + assertEquals(SessionLifeEventType.STATE, endType) + assertEquals(SessionLifeEventType.STATE, startType) + assertEquals(1223L, startupDuration) + assertEquals(5000L, startupThreshold) + assertEquals(109L, sdkStartupDuration) + assertEquals(1, unhandledExceptions) + assertEquals(ExceptionError(false), exceptionError) + assertEquals(listOf(Orientation(1, 16092342200)), orientations) + assertEquals(mapOf("fake-key" to "fake-value"), properties) + assertEquals(mapOf("fake-native-key" to "fake-native-value"), symbols) + assertEquals(BetaFeatures(), betaFeatures) + assertEquals(1, webViewInfo?.size) + } + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", Session::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/TestCacheService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/TestCacheService.kt new file mode 100644 index 0000000000..eff027f75a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/TestCacheService.kt @@ -0,0 +1,69 @@ +package io.embrace.android.embracesdk + +import com.google.gson.GsonBuilder +import io.embrace.android.embracesdk.comms.api.EmbraceUrl +import io.embrace.android.embracesdk.comms.api.EmbraceUrlAdapter +import io.embrace.android.embracesdk.comms.delivery.CacheService +import java.util.concurrent.ConcurrentHashMap +import java.util.regex.Pattern + +internal class TestCacheService : CacheService { + + private val gson = GsonBuilder() + .registerTypeAdapter(EmbraceUrl::class.java, EmbraceUrlAdapter()) + .create() + + private val cache: MutableMap = ConcurrentHashMap() + + override fun cacheObject(name: String, objectToCache: T, clazz: Class) { + cache[name] = gson.toJson(objectToCache, clazz.genericSuperclass).toByteArray() + } + + override fun loadObject(name: String, clazz: Class): T? { + if (!cache.containsKey(name)) { + return null + } + return gson.fromJson(String(cache[name]!!), clazz) + } + + override fun cacheBytes(name: String, bytes: ByteArray?) { + bytes?.let { cache[name] = it } + } + + override fun loadBytes(name: String): ByteArray? { + return cache[name] + } + + override fun deleteFile(name: String): Boolean { + return (cache.remove(name) != null) + } + + override fun deleteObject(name: String): Boolean { + return cache.remove(name) != null + } + + override fun deleteObjectsByRegex(regex: String): Boolean { + val pattern = Pattern.compile(regex) + var result = false + for (key in cache.keys) { + if (pattern.matcher(key).find()) { + cache.remove(key) + result = true + } + } + return result + } + + override fun moveObject(src: String, dst: String): Boolean { + if (cache[src] == null) { + return false + } + cache[dst] = cache[src]!! + cache.remove(src) + return true + } + + override fun listFilenamesByPrefix(prefix: String): MutableList { + return (cache.keys.filter { it.startsWith(prefix) }).toMutableList() + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ThreadInfoTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ThreadInfoTest.kt new file mode 100644 index 0000000000..c57899683b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ThreadInfoTest.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.ThreadInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class ThreadInfoTest { + + @Test + fun testThreadInfoMaxLines() { + val threadInfo = ThreadInfo.ofThread( + Thread.currentThread(), + arrayOf( + StackTraceElement("Foo", "bar", "Foo.kt", 5), + StackTraceElement("Foo", "wham", "Foo.kt", 27) + ), + 1 + ) + assertEquals(1, threadInfo.lines!!.size) + } + + @Test + fun testThreadInfoSerialization() { + val threadInfo = ThreadInfo( + 13, Thread.State.RUNNABLE, "my-thread", 5, + listOf( + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ) + ) + + val expectedInfo = ResourceReader.readResourceAsText("thread_info_expected.json") + .filter { !it.isWhitespace() } + + val observed = Gson().toJson(threadInfo) + assertEquals(expectedInfo, observed) + } + + @Test + fun testThreadInfoDeserialization() { + val json = ResourceReader.readResourceAsText("thread_info_expected.json") + val obj = Gson().fromJson(json, ThreadInfo::class.java) + assertEquals(13, obj.threadId) + assertEquals(5, obj.priority) + assertEquals(Thread.State.RUNNABLE, obj.state) + assertEquals("my-thread", obj.name) + assertEquals( + listOf( + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ), + obj.lines + ) + } + + @Test + fun testThreadInfoEmptyObject() { + val threadInfo = Gson().fromJson("{}", ThreadInfo::class.java) + assertNotNull(threadInfo) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UnityInternalInterfaceImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UnityInternalInterfaceImplTest.kt new file mode 100644 index 0000000000..73fcda40f3 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UnityInternalInterfaceImplTest.kt @@ -0,0 +1,110 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +internal class UnityInternalInterfaceImplTest { + + private lateinit var impl: UnityInternalInterfaceImpl + private lateinit var embrace: EmbraceImpl + private lateinit var preferencesService: PreferencesService + private lateinit var logger: InternalEmbraceLogger + + @Before + fun setUp() { + embrace = mockk(relaxed = true) + preferencesService = FakePreferenceService() + logger = mockk(relaxed = true) + impl = UnityInternalInterfaceImpl(embrace, mockk(), preferencesService, logger) + } + + @Test + fun testSetUnityMetaData() { + every { embrace.isStarted } returns true + impl.setUnityMetaData("unityVersion", "buildGuid", "unitySdkVersion") + assertEquals("unityVersion", preferencesService.unityVersionNumber) + assertEquals("buildGuid", preferencesService.unityBuildIdNumber) + assertEquals("unitySdkVersion", preferencesService.unitySdkVersionNumber) + } + + @Test + fun testSetUnityMetaDataAlreadyPresent() { + every { embrace.isStarted } returns true + preferencesService.unityVersionNumber = "myUnityVersion" + preferencesService.unityBuildIdNumber = "myBuildId" + preferencesService.unitySdkVersionNumber = "mySdkVersion" + impl.setUnityMetaData("unityVersion", "buildGuid", "unitySdkVersion") + assertEquals("unityVersion", preferencesService.unityVersionNumber) + assertEquals("buildGuid", preferencesService.unityBuildIdNumber) + assertEquals("unitySdkVersion", preferencesService.unitySdkVersionNumber) + } + + @Test + fun testSetUnityMetaDataNotStarted() { + every { embrace.isStarted } returns false + impl.setUnityMetaData("unityVersion", "buildGuid", "unitySdkVersion") + verify(exactly = 1) { + logger.logSDKNotInitialized(any()) + } + } + + @Test + fun testSetUnityMetaDataNull() { + every { embrace.isStarted } returns true + impl.setUnityMetaData(null, null, "unitySdkVersion") + assertNull(preferencesService.unityVersionNumber) + assertNull(preferencesService.unityBuildIdNumber) + assertNull(preferencesService.unitySdkVersionNumber) + verify(exactly = 1) { + logger.logError(any()) + } + } + + @Test + fun testLogUnhandledUnityException() { + every { embrace.isStarted } returns true + impl.logUnhandledUnityException("name", "msg", "stack") + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "Unity exception", + null, + null, + "stack", + LogExceptionType.UNHANDLED, + null, + null, + "name", + "msg" + ) + } + } + + @Test + fun testLogHandledUnityException() { + every { embrace.isStarted } returns true + impl.logHandledUnityException("name", "msg", "stack") + verify(exactly = 1) { + embrace.logMessage( + EmbraceEvent.Type.ERROR_LOG, + "Unity exception", + null, + null, + "stack", + LogExceptionType.HANDLED, + null, + null, + "name", + "msg" + ) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoSanitizerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoSanitizerTest.kt new file mode 100644 index 0000000000..c2f43dc388 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoSanitizerTest.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.gating.SessionGatingKeys.USER_PERSONAS +import io.embrace.android.embracesdk.gating.UserInfoSanitizer +import io.embrace.android.embracesdk.payload.UserInfo +import org.junit.Assert +import org.junit.Test + +internal class UserInfoSanitizerTest { + + private val userInfo = UserInfo( + personas = setOf("personas"), + email = "example@embrace.com" + ) + + @Test + fun `test if it keeps session properties`() { + val components = setOf(USER_PERSONAS) + + val result = UserInfoSanitizer(userInfo, components).sanitize() + + Assert.assertNotNull(result.personas) + Assert.assertNotNull(result.email) + } + + @Test + fun `test if it sanitizes session properties`() { + // enabled components doesn't contain USER_PERSONAS + val components = setOf() + + val result = UserInfoSanitizer(userInfo, components).sanitize() + + Assert.assertNotNull(result.email) + Assert.assertNull(result.personas) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoTest.kt new file mode 100644 index 0000000000..a9a31be868 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UserInfoTest.kt @@ -0,0 +1,120 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.payload.UserInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +internal class UserInfoTest { + + private val info = UserInfo( + userId = "123", + email = "fake@example.com", + username = "joebloggs", + personas = setOf("first_day"), + ) + + @Test + fun testSerialization() { + val data = ResourceReader.readResourceAsText("user_info_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(data, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("user_info_expected.json") + val obj = Gson().fromJson(json, UserInfo::class.java) + assertEquals("123", obj.userId) + assertEquals("fake@example.com", obj.email) + assertEquals("joebloggs", obj.username) + assertEquals(setOf("first_day"), obj.personas) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", UserInfo::class.java) + assertNotNull(info) + } + + /** + * Construct a default builder + */ + @Test + fun testBuilderDefault() { + val info = UserInfo() + assertNull(info.userId) + assertNull(info.email) + assertNull(info.username) + assertNull(info.personas) + } + + /** + * Construct a builder and set all its possible values + */ + @Test + fun testBuilderWithValues() { + val info = UserInfo( + userId = "123", + email = "fake@example.com", + username = "Mr F. Ake", + personas = setOf("first_day") + ) + assertEquals("123", info.userId) + assertEquals("fake@example.com", info.email) + assertEquals("Mr F. Ake", info.username) + assertEquals(setOf("first_day"), info.personas) + } + + /** + * Construct UserInfo object with nulls then set values + */ + @Test + fun testNullsInCtor() { + val info = UserInfo(null, null, null, null) + info.userId = "5" + info.username = "root" + info.email = "faker22@example.com" + info.personas = setOf("payer") + assertEquals("5", info.userId) + assertEquals("faker22@example.com", info.email) + assertEquals("root", info.username) + assertEquals(setOf("payer"), info.personas) + } + + /** + * Construct UserInfo from an empty PreferenceService + */ + @Test + fun testOfStoredDefault() { + val service = FakePreferenceService() + val info = UserInfo.ofStored(service) + assertNull(info.userId) + assertNull(info.email) + assertNull(info.username) + assertEquals(emptySet(), info.personas) + } + + /** + * Construct UserInfo from an empty PreferenceService + */ + @Test + fun testOfStoredWithValues() { + val service = FakePreferenceService() + service.userIdentifier = "123" + service.userEmailAddress = "testing@example.com" + service.username = "fogglesmash54" + service.userPersonas = setOf("payer") + service.userPayer = true + val info = UserInfo.ofStored(service) + assertEquals("123", info.userId) + assertEquals("testing@example.com", info.email) + assertEquals("fogglesmash54", info.username) + val expected: Set = HashSet(listOf("payer")) + assertEquals(expected, info.personas) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UuidTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UuidTest.kt new file mode 100644 index 0000000000..e2dc527038 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/UuidTest.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk + +import io.embrace.android.embracesdk.internal.utils.Uuid +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class UuidTest { + + @Test + fun testUuid() { + val uuid = Uuid.getEmbUuid("99fcae22-0db5-4b63-b49d-315eecce4889") + assertEquals("99FCAE220DB54B63B49D315EECCE4889", uuid) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ViewBreadcrumbTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ViewBreadcrumbTest.kt new file mode 100644 index 0000000000..152c9f9157 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ViewBreadcrumbTest.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.ViewBreadcrumb +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class ViewBreadcrumbTest { + + private val info = ViewBreadcrumb( + "screen", + 1600000000 + ).apply { + end = 1700000000 + } + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("view_breadcrumb_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("view_breadcrumb_expected.json") + val obj = Gson().fromJson(json, ViewBreadcrumb::class.java) + assertEquals("screen", obj.screen) + assertEquals(1600000000L, obj.getStartTime()) + assertEquals(1700000000L, obj.end) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", ViewBreadcrumb::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewBreadcrumbTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewBreadcrumbTest.kt new file mode 100644 index 0000000000..a7e057f092 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewBreadcrumbTest.kt @@ -0,0 +1,37 @@ +package io.embrace.android.embracesdk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.WebViewBreadcrumb +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class WebViewBreadcrumbTest { + + private val info = WebViewBreadcrumb( + "url", + 1600000000 + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("webview_breadcrumb_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("webview_breadcrumb_expected.json") + val obj = Gson().fromJson(json, WebViewBreadcrumb::class.java) + assertEquals("url", obj.url) + assertEquals(1600000000, obj.getStartTime()) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", WebViewBreadcrumb::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooksTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooksTest.kt new file mode 100644 index 0000000000..15ecfd2c08 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/WebViewClientSwazzledHooksTest.kt @@ -0,0 +1,25 @@ +package io.embrace.android.embracesdk + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +internal class WebViewClientSwazzledHooksTest { + + @Test + fun `verify logWebView is called`() { + val impl = mockk(relaxed = true) + Embrace.setImpl(impl) + val url = "url" + WebViewClientSwazzledHooks._preOnPageStarted(mockk(), url, mockk()) + verify { impl.logWebView(url) } + } + + @Test + fun `verify logWebView is called with null values`() { + val impl = mockk(relaxed = true) + Embrace.setImpl(impl) + WebViewClientSwazzledHooks._preOnPageStarted(null, null, null) + verify { impl.logWebView(null) } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrIntervalTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrIntervalTest.kt new file mode 100644 index 0000000000..4e4235c607 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrIntervalTest.kt @@ -0,0 +1,109 @@ +package io.embrace.android.embracesdk.anr + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.payload.AnrInterval +import io.embrace.android.embracesdk.payload.AnrSample +import io.embrace.android.embracesdk.payload.AnrSampleList +import io.embrace.android.embracesdk.payload.ThreadInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertNull +import org.junit.Test + +internal class AnrIntervalTest { + + private val threadInfo = ThreadInfo( + 13, Thread.State.RUNNABLE, "my-thread", 5, + listOf( + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ) + ) + + private val anrSample = AnrSample(150980980980, listOf(threadInfo), 0) + + private val anrSampleList = AnrSampleList(listOf(anrSample)) + + private val interval = AnrInterval( + startTime = 150980980980, + endTime = 150980980980 + 5000, + lastKnownTime = 150980980980 + 4000, + type = AnrInterval.Type.UI, + anrSampleList = anrSampleList, + code = AnrInterval.CODE_SAMPLES_CLEARED + ) + + @Test + fun testClearAnrSamples() { + val interval = interval.copy(code = AnrInterval.CODE_DEFAULT) + assertEquals(1, interval.size()) + assertEquals(AnrInterval.CODE_DEFAULT, interval.code) + + val copy = interval.clearSamples() + assertEquals(1, interval.size()) + assertEquals(AnrInterval.CODE_DEFAULT, interval.code) + assertNull(copy.anrSampleList) + assertEquals(AnrInterval.CODE_SAMPLES_CLEARED, copy.code) + } + + @Test + fun testAnrTickSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("anr_interval_expected.json") + .filter { !it.isWhitespace() } + + val observed = Gson().toJson(interval.copy()) + assertEquals(expectedInfo, observed) + } + + @Test + fun testAnrTickDeserialization() { + val json = ResourceReader.readResourceAsText("anr_interval_expected.json") + val obj = Gson().fromJson(json, AnrInterval::class.java) + assertEquals(150980980980, obj.startTime) + assertEquals(150980980980 + 5000, obj.endTime) + assertEquals(150980980980 + 4000, obj.lastKnownTime) + assertEquals(AnrInterval.Type.UI, obj.type) + assertEquals(anrSampleList, obj.anrSampleList) + } + + @Test + fun testAnrIntervalEmptyObject() { + val anrInterval = Gson().fromJson("{}", AnrInterval::class.java) + assertNotNull(anrInterval) + } + + @Test + fun testDeepCopy() { + val deepCopy = interval.deepCopy() + assertEquals(interval.startTime, deepCopy.startTime) + assertEquals(interval.endTime, deepCopy.endTime) + assertEquals(interval.lastKnownTime, deepCopy.lastKnownTime) + assertEquals(interval.type, deepCopy.type) + assertEquals(interval.code, deepCopy.code) + assertEquals(interval.anrSampleList, deepCopy.anrSampleList) + assertNotSame(interval.anrSampleList, deepCopy.anrSampleList) + } + + @Test + fun testDuration() { + assertEquals( + 5000L, + AnrInterval(startTime = 1600000000, endTime = 1600005000).duration() + ) + assertEquals( + 5000L, + AnrInterval(startTime = 1600000000, lastKnownTime = 1600005000).duration() + ) + assertEquals( + 5000L, + AnrInterval( + startTime = 1600000000, + endTime = 1600005000, + lastKnownTime = 1600003000 + ).duration() + ) + assertEquals(-1L, AnrInterval(startTime = 1600000000).duration()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrStacktraceSamplerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrStacktraceSamplerTest.kt new file mode 100644 index 0000000000..f184e090e3 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/AnrStacktraceSamplerTest.kt @@ -0,0 +1,185 @@ +package io.embrace.android.embracesdk.anr + +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.anr.detection.ThreadMonitoringState +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.payload.AnrInterval +import io.embrace.android.embracesdk.payload.AnrSample +import io.embrace.android.embracesdk.payload.AnrSampleList +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.util.concurrent.atomic.AtomicReference + +private const val BASELINE_MS = 16000000000 + +internal class AnrStacktraceSamplerTest { + + private val thread = Thread.currentThread() + private val anrMonitorThread = AtomicReference(thread) + private val clock = FakeClock() + private val configService = FakeConfigService() + private val state = ThreadMonitoringState(clock) + private val executor = MoreExecutors.newDirectExecutorService() + + @Test + fun testLeastValuableInterval() { + val sampler = AnrStacktraceSampler(configService, clock, thread, anrMonitorThread, executor) + assertNull(sampler.findLeastValuableIntervalWithSamples()) + val interval1 = AnrInterval( + startTime = BASELINE_MS, + lastKnownTime = BASELINE_MS + 5000, + anrSampleList = AnrSampleList(emptyList()) + ) + val interval2 = AnrInterval( + startTime = BASELINE_MS, + lastKnownTime = BASELINE_MS + 4000, + anrSampleList = AnrSampleList(emptyList()) + ) + val interval3 = AnrInterval( + startTime = BASELINE_MS, + lastKnownTime = BASELINE_MS + 1000, + anrSampleList = AnrSampleList(emptyList()) + ) + val interval4 = AnrInterval( + startTime = BASELINE_MS, + lastKnownTime = BASELINE_MS + 1000, + anrSampleList = AnrSampleList(emptyList()) + ) + val interval5 = AnrInterval( + startTime = BASELINE_MS, + lastKnownTime = BASELINE_MS + 500, + code = AnrInterval.CODE_SAMPLES_CLEARED + ) + + sampler.anrIntervals.add(interval1) + assertEquals(interval1, sampler.findLeastValuableIntervalWithSamples()) + + sampler.anrIntervals.add(interval2) + assertEquals(interval2, sampler.findLeastValuableIntervalWithSamples()) + + sampler.anrIntervals.add(interval3) + assertEquals(interval3, sampler.findLeastValuableIntervalWithSamples()) + + // most recent interval gets binned if duration is equal + sampler.anrIntervals.add(interval4) + assertEquals(interval3, sampler.findLeastValuableIntervalWithSamples()) + + // intervals without any samples are ignored + sampler.anrIntervals.add(interval5) + assertEquals(interval3, sampler.findLeastValuableIntervalWithSamples()) + } + + @Test + fun `exceed sample limit for one ANR interval`() { + clock.setCurrentTime(BASELINE_MS) + val repeatCount = 100 + val intervalMs: Long = 100 + val sampler = AnrStacktraceSampler(configService, clock, thread, anrMonitorThread, executor) + + // simulate one ANR with 100 intervals + sampler.onThreadBlocked(thread, clock.now()) + + repeat(repeatCount) { + sampler.onThreadBlockedInterval(thread, clock.now()) + clock.tick(intervalMs) + } + + sampler.onThreadUnblocked(thread, clock.now()) + + // verify one interval recorded + val intervals = sampler.getAnrIntervals(state, clock) + assertEquals(1, intervals.size) + + // verify basic metadata about the interval + val interval = intervals.single() + assertEquals(BASELINE_MS, interval.startTime) + assertEquals(clock.now(), interval.endTime) + assertEquals(AnrInterval.CODE_DEFAULT, interval.code) + + // verify samples were captured up to the limit + val samples = checkNotNull(interval.anrSampleList?.samples) + assertEquals(repeatCount, samples.size) + + // verify timestamps match + samples.forEachIndexed { index, sample -> + val expected = BASELINE_MS + (index * intervalMs) + assertEquals(expected, sample.timestamp) + } + + // verify samples after the sample limit record a code that they are cleared + samples.forEachIndexed { index, sample -> + val expected = when { + index >= 80 -> AnrSample.CODE_SAMPLE_LIMIT_REACHED + else -> AnrSample.CODE_DEFAULT + } + assertEquals(expected, sample.code) + } + } + + @Test + fun `exceed limit for number of ANRs`() { + clock.setCurrentTime(BASELINE_MS) + val anrRepeatCount = 15 + val intervalRepeatCount = 100 + val intervalMs: Long = 100 + val sampler = AnrStacktraceSampler(configService, clock, thread, anrMonitorThread, executor) + + // simulate multiple ANRs with intervals + repeat(anrRepeatCount) { index -> + sampler.onThreadBlocked(thread, clock.now()) + + repeat(intervalRepeatCount + index) { + sampler.onThreadBlockedInterval(thread, clock.now()) + clock.tick(intervalMs) + } + + sampler.onThreadUnblocked(thread, clock.now()) + } + + // verify 15 intervals were recorded + val intervals = sampler.getAnrIntervals(state, clock) + assertEquals(anrRepeatCount, intervals.size) + + // verify basic metadata about each interval + intervals.forEachIndexed { index, interval -> + if (index >= 10) { + assertEquals(AnrInterval.CODE_DEFAULT, interval.code) + assertNotNull(interval.anrSampleList) + } else { + assertEquals(AnrInterval.CODE_SAMPLES_CLEARED, interval.code) + assertNull(interval.anrSampleList) + } + } + + // verify samples were cleared in order of priority (longest intervals are retained) + intervals.forEachIndexed { index, interval -> + if (index < 10) { + return + } + assertEquals(intervalRepeatCount + index, interval.anrSampleList?.size()) + } + } + + @Test + fun `verify hard limit of 100 anr intervals`() { + clock.setCurrentTime(BASELINE_MS) + val anrRepeatCount = 110 + val intervalMs: Long = 100 + val sampler = AnrStacktraceSampler(configService, clock, thread, anrMonitorThread, executor) + + // simulate 110 ANRs with intervals + repeat(anrRepeatCount) { index -> + sampler.onThreadBlocked(thread, clock.now()) + sampler.onThreadBlockedInterval(thread, clock.now()) + clock.tick(intervalMs) + sampler.onThreadUnblocked(thread, clock.now()) + } + + // verify maximum of 100 intervals were recorded + val intervals = sampler.getAnrIntervals(state, clock) + assertEquals(100, intervals.size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceSigquitDetectionServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceSigquitDetectionServiceTest.kt new file mode 100644 index 0000000000..02595e48ad --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceSigquitDetectionServiceTest.kt @@ -0,0 +1,105 @@ +package io.embrace.android.embracesdk.anr + +import io.embrace.android.embracesdk.anr.sigquit.FindGoogleThread +import io.embrace.android.embracesdk.anr.sigquit.GoogleAnrHandlerNativeDelegate +import io.embrace.android.embracesdk.anr.sigquit.GoogleAnrTimestampRepository +import io.embrace.android.embracesdk.anr.sigquit.SigquitDetectionService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class EmbraceSigquitDetectionServiceTest { + + private lateinit var configService: FakeConfigService + private val logger = InternalEmbraceLogger() + private val mockSharedObjectLoader: SharedObjectLoader = mockk(relaxed = true) + private val mockFindGoogleThread: FindGoogleThread = mockk(relaxed = true) + private val mockGoogleAnrHandlerNativeDelegate: GoogleAnrHandlerNativeDelegate = mockk(relaxed = true) + private val mockGoogleAnrTimestampRepository: GoogleAnrTimestampRepository = mockk(relaxed = true) + + private lateinit var service: SigquitDetectionService + + @Before + fun setUp() { + configService = FakeConfigService() + service = SigquitDetectionService( + mockSharedObjectLoader, + mockFindGoogleThread, + mockGoogleAnrHandlerNativeDelegate, + mockGoogleAnrTimestampRepository, + configService, + logger + ) + } + + @Test + fun `finishing initialization won't install anr handler when embrace native library is not loaded`() { + // given embrace native library is not loaded + every { mockSharedObjectLoader.loadEmbraceNative() } returns false + + // when finishing initialization + service.setupGoogleAnrHandler() + verify(exactly = 0) { mockGoogleAnrHandlerNativeDelegate.install(any()) } + } + + @Test + fun `finishing initialization won't install anr handler when google thread was not found`() { + // given google thread wasn't found + every { mockSharedObjectLoader.loadEmbraceNative() } returns true + every { mockFindGoogleThread.invoke() } returns 0 + + // when finishing initialization + service.setupGoogleAnrHandler() + verify(exactly = 0) { mockGoogleAnrHandlerNativeDelegate.install(any()) } + } + + @Test + fun `finishing initialization will install anr handler when google thread was found`() { + // given google thread wasn't found + every { mockSharedObjectLoader.loadEmbraceNative() } returns true + every { mockFindGoogleThread.invoke() } returns 509 + + // when finishing initialization + service.setupGoogleAnrHandler() + verify(exactly = 1) { mockGoogleAnrHandlerNativeDelegate.install(any()) } + } + + @Test + fun `finishing initialization installs anr handler correctly with provided google thread`() { + // given a google thread + val testGoogleThreadId = 1234 + every { mockSharedObjectLoader.loadEmbraceNative() } returns true + every { mockFindGoogleThread.invoke() } returns testGoogleThreadId + + // when finishing initialization + service.setupGoogleAnrHandler() + verify(exactly = 1) { mockGoogleAnrHandlerNativeDelegate.install(testGoogleThreadId) } + } + + @Test + fun `finishing initialization adds a config service listener when google anr capture is disabled`() { + // given anr capture is disabled + assertEquals(0, configService.listeners.size) + service.initializeGoogleAnrTracking() + + // a listener is added to config service + assertEquals(1, configService.listeners.size) + } + + @Test + fun `clean collections`() { + // given google thread wasn't found + every { mockSharedObjectLoader.loadEmbraceNative() } returns true + every { mockFindGoogleThread.invoke() } returns 509 + + // when finishing initialization + service.cleanCollections() + verify(exactly = 1) { mockGoogleAnrTimestampRepository.clear() } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceStrictModeServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceStrictModeServiceTest.kt new file mode 100644 index 0000000000..411e610510 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/EmbraceStrictModeServiceTest.kt @@ -0,0 +1,56 @@ +package io.embrace.android.embracesdk.anr + +import android.os.strictmode.Violation +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.capture.strictmode.EmbraceStrictModeService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class EmbraceStrictModeServiceTest { + + private lateinit var configService: ConfigService + private lateinit var service: EmbraceStrictModeService + private val clock = Clock { 16900000000 } + + @Before + fun setUp() { + configService = FakeConfigService() + service = + EmbraceStrictModeService(configService, MoreExecutors.newDirectExecutorService(), clock) + } + + @Test + fun testCleanCollections() { + service.addViolation(mockk(relaxed = true)) + service.cleanCollections() + assertEquals(0, service.getCapturedData().size) + } + + @Test + fun testSessionEnd() { + val violation = mockk(relaxed = true) { + every { message } returns "Whoops!" + } + service.addViolation(violation) + val violations = service.getCapturedData() + val obj = violations.single() + assertTrue(obj.exceptionInfo.name.startsWith("android.os.strictmode.Violation")) + assertEquals("Whoops!", obj.exceptionInfo.message) + assertEquals(16900000000, obj.timestamp) + } + + @Test + fun testMaxSize() { + repeat(200) { + service.addViolation(mockk(relaxed = true)) + } + assertEquals(25, service.getCapturedData().size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/FindGoogleThreadTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/FindGoogleThreadTest.kt new file mode 100644 index 0000000000..132e6c7386 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/FindGoogleThreadTest.kt @@ -0,0 +1,53 @@ +package io.embrace.android.embracesdk.anr + +import io.embrace.android.embracesdk.anr.sigquit.FindGoogleThread +import io.embrace.android.embracesdk.anr.sigquit.GetThreadCommand +import io.embrace.android.embracesdk.anr.sigquit.GetThreadsInCurrentProcess +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class FindGoogleThreadTest { + + private val testThread = "12423" + private val anotherTestThread = "13215" + + private val mockLogger = mockk(relaxed = true) + private val mockGetThreadsInCurrentProcess = mockk() + private val mockGetThreadCommand = mockk() + + private val findGoogleThread = + FindGoogleThread(mockLogger, mockGetThreadsInCurrentProcess, mockGetThreadCommand) + + @Test + fun `return 0 when there are no threads in current process`() { + every { mockGetThreadsInCurrentProcess() } returns emptyList() + + val googleThread = findGoogleThread() + + assertEquals(0, googleThread) + } + + @Test + fun `return 0 when there are no commands for the threads`() { + every { mockGetThreadsInCurrentProcess() } returns listOf(testThread, anotherTestThread) + every { mockGetThreadCommand(any()) } returns "" + + val googleThread = findGoogleThread() + + assertEquals(0, googleThread) + } + + @Test + fun `return second thread when it has the correct command`() { + every { mockGetThreadsInCurrentProcess() } returns listOf(testThread, anotherTestThread) + every { mockGetThreadCommand(anotherTestThread) } returns "Signal Catcher" + every { mockGetThreadCommand(testThread) } returns "Not Signal Catcher" + + val googleThread = findGoogleThread() + + assertEquals(anotherTestThread.toInt(), googleThread) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ThreadInfoCollectorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ThreadInfoCollectorTest.kt new file mode 100644 index 0000000000..a8af3d8b30 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ThreadInfoCollectorTest.kt @@ -0,0 +1,131 @@ +package io.embrace.android.embracesdk.anr + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.embrace.android.embracesdk.payload.ThreadInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.lang.Thread.MAX_PRIORITY +import java.lang.Thread.MIN_PRIORITY +import java.lang.Thread.NORM_PRIORITY +import java.lang.Thread.State.RUNNABLE +import java.lang.Thread.currentThread + +internal class ThreadInfoCollectorTest { + + private val highPrioThread = ThreadInfo(1, RUNNABLE, "thread-1", MAX_PRIORITY, emptyList()) + private val medPrioThread = ThreadInfo(2, RUNNABLE, "thread-2", NORM_PRIORITY, emptyList()) + private val lowPrioThread = ThreadInfo(3, RUNNABLE, "thread-3", MIN_PRIORITY, emptyList()) + + private lateinit var configService: ConfigService + private lateinit var threadInfoCollector: ThreadInfoCollector + + @Before + fun setUp() { + configService = FakeConfigService( + anrBehavior = fakeAnrBehavior { + AnrRemoteConfig( + allowList = listOf(currentThread().name), + blockList = listOf("Finalizer") + ) + } + ) + threadInfoCollector = ThreadInfoCollector(currentThread()) + } + + @Test + fun testGetThreadsAllowList() { + val threadName = currentThread().name + val thread = threadInfoCollector.getAllowedThreads(configService).single() + assertEquals(threadName, thread.name) + } + + @Test + fun testGetThreadsPriority() { + assertEquals(1, threadInfoCollector.getAllowedThreads(configService).size) + } + + @Test + fun testGetAllowedThreads() { + assertTrue( + threadInfoCollector.getAllowedThreads(configService) + .none { it.name == "Finalizer" } + ) + } + + @Test + fun testIsAllowedByPriority() { + // 0 priority is always allowed + assertTrue( + threadInfoCollector.isAllowedByPriority( + 0, + highPrioThread.priority + ) + ) + assertTrue( + threadInfoCollector.isAllowedByPriority( + 0, + medPrioThread.priority + ) + ) + assertTrue( + threadInfoCollector.isAllowedByPriority( + 0, + lowPrioThread.priority + ) + ) + + // check low priority boundaries + assertTrue( + threadInfoCollector.isAllowedByPriority( + 1, + lowPrioThread.priority + ) + ) + assertFalse( + threadInfoCollector.isAllowedByPriority( + 2, + lowPrioThread.priority + ) + ) + + // check medium priority boundaries + assertTrue( + threadInfoCollector.isAllowedByPriority( + 4, + medPrioThread.priority + ) + ) + assertTrue( + threadInfoCollector.isAllowedByPriority( + 5, + medPrioThread.priority + ) + ) + assertFalse( + threadInfoCollector.isAllowedByPriority( + 6, + medPrioThread.priority + ) + ) + + // check high priority boundaries + assertTrue( + threadInfoCollector.isAllowedByPriority( + 9, + highPrioThread.priority + ) + ) + assertTrue( + threadInfoCollector.isAllowedByPriority( + 10, + highPrioThread.priority + ) + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSamplerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSamplerTest.kt new file mode 100644 index 0000000000..c857070dc5 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/AnrProcessErrorSamplerTest.kt @@ -0,0 +1,389 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.app.ActivityManager +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +internal class AnrProcessErrorSamplerTest { + + private lateinit var anrProcessErrorSampler: AnrProcessErrorSampler + + companion object { + private const val ANR_INTERVAL = 100L + private const val DELAY = 5 * 1000L + private const val PROCESS_ID = 1 + + // change interval so scheduler can get rescheduled + private const val CHANGED_INTERVAL = ANR_INTERVAL * 2 + + private val mockActivityManager: ActivityManager = mockk() + private var cfg: AnrRemoteConfig = AnrRemoteConfig( + // 15 seconds since thread is unblocked + anrProcessErrorsSchedulerExtraTimeAllowance = 15 * 1000, + anrProcessErrorsDelayMs = DELAY, + pctAnrProcessErrorsEnabled = 100 + ) + + private val configService: ConfigService = FakeConfigService(anrBehavior = fakeAnrBehavior { cfg }) + private val clock: Clock = FakeClock(30 * 1000) + private val mockLogger: InternalEmbraceLogger = mockk(relaxUnitFun = true) + private val mockScheduledFuture: ScheduledFuture<*> = mockk(relaxed = true) + private val anrProcessErrorStateInfo = AnrProcessErrorStateInfo( + "tag", + "sMessage", + "lMessage", + "stacktrace", + clock.now() + ) + private val secondAnrProcessErrorStateInfo = AnrProcessErrorStateInfo( + "tag2", + "sMessage2", + "lMessage2", + "stacktrace2", + clock.now() + ) + private val mockScheduler: ScheduledExecutorService = mockk(relaxed = true) + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(::findAnrProcessErrorStateInfo) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @Before + fun before() { + clearAllMocks(answers = false) + + // these are needed here because some tests change it + cfg = AnrRemoteConfig( + // 15 seconds since thread is unblocked + anrProcessErrorsSchedulerExtraTimeAllowance = 15 * 1000, + anrProcessErrorsDelayMs = DELAY, + anrProcessErrorsIntervalMs = ANR_INTERVAL, + pctAnrProcessErrorsEnabled = 100 + ) + every { + mockScheduler.scheduleAtFixedRate( + any(), + DELAY, + CHANGED_INTERVAL, + TimeUnit.MILLISECONDS + ) + } returns mockScheduledFuture + + anrProcessErrorSampler = AnrProcessErrorSampler( + mockActivityManager, + configService, + mockScheduler, + clock, + mockLogger, + PROCESS_ID + ) + anrProcessErrorSampler.scheduledFuture = mockScheduledFuture + } + + @Test + fun `verify scheduler should be allowed to run if thread has not been unblocked yet`() { + assertTrue(anrProcessErrorSampler.isSchedulerAllowedToRun()) + } + + @Test + fun `verify scheduler should run if thread is unblocked but maximum threshold has not been reached`() { + // only 5 seconds have elapsed since thread unblocked + val threadUnblockedTime = 25000L + anrProcessErrorSampler.onThreadUnblocked(Thread(), threadUnblockedTime) + + assertTrue(anrProcessErrorSampler.isSchedulerAllowedToRun()) + } + + // with this test we are also verifying that onThreadBlocked works fine + @Test + fun `verify scheduler doesn't run if thread has been unblocked and maximum threshold has been reached`() { + // 25 seconds have elapsed since thread unblocked + val threadUnblockedTime = 5000L + anrProcessErrorSampler.onThreadUnblocked(Thread(), threadUnblockedTime) + + assertFalse(anrProcessErrorSampler.isSchedulerAllowedToRun()) + } + + @Test + fun `verify searchForProcessErrors when scheduler not allowed to run and no anr process error found`() { + // with this setup scheduler is not allowed to run + val threadUnblockedTime = 5000L + anrProcessErrorSampler.onThreadUnblocked(Thread(), threadUnblockedTime) + + // no anr process error found + every { + findAnrProcessErrorStateInfo( + clock, + mockActivityManager, + PROCESS_ID + ) + } returns null + + anrProcessErrorSampler.onSearchForProcessErrors(/* any */1) + + // because scheduler is not allowed to run, it should be cancelled + verify { mockScheduledFuture.cancel(false) } + assertTrue(anrProcessErrorSampler.anrProcessErrors.isEmpty()) + } + + @Test + fun `verify searchForProcessErrors when scheduler not allowed to run and anr process error found`() { + // with this setup scheduler is not allowed to run + val threadUnblockedTime = 5000L + anrProcessErrorSampler.onThreadUnblocked(Thread(), threadUnblockedTime) + + // anr process error found + every { + findAnrProcessErrorStateInfo( + clock, + mockActivityManager, + PROCESS_ID + ) + } returns anrProcessErrorStateInfo + + val timestamp = 1L + anrProcessErrorSampler.onSearchForProcessErrors(/* any */timestamp) + + // because scheduler is not allowed to run, it should be cancelled + verify { mockScheduledFuture.cancel(false) } + assertEquals( + anrProcessErrorStateInfo, + anrProcessErrorSampler.anrProcessErrors[timestamp] + ) + } + + @Test + fun `verify searchForProcessErrors reschedule finds no anr process error found`() { + // with this setup scheduler is allowed to run + val threadUnblockedTime = 25000L + anrProcessErrorSampler.onThreadUnblocked(Thread(), threadUnblockedTime) + cfg = cfg.copy(anrProcessErrorsIntervalMs = CHANGED_INTERVAL) + + // no anr process error found + every { + findAnrProcessErrorStateInfo( + clock, + mockActivityManager, + PROCESS_ID + ) + } returns null + + anrProcessErrorSampler.onSearchForProcessErrors(/* any */1) + + // because scheduler is rescheduled, it should first be cancelled + verify { mockScheduledFuture.cancel(false) } + assertTrue(anrProcessErrorSampler.anrProcessErrors.isEmpty()) + // verify rescheduling with proper setup + verify { + mockScheduler.scheduleAtFixedRate( + any(), + DELAY, + CHANGED_INTERVAL, + TimeUnit.MILLISECONDS + ) + } + } + + @Test + fun `verify searchForProcessErrors reschedule does not fail if exception thrown`() { + // with this setup scheduler is allowed to run + val threadUnblockedTime = 25000L + anrProcessErrorSampler.onThreadUnblocked(Thread(), threadUnblockedTime) + cfg = cfg.copy(anrProcessErrorsIntervalMs = CHANGED_INTERVAL) + + // no anr process error found + every { + findAnrProcessErrorStateInfo( + clock, + mockActivityManager, + PROCESS_ID + ) + } returns null + every { + mockScheduler.scheduleAtFixedRate( + any(), + DELAY, + CHANGED_INTERVAL, + TimeUnit.MILLISECONDS + ) + } throws Exception() + + anrProcessErrorSampler.onSearchForProcessErrors(/* any */1) + + // because scheduler is rescheduled, it should first be cancelled + verify { mockScheduledFuture.cancel(false) } + assertTrue(anrProcessErrorSampler.anrProcessErrors.isEmpty()) + // verify rescheduling with proper setup + verify { + mockScheduler.scheduleAtFixedRate( + any(), + DELAY, + CHANGED_INTERVAL, + TimeUnit.MILLISECONDS + ) + } + + // if test does not fail, it means the exception got swallowed, which is what we're expecting + } + + @Test + fun `verify searchForProcessErrors when scheduler interval config changed and anr process error has been found`() { + // with this setup scheduler is allowed to run + val threadUnblockedTime = 25000L + anrProcessErrorSampler.onThreadUnblocked(Thread(), threadUnblockedTime) + cfg = cfg.copy(anrProcessErrorsIntervalMs = CHANGED_INTERVAL) + + // anr process error found + every { + findAnrProcessErrorStateInfo( + clock, + mockActivityManager, + PROCESS_ID + ) + } returns anrProcessErrorStateInfo + + val timestamp = 1L + anrProcessErrorSampler.onSearchForProcessErrors(/* any */timestamp) + + // because anr process error found, then it should cancelled + verify { mockScheduledFuture.cancel(true) } + assertEquals( + anrProcessErrorStateInfo, + anrProcessErrorSampler.anrProcessErrors[timestamp] + ) + // verify no rescheduling has been performed + verify(exactly = 0) { + mockScheduler.scheduleAtFixedRate( + any(), + any(), + any(), + any() + ) + } + } + + @Test + fun `verify on thread unblocked`() { + val unblockedTs = 10L + anrProcessErrorSampler.onThreadUnblocked(Thread(), unblockedTs) + + assertEquals(unblockedTs, anrProcessErrorSampler.threadUnblockedMs) + } + + @Test + fun `verify on thread unblocked should not do anything if feature is disabled`() { + cfg = cfg.copy(pctAnrProcessErrorsEnabled = 0) + val unblockedTs = 10L + + anrProcessErrorSampler.onThreadUnblocked(Thread(), unblockedTs) + + assertNull(anrProcessErrorSampler.threadUnblockedMs) + } + + @Test + fun `verify on thread blocked interval should not do anything`() { + anrProcessErrorSampler.onThreadBlockedInterval(Thread(), 1L) + + // verify mocks have not been called + verify { mockActivityManager wasNot Called } + verify { mockActivityManager wasNot Called } + verify { mockLogger wasNot Called } + verify { mockScheduledFuture wasNot Called } + verify { mockScheduler wasNot Called } + } + + @Test + fun `verify on thread blocked should reset and schedule`() { + anrProcessErrorSampler.onThreadBlocked(Thread(), 1L) + + assertNull(anrProcessErrorSampler.threadUnblockedMs) + verify { mockScheduledFuture.cancel(true) } + assertTrue(anrProcessErrorSampler.anrProcessErrors.isEmpty()) + // verify rescheduling with proper setup + verify { + mockScheduler.scheduleAtFixedRate( + any(), + DELAY, + ANR_INTERVAL, + TimeUnit.MILLISECONDS + ) + } + } + + @Test + fun `verify on thread blocked should not do anything if feature is disabled`() { + cfg = cfg.copy(pctAnrProcessErrorsEnabled = 0) + val unblockedTs = 100L + anrProcessErrorSampler.threadUnblockedMs = unblockedTs + anrProcessErrorSampler.anrProcessErrors[1] = mockk() + + anrProcessErrorSampler.onThreadBlocked(Thread(), 1L) + + // verify that threadUnblockedMs wasn't reset + assertEquals(unblockedTs, anrProcessErrorSampler.threadUnblockedMs) + verify { mockScheduledFuture wasNot Called } + // verify that anrProcessErrors wasn't reset + assertTrue(anrProcessErrorSampler.anrProcessErrors.size == 1) + } + + @Test + fun `get anr process errors with background anrs`() { + cfg = cfg.copy(pctBgEnabled = 100) + val startTime = 30000L + // this will be a bkg anr + anrProcessErrorSampler.anrProcessErrors[20000] = anrProcessErrorStateInfo + anrProcessErrorSampler.anrProcessErrors[40000] = secondAnrProcessErrorStateInfo + + val anrProcessErrors = anrProcessErrorSampler.getAnrProcessErrors(startTime) + + assertEquals(2, anrProcessErrors.size) + assertEquals(anrProcessErrorStateInfo, anrProcessErrors[0]) + assertEquals(secondAnrProcessErrorStateInfo, anrProcessErrors[1]) + } + + @Test + fun `get anr process errors without background anrs`() { + cfg = cfg.copy(pctBgEnabled = 0) + val startTime = 30000L + // this will be a bkg anr + anrProcessErrorSampler.anrProcessErrors[20000] = anrProcessErrorStateInfo + anrProcessErrorSampler.anrProcessErrors[40000] = secondAnrProcessErrorStateInfo + + val anrProcessErrors = anrProcessErrorSampler.getAnrProcessErrors(startTime) + + assertEquals(1, anrProcessErrors.size) + assertEquals(secondAnrProcessErrorStateInfo, anrProcessErrors[0]) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetectorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetectorTest.kt new file mode 100644 index 0000000000..968ce7eaf9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/BlockedThreadDetectorTest.kt @@ -0,0 +1,78 @@ +package io.embrace.android.embracesdk.anr.detection + +import io.embrace.android.embracesdk.anr.BlockedThreadListener +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicReference + +private const val BASELINE_MS = 1500000000L + +internal class BlockedThreadDetectorTest { + + private lateinit var detector: BlockedThreadDetector + private lateinit var configService: ConfigService + private lateinit var clock: FakeClock + private lateinit var listener: BlockedThreadListener + private lateinit var state: ThreadMonitoringState + private lateinit var anrMonitorThread: AtomicReference + + @Before + fun setUp() { + configService = FakeConfigService() + clock = FakeClock(BASELINE_MS) + listener = mockk(relaxUnitFun = true) + state = ThreadMonitoringState(clock) + anrMonitorThread = AtomicReference(Thread.currentThread()) + detector = BlockedThreadDetector( + configService, + clock, + listener, + state, + Thread.currentThread(), + anrMonitorThread = anrMonitorThread + ) + } + + @Test + fun testShouldAttemptAnrSample() { + assertFalse(detector.shouldAttemptAnrSample(BASELINE_MS)) + assertFalse(detector.shouldAttemptAnrSample(-23409)) + assertFalse(detector.shouldAttemptAnrSample(0)) + assertFalse(detector.shouldAttemptAnrSample(BASELINE_MS - 23409)) + assertFalse(detector.shouldAttemptAnrSample(BASELINE_MS + 50)) + assertTrue(detector.shouldAttemptAnrSample(BASELINE_MS + 51)) + assertTrue(detector.shouldAttemptAnrSample(BASELINE_MS + 100)) + assertTrue(detector.shouldAttemptAnrSample(BASELINE_MS + 30000)) + } + + @Test + fun testListenerFired() { + val now = BASELINE_MS + 3000 + clock.setCurrentTime(now) + detector.updateAnrTracking(BASELINE_MS + 2000) + verify(exactly = 1) { listener.onThreadBlockedInterval(any(), any()) } + assertEquals(now, state.lastMonitorThreadResponseMs) + assertEquals(now, state.lastSampleAttemptMs) + } + + @Test + fun testSampleBackoff() { + val now = BASELINE_MS + 2000 + clock.setCurrentTime(now) + state.lastMonitorThreadResponseMs = now - 10 + state.lastSampleAttemptMs = now - 10 + + detector.updateAnrTracking(now) + verify(exactly = 0) { listener.onThreadBlockedInterval(any(), any()) } + assertEquals(now, state.lastMonitorThreadResponseMs) + assertEquals(now - 10, state.lastSampleAttemptMs) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckSchedulerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckSchedulerTest.kt new file mode 100644 index 0000000000..699a5dac98 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/LivenessCheckSchedulerTest.kt @@ -0,0 +1,176 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.os.Looper +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicReference + +private const val CUSTOM_THREAD_PRIORITY = android.os.Process.THREAD_PRIORITY_BACKGROUND + +internal class LivenessCheckSchedulerTest { + + private lateinit var scheduler: LivenessCheckScheduler + + private lateinit var configService: ConfigService + private lateinit var anrExecutorService: BlockingScheduledExecutorService + private lateinit var logger: InternalEmbraceLogger + private lateinit var looper: Looper + private lateinit var fakeClock: FakeClock + private lateinit var fakeTargetThreadHandler: TargetThreadHandler + private lateinit var state: ThreadMonitoringState + private lateinit var detector: BlockedThreadDetector + private lateinit var cfg: AnrRemoteConfig + private lateinit var anrMonitorThread: AtomicReference + + @Before + fun setUp() { + anrMonitorThread = AtomicReference(Thread.currentThread()) + cfg = AnrRemoteConfig( + monitorThreadPriority = CUSTOM_THREAD_PRIORITY, + ) + fakeClock = FakeClock(160982340900) + configService = FakeConfigService(anrBehavior = fakeAnrBehavior { cfg }) + anrExecutorService = BlockingScheduledExecutorService(fakeClock) + logger = mockk(relaxUnitFun = true) + looper = mockk { + every { thread } returns mockk() + } + state = ThreadMonitoringState(fakeClock) + detector = BlockedThreadDetector( + configService = configService, + clock = fakeClock, + state = state, + targetThread = Thread.currentThread(), + anrMonitorThread = anrMonitorThread + ) + fakeTargetThreadHandler = mockk(relaxUnitFun = true) { + every { action = any() } returns Unit + every { sendMessage(any()) } returns true + } + every { fakeTargetThreadHandler.hasMessages(any()) } returns false + + scheduler = LivenessCheckScheduler( + configService, + anrExecutorService, + fakeClock, + state, + fakeTargetThreadHandler, + detector, + logger, + anrMonitorThread = anrMonitorThread + ) + } + + @Test + fun testMonitoringThreadStateWhenDoingStartStopStart() { + scheduler.startMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertTrue(state.started.get()) + + scheduler.stopMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertFalse(state.started.get()) + + scheduler.startMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertTrue(state.started.get()) + } + + @Test + fun testMonitoringThreadStateWhenDoingStartStopStartAndIsDoneReturnsFalse() { + scheduler.startMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertTrue(state.started.get()) + + scheduler.stopMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertFalse(state.started.get()) + + scheduler.startMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertTrue(state.started.get()) + } + + @Test + fun testStartMonitoringThreadDoubleCall() { + scheduler.startMonitoringThread() + val lastTimeThreadResponded = fakeClock.now() + anrExecutorService.runCurrentlyBlocked() + assertEquals(lastTimeThreadResponded, state.lastMonitorThreadResponseMs) + fakeClock.tick(10L) + assertEquals(lastTimeThreadResponded, state.lastMonitorThreadResponseMs) + // double-start should not schedule anything + scheduler.startMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertEquals(lastTimeThreadResponded, state.lastMonitorThreadResponseMs) + } + + @Test + fun testGetConfigService() { + assertEquals(configService, scheduler.configService) + val obj = mockk() + scheduler.configService = obj + assertEquals(obj, scheduler.configService) + } + + @Test + fun testExecuteHealthCheckSameInterval() { + mockkStatic(android.os.Process::class) + every { fakeTargetThreadHandler.hasMessages(any()) } returns false + scheduler.onMonitorThreadHeartbeat() + + // verify target thread handler called with scheduling + verify(exactly = 1) { fakeTargetThreadHandler.sendMessage(any()) } + + // verify heartbeat check + assertEquals(160982340900, state.lastMonitorThreadResponseMs) + // verify thread priority property set + verify(exactly = 1) { android.os.Process.setThreadPriority(CUSTOM_THREAD_PRIORITY) } + + unmockkStatic(android.os.Process::class) + } + + @Test + fun testExecuteHealthCheckPendingMessage() { + mockkStatic(android.os.Process::class) + every { fakeTargetThreadHandler.hasMessages(any()) } returns true + scheduler.onMonitorThreadHeartbeat() + + // verify target thread handler called with scheduling + verify(exactly = 0) { fakeTargetThreadHandler.sendMessage(any()) } + + // verify heartbeat check + assertEquals(160982340900, state.lastMonitorThreadResponseMs) + // verify thread priority property set + verify(exactly = 1) { android.os.Process.setThreadPriority(CUSTOM_THREAD_PRIORITY) } + + unmockkStatic(android.os.Process::class) + } + + @Test + fun testExecuteHealthCheckDifferentIntervalMs() { + // alter the intervalMs to trigger rescheduling + scheduler.startMonitoringThread() + anrExecutorService.runCurrentlyBlocked() + assertEquals(fakeClock.now(), state.lastMonitorThreadResponseMs) + cfg = cfg.copy(sampleIntervalMs = 10) + anrExecutorService.moveForwardAndRunBlocked(100) + anrExecutorService.runCurrentlyBlocked() + assertEquals(fakeClock.now(), state.lastMonitorThreadResponseMs) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandlerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandlerTest.kt new file mode 100644 index 0000000000..f8c25650f8 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandlerTest.kt @@ -0,0 +1,142 @@ +package io.embrace.android.embracesdk.anr.detection + +import android.os.Message +import android.os.MessageQueue +import io.embrace.android.embracesdk.anr.detection.TargetThreadHandler.Companion.HEARTBEAT_REQUEST +import io.embrace.android.embracesdk.concurrency.BlockableExecutorService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicReference + +private const val FAKE_TIME_MS = 16098230498234 + +internal class TargetThreadHandlerTest { + + private val clock = { FAKE_TIME_MS } + private val state = ThreadMonitoringState(clock) + private lateinit var runnable: Runnable + private lateinit var executorService: BlockableExecutorService + private lateinit var anrMonitorThread: AtomicReference + private lateinit var handler: TargetThreadHandler + private lateinit var configService: ConfigService + + @Before + fun setUp() { + runnable = mockk() + configService = FakeConfigService() + anrMonitorThread = AtomicReference() + executorService = BlockableExecutorService(blockingMode = true) + executorService.submit { anrMonitorThread.set(Thread.currentThread()) } + executorService.runCurrentlyBlocked() + handler = createHandler(null) + handler.action = mockk() + } + + private fun createHandler(messageQueue: MessageQueue?): TargetThreadHandler { + return TargetThreadHandler( + mockk(), + executorService, + anrMonitorThread = anrMonitorThread, + configService, + messageQueue + ) { FAKE_TIME_MS }.apply { + action = {} + } + } + + @Test + fun testTargetThreadHandlerWrongMsg() { + assertNotNull(handler) + state.lastTargetThreadResponseMs = 0L + + // process a message + handler.handleMessage(mockk()) + verify(exactly = 0) { handler.action.invoke(any()) } + } + + @Test + fun testNoAnrInProgress() { + assertNotNull(handler) + state.lastTargetThreadResponseMs = 0L + + // process a message + val msg = mockk() + msg.what = HEARTBEAT_REQUEST + handler.handleMessage(msg) + assertEquals(1, executorService.tasksBlockedCount()) + } + + @Test + fun testTargetThreadHandlerCorrectMsg() { + assertNotNull(handler) + state.lastTargetThreadResponseMs = 0L + state.anrInProgress = true + + // process a message + val msg = mockk() + msg.what = HEARTBEAT_REQUEST + handler.handleMessage(msg) + assertEquals(1, executorService.tasksBlockedCount()) + executorService.runCurrentlyBlocked() + verify { handler.action.invoke(FAKE_TIME_MS) } + } + + @Test + fun testCorrectMsgNonNullQueue() { + handler = createHandler(mockk()) + handler.installed = true + assertNotNull(handler) + state.lastTargetThreadResponseMs = 0L + state.anrInProgress = true + + // process a message + val msg = mockk() + msg.what = HEARTBEAT_REQUEST + handler.handleMessage(msg) + } + + @Test + fun testRejectedExecution() { + executorService.shutdownNow() + assertNotNull(handler) + state.lastTargetThreadResponseMs = 0L + + // RejectedExecutionException ignored as ScheduledExecutorService will be shutting down. + handler.handleMessage(mockk()) + assertEquals(0L, state.lastTargetThreadResponseMs) + } + + @Test + fun testStartIdleHandlerEnabled() { + val messageQueue = mockk(relaxUnitFun = true) + configService = FakeConfigService( + anrBehavior = fakeAnrBehavior { + AnrRemoteConfig(pctIdleHandlerEnabled = 100f) + } + ) + handler = createHandler(messageQueue) + handler.start() + verify(exactly = 1) { messageQueue.addIdleHandler(any()) } + } + + @Test + fun testStartIdleHandlerDisabled() { + val messageQueue = mockk(relaxUnitFun = true) + configService = FakeConfigService( + anrBehavior = fakeAnrBehavior { + AnrRemoteConfig(pctIdleHandlerEnabled = 0f) + } + ) + handler = createHandler(messageQueue) + handler.start() + verify(exactly = 0) { messageQueue.addIdleHandler(any()) } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetectorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetectorTest.kt new file mode 100644 index 0000000000..f84a956e5d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/detection/UnbalancedCallDetectorTest.kt @@ -0,0 +1,79 @@ +package io.embrace.android.embracesdk.anr.detection + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import io.mockk.MockKVerificationScope +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Test + +internal class UnbalancedCallDetectorTest { + + private lateinit var logger: InternalEmbraceLogger + private lateinit var detector: UnbalancedCallDetector + private val thread = Thread.currentThread() + + @Before + fun setUp() { + logger = mockk(relaxUnitFun = true) + detector = UnbalancedCallDetector(logger) + } + + @Test + fun testBalancedCalls() { + detector.onThreadBlocked(thread, 1) + detector.onThreadBlockedInterval(thread, 2) + detector.onThreadBlockedInterval(thread, 3) + detector.onThreadBlockedInterval(thread, 4) + detector.onThreadUnblocked(thread, 5) + detector.onThreadBlocked(thread, 6) + verify(exactly = 0) { internalErrorLog() } + } + + @Test + fun testUnbalancedOnThreadBlocked() { + detector.onThreadBlocked(thread, 1) + detector.onThreadBlocked(thread, 2) + verify(exactly = 1) { internalErrorLog() } + } + + @Test + fun testUnbalancedOnThreadBlockedInterval() { + detector.onThreadBlockedInterval(thread, 1) + verify(exactly = 1) { internalErrorLog() } + } + + @Test + fun testUnbalancedOnThreadUnblocked() { + detector.onThreadUnblocked(thread, 1) + verify(exactly = 1) { internalErrorLog() } + } + + @Test + fun testOnThreadBlockedWrongTimestamp() { + detector.onThreadBlocked(thread, 150000000) + detector.onThreadBlockedInterval(thread, 150000001) + detector.onThreadUnblocked(thread, 150000002) + detector.onThreadBlocked(thread, 140000000) + verify(exactly = 1) { internalErrorLog() } + } + + @Test + fun testOnThreadBlockedIntervalWrongTimestamp() { + detector.onThreadBlocked(thread, 150000000) + detector.onThreadBlockedInterval(thread, 140000000) + verify(exactly = 1) { internalErrorLog() } + } + + @Test + fun testOnThreadUnblockedWrongTimestamp() { + detector.onThreadBlocked(thread, 150000000) + detector.onThreadBlockedInterval(thread, 150000001) + detector.onThreadUnblocked(thread, 140000000) + verify(exactly = 1) { internalErrorLog() } + } + + private fun MockKVerificationScope.internalErrorLog() = + logger.log(any(), InternalStaticEmbraceLogger.Severity.ERROR, any(), true) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrSampleJsonTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrSampleJsonTest.kt new file mode 100644 index 0000000000..e7109ad387 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrSampleJsonTest.kt @@ -0,0 +1,55 @@ +package io.embrace.android.embracesdk.anr.ndk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.NativeThreadAnrSample +import io.embrace.android.embracesdk.payload.NativeThreadAnrStackframe +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class NativeThreadAnrSampleJsonTest { + + @Test + fun testSerialization() { + val sample = NativeThreadAnrSample( + 2, + 1560923409, + 5, + listOf( + NativeThreadAnrStackframe( + "0x5092afb9", + "0x00274fc1", + "/data/foo/libtest.so", + 5 + ) + ) + ) + + assertEquals(2, sample.result) + assertEquals(1560923409L, sample.sampleTimestamp) + assertEquals(5L, sample.sampleDurationMs) + + val stackframe = checkNotNull(sample.stackframes?.single()) + assertEquals("0x5092afb9", stackframe.pc) + assertEquals("0x00274fc1", stackframe.soLoadAddr) + assertEquals("/data/foo/libtest.so", stackframe.soPath) + assertEquals(5, stackframe.result) + + val tree = Gson().toJsonTree(sample).asJsonObject + assertNotNull(tree) + assertEquals(4, tree.size()) + assertEquals(2, tree.get("r").asInt) + assertEquals(1560923409, tree.get("t").asInt) + assertEquals(5, tree.get("d").asInt) + + val frames = tree.getAsJsonArray("s") + assertEquals(1, frames.size()) + + // assert stackframe serialized + val frame = frames.get(0).asJsonObject + assertEquals("0x5092afb9", frame.get("pc").asString) + assertEquals("0x00274fc1", frame.get("l").asString) + assertEquals("/data/foo/libtest.so", frame.get("p").asString) + assertEquals(5, frame.get("r").asInt) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrStackframeJsonTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrStackframeJsonTest.kt new file mode 100644 index 0000000000..2a54805cb4 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/ndk/NativeThreadAnrStackframeJsonTest.kt @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.anr.ndk + +import com.google.gson.Gson +import io.embrace.android.embracesdk.payload.NativeThreadAnrStackframe +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class NativeThreadAnrStackframeJsonTest { + + @Test + fun testSerialization() { + val frame = NativeThreadAnrStackframe( + "0x5092afb9", + "0x00274fc1", + "/data/foo/libtest.so", + 11 + ) + + val tree = Gson().toJsonTree(frame).asJsonObject + assertNotNull(tree) + assertEquals(4, tree.size()) + assertEquals("0x5092afb9", tree.get("pc").asString) + assertEquals("0x00274fc1", tree.get("l").asString) + assertEquals("/data/foo/libtest.so", tree.get("p").asString) + assertEquals(11, tree.get("r").asInt) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegateTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegateTest.kt new file mode 100644 index 0000000000..1b1e686f6d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/FilesDelegateTest.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File + +internal class FilesDelegateTest { + + @Test + fun testGetThreadsFileForCurrentProcess() { + assertEquals(File("/proc/self/task"), FilesDelegate().getThreadsFileForCurrentProcess()) + } + + @Test + fun testGetCommandFileForThread() { + assertEquals(File("/proc/512/comm"), FilesDelegate().getCommandFileForThread("512")) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommandTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommandTest.kt new file mode 100644 index 0000000000..d1a9169be0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadCommandTest.kt @@ -0,0 +1,52 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File +import java.nio.file.Files + +internal class GetThreadCommandTest { + private val testThreadId: String = "12129" + + private val mockFilesDelegate = mockk() + + private val getThreadCommand = GetThreadCommand(mockFilesDelegate) + + @Test + fun `return empty command name when command file is empty`() { + val directory = Files.createTempDirectory("test").toFile() + val emptyFile = File.createTempFile("file1", null, directory) + every { mockFilesDelegate.getCommandFileForThread(testThreadId) } returns emptyFile + + val commandName = getThreadCommand(testThreadId) + + assertEquals("", commandName) + } + + @Test + fun `return command file contents`() { + val directory = Files.createTempDirectory("test").toFile() + val testFile = File.createTempFile("file1", null, directory) + testFile.writeText(testThreadId) + every { mockFilesDelegate.getCommandFileForThread(testThreadId) } returns testFile + + val commandName = getThreadCommand(testThreadId) + + assertEquals(testThreadId, commandName) + } + + @Test + fun `file throws exception`() { + val directory = Files.createTempDirectory("test").toFile() + val testFile = File.createTempFile("file1", null, directory) + testFile.writeText(testThreadId) + testFile.setReadable(false) + every { mockFilesDelegate.getCommandFileForThread(testThreadId) } returns testFile + + val commandName = getThreadCommand(testThreadId) + + assertEquals("", commandName) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcessTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcessTest.kt new file mode 100644 index 0000000000..f5579c07ee --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GetThreadsInCurrentProcessTest.kt @@ -0,0 +1,51 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File +import java.nio.file.Files.createTempDirectory + +internal class GetThreadsInCurrentProcessTest { + private val mockFilesDelegate = mockk() + + private val getThreadsInCurrentProcess = GetThreadsInCurrentProcess(mockFilesDelegate) + + @Test + fun `return empty list when threads directory is empty`() { + val emptyDirectory = createTempDirectory("test") + every { mockFilesDelegate.getThreadsFileForCurrentProcess() } returns emptyDirectory.toFile() + + val threads = getThreadsInCurrentProcess() + + assertEquals(emptyList(), threads) + } + + @Test + fun `return file names when directory is not empty`() { + val directory = createTempDirectory("test").toFile() + val file = File.createTempFile("file1", null, directory) + val file2 = File.createTempFile("file2", null, directory) + val file3 = File.createTempFile("file3", null, directory) + every { mockFilesDelegate.getThreadsFileForCurrentProcess() } returns directory + + val threads = getThreadsInCurrentProcess.invoke() + + // we need to do this because the order is not guaranteed + val expectedList = listOf(file.name, file2.name, file3.name) + assertEquals(expectedList.size, threads.size) + assertTrue(expectedList.containsAll(threads)) + } + + @Test + fun `IO exception handled`() { + every { mockFilesDelegate.getThreadsFileForCurrentProcess() } returns mockk { + every { listFiles() } throws SecurityException() + } + + val threads = getThreadsInCurrentProcess() + assertEquals(emptyList(), threads) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepositoryTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepositoryTest.kt new file mode 100644 index 0000000000..9126643dc7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/anr/sigquit/GoogleAnrTimestampRepositoryTest.kt @@ -0,0 +1,94 @@ +package io.embrace.android.embracesdk.anr.sigquit + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class GoogleAnrTimestampRepositoryTest { + private val testTimestamp = 1234L + private val differentTimestamp = 1000L + + private val mockLogger = mockk(relaxed = true) + + private val googleAnrTimestampRepository = GoogleAnrTimestampRepository(mockLogger) + + @Test + fun `disregard new timestamps after limit has been reached`() { + repeat(50) { + googleAnrTimestampRepository.add(testTimestamp) + } + googleAnrTimestampRepository.add(differentTimestamp) + + val timestamps = googleAnrTimestampRepository.getGoogleAnrTimestamps(0L, 10000L) + assertEquals(50, timestamps.size) + assertFalse(timestamps.contains(differentTimestamp)) + verify { mockLogger.logWarning(any()) } + } + + @Test + fun `clear removes saved ANRs`() { + googleAnrTimestampRepository.add(testTimestamp) + + googleAnrTimestampRepository.clear() + + val timestamps = googleAnrTimestampRepository.getGoogleAnrTimestamps(0L, 10000L) + assertTrue(timestamps.isEmpty()) + } + + @Test + fun `return empty list if startTime is bigger than endTime`() { + val startTime = 2L + val endTime = 1L + googleAnrTimestampRepository.add(testTimestamp) + + val timestamps = googleAnrTimestampRepository.getGoogleAnrTimestamps(startTime, endTime) + + assertEquals(emptyList(), timestamps) + } + + @Test + fun `return only timestamps within the extended start and end range`() { + val startTime = 10L + val endTime = 20L + val timestampWithin = 15L + val timestampWithinExtendedStart = 5L + val timestampWithinExtendedEnd = 25L + val timestampHigherThanExtendedEndTime = 26L + val timestampLowerThanExtendedStartTime = 4L + + googleAnrTimestampRepository.add(timestampWithin) + googleAnrTimestampRepository.add(timestampWithinExtendedStart) + googleAnrTimestampRepository.add(timestampWithinExtendedEnd) + googleAnrTimestampRepository.add(timestampHigherThanExtendedEndTime) + googleAnrTimestampRepository.add(timestampLowerThanExtendedStartTime) + + val timestamps = googleAnrTimestampRepository.getGoogleAnrTimestamps(startTime, endTime) + + assertTrue(timestamps.contains(timestampWithin)) + assertTrue(timestamps.contains(timestampWithinExtendedStart)) + assertTrue(timestamps.contains(timestampWithinExtendedEnd)) + assertFalse(timestamps.contains(timestampLowerThanExtendedStartTime)) + assertFalse(timestamps.contains(timestampHigherThanExtendedEndTime)) + } + + @Test + fun `stop adding timestamps after getting one that exceeds endTime`() { + val startTime = 1L + val endTime = 4L + val timestampWithin1 = 2L + val timestampExceedingExtendedEndTime = 11L + val timestampWithin2 = 3L + googleAnrTimestampRepository.add(timestampWithin1) + googleAnrTimestampRepository.add(timestampExceedingExtendedEndTime) + googleAnrTimestampRepository.add(timestampWithin2) + + val timestamps = googleAnrTimestampRepository.getGoogleAnrTimestamps(startTime, endTime) + assertTrue(timestamps.contains(timestampWithin1)) + assertFalse(timestamps.contains(timestampExceedingExtendedEndTime)) + assertFalse(timestamps.contains(timestampWithin2)) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegateTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegateTest.kt new file mode 100644 index 0000000000..fda7e2e0c9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/cpu/EmbraceCpuInfoDelegateTest.kt @@ -0,0 +1,37 @@ +package io.embrace.android.embracesdk.capture.cpu + +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +internal class EmbraceCpuInfoDelegateTest { + + private val mockSharedObjectLoader: SharedObjectLoader = mockk(relaxed = true) + private lateinit var logger: InternalEmbraceLogger + private lateinit var cpuInfoDelegate: EmbraceCpuInfoDelegate + + @Before + fun before() { + logger = InternalEmbraceLogger() + cpuInfoDelegate = EmbraceCpuInfoDelegate(mockSharedObjectLoader, logger) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `check native library not loaded returns null`() { + every { mockSharedObjectLoader.loadEmbraceNative() } returns false + + Assert.assertEquals(null, cpuInfoDelegate.getCpuName()) + Assert.assertEquals(null, cpuInfoDelegate.getElg()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizerTest.kt new file mode 100644 index 0000000000..6737a4ddf7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/BreadcrumbsSanitizerTest.kt @@ -0,0 +1,53 @@ +package io.embrace.android.embracesdk.capture.crumbs + +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.payload.Breadcrumbs +import org.junit.Assert +import org.junit.Test + +internal class BreadcrumbsSanitizerTest { + + private val breadcrumbs = Breadcrumbs( + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + ) + + @Test + fun `test if it keeps breadcrumbs`() { + // enabled components contains everything about breadcrumbs + val components = setOf( + SessionGatingKeys.BREADCRUMBS_TAPS, + SessionGatingKeys.BREADCRUMBS_VIEWS, + SessionGatingKeys.BREADCRUMBS_CUSTOM_VIEWS, + SessionGatingKeys.BREADCRUMBS_WEB_VIEWS, + SessionGatingKeys.BREADCRUMBS_CUSTOM, + ) + + val result = BreadcrumbsSanitizer(breadcrumbs, components).sanitize() + + Assert.assertNotNull(result?.tapBreadcrumbs) + Assert.assertNotNull(result?.viewBreadcrumbs) + Assert.assertNotNull(result?.customBreadcrumbs) + Assert.assertNotNull(result?.webViewBreadcrumbs) + Assert.assertNotNull(result?.customBreadcrumbs) + } + + @Test + fun `test if it sanitizes breadcrumbs`() { + // enabled components doesn't contain any breadcrumb properties + val components = setOf() + + val result = BreadcrumbsSanitizer(breadcrumbs, components).sanitize() + + Assert.assertNull(result?.tapBreadcrumbs) + Assert.assertNull(result?.viewBreadcrumbs) + Assert.assertNull(result?.customBreadcrumbs) + Assert.assertNull(result?.webViewBreadcrumbs) + Assert.assertNull(result?.customBreadcrumbs) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbServiceTest.kt new file mode 100644 index 0000000000..18976de6ef --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/EmbraceBreadcrumbServiceTest.kt @@ -0,0 +1,588 @@ +package io.embrace.android.embracesdk.capture.crumbs + +import android.app.Activity +import com.google.gson.GsonBuilder +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.local.TapsLocalConfig +import io.embrace.android.embracesdk.config.local.WebViewLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeBreadcrumbBehavior +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.payload.TapBreadcrumb +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.session.EmbraceMemoryCleanerService +import io.mockk.mockk +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.CountDownLatch + +internal class EmbraceBreadcrumbServiceTest { + + private lateinit var configService: ConfigService + private lateinit var activityService: ActivityService + private lateinit var memoryCleanerService: EmbraceMemoryCleanerService + private lateinit var activity: Activity + private val logger = InternalEmbraceLogger() + private val gson = GsonBuilder().setPrettyPrinting().create() + private val clock = FakeClock() + + @Before + fun createMocks() { + configService = FakeConfigService( + breadcrumbBehavior = fakeBreadcrumbBehavior( + localCfg = { + SdkLocalConfig( + taps = TapsLocalConfig(true), + webViewConfig = WebViewLocalConfig(true) + ) + }, + remoteCfg = { + RemoteConfig() + } + ) + ) + activityService = FakeActivityService() + activity = mockk() + memoryCleanerService = EmbraceMemoryCleanerService() + clock.setCurrentTime(MILLIS_FOR_2020_01_01) + clock.tickSecond() + } + + private fun assertEmptyDataToStart(service: EmbraceBreadcrumbService) { + assertTrue( + "stack should be empty to start", + service.fragmentStack.isEmpty(), + ) + assertTrue( + "no breadcrumbs to start", + service.fragmentBreadcrumbs.isEmpty() + ) + } + + private fun assertJsonMessage(service: EmbraceBreadcrumbService, expected: String) { + val message = SessionMessage( + session = fakeSession(), + breadcrumbs = service.getBreadcrumbs( + 0, clock.now() + ) + ) + val jsonMessage = gson.toJson(message) + assertEquals(jsonMessage, expected) + } + + /* + * Views + */ + @Test + fun testViewCreate() { + val service = EmbraceBreadcrumbService( + clock, + configService, + InternalEmbraceLogger() + ) + service.logView("viewA", clock.now()) + clock.tickSecond() + service.logView("viewB", clock.now()) + clock.tickSecond() + service.onViewClose(activity) + val expected = ResourceReader.readResourceAsText("breadcrumb_view.json") + assertJsonMessage(service, expected) + } + + /* + * Web views + */ + @Test + fun testWebViewCreate() { + val service = initializeBreadcrumbService() + clock.tickSecond() + service.logWebView("https://example.com/path1", clock.now()) + clock.tickSecond() + service.logWebView("https://example.com/path2", clock.now()) + val webViews = service.webViewBreadcrumbs + assertEquals("two webviews captured", 2, webViews.size) + val expected = ResourceReader.readResourceAsText("breadcrumb_webview.json") + assertJsonMessage(service, expected) + } + + /* + * Custom breadcrumbs + */ + @Test + fun testBreadcrumbCreate() { + val service = initializeBreadcrumbService() + service.logCustom("a breadcrumb", clock.now()) + val breadcrumbs = service.customBreadcrumbs + assertEquals("one breadcrumb captured", 1, breadcrumbs.size) + val expected = ResourceReader.readResourceAsText("breadcrumb_custom.json") + assertJsonMessage(service, expected) + } + + /* + * Fragments + */ + @Test + fun testFragmentStart() { + val service = initializeBreadcrumbService() + assertTrue(service.fragmentStack.isEmpty()) + assertTrue( + "starting view worked", + service.startView("a") + ) + assertEquals( + "fragment stack has an entry", + 1, + service.fragmentStack.size, + ) + val fragment = service.fragmentStack[0] + assertEquals( + "right view name is captured", + "a", + fragment.name + ) + assertTrue( + "start time should be greater than 2020-01-01", + fragment.getStartTime() > MILLIS_FOR_2020_01_01 + ) + assertEquals("end time is not set", 0L, fragment.endTime) + } + + @Test + fun testFragmentStartWithEnd() { + val service = initializeBreadcrumbService() + assertEmptyDataToStart(service) + assertTrue( + "starting fragment should succeed", + service.startView("a"), + ) + assertEquals( + "should have one entry in the stack", + 1, + service.fragmentStack.size + ) + assertTrue( + "ending fragment should succeed", + service.endView("a") + ) + assertTrue( + "ending fragment should move it from the stack", + service.fragmentStack.isEmpty() + ) + assertEquals( + "fragment should have been moved to the breadcrumb list", + 1, + service.fragmentBreadcrumbs.size + ) + val fragment = checkNotNull(service.fragmentBreadcrumbs.element()) + assertEquals("a", fragment.name) + assertTrue(fragment.endTime > MILLIS_FOR_2020_01_01) + assertTrue(fragment.getStartTime() > MILLIS_FOR_2020_01_01) + assertTrue(fragment.getStartTime() <= fragment.endTime) + assertFalse( + "ending same fragment again should fail", + service.endView("a") + ) + } + + @Test + fun testFragmentStartTooMany() { + val service = initializeBreadcrumbService() + assertEmptyDataToStart(service) + val stackLimit = 20 + repeat(stackLimit) { + assertTrue( + "starting fragment should succeed", + service.startView("a") + ) + } + assertFalse( + "21st starting fragment should fail", + service.startView("a") + ) + assertEquals( + "stack should have max values in it", + stackLimit, + service.fragmentStack.size + ) + + // can add more fragments once one is closed + assertTrue( + "closing a fragment should succeed", + service.endView("a") + ) + assertTrue( + "should be able to start a fragment once we ended one", + service.startView("a") + ) + assertEquals( + "we have closed one fragment", + 1, + service.fragmentBreadcrumbs.size + ) + assertEquals( + "the stack is back full again", + stackLimit, + service.fragmentStack.size + ) + } + + @Test + fun testFragmentEndUnknown() { + val service = initializeBreadcrumbService() + assertEmptyDataToStart(service) + assertTrue( + "starting fragment should succeed", + service.startView("a") + ) + assertEquals( + "one fragment should be on the stack", + 1, + service.fragmentStack.size + ) + assertFalse( + "ending an unknown fragment should fail", + service.endView("b") + ) + assertEquals( + "the opened fragment should be on the stack", + 1, + service.fragmentStack.size + ) + } + + @Test + @Throws(Exception::class) + fun testFragmentAddFromMultipleThreads() { + val service = initializeBreadcrumbService() + assertEmptyDataToStart(service) + val viewNames = + "abcdefghij".split("".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val startSignal = CountDownLatch(1) + val doneSignal = CountDownLatch(viewNames.size) + for (viewName in viewNames) { + // start workers that will all add a fragment each + Thread(AddFragmentWorker(startSignal, doneSignal, service, viewName)).start() + } + startSignal.countDown() + // wait for all the workers to finish + doneSignal.await() + assertTrue( + "there should be no unclosed views", + service.fragmentStack.isEmpty() + ) + val actualViews = ArrayList() + for (fragmentBreadcrumb in service.fragmentBreadcrumbs) { + actualViews.add(checkNotNull(fragmentBreadcrumb).name) + } + actualViews.sort() + assertEquals( + "the expected views were not found", + actualViews, + listOf(*viewNames) + ) + } + + internal inner class AddFragmentWorker( + private val startSignal: CountDownLatch, + private val doneSignal: CountDownLatch, + private val service: BreadcrumbService, + private val viewName: String? + ) : Runnable { + override fun run() { + try { + startSignal.await() + service.startView(viewName) + Thread.sleep((Math.random() * 100).toLong()) + service.endView(viewName) + doneSignal.countDown() + } catch (ex: InterruptedException) { + Assert.fail("worker thread died") + } + } + } + + @Test + fun testFragmentEndOnClose() { + val service = initializeBreadcrumbService() + assertEmptyDataToStart(service) + assertTrue( + "starting fragment should succeed", + service.startView("a") + ) + assertTrue( + "starting fragment should succeed", + service.startView("b") + ) + assertEquals( + "should have a stack with 2 entries", + 2, + service.fragmentStack.size + ) + service.onViewClose(activity) + assertTrue( + "should have an empty stack after activity close", + service.fragmentStack.isEmpty() + ) + assertEquals( + "should have two fragment breadcrumbs", + 2, + service.fragmentBreadcrumbs.size + ) + service.onViewClose(activity) + assertEquals( + "should still have two fragment breadcrumbs", + 2, + service.fragmentBreadcrumbs.size + ) + } + + /* + * All breadcrumbs + */ + @Test + fun testClean() { + // TODO: add data to lists other than just fragments + val service = initializeBreadcrumbService() + assertEmptyDataToStart(service) + assertTrue( + "starting fragment should succeed", + service.startView("a") + ) + assertTrue( + "ending fragment should succeed", + service.endView("a") + ) + assertEquals( + "should have one fragment breadcrumb", + 1, + service.fragmentBreadcrumbs.size + ) + service.cleanCollections() + assertTrue( + "should not have any fragment breadcrumbs", + service.fragmentBreadcrumbs.isEmpty() + ) + } + + // TO DO: refactor BreadCrumbService to avoid accessing internal implementation + @Test + fun testCleanCollections() { + val service = initializeBreadcrumbService() + service.logTap(android.util.Pair(0f, 0f), "MyView", 0, TapBreadcrumb.TapBreadcrumbType.TAP) + service.logRnAction("MyAction", 0, 5, mapOf("key" to "value"), 100, "success") + service.logPushNotification( + "title", + "body", + "topic", + "id", + 5, + 9, + PushNotificationBreadcrumb.NotificationType.NOTIFICATION + ) + service.logView("test", clock.now()) + service.logWebView("https://example.com/path1", clock.now()) + service.logCustom("a breadcrumb", clock.now()) + service.startView("a") + service.endView("a") + + val breadcrumbs = service.getBreadcrumbs(0, clock.now()) + assertEquals(1, breadcrumbs.tapBreadcrumbs?.size) + assertEquals(1, breadcrumbs.rnActionBreadcrumbs?.size) + assertEquals(1, breadcrumbs.pushNotifications?.size) + assertEquals(1, breadcrumbs.viewBreadcrumbs?.size) + assertEquals(1, breadcrumbs.webViewBreadcrumbs?.size) + assertEquals(1, breadcrumbs.customBreadcrumbs?.size) + assertEquals(1, breadcrumbs.fragmentBreadcrumbs?.size) + + service.cleanCollections() + + val breadcrumbsAfterClean = service.getBreadcrumbs(0, clock.now()) + assertEquals(0, breadcrumbsAfterClean.tapBreadcrumbs?.size) + assertEquals(0, breadcrumbsAfterClean.rnActionBreadcrumbs?.size) + assertEquals(0, breadcrumbsAfterClean.pushNotifications?.size) + assertEquals(0, breadcrumbsAfterClean.viewBreadcrumbs?.size) + assertEquals(0, breadcrumbsAfterClean.webViewBreadcrumbs?.size) + assertEquals(0, breadcrumbsAfterClean.customBreadcrumbs?.size) + assertEquals(0, breadcrumbsAfterClean.fragmentBreadcrumbs?.size) + } + @Test + fun testGetBreadcrumbs() { + val service = initializeBreadcrumbService() + assertTrue( + "starting fragment should succeed", + service.startView("a") + ) + clock.tickSecond() + assertTrue( + "ending fragment should succeed", + service.endView("a") + ) + clock.tickSecond() + assertTrue( + "starting fragment should succeed", + service.startView("b") + ) + clock.tickSecond() + service.onViewClose(activity) + val message = SessionMessage( + session = fakeSession(), + breadcrumbs = service.getBreadcrumbs( + 0, clock.now() + ) + ) + val jsonMessage = gson.toJson(message) + val expected = ResourceReader.readResourceAsText("breadcrumb_fragment.json") + assertEquals(expected, jsonMessage) + } + + @Test + fun testFlushBreadcrumbs() { + val service = initializeBreadcrumbService() + service.startView("a") + clock.tickSecond() + service.endView("a") + clock.tickSecond() + service.startView("b") + clock.tickSecond() + service.logCustom("a breadcrumb", clock.now()) + val breadcrumbs = service.customBreadcrumbs + assertEquals("one breadcrumb captured", 1, breadcrumbs.size) + + service.onViewClose(activity) + val message = SessionMessage( + session = fakeSession(), + breadcrumbs = service.flushBreadcrumbs() + ) + val jsonMessage = gson.toJson(message) + val expected = ResourceReader.readResourceAsText("breadcrumb_view_custom.json") + assertEquals(expected, jsonMessage) + + val secondMessage = SessionMessage( + session = fakeSession(), + breadcrumbs = service.flushBreadcrumbs() + ) + val secondJsonMessage = gson.toJson(secondMessage) + val expectedEmpty = ResourceReader.readResourceAsText("breadcrumb_empty.json") + assertEquals(expectedEmpty, secondJsonMessage) + } + + @Test + fun testForceLogView() { + val service = initializeBreadcrumbService() + service.forceLogView("a", 0) + val crumbs = service.getViewBreadcrumbsForSession(0, Long.MAX_VALUE) + val breadcrumb = checkNotNull(crumbs.single()) + assertEquals("a", breadcrumb.screen) + } + + @Test + fun testReplaceFirstSessionView() { + val service = initializeBreadcrumbService() + service.logView("a", 0) + service.replaceFirstSessionView("b", 2) + + val crumbs = service.getViewBreadcrumbsForSession(0, Long.MAX_VALUE) + val breadcrumb = checkNotNull(crumbs.single()) + assertEquals("b", breadcrumb.screen) + } + + @Test + fun testLogTap() { + val service = initializeBreadcrumbService() + service.logTap(android.util.Pair(0f, 0f), "MyView", 0, TapBreadcrumb.TapBreadcrumbType.TAP) + + val crumbs = service.getTapBreadcrumbsForSession(0, Long.MAX_VALUE) + val breadcrumb = checkNotNull(crumbs.single()) + assertEquals("MyView", breadcrumb.tappedElementName) + assertEquals(TapBreadcrumb.TapBreadcrumbType.TAP, breadcrumb.type) + } + + @Test + fun testLogRnAction() { + val service = initializeBreadcrumbService() + service.logRnAction("MyAction", 0, 5, mapOf("key" to "value"), 100, "success") + + val crumbs = service.getRnActionBreadcrumbForSession(0, Long.MAX_VALUE) + val breadcrumb = checkNotNull(crumbs.single()) + assertEquals("MyAction", breadcrumb.name) + assertEquals("success", breadcrumb.output) + assertEquals(100, breadcrumb.bytesSent) + assertEquals(mapOf("key" to "value"), breadcrumb.properties) + } + + @Test + fun testGetLastViewBreadcrumbScreenName() { + val service = initializeBreadcrumbService() + assertNull(service.getLastViewBreadcrumbScreenName()) + service.logView("test", 0) + assertEquals("test", service.getLastViewBreadcrumbScreenName()) + } + + @Test + fun testLogPushNotification() { + val service = initializeBreadcrumbService() + service.logPushNotification( + "title", + "body", + "topic", + "id", + 5, + 9, + PushNotificationBreadcrumb.NotificationType.NOTIFICATION + ) + + val crumbs = service.getPushNotificationsBreadcrumbsForSession(0, Long.MAX_VALUE) + val breadcrumb = checkNotNull(crumbs.single()) + assertNull(breadcrumb.title) + assertNull(breadcrumb.body) + assertNull(breadcrumb.from) + assertEquals("id", breadcrumb.id) + assertEquals(5, breadcrumb.priority) + } + + @Test + fun testOnViewEnabled() { + val service = initializeBreadcrumbService() + service.onView(mockk(relaxed = true)) + + val crumbs = service.getViewBreadcrumbsForSession(0, Long.MAX_VALUE) + val breadcrumb = checkNotNull(crumbs.single()) + assertEquals("android.app.Activity", breadcrumb.screen) + } + + @Test + fun testBreadcrumbLimitExceeded() { + val service = initializeBreadcrumbService() + repeat(110) { count -> + service.logView("a$count", count.toLong()) + } + + val crumbs = service.getViewBreadcrumbsForSession(0, Long.MAX_VALUE) + assertEquals(100, crumbs.size) + assertEquals("a109", crumbs.first()?.screen) + assertEquals("a10", crumbs.last()?.screen) + } + + private fun initializeBreadcrumbService() = EmbraceBreadcrumbService( + clock, + configService, + logger + ) + + companion object { + private const val MILLIS_FOR_2020_01_01 = 1577836800000L + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureServiceTest.kt new file mode 100644 index 0000000000..010ae12966 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crumbs/PushNotificationCaptureServiceTest.kt @@ -0,0 +1,206 @@ +package io.embrace.android.embracesdk.capture.crumbs + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.PushNotificationBreadcrumb +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +internal class PushNotificationCaptureServiceTest { + + private lateinit var pushNotificationCaptureService: PushNotificationCaptureService + + companion object { + private val mockBreadCrumbService: BreadcrumbService = mockk(relaxUnitFun = true) + private val logger: InternalEmbraceLogger = InternalEmbraceLogger() + private val mockBundle: Bundle = mockk() + private val mockIntent: Intent = mockk { + every { extras } returns mockBundle + } + private val mockActivity: Activity = mockk { + every { intent } returns mockIntent + } + } + + @Before + fun before() { + clearAllMocks(answers = false) + // for bundle let's clear answers too + clearMocks(mockBundle) + + pushNotificationCaptureService = PushNotificationCaptureService( + mockBreadCrumbService, + logger + ) + } + + @Test + fun `verify log push notification successfully`() { + val title = "title" + val body = "body" + val from = "from" + val id = "id" + val notificationPriority = 1 + val messageDeliveredPriority = 1 + val type = PushNotificationBreadcrumb.NotificationType.NOTIFICATION + + pushNotificationCaptureService.logPushNotification( + title, + body, + from, + id, + notificationPriority, + messageDeliveredPriority, + type + ) + + verify { + mockBreadCrumbService.logPushNotification( + title, + body, + from, + id, + notificationPriority, + messageDeliveredPriority, + type + ) + } + } + + @Test + fun `verify onActivityCreated for a push notification intent with no user data`() { + // these are needed so to identify this is a push notification + val fromKey = "from" + val messageIdKey = "google.message_id" + val deliveredPriorityKey = "google.delivered_priority" + + val bundleData = mapOf( + fromKey to "123", + messageIdKey to "456", + deliveredPriorityKey to "normal" + ) + every { mockBundle.keySet() } returns bundleData.keys + every { mockBundle.getString(fromKey) } returns bundleData[fromKey] + every { mockBundle.getString(messageIdKey) } returns bundleData[messageIdKey] + every { mockBundle.getString(deliveredPriorityKey) } returns bundleData[deliveredPriorityKey] + + pushNotificationCaptureService.onActivityCreated(mockActivity, mockBundle) + + verify { + mockBreadCrumbService.logPushNotification( + null, + null, + "123", + "456", + null, + // 2 is normal priority + 2, + PushNotificationBreadcrumb.NotificationType.NOTIFICATION + ) + } + } + + @Test + fun `verify onActivityCreated for a push notification intent with user data`() { + // these are needed so to identify this is a push notification + val fromKey = "from" + val messageIdKey = "google.message_id" + val deliveredPriorityKey = "google.delivered_priority" + val userDefinedKey = "user-defined" + + val bundleData = mapOf( + fromKey to "123", + messageIdKey to "456", + deliveredPriorityKey to "normal", + userDefinedKey to "custom-value" + ) + every { mockBundle.keySet() } returns bundleData.keys + every { mockBundle.getString(fromKey) } returns bundleData[fromKey] + every { mockBundle.getString(messageIdKey) } returns bundleData[messageIdKey] + every { mockBundle.getString(deliveredPriorityKey) } returns bundleData[deliveredPriorityKey] + every { mockBundle.getString(userDefinedKey) } returns bundleData[userDefinedKey] + + pushNotificationCaptureService.onActivityCreated(mockActivity, mockBundle) + + verify { + mockBreadCrumbService.logPushNotification( + null, + null, + "123", + "456", + null, + // 2 is normal priority + 2, + PushNotificationBreadcrumb.NotificationType.NOTIFICATION_AND_DATA + ) + } + } + + @Test + fun `verify onActivityCreated that is not coming from push notification`() { + // empty key set so this is not a push notification intent + every { mockBundle.keySet() } returns emptySet() + + pushNotificationCaptureService.onActivityCreated(mockActivity, mockBundle) + + verify { mockBreadCrumbService wasNot Called } + } + + @Test + fun `verify get message priority`() { + // if null return 0 (unknown) + Assert.assertEquals(0, PushNotificationCaptureService.getMessagePriority(null)) + + // if high return 1 (high) + Assert.assertEquals(1, PushNotificationCaptureService.getMessagePriority("high")) + + // if normal return 2 (normal) + Assert.assertEquals(2, PushNotificationCaptureService.getMessagePriority("normal")) + + // if any other thing return 0 (unknown) + Assert.assertEquals(0, PushNotificationCaptureService.getMessagePriority("whatever")) + } + + @Test + fun `verify extract user defined data from bundle`() { + // if empty bundle it should return empty map + every { mockBundle.keySet() } returns emptySet() + Assert.assertTrue( + PushNotificationCaptureService.extractDeveloperDefinedPayload(mockBundle).isEmpty() + ) + + // if only reserved words it should return empty map + every { mockBundle.keySet() } returns setOf( + "google.key", "gcm.key", "from", "message_type", "collapse_key" + ) + Assert.assertTrue( + PushNotificationCaptureService.extractDeveloperDefinedPayload(mockBundle).isEmpty() + ) + + // reset mockBundle + clearMocks(mockBundle) + + // if reserved words plus user defined keys it should return user defined keys only + every { mockBundle.keySet() } returns setOf( + "google.key", "gcm.key", "from", "message_type", "collapse_key", "user_defined_key1", + "user_defined_key2" + ) + every { mockBundle.getString("user_defined_key1") } returns "value1" + every { mockBundle.getString("user_defined_key2") } returns "value2" + val userDefinedMap = + PushNotificationCaptureService.extractDeveloperDefinedPayload(mockBundle) + + Assert.assertEquals(2, userDefinedMap.size) + Assert.assertEquals("value1", userDefinedMap["user_defined_key1"]) + Assert.assertEquals("value2", userDefinedMap["user_defined_key2"]) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataReactNativeTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataReactNativeTest.kt new file mode 100644 index 0000000000..4950c03ced --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataReactNativeTest.kt @@ -0,0 +1,154 @@ +package io.embrace.android.embracesdk.capture.metadata + +import android.content.Context +import android.content.res.AssetManager +import android.view.WindowManager +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.capture.cpu.EmbraceCpuInfoDelegate +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeDeviceArchitecture +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.internal.BuildInfo +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.ActivityService +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.io.FileInputStream +import java.io.IOException +import java.nio.file.Files + +internal class EmbraceMetadataReactNativeTest { + + private val fakeClock = FakeClock() + private lateinit var context: Context + private lateinit var assetManager: AssetManager + private lateinit var buildInfo: BuildInfo + private lateinit var configService: ConfigService + private var appFramework: Embrace.AppFramework = Embrace.AppFramework.REACT_NATIVE + private lateinit var preferencesService: PreferencesService + private lateinit var activityService: ActivityService + private lateinit var cpuInfoDelegate: EmbraceCpuInfoDelegate + private lateinit var mockSharedObjectLoader: SharedObjectLoader + private val deviceArchitecture = FakeDeviceArchitecture() + + @Before + fun setUp() { + context = mockk(relaxed = true) { + every { packageName } returns "package-name" + every { getSystemService("window") } returns mockk(relaxed = true) + } + assetManager = mockk(relaxed = true) + buildInfo = BuildInfo("device-id", null, null) + configService = FakeConfigService() + preferencesService = FakePreferenceService() + activityService = FakeActivityService() + preferencesService.javaScriptBundleURL = null + preferencesService.javaScriptPatchNumber = "patch-number" + preferencesService.reactNativeVersionNumber = "rn-version-number" + mockSharedObjectLoader = mockk(relaxed = true) + cpuInfoDelegate = EmbraceCpuInfoDelegate(mockSharedObjectLoader, InternalEmbraceLogger()) + } + + private fun getMetadataService() = EmbraceMetadataService.ofContext( + context, + buildInfo, + configService, + appFramework, + preferencesService, + activityService, + MoreExecutors.newDirectExecutorService(), + mockk(), + mockk(), + mockk(), + fakeClock, + cpuInfoDelegate, + deviceArchitecture + ) + + @Test + fun `test React Native bundle ID setting as a default value`() { + val metadataService = getMetadataService() + assertEquals(buildInfo.buildId, metadataService.getReactNativeBundleId()) + } + + @Test + fun `test React Native bundle ID setting as a default value if jsBundleIdUrl is empty`() { + val metadataService = getMetadataService() + metadataService.setReactNativeBundleId(context, "") + assertEquals(buildInfo.buildId, metadataService.getReactNativeBundleId()) + } + + @Test + fun `test React Native bundle ID from preference if jsBundleIdUrl is the same as the value persisted `() { + preferencesService.javaScriptBundleURL = "javaScriptBundleURL" + val metadataService = getMetadataService() + + metadataService.setReactNativeBundleId(context, "javaScriptBundleURL") + assertEquals(buildInfo.buildId, metadataService.getReactNativeBundleId()) + } + + @Test + fun `test React Native bundle ID url as Asset`() { + val bundleIdFile = Files.createTempFile("bundle-test", ".temp").toFile() + val inputStream = FileInputStream(bundleIdFile) + preferencesService.javaScriptBundleURL = null + + every { context.assets } returns assetManager + every { assetManager.open(any()) } returns inputStream + + val metadataService = getMetadataService() + metadataService.setReactNativeBundleId(context, "assets://index.android.bundle") + // get the react native Bundle ID once to call the lazy property + metadataService.getReactNativeBundleId() + + verify(exactly = 1) { assetManager.open(eq("index.android.bundle")) } + + Assert.assertNotEquals(buildInfo.buildId, metadataService.getReactNativeBundleId()) + assertEquals("D41D8CD98F00B204E9800998ECF8427E", metadataService.getReactNativeBundleId()) + } + + @Test + fun `test React Native bundle ID url as a custom file`() { + val bundleIdFile = Files.createTempFile("index.android.bundle", "temp").toFile() + val metadataService = getMetadataService() + metadataService.setReactNativeBundleId( + context, + bundleIdFile.absolutePath + ) + // get the react native Bundle ID once to call the lazy property + metadataService.getReactNativeBundleId() + + Assert.assertNotEquals(buildInfo.buildId, metadataService.getReactNativeBundleId()) + assertEquals("D41D8CD98F00B204E9800998ECF8427E", metadataService.getReactNativeBundleId()) + } + + @Test + fun `test computeReactNativeBundleId with wrong assets path`() { + preferencesService.javaScriptBundleURL = "assets" + every { context.assets } returns assetManager + every { assetManager.open(any()) } throws IOException() + + // computing is null, so reactNativeBundleID should be set to the default value + val metadataService = getMetadataService() + assertEquals(metadataService.getReactNativeBundleId(), buildInfo.buildId) + } + + @Test + fun `test computeReactNativeBundleId with wrong custom bundle stream`() { + preferencesService.javaScriptBundleURL = "wrongFilePath" + + // computing is null, so reactNativeBundleID should be set to the default value + assertEquals(getMetadataService().getReactNativeBundleId(), buildInfo.buildId) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataServiceTest.kt new file mode 100644 index 0000000000..9357de9aac --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataServiceTest.kt @@ -0,0 +1,364 @@ +package io.embrace.android.embracesdk.capture.metadata + +import android.app.usage.StorageStatsManager +import android.content.Context +import android.content.pm.PackageInfo +import android.os.Environment +import android.view.WindowManager +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.capture.cpu.EmbraceCpuInfoDelegate +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeDeviceArchitecture +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import io.embrace.android.embracesdk.fakes.fakeSdkModeBehavior +import io.embrace.android.embracesdk.internal.BuildInfo +import io.embrace.android.embracesdk.prefs.EmbracePreferencesService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.io.File + +internal class EmbraceMetadataServiceTest { + + companion object { + private val context: Context = mockk(relaxed = true) + private val preferencesService: EmbracePreferencesService = mockk(relaxed = true) + private val fakeClock = FakeClock() + private val cpuInfoDelegate: EmbraceCpuInfoDelegate = mockk(relaxed = true) + private val fakeArchitecture = FakeDeviceArchitecture() + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(MetadataUtils::class) + mockkStatic(Environment::class) + + initContext() + initPreferences() + + every { Environment.getDataDirectory() }.returns(File("ANDROID_DATA")) + every { MetadataUtils.getInternalStorageFreeCapacity(any()) }.returns(123L) + } + + @After + fun tearDown() { + unmockkAll() + } + + private fun initContext() { + val packageInfo = PackageInfo() + packageInfo.versionName = "1.0.0" + @Suppress("DEPRECATION") + packageInfo.versionCode = 10 + + every { context.getSystemService(Context.WINDOW_SERVICE) }.returns(mockk()) + every { context.getSystemService(Context.STORAGE_STATS_SERVICE) }.returns(mockk()) + every { context.packageName }.returns("package-info") + every { context.packageManager.getPackageInfo("package-info", 0) }.returns(packageInfo) + } + + private fun initPreferences() { + every { preferencesService.appVersion }.returns("app-version") + every { preferencesService.osVersion }.returns("os-version") + + // to test Device Info: + every { MetadataUtils.isJailbroken() }.returns(true) + every { preferencesService.jailbroken }.returns(true) + every { preferencesService.screenResolution }.returns("200x300") + } + } + + private val buildInfo: BuildInfo = BuildInfo("1234", "debug", "free") + private val activityService = FakeActivityService() + private val configService: ConfigService = + FakeConfigService( + autoDataCaptureBehavior = fakeAutoDataCaptureBehavior( + localCfg = { + LocalConfig("appId", true, SdkLocalConfig()) + } + ), + sdkModeBehavior = fakeSdkModeBehavior( + localCfg = { + LocalConfig("appId", false, SdkLocalConfig()) + } + ) + ) + + @Before + fun setUp() { + clearAllMocks( + answers = false, + objectMocks = false, + constructorMocks = false, + staticMocks = false + ) + } + + private fun getMetadataService(framework: Embrace.AppFramework = Embrace.AppFramework.NATIVE) = + EmbraceMetadataService.ofContext( + context, + buildInfo, + configService, + framework, + preferencesService, + activityService, + MoreExecutors.newDirectExecutorService(), + mockk(), + mockk(), + mockk(), + fakeClock, + cpuInfoDelegate, + fakeArchitecture + ).apply { precomputeValues() } + + private fun getReactNativeMetadataService() = + EmbraceMetadataService.ofContext( + context, + buildInfo, + configService, + Embrace.AppFramework.REACT_NATIVE, + preferencesService, + activityService, + MoreExecutors.newDirectExecutorService(), + mockk(), + mockk(), + mockk(), + fakeClock, + cpuInfoDelegate, + fakeArchitecture + ) + + @Test + @Throws(InterruptedException::class) + fun `test EmbraceMetadataService creation loads AppVersion lazily`() { + verify(exactly = 0) { preferencesService.appVersion } + getMetadataService().isAppUpdated() + verify(exactly = 1) { preferencesService.appVersion } + } + + @Test + @Throws(InterruptedException::class) + fun `test EmbraceMetadataService creation loads OsVersion lazily`() { + verify(exactly = 0) { preferencesService.osVersion } + getMetadataService().isOsUpdated() + verify(exactly = 1) { preferencesService.osVersion } + } + + @Test + @Throws(InterruptedException::class) + fun `test EmbraceMetadataService creation loads DeviceIdentifier lazily`() { + every { preferencesService.deviceIdentifier }.returns("device-id") + + verify(exactly = 0) { preferencesService.deviceIdentifier } + getMetadataService().getDeviceId() + verify(exactly = 1) { preferencesService.deviceIdentifier } + } + + @Test + fun `test app info`() { + every { preferencesService.appVersion }.returns(null) + every { preferencesService.osVersion }.returns(null) + every { preferencesService.unityVersionNumber }.returns(null) + every { preferencesService.unityBuildIdNumber }.returns(null) + + every { MetadataUtils.appEnvironment(any()) }.returns("UNKNOWN") + + val expectedInfo = ResourceReader.readResourceAsText("metadata_appinfo_expected.json") + .replace("{versionName}", BuildConfig.VERSION_NAME) + .replace("{versionCode}", BuildConfig.VERSION_CODE.toString()) + .filter { !it.isWhitespace() } + + val appInfo = getMetadataService().getAppInfo().toJson() + assertEquals(expectedInfo, appInfo.replace(" ", "")) + } + + @Test + fun `test react native app info`() { + every { preferencesService.appVersion }.returns(null) + every { preferencesService.osVersion }.returns(null) + every { preferencesService.unityVersionNumber }.returns(null) + every { preferencesService.unityBuildIdNumber }.returns(null) + every { preferencesService.rnSdkVersion }.returns(null) + every { preferencesService.javaScriptPatchNumber }.returns(null) + every { MetadataUtils.appEnvironment(any()) }.returns("UNKNOWN") + + val expectedInfo = + ResourceReader.readResourceAsText("metadata_react_native_appinfo_expected.json") + .replace("{versionName}", BuildConfig.VERSION_NAME) + .replace("{versionCode}", BuildConfig.VERSION_CODE) + .filter { !it.isWhitespace() } + + val metadataService = getReactNativeMetadataService() + + metadataService.setReactNativeBundleId(context, "1234") + + val appInfo = metadataService.getAppInfo().toJson() + assertEquals(expectedInfo, appInfo.replace(" ", "")) + } + + @Test + fun `test startup complete`() { + every { preferencesService.installDate }.returns(null) + getMetadataService().applicationStartupComplete() + + verify(exactly = 1) { preferencesService.appVersion = any() } + verify(exactly = 1) { preferencesService.osVersion = any() } + verify(exactly = 1) { preferencesService.deviceIdentifier = any() } + verify(exactly = 1) { preferencesService.installDate = any() } + } + + @Test + fun `test startup complete if it is not the first time`() { + every { preferencesService.installDate }.returns(1234L) + getMetadataService().applicationStartupComplete() + verify(exactly = 0) { preferencesService.installDate = any() } + } + + @Test + fun `test device info`() { + every { Environment.getDataDirectory() }.returns(File("ANDROID_DATA")) + every { MetadataUtils.getInternalStorageTotalCapacity(any()) }.returns(123L) + every { MetadataUtils.getLocale() }.returns("en-US") + every { MetadataUtils.getSystemUptime() }.returns(123L) + + val deviceInfo = getMetadataService().getDeviceInfo().toJson() + + verify(exactly = 1) { MetadataUtils.getDeviceManufacturer() } + verify(exactly = 1) { MetadataUtils.getModel() } + verify(exactly = 1) { MetadataUtils.getLocale() } + verify(exactly = 1) { MetadataUtils.getInternalStorageTotalCapacity(any()) } + verify(exactly = 1) { MetadataUtils.getOperatingSystemType() } + verify(exactly = 1) { MetadataUtils.getOperatingSystemVersion() } + verify(exactly = 1) { MetadataUtils.getOperatingSystemVersionCode() } + verify(exactly = 1) { MetadataUtils.getTimezoneId() } + verify(exactly = 1) { MetadataUtils.getSystemUptime() } + verify(exactly = 1) { MetadataUtils.getNumberOfCores() } + + assertTrue(deviceInfo.contains("\"jb\":true")) + assertTrue(deviceInfo.contains("\"sr\":\"200x300\"")) + assertTrue(deviceInfo.contains("\"da\":\"arm64-v8a\"")) + } + + @Test + fun `test device info without running async operations`() { + every { Environment.getDataDirectory() }.returns(File("ANDROID_DATA")) + every { MetadataUtils.getInternalStorageTotalCapacity(any()) }.returns(123L) + every { MetadataUtils.getLocale() }.returns("en-US") + every { MetadataUtils.getSystemUptime() }.returns(123L) + + val metadataService = EmbraceMetadataService.ofContext( + context, + buildInfo, + configService, + Embrace.AppFramework.NATIVE, + preferencesService, + activityService, + mockk(relaxed = true), // No background worker to run async calculations + mockk(), + mockk(), + mockk(), + fakeClock, + cpuInfoDelegate, + fakeArchitecture + ) + + val deviceInfo = metadataService.getDeviceInfo().toJson() + + verify(exactly = 1) { MetadataUtils.getDeviceManufacturer() } + verify(exactly = 1) { MetadataUtils.getModel() } + verify(exactly = 1) { MetadataUtils.getLocale() } + verify(exactly = 1) { MetadataUtils.getInternalStorageTotalCapacity(any()) } + verify(exactly = 1) { MetadataUtils.getOperatingSystemType() } + verify(exactly = 1) { MetadataUtils.getOperatingSystemVersion() } + verify(exactly = 1) { MetadataUtils.getOperatingSystemVersionCode() } + verify(exactly = 1) { MetadataUtils.getTimezoneId() } + verify(exactly = 1) { MetadataUtils.getSystemUptime() } + + assertTrue(deviceInfo.contains("\"jb\":null")) + assertTrue(deviceInfo.contains("\"sr\":null")) + assertTrue(deviceInfo.contains("\"da\":\"arm64-v8a\"")) + } + + @Test + fun `test public methods`() { + val metadataService = getMetadataService() + + activityService.isInBackground = true + assertEquals("background", metadataService.getAppState()) + + activityService.isInBackground = false + assertEquals("active", metadataService.getAppState()) + + metadataService.setActiveSessionId("123") + assertEquals("123", metadataService.activeSessionId) + + assertEquals("appId", metadataService.getAppId()) + assertEquals("10", metadataService.getAppVersionCode()) + } + + @Test + fun `test flutter APIs`() { + val metadataService = getMetadataService(Embrace.AppFramework.FLUTTER) + metadataService.setEmbraceFlutterSdkVersion("1.1.0") + metadataService.setDartVersion("2.19.1") + verify(exactly = 1) { preferencesService.dartSdkVersion = "2.19.1" } + verify(exactly = 1) { preferencesService.embraceFlutterSdkVersion = "1.1.0" } + + val appInfo = metadataService.getAppInfo() + assertEquals("1.1.0", appInfo.hostedSdkVersion) + assertEquals("2.19.1", appInfo.hostedPlatformVersion) + } + + @Test + fun `test flutter API defaults to preferenceService`() { + val metadataService = getMetadataService(Embrace.AppFramework.FLUTTER) + every { preferencesService.dartSdkVersion }.returns("2.17.1") + every { preferencesService.embraceFlutterSdkVersion }.returns("1.0.0") + val defaultInfo = metadataService.getAppInfo() + + assertEquals("1.0.0", defaultInfo.hostedSdkVersion) + assertEquals("2.17.1", defaultInfo.hostedPlatformVersion) + } + + @Test + fun `test disk usage`() { + every { + MetadataUtils.getDeviceDiskAppUsage(any(), any(), any()) + }.returns(123L) + + val service = getMetadataService() + service.asyncRetrieveDiskUsage(true) + + assertEquals(123L, service.getDiskUsage()?.appDiskUsage) + } + + @Test + fun `test async additional device info`() { + every { preferencesService.cpuName } returns null + every { preferencesService.egl } returns null + every { cpuInfoDelegate.getCpuName() } returns "cpu" + every { cpuInfoDelegate.getElg() } returns "egl" + + val metadataService = getMetadataService() + + assertEquals("cpu", metadataService.getCpuName()) + assertEquals("egl", metadataService.getEgl()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataUnityTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataUnityTest.kt new file mode 100644 index 0000000000..a602bc1089 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/metadata/EmbraceMetadataUnityTest.kt @@ -0,0 +1,143 @@ +package io.embrace.android.embracesdk.capture.metadata + +import android.app.usage.StorageStatsManager +import android.content.Context +import android.content.pm.PackageInfo +import android.os.Environment +import android.view.WindowManager +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.capture.cpu.EmbraceCpuInfoDelegate +import io.embrace.android.embracesdk.comms.delivery.EmbraceCacheService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeDeviceArchitecture +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.internal.BuildInfo +import io.embrace.android.embracesdk.session.ActivityService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.AfterClass +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +internal class EmbraceMetadataUnityTest { + + companion object { + private val fakeClock = FakeClock() + private lateinit var context: Context + private lateinit var buildInfo: BuildInfo + private lateinit var configService: ConfigService + private lateinit var preferencesService: FakePreferenceService + private lateinit var activityService: ActivityService + private lateinit var cacheService: EmbraceCacheService + private lateinit var cpuInfoDelegate: EmbraceCpuInfoDelegate + private val deviceArchitecture = FakeDeviceArchitecture() + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(SdkLocalConfig::class) + mockkStatic(MetadataUtils::class) + mockkStatic(Environment::class) + + context = mockk(relaxed = true) + buildInfo = mockk() + configService = mockk(relaxed = true) + preferencesService = FakePreferenceService() + activityService = FakeActivityService() + cacheService = mockk() + cpuInfoDelegate = mockk(relaxed = true) + + initContext() + initPreferences() + } + + @AfterClass + fun tearDown() { + unmockkAll() + } + + private fun initContext() { + val packageInfo = PackageInfo() + packageInfo.versionName = "1.0.0" + @Suppress("DEPRECATION") + packageInfo.versionCode = 10 + + every { context.getSystemService(Context.WINDOW_SERVICE) }.returns(mockk()) + every { context.getSystemService(Context.STORAGE_STATS_SERVICE) }.returns(mockk()) + every { context.packageName }.returns("package-info") + every { context.packageManager.getPackageInfo("package-info", 0) }.returns(packageInfo) + } + + private fun initPreferences() { + every { buildInfo.buildId }.returns("1234") + every { buildInfo.buildType }.returns("debug") + every { buildInfo.buildFlavor }.returns("debug") + + preferencesService.appVersion = "app-version" + preferencesService.osVersion = "os-version" + } + } + + @Before + fun setUp() { + clearAllMocks( + answers = false, + objectMocks = false, + constructorMocks = false, + staticMocks = false + ) + } + + private fun getMetadataService() = EmbraceMetadataService.ofContext( + context, + buildInfo, + configService, + Embrace.AppFramework.UNITY, + preferencesService, + activityService, + MoreExecutors.newDirectExecutorService(), + mockk(), + mockk(), + mockk(), + fakeClock, + cpuInfoDelegate, + deviceArchitecture + ) + + @Test + fun `test unity framework`() { + preferencesService.unityVersionNumber = "unityVersionNumber" + preferencesService.unityBuildIdNumber = "unityBuildIdNumber" + + val metadataService = getMetadataService() + val appInfo = metadataService.getAppInfo().toJson() + + assertTrue(appInfo.contains("\"unv\":\"unityVersionNumber\"")) + assertTrue(appInfo.contains("\"ubg\":\"unityBuildIdNumber\"")) + } + + @Test + fun `test preferences null at beginning`() { + preferencesService.unityVersionNumber = null + preferencesService.unityBuildIdNumber = null + + val metadataService = getMetadataService() + + preferencesService.unityVersionNumber = "unityVersionNumber" + preferencesService.unityBuildIdNumber = "unityBuildIdNumber" + + val appInfo = metadataService.getAppInfo().toJson() + + assertTrue(appInfo.contains("\"unv\":\"unityVersionNumber\"")) + assertTrue(appInfo.contains("\"ubg\":\"unityBuildIdNumber\"")) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiClientTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiClientTest.kt new file mode 100644 index 0000000000..dff8c817c0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiClientTest.kt @@ -0,0 +1,205 @@ +package io.embrace.android.embracesdk.comms.api + +import com.google.gson.Gson +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPInputStream + +/** + * Runs a [MockWebServer] and asserts against our network code to ensure that it + * robustly handles various scenarios. + */ +internal class ApiClientTest { + + private lateinit var request: ApiRequest + private lateinit var apiClient: ApiClient + private lateinit var server: MockWebServer + private lateinit var baseUrl: String + + @Before + fun setUp() { + apiClient = ApiClient( + InternalEmbraceLogger() + ) + + // create mock web server + server = MockWebServer() + server.start() + baseUrl = server.url("test").toString() + request = ApiRequest(url = EmbraceUrl.getUrl(baseUrl)) + } + + @After + fun tearDown() { + server.shutdown() + } + + @Test(expected = RuntimeException::class) + fun testUnreachableHost() { + // attempt some unreachable port + val request = ApiRequest(url = EmbraceUrl.getUrl("http://localhost:1565")) + apiClient.post(request, "Hello world".toByteArray()) + } + + @Test + fun test200ResponseUncompressed() { + server.enqueue(MockResponse().setBody(DEFAULT_RESPONSE_BODY)) + val result = apiClient.rawPost(request, DEFAULT_REQUEST_BODY.toByteArray()) + + // assert on result parsed by ApiClient + Assert.assertEquals(DEFAULT_RESPONSE_BODY, result) + + // assert on request received by mock server + val delivered = server.takeRequest() + assertRequestContents(delivered) + Assert.assertEquals(DEFAULT_REQUEST_BODY, delivered.body.readUtf8()) + } + + @Test + fun test200ResponseCompressed() { + server.enqueue(MockResponse().setBody(DEFAULT_RESPONSE_BODY)) + val result = apiClient.post(request, DEFAULT_REQUEST_BODY.toByteArray()) + + // assert on result parsed by ApiClient + Assert.assertEquals(DEFAULT_RESPONSE_BODY, result) + + // assert on request received by mock server + val delivered = server.takeRequest() + assertRequestContents(delivered) + Assert.assertEquals(DEFAULT_REQUEST_BODY, delivered.readCompressedRequestBody()) + } + + @Test(expected = RuntimeException::class) + fun test400Response() { + server.enqueue(MockResponse().setBody(DEFAULT_RESPONSE_BODY).setResponseCode(400)) + apiClient.rawPost(request, DEFAULT_REQUEST_BODY.toByteArray()) + } + + @Test(expected = RuntimeException::class) + fun test500Response() { + server.enqueue(MockResponse().setBody(DEFAULT_RESPONSE_BODY).setResponseCode(500)) + apiClient.rawPost(request, DEFAULT_REQUEST_BODY.toByteArray()) + } + + @Test(expected = RuntimeException::class) + fun testClientSideConnectionTimeout() { + apiClient.timeoutMs = 1000 + apiClient.rawPost(request, DEFAULT_REQUEST_BODY.toByteArray()) + } + + /** + * Sends a large session payload & verifies that the server receives the expected JSON. The + * large payload helps assert that buffering & GZIP compression work as expected. + */ + @Test + fun testSendLargePayload() { + server.enqueue(MockResponse().setBody(DEFAULT_RESPONSE_BODY)) + + val payload = createLargeSessionPayload() + val result = apiClient.post(request, payload.toByteArray()) + + // assert on result parsed by ApiClient + Assert.assertEquals(DEFAULT_RESPONSE_BODY, result) + + // assert on request received by mock server + val delivered = server.takeRequest() + val observed = delivered.readCompressedRequestBody() + Assert.assertEquals(payload, observed) + } + + /** + * Simulates an I/O exception midway through a request. + */ + @Test(expected = RuntimeException::class) + fun testIoExceptionMidRequest() { + server.enqueue(MockResponse().throttleBody(1, 1000, TimeUnit.MILLISECONDS)) + + // shutdown the server mid-request + Executors.newSingleThreadScheduledExecutor().schedule({ + server.shutdown() + }, 25, TimeUnit.MILLISECONDS) + + // fire off the api request + apiClient.rawPost(request, DEFAULT_REQUEST_BODY.toByteArray()) + } + + @Test + fun testAllRequestHeadersSet() { + request = ApiRequest( + "application/json", + "Embrace/a/1", + "application/json", + "application/json", + "application/json", + "abcde", + "test_did", + "test_eid", + "test_lid", + EmbraceUrl.getUrl(baseUrl) + ) + server.enqueue(MockResponse().setBody(DEFAULT_RESPONSE_BODY)) + apiClient.rawPost(request, DEFAULT_REQUEST_BODY.toByteArray()) + + // assert all request headers were set + val delivered = server.takeRequest() + val headers = delivered.headers.toMap() + .minus("Host") + Assert.assertEquals( + mapOf( + "Accept" to "application/json", + "User-Agent" to "Embrace/a/1", + "Content-Type" to "application/json", + "Content-Encoding" to "application/json", + "Accept-Encoding" to "application/json", + "Connection" to "keep-alive", + "Content-Length" to "${delivered.bodySize}", + "X-EM-AID" to "abcde", + "X-EM-DID" to "test_did", + "X-EM-SID" to "test_eid", + "X-EM-LID" to "test_lid" + ), + headers + ) + } + + private fun RecordedRequest.readCompressedRequestBody(): String { + val inputStream = body.inputStream() + return GZIPInputStream(inputStream).bufferedReader().readText() + } + + private fun assertRequestContents(delivered: RecordedRequest) { + Assert.assertEquals("POST", delivered.method) + Assert.assertEquals("/test", delivered.path) + val headers = delivered.headers.toMap() + .minus("Host") + Assert.assertEquals( + mapOf( + "Accept" to "application/json", + "User-Agent" to "Embrace/a/${BuildConfig.VERSION_NAME}", + "Content-Type" to "application/json", + "Connection" to "keep-alive", + "Content-Length" to "${delivered.bodySize}", + ), + headers + ) + } + + private fun createLargeSessionPayload(): String { + val props = (1..5000).associate { "my_big_key_$it" to "my_big_val_$it" } + val session = fakeSession().copy(properties = props) + return Gson().toJson(session) + } +} + +private const val DEFAULT_RESPONSE_BODY = "{}" +private const val DEFAULT_REQUEST_BODY = "{}" diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiRequestTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiRequestTest.kt new file mode 100644 index 0000000000..f127bfc9f1 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/ApiRequestTest.kt @@ -0,0 +1,95 @@ +package io.embrace.android.embracesdk.comms.api + +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.network.http.HttpMethod +import org.junit.Assert +import org.junit.Test + +internal class ApiRequestTest { + + private val serializer = EmbraceSerializer() + + private val request = ApiRequest( + "application/json", + "Embrace/a/1", + "application/json", + "application/json", + "application/json", + "abcde", + "test_did", + "test_eid", + "test_lid", + EmbraceUrl.getUrl("https://google.com"), + HttpMethod.GET, + "d800f828fec4409dcabc7f5252e7ce71" + ) + + @Test + fun testFullHeaders() { + Assert.assertEquals( + mapOf( + "Accept" to "application/json", + "User-Agent" to "Embrace/a/1", + "Content-Type" to "application/json", + "Content-Encoding" to "application/json", + "Accept-Encoding" to "application/json", + "X-EM-AID" to "abcde", + "X-EM-DID" to "test_did", + "X-EM-SID" to "test_eid", + "X-EM-LID" to "test_lid", + "If-None-Match" to "d800f828fec4409dcabc7f5252e7ce71" + ), + request.getHeaders() + ) + } + + @Test + fun testMinimalHeaders() { + val minimal = ApiRequest(url = EmbraceUrl.getUrl("https://google.com")) + Assert.assertEquals( + mapOf( + "Accept" to "application/json", + "User-Agent" to "Embrace/a/${BuildConfig.VERSION_NAME}", + "Content-Type" to "application/json" + ), + minimal.getHeaders() + ) + } + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("api_request.json") + .filter { !it.isWhitespace() } + val observed = serializer.toJson(request) + Assert.assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("api_request.json") + val obj = serializer.fromJson(json, ApiRequest::class.java) + Assert.assertEquals( + mapOf( + "Accept" to "application/json", + "User-Agent" to "Embrace/a/1", + "Content-Type" to "application/json", + "Content-Encoding" to "application/json", + "Accept-Encoding" to "application/json", + "X-EM-AID" to "abcde", + "X-EM-DID" to "test_did", + "X-EM-SID" to "test_eid", + "X-EM-LID" to "test_lid", + "If-None-Match" to "d800f828fec4409dcabc7f5252e7ce71" + ), + obj?.getHeaders() + ) + } + + @Test + fun testEmptyObject() { + val info = serializer.fromJson("{}", ApiRequest::class.java) + Assert.assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/CachedConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/CachedConfigTest.kt new file mode 100644 index 0000000000..17271655d2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/CachedConfigTest.kt @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk.comms.api + +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import org.junit.Assert +import org.junit.Test + +internal class CachedConfigTest { + + @Test + fun isValid() { + Assert.assertFalse(CachedConfig(null, null).isValid()) + Assert.assertFalse(CachedConfig(RemoteConfig(), null).isValid()) + Assert.assertFalse(CachedConfig(null, "ba09cc").isValid()) + Assert.assertTrue(CachedConfig(RemoteConfig(), "ba09cc").isValid()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/EmbraceApiServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/EmbraceApiServiceTest.kt new file mode 100644 index 0000000000..124f72a67d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/api/EmbraceApiServiceTest.kt @@ -0,0 +1,79 @@ +package io.embrace.android.embracesdk.comms.api + +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class EmbraceApiServiceTest { + + private lateinit var mockApiClient: ApiClient + private lateinit var mockUrlBuilder: ApiUrlBuilder + private lateinit var cachedConfig: CachedConfig + private lateinit var apiService: ApiService + + @Before + fun setUp() { + mockApiClient = mockk() + mockUrlBuilder = mockk { + every { getConfigUrl() } returns "https://test.com" + } + cachedConfig = CachedConfig( + config = null, + eTag = null + ) + apiService = EmbraceApiService( + apiClientProvider = { mockApiClient }, + urlBuilder = mockUrlBuilder, + serializer = EmbraceSerializer(), + cachedConfigProvider = { _, _ -> cachedConfig }, + logger = mockk(relaxed = true) + ) + } + + @Test + fun `test getConfig returns correct values in Response`() { + val json = ResourceReader.readResourceAsText("remote_config_response.json") + every { mockApiClient.executeGet(any()) } returns ApiResponse( + statusCode = 200, + body = json, + headers = emptyMap() + ) + val remoteConfig = apiService.getConfig() + + // verify a few fields were serialized correctly. + checkNotNull(remoteConfig) + assertTrue(checkNotNull(remoteConfig.sessionConfig?.isEnabled)) + assertFalse(checkNotNull(remoteConfig.sessionConfig?.endAsync)) + assertEquals(100, remoteConfig.threshold) + } + + @Test(expected = IllegalStateException::class) + fun `test getConfig rethrows an exception thrown by apiClient`() { + every { mockApiClient.executeGet(any()) } throws IllegalStateException("Test exception message") + + // exception will be thrown and caught by this test's annotation + apiService.getConfig() + } + + @Test + fun testGetConfigWithMatchingEtag() { + val cfg = RemoteConfig() + cachedConfig = CachedConfig(cfg, "my_etag") + every { mockApiClient.executeGet(any()) } returns ApiResponse( + statusCode = 304, + body = "", + headers = emptyMap() + ) + + val remoteConfig = apiService.getConfig() + assertSame(cfg, remoteConfig) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManagerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManagerTest.kt new file mode 100644 index 0000000000..8adaa79444 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/DeliveryNetworkManagerTest.kt @@ -0,0 +1,366 @@ +package io.embrace.android.embracesdk.comms.delivery + +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.comms.api.ApiUrlBuilder +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeSdkModeBehavior +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +internal class DeliveryNetworkManagerTest { + + private lateinit var networkManager: DeliveryNetworkManager + + companion object { + private val metadataService = FakeAndroidMetadataService() + private val connectedNetworkStatuses = + NetworkStatus.values().filter { it != NetworkStatus.NOT_REACHABLE } + + private lateinit var mockApiUrlBuilder: ApiUrlBuilder + private lateinit var mockApiClient: ApiClient + private lateinit var configService: ConfigService + private lateinit var cfg: LocalConfig + private lateinit var mockCacheManager: DeliveryCacheManager + private lateinit var blockingScheduledExecutorService: BlockingScheduledExecutorService + private lateinit var testScheduledExecutor: ScheduledExecutorService + private lateinit var networkConnectivityService: NetworkConnectivityService + private lateinit var failedApiCalls: DeliveryFailedApiCalls + private lateinit var mockUserService: UserService + + @BeforeClass + @JvmStatic + fun setupBeforeAll() { + cfg = LocalConfig("", false, SdkLocalConfig()) + mockApiUrlBuilder = mockk(relaxUnitFun = true) + every { mockApiUrlBuilder.getEmbraceUrlWithSuffix(any()) } returns "http://fake.url" + + networkConnectivityService = mockk(relaxUnitFun = true) + } + + /** + * Setup after all tests get executed. Un-mock all here. + */ + @AfterClass + @JvmStatic + fun tearDownAfterAll() { + unmockkAll() + } + } + + @Before + fun setup() { + configService = FakeConfigService( + sdkModeBehavior = fakeSdkModeBehavior( + localCfg = { cfg } + ) + ) + mockApiClient = mockk() + every { mockApiClient.post(any(), any()) } returns "" + failedApiCalls = DeliveryFailedApiCalls() + clearApiPipeline() + mockCacheManager = mockk(relaxUnitFun = true) + every { mockCacheManager.loadPayload("cached_payload_1") } returns "{payload 1}".toByteArray() + every { mockCacheManager.loadFailedApiCalls() } returns failedApiCalls + every { mockCacheManager.savePayload(any()) } returns "fake_cache" + mockUserService = mockk() + } + + @After + fun tearDown() { + clearMocks(mockApiClient) + clearMocks(mockCacheManager) + } + + @Test + fun `scheduled retry job active at init time`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager(status = status, runRetryJobAfterScheduling = true) + retryTaskActive(status) + clearApiPipeline() + } + } + + @Test + fun `retryTask is not active and doesn't run if there are no failed API requests`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager( + status = status, + loadFailedRequest = false, + runRetryJobAfterScheduling = true + ) + retryTaskNotActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkNoApiRequestSent() + retryTaskNotActive(status) + clearApiPipeline() + } + } + + @Test + fun `retryTask is active and runs after init if network is connected`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager(status = status, runRetryJobAfterScheduling = true) + retryTaskActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkRequestSendAttempt() + retryTaskNotActive(status) + clearApiPipeline() + } + } + + @Test + fun `retryTask will be scheduled again if retry fails`() { + connectedNetworkStatuses.forEach { status -> + every { mockApiClient.post(any(), any()) } throws Exception() + initDeliveryNetworkManager(status = status, runRetryJobAfterScheduling = true) + retryTaskActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkRequestSendAttempt() + retryTaskNotActive(status) + // Previous failed attempt will queue another retry. Let it run so a new retry task is active + blockingScheduledExecutorService.runCurrentlyBlocked() + retryTaskActive(status) + + // First failure will result in another retry in 120 seconds + // Go most of the way to check it didn't run + blockingScheduledExecutorService.moveForwardAndRunBlocked(TimeUnit.SECONDS.toMillis(119L)) + retryTaskActive(status) + checkRequestSendAttempt() + + // Go the full 120 seconds and check that the retry runs and fails + blockingScheduledExecutorService.moveForwardAndRunBlocked(TimeUnit.SECONDS.toMillis(1L)) + checkRequestSendAttempt(count = 2) + + // Previous failed attempt will queue another retry. Let it run + blockingScheduledExecutorService.runCurrentlyBlocked() + + // Let the next retry succeed + every { mockApiClient.post(any(), any()) } returns "" + + // Second failure will result in another retry in double the last time, 240 seconds + // Go most of the way to check it didn't run, then go all the way to check that it did. + blockingScheduledExecutorService.moveForwardAndRunBlocked(TimeUnit.SECONDS.toMillis(239L)) + retryTaskActive(status) + checkRequestSendAttempt(count = 2) + blockingScheduledExecutorService.moveForwardAndRunBlocked(TimeUnit.SECONDS.toMillis(1L)) + retryTaskNotActive(status) + checkRequestSendAttempt(count = 3) + clearApiPipeline() + } + } + + @Test + fun `retryTask is not active and doesn't run after init if network not reachable`() { + initDeliveryNetworkManager( + status = NetworkStatus.NOT_REACHABLE, + runRetryJobAfterScheduling = true + ) + blockingScheduledExecutorService.runCurrentlyBlocked() + retryTaskNotActive(NetworkStatus.NOT_REACHABLE) + checkNoApiRequestSent() + } + + @Test + fun `retryTask isn't active and won't run if there are no failed requests after getting a connection before retry job is scheduled`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager( + status = NetworkStatus.NOT_REACHABLE, + loadFailedRequest = false, + runRetryJobAfterScheduling = true + ) + blockingScheduledExecutorService.runCurrentlyBlocked() + networkManager.onNetworkConnectivityStatusChanged(status) + retryTaskNotActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkNoApiRequestSent() + retryTaskNotActive(status) + clearApiPipeline() + } + } + + @Test + fun `retryTask is active and runs after connection changes from not reachable to connected after retry job runs`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager( + status = NetworkStatus.NOT_REACHABLE, + runRetryJobAfterScheduling = true + ) + blockingScheduledExecutorService.runCurrentlyBlocked() + networkManager.onNetworkConnectivityStatusChanged(status) + retryTaskActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkRequestSendAttempt() + retryTaskNotActive(status) + clearApiPipeline() + } + } + + @Test + fun `retryTask isn't active and doesn't run if there are no failed request after getting a connection before retry job is scheduled`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager( + status = NetworkStatus.NOT_REACHABLE, + loadFailedRequest = false + ) + networkManager.onNetworkConnectivityStatusChanged(status) + retryTaskNotActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkNoApiRequestSent() + retryTaskNotActive(status) + clearApiPipeline() + } + } + + @Test + fun `retryTask is active and runs after connection changes from not reachable to connected before retry job is scheduled`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager(status = NetworkStatus.NOT_REACHABLE) + networkManager.onNetworkConnectivityStatusChanged(status) + retryTaskActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkRequestSendAttempt() + retryTaskNotActive(status) + clearApiPipeline() + } + } + + @Test + fun `retryTask is not active and doesn't run after connection changes from connected to not reachable before retry job is scheduled`() { + connectedNetworkStatuses.forEach { status -> + initDeliveryNetworkManager(status = status) + networkManager.onNetworkConnectivityStatusChanged(NetworkStatus.NOT_REACHABLE) + retryTaskNotActive(status) + blockingScheduledExecutorService.runCurrentlyBlocked() + checkNoApiRequestSent() + } + } + + @Test + fun `queue size should be bounded`() { + initDeliveryNetworkManager(status = NetworkStatus.WIFI, loadFailedRequest = false) + every { mockApiClient.post(any(), any()) } throws Exception() + + Assert.assertEquals(0, networkManager.pendingRetriesCount()) + + repeat(201) { + networkManager.sendSession("{ dummy_session }".toByteArray(), null) + blockingScheduledExecutorService.runCurrentlyBlocked() + } + Assert.assertEquals(200, networkManager.pendingRetriesCount()) + } + + @Test + fun `test app and device info verification is not executed if integrationMode is false`() { + initDeliveryNetworkManager(status = NetworkStatus.WIFI) + val mocked = spyk(networkManager, recordPrivateCalls = true) + val eventMessage = mockk(relaxed = true) + val event = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.CRASH + ) + every { eventMessage.event } returns event + mocked.sendEvent(eventMessage) + verify(exactly = 0) { mocked["verifyDeviceInfo"](eventMessage) } + verify(exactly = 0) { mocked["verifyAppInfo"](eventMessage) } + } + + @Test + fun `test app and device info verification is executed if integrationMode is true`() { + initDeliveryNetworkManager(status = NetworkStatus.WIFI, integrationMode = true) + + val mocked = spyk(networkManager, recordPrivateCalls = true) + val event = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.CRASH + ) + val eventMessage = mockk(relaxed = true) + every { eventMessage.event } returns event + mocked.sendEvent(eventMessage) + verify { mocked["verifyDeviceInfo"](eventMessage) } + verify { mocked["verifyAppInfo"](eventMessage) } + } + + private fun clearApiPipeline() { + clearMocks(mockApiClient, answers = false) + failedApiCalls.clear() + blockingScheduledExecutorService = BlockingScheduledExecutorService() + testScheduledExecutor = blockingScheduledExecutorService + } + + private fun initDeliveryNetworkManager( + status: NetworkStatus, + loadFailedRequest: Boolean = true, + runRetryJobAfterScheduling: Boolean = false, + integrationMode: Boolean = false, + ) { + cfg = LocalConfig("", false, SdkLocalConfig(integrationModeEnabled = integrationMode)) + every { networkConnectivityService.getCurrentNetworkStatus() } returns status + + networkManager = DeliveryNetworkManager( + metadataService, + mockApiUrlBuilder, + mockApiClient, + mockCacheManager, + mockk(relaxUnitFun = true), + configService, + testScheduledExecutor, + networkConnectivityService, + EmbraceSerializer(), + mockUserService + ) + + failedApiCalls.clear() + if (loadFailedRequest) { + failedApiCalls.add(DeliveryFailedApiCall(mockk(), "cached_payload_1")) + } + + if (runRetryJobAfterScheduling) { + blockingScheduledExecutorService.runCurrentlyBlocked() + } + } + + private fun retryTaskActive(status: NetworkStatus) { + Assert.assertTrue("Failed for network status = $status", networkManager.isRetryTaskActive()) + } + + private fun retryTaskNotActive(status: NetworkStatus) { + Assert.assertFalse( + "Failed for network status = $status", + networkManager.isRetryTaskActive() + ) + } + + private fun checkRequestSendAttempt(count: Int = 1) { + verify(exactly = count) { mockApiClient.post(any(), "{payload 1}".toByteArray()) } + } + + private fun checkNoApiRequestSent() { + verify(exactly = 0) { mockApiClient.post(any(), any()) } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryServiceTest.kt new file mode 100644 index 0000000000..fceae3a68a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/comms/delivery/EmbraceDeliveryServiceTest.kt @@ -0,0 +1,281 @@ +package io.embrace.android.embracesdk.comms.delivery + +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.fakeBackgroundActivity +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.SessionMessage +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +internal class EmbraceDeliveryServiceTest { + + private lateinit var deliveryService: EmbraceDeliveryService + private val executor = MoreExecutors.newDirectExecutorService() + + companion object { + private lateinit var mockDeliveryCacheManager: DeliveryCacheManager + private lateinit var mockDeliveryNetworkManager: DeliveryNetworkManager + private lateinit var logger: InternalEmbraceLogger + private lateinit var configService: ConfigService + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockDeliveryCacheManager = mockk(relaxed = true) + every { mockDeliveryCacheManager.loadCrash() } returns null + every { mockDeliveryCacheManager.getAllCachedSessionIds() } returns emptyList() + mockDeliveryNetworkManager = mockk(relaxed = true) + logger = InternalEmbraceLogger() + } + } + + @Before + fun setUp() { + configService = FakeConfigService() + } + + @After + fun after() { + clearAllMocks() + executor.shutdown() + } + + private fun initializeDeliveryService() { + deliveryService = EmbraceDeliveryService( + mockDeliveryCacheManager, + mockDeliveryNetworkManager, + executor, + executor, + logger, + configService + ) + } + + @Test + fun `cache current session successfully`() { + initializeDeliveryService() + val mockSessionMessage: SessionMessage = mockk() + every { + mockDeliveryCacheManager.saveSession(mockSessionMessage) + } returns "cached_session".toByteArray() + + deliveryService.saveSession(mockSessionMessage) + + verify { mockDeliveryCacheManager.saveSession(mockSessionMessage) } + } + + @Test + fun `if no previous cached session then send previous cached sessions should not send anything`() { + initializeDeliveryService() + every { mockDeliveryCacheManager.getAllCachedSessionIds() } returns emptyList() + every { mockDeliveryCacheManager.loadCrash() } returns null + + deliveryService.sendCachedSessions(false, mockk(), null) + executor.awaitTermination(1, TimeUnit.SECONDS) + + verify { mockDeliveryNetworkManager wasNot Called } + verify(exactly = 0) { mockDeliveryCacheManager.deleteSession(any()) } + } + + @Test + fun `send previously cached sessions successfully`() { + initializeDeliveryService() + every { mockDeliveryCacheManager.getAllCachedSessionIds() } returns listOf( + "session1", + "session2" + ) + every { mockDeliveryCacheManager.loadSessionBytes("session1") } returns "cached_session_1".toByteArray() + every { mockDeliveryCacheManager.loadSessionBytes("session2") } returns "cached_session_2".toByteArray() + every { mockDeliveryCacheManager.loadCrash() } returns null + + deliveryService.sendCachedSessions(false, mockk(), null) + executor.awaitTermination(1, TimeUnit.SECONDS) + + verify { mockDeliveryCacheManager.loadSessionBytes("session1") } + verify { mockDeliveryCacheManager.loadSessionBytes("session2") } + verify { mockDeliveryNetworkManager.sendSession("cached_session_1".toByteArray(), any()) } + verify { mockDeliveryNetworkManager.sendSession("cached_session_2".toByteArray(), any()) } + } + + @Test + fun `ignore current session when sending previously cached sessions`() { + initializeDeliveryService() + every { mockDeliveryCacheManager.getAllCachedSessionIds() } returns listOf( + "session1", + "session2" + ) + every { mockDeliveryCacheManager.loadSessionBytes("session1") } returns "cached_session_1".toByteArray() + every { mockDeliveryCacheManager.loadSessionBytes("session2") } returns "cached_session_2".toByteArray() + every { mockDeliveryCacheManager.loadCrash() } returns null + + deliveryService.sendCachedSessions(false, mockk(), "session2") + executor.awaitTermination(1, TimeUnit.SECONDS) + + verify { mockDeliveryCacheManager.loadSessionBytes("session1") } + verify(exactly = 0) { mockDeliveryCacheManager.loadSessionBytes("session2") } + verify { mockDeliveryNetworkManager.sendSession("cached_session_1".toByteArray(), any()) } + verify(exactly = 0) { + mockDeliveryNetworkManager.sendSession( + "cached_session_2".toByteArray(), + any() + ) + } + } + + @Test + fun `if an exception is thrown while sending cached session then sendCachedSession should not crash`() { + initializeDeliveryService() + every { mockDeliveryCacheManager.getAllCachedSessionIds() } returns listOf("session1") + every { mockDeliveryCacheManager.loadSessionBytes("session1") } returns "cached_session".toByteArray() + every { mockDeliveryNetworkManager.sendSession(any(), any()) } throws Exception() + every { mockDeliveryCacheManager.loadCrash() } returns null + + deliveryService.sendCachedSessions(false, mockk(), null) + executor.awaitTermination(1, TimeUnit.SECONDS) + + verify { mockDeliveryCacheManager.loadSessionBytes("session1") } + verify { mockDeliveryNetworkManager.sendSession("cached_session".toByteArray(), any()) } + } + + @Test + fun `send session start`() { + initializeDeliveryService() + + every { + mockDeliveryCacheManager.saveSession(any()) + } returns "cached_session".toByteArray() + + val mockFuture: Future = mockk() + every { mockDeliveryNetworkManager.sendSession(any(), any()) } returns mockFuture + + deliveryService.sendSession(mockk(), SessionMessageState.START) + + verify(exactly = 1) { + mockDeliveryNetworkManager.sendSession( + any(), + null + ) + } + verify { mockFuture wasNot Called } + } + + @Test + fun `send session end`() { + initializeDeliveryService() + + every { + mockDeliveryCacheManager.saveSession(any()) + } returns "cached_session".toByteArray() + + val mockFuture: Future = mockk() + every { mockDeliveryNetworkManager.sendSession(any(), any()) } returns mockFuture + + deliveryService.sendSession(mockk(), SessionMessageState.END) + + verify(exactly = 1) { + mockDeliveryNetworkManager.sendSession( + any(), + withArg { + assertNotNull(it) + } + ) + } + verify { mockFuture wasNot Called } + } + + @Test + fun `send session end with crash`() { + initializeDeliveryService() + + every { + mockDeliveryCacheManager.saveSession(any()) + } returns "cached_session".toByteArray() + + val mockFuture: Future = mockk() + every { mockDeliveryNetworkManager.sendSession(any(), any()) } returns mockFuture + + deliveryService.sendSession(mockk(), SessionMessageState.END_WITH_CRASH) + + verify(exactly = 1) { + mockDeliveryNetworkManager.sendSession( + any(), + withArg { + assertNotNull(it) + } + ) + } + verify(exactly = 1) { + mockFuture.get(1L, TimeUnit.SECONDS) + } + } + + @Test + fun `check for native crash info if ndk feature is enabled`() { + val mockNdkService: NdkService = mockk() + initializeDeliveryService() + deliveryService.sendCachedSessions(true, mockNdkService, "") + verify(exactly = 1) { mockNdkService.checkForNativeCrash() } + } + + @Test + fun testSaveBackgroundActivity() { + initializeDeliveryService() + val obj = fakeBackgroundActivity() + deliveryService.saveBackgroundActivity(obj) + verify(exactly = 1) { mockDeliveryCacheManager.saveBackgroundActivity(obj) } + } + + @Test + fun testSendEventAsync() { + initializeDeliveryService() + val obj = EventMessage(Event()) + deliveryService.sendEventAsync(obj) + verify(exactly = 1) { mockDeliveryNetworkManager.sendEvent(obj) } + } + + @Test + fun testSaveCrash() { + initializeDeliveryService() + val obj = EventMessage(Event()) + deliveryService.saveCrash(obj) + verify(exactly = 1) { mockDeliveryCacheManager.saveCrash(obj) } + } + + @Test + fun testSendBackgroundActivity() { + initializeDeliveryService() + val obj = fakeBackgroundActivity() + deliveryService.sendBackgroundActivity(obj) + + // cache the object first in case process terminates + verify(exactly = 1) { mockDeliveryCacheManager.saveBackgroundActivity(obj) } + verify(exactly = 1) { mockDeliveryNetworkManager.sendSession(any(), any()) } + } + + @Test + fun testSendBackgroundActivities() { + val bytes = ByteArray(5) + initializeDeliveryService() + val obj = fakeBackgroundActivity() + deliveryService.saveBackgroundActivity(obj) + + every { mockDeliveryCacheManager.loadBackgroundActivity(any()) } returns bytes + deliveryService.sendBackgroundActivities() + verify(exactly = 1) { mockDeliveryNetworkManager.sendSession(bytes, any()) } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorService.kt new file mode 100644 index 0000000000..0c529125e3 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorService.kt @@ -0,0 +1,104 @@ +package io.embrace.android.embracesdk.concurrency + +import io.embrace.android.embracesdk.InternalApi +import org.jetbrains.annotations.TestOnly +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.ExecutorService +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.TimeUnit + +/** + * An [ExecutorService] that can be set to a mode that blocks tasks from executing unless explicitly unblocked. In either mode, + * when they run, they are executed on the current thread so their execution is predictable in tests. + * + * While blocking mode defaults to false, it can be instantiated in in blocking mode using the constructor parameter "blockingMode" + * (and subsequently toggled using the [blockingMode] attribute). So by default, this executor behaves like + * [com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService] unless switched to blocking mode. + */ +@InternalApi +internal class BlockableExecutorService(blockingMode: Boolean = false) : AbstractExecutorService() { + private val tasks = ConcurrentLinkedQueue() + private var shutdown = false + + @Volatile + var blockingMode: Boolean = blockingMode + set(value) { + field = value + if (!field) { + runCurrentlyBlocked() + } + } + + /** + * Unblock and run all submitted tasks. New submissions will NOT be run until explicitly told to after the current batch of tasks + * are completed. + */ + fun runCurrentlyBlocked() { + rejectIfShutdown() + var taskCount = tasks.size + if (taskCount > 0) { + var task = tasks.poll() + + while (task != null && taskCount > 0) { + task.run() + taskCount-- + if (taskCount > 0) { + task = tasks.poll() + } + } + } + } + + /** + * Unblock and run one (1) submitted task at the head of the queue if it exists + */ + fun runNext() { + rejectIfShutdown() + tasks.poll()?.run() + } + + /** + * Return the number of blocked tasks + */ + fun tasksBlockedCount(): Int = tasks.size + + @TestOnly + override fun execute(command: Runnable?) { + checkNotNull(command) + rejectIfShutdown() + tasks.add(command) + if (!blockingMode) { + runCurrentlyBlocked() + } + } + + override fun shutdown() { + do { + tasks.poll()?.run() + } while (tasks.isNotEmpty()) + + shutdown = true + } + + override fun shutdownNow(): MutableList { + shutdown = true + val remainingTasks = tasks.toMutableList() + tasks.clear() + return remainingTasks + } + + override fun isShutdown(): Boolean { + return shutdown + } + + override fun isTerminated(): Boolean = shutdown && tasks.isEmpty() + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean = throw UnsupportedOperationException() + + private fun rejectIfShutdown() { + if (isShutdown) { + throw RejectedExecutionException() + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorServiceTests.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorServiceTests.kt new file mode 100644 index 0000000000..4b3aae7cde --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockableExecutorServiceTests.kt @@ -0,0 +1,141 @@ +package io.embrace.android.embracesdk.concurrency + +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.RejectedExecutionException + +internal class BlockableExecutorServiceTests { + + private lateinit var executorService: BlockableExecutorService + private var tasksExecuted = 0 + private val task = { tasksExecuted++ } + + @Before + fun setup() { + executorService = BlockableExecutorService(blockingMode = true) + } + + @After + fun tearDown() { + tasksExecuted = 0 + } + + @Test + fun `test executor executes on the current thread`() { + var executorRunThread: Thread? = null + executorService.submit { + executorRunThread = Thread.currentThread() + } + executorService.runNext() + assertEquals(Thread.currentThread(), executorRunThread) + } + + @Test + fun `test tasks blocked until told to run`() { + repeat(3) { + executorService.submit(task) + } + + assertEquals(0, tasksExecuted) + assertEquals(3, executorService.tasksBlockedCount()) + executorService.runNext() + assertEquals(1, tasksExecuted) + assertEquals(2, executorService.tasksBlockedCount()) + executorService.runCurrentlyBlocked() + assertEquals(3, tasksExecuted) + assertEquals(0, executorService.tasksBlockedCount()) + } + + @Test + fun `test tasks queued while unblocked tasks are running will not run`() { + repeat(3) { + executorService.submit { + tasksExecuted++ + executorService.submit(task) + } + } + + executorService.runCurrentlyBlocked() + assertEquals(3, tasksExecuted) + assertEquals(3, executorService.tasksBlockedCount()) + executorService.runCurrentlyBlocked() + assertEquals(6, tasksExecuted) + assertEquals(0, executorService.tasksBlockedCount()) + } + + @Test + fun `tasks run immediately on the current thread in non-blocking mode`() { + val nonBlockingModeExecutor = BlockableExecutorService(blockingMode = false) + var executorRunThread: Thread? = null + nonBlockingModeExecutor.submit { + executorRunThread = Thread.currentThread() + } + assertEquals(Thread.currentThread(), executorRunThread) + } + + @Test + fun `tasks currently blocked run immediately when switching to non-blocking mode`() { + executorService.submit(task) + assertEquals(0, tasksExecuted) + assertEquals(1, executorService.tasksBlockedCount()) + executorService.blockingMode = false + assertEquals(1, tasksExecuted) + assertEquals(0, executorService.tasksBlockedCount()) + } + + @Test + fun `tasks blocked after enabling blocking mode`() { + val executor = BlockableExecutorService() + executor.submit(task) + assertEquals(1, tasksExecuted) + executor.blockingMode = true + executor.submit(task) + assertEquals(1, tasksExecuted) + executor.runNext() + assertEquals(2, tasksExecuted) + } + + @Test + fun `test shutdown`() { + repeat(3) { + executorService.submit { + tasksExecuted++ + executorService.submit(task) + } + } + + executorService.shutdown() + assertEquals(6, tasksExecuted) + assertTrue(executorService.isShutdown) + assertTrue(executorService.isTerminated) + + assertThrows(RejectedExecutionException::class.java) { + executorService.submit {} + } + + assertThrows(RejectedExecutionException::class.java) { + executorService.runCurrentlyBlocked() + } + + assertThrows(RejectedExecutionException::class.java) { + executorService.runNext() + } + } + + @Test + fun `test shutdownNow`() { + repeat(3) { + executorService.submit(task) + } + + val remainingTasks = executorService.shutdownNow() + assertEquals(3, remainingTasks.size) + assertEquals(0, tasksExecuted) + assertTrue(executorService.isShutdown) + assertTrue(executorService.isTerminated) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorService.kt new file mode 100644 index 0000000000..1874f166b6 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorService.kt @@ -0,0 +1,280 @@ +package io.embrace.android.embracesdk.concurrency + +import io.embrace.android.embracesdk.InternalApi +import io.embrace.android.embracesdk.fakes.FakeClock +import java.util.LinkedList +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.Callable +import java.util.concurrent.Delayed +import java.util.concurrent.Future +import java.util.concurrent.FutureTask +import java.util.concurrent.PriorityBlockingQueue +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.RunnableScheduledFuture +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +/** + * An [ScheduledExecutorService] to be used for tests that will block the execution of tasks until explicitly unblocked. You can simulate + * moving forward in time to triggered the execution of tasks scheduled for the future, and they will be run in the appropriate execution + * order on the current thread. + * + * Note that worked to be run immediately will be sent directly to the instance of [ScheduledExecutorService] used internally. Work + * scheduled for the future will be stored in separately until it is time for it to execute, which will be done on the delegate. + * + * Limitations: + * + * - [awaitTermination] is not implemented and will throw a [NotImplementedError]. Code that requires that method cannot use this. + * - The value of [ScheduledFuture.get] for [schedule] when you pass in a [Callable] will not return the execution result. + * + */ +@InternalApi +internal class BlockingScheduledExecutorService( + private val fakeClock: FakeClock = FakeClock() +) : AbstractExecutorService(), ScheduledExecutorService { + private val scheduledTasks = PriorityBlockingQueue(10, BlockedScheduledFutureTaskComparator()) + private val delegateExecutorService = BlockableExecutorService(blockingMode = true) + + /** + * Run all tasks due to run at the current time and return when all the tasks have finished running. This does not include tasks + * submitted during the running of these tasks. + */ + fun runCurrentlyBlocked() { + rejectIfShutdown() + val tasksToRun = LinkedList() + var nextTask = scheduledTasks.peek() + while (nextTask != null) { + nextTask = if (nextTask.executionTimeMs <= fakeClock.now()) { + val task = scheduledTasks.poll() + if (task != null) { + tasksToRun.add(task) + } + scheduledTasks.peek() + } else { + null + } + } + + tasksToRun.forEach { submit(it) } + delegateExecutorService.runCurrentlyBlocked() + } + + /** + * Runs all tasks that have been submitted to the ExecutorService, regardless of scheduled time. + */ + fun runAllSubmittedTasks() { + rejectIfShutdown() + val tasksToRun = LinkedList() + var nextTask = scheduledTasks.peek() + while (nextTask != null) { + nextTask = scheduledTasks.poll() + if (nextTask != null) { + tasksToRun.add(nextTask) + } + } + + tasksToRun.forEach { submit(it) } + delegateExecutorService.runCurrentlyBlocked() + } + + /** + * Move time forward and run the tasks that are expected to be run by that time. + */ + fun moveForwardAndRunBlocked(timeIncrementMs: Long) { + rejectIfShutdown() + fakeClock.tick(timeIncrementMs) + runCurrentlyBlocked() + } + + override fun execute(command: Runnable?) { + requireNotNull(command) + delegateExecutorService.execute(command) + } + + override fun submit(task: Runnable?): Future<*> { + requireNotNull(task) + return delegateExecutorService.submit(task) + } + + override fun shutdown() { + drainToDelegate() + delegateExecutorService.shutdown() + } + + override fun shutdownNow(): MutableList { + drainToDelegate() + return delegateExecutorService.shutdownNow() + } + + override fun isShutdown(): Boolean = delegateExecutorService.isShutdown + + override fun isTerminated(): Boolean = delegateExecutorService.isTerminated + + override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean = throw UnsupportedOperationException() + + override fun schedule(command: Runnable?, delay: Long, unit: TimeUnit?): ScheduledFuture { + requireNotNull(command) + requireNotNull(unit) + require(delay >= 0) { "The delay parameter cannot be negative" } + + rejectIfShutdown() + val futureTask = BlockedFutureScheduledTask( + runnable = command, + executionTimeMs = fakeClock.now() + unit.toMillis(delay) + ) + + submitOrQueue(delay, futureTask) + + return futureTask + } + + /** + * This implementation is only partially working as the [ScheduledFuture] doesn't return the result of the [Callable]. This needs to be + * fixed if need to test the result of the [Callable] run from the [ScheduledFuture]... in the future. + */ + override fun schedule(callable: Callable?, delay: Long, unit: TimeUnit?): ScheduledFuture { + requireNotNull(callable) + requireNotNull(unit) + require(delay >= 0) { "The delay parameter cannot be negative" } + rejectIfShutdown() + val futureTask = BlockedFutureScheduledTask( + callable = callable, + executionTimeMs = fakeClock.now() + unit.toMillis(delay) + ) + + submitOrQueue(delay, futureTask) + + return futureTask + } + + override fun scheduleAtFixedRate(command: Runnable?, initialDelay: Long, period: Long, unit: TimeUnit?): ScheduledFuture { + requireNotNull(command) + requireNotNull(unit) + require(initialDelay >= 0) { "The initialDelay parameter cannot be negative" } + require(period > 0) { "The period parameter has to be positive number" } + rejectIfShutdown() + val futureTask = BlockedFutureScheduledTask( + runnable = command, + executionTimeMs = fakeClock.now() + unit.toMillis(initialDelay), + periodMs = unit.toMillis(period) + ) + + if (initialDelay <= 0L) { + submit(futureTask) + } else { + scheduledTasks.add(futureTask) + } + + return futureTask + } + + override fun scheduleWithFixedDelay(command: Runnable?, initialDelay: Long, delay: Long, unit: TimeUnit?): ScheduledFuture { + requireNotNull(command) + requireNotNull(unit) + require(initialDelay >= 0) { "The initialDelay parameter cannot be negative" } + require(delay > 0) { "The delay parameter has to be positive number" } + rejectIfShutdown() + val futureTask = BlockedFutureScheduledTask( + runnable = command, + executionTimeMs = fakeClock.now() + unit.toMillis(initialDelay), + periodMs = unit.toMillis(delay), + runFromLastExecution = true + ) + + if (initialDelay <= 0L) { + submit(futureTask) + } else { + scheduledTasks.add(futureTask) + } + + return futureTask + } + + private fun submitOrQueue(delay: Long, futureTask: BlockedFutureScheduledTask) { + if (delay <= 0L) { + submit(futureTask) + } else { + scheduledTasks.add(futureTask) + } + } + + private fun drainToDelegate() { + do { + val task = scheduledTasks.poll() + if (task != null) { + submit(task) + } + } while (task != null) + } + + private fun rejectIfShutdown() { + if (isShutdown) { + throw RejectedExecutionException() + } + } + + inner class BlockedFutureScheduledTask : FutureTask, RunnableScheduledFuture { + var executionTimeMs: Long + private val periodMs: Long + private val runFromLastExecution: Boolean + + constructor( + runnable: Runnable, + executionTimeMs: Long, + periodMs: Long = 0L, + runFromLastExecution: Boolean = false + ) : super(runnable, null) { + this.executionTimeMs = executionTimeMs + this.periodMs = periodMs + this.runFromLastExecution = runFromLastExecution + } + + constructor( + callable: Callable, + executionTimeMs: Long + ) : super(callable) { + this.executionTimeMs = executionTimeMs + this.periodMs = 0L + this.runFromLastExecution = false + } + + override fun run() { + if (!isPeriodic) { + super.run() + } else { + runAndReset() + if (runFromLastExecution) { + executionTimeMs = fakeClock.now() + periodMs + } else { + executionTimeMs += periodMs + } + if (executionTimeMs <= fakeClock.now()) { + submit(this) + runCurrentlyBlocked() + } else { + scheduledTasks.add(this) + } + } + } + + override fun compareTo(other: Delayed?): Int { + requireNotNull(other) + val delay = executionTimeMs - fakeClock.now() + return delay.compareTo(other.getDelay(TimeUnit.MILLISECONDS)) + } + + override fun getDelay(unit: TimeUnit?): Long { + requireNotNull(unit) + return unit.convert(executionTimeMs - fakeClock.now(), TimeUnit.MILLISECONDS) + } + + override fun isPeriodic(): Boolean = periodMs != 0L + } + + private class BlockedScheduledFutureTaskComparator : Comparator> { + override fun compare(taskA: BlockedFutureScheduledTask<*>, taskB: BlockedFutureScheduledTask<*>): Int { + return taskA.executionTimeMs.compareTo(taskB.executionTimeMs) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorServiceTests.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorServiceTests.kt new file mode 100644 index 0000000000..2f191d7806 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/BlockingScheduledExecutorServiceTests.kt @@ -0,0 +1,385 @@ +package io.embrace.android.embracesdk.concurrency + +import io.embrace.android.embracesdk.fakes.FakeClock +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.Callable +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.TimeUnit + +internal class BlockingScheduledExecutorServiceTests { + private lateinit var executorService: BlockingScheduledExecutorService + private var tasksExecuted = 0 + private lateinit var clock: FakeClock + + @Before + fun setup() { + clock = FakeClock() + executorService = BlockingScheduledExecutorService(clock) + } + + @After + fun tearDown() { + tasksExecuted = 0 + } + + @Test + fun `test executor executes on the current thread`() { + var executorRunThread: Thread? = null + executorService.submit { + executorRunThread = Thread.currentThread() + } + executorService.runCurrentlyBlocked() + assertEquals(Thread.currentThread(), executorRunThread) + + executorService.schedule( + { executorRunThread = Thread.currentThread() }, + 10L, + TimeUnit.MILLISECONDS + ) + executorService.moveForwardAndRunBlocked(10L) + + assertEquals(Thread.currentThread(), executorRunThread) + } + + @Test + fun `test tasks blocked until told to run`() { + repeat(3) { + executorService.submit { + tasksExecuted++ + } + } + + assertEquals(0, tasksExecuted) + executorService.runCurrentlyBlocked() + assertEquals(3, tasksExecuted) + } + + @Test + fun `test scheduled tasks blocked until told to run`() { + repeat(3) { + executorService.schedule( + { tasksExecuted++ }, + 100L + it, + TimeUnit.MILLISECONDS + ) + } + + executorService.runCurrentlyBlocked() + assertEquals(0, tasksExecuted) + executorService.moveForwardAndRunBlocked(50L) + assertEquals(0, tasksExecuted) + executorService.moveForwardAndRunBlocked(50L) + assertEquals(1, tasksExecuted) + executorService.moveForwardAndRunBlocked(10L) + assertEquals(3, tasksExecuted) + } + + @Test + fun `test tasks queued while unblocked tasks are running will not run`() { + var tasksExecuted = 0 + repeat(3) { + executorService.submit { + tasksExecuted++ + executorService.submit { + tasksExecuted++ + } + } + } + + executorService.runCurrentlyBlocked() + assertEquals(3, tasksExecuted) + executorService.runCurrentlyBlocked() + assertEquals(6, tasksExecuted) + } + + @Test + fun `test submit`() { + val taskStatus = executorService.submit { + tasksExecuted++ + } + assertFalse(taskStatus.isDone) + executorService.runCurrentlyBlocked() + assertEquals(1, tasksExecuted) + assertTrue(taskStatus.isDone) + } + + @Test + fun `test cancellation via future from submit`() { + val taskStatus = executorService.submit { + tasksExecuted++ + } + assertFalse(taskStatus.isDone) + assertFalse(taskStatus.isCancelled) + taskStatus.cancel(true) + executorService.runCurrentlyBlocked() + assertEquals(0, tasksExecuted) + assertTrue(taskStatus.isDone) + assertTrue(taskStatus.isCancelled) + } + + @Test + fun `test schedule runnable`() { + val taskStatus = executorService.schedule( + command = { tasksExecuted++ }, + delay = 10L, + unit = TimeUnit.SECONDS + ) + assertFalse(taskStatus.isDone) + executorService.moveForwardAndRunBlocked(TimeUnit.SECONDS.toMillis(10L)) + assertEquals(1, tasksExecuted) + assertTrue(taskStatus.isDone) + } + + @Test + fun `test schedule callable`() { + val taskStatus = executorService.schedule( + callable = { tasksExecuted++ }, + delay = 10L, + unit = TimeUnit.SECONDS + ) + assertFalse(taskStatus.isDone) + executorService.moveForwardAndRunBlocked(TimeUnit.SECONDS.toMillis(10L)) + assertEquals(1, tasksExecuted) + assertTrue(taskStatus.isDone) + // welp, callable result not returned + assertEquals(0, taskStatus.get()) + } + + @Test + fun `test cancellation via future from schedule`() { + val taskStatus = executorService.schedule( + command = { tasksExecuted++ }, + delay = 10L, + unit = TimeUnit.SECONDS + ) + assertFalse(taskStatus.isDone) + assertFalse(taskStatus.isCancelled) + taskStatus.cancel(true) + executorService.moveForwardAndRunBlocked(TimeUnit.SECONDS.toMillis(10L)) + assertEquals(0, tasksExecuted) + assertTrue(taskStatus.isDone) + assertTrue(taskStatus.isCancelled) + } + + @Test + fun `test scheduleAtFixedRate`() { + val taskStatus = executorService.scheduleAtFixedRate( + command = { tasksExecuted++ }, + initialDelay = 500L, + period = 10L, + unit = TimeUnit.MILLISECONDS + ) + assertFalse(taskStatus.isDone) + executorService.runCurrentlyBlocked() + assertEquals(0, tasksExecuted) + executorService.moveForwardAndRunBlocked(500L) + executorService.runCurrentlyBlocked() + assertEquals(1, tasksExecuted) + executorService.moveForwardAndRunBlocked(10L) + assertEquals(2, tasksExecuted) + executorService.moveForwardAndRunBlocked(9L) + assertEquals(2, tasksExecuted) + executorService.moveForwardAndRunBlocked(1L) + assertEquals(3, tasksExecuted) + assertFalse(taskStatus.isDone) + taskStatus.cancel(true) + assertTrue(taskStatus.isDone) + assertTrue(taskStatus.isCancelled) + executorService.moveForwardAndRunBlocked(10L) + assertEquals(3, tasksExecuted) + } + + @Test + fun `test delayed runs of scheduleAtFixedRate`() { + executorService.scheduleAtFixedRate( + command = { tasksExecuted++ }, + initialDelay = 0L, + period = 10L, + unit = TimeUnit.MILLISECONDS + ) + executorService.runCurrentlyBlocked() + assertEquals(1, tasksExecuted) + executorService.moveForwardAndRunBlocked(40L) + assertEquals(5, tasksExecuted) + } + + @Test + fun `test scheduleWithFixedDelay`() { + val taskStatus = executorService.scheduleWithFixedDelay( + command = { + clock.tick(100L) + tasksExecuted++ + }, + initialDelay = 500L, + delay = 10L, + unit = TimeUnit.MILLISECONDS + ) + assertFalse(taskStatus.isDone) + executorService.runCurrentlyBlocked() + assertEquals(0, tasksExecuted) + executorService.moveForwardAndRunBlocked(500L) + executorService.runCurrentlyBlocked() + assertEquals(1, tasksExecuted) + executorService.moveForwardAndRunBlocked(10L) + assertEquals(2, tasksExecuted) + executorService.moveForwardAndRunBlocked(9L) + assertEquals(2, tasksExecuted) + executorService.moveForwardAndRunBlocked(1L) + assertEquals(3, tasksExecuted) + assertFalse(taskStatus.isDone) + taskStatus.cancel(true) + assertTrue(taskStatus.isDone) + assertTrue(taskStatus.isCancelled) + executorService.moveForwardAndRunBlocked(10L) + assertEquals(3, tasksExecuted) + } + + @Test + fun `test delayed runs of scheduleWithFixedDelay`() { + executorService.scheduleWithFixedDelay( + command = { + clock.tick(100L) + tasksExecuted++ + }, + initialDelay = 0L, + delay = 10L, + unit = TimeUnit.MILLISECONDS + ) + executorService.runCurrentlyBlocked() + assertEquals(1, tasksExecuted) + executorService.moveForwardAndRunBlocked(99L) + assertEquals(2, tasksExecuted) + } + + @Test + fun `test invalid parameters for scheduling Runnable`() { + val nullRunnable: Runnable? = null + assertThrows(IllegalArgumentException::class.java) { + executorService.schedule(nullRunnable, 0L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.schedule(Runnable {}, -1L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.schedule(Runnable {}, 0L, null) + } + } + + @Test + fun `test invalid parameters for scheduling Callable`() { + val nullCallable: Callable? = null + assertThrows(IllegalArgumentException::class.java) { + executorService.schedule(nullCallable, 0L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.schedule(Callable {}, -1L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.schedule(Callable {}, 0L, null) + } + } + + @Test + fun `test invalid parameters for scheduleAtFixedRate`() { + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleAtFixedRate(null, 0L, 100L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleAtFixedRate({}, -1L, 100L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleAtFixedRate({}, 0L, 0L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleAtFixedRate({}, 0L, 100L, null) + } + } + + @Test + fun `test invalid parameters for scheduleWithFixedDelay`() { + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleWithFixedDelay(null, 0L, 100L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleWithFixedDelay({}, -1L, 100L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleWithFixedDelay({}, 0L, 0L, TimeUnit.MILLISECONDS) + } + + assertThrows(IllegalArgumentException::class.java) { + executorService.scheduleWithFixedDelay({}, 0L, 100L, null) + } + } + + @Test + fun `test shutdown`() { + repeat(3) { + executorService.submit { + tasksExecuted++ + executorService.submit { tasksExecuted++ } + } + } + + repeat(2) { + executorService.schedule( + { tasksExecuted++ }, + 100L + it, + TimeUnit.MILLISECONDS + ) + } + + executorService.shutdown() + assertEquals(8, tasksExecuted) + assertTrue(executorService.isShutdown) + assertTrue(executorService.isTerminated) + + assertThrows(RejectedExecutionException::class.java) { + executorService.submit {} + } + + assertThrows(RejectedExecutionException::class.java) { + executorService.runCurrentlyBlocked() + } + + assertThrows(RejectedExecutionException::class.java) { + executorService.moveForwardAndRunBlocked(10L) + } + } + + @Test + fun `test shutdownNow`() { + repeat(3) { + executorService.submit { tasksExecuted++ } + } + + repeat(2) { + executorService.schedule( + { tasksExecuted++ }, + 100L + it, + TimeUnit.MILLISECONDS + ) + } + + val remainingTasks = executorService.shutdownNow() + assertEquals(5, remainingTasks.size) + assertEquals(0, tasksExecuted) + assertTrue(executorService.isShutdown) + assertTrue(executorService.isTerminated) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutor.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutor.kt new file mode 100644 index 0000000000..98c2c9074a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutor.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk.concurrency + +import io.embrace.android.embracesdk.internal.ConstantNameThreadFactory +import java.util.concurrent.ExecutionException +import java.util.concurrent.Future +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.ThreadFactory +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +/** + * A single-threaded [java.util.concurrent.ScheduledExecutorService] used for tests that exposes the last [Throwable] that interrupted + * the execution of a task + */ +internal class SingleThreadTestScheduledExecutor( + threadFactory: ThreadFactory = defaultThreadFactory +) : ScheduledThreadPoolExecutor(1, threadFactory) { + + val executing = AtomicBoolean(false) + private val lastThrowable = AtomicReference() + + override fun beforeExecute(t: Thread?, r: Runnable?) { + super.beforeExecute(t, r) + synchronized(lastThrowable) { + executing.set(true) + } + } + + override fun afterExecute(r: Runnable?, t: Throwable?) { + synchronized(lastThrowable) { + try { + if (r is Future<*>) { + r.get(500, TimeUnit.MILLISECONDS) + } + } catch (e: ExecutionException) { + lastThrowable.set(e.cause) + } catch (t: Throwable) { + lastThrowable.set(t) + } finally { + executing.set(false) + } + } + super.afterExecute(r, t) + } + + /** + * The last [Throwable] thrown during the execution of a job. Note that is achieved by catching the [ExecutionException] thrown when + * we try to get the result of the task via the [Future] associated with the execution, which is done after the [Future] completes. So + * there may be a time when the task is done but this function returns the wrong value - you have to check that [executing] is false to + * make sure that we are not in the process of setting a new [lastThrowable] in order for this value to be truly valid. + */ + fun lastThrowable() = synchronized(lastThrowable) { lastThrowable.get() } + + /** + * Resets the error tracking + */ + fun reset() = synchronized(lastThrowable) { lastThrowable.set(null) } + + companion object { + private val defaultThreadFactory = ConstantNameThreadFactory("test") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutorTest.kt new file mode 100644 index 0000000000..1ec865b1ba --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/concurrency/SingleThreadTestScheduledExecutorTest.kt @@ -0,0 +1,44 @@ +package io.embrace.android.embracesdk.concurrency + +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit + +internal class SingleThreadTestScheduledExecutorTest { + private lateinit var executorService: SingleThreadTestScheduledExecutor + + @Before + fun setup() { + executorService = SingleThreadTestScheduledExecutor() + } + + @Test + fun `last throwable from execution captured and executor can be reset`() { + val f1 = executorService.submit { throw RuntimeException() } + Thread.sleep(50L) + assertThrows(ExecutionException::class.java) { + f1.get(1L, TimeUnit.SECONDS) + } + + val t1 = executorService.lastThrowable() + checkNotNull(t1) + assertTrue("Last throwable is ${t1::class}, not RuntimeException", t1 is RuntimeException) + + executorService.reset() + assertNull(executorService.lastThrowable()) + + val f2 = executorService.submit { throw NotImplementedError() } + Thread.sleep(50L) + assertThrows(ExecutionException::class.java) { + f2.get(1L, TimeUnit.SECONDS) + } + + val t2 = executorService.lastThrowable() + checkNotNull(t2) + assertTrue("Last throwable is ${t2::class}, not NotImplementedError", t2 is NotImplementedError) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/ApplicationExitInfoRemoteConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/ApplicationExitInfoRemoteConfigTest.kt new file mode 100644 index 0000000000..4f81fc1a56 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/ApplicationExitInfoRemoteConfigTest.kt @@ -0,0 +1,46 @@ +package io.embrace.android.embracesdk.config + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.behavior.AppExitInfoBehavior.Companion.AEI_MAX_NUM_DEFAULT +import io.embrace.android.embracesdk.config.remote.AppExitInfoConfig +import org.junit.Assert +import org.junit.Test + +internal class ApplicationExitInfoRemoteConfigTest { + + @Test + fun testDefaults() { + val appExitInfoConfig = AppExitInfoConfig() + Assert.assertEquals(AEI_MAX_NUM_DEFAULT, appExitInfoConfig.aeiMaxNum) + } + + @Test + fun testOverride() { + val appExitInfoConfig = AppExitInfoConfig( + 100, + 100f, + 50 + ) + Assert.assertEquals(100, appExitInfoConfig.appExitInfoTracesLimit) + Assert.assertEquals(100f, appExitInfoConfig.pctAeiCaptureEnabled) + Assert.assertEquals(50, appExitInfoConfig.aeiMaxNum) + } + + @Test + fun testDeserialization() { + val data = ResourceReader.readResourceAsText("application_exit_info_remote_config.json") + val appExitInfoConfig = Gson().fromJson(data, AppExitInfoConfig::class.java) + Assert.assertEquals(100, appExitInfoConfig.appExitInfoTracesLimit) + Assert.assertEquals(100f, appExitInfoConfig.pctAeiCaptureEnabled) + Assert.assertEquals(50, appExitInfoConfig.aeiMaxNum) + } + + @Test + fun testEmptyObject() { + val appExitInfoConfig = Gson().fromJson("{}", AppExitInfoConfig::class.java) + Assert.assertNull(appExitInfoConfig.appExitInfoTracesLimit) + Assert.assertNull(appExitInfoConfig.pctAeiCaptureEnabled) + Assert.assertEquals(AEI_MAX_NUM_DEFAULT, appExitInfoConfig.aeiMaxNum) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/BgActivityConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/BgActivityConfigTest.kt new file mode 100644 index 0000000000..e4a37ae0f2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/BgActivityConfigTest.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk.config + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.remote.BackgroundActivityRemoteConfig +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class BgActivityConfigTest { + + @Test + fun testDefaults() { + val cfg = BackgroundActivityRemoteConfig(null) + assertNull(cfg.threshold) + } + + @Test + fun testOverride() { + val cfg = BackgroundActivityRemoteConfig( + 5f, + ) + assertEquals(5f, cfg.threshold) + } + + @Test + fun testDeserialization() { + val data = ResourceReader.readResourceAsText("bg_activity_config.json") + val cfg = Gson().fromJson(data, BackgroundActivityRemoteConfig::class.java) + assertEquals(0.5f, cfg.threshold) + } + + @Test + fun testDeserializationEmptyObj() { + val cfg = Gson().fromJson("{}", BackgroundActivityRemoteConfig::class.java) + assertNull(cfg.threshold) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/KillSwitchRemoteConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/KillSwitchRemoteConfigTest.kt new file mode 100644 index 0000000000..532927d1a9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/KillSwitchRemoteConfigTest.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.config + +import io.embrace.android.embracesdk.config.remote.KillSwitchRemoteConfig +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class KillSwitchRemoteConfigTest { + + @Test + fun isSigHandlerDetectionEnabled() { + assertFalse(checkNotNull(KillSwitchRemoteConfig(false).sigHandlerDetection)) + } + + @Test + fun ofDefault() { + assertNull(KillSwitchRemoteConfig().sigHandlerDetection) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/LogRemoteConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/LogRemoteConfigTest.kt new file mode 100644 index 0000000000..2e5e033221 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/LogRemoteConfigTest.kt @@ -0,0 +1,54 @@ +package io.embrace.android.embracesdk.config + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.remote.LogRemoteConfig +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class LogRemoteConfigTest { + + @Test + fun testDefaults() { + val cfg = LogRemoteConfig(null, null, null, null) + verifyDefaults(cfg) + } + + @Test + fun testOverride() { + val cfg = LogRemoteConfig( + 768, + 50, + 200, + 500, + ) + assertEquals(768, cfg.logMessageMaximumAllowedLength) + assertEquals(50, cfg.logInfoLimit) + assertEquals(200, cfg.logWarnLimit) + assertEquals(500, cfg.logErrorLimit) + } + + @Test + fun testDeserialization() { + val data = ResourceReader.readResourceAsText("log_config.json") + val cfg = Gson().fromJson(data, LogRemoteConfig::class.java) + assertEquals(768, cfg.logMessageMaximumAllowedLength) + assertEquals(50, cfg.logInfoLimit) + assertEquals(200, cfg.logWarnLimit) + assertEquals(500, cfg.logErrorLimit) + } + + @Test + fun testDeserializationEmptyObj() { + val cfg = Gson().fromJson("{}", LogRemoteConfig::class.java) + verifyDefaults(cfg) + } + + private fun verifyDefaults(cfg: LogRemoteConfig) { + assertNull(cfg.logMessageMaximumAllowedLength) + assertNull(cfg.logInfoLimit) + assertNull(cfg.logWarnLimit) + assertNull(cfg.logErrorLimit) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/NetworkRemoteConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/NetworkRemoteConfigTest.kt new file mode 100644 index 0000000000..0edb8ae8aa --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/NetworkRemoteConfigTest.kt @@ -0,0 +1,47 @@ +package io.embrace.android.embracesdk.config + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.remote.NetworkRemoteConfig +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class NetworkRemoteConfigTest { + + @Test + fun testDefaults() { + val cfg = NetworkRemoteConfig(null, null) + verifyDefaults(cfg) + } + + @Test + fun testOverride() { + val cfg = NetworkRemoteConfig( + 2000, + mapOf("google.com" to 500) + ) + assertEquals(2000, cfg.defaultCaptureLimit) + assertEquals(mapOf("google.com" to 500), cfg.domainLimits) + } + + @Test + fun testDeserialization() { + val data = ResourceReader.readResourceAsText("network_config.json") + val cfg = Gson().fromJson(data, NetworkRemoteConfig::class.java) + + assertEquals(2000, cfg.defaultCaptureLimit) + assertEquals(mapOf("google.com" to 500), cfg.domainLimits) + } + + @Test + fun testDeserializationEmptyObj() { + val cfg = Gson().fromJson("{}", NetworkRemoteConfig::class.java) + verifyDefaults(cfg) + } + + private fun verifyDefaults(cfg: NetworkRemoteConfig) { + assertNull(cfg.defaultCaptureLimit) + assertNull(cfg.domainLimits) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/UiRemoteConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/UiRemoteConfigTest.kt new file mode 100644 index 0000000000..4fd624e6be --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/UiRemoteConfigTest.kt @@ -0,0 +1,58 @@ +package io.embrace.android.embracesdk.config + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.remote.UiRemoteConfig +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class UiRemoteConfigTest { + + @Test + fun testDefaults() { + val cfg = UiRemoteConfig(null, null, null, null, null) + verifyDefaults(cfg) + } + + @Test + fun testOverride() { + val cfg = UiRemoteConfig( + 100, + 50, + 200, + 500, + 300 + ) + assertEquals(100, cfg.breadcrumbs) + assertEquals(50, cfg.taps) + assertEquals(200, cfg.views) + assertEquals(500, cfg.webViews) + assertEquals(300, cfg.fragments) + } + + @Test + fun testDeserialization() { + val data = ResourceReader.readResourceAsText("ui_config.json") + val cfg = Gson().fromJson(data, UiRemoteConfig::class.java) + assertEquals(80, cfg.breadcrumbs) + assertEquals(50, cfg.taps) + assertEquals(200, cfg.views) + assertEquals(500, cfg.webViews) + assertEquals(300, cfg.fragments) + } + + @Test + fun testDeserializationEmptyObj() { + val cfg = Gson().fromJson("{}", UiRemoteConfig::class.java) + verifyDefaults(cfg) + } + + private fun verifyDefaults(cfg: UiRemoteConfig) { + assertNull(cfg.breadcrumbs) + assertNull(cfg.taps) + assertNull(cfg.views) + assertNull(cfg.webViews) + assertNull(cfg.fragments) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AnrBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AnrBehaviorTest.kt new file mode 100644 index 0000000000..1d1a99420a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AnrBehaviorTest.kt @@ -0,0 +1,135 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.AnrLocalConfig +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig.Unwinder +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.regex.Pattern + +internal class AnrBehaviorTest { + + private val local = AnrLocalConfig( + captureGoogle = true, + captureUnityThread = true + ) + + private val remote = AnrRemoteConfig( + pctEnabled = 0, + pctAnrProcessErrorsEnabled = 100, + pctBgEnabled = 100, + sampleIntervalMs = 200, + anrProcessErrorsIntervalMs = 300, + anrProcessErrorsDelayMs = 2000, + anrProcessErrorsSchedulerExtraTimeAllowance = 50000, + maxStacktracesPerInterval = 120, + stacktraceFrameLimit = 300, + anrPerSession = 10, + mainThreadOnly = false, + minThreadPriority = 2, + minDuration = 2000, + allowList = listOf("test"), + blockList = listOf("test2"), + nativeThreadAnrSamplingFactor = 2, + nativeThreadAnrSamplingUnwinder = "libunwindstack", + pctNativeThreadAnrSamplingEnabled = 100.0f, + nativeThreadAnrSamplingOffsetEnabled = false, + pctIdleHandlerEnabled = 100.0f, + pctStrictModeListenerEnabled = 100.0f, + strictModeViolationLimit = 209, + ignoreNativeThreadAnrSamplingAllowlist = false, + nativeThreadAnrSamplingAllowlist = listOf( + AnrRemoteConfig.AllowedNdkSampleMethod( + "MyFoo", + "bar" + ) + ), + googlePctEnabled = 0, + monitorThreadPriority = 3 + ) + + @Test + fun testDefaults() { + with(fakeAnrBehavior()) { + assertFalse(isGoogleAnrCaptureEnabled()) + assertEquals(100L, getSamplingIntervalMs()) + assertTrue(shouldCaptureMainThreadOnly()) + assertEquals(25, getStrictModeViolationLimit()) + assertTrue(isNativeThreadAnrSamplingOffsetEnabled()) + assertTrue(isNativeThreadAnrSamplingAllowlistIgnored()) + assertEquals(1000, getAnrProcessErrorsIntervalMs()) + assertEquals(5000, getAnrProcessErrorsDelayMs()) + assertEquals(30000, getAnrProcessErrorsSchedulerExtraTimeAllowanceMs()) + assertEquals(80, getMaxStacktracesPerInterval()) + assertEquals(100, getStacktraceFrameLimit()) + assertEquals(5, getMaxAnrIntervalsPerSession()) + assertEquals(0, getMinThreadPriority()) + assertEquals(1000, getMinDuration()) + assertEquals(0, getMonitorThreadPriority()) + assertFalse(isIdleHandlerEnabled()) + assertFalse(isStrictModeListenerEnabled()) + assertFalse(isBgAnrCaptureEnabled()) + assertFalse(isNativeThreadAnrSamplingEnabled()) + assertFalse(isAnrProcessErrorsCaptureEnabled()) + assertTrue(isAnrCaptureEnabled()) + assertEquals(Unwinder.LIBUNWIND, getNativeThreadAnrSamplingUnwinder()) + assertEquals(500, getNativeThreadAnrSamplingIntervalMs()) + assertEquals(5, getNativeThreadAnrSamplingFactor()) + assertEquals(emptyList(), allowPatternList) + assertEquals(emptyList(), blockPatternList) + + val expected = AnrRemoteConfig.AllowedNdkSampleMethod("UnityPlayer", "pauseUnity") + val observed = getNativeThreadAnrSamplingAllowlist().single() + assertEquals(expected.clz, observed.clz) + assertEquals(expected.method, observed.method) + } + } + + @Test + fun testLocalOnly() { + with(fakeAnrBehavior(localCfg = { local })) { + assertTrue(isGoogleAnrCaptureEnabled()) + assertTrue(isNativeThreadAnrSamplingEnabled()) + } + } + + @Test + fun testRemoteAndLocal() { + with(fakeAnrBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertFalse(isGoogleAnrCaptureEnabled()) + assertEquals(200L, getSamplingIntervalMs()) + assertFalse(shouldCaptureMainThreadOnly()) + assertEquals(209, getStrictModeViolationLimit()) + assertFalse(isNativeThreadAnrSamplingOffsetEnabled()) + assertFalse(isNativeThreadAnrSamplingAllowlistIgnored()) + assertEquals(300, getAnrProcessErrorsIntervalMs()) + assertEquals(2000, getAnrProcessErrorsDelayMs()) + assertEquals(50000, getAnrProcessErrorsSchedulerExtraTimeAllowanceMs()) + assertEquals(120, getMaxStacktracesPerInterval()) + assertEquals(300, getStacktraceFrameLimit()) + assertEquals(10, getMaxAnrIntervalsPerSession()) + assertEquals(2, getMinThreadPriority()) + assertEquals(2000, getMinDuration()) + assertEquals(3, getMonitorThreadPriority()) + assertTrue(isIdleHandlerEnabled()) + assertTrue(isStrictModeListenerEnabled()) + assertTrue(isBgAnrCaptureEnabled()) + assertTrue(isNativeThreadAnrSamplingEnabled()) + assertTrue(isAnrProcessErrorsCaptureEnabled()) + assertFalse(isAnrCaptureEnabled()) + assertEquals(Unwinder.LIBUNWINDSTACK, getNativeThreadAnrSamplingUnwinder()) + assertEquals(400, getNativeThreadAnrSamplingIntervalMs()) + assertEquals(2, getNativeThreadAnrSamplingFactor()) + assertEquals("test", allowPatternList.single().pattern()) + assertEquals("test2", blockPatternList.single().pattern()) + + val expected = AnrRemoteConfig.AllowedNdkSampleMethod("MyFoo", "bar") + val observed = getNativeThreadAnrSamplingAllowlist().single() + assertEquals(expected.clz, observed.clz) + assertEquals(expected.method, observed.method) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehaviorTest.kt new file mode 100644 index 0000000000..fb0dc73aed --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AppExitInfoBehaviorTest.kt @@ -0,0 +1,43 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.AppExitInfoLocalConfig +import io.embrace.android.embracesdk.config.remote.AppExitInfoConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.fakeAppExitInfoBehavior +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class AppExitInfoBehaviorTest { + + private val local = AppExitInfoLocalConfig(33792, false) + + private val remote = RemoteConfig( + appExitInfoConfig = AppExitInfoConfig(55209, 100f) + ) + + @Test + fun testDefaults() { + with(fakeAppExitInfoBehavior()) { + assertEquals(2097152, getTraceMaxLimit()) + assertTrue(isEnabled()) + } + } + + @Test + fun testLocalOnly() { + with(fakeAppExitInfoBehavior(localCfg = { local })) { + assertEquals(33792, getTraceMaxLimit()) + assertFalse(isEnabled()) + } + } + + @Test + fun testLocalAndRemote() { + with(fakeAppExitInfoBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertEquals(55209, getTraceMaxLimit()) + assertTrue(isEnabled()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehaviorTest.kt new file mode 100644 index 0000000000..a98efd680a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/AutoDataCaptureBehaviorTest.kt @@ -0,0 +1,116 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.EmbraceConfigServiceTest.Companion.createLocalConfig +import io.embrace.android.embracesdk.config.local.AppLocalConfig +import io.embrace.android.embracesdk.config.local.AutomaticDataCaptureLocalConfig +import io.embrace.android.embracesdk.config.local.ComposeLocalConfig +import io.embrace.android.embracesdk.config.local.CrashHandlerLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.KillSwitchRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class AutoDataCaptureBehaviorTest { + + private val local = LocalConfig( + appId = "", + ndkEnabled = true, + sdkConfig = SdkLocalConfig( + automaticDataCaptureConfig = AutomaticDataCaptureLocalConfig( + memoryServiceEnabled = false, + powerSaveModeServiceEnabled = false, + networkConnectivityServiceEnabled = false, + anrServiceEnabled = false + ), + crashHandler = CrashHandlerLocalConfig(false), + composeConfig = ComposeLocalConfig(true), + app = AppLocalConfig(reportDiskUsage = false) + ), + ) + + private val remote = RemoteConfig( + killSwitchConfig = KillSwitchRemoteConfig(sigHandlerDetection = false, jetpackCompose = false) + ) + + @Test + fun testDefaults() { + with(fakeAutoDataCaptureBehavior()) { + assertTrue(isMemoryServiceEnabled()) + assertTrue(isPowerSaveModeServiceEnabled()) + assertTrue(isNetworkConnectivityServiceEnabled()) + assertTrue(isAnrServiceEnabled()) + assertTrue(isUncaughtExceptionHandlerEnabled()) + assertFalse(isComposeOnClickEnabled()) + assertTrue(isSigHandlerDetectionEnabled()) + assertFalse(isNdkEnabled()) + assertTrue(isDiskUsageReportingEnabled()) + } + } + + @Test + fun testLocalOnly() { + with(fakeAutoDataCaptureBehavior(localCfg = { local })) { + assertFalse(isMemoryServiceEnabled()) + assertFalse(isPowerSaveModeServiceEnabled()) + assertFalse(isNetworkConnectivityServiceEnabled()) + assertFalse(isAnrServiceEnabled()) + assertFalse(isUncaughtExceptionHandlerEnabled()) + assertTrue(isComposeOnClickEnabled()) + assertTrue(isSigHandlerDetectionEnabled()) + assertTrue(isNdkEnabled()) + assertFalse(isDiskUsageReportingEnabled()) + } + } + + @Test + fun testLocalAndRemote() { + with(fakeAutoDataCaptureBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertFalse(isSigHandlerDetectionEnabled()) + assertFalse(isComposeOnClickEnabled()) + } + } + + @Test + fun testJetpackCompose() { + // Jetpack Compose is disabled by default + with(fakeAutoDataCaptureBehavior()) { + assertFalse(isComposeOnClickEnabled()) + } + + // Jetpack Compose is enabled locally, no remote config + with(fakeAutoDataCaptureBehavior(localCfg = { local })) { + assertTrue(isComposeOnClickEnabled()) + } + + // Jetpack Compose disabled remotely, overrides local: killswitch + with(fakeAutoDataCaptureBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertFalse(isComposeOnClickEnabled()) + } + + val localComposeOff = createLocalConfig { + SdkLocalConfig( + composeConfig = ComposeLocalConfig( + false + ) + ) + } + + val remoteComposeKillSwitchOff = RemoteConfig( + killSwitchConfig = KillSwitchRemoteConfig(sigHandlerDetection = false, jetpackCompose = true) + ) + + // Jetpack Compose enabled remotely, but explicit disabled locally, remote ignored + with(fakeAutoDataCaptureBehavior(localCfg = { localComposeOff }, remoteCfg = { remoteComposeKillSwitchOff })) { + assertFalse(isComposeOnClickEnabled()) + } + + // Jetpack Compose enabled remotely, and explicit enabled locally + with(fakeAutoDataCaptureBehavior(localCfg = { local }, remoteCfg = { remoteComposeKillSwitchOff })) { + assertTrue(isComposeOnClickEnabled()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehaviorTest.kt new file mode 100644 index 0000000000..3de03a8e5d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BackgroundActivityBehaviorTest.kt @@ -0,0 +1,53 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.BackgroundActivityLocalConfig +import io.embrace.android.embracesdk.config.remote.BackgroundActivityRemoteConfig +import io.embrace.android.embracesdk.fakes.fakeBackgroundActivityBehavior +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class BackgroundActivityBehaviorTest { + + private val local = BackgroundActivityLocalConfig( + true, + 50, + 3000L, + 50 + ) + + private val remote = BackgroundActivityRemoteConfig( + 0f + ) + + @Test + fun testDefaults() { + with(fakeBackgroundActivityBehavior()) { + assertFalse(isEnabled()) + assertEquals(100, getManualBackgroundActivityLimit()) + assertEquals(5000L, getMinBackgroundActivityDuration()) + assertEquals(30, getMaxCachedActivities()) + } + } + + @Test + fun testLocalOnly() { + with(fakeBackgroundActivityBehavior(localCfg = { local })) { + assertTrue(isEnabled()) + assertEquals(50, getManualBackgroundActivityLimit()) + assertEquals(3000L, getMinBackgroundActivityDuration()) + assertEquals(50, getMaxCachedActivities()) + } + } + + @Test + fun testRemoteAndLocal() { + with(fakeBackgroundActivityBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertFalse(isEnabled()) + assertEquals(50, getManualBackgroundActivityLimit()) + assertEquals(3000L, getMinBackgroundActivityDuration()) + assertEquals(50, getMaxCachedActivities()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehaviorTest.kt new file mode 100644 index 0000000000..a5d285d68f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/BreadcrumbBehaviorTest.kt @@ -0,0 +1,71 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.local.TapsLocalConfig +import io.embrace.android.embracesdk.config.local.ViewLocalConfig +import io.embrace.android.embracesdk.config.local.WebViewLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.UiRemoteConfig +import io.embrace.android.embracesdk.fakes.fakeBreadcrumbBehavior +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class BreadcrumbBehaviorTest { + + private val remote = RemoteConfig( + uiConfig = UiRemoteConfig( + 99, + 98, + 97, + 96, + 95 + ) + ) + + private val local = SdkLocalConfig( + taps = TapsLocalConfig(false), + viewConfig = ViewLocalConfig(false), + webViewConfig = WebViewLocalConfig(captureWebViews = false, captureQueryParams = false), + captureFcmPiiData = true + ) + + @Test + fun testDefaults() { + with(fakeBreadcrumbBehavior()) { + assertEquals(100, getCustomBreadcrumbLimit()) + assertEquals(100, getTapBreadcrumbLimit()) + assertEquals(100, getViewBreadcrumbLimit()) + assertEquals(100, getWebViewBreadcrumbLimit()) + assertEquals(100, getFragmentBreadcrumbLimit()) + assertTrue(isTapCoordinateCaptureEnabled()) + assertTrue(isActivityBreadcrumbCaptureEnabled()) + assertTrue(isWebViewBreadcrumbCaptureEnabled()) + assertTrue(isQueryParamCaptureEnabled()) + assertFalse(isCaptureFcmPiiDataEnabled()) + } + } + + @Test + fun testLocalOnly() { + with(fakeBreadcrumbBehavior(localCfg = { local })) { + assertFalse(isTapCoordinateCaptureEnabled()) + assertFalse(isActivityBreadcrumbCaptureEnabled()) + assertFalse(isWebViewBreadcrumbCaptureEnabled()) + assertFalse(isQueryParamCaptureEnabled()) + assertTrue(isCaptureFcmPiiDataEnabled()) + } + } + + @Test + fun testRemoteAndLocal() { + with(fakeBreadcrumbBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertEquals(99, getCustomBreadcrumbLimit()) + assertEquals(98, getTapBreadcrumbLimit()) + assertEquals(97, getViewBreadcrumbLimit()) + assertEquals(96, getWebViewBreadcrumbLimit()) + assertEquals(95, getFragmentBreadcrumbLimit()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehaviorTest.kt new file mode 100644 index 0000000000..bdfa32a76a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/DataCaptureEventBehaviorTest.kt @@ -0,0 +1,45 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.fakeDataCaptureEventBehavior +import io.embrace.android.embracesdk.internal.MessageType +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class DataCaptureEventBehaviorTest { + + private val remote = RemoteConfig( + internalExceptionCaptureEnabled = false, + disabledMessageTypes = setOf("event"), + disabledEventAndLogPatterns = setOf("my_event", "my_log"), + eventLimits = mapOf("test" to 100) + ) + + @Test + fun testDefaults() { + with(fakeDataCaptureEventBehavior()) { + assertTrue(isMessageTypeEnabled(MessageType.EVENT)) + assertTrue(isInternalExceptionCaptureEnabled()) + assertTrue(isEventEnabled("my_event")) + assertTrue(isEventEnabled("other_event")) + assertTrue(isLogMessageEnabled("my_log")) + assertTrue(isLogMessageEnabled("other_log")) + assertEquals(mapOf(), getEventLimits()) + } + } + + @Test + fun testRemoteOnly() { + with(fakeDataCaptureEventBehavior(remoteCfg = { remote })) { + assertFalse(isMessageTypeEnabled(MessageType.EVENT)) + assertFalse(isInternalExceptionCaptureEnabled()) + assertFalse(isEventEnabled("my_event")) + assertTrue(isEventEnabled("other_event")) + assertFalse(isLogMessageEnabled("my_log")) + assertTrue(isLogMessageEnabled("other_log")) + assertEquals(100L, getEventLimits()["test"]) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehaviorTest.kt new file mode 100644 index 0000000000..6142f686ff --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/LogMessageBehaviorTest.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.LogRemoteConfig +import io.embrace.android.embracesdk.fakes.fakeLogMessageBehavior +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class LogMessageBehaviorTest { + + private val remote = LogRemoteConfig( + 256, + 200, + 300, + 400 + ) + + @Test + fun testDefaults() { + with(fakeLogMessageBehavior()) { + assertEquals(128, getLogMessageMaximumAllowedLength()) + assertEquals(100, getInfoLogLimit()) + assertEquals(100, getWarnLogLimit()) + assertEquals(250, getErrorLogLimit()) + } + } + + @Test + fun testRemoteAndLocal() { + with(fakeLogMessageBehavior(remoteCfg = { remote })) { + assertEquals(256, getLogMessageMaximumAllowedLength()) + assertEquals(200, getInfoLogLimit()) + assertEquals(300, getWarnLogLimit()) + assertEquals(400, getErrorLogLimit()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkBehaviorTest.kt new file mode 100644 index 0000000000..57b7fe413e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkBehaviorTest.kt @@ -0,0 +1,134 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.config.local.DomainLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.NetworkLocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.NetworkCaptureRuleRemoteConfig +import io.embrace.android.embracesdk.config.remote.NetworkRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.fakeNetworkBehavior +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class NetworkBehaviorTest { + + companion object { + private const val testCleanPublicKey = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE" + + "AuAZAv5tzK9Ab/DsVpNaYiuslKQsOHjz4N4haZLT8VaVIrlVjtkd5nPrVgEKStQf6PKn" + + "Q+1C0Tp069b6aPUkG22UL96nCKQ1eCIwRUT+Da7ac2YVuL21+HTs1KxLEWgN7qGy1uYN" + + "onrpsiY3XqzDvYMo65oFzbBV+yctuGHDFaulULJiLL8cE3/Rg3T0RfHK+C5/PqC8FBj6" + + "kn3FP9FZJM4cty3nzbNWknj8r7+ikmOwma6CHEZz2u1gwPhIchNxNKuUF+4vxcBre9V/" + + "96LYOjSOGSDJmJN6ehUJjUpu7YSuGCki8YoLHAyoD/mYy7N/hYSeZwHiNjM+r44lZHNQ" + + "TpwIDAQAB" + } + + private val local = SdkLocalConfig( + networking = NetworkLocalConfig( + traceIdHeader = "x-custom-trace", + captureRequestContentLength = true, + enableNativeMonitoring = false, + domains = listOf( + DomainLocalConfig( + "google.com", + 100 + ) + ), + disabledUrlPatterns = listOf("google.com"), + defaultCaptureLimit = 220, + ), + capturePublicKey = "test" + ) + + private val remote = RemoteConfig( + networkConfig = NetworkRemoteConfig( + defaultCaptureLimit = 409, + domainLimits = mapOf( + "google.com" to 50 + ) + ), + disabledUrlPatterns = setOf("example.com"), + networkCaptureRules = setOf( + NetworkCaptureRuleRemoteConfig( + "test", + 5000, + "GET", + "google.com", + ) + ) + ) + + @Test + fun testDefaults() { + with(fakeNetworkBehavior(localCfg = { null }, remoteCfg = { null })) { + assertEquals("x-emb-trace-id", getTraceIdHeader()) + assertFalse(isRequestContentLengthCaptureEnabled()) + assertTrue(isNativeNetworkingMonitoringEnabled()) + assertEquals(1000, getNetworkCaptureLimit()) + assertEquals(emptyMap(), getNetworkCallLimitsPerDomain()) + assertTrue(isUrlEnabled("google.com")) + assertFalse(isCaptureBodyEncryptionEnabled()) + assertNull(getCapturePublicKey()) + assertEquals(emptySet(), getNetworkCaptureRules()) + } + } + + @Test + fun testLocalOnly() { + with(fakeNetworkBehavior(localCfg = { local }, remoteCfg = { null })) { + assertEquals("x-custom-trace", getTraceIdHeader()) + assertTrue(isRequestContentLengthCaptureEnabled()) + assertFalse(isNativeNetworkingMonitoringEnabled()) + assertEquals(mapOf("google.com" to 100), getNetworkCallLimitsPerDomain()) + assertEquals(220, getNetworkCaptureLimit()) + assertFalse(isUrlEnabled("google.com")) + assertTrue(isCaptureBodyEncryptionEnabled()) + assertEquals("test", getCapturePublicKey()) + } + } + + @Test + fun testRemoteAndLocal() { + with(fakeNetworkBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertEquals(409, getNetworkCaptureLimit()) + assertEquals(mapOf("google.com" to 50), getNetworkCallLimitsPerDomain()) + assertTrue(isUrlEnabled("google.com")) + assertFalse(isUrlEnabled("example.com")) + assertEquals( + NetworkCaptureRuleRemoteConfig( + "test", + 5000, + "GET", + "google.com", + ), + getNetworkCaptureRules().single() + ) + } + } + + @Test + fun testNetworkingInvalidDisabledRegexIgnored() { + val cfg = SdkLocalConfig( + networking = NetworkLocalConfig( + disabledUrlPatterns = listOf("a.b.c", "invalid[}regex") + ) + ) + with(fakeNetworkBehavior(localCfg = { cfg })) { + assertTrue(isUrlEnabled("invalid[}regex")) + } + } + + @Test + fun testGetCapturePublicKey() { + val json = ResourceReader.readResourceAsText("public_key_config.json") + val localConfig = LocalConfig.buildConfig("aaa", false, json, EmbraceSerializer()) + val behavior = fakeNetworkBehavior(localCfg = localConfig::sdkConfig) + assertEquals(testCleanPublicKey, behavior.getCapturePublicKey()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehaviorTest.kt new file mode 100644 index 0000000000..eb276679e0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/NetworkSpanForwardingBehaviorTest.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.NetworkSpanForwardingRemoteConfig +import io.embrace.android.embracesdk.fakes.fakeNetworkSpanForwardingBehavior +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class NetworkSpanForwardingBehaviorTest { + + @Test + fun testDefault() { + with(fakeNetworkSpanForwardingBehavior()) { + assertFalse(isNetworkSpanForwardingEnabled()) + } + } + + @Test + fun testRemote() { + with(fakeNetworkSpanForwardingBehavior(remoteConfig = { remoteEnabled })) { + assertTrue(isNetworkSpanForwardingEnabled()) + } + + with(fakeNetworkSpanForwardingBehavior(remoteConfig = { remoteDisabled })) { + assertFalse(isNetworkSpanForwardingEnabled()) + } + } + + companion object { + private val remoteEnabled = NetworkSpanForwardingRemoteConfig(pctEnabled = 100.0f) + private val remoteDisabled = NetworkSpanForwardingRemoteConfig(pctEnabled = 0.0f) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehaviorTest.kt new file mode 100644 index 0000000000..c40db50435 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkEndpointBehaviorTest.kt @@ -0,0 +1,34 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.BaseUrlLocalConfig +import io.embrace.android.embracesdk.fakes.fakeSdkEndpointBehavior +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class SdkEndpointBehaviorTest { + + private val local = BaseUrlLocalConfig( + "https://config.example.com", + "https://data.example.com", + "https://data-dev.example.com", + "https://images.example.com" + ) + + @Test + fun testDefaults() { + with(fakeSdkEndpointBehavior()) { + assertEquals("https://a-12345.config.emb-api.com", getConfig("12345")) + assertEquals("https://a-12345.data.emb-api.com", getData("12345")) + assertEquals("https://a-12345.data-dev.emb-api.com", getDataDev("12345")) + } + } + + @Test + fun testLocalOnly() { + with(fakeSdkEndpointBehavior(localCfg = { local })) { + assertEquals("https://config.example.com", getConfig("12345")) + assertEquals("https://data.example.com", getData("12345")) + assertEquals("https://data-dev.example.com", getDataDev("12345")) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehaviorTest.kt new file mode 100644 index 0000000000..50733f5f43 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SdkModeBehaviorTest.kt @@ -0,0 +1,138 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.fakeSdkModeBehavior +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class SdkModeBehaviorTest { + + private val local = LocalConfig( + "", false, + SdkLocalConfig( + integrationModeEnabled = true, + betaFeaturesEnabled = true + ) + ) + + // 100% enabled + private val enabled = BehaviorThresholdCheck { "07D85B44E4E245F4A30E559BFC000000" } + + // ~50% enabled + private val halfEnabled = BehaviorThresholdCheck { "07D85B44E4E245F4A30E559BFC888888" } + + // 0% enabled + private val disabled = BehaviorThresholdCheck { "07D85B44E4E245F4A30E559BFCFFFFFF" } + + @Test + fun testDefaults() { + with( + fakeSdkModeBehavior( + thresholdCheck = disabled + ) + ) { + assertFalse(isIntegrationModeEnabled()) + assertFalse(isBetaFeaturesEnabled()) + assertFalse(isSdkDisabled()) + } + } + + @Test + fun testLocalOnly() { + with( + fakeSdkModeBehavior( + thresholdCheck = enabled, + localCfg = { local } + ) + ) { + assertTrue(isIntegrationModeEnabled()) + assertTrue(isBetaFeaturesEnabled()) + } + } + + @Test + fun testBetaFeaturesEnabled() { + var behavior = fakeSdkModeBehavior( + thresholdCheck = enabled + ) + assertTrue(behavior.isBetaFeaturesEnabled()) + + behavior = fakeSdkModeBehavior( + thresholdCheck = disabled + ) + assertFalse(behavior.isBetaFeaturesEnabled()) + + behavior = fakeSdkModeBehavior( + thresholdCheck = enabled, + localCfg = { LocalConfig("", false, SdkLocalConfig(betaFeaturesEnabled = false)) } + ) + assertFalse(behavior.isBetaFeaturesEnabled()) + + behavior = + fakeSdkModeBehavior( + thresholdCheck = enabled, + localCfg = { local }, + remoteCfg = { RemoteConfig(pctBetaFeaturesEnabled = 100f) } + ) + assertTrue(behavior.isBetaFeaturesEnabled()) + + behavior = + fakeSdkModeBehavior( + thresholdCheck = disabled, + localCfg = { local }, + remoteCfg = { RemoteConfig(pctBetaFeaturesEnabled = 0f) } + ) + assertFalse(behavior.isBetaFeaturesEnabled()) + } + + @Test + fun testMetadataDebug() { + val behaviorNotDebug = + fakeSdkModeBehavior( + isDebug = false, + thresholdCheck = disabled + ) + assertFalse(behaviorNotDebug.isBetaFeaturesEnabled()) + + val behaviorDebug = + fakeSdkModeBehavior( + isDebug = true, + thresholdCheck = disabled + ) + assertTrue(behaviorDebug.isBetaFeaturesEnabled()) + } + + @Test + fun testSdkEnabled() { + // SDK disabled + var behavior = fakeSdkModeBehavior( + thresholdCheck = enabled, + remoteCfg = { RemoteConfig(threshold = 0) } + ) + assertTrue(behavior.isSdkDisabled()) + + // SDK enabled + behavior = fakeSdkModeBehavior( + thresholdCheck = enabled, + remoteCfg = { RemoteConfig(threshold = 100) } + ) + assertFalse(behavior.isSdkDisabled()) + + // SDK 30% enabled with default offset + behavior = fakeSdkModeBehavior( + thresholdCheck = halfEnabled, + remoteCfg = { RemoteConfig(threshold = 30) } + ) + assertTrue(behavior.isSdkDisabled()) + + // SDK 30% enabled with non-default offset + behavior = fakeSdkModeBehavior( + thresholdCheck = halfEnabled, + remoteCfg = { RemoteConfig(threshold = 30, offset = 25) } + ) + assertFalse(behavior.isSdkDisabled()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SessionBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SessionBehaviorTest.kt new file mode 100644 index 0000000000..e78e2e6533 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SessionBehaviorTest.kt @@ -0,0 +1,96 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.SessionLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SessionRemoteConfig +import io.embrace.android.embracesdk.fakes.fakeSessionBehavior +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class SessionBehaviorTest { + + private val local = SessionLocalConfig( + maxSessionSeconds = 120, + asyncEnd = true, + sessionComponents = setOf("breadcrumbs"), + fullSessionEvents = setOf("crash"), + sessionEnableErrorLogStrictMode = true + ) + + private val remote = RemoteConfig( + sessionConfig = SessionRemoteConfig( + isEnabled = true, + endAsync = false, + sessionComponents = setOf("test"), + fullSessionEvents = setOf("test2") + ), + maxSessionProperties = 57 + ) + + @Test + fun testDefaults() { + with(fakeSessionBehavior()) { + assertNull(getMaxSessionSecondsAllowed()) + assertFalse(isAsyncEndEnabled()) + assertFalse(isSessionErrorLogStrictModeEnabled()) + assertEquals(emptySet(), getFullSessionEvents()) + assertNull(getSessionComponents()) + assertFalse(isGatingFeatureEnabled()) + assertFalse(isSessionControlEnabled()) + assertEquals(10, getMaxSessionProperties()) + } + } + + @Test + fun testLocalOnly() { + with(fakeSessionBehavior(localCfg = { local })) { + assertEquals(120, getMaxSessionSecondsAllowed()) + assertTrue(isAsyncEndEnabled()) + assertTrue(isSessionErrorLogStrictModeEnabled()) + assertEquals(setOf("breadcrumbs"), getSessionComponents()) + assertEquals(setOf("crash"), getFullSessionEvents()) + assertTrue(isGatingFeatureEnabled()) + } + } + + @Test + fun testRemoteAndLocal() { + with(fakeSessionBehavior(localCfg = { local }, remoteCfg = { remote })) { + assertFalse(isAsyncEndEnabled()) + assertTrue(isGatingFeatureEnabled()) + assertTrue(isSessionControlEnabled()) + assertEquals(setOf("test"), getSessionComponents()) + assertEquals(setOf("test2"), getFullSessionEvents()) + assertEquals(57, getMaxSessionProperties()) + } + } + + @Test + fun `test upper case full session events`() { + val behavior = fakeSessionBehavior( + remoteCfg = { + buildGatingConfig(setOf("CRASHES", "ERRORS")) + } + ) + assertEquals(setOf("crashes", "errors"), behavior.getFullSessionEvents()) + } + + @Test + fun `test lower case full session events`() { + val behavior = fakeSessionBehavior( + remoteCfg = { + buildGatingConfig(setOf("crashes", "errors")) + } + ) + assertEquals(setOf("crashes", "errors"), behavior.getFullSessionEvents()) + } + + private fun buildGatingConfig(events: Set) = RemoteConfig( + sessionConfig = SessionRemoteConfig( + fullSessionEvents = events + ) + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SpansBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SpansBehaviorTest.kt new file mode 100644 index 0000000000..07709de4d2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/SpansBehaviorTest.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig +import io.embrace.android.embracesdk.fakes.fakeSpansBehavior +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class SpansBehaviorTest { + private val remote = SpansRemoteConfig( + 100.0f + ) + + @Test + fun testDefaults() { + with(fakeSpansBehavior()) { + assertFalse(isSpansEnabled()) + } + } + + @Test + fun testRemote() { + with(fakeSpansBehavior(remoteConfig = { remote })) { + assertTrue(isSpansEnabled()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/StartupBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/StartupBehaviorTest.kt new file mode 100644 index 0000000000..8da831219f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/StartupBehaviorTest.kt @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.local.StartupMomentLocalConfig +import io.embrace.android.embracesdk.fakes.fakeStartupBehavior +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class StartupBehaviorTest { + + private val local = StartupMomentLocalConfig( + automaticallyEnd = false + ) + + @Test + fun testDefaults() { + with(fakeStartupBehavior()) { + assertTrue(isAutomaticEndEnabled()) + } + } + + @Test + fun testLocalOnly() { + with(fakeStartupBehavior(localCfg = { local })) { + assertFalse(isAutomaticEndEnabled()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/WebVitalsBehaviorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/WebVitalsBehaviorTest.kt new file mode 100644 index 0000000000..0d9e8c6dda --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/behavior/WebVitalsBehaviorTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.embracesdk.config.behavior + +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.WebViewVitals +import io.embrace.android.embracesdk.fakes.fakeWebViewVitalsBehavior +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class WebVitalsBehaviorTest { + + private val remote = RemoteConfig(webViewVitals = WebViewVitals(0f, 100)) + + @Test + fun testDefaults() { + with(fakeWebViewVitalsBehavior()) { + assertTrue(isWebViewVitalsEnabled()) + assertEquals(300, getMaxWebViewVitals()) + } + } + + @Test + fun testRemote() { + with(fakeWebViewVitalsBehavior(remoteCfg = { remote })) { + assertEquals(100, getMaxWebViewVitals()) + assertFalse(isWebViewVitalsEnabled()) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AnrLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AnrLocalConfigTest.kt new file mode 100644 index 0000000000..e751af4b79 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AnrLocalConfigTest.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class AnrLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = AnrLocalConfig() + assertNull(cfg.captureGoogle) + assertNull(cfg.captureUnityThread) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("anr_config.json") + val obj = Gson().fromJson(json, AnrLocalConfig::class.java) + assertTrue(checkNotNull(obj.captureGoogle)) + assertTrue(checkNotNull(obj.captureUnityThread)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", AnrLocalConfig::class.java) + assertNull(obj.captureGoogle) + assertNull(obj.captureUnityThread) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AppLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AppLocalConfigTest.kt new file mode 100644 index 0000000000..0cc896eaa6 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AppLocalConfigTest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class AppLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = AppLocalConfig() + assertNull(cfg.reportDiskUsage) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("app_config.json") + val obj = Gson().fromJson(json, AppLocalConfig::class.java) + assertFalse(checkNotNull(obj.reportDiskUsage)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", AppLocalConfig::class.java) + assertNull(obj.reportDiskUsage) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ApplicationExitInfoLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ApplicationExitInfoLocalConfigTest.kt new file mode 100644 index 0000000000..206a76df52 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ApplicationExitInfoLocalConfigTest.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class ApplicationExitInfoLocalConfigTest { + + @Test + fun testDefaults() { + val appExitInfoLocalConfig = AppExitInfoLocalConfig() + assertNull(appExitInfoLocalConfig.appExitInfoTracesLimit) + assertNull(appExitInfoLocalConfig.aeiCaptureEnabled) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("application_exit_info_local_config.json") + val obj = Gson().fromJson(json, AppExitInfoLocalConfig::class.java) + Assert.assertEquals(10, obj.appExitInfoTracesLimit) + assertTrue(obj.aeiCaptureEnabled ?: false) + } + + @Test + fun testEmptyObject() { + val appExitInfoLocalConfig = Gson().fromJson("{}", AppExitInfoLocalConfig::class.java) + assertNull(appExitInfoLocalConfig.appExitInfoTracesLimit) + assertNull(appExitInfoLocalConfig.aeiCaptureEnabled) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfigTest.kt new file mode 100644 index 0000000000..882dceaab1 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/AutomaticDataCaptureLocalConfigTest.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class AutomaticDataCaptureLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = AutomaticDataCaptureLocalConfig() + verifyDefaults(cfg) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("auto_data_capture_config.json") + val obj = Gson().fromJson(json, AutomaticDataCaptureLocalConfig::class.java) + + assertFalse(checkNotNull(obj.anrServiceEnabled)) + assertFalse(checkNotNull(obj.memoryServiceEnabled)) + assertFalse(checkNotNull(obj.networkConnectivityServiceEnabled)) + assertFalse(checkNotNull(obj.powerSaveModeServiceEnabled)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", AutomaticDataCaptureLocalConfig::class.java) + verifyDefaults(obj) + } + + private fun verifyDefaults(cfg: AutomaticDataCaptureLocalConfig) { + assertNull(cfg.anrServiceEnabled) + assertNull(cfg.memoryServiceEnabled) + assertNull(cfg.networkConnectivityServiceEnabled) + assertNull(cfg.powerSaveModeServiceEnabled) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfigTest.kt new file mode 100644 index 0000000000..23da4ad039 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BackgroundActivityLocalConfigTest.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class BackgroundActivityLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = BackgroundActivityLocalConfig() + verifyDefaults(cfg) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("background_activity_config.json") + val obj = Gson().fromJson(json, BackgroundActivityLocalConfig::class.java) + assertTrue(checkNotNull(obj.backgroundActivityCaptureEnabled)) + assertEquals(15, obj.manualBackgroundActivityLimit) + assertEquals(10000L, obj.minBackgroundActivityDuration) + assertEquals(16, obj.maxCachedActivities) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", BackgroundActivityLocalConfig::class.java) + verifyDefaults(obj) + } + + private fun verifyDefaults(cfg: BackgroundActivityLocalConfig) { + assertNull(cfg.backgroundActivityCaptureEnabled) + assertNull(cfg.manualBackgroundActivityLimit) + assertNull(cfg.minBackgroundActivityDuration) + assertNull(cfg.maxCachedActivities) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfigTest.kt new file mode 100644 index 0000000000..406a7825b0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/BaseUrlLocalConfigTest.kt @@ -0,0 +1,39 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class BaseUrlLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = BaseUrlLocalConfig() + verifyDefaults(cfg) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("base_url_config.json") + val obj = Gson().fromJson(json, BaseUrlLocalConfig::class.java) + assertEquals("https://config.example.com", obj.config) + assertEquals("https://data.example.com", obj.data) + assertEquals("https://data-dev.example.com", obj.dataDev) + assertEquals("https://images.example.com", obj.images) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", BaseUrlLocalConfig::class.java) + verifyDefaults(obj) + } + + private fun verifyDefaults(obj: BaseUrlLocalConfig) { + assertNull("https://config.emb-api.com", obj.config) + assertNull("https://data.emb-api.com", obj.data) + assertNull("https://data-dev.emb-api.com", obj.dataDev) + assertNull("https://images.emb-api.com", obj.images) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfigTest.kt new file mode 100644 index 0000000000..93dda79528 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/CrashHandlerLocalConfigTest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class CrashHandlerLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = CrashHandlerLocalConfig() + assertNull(cfg.enabled) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("crash_handler_config.json") + val obj = Gson().fromJson(json, CrashHandlerLocalConfig::class.java) + assertFalse(checkNotNull(obj.enabled)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", CrashHandlerLocalConfig::class.java) + assertNull(obj.enabled) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/DomainLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/DomainLocalConfigTest.kt new file mode 100644 index 0000000000..22d1313789 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/DomainLocalConfigTest.kt @@ -0,0 +1,35 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class DomainLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = DomainLocalConfig() + verifyDefaults(cfg) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("domain_config.json") + val obj = Gson().fromJson(json, DomainLocalConfig::class.java) + assertEquals("example-apis.com", obj.domain) + assertEquals(400, obj.limit) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", DomainLocalConfig::class.java) + verifyDefaults(obj) + } + + private fun verifyDefaults(cfg: DomainLocalConfig) { + assertNull(cfg.domain) + assertNull(cfg.limit) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfigTest.kt new file mode 100644 index 0000000000..06e680b83e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/NetworkLocalConfigTest.kt @@ -0,0 +1,46 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class NetworkLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = NetworkLocalConfig() + verifyDefaults(cfg) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("local_network_config.json") + val obj = Gson().fromJson(json, NetworkLocalConfig::class.java) + + assertEquals(200, obj.defaultCaptureLimit) + assertEquals(DomainLocalConfig("google.com", 80).domain, obj.domains?.single()?.domain) + assertEquals("x-my-header-id", obj.traceIdHeader) + assertEquals(1, obj.disabledUrlPatterns?.size) + assertTrue(checkNotNull(obj.captureRequestContentLength)) + assertFalse(checkNotNull(obj.enableNativeMonitoring)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", NetworkLocalConfig::class.java) + verifyDefaults(obj) + } + + private fun verifyDefaults(obj: NetworkLocalConfig) { + assertNull(obj.defaultCaptureLimit) + assertNull(obj.domains) + assertNull(obj.traceIdHeader) + assertNull(obj.disabledUrlPatterns) + assertNull(obj.captureRequestContentLength) + assertNull(obj.enableNativeMonitoring) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/SessionLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/SessionLocalConfigTest.kt new file mode 100644 index 0000000000..aca41abb12 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/SessionLocalConfigTest.kt @@ -0,0 +1,42 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class SessionLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = SessionLocalConfig() + verifyDefaults(cfg) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("session_config.json") + val obj = Gson().fromJson(json, SessionLocalConfig::class.java) + assertEquals(120, obj.maxSessionSeconds) + assertTrue(checkNotNull(obj.asyncEnd)) + assertTrue(checkNotNull(obj.sessionEnableErrorLogStrictMode)) + assertEquals(setOf("breadcrumbs"), obj.sessionComponents) + assertEquals(setOf("crash"), obj.fullSessionEvents) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", SessionLocalConfig::class.java) + verifyDefaults(obj) + } + + private fun verifyDefaults(cfg: SessionLocalConfig) { + assertNull(cfg.maxSessionSeconds) + assertNull(cfg.asyncEnd) + assertNull(cfg.sessionEnableErrorLogStrictMode) + assertNull(cfg.sessionComponents) + assertNull(cfg.fullSessionEvents) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfigTest.kt new file mode 100644 index 0000000000..d759c81c8b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/StartupMomentLocalConfigTest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class StartupMomentLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = StartupMomentLocalConfig() + assertNull(cfg.automaticallyEnd) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("startup_moment_config.json") + val obj = Gson().fromJson(json, StartupMomentLocalConfig::class.java) + assertFalse(checkNotNull(obj.automaticallyEnd)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", StartupMomentLocalConfig::class.java) + assertNull(obj.automaticallyEnd) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/TapsLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/TapsLocalConfigTest.kt new file mode 100644 index 0000000000..97caffeab7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/TapsLocalConfigTest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class TapsLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = TapsLocalConfig() + assertNull(cfg.captureCoordinates) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("taps_config.json") + val obj = Gson().fromJson(json, TapsLocalConfig::class.java) + assertFalse(checkNotNull(obj.captureCoordinates)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", TapsLocalConfig::class.java) + assertNull(obj.captureCoordinates) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ViewLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ViewLocalConfigTest.kt new file mode 100644 index 0000000000..43e63b1693 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/ViewLocalConfigTest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class ViewLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = ViewLocalConfig() + assertNull(cfg.enableAutomaticActivityCapture) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("view_config.json") + val obj = Gson().fromJson(json, ViewLocalConfig::class.java) + assertFalse(checkNotNull(obj.enableAutomaticActivityCapture)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", ViewLocalConfig::class.java) + assertNull(obj.enableAutomaticActivityCapture) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfigTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfigTest.kt new file mode 100644 index 0000000000..f0fdc0c9ff --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/config/local/WebViewLocalConfigTest.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk.config.local + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +internal class WebViewLocalConfigTest { + + @Test + fun testDefaults() { + val cfg = WebViewLocalConfig() + assertNull(cfg.captureWebViews) + assertNull(cfg.captureQueryParams) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("web_view_config.json") + val obj = Gson().fromJson(json, WebViewLocalConfig::class.java) + assertFalse(checkNotNull(obj.captureWebViews)) + assertFalse(checkNotNull(obj.captureQueryParams)) + } + + @Test + fun testEmptyObject() { + val obj = Gson().fromJson("{}", WebViewLocalConfig::class.java) + assertNull(obj.captureWebViews) + assertNull(obj.captureQueryParams) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceEventServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceEventServiceTest.kt new file mode 100644 index 0000000000..a9001ffbca --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceEventServiceTest.kt @@ -0,0 +1,470 @@ +package io.embrace.android.embracesdk.event + +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.FakeDeliveryService +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.EmbraceUserService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.config.local.StartupMomentLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig +import io.embrace.android.embracesdk.event.EmbraceEventService.Companion.STARTUP_EVENT_NAME +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeGatingService +import io.embrace.android.embracesdk.fakes.FakePerformanceInfoService +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.fakes.fakeDataCaptureEventBehavior +import io.embrace.android.embracesdk.fakes.fakeSpansBehavior +import io.embrace.android.embracesdk.fakes.fakeStartupBehavior +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.internal.OpenTelemetryClock +import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.prefs.PreferencesService +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.embrace.android.embracesdk.worker.ExecutorName +import io.mockk.clearAllMocks +import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.TimeUnit + +internal class EmbraceEventServiceTest { + + private lateinit var deliveryService: FakeDeliveryService + private lateinit var configService: FakeConfigService + private lateinit var gatingService: GatingService + private lateinit var fakeWorkerThreadModule: FakeWorkerThreadModule + private lateinit var spansService: EmbraceSpansService + private lateinit var sessionProperties: EmbraceSessionProperties + private lateinit var eventService: EmbraceEventService + private lateinit var fakeClock: FakeClock + private lateinit var eventHandler: EventHandler + private lateinit var startupMomentLocalConfig: StartupMomentLocalConfig + private lateinit var remoteConfig: RemoteConfig + + companion object { + private lateinit var metadataService: MetadataService + private lateinit var preferenceService: PreferencesService + private lateinit var performanceInfoService: PerformanceInfoService + private lateinit var userService: UserService + private lateinit var activityService: ActivityService + private lateinit var mockMemoryCleanerService: MemoryCleanerService + private lateinit var logger: InternalEmbraceLogger + + @BeforeClass + @JvmStatic + fun beforeClass() { + metadataService = FakeAndroidMetadataService() + preferenceService = FakePreferenceService() + performanceInfoService = FakePerformanceInfoService() + activityService = FakeActivityService() + mockMemoryCleanerService = mockk(relaxUnitFun = true) + logger = InternalEmbraceLogger() + userService = EmbraceUserService( + preferencesService = preferenceService, + logger = logger + ) + } + + @AfterClass + @JvmStatic + fun tearDown() { + unmockkAll() + } + } + + @Before + fun before() { + clearAllMocks( + answers = false, + objectMocks = false, + constructorMocks = false, + staticMocks = false + ) + + fakeClock = FakeClock() + fakeClock.setCurrentTime(10L) + remoteConfig = RemoteConfig() + deliveryService = FakeDeliveryService() + startupMomentLocalConfig = StartupMomentLocalConfig() + configService = FakeConfigService( + spansBehavior = fakeSpansBehavior { SpansRemoteConfig(pctEnabled = 100f) }, + startupBehavior = fakeStartupBehavior { startupMomentLocalConfig }, + dataCaptureEventBehavior = fakeDataCaptureEventBehavior { remoteConfig } + ) + sessionProperties = EmbraceSessionProperties( + FakePreferenceService(), + logger, + configService + ) + gatingService = FakeGatingService(configService) + fakeWorkerThreadModule = FakeWorkerThreadModule(blockingMode = true) + spansService = EmbraceSpansService(clock = OpenTelemetryClock(embraceClock = fakeClock)) + configService.addListener(spansService) + eventHandler = EventHandler( + metadataService = metadataService, + configService = configService, + userService = userService, + performanceInfoService = performanceInfoService, + deliveryService = deliveryService, + logger = logger, + clock = fakeClock, + scheduledExecutor = fakeWorkerThreadModule.scheduledExecutor(ExecutorName.SCHEDULED_REGISTRATION) + ) + eventService = EmbraceEventService( + 1, + deliveryService, + configService, + metadataService, + performanceInfoService, + userService, + sessionProperties, + logger, + fakeWorkerThreadModule, + fakeClock, + spansService + ) + startupMomentLocalConfig = StartupMomentLocalConfig() + eventService.eventHandler = eventHandler + } + + @Test + fun `if event is not allowed to start it should not continue processing event`() { + val disabledEvent = "disabled-event" + remoteConfig = RemoteConfig( + disabledEventAndLogPatterns = setOf("disabled-event") + ) + eventService.startEvent(disabledEvent) + assertFalse(eventService.activeEvents.containsKey(disabledEvent)) + } + + @Test + fun `verify an event is started successfully with the expected internal keys`() { + val eventNames = listOf("event-to-start", "another-event-to-start", "yet-another-event-to-start") + val identifiers = listOf("identifier", null, "") + repeat(3) { + eventService.startEvent(eventNames[it], identifiers[it]) + assertNotNull(eventService.getActiveEvent(eventNames[it], identifiers[it])) + } + assertTrue(eventService.activeEvents.containsKey("${eventNames[0]}#${identifiers[0]}")) + assertTrue(eventService.activeEvents.containsKey(eventNames[1])) + assertTrue(eventService.activeEvents.containsKey(eventNames[2])) + } + + @Test + fun `verify an event is started successfully for startEvent method specifying screenshotting`() { + val eventName = "event-to-start" + eventService.startEvent(eventName, null) + val eventDescription = eventService.getActiveEvent(eventName, null) + assertNotNull(eventDescription) + } + + @Test + fun `verify an event is started successfully for start method with custom properties`() { + val eventName = "event-to-start" + val customProperties = mapOf("u" to "orns") + eventService.startEvent(eventName, null, customProperties) + val eventDescription = eventService.getActiveEvent(eventName, null) + assertNotNull(eventDescription) + val eventProperties = eventDescription?.event?.customPropertiesMap + checkNotNull(eventProperties) + assertEquals(customProperties.size, eventProperties.size) + customProperties.forEach { + assertEquals(it.value, eventProperties[it.key]) + } + } + + @Test + fun `verify an event is started successfully for start method with screenshotting and custom properties`() { + val eventName = "event-to-start" + val customProperties = mapOf("u" to "orns") + eventService.startEvent(eventName, null, customProperties) + val eventDescription = eventService.getActiveEvent(eventName, null) + assertNotNull(eventDescription) + val eventProperties = eventDescription?.event?.customPropertiesMap + checkNotNull(eventProperties) + assertEquals(customProperties.size, eventProperties.size) + customProperties.forEach { + assertEquals(it.value, eventProperties[it.key]) + } + } + + @Test + fun `if event is not allowed to end it should not continue processing event`() { + val disabledEvent = "disabled-event" + remoteConfig = RemoteConfig( + disabledEventAndLogPatterns = setOf("disabled-event") + ) + eventService.endEvent(disabledEvent) + assertNull(deliveryService.lastEventSentAsync) + } + + @Test + fun `verify an event without an identifier ended successfully`() { + val eventName = "event-to-end" + eventService.startEvent(eventName) + assertNotNull(deliveryService.lastEventSentAsync) + assertEquals(EmbraceEvent.Type.START, deliveryService.lastEventSentAsync?.event?.type) + eventService.endEvent(eventName) + assertEquals(EmbraceEvent.Type.END, deliveryService.lastEventSentAsync?.event?.type) + } + + @Test + fun `verify an event with identifier is ended successfully`() { + val eventName = "event-to-end" + val identifier = "identifier" + eventService.startEvent(eventName, identifier) + assertEquals(EmbraceEvent.Type.START, deliveryService.lastEventSentAsync?.event?.type) + eventService.endEvent(eventName, identifier) + assertEquals(EmbraceEvent.Type.END, deliveryService.lastEventSentAsync?.event?.type) + } + + @Test + fun `verify an event with custom properties is ended successfully`() { + val eventName = "event-to-end" + val customProperties = mapOf("yel" to "lows") + eventService.startEvent(eventName) + assertEquals(EmbraceEvent.Type.START, deliveryService.lastEventSentAsync?.event?.type) + eventService.endEvent(eventName, customProperties) + assertEquals(EmbraceEvent.Type.END, deliveryService.lastEventSentAsync?.event?.type) + val eventProperties = deliveryService.lastEventSentAsync?.event?.customPropertiesMap + checkNotNull(eventProperties) + assertEquals(customProperties.size, eventProperties.size) + customProperties.forEach { + assertEquals(it.value, eventProperties[it.key]) + } + } + + @Test + fun `verify an event with an identifier and custom properties ended successfully`() { + val eventName = "event-to-end" + val customProperties = mapOf("yel" to "lows") + eventService.startEvent(eventName) + assertEquals(EmbraceEvent.Type.START, deliveryService.lastEventSentAsync?.event?.type) + eventService.endEvent(eventName, customProperties) + assertEquals(EmbraceEvent.Type.END, deliveryService.lastEventSentAsync?.event?.type) + val eventProperties = deliveryService.lastEventSentAsync?.event?.customPropertiesMap + checkNotNull(eventProperties) + assertEquals(customProperties.size, eventProperties.size) + customProperties.forEach { + assertEquals(it.value, eventProperties[it.key]) + } + } + + @Test + fun `verify an event is ended successfully for a startup event`() { + eventService.startEvent(STARTUP_EVENT_NAME) + val originalEvent = checkNotNull(eventService.getActiveEvent(STARTUP_EVENT_NAME, null)) + fakeClock.setCurrentTime(25L) + eventService.endEvent(STARTUP_EVENT_NAME) + assertNotNull(eventService.getStartupMomentInfo()) + assertEquals(15L, eventService.getStartupMomentInfo()?.duration) + assertEquals(originalEvent.event.lateThreshold, eventService.getStartupMomentInfo()?.threshold) + } + + @Test + fun `verify send a startup moment successfully`() { + eventService.sendStartupMoment() + assertNotNull(eventService.getActiveEvent(STARTUP_EVENT_NAME, null)) + } + + @Test + fun `sending a startup moment twice, should not do anything the 2nd time`() { + eventService.sendStartupMoment() + assertEquals(1, deliveryService.eventSentAsyncInvokedCount) + eventService.sendStartupMoment() + assertEquals(1, deliveryService.eventSentAsyncInvokedCount) + } + + @Test + fun `verify onForeground for a cold start sends a startup moment`() { + eventService.onForeground(true, 123, 456) + assertNotNull(eventService.getActiveEvent(STARTUP_EVENT_NAME, null)) + val lastEvent = deliveryService.lastEventSentAsync + assertEquals(1, deliveryService.eventSentAsyncInvokedCount) + assertNotNull(lastEvent) + assertEquals(EmbraceEvent.Type.START, lastEvent?.event?.type) + assertEquals(STARTUP_EVENT_NAME, lastEvent?.event?.name) + } + + @Test + fun `verify onForeground for a non cold start does not do anything`() { + eventService.onForeground(false, 123, 456) + assertNull(eventService.getActiveEvent(STARTUP_EVENT_NAME, null)) + assertNull(deliveryService.lastEventSentAsync) + } + + @Test + fun `applicationStartupComplete if automatically end is enabled ends startup event`() { + eventService.startEvent(STARTUP_EVENT_NAME) + eventService.applicationStartupComplete() + val lastEvent = deliveryService.lastEventSentAsync + assertEquals(2, deliveryService.eventSentAsyncInvokedCount) + assertNotNull(lastEvent) + assertEquals(EmbraceEvent.Type.END, lastEvent?.event?.type) + assertEquals(STARTUP_EVENT_NAME, lastEvent?.event?.name) + } + + @Test + fun `applicationStartupComplete if automatically end is disabled does not do anything`() { + startupMomentLocalConfig = StartupMomentLocalConfig(automaticallyEnd = false) + eventService.applicationStartupComplete() + assertNull(deliveryService.lastEventSentAsync) + } + + @Test + fun `verify we are getting active event ids`() { + val eventName = "event-to-start" + eventService.startEvent(eventName) + assertTrue(eventService.getActiveEventIds().size == 1) + val event = checkNotNull(eventService.getActiveEvent(eventName, null)) + assertEquals(event.event.eventId, eventService.getActiveEventIds()[0]) + } + + @Test + fun `verify no active events if no event has been started`() { + assertTrue(eventService.getActiveEventIds().size == 0) + } + + @Test + fun `verify no startup event info is available if no startup event has been started`() { + assertNull(eventService.getStartupMomentInfo()) + } + + @Test + fun `verify find event ids using findEventIdsForSession()`() { + // Simulate the session moving forward in time, and having new Moments added and us retrieving the eventIds given the new time range + // Note that because of how the underlying cache works, if the size of the eventIds collection didn't change from the last time + // this method was invoked, the previously cached value will be returned. So while calling this method with arbitrary start/end + // times can return wrong values, how it is being used, that won't happen. This test will simulate the EXPECTED usage rather than + // the arbitrary usage. + + val sessionBeginTime = 100L + fakeClock.setCurrentTime(sessionBeginTime) + eventService.startEvent("first") + fakeClock.setCurrentTime(fakeClock.now() + 1L) + + // after a new Moment is logged and the time ticks forward, we should see it reflected in the cache + assertEquals(1, eventService.findEventIdsForSession(sessionBeginTime, fakeClock.now()).size) + fakeClock.setCurrentTime(fakeClock.now() + 50L) + eventService.startEvent("second") + fakeClock.setCurrentTime(fakeClock.now() + 1L) + eventService.startEvent("third") + + // the new time range will only return 2 of the logged moments because the clock hasn't ticked forward + assertEquals(2, eventService.findEventIdsForSession(sessionBeginTime, fakeClock.now()).size) + fakeClock.setCurrentTime(fakeClock.now() + 1L) + + // After the clock ticks forward, because of the caching, we will still only return 2. This is a perf optimization that will be + // OK in practice because we should only bust the cache if there's a new moment - this check is just to verify the caching works + // because it won't really happen in practice + assertEquals(2, eventService.findEventIdsForSession(sessionBeginTime, fakeClock.now()).size) + + // After logging another moment, the cache is busted so after the clock ticks forward, we get all 4 moments + eventService.startEvent("fourth") + fakeClock.setCurrentTime(fakeClock.now() + 1L) + assertEquals(4, eventService.findEventIdsForSession(sessionBeginTime, fakeClock.now()).size) + } + + @Test + fun `verify close clears the existing events`() { + eventService.startEvent("event-yeah") + eventService.close() + assertTrue(eventService.activeEvents.isEmpty()) + } + + @Test + fun `verify clean collections`() { + val time = 123L + fakeClock.setCurrentTime(time) + val eventName = "event-to-start" + val identifier = "identifier" + + eventService.startEvent(eventName, identifier) + // assert that active events is not empty + assertTrue(eventService.activeEvents.isNotEmpty()) + assertTrue(eventService.findEventIdsForSession(time - 1, time + 1).isNotEmpty()) + + eventService.cleanCollections() + + assertTrue(eventService.activeEvents.isEmpty()) + assertTrue(eventService.findEventIdsForSession(time - 1, time + 1).isEmpty()) + } + + @Test + fun `startup event name is case sensitive`() { + eventService.startEvent(STARTUP_EVENT_NAME.toUpperCase()) + assertNull(eventService.getActiveEvent(STARTUP_EVENT_NAME, null)) + eventService.applicationStartupComplete() + val lastEvent = deliveryService.lastEventSentAsync + assertEquals(1, deliveryService.eventSentAsyncInvokedCount) + assertNotEquals(EmbraceEvent.Type.END, lastEvent?.event?.type) + assertNotEquals(STARTUP_EVENT_NAME, lastEvent?.event?.name) + } + + @Test + fun `startup logged as span if startup moment automatic end is enabled`() { + spansService.initializeService( + sdkInitStartTimeNanos = TimeUnit.MILLISECONDS.toNanos(1), + sdkInitEndTimeNanos = TimeUnit.MILLISECONDS.toNanos(3) + ) + configService.updateListeners() + spansService.flushSpans() + eventService.sendStartupMoment() + eventService.applicationStartupComplete() + val executor = fakeWorkerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION) + executor.runCurrentlyBlocked() + assertEquals(1, spansService.completedSpans()?.size) + val startupSpan = spansService.completedSpans()!![0] + with(startupSpan) { + assertEquals("emb-startup-moment", name) + assertEquals(TimeUnit.MILLISECONDS.toNanos(1), startTimeNanos) + assertEquals(TimeUnit.MILLISECONDS.toNanos(10), endTimeNanos) + } + } + + @Test + fun `startup logged as span if when startup moment manually ends`() { + startupMomentLocalConfig = StartupMomentLocalConfig(automaticallyEnd = false) + spansService.initializeService( + sdkInitStartTimeNanos = TimeUnit.MILLISECONDS.toNanos(1), + sdkInitEndTimeNanos = TimeUnit.MILLISECONDS.toNanos(3) + ) + configService.updateListeners() + spansService.flushSpans() + eventService.sendStartupMoment() + eventService.applicationStartupComplete() + val executor = fakeWorkerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION) + executor.runCurrentlyBlocked() + assertEquals(0, spansService.completedSpans()?.size) + + fakeClock.setCurrentTime(20L) + eventService.endEvent(STARTUP_EVENT_NAME) + executor.runCurrentlyBlocked() + assertEquals(1, spansService.completedSpans()?.size) + + val startupSpan = spansService.completedSpans()!![0] + with(startupSpan) { + assertEquals("emb-startup-moment", name) + assertEquals(TimeUnit.MILLISECONDS.toNanos(1), startTimeNanos) + assertEquals(TimeUnit.MILLISECONDS.toNanos(20), endTimeNanos) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceRemoteLoggerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceRemoteLoggerTest.kt new file mode 100644 index 0000000000..1ae657c547 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EmbraceRemoteLoggerTest.kt @@ -0,0 +1,469 @@ +package io.embrace.android.embracesdk.event + +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.LogExceptionType +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.comms.delivery.EmbraceDeliveryService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.LogRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SessionRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.fakeLogMessageBehavior +import io.embrace.android.embracesdk.fakes.fakeSessionBehavior +import io.embrace.android.embracesdk.gating.EmbraceGatingService +import io.embrace.android.embracesdk.gating.SessionGatingKeys +import io.embrace.android.embracesdk.internal.MessageType +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.NetworkCapturedCall +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.AfterClass +import org.junit.Assert +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong + +internal class EmbraceRemoteLoggerTest { + + companion object { + private lateinit var remoteLogger: EmbraceRemoteLogger + private lateinit var metadataService: FakeAndroidMetadataService + private lateinit var deliveryService: EmbraceDeliveryService + private lateinit var userService: UserService + private lateinit var configService: ConfigService + private lateinit var memoryCleanerService: MemoryCleanerService + private lateinit var sessionProperties: EmbraceSessionProperties + private lateinit var gatingService: EmbraceGatingService + private lateinit var logcat: InternalEmbraceLogger + private lateinit var executor: ExecutorService + private lateinit var tick: AtomicLong + private lateinit var clock: Clock + + @BeforeClass + @JvmStatic + fun beforeClass() { + metadataService = FakeAndroidMetadataService() + deliveryService = mockk(relaxed = true) + userService = mockk(relaxed = true) + memoryCleanerService = mockk(relaxed = true) + sessionProperties = mockk(relaxed = true) + logcat = mockk(relaxed = true) + configService = mockk(relaxed = true) { + every { sessionBehavior } returns fakeSessionBehavior() + } + executor = Executors.newSingleThreadExecutor() + tick = AtomicLong(1609823408L) + clock = Clock { tick.incrementAndGet() } + mockkStatic(Uuid::class) + every { Uuid.getEmbUuid() } returns "id" + + gatingService = EmbraceGatingService( + configService + ) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + private var cfg: RemoteConfig? = RemoteConfig() + + @Before + fun setUp() { + clearAllMocks( + answers = false, + objectMocks = false, + constructorMocks = false, + staticMocks = false + ) + + every { configService.sessionBehavior } returns fakeSessionBehavior { + cfg + } + every { configService.logMessageBehavior } returns fakeLogMessageBehavior { + LogRemoteConfig() + } + every { configService.dataCaptureEventBehavior.isLogMessageEnabled(any()) } returns true + every { configService.dataCaptureEventBehavior.isMessageTypeEnabled(any()) } returns true + metadataService.setActiveSessionId("session-123") + metadataService.setAppForeground() + metadataService.setAppId("appId") + } + + private fun getRemoteLogger(): EmbraceRemoteLogger { + return EmbraceRemoteLogger( + metadataService, + deliveryService, + userService, + configService, + sessionProperties, + logcat, + clock, + MoreExecutors.newDirectExecutorService(), + gatingService, + mockk(relaxed = true) + ) + } + + @Test + fun testLogSimple() { + every { Uuid.getEmbUuid() } returns "id" + remoteLogger = getRemoteLogger() + + val props = mapOf("foo" to "bar") + remoteLogger.log("Hello world", EmbraceEvent.Type.INFO_LOG, props) + remoteLogger.log("Warning world", EmbraceEvent.Type.WARNING_LOG, null) + remoteLogger.log("Hello errors", EmbraceEvent.Type.ERROR_LOG, null) + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertEquals("Hello world", it.event.name) + Assert.assertNotNull(it.event.timestamp) + Assert.assertEquals(EmbraceEvent.Type.INFO_LOG, it.event.type) + Assert.assertEquals(props, it.event.customPropertiesMap) + Assert.assertNotNull(it.event.messageId) + Assert.assertNotNull(it.event.eventId) + Assert.assertNotNull(it.event.sessionId) + it.event.screenshotTaken?.let { Assert.assertFalse(it) } + Assert.assertEquals(LogExceptionType.NONE.value, it.event.logExceptionType) + } + ) + } + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertEquals("Warning world", it.event.name) + Assert.assertEquals(EmbraceEvent.Type.WARNING_LOG, it.event.type) + Assert.assertNull(it.event.customPropertiesMap) + Assert.assertNotNull(it.event.messageId) + Assert.assertNotNull(it.event.eventId) + Assert.assertNotNull(it.event.sessionId) + it.event.screenshotTaken?.let { st -> Assert.assertFalse(st) } + Assert.assertEquals(LogExceptionType.NONE.value, it.event.logExceptionType) + } + ) + } + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertEquals("Hello errors", it.event.name) + Assert.assertEquals(EmbraceEvent.Type.ERROR_LOG, it.event.type) + Assert.assertNull(it.event.customPropertiesMap) + Assert.assertNotNull(it.event.messageId) + Assert.assertNotNull(it.event.eventId) + Assert.assertNotNull(it.event.sessionId) + it.event.screenshotTaken?.let { Assert.assertFalse(it) } + Assert.assertEquals(LogExceptionType.NONE.value, it.event.logExceptionType) + } + ) + } + + // verify sent counts + Assert.assertEquals(1, remoteLogger.getInfoLogsAttemptedToSend()) + Assert.assertEquals(1, remoteLogger.getWarnLogsAttemptedToSend()) + Assert.assertEquals(1, remoteLogger.getErrorLogsAttemptedToSend()) + Assert.assertEquals(0, remoteLogger.getUnhandledExceptionsSent()) + } + + @Test + fun testExceptionLog() { + remoteLogger = getRemoteLogger() + val exception = NullPointerException("exception message") + + remoteLogger.log( + "Hello world", + EmbraceEvent.Type.ERROR_LOG, + LogExceptionType.NONE, + null, + exception.stackTrace, + null, + Embrace.AppFramework.NATIVE, + null, + null, + exception.javaClass.simpleName, + exception.message, + ) + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertEquals("Hello world", it.event.name) + Assert.assertEquals(EmbraceEvent.Type.ERROR_LOG, it.event.type) + Assert.assertEquals("NullPointerException", it.event.exceptionName) + Assert.assertEquals("exception message", it.event.exceptionMessage) + Assert.assertNotNull(it.event.messageId) + Assert.assertNotNull(it.event.eventId) + Assert.assertNotNull(it.event.sessionId) + Assert.assertNotNull(it.event.sessionId) + Assert.assertNotNull(it.event.sessionId) + Assert.assertNotNull(it.event.logExceptionType) + Assert.assertEquals(LogExceptionType.NONE.value, it.event.logExceptionType) + } + ) + } + } + + @Test + fun testLogNetwork() { + val networkCaptureCall: NetworkCapturedCall = mockk(relaxed = true) + + remoteLogger = getRemoteLogger() + remoteLogger.logNetwork(networkCaptureCall) + + verify { + deliveryService.sendNetworkCall( + withArg { + Assert.assertEquals("appId", it.appId) + Assert.assertEquals("session-123", it.sessionId) + Assert.assertNotNull(it.appInfo) + Assert.assertNotNull(it.networkCaptureCall) + } + ) + } + + Assert.assertEquals(1, remoteLogger.findNetworkLogIds(0, clock.now()).size) + } + + @Test + fun `testLogNetwork with no info`() { + remoteLogger = getRemoteLogger() + remoteLogger.logNetwork(null) + + verify(exactly = 0) { + deliveryService.sendNetworkCall(any()) + } + + Assert.assertEquals(0, remoteLogger.findNetworkLogIds(0, clock.now()).size) + } + + @Test + fun testDefaultMaxMessageLength() { + remoteLogger = getRemoteLogger() + remoteLogger.log("Hi".repeat(65), EmbraceEvent.Type.INFO_LOG, null) + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertTrue(it.event.name == "Hi".repeat(62) + "H...") + } + ) + } + } + + @Test + fun testCustomMaxMessageLength() { + every { configService.logMessageBehavior.getInfoLogLimit() } returns 50 + every { configService.logMessageBehavior.getLogMessageMaximumAllowedLength() } returns 50 + + remoteLogger = getRemoteLogger() + remoteLogger.log("Hi".repeat(50), EmbraceEvent.Type.INFO_LOG, null) + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertTrue(it.event.name == "Hi".repeat(23) + "H...") + } + ) + } + } + + @Test + fun testLogMessageEabled() { + every { configService.dataCaptureEventBehavior.isLogMessageEnabled("Hello World") } returns false + remoteLogger = getRemoteLogger() + + remoteLogger.log("Hello World", EmbraceEvent.Type.INFO_LOG, null) + remoteLogger.log("Another", EmbraceEvent.Type.INFO_LOG, null) + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertTrue(it.event.name == "Another") + } + ) + } + + verify(exactly = 0) { + deliveryService.sendLogs( + withArg { + Assert.assertTrue(it.event.name == "Hello World") + } + ) + } + } + + @Test + fun testMessageTypeEnabled() { + every { configService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.LOG) } returns false + remoteLogger = getRemoteLogger() + + remoteLogger.log("Hello World", EmbraceEvent.Type.INFO_LOG, null) + + verify(exactly = 0) { deliveryService.sendLogs(any()) } + } + + @Test + fun testDefaultMaxMessageCountLimits() { + remoteLogger = getRemoteLogger() + + repeat(500) { k -> + remoteLogger.log("Test info $k", EmbraceEvent.Type.INFO_LOG, null) + remoteLogger.log("Test warning $k", EmbraceEvent.Type.WARNING_LOG, null) + remoteLogger.log("Test error $k", EmbraceEvent.Type.ERROR_LOG, null) + } + + Assert.assertEquals(100, remoteLogger.findInfoLogIds(0L, Long.MAX_VALUE).size) + Assert.assertEquals(500, remoteLogger.getInfoLogsAttemptedToSend()) + Assert.assertEquals(100, remoteLogger.findWarningLogIds(0L, Long.MAX_VALUE).size) + Assert.assertEquals(500, remoteLogger.getWarnLogsAttemptedToSend()) + Assert.assertEquals(250, remoteLogger.findErrorLogIds(0L, Long.MAX_VALUE).size) + Assert.assertEquals(500, remoteLogger.getErrorLogsAttemptedToSend()) + } + + @Test + fun testLoggingUnityMessage() { + remoteLogger = getRemoteLogger() + + remoteLogger.log( + "Unity".repeat(1000), + EmbraceEvent.Type.INFO_LOG, + LogExceptionType.HANDLED, + null, + null, + "my stacktrace", + Embrace.AppFramework.UNITY, + null, + null, + null, + null + ) + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertTrue(it.event.name == "Unity".repeat(1000)) // log limit higher on unity + Assert.assertTrue(it.stacktraces?.unityStacktrace == "my stacktrace") + Assert.assertEquals(LogExceptionType.HANDLED.value, it.event.logExceptionType) + } + ) + } + + Assert.assertEquals(0, remoteLogger.getUnhandledExceptionsSent()) + } + + @Test + fun testLoggingUnityUnhandledException() { + remoteLogger = getRemoteLogger() + + remoteLogger.log( + "Unity".repeat(1000), + EmbraceEvent.Type.INFO_LOG, + LogExceptionType.UNHANDLED, + null, + null, + "my stacktrace", + Embrace.AppFramework.UNITY, + null, + null, + null, + null + ) + + verify { + deliveryService.sendLogs( + withArg { + Assert.assertTrue(it.event.name == "Unity".repeat(1000)) // log limit higher on unity + Assert.assertTrue(it.stacktraces?.unityStacktrace == "my stacktrace") + Assert.assertEquals(LogExceptionType.UNHANDLED.value, it.event.logExceptionType) + } + ) + } + + Assert.assertEquals(1, remoteLogger.getUnhandledExceptionsSent()) + } + + @Test + fun testLoggingFlutterMessage() { + remoteLogger = getRemoteLogger() + remoteLogger.log( + "Dart error", + EmbraceEvent.Type.ERROR_LOG, + LogExceptionType.UNHANDLED, + null, + null, + "my stacktrace", + Embrace.AppFramework.FLUTTER, + "dart context", + "dart library", + "Dart error name", + "Dart error message" + ) + executor.shutdown() + executor.awaitTermination(1, TimeUnit.SECONDS) + + val action = slot() + verify(exactly = 1) { deliveryService.sendLogs(capture(action)) } + val msg = action.captured + Assert.assertEquals("Dart error name", msg.event.exceptionName) + Assert.assertEquals("Dart error message", msg.event.exceptionMessage) + Assert.assertEquals("my stacktrace", msg.stacktraces?.flutterStacktrace) + Assert.assertEquals("dart context", msg.stacktraces?.context) + Assert.assertEquals("dart library", msg.stacktraces?.library) + Assert.assertEquals(1, remoteLogger.getUnhandledExceptionsSent()) + } + + @Test + fun testIfShouldGateInfoLog() { + remoteLogger = getRemoteLogger() + cfg = buildCustomRemoteConfig( + setOf(), + null + ) + Assert.assertTrue(remoteLogger.checkIfShouldGateLog(EmbraceEvent.Type.INFO_LOG)) + Assert.assertTrue(remoteLogger.checkIfShouldGateLog(EmbraceEvent.Type.WARNING_LOG)) + } + + @Test + fun testIfShouldNotGateInfoLog() { + remoteLogger = getRemoteLogger() + cfg = buildCustomRemoteConfig( + setOf(SessionGatingKeys.LOGS_INFO, SessionGatingKeys.LOGS_WARN), + null + ) + Assert.assertFalse(remoteLogger.checkIfShouldGateLog(EmbraceEvent.Type.INFO_LOG)) + Assert.assertFalse(remoteLogger.checkIfShouldGateLog(EmbraceEvent.Type.WARNING_LOG)) + } + + private fun buildCustomRemoteConfig(components: Set?, fullSessionEvents: Set?) = + RemoteConfig( + sessionConfig = SessionRemoteConfig( + true, + false, + components, + fullSessionEvents + ) + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EventHandlerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EventHandlerTest.kt new file mode 100644 index 0000000000..b838386914 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/event/EventHandlerTest.kt @@ -0,0 +1,671 @@ +package io.embrace.android.embracesdk.event + +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SessionRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeGatingService +import io.embrace.android.embracesdk.fakes.fakeDataCaptureEventBehavior +import io.embrace.android.embracesdk.fakes.fakeSessionBehavior +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.internal.EventDescription +import io.embrace.android.embracesdk.internal.MessageType +import io.embrace.android.embracesdk.internal.StartupEventInfo +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.Event +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.UserInfo +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture + +internal class EventHandlerTest { + + private lateinit var eventHandler: EventHandler + private lateinit var clock: FakeClock + lateinit var cfg: RemoteConfig + + companion object { + private lateinit var mockDeliveryService: DeliveryService + private lateinit var mockConfigService: ConfigService + private lateinit var mockPerformanceService: PerformanceInfoService + private lateinit var mockUserService: UserService + private lateinit var gatingService: GatingService + private lateinit var mockSessionProperties: EmbraceSessionProperties + private lateinit var logger: InternalEmbraceLogger + private lateinit var mockStartup: StartupEventInfo + private lateinit var mockLateTimer: ScheduledFuture<*> + private lateinit var mockUserInfo: UserInfo + private lateinit var fakeMetadataService: FakeAndroidMetadataService + private lateinit var blockingScheduledExecutorService: BlockingScheduledExecutorService + private lateinit var scheduledExecutorService: ScheduledExecutorService + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockDeliveryService = mockk(relaxed = true) + mockConfigService = mockk(relaxed = true) + mockPerformanceService = mockk() + mockUserService = mockk() + gatingService = FakeGatingService() + mockSessionProperties = mockk() + logger = InternalEmbraceLogger() + mockStartup = mockk(relaxed = true) + mockLateTimer = mockk(relaxed = true) + mockUserInfo = mockk() + mockkStatic(StartupEventInfo::class) + mockkStatic(Event::class) + mockkStatic(EventMessage::class) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @Before + fun before() { + cfg = RemoteConfig() + every { mockConfigService.sessionBehavior } returns fakeSessionBehavior { cfg } + every { mockConfigService.dataCaptureEventBehavior } returns fakeDataCaptureEventBehavior { cfg } + + clock = FakeClock() + blockingScheduledExecutorService = BlockingScheduledExecutorService() + scheduledExecutorService = blockingScheduledExecutorService + fakeMetadataService = FakeAndroidMetadataService(sessionId = "session-id") + eventHandler = EventHandler( + fakeMetadataService, + mockConfigService, + mockUserService, + mockPerformanceService, + mockDeliveryService, + logger, + clock, + scheduledExecutorService + ) + } + + @After + fun after() { + clearAllMocks(answers = false) + } + + @Test + fun `if event name is empty then event should not be allowed to start`() { + val allowed = eventHandler.isAllowedToStart("") + assertFalse(allowed) + } + + @Test + fun `if event name is disabled then event should not be allowed to start`() { + val disabledEvent = "disabled-event" + every { mockConfigService.dataCaptureEventBehavior.isEventEnabled(disabledEvent) } returns false + val allowed = eventHandler.isAllowedToStart(disabledEvent) + + assertFalse(allowed) + } + + @Test + fun `if event type is disabled then event should not be allowed to start`() { + val event = "event" + every { mockConfigService.dataCaptureEventBehavior.isEventEnabled(event) } returns true + every { mockConfigService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.EVENT) } returns false + val allowed = eventHandler.isAllowedToStart(event) + + assertFalse(allowed) + } + + @Test + fun `if worker is shut down, then event should not be allowed to start`() { + val event = "event" + every { mockConfigService.dataCaptureEventBehavior.isEventEnabled(event) } returns true + every { mockConfigService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.EVENT) } returns true + scheduledExecutorService.shutdown() + val allowed = eventHandler.isAllowedToStart(event) + + assertFalse(allowed) + } + + @Test + fun `if none of the above, event should be allowed to start`() { + val event = "event" + every { mockConfigService.dataCaptureEventBehavior.isEventEnabled(event) } returns true + every { mockConfigService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.EVENT) } returns true + val allowed = eventHandler.isAllowedToStart(event) + + assertTrue(allowed) + } + + @Test + fun `if event type is disabled then event should not be allowed to end`() { + every { mockConfigService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.EVENT) } returns false + val allowed = eventHandler.isAllowedToEnd() + + assertFalse(allowed) + } + + @Test + fun `verify event is allowed to end`() { + every { mockConfigService.dataCaptureEventBehavior.isMessageTypeEnabled(MessageType.EVENT) } returns true + val allowed = eventHandler.isAllowedToEnd() + + assertTrue(allowed) + } + + @Test + fun `verify build startup event successfully`() { + val duration = 456L + val threshold = 123L + val startEvent = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 100L, + type = EmbraceEvent.Type.START, + lateThreshold = threshold + ) + val endEvent = Event( + eventId = Uuid.getEmbUuid(), + timestamp = 200L, + type = EmbraceEvent.Type.END, + duration = duration + ) + + val event = eventHandler.buildStartupEventInfo(startEvent, endEvent) + + assertEquals(event.duration, duration) + assertEquals(event.threshold, threshold) + } + + @Test + fun `verify onEventEnded builds event and sends the event for a non late event`() { + val eventProperties = mapOf() + val startTime = 100L + val endTime = 300L + val sessionPropertiesMap: Map = mapOf() + val customPropertiesMap: Map = mapOf() + clock.setCurrentTime(endTime) + val originEventId = "origin-event-id" + val originEventName = "origin-event-name" + + val originEvent = Event( + timestamp = startTime, + eventId = originEventId, + name = originEventName, + type = EmbraceEvent.Type.START + ) + val originEventDescription = EventDescription(mockLateTimer, originEvent) + + val builtEndEvent = Event( + eventId = originEventId, + type = EmbraceEvent.Type.END, + appState = fakeMetadataService.getAppState(), + name = originEventName, + timestamp = endTime, + customProperties = customPropertiesMap, + sessionProperties = sessionPropertiesMap, + sessionId = fakeMetadataService.activeSessionId, + duration = endTime - startTime + ) + + val mockPerformanceInfo: PerformanceInfo = mockk() + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { + mockPerformanceService.getPerformanceInfo( + startTime, + endTime, + false + ) + } returns mockPerformanceInfo + every { mockUserService.getUserInfo() } returns mockUserInfo + + val builtEndEventMessage = EventMessage( + event = builtEndEvent, + userInfo = mockUserInfo, + performanceInfo = mockPerformanceInfo + ) + + val result = eventHandler.onEventEnded( + originEventDescription, + false, + eventProperties, + mockSessionProperties + ) + + verify { mockLateTimer.cancel(false) } + verify { mockDeliveryService.sendEventAsync(any()) } + assertEquals(builtEndEventMessage, result) + } + @Test + fun `verify onEventEnded builds event and sends the event`() { + val eventProperties = mapOf() + val startTime = 100L + val endTime = 300L + val sessionPropertiesMap: Map = mapOf() + val customPropertiesMap: Map = mapOf() + clock.setCurrentTime(endTime) + val originEventId = "origin-event-id" + val originEventName = "origin-event-name" + val mockPerformanceInfo: PerformanceInfo = mockk() + + val originEvent = Event( + timestamp = startTime, + eventId = originEventId, + name = originEventName, + type = EmbraceEvent.Type.START + ) + val originEventDescription = EventDescription(mockLateTimer, originEvent) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { + mockPerformanceService.getPerformanceInfo( + startTime, + endTime, + false + ) + } returns mockPerformanceInfo + every { mockUserService.getUserInfo() } returns mockUserInfo + val endEvent = Event( + eventId = originEventId, + type = EmbraceEvent.Type.LATE, + appState = fakeMetadataService.getAppState(), + name = originEventName, + timestamp = endTime, + customProperties = customPropertiesMap, + sessionProperties = sessionPropertiesMap, + sessionId = fakeMetadataService.activeSessionId, + duration = endTime - startTime + ) + val builtEndEventMessage = EventMessage( + event = endEvent, + userInfo = mockUserInfo, + performanceInfo = mockPerformanceInfo + ) + cfg = createGatingConfig(setOf("s_mts")) + + val result = eventHandler.onEventEnded( + originEventDescription, + true, + eventProperties, + mockSessionProperties + ) + + verify { mockLateTimer.cancel(false) } + verify { mockDeliveryService.sendEventAsync(any()) } + assertEquals(builtEndEventMessage, result) + } + + @Test + fun `verify onEventStarted builds event, sends event and sets the timer`() { + val eventId = "event-id" + val eventName = "event-name" + val startTime = 123L + val threshold = 100L + val sessionPropertiesMap: Map = mapOf() + val customProperties: Map = mapOf() + val builtEvent = Event( + eventId = eventId, + type = EmbraceEvent.Type.START, + appState = fakeMetadataService.getAppState(), + name = eventName, + lateThreshold = threshold, + timestamp = startTime, + sessionProperties = sessionPropertiesMap, + customProperties = customProperties, + sessionId = fakeMetadataService.activeSessionId + ) + + clock.setCurrentTime(456) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { mockUserService.getUserInfo() } returns mockUserInfo + + val builtEventMessage = EventMessage( + event = builtEvent, + appInfo = fakeMetadataService.getAppInfo(), + userInfo = mockUserInfo, + deviceInfo = fakeMetadataService.getDeviceInfo() + ) + cfg = RemoteConfig( + sessionConfig = SessionRemoteConfig( + sessionComponents = setOf("s_mts") + ), + eventLimits = mapOf(eventId to threshold) + ) + + var hasCallableBeenInvoked = false + val eventDescription = eventHandler.onEventStarted( + eventId, + eventName, + startTime, + mockSessionProperties, + mapOf() + ) { + hasCallableBeenInvoked = true + } + assertNotNull(eventDescription) + assertEquals(builtEvent, eventDescription.event) + verify { mockDeliveryService.sendEventAsync(builtEventMessage) } + blockingScheduledExecutorService.runCurrentlyBlocked() + assertTrue(hasCallableBeenInvoked) + } + + @Test + fun `verify onEventStarted prevents send the startup event if feature gating gates it`() { + val eventId = "event-id" + val eventName = EmbraceEventService.STARTUP_EVENT_NAME + val startTime = 123L + val threshold = 100L + val sessionPropertiesMap: Map = mapOf() + val mockTimeoutCallback: Runnable = mockk() + clock.setCurrentTime(456) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { mockUserService.getUserInfo() } returns mockUserInfo + + cfg = RemoteConfig( + sessionConfig = SessionRemoteConfig( + sessionComponents = emptySet() + ), + eventLimits = mapOf(eventId to threshold) + ) + + eventHandler.onEventStarted( + eventId, + eventName, + startTime, + mockSessionProperties, + mapOf(), + mockTimeoutCallback + ) + + verify { mockDeliveryService wasNot Called } + } + + @Test + fun `verify onEventStarted sends the startup event if feature gating allows it`() { + val eventId = "event-id" + val eventName = EmbraceEventService.STARTUP_EVENT_NAME + val startTime = 123L + val threshold = 100L + val sessionPropertiesMap: Map = mapOf() + val mockTimeoutCallback: Runnable = mockk() + clock.setCurrentTime(456) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { mockUserService.getUserInfo() } returns mockUserInfo + + cfg = RemoteConfig( + sessionConfig = SessionRemoteConfig( + sessionComponents = setOf("mts_st") + ), + eventLimits = mapOf(eventId to threshold) + ) + + eventHandler.onEventStarted( + eventId, + eventName, + startTime, + mockSessionProperties, + mapOf(), + mockTimeoutCallback + ) + + verify { mockDeliveryService.sendEventAsync(any()) } + } + + @Test + fun `verify onEventStarted prevents send the event if feature gating gates it`() { + val eventId = "event-id" + val eventName = "event-name" + val startTime = 123L + val threshold = 100L + val sessionPropertiesMap: Map = mapOf() + val mockTimeoutCallback: Runnable = mockk() + clock.setCurrentTime(456) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { mockUserService.getUserInfo() } returns mockUserInfo + + cfg = RemoteConfig( + sessionConfig = SessionRemoteConfig( + sessionComponents = emptySet() + ), + eventLimits = mapOf(eventId to threshold) + ) + + eventHandler.onEventStarted( + eventId, + eventName, + startTime, + mockSessionProperties, + mapOf(), + mockTimeoutCallback + ) + + verify { mockDeliveryService wasNot Called } + } + + @Test + fun `verify onEventStarted sends the event if feature gating allows it`() { + val eventId = "event-id" + val eventName = "event-name" + val startTime = 123L + val threshold = 100L + val sessionPropertiesMap: Map = mapOf() + val mockTimeoutCallback: Runnable = mockk() + clock.setCurrentTime(456) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { mockUserService.getUserInfo() } returns mockUserInfo + + cfg = RemoteConfig( + sessionConfig = SessionRemoteConfig( + sessionComponents = setOf("s_mts") + ), + eventLimits = mapOf(eventId to threshold) + ) + + eventHandler.onEventStarted( + eventId, + eventName, + startTime, + mockSessionProperties, + mapOf(), + mockTimeoutCallback + ) + + verify { mockDeliveryService.sendEventAsync(any()) } + } + + @Test + fun `verify onEventEnded prevents send event if feature gating gates it`() { + val eventProperties = mapOf() + val startTime = 100L + val endTime = 300L + val sessionPropertiesMap: Map = mapOf() + clock.setCurrentTime(endTime) + val originEventId = "origin-event-id" + val originEventName = "origin-event-name" + val mockPerformanceInfo: PerformanceInfo = mockk() + + val originEvent = Event( + timestamp = startTime, + eventId = originEventId, + name = originEventName, + type = EmbraceEvent.Type.START + ) + val originEventDescription = EventDescription(mockLateTimer, originEvent) + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { + mockPerformanceService.getPerformanceInfo( + startTime, + endTime, + false + ) + } returns mockPerformanceInfo + every { mockUserService.getUserInfo() } returns mockUserInfo + cfg = createGatingConfig(emptySet()) + + eventHandler.onEventEnded( + originEventDescription, + false, + eventProperties, + mockSessionProperties + ) + + verify { mockDeliveryService wasNot Called } + } + + @Test + fun `verify onEventEnded sends event if feature gating allows it`() { + val eventProperties = mapOf() + val startTime = 100L + val endTime = 300L + val sessionPropertiesMap: Map = mapOf() + clock.setCurrentTime(endTime) + val originEventId = "origin-event-id" + val originEventName = "origin-event-name" + val mockPerformanceInfo: PerformanceInfo = mockk() + + val originEvent = Event( + timestamp = startTime, + eventId = originEventId, + name = originEventName, + type = EmbraceEvent.Type.START + ) + val originEventDescription = EventDescription(mockLateTimer, originEvent) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { + mockPerformanceService.getPerformanceInfo( + startTime, + endTime, + false + ) + } returns mockPerformanceInfo + every { mockUserService.getUserInfo() } returns mockUserInfo + cfg = createGatingConfig(setOf("s_mts")) + + eventHandler.onEventEnded( + originEventDescription, + false, + eventProperties, + mockSessionProperties + ) + + verify { mockDeliveryService.sendEventAsync(any()) } + } + + @Test + fun `verify onEventEnded prevents send startup event if feature gating gates it`() { + val eventProperties = mapOf() + val startTime = 100L + val endTime = 300L + val sessionPropertiesMap: Map = mapOf() + clock.setCurrentTime(endTime) + val originEventId = "origin-event-id" + val originEventName = EmbraceEventService.STARTUP_EVENT_NAME + val mockPerformanceInfo: PerformanceInfo = mockk() + + val originEvent = Event( + timestamp = startTime, + eventId = originEventId, + name = originEventName, + type = EmbraceEvent.Type.START + ) + val originEventDescription = EventDescription(mockLateTimer, originEvent) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { + mockPerformanceService.getPerformanceInfo( + startTime, + endTime, + false + ) + } returns mockPerformanceInfo + every { mockUserService.getUserInfo() } returns mockUserInfo + cfg = createGatingConfig(emptySet()) + + eventHandler.onEventEnded( + originEventDescription, + false, + eventProperties, + mockSessionProperties + ) + + verify { mockDeliveryService wasNot Called } + } + + @Test + fun `verify onEventEnded sends startup event if feature gating allows it`() { + val eventProperties = mapOf() + val startTime = 100L + val endTime = 300L + val sessionPropertiesMap: Map = mapOf() + clock.setCurrentTime(endTime) + val originEventId = "origin-event-id" + val originEventName = EmbraceEventService.STARTUP_EVENT_NAME + + val mockPerformanceInfo: PerformanceInfo = mockk() + + val originEvent = Event( + timestamp = startTime, + eventId = originEventId, + name = originEventName, + type = EmbraceEvent.Type.START + ) + val originEventDescription = EventDescription(mockLateTimer, originEvent) + + every { mockSessionProperties.get() } returns sessionPropertiesMap + every { + mockPerformanceService.getPerformanceInfo( + startTime, + endTime, + false + ) + } returns mockPerformanceInfo + every { mockUserService.getUserInfo() } returns mockUserInfo + createGatingConfig(setOf("s_mts")) + + eventHandler.onEventEnded( + originEventDescription, + false, + eventProperties, + mockSessionProperties + ) + + verify { mockDeliveryService.sendEventAsync(any()) } + } + + private fun createGatingConfig(components: Set) = RemoteConfig( + sessionConfig = SessionRemoteConfig( + sessionComponents = components + ) + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/BehaviorFakes.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/BehaviorFakes.kt new file mode 100644 index 0000000000..668ffcfe6a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/BehaviorFakes.kt @@ -0,0 +1,161 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.config.behavior.AnrBehavior +import io.embrace.android.embracesdk.config.behavior.AppExitInfoBehavior +import io.embrace.android.embracesdk.config.behavior.AutoDataCaptureBehavior +import io.embrace.android.embracesdk.config.behavior.BackgroundActivityBehavior +import io.embrace.android.embracesdk.config.behavior.BehaviorThresholdCheck +import io.embrace.android.embracesdk.config.behavior.BreadcrumbBehavior +import io.embrace.android.embracesdk.config.behavior.DataCaptureEventBehavior +import io.embrace.android.embracesdk.config.behavior.LogMessageBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.config.behavior.SdkEndpointBehavior +import io.embrace.android.embracesdk.config.behavior.SdkModeBehavior +import io.embrace.android.embracesdk.config.behavior.SessionBehavior +import io.embrace.android.embracesdk.config.behavior.SpansBehavior +import io.embrace.android.embracesdk.config.behavior.StartupBehavior +import io.embrace.android.embracesdk.config.behavior.WebViewVitalsBehavior +import io.embrace.android.embracesdk.config.local.AnrLocalConfig +import io.embrace.android.embracesdk.config.local.AppExitInfoLocalConfig +import io.embrace.android.embracesdk.config.local.BackgroundActivityLocalConfig +import io.embrace.android.embracesdk.config.local.BaseUrlLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.local.SessionLocalConfig +import io.embrace.android.embracesdk.config.local.StartupMomentLocalConfig +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.config.remote.BackgroundActivityRemoteConfig +import io.embrace.android.embracesdk.config.remote.LogRemoteConfig +import io.embrace.android.embracesdk.config.remote.NetworkSpanForwardingRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig +import io.embrace.android.embracesdk.internal.utils.Uuid + +private val behaviorThresholdCheck = BehaviorThresholdCheck { Uuid.getEmbUuid() } + +/** + * A fake [AnrBehavior] that returns default values. + */ +internal fun fakeAnrBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> AnrLocalConfig? = { null }, + remoteCfg: () -> AnrRemoteConfig? = { null } +) = AnrBehavior(thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [SessionBehavior] that returns default values. + */ +internal fun fakeSessionBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> SessionLocalConfig? = { null }, + remoteCfg: () -> RemoteConfig? = { null } +) = SessionBehavior(thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [NetworkBehavior] that returns default values. + */ +internal fun fakeNetworkBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> SdkLocalConfig? = { null }, + remoteCfg: () -> RemoteConfig? = { null } +) = NetworkBehavior(thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [BackgroundActivityBehavior] that returns default values. + */ +internal fun fakeBackgroundActivityBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> BackgroundActivityLocalConfig? = { null }, + remoteCfg: () -> BackgroundActivityRemoteConfig? = { null } +) = BackgroundActivityBehavior(thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [AutoDataCaptureBehavior] that returns default values. + */ +internal fun fakeAutoDataCaptureBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> LocalConfig? = { null }, + remoteCfg: () -> RemoteConfig? = { null } +) = AutoDataCaptureBehavior(thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [BreadcrumbBehavior] that returns default values. + */ +internal fun fakeBreadcrumbBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> SdkLocalConfig? = { null }, + remoteCfg: () -> RemoteConfig? = { null } +) = BreadcrumbBehavior(thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [LogMessageBehavior] that returns default values. + */ +internal fun fakeLogMessageBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + remoteCfg: () -> LogRemoteConfig? = { null } +) = LogMessageBehavior(thresholdCheck, remoteCfg) + +/** + * A fake [SpanBehavior] that returns default values. + */ +internal fun fakeSpansBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + remoteConfig: () -> SpansRemoteConfig? = { null } +) = SpansBehavior(thresholdCheck, remoteConfig) + +/** + * A fake [StartupBehavior] that returns default values. + */ +internal fun fakeStartupBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> StartupMomentLocalConfig? = { null } +) = StartupBehavior(thresholdCheck, localCfg) + +/** + * A fake [DataCaptureEventBehavior] that returns default values. + */ +internal fun fakeDataCaptureEventBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + remoteCfg: () -> RemoteConfig? = { null } +) = DataCaptureEventBehavior(thresholdCheck, remoteCfg) + +/** + * A fake [SdkModeBehavior] that returns default values. + */ +internal fun fakeSdkModeBehavior( + isDebug: Boolean = false, + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> LocalConfig? = { null }, + remoteCfg: () -> RemoteConfig? = { null } +) = SdkModeBehavior(isDebug, thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [SdkModeBehavior] that returns default values. + */ +internal fun fakeSdkEndpointBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> BaseUrlLocalConfig? = { null }, +) = SdkEndpointBehavior(thresholdCheck, localCfg) + +/** + * A fake [AppExitInfoBehavior] that returns default values. + */ +internal fun fakeAppExitInfoBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + localCfg: () -> AppExitInfoLocalConfig? = { null }, + remoteCfg: () -> RemoteConfig? = { null }, +) = AppExitInfoBehavior(thresholdCheck, localCfg, remoteCfg) + +/** + * A fake [NetworkSpanForwardingBehavior] that returns default values. + */ +internal fun fakeNetworkSpanForwardingBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + remoteConfig: () -> NetworkSpanForwardingRemoteConfig? = { null } +) = NetworkSpanForwardingBehavior(thresholdCheck, remoteConfig) + +internal fun fakeWebViewVitalsBehavior( + thresholdCheck: BehaviorThresholdCheck = behaviorThresholdCheck, + remoteCfg: () -> RemoteConfig? = { null }, +) = WebViewVitalsBehavior(thresholdCheck, remoteCfg) diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeActivityService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeActivityService.kt new file mode 100644 index 0000000000..3d57caec7b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeActivityService.kt @@ -0,0 +1,69 @@ +package io.embrace.android.embracesdk.fakes + +import android.app.Activity +import android.content.res.Configuration +import android.os.Bundle +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.ActivityService + +internal class FakeActivityService( + override var isInBackground: Boolean = false, + override var foregroundActivity: Activity? = null +) : ActivityService { + + val listeners: MutableList = mutableListOf() + var config: Configuration? = null + + override fun addListener(listener: ActivityListener) { + listeners.add(listener) + } + + override fun onConfigurationChanged(config: Configuration) { + this.config = config + } + + override fun onLowMemory() { + TODO("Not yet implemented") + } + + override fun onTrimMemory(memoryTrimLevel: Int) { + TODO("Not yet implemented") + } + + override fun onActivityCreated(p0: Activity, p1: Bundle?) { + TODO("Not yet implemented") + } + + override fun onActivityStarted(p0: Activity) { + TODO("Not yet implemented") + } + + override fun onActivityResumed(p0: Activity) { + TODO("Not yet implemented") + } + + override fun onActivityPaused(p0: Activity) { + TODO("Not yet implemented") + } + + override fun onActivityStopped(p0: Activity) { + TODO("Not yet implemented") + } + + override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) { + TODO("Not yet implemented") + } + + override fun onActivityDestroyed(p0: Activity) { + TODO("Not yet implemented") + } + + override fun close() { + } + + override fun onForeground() { + } + + override fun onBackground() { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidMetadataService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidMetadataService.kt new file mode 100644 index 0000000000..219a5d55e7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidMetadataService.kt @@ -0,0 +1,132 @@ +package io.embrace.android.embracesdk.fakes + +import android.content.Context +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.DiskUsage + +/** + * Fake implementation of [MetadataService] that represents an Android device. A [UnsupportedOperationException] will be thrown + * if you attempt set info about Flutter/Unity/ReactNative on this fake, which is decided for an Android device. + */ +internal class FakeAndroidMetadataService(sessionId: String? = null) : MetadataService { + companion object { + private val androidAppInfo = AppInfo( + appVersion = "1.0.0", + appFramework = Embrace.AppFramework.NATIVE.value, + buildId = "100", + buildType = "release", + buildFlavor = "oem", + environment = "prod", + bundleVersion = "5ac7fe", + sdkSimpleVersion = "5.10.0", + sdkVersion = "5.11.0", + buildGuid = "5092abc" + ) + private val androidDeviceInfo = DeviceInfo() + private val diskUsage = DiskUsage( + appDiskUsage = 10000000L, + deviceDiskFree = 500000000L + ) + private const val screenResolution = "1080x720" + private const val fakeAppVersion: String = "1" + private const val fakeAppVersionName: String = "1.0.0" + private const val APP_STATE_FOREGROUND = "foreground" + private const val APP_STATE_BACKGROUND = "background" + private const val cpuName = "fakeCpu" + private const val egl = "fakeEgl" + } + + var appUpdated = false + var osUpdated = false + var fakeAppId: String = "o0o0o" + var fakeDeviceId: String = "07D85B44E4E245F4A30E559BFC0D07FF" + var fakeReactNativeBundleId: String? = "fakeReactNativeBundleId" + var fakeFlutterSdkVersion: String? = "fakeFlutterSdkVersion" + var fakeDartVersion: String? = "fakeDartVersion" + var fakeRnSdkVersion: String? = "fakeRnSdkVersion" + + private lateinit var appState: String + private var appSessionId: String? = null + + init { + setAppForeground() + appSessionId = sessionId + } + + fun setAppForeground() { + appState = APP_STATE_FOREGROUND + } + + fun setAppId(id: String) { + fakeAppId = id + } + + fun setAppBackground() { + appState = APP_STATE_BACKGROUND + } + + override fun getAppInfo(): AppInfo = androidAppInfo + + override fun getLightweightAppInfo(): AppInfo = androidAppInfo + + override fun getAppId(): String = fakeAppId + + override fun getDeviceInfo(): DeviceInfo = androidDeviceInfo + + override fun getLightweightDeviceInfo(): DeviceInfo = androidDeviceInfo + + override fun getDiskUsage(): DiskUsage = diskUsage + + override fun getScreenResolution(): String = screenResolution + + override fun isJailbroken(): Boolean = false + + override fun getDeviceId(): String = fakeDeviceId + + override fun getAppVersionCode(): String = fakeAppVersion + + override fun getAppVersionName(): String = fakeAppVersionName + + override fun isAppUpdated(): Boolean = appUpdated + + override fun isOsUpdated(): Boolean = osUpdated + + override val activeSessionId: String? + get() = appSessionId + + override fun setActiveSessionId(sessionId: String?) { + appSessionId = sessionId + } + + override fun removeActiveSessionId(sessionId: String?) { + if (appSessionId == sessionId) { + appSessionId = null + } + } + + override fun getAppState(): String = appState + + override fun setReactNativeBundleId(context: Context, jsBundleIdUrl: String?) { + fakeReactNativeBundleId = jsBundleIdUrl + } + + override fun setEmbraceFlutterSdkVersion(version: String?) { + fakeFlutterSdkVersion = version + } + + override fun setRnSdkVersion(version: String?) { + fakeRnSdkVersion = version + } + + override fun setDartVersion(version: String?) { + fakeDartVersion = version + } + + override fun precomputeValues() {} + override fun getCpuName(): String? = cpuName + + override fun getEgl(): String? = egl +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidResourcesService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidResourcesService.kt new file mode 100644 index 0000000000..80eaaaaae2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAndroidResourcesService.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.fakes + +import android.content.res.Resources +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.internal.AndroidResourcesService +import io.embrace.android.embracesdk.internal.BuildInfo + +/** + * Fake [AndroidResourcesService] loaded with gradle-populated resources from the build that the SDK expects. + * New identifiers and associated strings can be added by directly accessing [resourceValues], but the identifiers returned from + * [getIdentifier] are fixed to the few that the SDK expects + */ +internal class FakeAndroidResourcesService : AndroidResourcesService { + val resourceValues = sdkResources.associate { it.second to it.third }.toMutableMap() + private val identifiers = sdkResources.associate { it.first to it.second } + + override fun getIdentifier(name: String?, defType: String?, defPackage: String?): Int = identifiers[requireNotNull(name)] ?: 0 + + override fun getString(id: Int): String = resourceValues[id] ?: throw Resources.NotFoundException() + + companion object { + private val sdkResources = listOf( + Triple(BuildInfo.BUILD_INFO_BUILD_ID, 9991, "5.22.0"), + Triple(BuildInfo.BUILD_INFO_BUILD_TYPE, 9992, "release"), + Triple(BuildInfo.BUILD_INFO_BUILD_FLAVOR, 9993, "delicious"), + Triple(LocalConfig.BUILD_INFO_APP_ID, 9994, "true"), + Triple(LocalConfig.BUILD_INFO_NDK_ENABLED, 9995, "true"), + Triple(LocalConfig.NDK_ENABLED_DEFAULT.toString(), 9996, "true") + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAnrService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAnrService.kt new file mode 100644 index 0000000000..e8601b8425 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeAnrService.kt @@ -0,0 +1,39 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.anr.BlockedThreadListener +import io.embrace.android.embracesdk.anr.detection.AnrProcessErrorStateInfo +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.payload.AnrInterval + +internal class FakeAnrService : AnrService { + + var data: List = mutableListOf() + var anrProcessErrors: List = mutableListOf() + + override fun cleanCollections() { + TODO("Not yet implemented") + } + + override fun getCapturedData(): List = data + + override fun getAnrProcessErrors(startTime: Long): List = anrProcessErrors + + override fun forceAnrTrackingStopOnCrash() { + TODO("Not yet implemented") + } + + override fun finishInitialization( + configService: ConfigService + ) { + TODO("Not yet implemented") + } + + override fun addBlockedThreadListener(listener: BlockedThreadListener) { + TODO("Not yet implemented") + } + + override fun close() { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApiService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApiService.kt new file mode 100644 index 0000000000..0feb12c5e3 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApiService.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.comms.api.ApiService +import io.embrace.android.embracesdk.comms.api.CachedConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig + +internal class FakeApiService : ApiService { + override fun getConfig(): RemoteConfig? { + TODO("Not yet implemented") + } + + override fun getCachedConfig(): CachedConfig { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApplicationExitInfoService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApplicationExitInfoService.kt new file mode 100644 index 0000000000..03722e704a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeApplicationExitInfoService.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.aei.ApplicationExitInfoService +import io.embrace.android.embracesdk.payload.AppExitInfoData + +internal class FakeApplicationExitInfoService : ApplicationExitInfoService { + + var data: List = + listOf(AppExitInfoData(null, null, null, null, null, null, null, null, null, null, null)) + + override fun cleanCollections() { + } + + override fun getCapturedData(): List = data +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt new file mode 100644 index 0000000000..4b7981b58e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.clock.Clock + +internal class FakeClock( + private var currentTime: Long = 0 +) : Clock { + + fun setCurrentTime(currentTime: Long) { + this.currentTime = currentTime + } + + @JvmOverloads + fun tick(millis: Long = 1) { + currentTime += millis + } + + fun tickSecond() = tick(1000) + + override fun now(): Long = currentTime +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeConfigService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeConfigService.kt new file mode 100644 index 0000000000..8b942d4359 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeConfigService.kt @@ -0,0 +1,67 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.config.ConfigListener +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.AnrBehavior +import io.embrace.android.embracesdk.config.behavior.AppExitInfoBehavior +import io.embrace.android.embracesdk.config.behavior.AutoDataCaptureBehavior +import io.embrace.android.embracesdk.config.behavior.BackgroundActivityBehavior +import io.embrace.android.embracesdk.config.behavior.BreadcrumbBehavior +import io.embrace.android.embracesdk.config.behavior.DataCaptureEventBehavior +import io.embrace.android.embracesdk.config.behavior.LogMessageBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkBehavior +import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.config.behavior.SdkEndpointBehavior +import io.embrace.android.embracesdk.config.behavior.SdkModeBehavior +import io.embrace.android.embracesdk.config.behavior.SessionBehavior +import io.embrace.android.embracesdk.config.behavior.SpansBehavior +import io.embrace.android.embracesdk.config.behavior.StartupBehavior +import io.embrace.android.embracesdk.config.behavior.WebViewVitalsBehavior + +/** + * Fake [ConfigService] used for testing. Updates to registered listeners can be triggered by calling [updateListeners]. Note that the + * current config values of this object will be propagated, and you can trigger this fake update even if you have not changed the underlying + * data. Beware of this difference in implementation compared to the real EmbraceConfigService + */ +internal class FakeConfigService( + var sdkDisabled: Boolean = false, + private val backgroundActivityCaptureEnabled: Boolean = false, + private val hasValidRemoteConfig: Boolean = false, + override val backgroundActivityBehavior: BackgroundActivityBehavior = fakeBackgroundActivityBehavior(), + override val autoDataCaptureBehavior: AutoDataCaptureBehavior = fakeAutoDataCaptureBehavior(), + override val breadcrumbBehavior: BreadcrumbBehavior = fakeBreadcrumbBehavior(), + override val logMessageBehavior: LogMessageBehavior = fakeLogMessageBehavior(), + override val anrBehavior: AnrBehavior = fakeAnrBehavior(), + override val sessionBehavior: SessionBehavior = fakeSessionBehavior(), + override val networkBehavior: NetworkBehavior = fakeNetworkBehavior(), + override val spansBehavior: SpansBehavior = fakeSpansBehavior(), + override val startupBehavior: StartupBehavior = fakeStartupBehavior(), + override val dataCaptureEventBehavior: DataCaptureEventBehavior = fakeDataCaptureEventBehavior(), + override val sdkModeBehavior: SdkModeBehavior = fakeSdkModeBehavior(), + override val sdkEndpointBehavior: SdkEndpointBehavior = fakeSdkEndpointBehavior(), + override val webViewVitalsBehavior: WebViewVitalsBehavior = fakeWebViewVitalsBehavior(), + override val appExitInfoBehavior: AppExitInfoBehavior = fakeAppExitInfoBehavior(), + override val networkSpanForwardingBehavior: NetworkSpanForwardingBehavior = fakeNetworkSpanForwardingBehavior(), +) : ConfigService { + + val listeners = mutableSetOf() + + override fun addListener(configListener: ConfigListener) { + listeners.add(configListener) + } + + override fun isSdkDisabled(): Boolean = sdkDisabled + + override fun isBackgroundActivityCaptureEnabled() = backgroundActivityCaptureEnabled + + override fun hasValidRemoteConfig(): Boolean = hasValidRemoteConfig + override fun isAppExitInfoCaptureEnabled(): Boolean = appExitInfoBehavior.isEnabled() + + override fun close() {} + + fun updateListeners() { + listeners.forEach { + it.onConfigChange(this) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCpuInfoDelegate.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCpuInfoDelegate.kt new file mode 100644 index 0000000000..42839728ee --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCpuInfoDelegate.kt @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.cpu.CpuInfoDelegate + +internal class FakeCpuInfoDelegate( + private val cpuName: String? = "fake_cpu", + private val elg: String = "fake_elg" +) : CpuInfoDelegate { + override fun getCpuName(): String? = cpuName + + override fun getElg(): String? = elg +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCrashService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCrashService.kt new file mode 100644 index 0000000000..eb5c5b55a0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeCrashService.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.crash.CrashService +import io.embrace.android.embracesdk.payload.JsException + +internal class FakeCrashService : CrashService { + internal var exception: Throwable? = null + internal var jsException: JsException? = null + + override fun handleCrash(thread: Thread, exception: Throwable) { + this.exception = exception + } + + override fun logUnhandledJsException(exception: JsException) { + jsException = exception + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDataCaptureService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDataCaptureService.kt new file mode 100644 index 0000000000..c6f3f77514 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDataCaptureService.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.arch.DataCaptureService + +internal abstract class FakeDataCaptureService : DataCaptureService?> { + + internal var data: List? = mutableListOf() + + override fun cleanCollections() { + } + + override fun getCapturedData(): List? = data +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDeviceArchitecture.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDeviceArchitecture.kt new file mode 100644 index 0000000000..eee83c4486 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeDeviceArchitecture.kt @@ -0,0 +1,8 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.internal.DeviceArchitecture + +internal class FakeDeviceArchitecture( + override var architecture: String = "arm64-v8a", + override var is32BitDevice: Boolean = true +) : DeviceArchitecture diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeEventService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeEventService.kt new file mode 100644 index 0000000000..873cc26e1c --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeEventService.kt @@ -0,0 +1,67 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.internal.StartupEventInfo + +internal class FakeEventService : EventService { + override fun startEvent(name: String) { + TODO("Not yet implemented") + } + + override fun startEvent(name: String, identifier: String?) { + TODO("Not yet implemented") + } + + override fun startEvent(name: String, identifier: String?, properties: Map?) { + TODO("Not yet implemented") + } + + override fun startEvent( + name: String, + identifier: String?, + properties: Map?, + startTime: Long? + ) { + TODO("Not yet implemented") + } + + override fun endEvent(name: String) { + TODO("Not yet implemented") + } + + override fun endEvent(name: String, identifier: String?) { + TODO("Not yet implemented") + } + + override fun endEvent(name: String, properties: Map?) { + TODO("Not yet implemented") + } + + override fun endEvent(name: String, identifier: String?, properties: Map?) { + TODO("Not yet implemented") + } + + override fun findEventIdsForSession(startTime: Long, endTime: Long): List { + TODO("Not yet implemented") + } + + override fun getActiveEventIds(): List? { + TODO("Not yet implemented") + } + + override fun getStartupMomentInfo(): StartupEventInfo? { + TODO("Not yet implemented") + } + + override fun sendStartupMoment() { + TODO("Not yet implemented") + } + + override fun setProcessStartedByNotification() { + TODO("Not yet implemented") + } + + override fun close() { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeGatingService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeGatingService.kt new file mode 100644 index 0000000000..9110bc6369 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeGatingService.kt @@ -0,0 +1,30 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.gating.EmbraceGatingService +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.SessionMessage + +/** + * An implementation of [GatingService] that does a pass-through to [EmbraceGatingService] but tracks the message that go through it + */ +internal class FakeGatingService(configService: ConfigService = FakeConfigService()) : + GatingService { + val sessionMessagesFiltered = mutableListOf() + val eventMessagesFiltered = mutableListOf() + + private val realGatingService = EmbraceGatingService(configService) + + override fun gateSessionMessage(sessionMessage: SessionMessage): SessionMessage { + val filteredMessage = realGatingService.gateSessionMessage(sessionMessage) + sessionMessagesFiltered.add(sessionMessage) + return filteredMessage + } + + override fun gateEventMessage(eventMessage: EventMessage): EventMessage { + val filteredMessage = realGatingService.gateEventMessage(eventMessage) + eventMessagesFiltered.add(eventMessage) + return filteredMessage + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeLoggerAction.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeLoggerAction.kt new file mode 100644 index 0000000000..5ff994e833 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeLoggerAction.kt @@ -0,0 +1,26 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Severity +import java.util.LinkedList + +internal class FakeLoggerAction : InternalEmbraceLogger.LoggerAction { + + val msgQueue = LinkedList() + + override fun log( + msg: String, + severity: Severity, + throwable: Throwable?, + logStacktrace: Boolean + ) { + msgQueue.add(LogMessage(msg, severity, throwable, logStacktrace)) + } + + internal data class LogMessage( + val msg: String, + val severity: Severity, + val throwable: Throwable?, + val logStacktrace: Boolean + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerListener.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerListener.kt new file mode 100644 index 0000000000..0b92e0e8d9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerListener.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.session.MemoryCleanerListener + +internal class FakeMemoryCleanerListener : MemoryCleanerListener { + + val callCount: Int get() = counter + private var counter = 0 + + override fun cleanCollections() { + counter += 1 + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerService.kt new file mode 100644 index 0000000000..44f48eaa55 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryCleanerService.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import io.embrace.android.embracesdk.session.MemoryCleanerService + +internal class FakeMemoryCleanerService : MemoryCleanerService { + + val listeners = mutableListOf() + + override fun addListener(listener: MemoryCleanerListener) { + listeners.add(listener) + } + + override fun cleanServicesCollections( + exceptionService: EmbraceInternalErrorService + ) { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryService.kt new file mode 100644 index 0000000000..2c62f33024 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeMemoryService.kt @@ -0,0 +1,11 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.memory.MemoryService +import io.embrace.android.embracesdk.payload.MemoryWarning + +internal class FakeMemoryService : FakeDataCaptureService(), MemoryService { + + override fun onMemoryWarning() { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkConnectivityService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkConnectivityService.kt new file mode 100644 index 0000000000..b87fe43936 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkConnectivityService.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityListener +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.comms.delivery.NetworkStatus +import io.embrace.android.embracesdk.payload.Interval + +internal class FakeNetworkConnectivityService : FakeDataCaptureService(), NetworkConnectivityService { + + override fun networkStatusOnSessionStarted(startTime: Long) { + TODO("Not yet implemented") + } + + override fun addNetworkConnectivityListener(listener: NetworkConnectivityListener) { + TODO("Not yet implemented") + } + + override fun removeNetworkConnectivityListener(listener: NetworkConnectivityListener) { + TODO("Not yet implemented") + } + + override fun getCurrentNetworkStatus(): NetworkStatus { + TODO("Not yet implemented") + } + + override val ipAddress: String + get() = TODO("Not yet implemented") + + override fun close() { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt new file mode 100644 index 0000000000..3c578ff345 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt @@ -0,0 +1,42 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.network.logging.NetworkLoggingService +import io.embrace.android.embracesdk.payload.NetworkSessionV2 + +internal class FakeNetworkLoggingService : NetworkLoggingService { + + var data: NetworkSessionV2 = NetworkSessionV2(emptyList(), emptyMap()) + + override fun getNetworkCallsForSession(startTime: Long, lastKnownTime: Long): NetworkSessionV2 = + data + + override fun logNetworkCall( + url: String, + httpMethod: String, + statusCode: Int, + startTime: Long, + endTime: Long, + bytesSent: Long, + bytesReceived: Long, + traceId: String?, + w3cTraceparent: String?, + networkCaptureData: NetworkCaptureData? + ) { + TODO("Not yet implemented") + } + + override fun logNetworkError( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + errorType: String?, + errorMessage: String?, + traceId: String?, + w3cTraceparent: String?, + networkCaptureData: NetworkCaptureData? + ) { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOpenTelemetryClock.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOpenTelemetryClock.kt new file mode 100644 index 0000000000..cc643ec587 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOpenTelemetryClock.kt @@ -0,0 +1,20 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.internal.OpenTelemetryClock + +/** + * An OpenTelemetry-compatible clock used for tests that calculates the elapsed time using the difference between the current time and when + * the instance is initialized plus the fake elapsed time that can be passed in + */ +internal class FakeOpenTelemetryClock( + embraceClock: Clock, + private val startingElapsedTimeNanos: Long = 0L +) : io.opentelemetry.sdk.common.Clock { + + private val realOpenTelemetryClock = OpenTelemetryClock(embraceClock = embraceClock) + private val startingTimeNanos = now() + override fun now(): Long = realOpenTelemetryClock.now() + + override fun nanoTime(): Long = startingElapsedTimeNanos + now() - startingTimeNanos +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOrientationService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOrientationService.kt new file mode 100644 index 0000000000..00864c0fe2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeOrientationService.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.orientation.OrientationService +import io.embrace.android.embracesdk.payload.Orientation + +internal class FakeOrientationService : FakeDataCaptureService(), OrientationService { + + override fun onOrientationChanged(orientation: Int?) { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePerformanceInfoService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePerformanceInfoService.kt new file mode 100644 index 0000000000..6161c7a5b2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePerformanceInfoService.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.payload.PerformanceInfo + +/** + * Fake [PerformanceInfoService] that allows you to set whatever you want on it to be returned + */ +internal class FakePerformanceInfoService( + var performanceInfo: PerformanceInfo = PerformanceInfo(), + var sessionPerformanceInfo: PerformanceInfo = PerformanceInfo() +) : PerformanceInfoService { + override fun getPerformanceInfo(startTime: Long, endTime: Long, coldStart: Boolean): PerformanceInfo = performanceInfo + + override fun getSessionPerformanceInfo( + sessionStart: Long, + sessionLastKnownTime: Long, + coldStart: Boolean, + receivedTermination: Boolean? + ): PerformanceInfo = + sessionPerformanceInfo +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePowerSaveModeService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePowerSaveModeService.kt new file mode 100644 index 0000000000..e8c7389192 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePowerSaveModeService.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.powersave.PowerSaveModeService +import io.embrace.android.embracesdk.payload.PowerModeInterval + +internal class FakePowerSaveModeService : FakeDataCaptureService(), PowerSaveModeService { + + override fun close() { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePreferenceService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePreferenceService.kt new file mode 100644 index 0000000000..761644f91d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakePreferenceService.kt @@ -0,0 +1,51 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.prefs.PreferencesService + +@Suppress("LongParameterList") +internal class FakePreferenceService( + override var appVersion: String? = null, + override var osVersion: String? = null, + override var installDate: Long? = 0, + override var deviceIdentifier: String = "", + override val sdkStartupStatus: String? = null, + override var sdkDisabled: Boolean = false, + override var userPayer: Boolean = false, + override var userIdentifier: String? = null, + override var userEmailAddress: String? = null, + override var userPersonas: Set? = null, + override var username: String? = null, + override var permanentSessionProperties: Map? = null, + @Deprecated("") override var customPersonas: Set? = null, + override var lastConfigFetchDate: Long? = null, + override var userMessageNeedsRetry: Boolean = false, + override var sessionNumber: Int = 0, + override var reactNativeVersionNumber: String? = null, + override var unityVersionNumber: String? = null, + override var unityBuildIdNumber: String? = null, + override var unitySdkVersionNumber: String? = null, + override var screenResolution: String? = null, + override var backgroundActivityEnabled: Boolean = false, + override var dartSdkVersion: String? = null, + override var javaScriptBundleURL: String? = null, + override var rnSdkVersion: String? = null, + override var javaScriptPatchNumber: String? = null, + override var embraceFlutterSdkVersion: String? = null, + override var jailbroken: Boolean? = null, + override var applicationExitInfoHistory: Set? = null, + override var cpuName: String? = null, + override var egl: String? = null +) : PreferencesService { + + var networkCaptureRuleOver = false + var firstDay: Boolean = false + + override fun isNetworkCaptureRuleOver(id: String): Boolean { + return networkCaptureRuleOver + } + + override fun decreaseNetworkCaptureRuleRemainingCount(id: String, maxCount: Int) { + } + + override fun isUsersFirstDay(): Boolean = firstDay +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSession.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSession.kt new file mode 100644 index 0000000000..83dce51549 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSession.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.payload.Session + +internal fun fakeSession(): Session = Session.buildStartSession( + "fakeSessionId", + true, + Session.SessionLifeEventType.STATE, + 160000000000L, + 1, + null, + mapOf() +) diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSessionProperties.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSessionProperties.kt new file mode 100644 index 0000000000..28465587d7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeSessionProperties.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.session.EmbraceSessionProperties + +internal fun fakeEmbraceSessionProperties() = EmbraceSessionProperties( + FakePreferenceService(), + InternalEmbraceLogger(), + FakeConfigService() +) diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeStrictModeService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeStrictModeService.kt new file mode 100644 index 0000000000..15ea1fc4ed --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeStrictModeService.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.strictmode.StrictModeService +import io.embrace.android.embracesdk.payload.StrictModeViolation + +internal class FakeStrictModeService : FakeDataCaptureService(), StrictModeService { + + override fun start() { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeThermalStatusService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeThermalStatusService.kt new file mode 100644 index 0000000000..e32ff77898 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeThermalStatusService.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.thermalstate.ThermalStatusService +import io.embrace.android.embracesdk.payload.ThermalState + +internal class FakeThermalStatusService : FakeDataCaptureService(), ThermalStatusService { + override fun close() { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeUserService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeUserService.kt new file mode 100644 index 0000000000..1203153167 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeUserService.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.payload.UserInfo + +internal class FakeUserService : UserService { + + var obj: UserInfo = UserInfo() + + override fun getUserInfo(): UserInfo = obj + + override fun clearAllUserInfo() { + TODO("Not yet implemented") + } + + override fun loadUserInfoFromDisk(): UserInfo? { + return obj + } + + override fun setUserIdentifier(userId: String?) { + TODO("Not yet implemented") + } + + override fun clearUserIdentifier() { + TODO("Not yet implemented") + } + + override fun setUserEmail(email: String?) { + TODO("Not yet implemented") + } + + override fun clearUserEmail() { + TODO("Not yet implemented") + } + + override fun setUserAsPayer() { + TODO("Not yet implemented") + } + + override fun clearUserAsPayer() { + TODO("Not yet implemented") + } + + override fun addUserPersona(persona: String?) { + TODO("Not yet implemented") + } + + override fun clearUserPersona(persona: String?) { + TODO("Not yet implemented") + } + + override fun clearAllUserPersonas() { + TODO("Not yet implemented") + } + + override fun setUsername(username: String?) { + TODO("Not yet implemented") + } + + override fun clearUsername() { + TODO("Not yet implemented") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeVersionChecker.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeVersionChecker.kt new file mode 100644 index 0000000000..53935a7fd8 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeVersionChecker.kt @@ -0,0 +1,7 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.utils.VersionChecker + +internal class FakeVersionChecker(private val enabled: Boolean) : VersionChecker { + override fun isAtLeast(min: Int): Boolean = enabled +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeWebViewService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeWebViewService.kt new file mode 100644 index 0000000000..c58f97a8b7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeWebViewService.kt @@ -0,0 +1,10 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.capture.webview.WebViewService +import io.embrace.android.embracesdk.payload.WebViewInfo + +internal class FakeWebViewService : FakeDataCaptureService(), WebViewService { + + override fun collectWebData(tag: String, message: String) { + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAndroidServicesModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAndroidServicesModule.kt new file mode 100644 index 0000000000..53e450575b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAndroidServicesModule.kt @@ -0,0 +1,9 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.injection.AndroidServicesModule +import io.embrace.android.embracesdk.prefs.PreferencesService + +internal class FakeAndroidServicesModule( + override val preferencesService: PreferencesService = FakePreferenceService() +) : AndroidServicesModule diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAnrModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAnrModule.kt new file mode 100644 index 0000000000..94f4e433e7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeAnrModule.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.anr.AnrService +import io.embrace.android.embracesdk.anr.sigquit.GoogleAnrTimestampRepository +import io.embrace.android.embracesdk.fakes.FakeAnrService +import io.embrace.android.embracesdk.injection.AnrModule +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger + +internal class FakeAnrModule( + override val anrService: AnrService = FakeAnrService(), + override val googleAnrTimestampRepository: GoogleAnrTimestampRepository = GoogleAnrTimestampRepository( + InternalEmbraceLogger() + ) +) : AnrModule diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt new file mode 100644 index 0000000000..8130bee9e2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.fakes.injection + +import android.app.Application +import android.content.Context +import io.embrace.android.embracesdk.Embrace.AppFramework +import io.embrace.android.embracesdk.fakes.FakeAndroidResourcesService +import io.embrace.android.embracesdk.injection.CoreModule +import io.embrace.android.embracesdk.injection.isDebug +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.registry.ServiceRegistry +import io.mockk.isMockKMock +import io.mockk.mockk +import org.robolectric.RuntimeEnvironment + +/** + * If used in a Robolectric test, [application] and [context] will be fakes supplied by the Robolectric framework + */ +internal class FakeCoreModule( + override val application: Application = + if (RuntimeEnvironment.getApplication() == null) mockk(relaxed = true) else RuntimeEnvironment.getApplication(), + override val context: Context = + if (isMockKMock(application)) mockk(relaxed = true) else application.applicationContext, + override val appFramework: AppFramework = AppFramework.NATIVE, + override val logger: InternalEmbraceLogger = InternalEmbraceLogger(), + override val serviceRegistry: ServiceRegistry = ServiceRegistry(), + override val jsonSerializer: EmbraceSerializer = EmbraceSerializer(), + override val resources: FakeAndroidResourcesService = FakeAndroidResourcesService(), + override val isDebug: Boolean = + if (isMockKMock(context)) false else context.applicationInfo.isDebug() +) : CoreModule diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCrashModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCrashModule.kt new file mode 100644 index 0000000000..840621dd52 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCrashModule.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.fakes.FakeCrashService +import io.embrace.android.embracesdk.injection.CrashModule +import io.embrace.android.embracesdk.internal.crash.CrashFileMarker +import io.embrace.android.embracesdk.internal.crash.LastRunCrashVerifier +import io.embrace.android.embracesdk.samples.AutomaticVerificationExceptionHandler +import io.mockk.mockk + +internal class FakeCrashModule : CrashModule { + override val lastRunCrashVerifier = LastRunCrashVerifier(CrashFileMarker(mockk(relaxed = true))) + override val crashService = FakeCrashService() + override val automaticVerificationExceptionHandler = AutomaticVerificationExceptionHandler(null) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCustomerLogModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCustomerLogModule.kt new file mode 100644 index 0000000000..b8e98f89c6 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCustomerLogModule.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk.fakes.injection + +import com.google.common.util.concurrent.MoreExecutors +import io.embrace.android.embracesdk.FakeDeliveryService +import io.embrace.android.embracesdk.capture.connectivity.NoOpNetworkConnectivityService +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeGatingService +import io.embrace.android.embracesdk.fakes.FakeNetworkLoggingService +import io.embrace.android.embracesdk.fakes.FakeUserService +import io.embrace.android.embracesdk.fakes.fakeEmbraceSessionProperties +import io.embrace.android.embracesdk.injection.CustomerLogModule +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.network.logging.NetworkCaptureService +import io.embrace.android.embracesdk.network.logging.NetworkLoggingService + +internal class FakeCustomerLogModule( + override val networkLoggingService: NetworkLoggingService = FakeNetworkLoggingService(), + + override val remoteLogger: EmbraceRemoteLogger = EmbraceRemoteLogger( + FakeAndroidMetadataService(), + FakeDeliveryService(), + FakeUserService(), + FakeConfigService(), + fakeEmbraceSessionProperties(), + InternalEmbraceLogger(), + FakeClock(), + MoreExecutors.newDirectExecutorService(), + FakeGatingService(), + NoOpNetworkConnectivityService() + ) +) : CustomerLogModule { + + override val networkCaptureService: NetworkCaptureService + get() = TODO("Not yet implemented") +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataCaptureServiceModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataCaptureServiceModule.kt new file mode 100644 index 0000000000..1537fb2604 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataCaptureServiceModule.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.FakeBreadcrumbService +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.connectivity.NoOpNetworkConnectivityService +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbService +import io.embrace.android.embracesdk.capture.crumbs.PushNotificationCaptureService +import io.embrace.android.embracesdk.capture.crumbs.activity.EmbraceActivityLifecycleBreadcrumbService +import io.embrace.android.embracesdk.capture.memory.MemoryService +import io.embrace.android.embracesdk.capture.powersave.NoOpPowerSaveModeService +import io.embrace.android.embracesdk.capture.powersave.PowerSaveModeService +import io.embrace.android.embracesdk.capture.strictmode.NoOpStrictModeService +import io.embrace.android.embracesdk.capture.strictmode.StrictModeService +import io.embrace.android.embracesdk.capture.thermalstate.NoOpThermalStatusService +import io.embrace.android.embracesdk.capture.thermalstate.ThermalStatusService +import io.embrace.android.embracesdk.capture.webview.EmbraceWebViewService +import io.embrace.android.embracesdk.capture.webview.WebViewService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeMemoryService +import io.embrace.android.embracesdk.injection.DataCaptureServiceModule +import io.embrace.android.embracesdk.internal.EmbraceSerializer + +internal class FakeDataCaptureServiceModule( + override val networkConnectivityService: NetworkConnectivityService = NoOpNetworkConnectivityService(), + override val strictModeService: StrictModeService = NoOpStrictModeService(), + override val thermalStatusService: ThermalStatusService = NoOpThermalStatusService(), + override val activityLifecycleBreadcrumbService: EmbraceActivityLifecycleBreadcrumbService? = null, + override val powerSaveModeService: PowerSaveModeService = NoOpPowerSaveModeService(), + override val memoryService: MemoryService = FakeMemoryService(), + override val breadcrumbService: BreadcrumbService = FakeBreadcrumbService(), + override val webviewService: WebViewService = EmbraceWebViewService(FakeConfigService(), EmbraceSerializer()) +) : DataCaptureServiceModule { + + override val pushNotificationService: PushNotificationCaptureService + get() = TODO("Not yet implemented") +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataContainerModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataContainerModule.kt new file mode 100644 index 0000000000..2bd1e8549f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDataContainerModule.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.aei.ApplicationExitInfoService +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.fakes.FakeApplicationExitInfoService +import io.embrace.android.embracesdk.fakes.FakeEventService +import io.embrace.android.embracesdk.fakes.FakePerformanceInfoService +import io.embrace.android.embracesdk.injection.DataContainerModule + +internal class FakeDataContainerModule( + override val applicationExitInfoService: ApplicationExitInfoService = FakeApplicationExitInfoService(), + override val eventService: EventService = FakeEventService(), + override val performanceInfoService: PerformanceInfoService = FakePerformanceInfoService() +) : DataContainerModule diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt new file mode 100644 index 0000000000..ab70160152 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt @@ -0,0 +1,29 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.FakeDeliveryService +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.injection.CoreModule +import io.embrace.android.embracesdk.injection.DataCaptureServiceModule +import io.embrace.android.embracesdk.injection.DeliveryModule +import io.embrace.android.embracesdk.injection.DeliveryModuleImpl +import io.embrace.android.embracesdk.injection.EssentialServiceModule +import io.embrace.android.embracesdk.injection.InitModule +import io.embrace.android.embracesdk.injection.InitModuleImpl +import io.embrace.android.embracesdk.worker.WorkerThreadModule + +internal class FakeDeliveryModule( + initModule: InitModule = InitModuleImpl(), + coreModule: CoreModule = FakeCoreModule(), + essentialServiceModule: EssentialServiceModule = FakeEssentialServiceModule(), + dataCaptureServiceModule: DataCaptureServiceModule = FakeDataCaptureServiceModule(), + workerThreadModule: WorkerThreadModule = FakeWorkerThreadModule(), + deliveryServiceImpl: DeliveryModule = DeliveryModuleImpl( + initModule = initModule, + coreModule = coreModule, + essentialServiceModule = essentialServiceModule, + dataCaptureServiceModule = dataCaptureServiceModule, + workerThreadModule = workerThreadModule + ) +) : DeliveryModule by deliveryServiceImpl { + override val deliveryService: FakeDeliveryService = FakeDeliveryService() +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeEssentialServiceModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeEssentialServiceModule.kt new file mode 100644 index 0000000000..dd1f0f7849 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeEssentialServiceModule.kt @@ -0,0 +1,56 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.capture.cpu.CpuInfoDelegate +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.orientation.NoOpOrientationService +import io.embrace.android.embracesdk.capture.orientation.OrientationService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.comms.api.ApiClient +import io.embrace.android.embracesdk.comms.api.ApiResponseCache +import io.embrace.android.embracesdk.comms.api.ApiService +import io.embrace.android.embracesdk.comms.api.ApiUrlBuilder +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeApiService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeCpuInfoDelegate +import io.embrace.android.embracesdk.fakes.FakeDeviceArchitecture +import io.embrace.android.embracesdk.fakes.FakeGatingService +import io.embrace.android.embracesdk.fakes.FakeMemoryCleanerService +import io.embrace.android.embracesdk.fakes.FakeUserService +import io.embrace.android.embracesdk.gating.GatingService +import io.embrace.android.embracesdk.injection.EssentialServiceModule +import io.embrace.android.embracesdk.internal.DeviceArchitecture +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.session.MemoryCleanerService + +internal class FakeEssentialServiceModule( + override val activityService: ActivityService = FakeActivityService(), + override val metadataService: MetadataService = FakeAndroidMetadataService(), + override val configService: ConfigService = FakeConfigService(), + override val memoryCleanerService: MemoryCleanerService = FakeMemoryCleanerService(), + override val gatingService: GatingService = FakeGatingService(), + override val orientationService: OrientationService = NoOpOrientationService(), + override val urlBuilder: ApiUrlBuilder = ApiUrlBuilder( + configService = configService, + metadataService = metadataService, + enableIntegrationTesting = true, + isDebug = false + ), + override val apiClient: ApiClient = ApiClient( + InternalEmbraceLogger() + ), + override val userService: UserService = FakeUserService(), + override val sharedObjectLoader: SharedObjectLoader = SharedObjectLoader(), + override val deviceArchitecture: DeviceArchitecture = FakeDeviceArchitecture(), + override val apiService: ApiService = FakeApiService() +) : EssentialServiceModule { + + override val cache: ApiResponseCache + get() = throw UnsupportedOperationException() + + override val cpuInfoDelegate: CpuInfoDelegate = FakeCpuInfoDelegate() +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt new file mode 100644 index 0000000000..e3d444069c --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt @@ -0,0 +1,14 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.clock.NormalizedIntervalClock +import io.embrace.android.embracesdk.clock.SystemClock +import io.embrace.android.embracesdk.fakes.FakeOpenTelemetryClock +import io.embrace.android.embracesdk.injection.InitModule +import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService +import io.embrace.android.embracesdk.internal.spans.SpansService + +internal class FakeInitModule( + override val clock: Clock = NormalizedIntervalClock(systemClock = SystemClock()), + override val spansService: SpansService = EmbraceSpansService(clock = FakeOpenTelemetryClock(embraceClock = clock)) +) : InitModule diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeNativeModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeNativeModule.kt new file mode 100644 index 0000000000..7b61a891c0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeNativeModule.kt @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.FakeNdkService +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerInstaller +import io.embrace.android.embracesdk.anr.ndk.NativeThreadSamplerService +import io.embrace.android.embracesdk.ndk.NativeModule +import io.embrace.android.embracesdk.ndk.NdkService + +internal class FakeNativeModule( + override val nativeThreadSamplerService: NativeThreadSamplerService? = null, + override val nativeThreadSamplerInstaller: NativeThreadSamplerInstaller? = null, + override val ndkService: NdkService = FakeNdkService() +) : NativeModule diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSdkObservabilityModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSdkObservabilityModule.kt new file mode 100644 index 0000000000..70b28f9775 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSdkObservabilityModule.kt @@ -0,0 +1,19 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.injection.SdkObservabilityModule +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.logging.InternalErrorLogger + +internal class FakeSdkObservabilityModule( + override val exceptionService: EmbraceInternalErrorService = EmbraceInternalErrorService( + FakeActivityService(), + FakeClock(), + true + ) +) : SdkObservabilityModule { + + override val internalErrorLogger: InternalErrorLogger + get() = TODO("Not yet implemented") +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSessionModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSessionModule.kt new file mode 100644 index 0000000000..028d1e0804 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSessionModule.kt @@ -0,0 +1,15 @@ +package io.embrace.android.embracesdk.fakes.injection + +import io.embrace.android.embracesdk.FakeSessionService +import io.embrace.android.embracesdk.injection.SessionModule +import io.embrace.android.embracesdk.session.BackgroundActivityService +import io.embrace.android.embracesdk.session.SessionHandler +import io.embrace.android.embracesdk.session.SessionService + +internal class FakeSessionModule( + override val backgroundActivityService: BackgroundActivityService? = null, + override val sessionService: SessionService = FakeSessionService() +) : SessionModule { + override val sessionHandler: SessionHandler + get() = TODO("Not yet implemented") +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSystemServiceModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSystemServiceModule.kt new file mode 100644 index 0000000000..2802920187 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeSystemServiceModule.kt @@ -0,0 +1,16 @@ +package io.embrace.android.embracesdk.fakes.injection + +import android.app.ActivityManager +import android.app.usage.StorageStatsManager +import android.net.ConnectivityManager +import android.os.PowerManager +import android.view.WindowManager +import io.embrace.android.embracesdk.injection.SystemServiceModule + +internal class FakeSystemServiceModule( + override val activityManager: ActivityManager? = null, + override val powerManager: PowerManager? = null, + override val connectivityManager: ConnectivityManager? = null, + override val storageManager: StorageStatsManager? = null, + override val windowManager: WindowManager? = null +) : SystemServiceModule diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fixtures/SpansTestFixtures.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fixtures/SpansTestFixtures.kt new file mode 100644 index 0000000000..c928c52ddc --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fixtures/SpansTestFixtures.kt @@ -0,0 +1,65 @@ +package io.embrace.android.embracesdk.fixtures + +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanImpl +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.opentelemetry.api.trace.SpanId +import io.opentelemetry.api.trace.StatusCode + +internal val testSpan = EmbraceSpanData( + traceId = "19bb482ec1c7e6b2f10fb89e0ccc85fa", + spanId = "342eb9c7f8cb54ff", + parentSpanId = SpanId.getInvalid(), + name = "emb-sdk-init", + startTimeNanos = 1681972471806000000L, + endTimeNanos = 1681972471871000000L, + status = StatusCode.OK, + events = listOf( + checkNotNull( + EmbraceSpanEvent.create( + name = "start-time", + timestampNanos = 1681972471807000000L, + attributes = mapOf(Pair("test1", "value1"), Pair("test2", "value2")) + ) + ), + checkNotNull( + EmbraceSpanEvent.create( + name = "end-span-event", + timestampNanos = 1681972471871000000L, + attributes = null + ) + ) + ), + attributes = mapOf(Pair("emb.sequence_id", "3"), Pair("emb.type", "PERFORMANCE")) +) + +private fun createMapOfSize(size: Int): Map { + val mutableMap = mutableMapOf() + repeat(size) { + mutableMap[it.toString()] = "value" + } + return mutableMap +} +private fun createEventsListOfSize(size: Int): List { + val events = mutableListOf() + repeat(size) { + events.add(checkNotNull(EmbraceSpanEvent.create("name $it", 1L, emptyMap()))) + } + return events +} + +internal val MAX_LENGTH_SPAN_NAME = "s".repeat(EmbraceSpanImpl.MAX_NAME_LENGTH) +internal val TOO_LONG_SPAN_NAME = "s".repeat(EmbraceSpanImpl.MAX_NAME_LENGTH + 1) +internal val MAX_LENGTH_EVENT_NAME = "s".repeat(EmbraceSpanEvent.MAX_EVENT_NAME_LENGTH) +internal val TOO_LONG_EVENT_NAME = "s".repeat(EmbraceSpanEvent.MAX_EVENT_NAME_LENGTH + 1) +internal val MAX_LENGTH_ATTRIBUTE_KEY = "s".repeat(EmbraceSpanImpl.MAX_ATTRIBUTE_KEY_LENGTH) +internal val TOO_LONG_ATTRIBUTE_KEY = "s".repeat(EmbraceSpanImpl.MAX_ATTRIBUTE_KEY_LENGTH + 1) +internal val MAX_LENGTH_ATTRIBUTE_VALUE = "s".repeat(EmbraceSpanImpl.MAX_ATTRIBUTE_VALUE_LENGTH) +internal val TOO_LONG_ATTRIBUTE_VALUE = "s".repeat(EmbraceSpanImpl.MAX_ATTRIBUTE_VALUE_LENGTH + 1) + +internal val maxSizeAttributes = createMapOfSize(EmbraceSpanImpl.MAX_ATTRIBUTE_COUNT) +internal val tooBigAttributes = createMapOfSize(EmbraceSpanImpl.MAX_ATTRIBUTE_COUNT + 1) +internal val maxSizeEventAttributes = createMapOfSize(EmbraceSpanEvent.MAX_EVENT_ATTRIBUTE_COUNT) +internal val tooBigEventAttributes = createMapOfSize(EmbraceSpanEvent.MAX_EVENT_ATTRIBUTE_COUNT + 1) +internal val maxSizeEvents = createEventsListOfSize(EmbraceSpanImpl.MAX_EVENT_COUNT) +internal val tooBigEvents = createEventsListOfSize(EmbraceSpanImpl.MAX_EVENT_COUNT + 1) diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AndroidServicesModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AndroidServicesModuleImplTest.kt new file mode 100644 index 0000000000..355a480d78 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AndroidServicesModuleImplTest.kt @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.prefs.EmbracePreferencesService +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class AndroidServicesModuleImplTest { + + @Test + fun testDefault() { + val module = AndroidServicesModuleImpl( + initModule = InitModuleImpl(), + coreModule = FakeCoreModule(), + workerThreadModule = FakeWorkerThreadModule() + ) + + assertTrue(module.preferencesService is EmbracePreferencesService) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AnrModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AnrModuleImplTest.kt new file mode 100644 index 0000000000..928574527b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/AnrModuleImplTest.kt @@ -0,0 +1,67 @@ +package io.embrace.android.embracesdk.injection + +import android.os.Looper +import io.embrace.android.embracesdk.anr.NoOpAnrService +import io.embrace.android.embracesdk.config.local.AutomaticDataCaptureLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeSystemServiceModule +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class AnrModuleImplTest { + + @Before + fun setUp() { + mockkStatic(Looper::class) + every { Looper.getMainLooper() } returns mockk(relaxed = true) + } + + @Test + fun testDefaultImplementations() { + val module = AnrModuleImpl( + InitModuleImpl(), + FakeCoreModule(), + FakeSystemServiceModule(), + FakeEssentialServiceModule() + ) + assertNotNull(module.anrService) + assertNotNull(module.googleAnrTimestampRepository) + } + + @Test + fun testBehaviorDisabled() { + val module = AnrModuleImpl( + InitModuleImpl(), + FakeCoreModule(), + FakeSystemServiceModule(), + FakeEssentialServiceModule( + configService = createConfigServiceWithAnrDisabled() + ) + ) + assertTrue(module.anrService is NoOpAnrService) + assertNotNull(module.googleAnrTimestampRepository) + } + + private fun createConfigServiceWithAnrDisabled() = FakeConfigService( + autoDataCaptureBehavior = fakeAutoDataCaptureBehavior(localCfg = { + LocalConfig( + "", false, + SdkLocalConfig( + automaticDataCaptureConfig = AutomaticDataCaptureLocalConfig( + anrServiceEnabled = false + ) + ) + ) + }) + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CoreModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CoreModuleImplTest.kt new file mode 100644 index 0000000000..5d23a47ee0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CoreModuleImplTest.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.injection + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RuntimeEnvironment + +@RunWith(AndroidJUnit4::class) +internal class CoreModuleImplTest { + + @Test + fun testApplicationObject() { + val ctx = RuntimeEnvironment.getApplication().applicationContext + val module = CoreModuleImpl(ctx, Embrace.AppFramework.NATIVE) + assertSame(ctx, module.context) + assertSame(ctx, module.application) + assertSame(InternalStaticEmbraceLogger.logger, module.logger) + assertNotNull(module.serviceRegistry) + assertNotNull(module.jsonSerializer) + } + + @Test + fun testContextObject() { + val application = RuntimeEnvironment.getApplication() + val isDebug = application.applicationInfo.isDebug() + val ctx = application.applicationContext + val module = CoreModuleImpl(ctx, Embrace.AppFramework.NATIVE) + assertSame(application, module.application) + assertEquals(isDebug, module.isDebug) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CrashModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CrashModuleImplTest.kt new file mode 100644 index 0000000000..8a3a65e4c2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CrashModuleImplTest.kt @@ -0,0 +1,31 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.fakes.injection.FakeAnrModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeDataContainerModule +import io.embrace.android.embracesdk.fakes.injection.FakeDeliveryModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeNativeModule +import io.embrace.android.embracesdk.fakes.injection.FakeSessionModule +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class CrashModuleImplTest { + + @Test + fun testDefaultImplementations() { + val module = CrashModuleImpl( + InitModuleImpl(), + FakeEssentialServiceModule(), + FakeDeliveryModule(), + FakeNativeModule(), + FakeSessionModule(), + FakeAnrModule(), + FakeDataContainerModule(), + FakeCoreModule() + ) + assertNotNull(module.lastRunCrashVerifier) + assertNotNull(module.crashService) + assertNotNull(module.automaticVerificationExceptionHandler) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CustomerLogModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CustomerLogModuleImplTest.kt new file mode 100644 index 0000000000..1b40f3bd3a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/CustomerLogModuleImplTest.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.fakes.fakeEmbraceSessionProperties +import io.embrace.android.embracesdk.fakes.injection.FakeAndroidServicesModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeDataCaptureServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeDeliveryModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class CustomerLogModuleImplTest { + + @Test + fun testDefaultImplementations() { + val module = CustomerLogModuleImpl( + InitModuleImpl(), + FakeCoreModule(), + FakeAndroidServicesModule(), + FakeEssentialServiceModule(), + FakeDeliveryModule(), + fakeEmbraceSessionProperties(), + FakeDataCaptureServiceModule(), + FakeWorkerThreadModule() + ) + + assertNotNull(module.networkCaptureService) + assertNotNull(module.networkLoggingService) + assertNotNull(module.remoteLogger) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModuleImplTest.kt new file mode 100644 index 0000000000..fc57707b97 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataCaptureServiceModuleImplTest.kt @@ -0,0 +1,133 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.capture.connectivity.EmbraceNetworkConnectivityService +import io.embrace.android.embracesdk.capture.connectivity.NoOpNetworkConnectivityService +import io.embrace.android.embracesdk.capture.crumbs.EmbraceBreadcrumbService +import io.embrace.android.embracesdk.capture.crumbs.activity.EmbraceActivityLifecycleBreadcrumbService +import io.embrace.android.embracesdk.capture.memory.EmbraceMemoryService +import io.embrace.android.embracesdk.capture.memory.NoOpMemoryService +import io.embrace.android.embracesdk.capture.powersave.EmbracePowerSaveModeService +import io.embrace.android.embracesdk.capture.powersave.NoOpPowerSaveModeService +import io.embrace.android.embracesdk.capture.strictmode.EmbraceStrictModeService +import io.embrace.android.embracesdk.capture.strictmode.NoOpStrictModeService +import io.embrace.android.embracesdk.capture.thermalstate.EmbraceThermalStatusService +import io.embrace.android.embracesdk.capture.thermalstate.NoOpThermalStatusService +import io.embrace.android.embracesdk.capture.webview.EmbraceWebViewService +import io.embrace.android.embracesdk.config.local.AutomaticDataCaptureLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.AnrRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeVersionChecker +import io.embrace.android.embracesdk.fakes.fakeAnrBehavior +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import io.embrace.android.embracesdk.fakes.fakeSdkModeBehavior +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeSystemServiceModule +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class DataCaptureServiceModuleImplTest { + + private val coreModule = FakeCoreModule() + private val systemServiceModule = FakeSystemServiceModule() + + @Test + fun testDefaultImplementations() { + val module = DataCaptureServiceModuleImpl( + InitModuleImpl(), + coreModule, + systemServiceModule, + createEnabledBehavior(), + FakeWorkerThreadModule(), + FakeVersionChecker(true) + ) + + assertTrue(module.memoryService is EmbraceMemoryService) + assertTrue(module.powerSaveModeService is EmbracePowerSaveModeService) + assertTrue(module.webviewService is EmbraceWebViewService) + assertTrue(module.breadcrumbService is EmbraceBreadcrumbService) + assertTrue(module.networkConnectivityService is EmbraceNetworkConnectivityService) + assertTrue(module.strictModeService is EmbraceStrictModeService) + assertTrue(module.thermalStatusService is EmbraceThermalStatusService) + assertTrue(module.activityLifecycleBreadcrumbService is EmbraceActivityLifecycleBreadcrumbService) + assertNotNull(module.pushNotificationService) + } + + @Test + fun testOldVersionChecks() { + val module = DataCaptureServiceModuleImpl( + InitModuleImpl(), + coreModule, + systemServiceModule, + FakeEssentialServiceModule(), + FakeWorkerThreadModule(), + FakeVersionChecker(false) + ) + + assertTrue(module.powerSaveModeService is NoOpPowerSaveModeService) + assertTrue(module.thermalStatusService is NoOpThermalStatusService) + assertNull(module.activityLifecycleBreadcrumbService) + } + + @Test + fun testDisabledImplementations() { + val module = DataCaptureServiceModuleImpl( + InitModuleImpl(), + coreModule, + systemServiceModule, + createDisabledBehavior(), + FakeWorkerThreadModule(), + FakeVersionChecker(true) + ) + + assertTrue(module.memoryService is NoOpMemoryService) + assertTrue(module.powerSaveModeService is NoOpPowerSaveModeService) + assertTrue(module.networkConnectivityService is NoOpNetworkConnectivityService) + assertTrue(module.strictModeService is NoOpStrictModeService) + assertTrue(module.thermalStatusService is NoOpThermalStatusService) + assertNull(module.activityLifecycleBreadcrumbService) + } + + private fun createEnabledBehavior(): FakeEssentialServiceModule { + return FakeEssentialServiceModule( + configService = FakeConfigService( + anrBehavior = fakeAnrBehavior { AnrRemoteConfig(pctStrictModeListenerEnabled = 100f) }, + sdkModeBehavior = fakeSdkModeBehavior( + isDebug = true + ) + ) + ) + } + + private fun createDisabledBehavior(): FakeEssentialServiceModule { + val cfg = AutomaticDataCaptureLocalConfig( + memoryServiceEnabled = false, + powerSaveModeServiceEnabled = false, + networkConnectivityServiceEnabled = false, + anrServiceEnabled = false + ) + val behavior = fakeAutoDataCaptureBehavior(localCfg = { + LocalConfig( + "", + true, + SdkLocalConfig( + automaticDataCaptureConfig = cfg + ) + ) + }) + return FakeEssentialServiceModule( + configService = FakeConfigService( + autoDataCaptureBehavior = behavior, + sdkModeBehavior = fakeSdkModeBehavior( + remoteCfg = { RemoteConfig(pctBetaFeaturesEnabled = 0.0f) } + ), + ) + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataContainerModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataContainerModuleImplTest.kt new file mode 100644 index 0000000000..53d3927eb9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DataContainerModuleImplTest.kt @@ -0,0 +1,39 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.fakes.fakeEmbraceSessionProperties +import io.embrace.android.embracesdk.fakes.injection.FakeAndroidServicesModule +import io.embrace.android.embracesdk.fakes.injection.FakeAnrModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeCustomerLogModule +import io.embrace.android.embracesdk.fakes.injection.FakeDataCaptureServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeDeliveryModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeNativeModule +import io.embrace.android.embracesdk.fakes.injection.FakeSystemServiceModule +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class DataContainerModuleImplTest { + + @Test + fun testDefaultImplementations() { + val module = DataContainerModuleImpl( + InitModuleImpl(), + FakeCoreModule(), + FakeWorkerThreadModule(), + FakeSystemServiceModule(), + FakeAndroidServicesModule(), + FakeEssentialServiceModule(), + FakeDataCaptureServiceModule(), + FakeAnrModule(), + FakeCustomerLogModule(), + FakeDeliveryModule(), + FakeNativeModule(), + fakeEmbraceSessionProperties(), + 0 + ) + assertNotNull(module.eventService) + assertNotNull(module.performanceInfoService) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DeliveryModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DeliveryModuleImplTest.kt new file mode 100644 index 0000000000..40185d2f3c --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DeliveryModuleImplTest.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeDataCaptureServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class DeliveryModuleImplTest { + + @Test + fun testDefaultImplementations() { + val module = DeliveryModuleImpl( + InitModuleImpl(), + FakeCoreModule(), + FakeEssentialServiceModule(), + FakeDataCaptureServiceModule(), + FakeWorkerThreadModule() + ) + + assertNotNull(module.deliveryService) + assertNotNull(module.deliveryCacheManager) + assertNotNull(module.deliveryNetworkManager) + assertNotNull(module.cacheService) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DependencyInjectionKtTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DependencyInjectionKtTest.kt new file mode 100644 index 0000000000..13ad1c0ceb --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/DependencyInjectionKtTest.kt @@ -0,0 +1,34 @@ +package io.embrace.android.embracesdk.injection + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger + +internal class DependencyInjectionKtTest { + + private val factoryCounter = AtomicInteger(0) + private val eagerSingletonCounter = AtomicInteger(0) + private val lazySingletonCounter = AtomicInteger(0) + + private val factory by factory { factoryCounter.incrementAndGet() } + private val eagerSingleton by singleton(LoadType.EAGER) { eagerSingletonCounter.incrementAndGet() } + private val lazySingleton by singleton(LoadType.LAZY) { lazySingletonCounter.incrementAndGet() } + + @Test + fun testInjection() { + // assert defaults + assertEquals(0, factoryCounter.get()) + assertEquals(1, eagerSingletonCounter.get()) + assertEquals(0, lazySingletonCounter.get()) + + // get values from properties + assertEquals(1, factory) + assertEquals(1, eagerSingleton) + assertEquals(1, lazySingleton) + + // get values again from properties + assertEquals(2, factory) + assertEquals(1, eagerSingleton) + assertEquals(1, lazySingleton) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/InitModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/InitModuleImplTest.kt new file mode 100644 index 0000000000..ace8f97296 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/InitModuleImplTest.kt @@ -0,0 +1,34 @@ +package io.embrace.android.embracesdk.injection + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.clock.NormalizedIntervalClock +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService +import io.embrace.android.embracesdk.internal.spans.SpansService +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class InitModuleImplTest { + + @Test + fun testInitModuleImplDefaults() { + val initModule = InitModuleImpl() + assertTrue(initModule.clock is NormalizedIntervalClock) + assertTrue(initModule.spansService is EmbraceSpansService) + } + + @Test + fun testInitModuleImplOverrideComponents() { + val clock = FakeClock() + val spansService = SpansService.featureDisabledSpansService + val initModule = InitModuleImpl( + clock = clock, + spansService = spansService + ) + assertSame(clock, initModule.clock) + assertSame(spansService, initModule.spansService) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SdkObservabilityModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SdkObservabilityModuleImplTest.kt new file mode 100644 index 0000000000..9af8256fe9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SdkObservabilityModuleImplTest.kt @@ -0,0 +1,18 @@ +package io.embrace.android.embracesdk.injection + +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class SdkObservabilityModuleImplTest { + + @Test + fun testDefaultImplementations() { + val module = SdkObservabilityModuleImpl( + InitModuleImpl(), + FakeEssentialServiceModule() + ) + assertNotNull(module.exceptionService) + assertNotNull(module.internalErrorLogger) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SystemServiceModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SystemServiceModuleImplTest.kt new file mode 100644 index 0000000000..b6a592ba66 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/injection/SystemServiceModuleImplTest.kt @@ -0,0 +1,65 @@ +package io.embrace.android.embracesdk.injection + +import android.app.ActivityManager +import android.app.usage.StorageStatsManager +import android.content.Context +import android.net.ConnectivityManager +import android.os.PowerManager +import android.view.WindowManager +import io.embrace.android.embracesdk.fakes.FakeVersionChecker +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +internal class SystemServiceModuleImplTest { + + @Test + fun testVersionChecks() { + val ctx = mockk(relaxed = true) { + every { getSystemService("storagestats") } returns mockk() + } + val old = SystemServiceModuleImpl( + FakeCoreModule(context = ctx), + FakeVersionChecker(false) + ) + assertNull(old.storageManager) + + val new = SystemServiceModuleImpl( + FakeCoreModule(context = ctx), + FakeVersionChecker(true) + ) + assertNotNull(new.storageManager) + } + + @Test + fun testSystemServiceModuleDefault() { + val ctx = mockk(relaxed = true) { + every { getSystemService("activity") } returns mockk() + every { getSystemService("power") } returns mockk() + every { getSystemService("window") } returns mockk() + every { getSystemService("connectivity") } returns mockk() + } + val module = SystemServiceModuleImpl(FakeCoreModule(context = ctx)) + + assertNotNull(module.activityManager) + assertNotNull(module.powerManager) + assertNotNull(module.windowManager) + assertNotNull(module.connectivityManager) + assertNull(module.storageManager) + } + + @Test + fun testSystemServiceModuleException() { + val ctx = mockk() + val module = SystemServiceModuleImpl(FakeCoreModule(context = ctx)) + + assertNull(module.activityManager) + assertNull(module.powerManager) + assertNull(module.storageManager) + assertNull(module.windowManager) + assertNull(module.connectivityManager) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactoryTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactoryTest.kt new file mode 100644 index 0000000000..e7d5a3f3d8 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ConstantNameThreadFactoryTest.kt @@ -0,0 +1,61 @@ +package io.embrace.android.embracesdk.internal + +import io.embrace.android.embracesdk.concurrency.SingleThreadTestScheduledExecutor +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +internal class ConstantNameThreadFactoryTest { + private lateinit var executor: SingleThreadTestScheduledExecutor + + @Test + fun `verify constant default thread name`() { + executor = SingleThreadTestScheduledExecutor(ConstantNameThreadFactory()) + verifyThreadNames("emb-thread") + } + + @Test + fun `verify constant thread name with custom token`() { + executor = SingleThreadTestScheduledExecutor(ConstantNameThreadFactory(namePrefix = "blob")) + verifyThreadNames("emb-blob") + } + + @Test + fun `verify thread name with unique instance token only`() { + val threadFactory = ConstantNameThreadFactory(uniquePerInstance = true) + val token = threadFactory.hashCode() + executor = SingleThreadTestScheduledExecutor(threadFactory) + verifyThreadNames("emb-thread-$token") + } + + @Test + fun `verify constant thread name with unique instance token`() { + val threadFactory = ConstantNameThreadFactory(namePrefix = "plop", uniquePerInstance = true) + val token = threadFactory.hashCode() + executor = SingleThreadTestScheduledExecutor(threadFactory) + verifyThreadNames("emb-plop-$token") + } + + private fun verifyThreadNames(expectedName: String) { + val countDownLatch = CountDownLatch(1) + executor.setKeepAliveTime(1L, TimeUnit.MILLISECONDS) + executor.allowCoreThreadTimeOut(true) + val firstThread = AtomicReference() + val secondThread = AtomicReference() + executor.submit { firstThread.set(Thread.currentThread()) }.get(1L, TimeUnit.SECONDS) + assertNotNull(firstThread.get()) + assertEquals(expectedName, firstThread.get().name) + + // Wait long enough for the existing thread in the executor to time out. A better way to do this would be nice... + countDownLatch.await(100L, TimeUnit.MILLISECONDS) + executor.submit { secondThread.set(Thread.currentThread()) }.get(1L, TimeUnit.SECONDS) + assertNotNull(secondThread.get()) + assertEquals(firstThread.get().name, secondThread.get().name) + assertNotEquals(firstThread.get().id, secondThread.get().id) + assertNotEquals(firstThread.get(), secondThread.get()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/EmbraceSerializerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/EmbraceSerializerTest.kt new file mode 100644 index 0000000000..a9244332c0 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/EmbraceSerializerTest.kt @@ -0,0 +1,36 @@ +package io.embrace.android.embracesdk.internal + +import com.google.gson.Gson +import com.google.gson.stream.JsonReader +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.payload.Session +import io.embrace.android.embracesdk.payload.SessionMessage +import io.mockk.mockk +import org.junit.Assert +import org.junit.Test +import java.io.StringReader + +internal class EmbraceSerializerTest { + private val serializer = EmbraceSerializer() + private val session: Session = fakeSession() + private val payload: SessionMessage = SessionMessage(session) + + @Test + fun testWriteToFile() { + val result = serializer.writeToFile(payload, SessionMessage::class.java, mockk(relaxed = true)) + Assert.assertTrue(result) + } + + @Test + fun testLoadObject() { + val reader = JsonReader(StringReader(Gson().toJson(payload))) + val result = serializer.loadObject(reader, SessionMessage::class.java) + Assert.assertEquals("fakeSessionId", result?.session?.sessionId) + } + + @Test + fun testBytesFromPayload() { + val result = serializer.bytesFromPayload(payload, SessionMessage::class.java) + Assert.assertNotNull(result) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/OpenTelemetryClockTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/OpenTelemetryClockTest.kt new file mode 100644 index 0000000000..46abd946c2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/OpenTelemetryClockTest.kt @@ -0,0 +1,37 @@ +package io.embrace.android.embracesdk.internal + +import android.os.Build.VERSION_CODES.TIRAMISU +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.clock.NormalizedIntervalClock +import io.embrace.android.embracesdk.clock.SystemClock +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +internal class OpenTelemetryClockTest { + + private lateinit var embraceClock: Clock + private lateinit var openTelemetryClock: OpenTelemetryClock + + @Before + fun setup() { + embraceClock = NormalizedIntervalClock(systemClock = SystemClock()) + openTelemetryClock = OpenTelemetryClock(embraceClock = embraceClock) + } + + @Config(sdk = [TIRAMISU]) + @Test + fun `verify consistency in 33`() { + verifyConsistency() + } + + private fun verifyConsistency() { + assertTrue(embraceClock.now() <= TimeUnit.NANOSECONDS.toMillis(openTelemetryClock.now())) + assertTrue(openTelemetryClock.nanoTime() <= openTelemetryClock.nanoTime()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheckTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheckTest.kt new file mode 100644 index 0000000000..94ac757ca4 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/ThreadEnforcementCheckTest.kt @@ -0,0 +1,91 @@ +package io.embrace.android.embracesdk.internal + +import io.embrace.android.embracesdk.BuildConfig +import io.embrace.android.embracesdk.concurrency.SingleThreadTestScheduledExecutor +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.lang.Thread.currentThread +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +internal class ThreadEnforcementCheckTest { + + private lateinit var executor: SingleThreadTestScheduledExecutor + + @Before + fun setup() { + executor = SingleThreadTestScheduledExecutor() + } + + @Test + fun testCorrectThread() { + enforceThread(AtomicReference(currentThread())) // no exception thrown + } + + @Test + fun `wrong thread throws exception in debug only`() { + if (BuildConfig.DEBUG) { + assertThrows(WrongThreadException::class.java) { + enforceThread(nonExecutingThread) + } + } else { + enforceThread(nonExecutingThread) + } + } + + @Test + fun `wrong thread in executor task will throw swallowed exception in debug`() { + val latch = CountDownLatch(1) + val future = executor.submit { + enforceThread(nonExecutingThread) + latch.countDown() + } + + var executionExceptionThrown = false + try { + future.get(1L, TimeUnit.SECONDS) + } catch (e: ExecutionException) { + assertTrue(e.cause is WrongThreadException) + executionExceptionThrown = true + } + + if (BuildConfig.DEBUG) { + assertTrue(executionExceptionThrown) + assertEquals(1, latch.count) + } else { + assertFalse(executionExceptionThrown) + assertEquals(0, latch.count) + } + } + + @Test + fun `different threads with the same name is not considered wrong`() { + executor.allowCoreThreadTimeOut(true) + executor.setKeepAliveTime(1L, TimeUnit.MILLISECONDS) + + val firstThread = AtomicReference() + val secondThread = AtomicReference() + executor.submit { firstThread.set(currentThread()) }.get(1L, TimeUnit.SECONDS) + assertNotNull(firstThread.get()) + + // Wait long enough for the existing thread in the executor to time out. A better way to do this would be nice... + Thread.sleep(100L) + executor.submit { + secondThread.set(currentThread()) + enforceThread(firstThread) + }.get(1L, TimeUnit.SECONDS) + assertNotEquals(firstThread.get(), secondThread.get()) + } + + companion object { + private val nonExecutingThread = AtomicReference(Thread()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/TraceparentGeneratorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/TraceparentGeneratorTest.kt new file mode 100644 index 0000000000..86805d662d --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/TraceparentGeneratorTest.kt @@ -0,0 +1,67 @@ +package io.embrace.android.embracesdk.internal + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.random.Random + +internal class TraceparentGeneratorTest { + + @Test + fun `check format conforms to expected standard`() { + // Can't exhaustively verify that the generated traceparents will always fit, so lets just do a bunch to verify the unlikeliness + val generator = TraceparentGenerator() + repeat(1000) { + assertTrue(validPattern.matches(generator.generate())) + } + } + + @Test + fun `check exact traceparent generated with Random with a known seed`() { + val knownGenerator = TraceparentGenerator(random = Random(1881)) + assertEquals("00-f3805483a79dec663e81467524fc2f7d-9001c43540253a1a-01", knownGenerator.generate()) + } + + @Test + fun `check random returning 0s won't generate invalid traceheader`() { + val zeroGenerator = TraceparentGenerator(random = TestRandom()) + assertNotEquals("00-00000000000000000000000000000000-0000000000000000-01", zeroGenerator.generate()) + } + + @Test + fun validateRegex() { + assertTrue(validPattern.matches("00-b583a45b2c7c813e0ebc6aa0835b9d98-b5475c618bb98e67-01")) + assertFalse(validPattern.matches("00-b583a45b2c7c813e0ebc6aa0835b9d98-b5475c618bb98e67-00")) + assertFalse(validPattern.matches("01-b583a45b2c7c813e0ebc6aa0835b9d98-b5475c618bb98e67-01")) + assertFalse(validPattern.matches("00-b583a45b2c7c813e0ebc6aa0835b9d98s-b5475c618bb98e67-01")) + assertFalse(validPattern.matches("00-b583a45b2c7c813e0ebc6aa0835b9d98-b5475c618bb98e67s-01")) + assertFalse(validPattern.matches("00-b583a45b2c7c813e0ebc6aa0835b9d98-5475c618bb98e67-01")) + assertFalse(validPattern.matches("00-b583a45b2c7c813e0ebc6aa0835b9d9-b5475c618bb98e67-01")) + assertFalse(validPattern.matches("00-g583a45b2c7c813e0ebc6aa0835b9d98-b5475c618bb98e67-01")) + assertFalse(validPattern.matches("00-b583a45b2c7c813e0ebc6aa0835b9d98-h5475c618bb98e67-01")) + } + + companion object { + val validPattern = Regex("^00-" + "[0-9a-fA-F]{32}" + "-" + "[0-9a-fA-F]{16}" + "-01$") + + /** + * [Random] that returns 0s for the first 10 invocations of [nextLong] before generating a random long based on the default impl + */ + class TestRandom : Random() { + private var zeroCount = 0 + + override fun nextLong(): Long { + return if (zeroCount < 10) { + zeroCount++ + 0 + } else { + super.nextLong() + } + } + + override fun nextBits(bitCount: Int): Int = Default.nextBits(bitCount) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarkerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarkerTest.kt new file mode 100644 index 0000000000..52105fac19 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/CrashFileMarkerTest.kt @@ -0,0 +1,121 @@ +package io.embrace.android.embracesdk.internal.crash + +import io.mockk.every +import io.mockk.spyk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +internal class CrashFileMarkerTest { + @get:Rule + val tempFolder = TemporaryFolder() + + /** + * Lazy reference to the file used by the class being tested + */ + private lateinit var markerLazyFile: Lazy + + /** + * Mock of the file used by the class being tested + */ + private lateinit var mockFile: File + + /** + * Reference to the real file used in the test + */ + private lateinit var testFile: File + + /** + * Class being tested + */ + private lateinit var crashMarker: CrashFileMarker + + @Before + fun setUp() { + testFile = File(tempFolder.root.path, CrashFileMarker.CRASH_MARKER_FILE_NAME) + mockFile = spyk(testFile) + markerLazyFile = lazy { mockFile } + crashMarker = CrashFileMarker(markerLazyFile) + } + + @After + fun tearDown() { + if (testFile.exists()) { + testFile.delete() + } + unmockkAll() + } + + @Test + fun `test calling mark() creates the file`() { + assertEquals(false, testFile.exists()) + crashMarker.mark() + assertEquals(true, testFile.exists()) + } + + @Test + fun `test creating a marker twice rewrites the file without throwing an exception`() { + assertEquals(false, testFile.exists()) + crashMarker.mark() + crashMarker.mark() + assertEquals(true, testFile.exists()) + } + + @Test + fun `test calling removeMark() deletes the file`() { + crashMarker.mark() + assertEquals(true, testFile.exists()) + crashMarker.removeMark() + assertEquals(false, testFile.exists()) + } + + @Test + fun `test isMarked() returns true if file exists and false if not`() { + assertEquals(false, testFile.exists()) + assertEquals(false, crashMarker.isMarked()) + crashMarker.mark() + assertEquals(true, testFile.exists()) + assertEquals(true, crashMarker.isMarked()) + } + + @Test + fun `test isMarked() returns false after exception is thrown twice in File_exists()`() { + crashMarker.mark() + assertEquals(true, testFile.exists()) + every { mockFile.exists() } throws SecurityException() + assertEquals(false, crashMarker.isMarked()) + } + + @Test + fun `test removeMark() tries to delete the file twice when delete() returns false`() { + crashMarker.mark() + every { mockFile.delete() } returns false + crashMarker.removeMark() + verify(exactly = 2) { mockFile.delete() } + } + + @Test + fun `test removeMark() tries to delete the file twice when delete() throws an exception`() { + crashMarker.mark() + every { mockFile.delete() } throws SecurityException() + crashMarker.removeMark() + verify(exactly = 2) { mockFile.delete() } + } + + @Test + fun `test testGetAndCleanMarker() verifies if marker exists and cleans the marker`() { + assertEquals(false, testFile.exists()) + assertEquals(false, crashMarker.getAndCleanMarker()) + crashMarker.mark() + assertEquals(true, testFile.exists()) + assertEquals(true, crashMarker.getAndCleanMarker()) + assertEquals(false, crashMarker.getAndCleanMarker()) + assertEquals(false, testFile.exists()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifierTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifierTest.kt new file mode 100644 index 0000000000..99f03ae80c --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/crash/LastRunCrashVerifierTest.kt @@ -0,0 +1,62 @@ +package io.embrace.android.embracesdk.internal.crash + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.concurrency.BlockableExecutorService +import io.embrace.android.embracesdk.worker.ExecutorName +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class LastRunCrashVerifierTest { + + private lateinit var lastRunCrashVerifier: LastRunCrashVerifier + private lateinit var mockCrashFileMarker: CrashFileMarker + private lateinit var fakeWorkerThreadModule: FakeWorkerThreadModule + private lateinit var executor: BlockableExecutorService + + @Before + fun setUp() { + mockCrashFileMarker = mockk() + lastRunCrashVerifier = LastRunCrashVerifier(mockCrashFileMarker) + fakeWorkerThreadModule = FakeWorkerThreadModule() + executor = fakeWorkerThreadModule.backgroundExecutor(ExecutorName.BACKGROUND_REGISTRATION) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `test calling didLastRunCrash() returns true if marker file exists`() { + every { mockCrashFileMarker.getAndCleanMarker() } returns true + assertTrue(lastRunCrashVerifier.didLastRunCrash()) + assertTrue(lastRunCrashVerifier.didLastRunCrash()) // check the result is cached + } + + @Test + fun `test calling didLastRunCrash() returns false if marker file does not exist`() { + every { mockCrashFileMarker.getAndCleanMarker() } returns false + assertFalse(lastRunCrashVerifier.didLastRunCrash()) + assertFalse(lastRunCrashVerifier.didLastRunCrash()) // check the result is cached + } + + @Test + fun `test calling readAndCleanMarkerAsync and then didLastRunCrash() returns true if marker file exists`() { + every { mockCrashFileMarker.getAndCleanMarker() } returns true + lastRunCrashVerifier.readAndCleanMarkerAsync(executor) + assertTrue(lastRunCrashVerifier.didLastRunCrash()) + } + + @Test + fun `test calling readAndCleanMarkerAsync and then didLastRunCrash() returns false if marker file doesn't exist`() { + every { mockCrashFileMarker.getAndCleanMarker() } returns false + lastRunCrashVerifier.readAndCleanMarkerAsync(executor) + assertFalse(lastRunCrashVerifier.didLastRunCrash()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanDataTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanDataTest.kt new file mode 100644 index 0000000000..bd3b128340 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanDataTest.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.internal.spans + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.fixtures.testSpan +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class EmbraceSpanDataTest { + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("span_expected.json") + val deserializedSpan = Gson().fromJson(json, EmbraceSpanData::class.java) + assertEquals(testSpan.name, deserializedSpan.name) + assertEquals(testSpan.spanId, deserializedSpan.spanId) + assertEquals(testSpan.parentSpanId, deserializedSpan.parentSpanId) + assertEquals(testSpan.traceId, deserializedSpan.traceId) + assertEquals(testSpan.startTimeNanos, deserializedSpan.startTimeNanos) + assertEquals(testSpan.endTimeNanos, deserializedSpan.endTimeNanos) + assertEquals(testSpan.status, deserializedSpan.status) + testSpan.events.forEachIndexed { index, event -> + assertEquals("Failed for event ${event.name}", event, deserializedSpan.events[index]) + event.attributes.forEach { + assertEquals("Failed for attribute ${it.key}", it.value, event.attributes[it.key]) + } + } + testSpan.attributes.forEach { + assertEquals("Failed for attribute ${it.key}", it.value, deserializedSpan.attributes[it.key]) + } + } + + @Test + fun testSerialization() { + // Take a span, serialize it to JSON, then deserialize it back and compare. This avoids having to deal with the exactly + // serialized form details like whitespace that won't matter when we deserialize it back to the object form. + val serializedSpanJson = Gson().toJson(testSpan) + val deserializedSpan = Gson().fromJson(serializedSpanJson, EmbraceSpanData::class.java) + assertEquals(testSpan, deserializedSpan) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImplTest.kt new file mode 100644 index 0000000000..cf00ae65c6 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImplTest.kt @@ -0,0 +1,144 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.fixtures.MAX_LENGTH_ATTRIBUTE_KEY +import io.embrace.android.embracesdk.fixtures.MAX_LENGTH_ATTRIBUTE_VALUE +import io.embrace.android.embracesdk.fixtures.MAX_LENGTH_EVENT_NAME +import io.embrace.android.embracesdk.fixtures.TOO_LONG_ATTRIBUTE_KEY +import io.embrace.android.embracesdk.fixtures.TOO_LONG_ATTRIBUTE_VALUE +import io.embrace.android.embracesdk.fixtures.TOO_LONG_EVENT_NAME +import io.embrace.android.embracesdk.fixtures.maxSizeEventAttributes +import io.embrace.android.embracesdk.fixtures.tooBigEventAttributes +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.SdkTracerProvider +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class EmbraceSpanImplTest { + private lateinit var embraceSpan: EmbraceSpanImpl + private val tracer = OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().build()).build() + .getTracer(EmbraceSpanImplTest::class.java.name) + + @Before + fun setup() { + embraceSpan = EmbraceSpanImpl(tracer.spanBuilder("test-span")) + } + + @Test + fun `validate default state`() { + with(embraceSpan) { + assertNull(traceId) + assertNull(spanId) + assertFalse(isRecording) + assertFalse(addEvent("eventName")) + assertFalse(addAttribute("first", "value")) + } + } + + @Test + fun `validate span started state`() { + with(embraceSpan) { + assertTrue(start()) + assertFalse(start()) + assertNotNull(traceId) + assertNotNull(spanId) + assertTrue(isRecording) + assertTrue(addEvent("eventName")) + assertTrue(addAttribute("first", "value")) + } + } + + @Test + fun `validate span stopped state`() { + with(embraceSpan) { + assertTrue(start()) + assertTrue(stop()) + assertFalse(stop()) + assertNotNull(traceId) + assertNotNull(spanId) + assertFalse(isRecording) + assertFalse(addEvent("eventName")) + assertFalse(addAttribute("first", "value")) + } + } + + @Test + fun `check adding events`() { + with(embraceSpan) { + assertTrue(start()) + assertTrue(addEvent(name = "current event")) + assertTrue( + addEvent( + name = "second current event", + time = null, + attributes = mapOf(Pair("key", "value"), Pair("key2", "value1")) + ) + ) + assertTrue(addEvent(name = "past event", time = 1L, attributes = null)) + assertTrue(addEvent(name = "future event", time = 2L, mapOf(Pair("key", "value"), Pair("key2", "value1")))) + } + } + + @Test + fun `cannot stop twice irrespective of error code`() { + with(embraceSpan) { + assertTrue(start()) + assertTrue(stop(ErrorCode.FAILURE)) + assertFalse(stop()) + } + } + + @Test + fun `check event limits`() { + with(embraceSpan) { + assertTrue(start()) + assertFalse(addEvent(name = TOO_LONG_EVENT_NAME)) + assertFalse(addEvent(name = TOO_LONG_EVENT_NAME, time = null, attributes = null)) + assertFalse(addEvent(name = "yo", time = null, attributes = tooBigEventAttributes)) + assertTrue(addEvent(name = MAX_LENGTH_EVENT_NAME)) + assertTrue(addEvent(name = MAX_LENGTH_EVENT_NAME, time = null, attributes = null)) + assertTrue(addEvent(name = "yo", time = null, attributes = maxSizeEventAttributes)) + repeat(EmbraceSpanImpl.MAX_EVENT_COUNT - 4) { + assertTrue(addEvent(name = "event $it")) + } + val eventAttributesAMap = mutableMapOf( + Pair(TOO_LONG_ATTRIBUTE_KEY, "value"), + Pair("key", TOO_LONG_ATTRIBUTE_VALUE), + ) + repeat(EmbraceSpanEvent.MAX_EVENT_ATTRIBUTE_COUNT - 2) { + eventAttributesAMap["key$it"] = "value" + } + assertTrue( + addEvent( + name = "yo", + time = null, + attributes = eventAttributesAMap + ) + ) + assertFalse(addEvent("failed event")) + assertTrue(stop()) + } + } + + @Test + fun `check attribute limits`() { + with(embraceSpan) { + assertTrue(start()) + assertFalse(addAttribute(key = TOO_LONG_ATTRIBUTE_KEY, value = "value")) + assertFalse(addAttribute(key = "key", value = TOO_LONG_ATTRIBUTE_VALUE)) + assertTrue(addAttribute(key = MAX_LENGTH_ATTRIBUTE_KEY, value = "value")) + assertTrue(addAttribute(key = "key", value = MAX_LENGTH_ATTRIBUTE_VALUE)) + assertTrue(addAttribute(key = "Key", value = MAX_LENGTH_ATTRIBUTE_VALUE)) + repeat(EmbraceSpanImpl.MAX_ATTRIBUTE_COUNT - 3) { + assertTrue(addAttribute(key = "key$it", value = "value")) + } + assertFalse(addAttribute(key = "failedKey", value = "value")) + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansServiceTest.kt new file mode 100644 index 0000000000..d162353434 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceSpansServiceTest.kt @@ -0,0 +1,224 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeSpansBehavior +import io.embrace.android.embracesdk.internal.OpenTelemetryClock +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.opentelemetry.sdk.common.CompletableResultCode +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class EmbraceSpansServiceTest { + + private lateinit var spansRemoteConfig: SpansRemoteConfig + private lateinit var configService: FakeConfigService + private lateinit var spansService: EmbraceSpansService + private val clock = FakeClock(10000L) + + @Before + fun setup() { + spansRemoteConfig = SpansRemoteConfig() + configService = FakeConfigService( + spansBehavior = fakeSpansBehavior { spansRemoteConfig } + ) + spansService = EmbraceSpansService(clock = OpenTelemetryClock(embraceClock = clock)) + configService.addListener(spansService) + } + + @Test + fun `verify default behaviour before initialization`() { + assertFalse(spansService.initialized()) + assertNull(spansService.createSpan("test-span")) + assertTrue(spansService.recordCompletedSpan("test-span", 10, 20)) + var lambdaRan = false + spansService.recordSpan("test-span") { lambdaRan = true } + assertTrue(lambdaRan) + assertNull(spansService.completedSpans()) + assertNull(spansService.flushSpans()) + assertEquals(CompletableResultCode.ofFailure(), spansService.storeCompletedSpans(listOf())) + } + + @Test + fun `initializing service if the config is not on won't actually initialize it`() { + configService.updateListeners() + spansService.initializeService(1, 5) + configService.updateListeners() + assertFalse(spansService.initialized()) + } + + @Test + fun `service works once initialized`() { + initializeServiceThenEnableConfig() + assertTrue(spansService.initialized()) + assertTrue(spansService.recordCompletedSpan("test-span", 10, 20)) + var lambdaRan = false + spansService.recordSpan("test-span") { lambdaRan = true } + assertTrue(lambdaRan) + assertEquals(3, spansService.completedSpans()?.size) + assertEquals(4, spansService.flushSpans()?.size) + } + + @Test + fun `service can be initialized after config enabled`() { + spansService.initializeService(1, 5) + spansRemoteConfig = SpansRemoteConfig(pctEnabled = 100f) + configService.updateListeners() + assertTrue(spansService.initialized()) + assertEquals(1, spansService.completedSpans()?.size) + } + + @Test + fun `second sdk startup span will not be recorded if you try to initialize the service twice`() { + initializeServiceThenEnableConfig() + assertEquals(1, spansService.completedSpans()?.size) + spansService.initializeService(10, 20) + assertEquals(1, spansService.completedSpans()?.size) + } + + @Test + fun `record internal completed span recording with all the fixings`() { + initializeServiceThenEnableConfig() + spansService.flushSpans() + val expectedName = "test-span" + val expectedStartTime = clock.now() + val expectedEndTime = expectedStartTime + 100L + val expectedType = EmbraceAttributes.Type.PERFORMANCE + val expectedAttributes = mapOf( + Pair("attribute1", "value1"), + Pair("attribute2", "value2") + ) + val expectedEvents = listOf( + EmbraceSpanEvent(name = "event1", timestampNanos = 0L, attributes = expectedAttributes), + EmbraceSpanEvent(name = "event2", timestampNanos = 5L, attributes = expectedAttributes) + ) + + spansService.recordCompletedSpan( + name = expectedName, + startTimeNanos = expectedStartTime, + endTimeNanos = expectedEndTime, + type = expectedType, + attributes = expectedAttributes, + events = expectedEvents + ) + + val name = "emb-$expectedName" + val currentSpans = checkNotNull(spansService.completedSpans()) + assertEquals(1, currentSpans.size) + val span = currentSpans[0] + + with(span) { + assertEquals(name, name) + assertEquals(expectedStartTime, startTimeNanos) + assertEquals(expectedEndTime, endTimeNanos) + assertEquals(expectedType.name, attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()]) + expectedAttributes.forEach { + assertEquals(it.value, attributes[it.key]) + } + assertEquals(expectedEvents, events) + } + } + + @Test + fun `can create spans after init`() { + initializeServiceThenEnableConfig() + spansService.flushSpans() + val parent = checkNotNull(spansService.createSpan("test-span")) + assertTrue(parent.start()) + val child = checkNotNull(spansService.createSpan(name = "test-span", parent = parent)) + assertTrue(child.start()) + assertTrue(parent.traceId == child.traceId) + assertTrue(parent.spanId == checkNotNull(child.parent).spanId) + } + + @Test + fun `can record completed span after init`() { + initializeServiceThenEnableConfig() + spansService.flushSpans() + val expectedName = "test-span" + val expectedStartTime = clock.now() + val expectedEndTime = expectedStartTime + 100L + assertTrue( + spansService.recordCompletedSpan( + name = expectedName, + startTimeNanos = expectedStartTime, + endTimeNanos = expectedEndTime + ) + ) + + assertEquals(1, checkNotNull(spansService.completedSpans()).size) + } + + @Test + fun `can record completed child span after init`() { + initializeServiceThenEnableConfig() + spansService.flushSpans() + val expectedName = "child-span" + val expectedStartTime = clock.now() + val expectedEndTime = expectedStartTime + 100L + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertTrue(parentSpan.start()) + assertTrue( + spansService.recordCompletedSpan( + name = expectedName, + parent = parentSpan, + startTimeNanos = expectedStartTime, + endTimeNanos = expectedEndTime + ) + ) + assertTrue(parentSpan.stop()) + + val currentSpans = checkNotNull(spansService.completedSpans()) + assertEquals(2, currentSpans.size) + assertTrue(currentSpans[0].traceId == currentSpans[1].traceId) + assertTrue(currentSpans[0].parentSpanId == currentSpans[1].spanId) + } + + @Test + fun `can record span after init`() { + initializeServiceThenEnableConfig() + spansService.flushSpans() + spansService.recordSpan(name = "test-span") { + spansService.hashCode() + } + + assertEquals(1, checkNotNull(spansService.completedSpans()).size) + } + + @Test + fun `can record child span after init`() { + initializeServiceThenEnableConfig() + spansService.flushSpans() + val parent = checkNotNull(spansService.createSpan("test-span")) + assertTrue(parent.start()) + spansService.recordSpan(name = "child-span", parent = parent) { + spansService.hashCode() + } + assertTrue(parent.stop()) + + val currentSpans = checkNotNull(spansService.completedSpans()) + assertEquals(2, currentSpans.size) + assertTrue(currentSpans[0].traceId == currentSpans[1].traceId) + assertTrue(currentSpans[0].parentSpanId == currentSpans[1].spanId) + } + + @Test + fun `completed spans recorded before initialization will saved and recorded upon initialization`() { + assertFalse(spansService.initialized()) + assertTrue(spansService.recordCompletedSpan("test-span", 10, 20)) + assertTrue(spansService.recordCompletedSpan("test-span", 15, 25)) + initializeServiceThenEnableConfig() + assertEquals(3, spansService.completedSpans()?.size) + } + + private fun initializeServiceThenEnableConfig() { + spansRemoteConfig = SpansRemoteConfig(pctEnabled = 100f) + configService.updateListeners() + spansService.initializeService(1, 5) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracerTest.kt new file mode 100644 index 0000000000..bac1bfeb4b --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/EmbraceTracerTest.kt @@ -0,0 +1,113 @@ +package io.embrace.android.embracesdk.internal.spans + +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeOpenTelemetryClock +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class EmbraceTracerTest { + + private lateinit var spansService: SpansServiceImpl + private lateinit var embraceTracer: EmbraceTracer + private val clock = FakeClock(10000L) + + @Before + fun setup() { + spansService = SpansServiceImpl(100L, 200L, FakeOpenTelemetryClock(embraceClock = clock)) + embraceTracer = EmbraceTracer(spansService) + spansService.flushSpans() + } + + @Test + fun `create and use EmbraceSpan using public interface`() { + val embraceSpan = checkNotNull(embraceTracer.createSpan(name = "test-span")) + assertNotNull(embraceSpan) + assertTrue(embraceSpan.start()) + assertTrue(embraceSpan.stop()) + verifyPublicSpan("test-span") + } + + @Test + fun `stop EmbraceSpan with different error codes`() { + ErrorCode.values().forEach { errorCode -> + val embraceSpan = checkNotNull(embraceTracer.createSpan(name = "test-span")) + assertNotNull(embraceSpan) + assertTrue(embraceSpan.start()) + assertTrue(embraceSpan.stop(errorCode)) + verifyPublicSpan("test-span") + spansService.flushSpans() + } + } + + @Test + fun `record lambda running as span`() { + val returnThis = 1881L + val lambdaReturn = embraceTracer.recordSpan(name = "lambda-test-span") { + returnThis + } + verifyPublicSpan("lambda-test-span") + assertEquals(returnThis, lambdaReturn) + } + + @Test + fun `record completed span with all the fixings`() { + val expectedName = "test-span" + val expectedStartTime = clock.now() + val expectedEndTime = expectedStartTime + 100L + val expectedType = EmbraceAttributes.Type.PERFORMANCE + val expectedAttributes = mapOf( + Pair("attribute1", "value1"), + Pair("attribute2", "value2") + ) + val expectedEvents: List = + listOf( + EmbraceSpanEvent(name = "event1", timestampNanos = 0L, expectedAttributes), + EmbraceSpanEvent(name = "event2", timestampNanos = 5L, expectedAttributes), + ) + + assertTrue( + embraceTracer.recordCompletedSpan( + name = expectedName, + startTimeNanos = expectedStartTime, + endTimeNanos = expectedEndTime, + attributes = expectedAttributes, + events = expectedEvents + ) + ) + + with(verifyPublicSpan(expectedName)) { + assertEquals(expectedStartTime, startTimeNanos) + assertEquals(expectedEndTime, endTimeNanos) + assertEquals( + expectedType.name, + attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()] + ) + assertEquals("true", attributes["emb.key"]) + expectedAttributes.forEach { + assertEquals(it.value, attributes[it.key]) + } + assertEquals(expectedEvents, events) + } + } + + private fun verifyPublicSpan(name: String, errorCode: ErrorCode? = null): EmbraceSpanData { + val currentSpans = spansService.completedSpans() + assertEquals(1, currentSpans.size) + val currentSpan = currentSpans[0] + assertEquals(name, currentSpan.name) + assertEquals( + EmbraceAttributes.Type.PERFORMANCE.name, + currentSpan.attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()] + ) + assertEquals("true", currentSpan.attributes["emb.key"]) + assertEquals(errorCode?.name, currentSpan.attributes[errorCode?.keyName()]) + assertFalse(currentSpan.isPrivate()) + return currentSpan + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImplTest.kt new file mode 100644 index 0000000000..07be82e8bd --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/spans/SpansServiceImplTest.kt @@ -0,0 +1,703 @@ +package io.embrace.android.embracesdk.internal.spans + +import android.os.Build.VERSION_CODES.TIRAMISU +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeOpenTelemetryClock +import io.embrace.android.embracesdk.fixtures.MAX_LENGTH_SPAN_NAME +import io.embrace.android.embracesdk.fixtures.TOO_LONG_ATTRIBUTE_KEY +import io.embrace.android.embracesdk.fixtures.TOO_LONG_ATTRIBUTE_VALUE +import io.embrace.android.embracesdk.fixtures.TOO_LONG_SPAN_NAME +import io.embrace.android.embracesdk.fixtures.maxSizeAttributes +import io.embrace.android.embracesdk.fixtures.maxSizeEvents +import io.embrace.android.embracesdk.fixtures.tooBigAttributes +import io.embrace.android.embracesdk.fixtures.tooBigEvents +import io.embrace.android.embracesdk.internal.spans.SpansServiceImpl.Companion.MAX_SPAN_COUNT_PER_TRACE +import io.embrace.android.embracesdk.internal.spans.SpansServiceImpl.Companion.MAX_TRACE_COUNT_PER_SESSION +import io.embrace.android.embracesdk.spans.EmbraceSpan +import io.embrace.android.embracesdk.spans.EmbraceSpanEvent +import io.embrace.android.embracesdk.spans.ErrorCode +import io.opentelemetry.api.trace.SpanId +import io.opentelemetry.api.trace.StatusCode +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.util.concurrent.TimeUnit + +@Config(sdk = [TIRAMISU]) +@RunWith(AndroidJUnit4::class) +internal class SpansServiceImplTest { + + private lateinit var spansService: SpansServiceImpl + private val clock = FakeClock(1000L) + + @Test + fun `initialization records SDK startup span`() { + val startTimeMillis = clock.now() + val endTimeMillis = startTimeMillis + 10L + initService(startTimeMillis, endTimeMillis) + with(verifyAndReturnSoleCompletedSpan("emb-sdk-init")) { + assertEquals(SpanId.getInvalid(), parentSpanId) + assertEquals(TimeUnit.MILLISECONDS.toNanos(startTimeMillis), startTimeNanos) + assertEquals(TimeUnit.MILLISECONDS.toNanos(endTimeMillis), endTimeNanos) + assertEquals( + EmbraceAttributes.Type.PERFORMANCE.name, + attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()] + ) + assertTrue(isPrivate()) + assertEquals(StatusCode.OK, status) + } + } + + @Test + fun `create trace with default parameters`() { + initAndFlushService() + val embraceSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertNull(embraceSpan.parent) + assertTrue(embraceSpan.start()) + assertTrue(embraceSpan.stop()) + with(verifyAndReturnSoleCompletedSpan("emb-test-span")) { + assertEquals(SpanId.getInvalid(), parentSpanId) + assertEquals( + EmbraceAttributes.Type.PERFORMANCE.name, + attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()] + ) + assertTrue(isKey()) + } + } + + @Test + fun `create trace with custom type`() { + initAndFlushService() + val embraceSpan = checkNotNull( + spansService.createSpan( + name = "test-span", + type = EmbraceAttributes.Type.PERFORMANCE + ) + ) + assertTrue(embraceSpan.start()) + assertTrue(embraceSpan.stop()) + with(verifyAndReturnSoleCompletedSpan("emb-test-span")) { + assertEquals(SpanId.getInvalid(), parentSpanId) + assertEquals( + EmbraceAttributes.Type.PERFORMANCE.name, + attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()] + ) + assertTrue(isKey()) + } + } + + @Test + fun `create trace with children`() { + initAndFlushService() + val parentSpan = spansService.createSpan(name = "test-span") + checkNotNull(parentSpan).start() + val childSpan = spansService.createSpan(name = "child-span", parent = parentSpan) + checkNotNull(childSpan).start() + assertTrue(parentSpan.traceId == childSpan.traceId) + assertTrue(parentSpan.spanId == checkNotNull(childSpan.parent).spanId) + assertTrue(childSpan.stop()) + assertTrue(parentSpan.stop()) + + val currentSpans = spansService.completedSpans() + assertEquals(2, currentSpans.size) + assertTrue(currentSpans[0].traceId == currentSpans[1].traceId) + + with(currentSpans[0]) { + assertEquals("emb-child-span", name) + assertEquals(childSpan.spanId, spanId) + assertEquals(childSpan.traceId, traceId) + assertFalse(isKey()) + assertTrue(isPrivate()) + } + + with(currentSpans[1]) { + assertEquals("emb-test-span", name) + assertEquals(SpanId.getInvalid(), parentSpanId) + assertEquals(parentSpan.spanId, spanId) + assertEquals(parentSpan.traceId, traceId) + assertTrue(isKey()) + assertTrue(isPrivate()) + } + } + + @Test + fun `start span created from previous session`() { + initAndFlushService() + val embraceSpan = checkNotNull(spansService.createSpan(name = "test-span")) + spansService.flushSpans() + assertTrue(embraceSpan.start()) + } + + @Test + fun `cannot create span with no session span`() { + initAndFlushService() + spansService.flushSpans(appTerminationCause = EmbraceAttributes.AppTerminationCause.USER_TERMINATION) + assertNull(spansService.createSpan(name = "test")) + } + + @Test + fun `cannot create span with blank name`() { + initAndFlushService() + assertNull(spansService.createSpan(name = "")) + assertNull(spansService.createSpan(name = " ")) + } + + @Test + fun `cannot create child if parent not started`() { + initAndFlushService() + val parentSpan = spansService.createSpan(name = "test-span") + val childSpan = spansService.createSpan(name = "child-span", parent = parentSpan) + assertNull(childSpan) + } + + @Test + fun `can create child if parent has stopped`() { + initAndFlushService() + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertTrue(parentSpan.start()) + assertTrue(parentSpan.stop()) + val childSpan = spansService.createSpan(name = "child-span", parent = parentSpan) + assertNotNull(childSpan) + } + + @Test + fun `record internal completed span with all the fixings`() { + initAndFlushService() + val expectedName = "test-span" + val expectedStartTime = clock.now() + val expectedEndTime = expectedStartTime + 100L + val expectedType = EmbraceAttributes.Type.PERFORMANCE + val expectedAttributes = mapOf( + Pair("attribute1", "value1"), + Pair("attribute2", "value2") + ) + val expectedEvents = listOf( + EmbraceSpanEvent(name = "event1", timestampNanos = 0L, attributes = expectedAttributes), + EmbraceSpanEvent(name = "event2", timestampNanos = 5L, attributes = expectedAttributes) + ) + + spansService.recordCompletedSpan( + name = expectedName, + startTimeNanos = expectedStartTime, + endTimeNanos = expectedEndTime, + type = expectedType, + attributes = expectedAttributes, + events = expectedEvents + ) + + with(verifyAndReturnSoleCompletedSpan("emb-$expectedName")) { + assertEquals(expectedStartTime, startTimeNanos) + assertEquals(expectedEndTime, endTimeNanos) + assertEquals(expectedType.name, attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()]) + assertEquals(SpanId.getInvalid(), parentSpanId) + assertTrue(isKey()) + assertTrue(isPrivate()) + expectedAttributes.forEach { + assertEquals(it.value, attributes[it.key]) + } + assertEquals(expectedEvents, events) + } + } + + @Test + fun `record completed child span`() { + initAndFlushService() + val expectedName = "child-span" + val expectedStartTime = clock.now() + val expectedEndTime = expectedStartTime + 100L + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertTrue(parentSpan.start()) + assertTrue( + spansService.recordCompletedSpan( + name = expectedName, + parent = parentSpan, + startTimeNanos = expectedStartTime, + endTimeNanos = expectedEndTime + ) + ) + + with(verifyAndReturnSoleCompletedSpan("emb-$expectedName")) { + assertEquals(expectedStartTime, startTimeNanos) + assertEquals(expectedEndTime, endTimeNanos) + assertFalse(isKey()) + assertTrue(isPrivate()) + } + assertTrue(parentSpan.stop()) + + val currentSpans = spansService.completedSpans() + assertEquals(2, currentSpans.size) + assertTrue(currentSpans[0].traceId == currentSpans[1].traceId) + assertTrue(currentSpans[0].parentSpanId == currentSpans[1].spanId) + } + + @Test + fun `record completed child span with stopped parent`() { + initAndFlushService() + val expectedName = "child-span" + val expectedStartTime = clock.now() + val expectedEndTime = expectedStartTime + 100L + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertTrue(parentSpan.start()) + assertTrue(parentSpan.stop()) + spansService.flushSpans() + assertTrue( + spansService.recordCompletedSpan( + name = expectedName, + parent = parentSpan, + startTimeNanos = expectedStartTime, + endTimeNanos = expectedEndTime + ) + ) + } + + @Test + fun `can't record completed child span with not-started parent`() { + initAndFlushService() + val expectedName = "child-span" + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertFalse( + spansService.recordCompletedSpan( + name = expectedName, + parent = parentSpan, + startTimeNanos = 10L, + endTimeNanos = 100L + ) + ) + } + + @Test + fun `record spans with different ending error codes `() { + initAndFlushService() + ErrorCode.values().forEach { + assertTrue( + spansService.recordCompletedSpan( + name = "test${it.name}", + startTimeNanos = 0, + endTimeNanos = 1, + errorCode = it + ) + ) + with(verifyAndReturnSoleCompletedSpan("emb-test${it.name}")) { + assertEquals(it.name, attributes[it.keyName()]) + } + spansService.flushSpans() + } + } + + @Test + fun `validate start and end times for a completed span`() { + initAndFlushService() + assertFalse( + spansService.recordCompletedSpan( + name = "test-pan", + startTimeNanos = 500, + endTimeNanos = 499 + ) + ) + } + + @Test + fun `cannot record completed span if there is not current session span`() { + initAndFlushService() + spansService.flushSpans(appTerminationCause = EmbraceAttributes.AppTerminationCause.USER_TERMINATION) + assertFalse( + spansService.recordCompletedSpan( + name = "test-span", + startTimeNanos = 500, + endTimeNanos = 600 + ) + ) + } + + @Test + fun `record lambda running as trace`() { + initAndFlushService() + val returnThis = "yooooo" + val lambdaReturn = spansService.recordSpan(name = "test-span") { + returnThis + } + + assertEquals(returnThis, lambdaReturn) + with(verifyAndReturnSoleCompletedSpan("emb-test-span")) { + assertEquals(SpanId.getInvalid(), parentSpanId) + assertEquals( + EmbraceAttributes.Type.PERFORMANCE.name, + attributes[EmbraceAttributes.Type.PERFORMANCE.keyName()] + ) + assertTrue(isKey()) + assertTrue(isPrivate()) + } + } + + @Test + fun `record lambda running as a child span`() { + initAndFlushService() + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertTrue(parentSpan.start()) + spansService.recordSpan(name = "child-span", parent = parentSpan) { + parentSpan.hashCode() + } + + assertTrue(parentSpan.stop()) + + val currentSpans = spansService.completedSpans() + assertEquals(2, currentSpans.size) + assertTrue(currentSpans[0].traceId == currentSpans[1].traceId) + assertTrue(currentSpans[0].parentSpanId == currentSpans[1].spanId) + + with(currentSpans[0]) { + assertEquals("emb-child-span", name) + assertFalse(isKey()) + assertTrue(isPrivate()) + } + } + + @Test + fun `lambda with not-started parent will still run and return value`() { + initAndFlushService() + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + val returnThis = 1 + val returnValue = spansService.recordSpan(name = "child-span", parent = parentSpan) { + returnThis + 1 + } + + assertEquals(2, returnValue) + assertEquals(0, spansService.completedSpans().size) + } + + @Test + fun `lambda with stopped parent will still be recorded`() { + initAndFlushService() + val parentSpan = checkNotNull(spansService.createSpan(name = "test-span")) + assertTrue(parentSpan.start()) + assertTrue(parentSpan.stop()) + val returnThis = 1 + val returnValue = spansService.recordSpan(name = "child-span", parent = parentSpan) { + returnThis + 1 + } + + assertEquals(2, returnValue) + assertEquals(2, spansService.completedSpans().size) + } + + @Test + fun `recording span as lambda throws an exception will record a failed span and rethrows exception`() { + initAndFlushService() + assertThrows(RuntimeException::class.java) { + spansService.recordSpan(name = "test-span") { + throw RuntimeException("You done bad") + } + } + + with(verifyAndReturnSoleCompletedSpan("emb-test-span")) { + assertEquals( + ErrorCode.FAILURE.name, + attributes[ErrorCode.FAILURE.keyName()] + ) + } + } + + @Test + fun `logging span as lambda with no current active session will run code but not log span`() { + initAndFlushService() + spansService.flushSpans(appTerminationCause = EmbraceAttributes.AppTerminationCause.USER_TERMINATION) + var executed = false + spansService.recordSpan(name = "test-span") { + executed = true + } + + assertTrue(executed) + assertEquals(0, spansService.completedSpans().size) + } + + @Test + fun `check name length limit`() { + initAndFlushService() + assertNull(spansService.createSpan(name = TOO_LONG_SPAN_NAME)) + assertFalse(spansService.recordCompletedSpan(name = TOO_LONG_SPAN_NAME, startTimeNanos = 100L, endTimeNanos = 200L)) + assertNotNull(spansService.recordSpan(name = TOO_LONG_SPAN_NAME) { 1 }) + assertEquals(0, spansService.completedSpans().size) + assertNotNull(spansService.createSpan(name = MAX_LENGTH_SPAN_NAME)) + assertNotNull(spansService.recordSpan(name = MAX_LENGTH_SPAN_NAME) { 2 }) + assertTrue(spansService.recordCompletedSpan(name = MAX_LENGTH_SPAN_NAME, startTimeNanos = 100L, endTimeNanos = 200L)) + assertEquals(2, spansService.completedSpans().size) + } + + @Test + fun `check events limit`() { + initAndFlushService() + assertFalse( + spansService.recordCompletedSpan( + name = "too many events", + startTimeNanos = 100L, + endTimeNanos = 200L, + events = tooBigEvents + ) + ) + assertTrue( + spansService.recordCompletedSpan( + name = MAX_LENGTH_SPAN_NAME, + startTimeNanos = 100L, + endTimeNanos = 200L, + events = maxSizeEvents + ) + ) + + spansService.flushSpans() + + val attributesMap = mutableMapOf( + Pair(TOO_LONG_ATTRIBUTE_KEY, "value"), + Pair("key", TOO_LONG_ATTRIBUTE_VALUE), + ) + repeat(EmbraceSpanEvent.MAX_EVENT_ATTRIBUTE_COUNT - 2) { + attributesMap["key$it"] = "value" + } + + val events = mutableListOf(checkNotNull(EmbraceSpanEvent.create("event", 100L, attributesMap))) + repeat(EmbraceSpanImpl.MAX_EVENT_COUNT - 1) { + events.add(checkNotNull(EmbraceSpanEvent.create("event", 100L, null))) + } + assertTrue( + spansService.recordCompletedSpan( + name = MAX_LENGTH_SPAN_NAME, + startTimeNanos = 100L, + endTimeNanos = 200L, + events = events + ) + ) + + val completedSpans = spansService.completedSpans() + assertEquals(1, completedSpans.size) + assertEquals(10, completedSpans[0].events.size) + assertEquals(8, completedSpans[0].events[0].attributes.size) + } + + @Test + fun `check attributes limit`() { + initAndFlushService() + assertFalse( + spansService.recordCompletedSpan( + name = "too many attributes", + startTimeNanos = 100L, + endTimeNanos = 200L, + attributes = tooBigAttributes + ) + ) + assertTrue( + spansService.recordCompletedSpan( + name = MAX_LENGTH_SPAN_NAME, + startTimeNanos = 100L, + endTimeNanos = 200L, + attributes = maxSizeAttributes + ) + ) + + spansService.flushSpans() + + val attributesMap = mutableMapOf( + Pair(TOO_LONG_ATTRIBUTE_KEY, "value"), + Pair("key", TOO_LONG_ATTRIBUTE_VALUE), + ) + repeat(EmbraceSpanImpl.MAX_ATTRIBUTE_COUNT - 2) { + attributesMap["key$it"] = "value" + } + + assertTrue( + spansService.recordCompletedSpan( + name = MAX_LENGTH_SPAN_NAME, + startTimeNanos = 100L, + endTimeNanos = 200L, + attributes = attributesMap + ) + ) + + val completedSpans = spansService.completedSpans() + assertEquals(1, completedSpans.size) + assertEquals(48, completedSpans[0].attributes.filterNot { it.key.startsWith("emb.") }.size) + } + + @Test + fun `check trace limits with maximum not started traces`() { + initAndFlushService() + repeat(MAX_TRACE_COUNT_PER_SESSION) { + assertNotNull(spansService.createSpan(name = "spanzzz$it", internal = false)) + } + assertNull(spansService.createSpan(name = "failed-span", internal = false)) + } + + @Test + fun `check trace limits with maximum traces recorded around a lambda`() { + initAndFlushService() + repeat(MAX_TRACE_COUNT_PER_SESSION) { + assertEquals("derp", spansService.recordSpan(name = "record$it", internal = false) { "derp" }) + } + assertNull(spansService.createSpan(name = "failed-span", internal = false)) + } + + @Test + fun `check trace limits with maximum completed traces`() { + initAndFlushService() + repeat(MAX_TRACE_COUNT_PER_SESSION) { + assertTrue( + spansService.recordCompletedSpan( + name = "complete$it", + startTimeNanos = 100L, + endTimeNanos = 200L, + internal = false + ) + ) + } + assertNull(spansService.createSpan(name = "failed-span", internal = false)) + } + + @Test + fun `check internal traces and child spans don't count towards limit`() { + initAndFlushService() + val parent = checkNotNull(spansService.createSpan(name = "test-span", internal = false)) + assertTrue(parent.start()) + assertNotNull(spansService.createSpan(name = "child-span", parent = parent, internal = false)) + assertNotNull(spansService.createSpan(name = "internal-span", parent = parent, internal = true)) + repeat(MAX_TRACE_COUNT_PER_SESSION - 1) { + assertNotNull(spansService.createSpan(name = "spanzzz$it", internal = false)) + } + assertNull(spansService.createSpan(name = "failed-span", internal = false)) + assertNotNull(spansService.createSpan(name = "child-span", parent = parent, internal = false)) + assertNotNull(spansService.createSpan(name = "internal-again", internal = true)) + } + + @Test + fun `check child span per trace limit`() { + initAndFlushService() + var parentSpan: EmbraceSpan? = null + repeat(MAX_SPAN_COUNT_PER_TRACE) { + val span = spansService.createSpan(name = "spanzzz$it", parent = parentSpan, internal = false) + assertTrue(checkNotNull(span).start()) + parentSpan = span + } + assertNull(spansService.createSpan(name = "failed-span", parent = parentSpan, internal = false)) + assertFalse( + spansService.recordCompletedSpan( + name = "failed-span", + startTimeNanos = 100L, + endTimeNanos = 200L, + parent = parentSpan, + internal = false + ) + ) + spansService.flushSpans() + assertEquals(2, spansService.recordSpan(name = "failed-span", parent = parentSpan, internal = false) { 2 }) + assertEquals(0, spansService.completedSpans().size) + } + + @Test + fun `check internal child spans don't count towards limit`() { + initAndFlushService() + val parentSpan = checkNotNull(spansService.createSpan(name = "parent-span", internal = true)) + assertTrue(parentSpan.start()) + assertNotNull(spansService.createSpan(name = "failed-span", parent = parentSpan, internal = true)) + assertNotNull(spansService.recordSpan(name = "failed-span", parent = parentSpan, internal = true) { }) + assertTrue( + spansService.recordCompletedSpan( + name = "failed-span", + startTimeNanos = 100L, + endTimeNanos = 200L, + parent = parentSpan, + internal = true + ) + ) + + repeat(MAX_SPAN_COUNT_PER_TRACE - 1) { + assertNotNull(spansService.createSpan(name = "spanzzz$it", parent = parentSpan, internal = false)) + } + assertNull(spansService.createSpan(name = "failed-span", parent = parentSpan, internal = false)) + assertNotNull(spansService.createSpan(name = "internal-span", parent = parentSpan, internal = true)) + } + + @Test + fun `flushing clears completed spans and current session span`() { + initAndFlushService() + repeat(3) { + spansService.recordSpan("test$it") { } + } + assertEquals(3, spansService.completedSpans().size) + + val flushedSpans = spansService.flushSpans() + assertEquals(4, flushedSpans.size) + + val lastFlushedSpan = flushedSpans[3] + with(lastFlushedSpan) { + assertEquals("emb-session-span", name) + assertEquals( + EmbraceAttributes.Type.SESSION.name, + attributes[EmbraceAttributes.Type.SESSION.keyName()] + ) + assertFalse(isKey()) + assertEquals(StatusCode.OK, status) + } + + assertEquals(0, spansService.completedSpans().size) + } + + @Test + fun `flushing with app termination and termination reason flushes session span with right termination type`() { + EmbraceAttributes.AppTerminationCause.values().forEach { + initAndFlushService() + val flushedSpans = spansService.flushSpans(it) + assertEquals(1, flushedSpans.size) + + val lastFlushedSpan = flushedSpans[0] + with(lastFlushedSpan) { + assertEquals("emb-session-span", name) + assertEquals( + EmbraceAttributes.Type.SESSION.name, + attributes[EmbraceAttributes.Type.SESSION.keyName()] + ) + assertEquals(StatusCode.OK, status) + assertFalse(isKey()) + assertEquals(it.name, attributes[it.keyName()]) + } + + assertEquals(0, spansService.completedSpans().size) + } + } + + @Test + fun `after flushing with app termination, spans cannot be recorded`() { + initAndFlushService() + spansService.flushSpans(EmbraceAttributes.AppTerminationCause.USER_TERMINATION) + spansService.recordSpan("test-span") { + // do thing + } + assertFalse(spansService.recordCompletedSpan("test-span-2", startTimeNanos = 0, endTimeNanos = 1)) + assertEquals(0, spansService.completedSpans().size) + } + + private fun initService(sdkInitStartTimeMillis: Long, sdkInitEndTimeMillis: Long) { + spansService = SpansServiceImpl( + sdkInitStartTimeNanos = TimeUnit.MILLISECONDS.toNanos(sdkInitStartTimeMillis), + sdkInitEndTimeNanos = TimeUnit.MILLISECONDS.toNanos(sdkInitEndTimeMillis), + clock = FakeOpenTelemetryClock(embraceClock = clock) + ) + } + + private fun initAndFlushService() { + val start = clock.now() + val end = start + 50L + initService(sdkInitStartTimeMillis = start, sdkInitEndTimeMillis = end) + spansService.flushSpans() + } + + private fun verifyAndReturnSoleCompletedSpan(name: String): EmbraceSpanData { + val currentSpans = spansService.completedSpans() + assertEquals(1, currentSpans.size) + assertEquals(name, currentSpans[0].name) + return currentSpans[0] + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtilsTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtilsTest.kt new file mode 100644 index 0000000000..d59322e051 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/internal/utils/ThrowableUtilsTest.kt @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.internal.utils + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class ThrowableUtilsTest { + + @Test + fun `test throwableName`() { + assertEquals("name should be empty string if the Throwable is null", causeName(null), "") + assertEquals( + "name should be empty string if the Throwable's cause is null", + causeName(RuntimeException("message", null)), + "" + ) + assertEquals( + "name is unexpected", + causeName( + RuntimeException("message", IllegalArgumentException()) + ), + IllegalArgumentException::class.qualifiedName + ) + } + + @Test + fun `test throwableMessage`() { + assertEquals( + "message should be empty string if Throwable is null", + causeMessage(null), + "" + ) + assertEquals( + "message should be empty string if the Throwable's cause is null", + causeMessage(RuntimeException("message", null)), + "" + ) + assertEquals( + "message should be empty string if the Throwable's cause's message is null", + causeMessage(RuntimeException("message", IllegalArgumentException())), + "" + ) + val message = "this is a message" + assertEquals( + "message is unexpected", + causeMessage(RuntimeException("message", IllegalArgumentException(message))), + message + ) + } + + @Test + fun `test safe stacktrace`() { + assertNull(DangerousException().getSafeStackTrace()) + } + + class DangerousException : Exception("DangerousException") { + override fun getStackTrace(): Array = error("lol") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorServiceTest.kt new file mode 100644 index 0000000000..b0a11bf8ed --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/EmbraceInternalErrorServiceTest.kt @@ -0,0 +1,129 @@ +package io.embrace.android.embracesdk.logging + +import io.embrace.android.embracesdk.clock.Clock +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeDataCaptureEventBehavior +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import java.net.ConnectException +import java.net.SocketException + +internal class EmbraceInternalErrorServiceTest { + + private lateinit var service: EmbraceInternalErrorService + private lateinit var cfgService: ConfigService + private lateinit var activityService: FakeActivityService + private lateinit var cfg: RemoteConfig + private val clock = Clock { 1509234092L } + + @Before + fun setUp() { + activityService = FakeActivityService() + service = EmbraceInternalErrorService(activityService, clock, false) + cfg = RemoteConfig() + cfgService = + FakeConfigService(dataCaptureEventBehavior = fakeDataCaptureEventBehavior { cfg }) + } + + @Test + fun testExceptionReportingEnabled() { + cfg = cfg.copy(internalExceptionCaptureEnabled = true) + service.setConfigService(cfgService) + service.handleInternalError(RuntimeException("Whoops!")) + + val error = checkNotNull(service.currentExceptionError) + assertEquals(1, error.occurrences) + with(error.exceptionErrors.single()) { + assertEquals("active", state) + assertEquals(clock.now(), timestamp) + + // verify exc object + val exc = exceptions?.single() + assertEquals("Whoops!", exc?.message) + assertEquals("java.lang.RuntimeException", exc?.name) + } + } + + @Test + fun testExceptionReportingDisabled() { + cfg = cfg.copy(internalExceptionCaptureEnabled = false) + service.setConfigService(cfgService) + service.handleInternalError(RuntimeException()) + val error = checkNotNull(service.currentExceptionError) + assertEquals(0, error.occurrences) + } + + @Test + fun testExceptionReportingUnknown() { + service.handleInternalError(RuntimeException()) + val error = checkNotNull(service.currentExceptionError) + assertEquals(1, error.occurrences) + } + + @Test + fun testExceptionReset() { + cfg = cfg.copy(internalExceptionCaptureEnabled = true) + service.setConfigService(cfgService) + service.handleInternalError(RuntimeException()) + + val error = checkNotNull(service.currentExceptionError) + assertEquals(1, error.occurrences) + + service.resetExceptionErrorObject() + assertNull(service.currentExceptionError) + } + + @Test + fun testIsInBackground() { + activityService.isInBackground = true + service.handleInternalError(RuntimeException("Whoops!")) + + val error = checkNotNull(service.currentExceptionError) + assertEquals(1, error.occurrences) + + val info = error.exceptionErrors.single() + assertEquals("background", info.state) + } + + @Test + fun testMultipleExceptionTypes() { + service.handleInternalError(RuntimeException("Whoops!")) + service.handleInternalError(IllegalStateException("Another!")) + service.handleInternalError(IllegalStateException("Another 2!")) + + val error = checkNotNull(service.currentExceptionError) + assertEquals(3, error.occurrences) + + assertEquals("Whoops!", error.exceptionErrors[0].exceptions?.single()?.message) + assertEquals("Another!", error.exceptionErrors[1].exceptions?.single()?.message) + assertEquals("Another 2!", error.exceptionErrors[2].exceptions?.single()?.message) + } + + @Test + fun testExceptionMaxLimit() { + repeat(8) { k -> + service.handleInternalError(RuntimeException("Oh no $k")) + } + val err = checkNotNull(service.currentExceptionError) + assertEquals(8, err.occurrences) + assertEquals(5, err.exceptionErrors.size) + } + + @Test + fun testWrappedIgnoredException() { + val exc = IllegalStateException(ConnectException("It took too long...")) + service.handleInternalError(exc) + assertNull(service.currentExceptionError) + } + + @Test + fun testIgnoredNetworkException() { + service.handleInternalError(SocketException("Timeout...")) + assertNull(service.currentExceptionError) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalEmbraceLoggerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalEmbraceLoggerTest.kt new file mode 100644 index 0000000000..0acc94ba4e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalEmbraceLoggerTest.kt @@ -0,0 +1,80 @@ +package io.embrace.android.embracesdk.logging + +import io.embrace.android.embracesdk.fakes.FakeLoggerAction +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Severity +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class InternalEmbraceLoggerTest { + + private val action = FakeLoggerAction() + private var logger = InternalEmbraceLogger().apply { + addLoggerAction(action) + } + + @Before + fun setUp() { + ApkToolsConfig.IS_DEVELOPER_LOGGING_ENABLED = false + } + + @Test + fun `a log with a severity that does not surpass the threshold does not trigger actions`() { + // given the threshold is .INFO + logger.setThreshold(Severity.INFO) + + // when log is called with a lower severity + logger.log("test", Severity.DEBUG, Exception(), false) + + // then logger actions are not triggered + assertTrue(action.msgQueue.isEmpty()) + } + + @Test + fun `a log with the same severity as the threshold triggers logging actions`() { + // given the threshold is .INFO + logger.setThreshold(Severity.INFO) + + // when log is called with the same severity + val throwable = Exception() + logger.log("test", Severity.INFO, throwable, false) + + // then logger actions are triggered + val msg = action.msgQueue.single() + val expected = FakeLoggerAction.LogMessage("test", Severity.INFO, throwable, false) + assertEquals(expected, msg) + } + + @Test + fun `a log with a higher severity than the threshold triggers logging actions`() { + // given the threshold is .INFO + logger.setThreshold(Severity.INFO) + + // when log is called with a higher severity + val throwable = Exception() + logger.log("test", Severity.WARNING, throwable, false) + + // then logger actions are triggered + val msg = action.msgQueue.single() + val expected = FakeLoggerAction.LogMessage("test", Severity.WARNING, throwable, false) + assertEquals(expected, msg) + } + + @Test + fun `a log with lower severity than the threshold triggers actions when developer logging is enabled`() { + // given the threshold is .INFO and developer logging is enabled + logger.setThreshold(Severity.INFO) + ApkToolsConfig.IS_DEVELOPER_LOGGING_ENABLED = true + + // when log is called with a lower severity + val throwable = Exception() + logger.log("test", Severity.DEBUG, throwable, false) + + // then logger actions are triggered + val msg = action.msgQueue.single() + val expected = FakeLoggerAction.LogMessage("test", Severity.DEBUG, throwable, false) + assertEquals(expected, msg) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalErrorLoggerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalErrorLoggerTest.kt new file mode 100644 index 0000000000..e36badc230 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/logging/InternalErrorLoggerTest.kt @@ -0,0 +1,141 @@ +package io.embrace.android.embracesdk.logging + +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.After +import org.junit.BeforeClass +import org.junit.Test + +internal class InternalErrorLoggerTest { + + private lateinit var internalErrorLogger: InternalErrorLogger + + companion object { + private lateinit var mockExceptionService: EmbraceInternalErrorService + private lateinit var mockLogger: InternalEmbraceLogger.LoggerAction + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockExceptionService = mockk(relaxUnitFun = true) + mockLogger = mockk(relaxUnitFun = true) + } + } + + private fun setupService(strictModeEnabled: Boolean = false) { + internalErrorLogger = + InternalErrorLogger(mockExceptionService, mockLogger, strictModeEnabled) + } + + @After + fun after() { + clearAllMocks(answers = false) + } + + @Test + fun `if no throwable then do not handle exception`() { + setupService() + internalErrorLogger.log("message", InternalStaticEmbraceLogger.Severity.DEBUG, null, true) + + verify { mockExceptionService wasNot Called } + } + + @Test + fun `if throwable available, then do handle exception`() { + setupService() + val exception = Exception() + internalErrorLogger.log("message", InternalStaticEmbraceLogger.Severity.DEBUG, exception, true) + + verify { mockExceptionService.handleInternalError(exception) } + } + + @Test + fun `if an exception is thrown while handling exception, then log it and dont rethrow it`() { + setupService() + val exceptionMessage = "root cause" + val exception = Exception() + val msg = "message" + every { mockExceptionService.handleInternalError(exception) } throws RuntimeException( + exceptionMessage + ) + + internalErrorLogger.log(msg, InternalStaticEmbraceLogger.Severity.DEBUG, exception, true) + + verify { mockExceptionService.handleInternalError(exception) } + verify { mockLogger.log(exceptionMessage, InternalStaticEmbraceLogger.Severity.ERROR, null, false) } + } + + @Test + fun `if an exception is thrown while handling exception, then log it and dont rethrow it with no root cause message`() { + setupService() + val exception = Exception() + val msg = "message" + every { mockExceptionService.handleInternalError(exception) } throws RuntimeException() + + internalErrorLogger.log(msg, InternalStaticEmbraceLogger.Severity.DEBUG, exception, true) + + verify { mockExceptionService.handleInternalError(exception) } + verify { mockLogger.log("", InternalStaticEmbraceLogger.Severity.ERROR, null, false) } + } + + @Test + fun `if logStrictMode is enabled and a throwable is available with ERROR severity`() { + setupService(true) + val exception = Exception() + val errorMsg = "Error message" + internalErrorLogger.log(errorMsg, InternalStaticEmbraceLogger.Severity.ERROR, exception, true) + + verify { mockExceptionService.handleInternalError(exception) } + } + + @Test + fun `if logStrictMode is enabled and a throwable is not available with ERROR severity then handle exception`() { + setupService(true) + val errorMsg = "Error message" + internalErrorLogger.log(errorMsg, InternalStaticEmbraceLogger.Severity.ERROR, null, true) + + verify(exactly = 1) { + mockExceptionService.handleInternalError( + any() as InternalErrorLogger.LogStrictModeException + ) + } + } + + @Test + fun `if logStrictMode is enabled and a throwable is not available with INFO severity then dont handle exception`() { + setupService(true) + val errorMsg = "Error message" + internalErrorLogger.log(errorMsg, InternalStaticEmbraceLogger.Severity.INFO, null, true) + + verify(exactly = 0) { mockExceptionService.handleInternalError(any() as Exception) } + } + + @Test + fun `if logStrictMode is disabled and a throwable is available with ERROR severity`() { + setupService(false) + val exception = Exception() + val errorMsg = "Error message" + internalErrorLogger.log(errorMsg, InternalStaticEmbraceLogger.Severity.ERROR, exception, true) + + verify { mockExceptionService.handleInternalError(exception) } + } + + @Test + fun `if logStrictMode is enabled and an exception is thrown while handling exception, then log it and dont rethrow it`() { + setupService(true) + val exceptionMessage = "root cause" + val exception = Exception() + val msg = "message" + every { mockExceptionService.handleInternalError(any() as InternalErrorLogger.LogStrictModeException) } throws RuntimeException( + exceptionMessage + ) + + internalErrorLogger.log(msg, InternalStaticEmbraceLogger.Severity.DEBUG, exception, true) + + verify { mockExceptionService.handleInternalError(any() as InternalErrorLogger.LogStrictModeException) } + verify { mockLogger.log(exceptionMessage, InternalStaticEmbraceLogger.Severity.ERROR, null, false) } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepositoryTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepositoryTest.kt new file mode 100644 index 0000000000..af42eef052 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceRepositoryTest.kt @@ -0,0 +1,199 @@ +package io.embrace.android.embracesdk.ndk + +import android.content.Context +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.NativeCrashData +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.spyk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.Test +import java.io.File +import java.io.FilenameFilter + +internal class EmbraceNdkServiceRepositoryTest { + + companion object { + private lateinit var repository: EmbraceNdkServiceRepository + private lateinit var context: Context + private lateinit var logger: InternalEmbraceLogger + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(InternalEmbraceLogger::class) + context = mockk(relaxUnitFun = true) + logger = mockk(relaxed = true) + repository = EmbraceNdkServiceRepository(context, logger) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @Test + fun `test sortNativeCrashes by oldest`() { + val file1: File = mockk(relaxed = true) + val file2: File = mockk(relaxed = true) + val mockedRepository = spyk(repository, recordPrivateCalls = true) + every { file1.lastModified() } returns 1 + every { file2.lastModified() } returns 2 + every { mockedRepository["getNativeCrashFiles"]() } returns arrayOf(file1, file2) + val result = mockedRepository.sortNativeCrashes(true) + assert(result[0].lastModified() < result[1].lastModified()) + } + + @Test + fun `Test sortNativeCrashes calls getNativeFiles and getNativeCrashFiles`() { + val file1: File = mockk(relaxed = true) + val file2: File = mockk(relaxed = true) + val mockedRepository = spyk(repository, recordPrivateCalls = true) + every { file1.lastModified() } returns 1 + every { file2.lastModified() } returns 2 + every { file1.isDirectory } returns true + every { file2.isDirectory } returns true + every { file1.name } returns "ndk" + every { file2.name } returns "ndk" + + val fileArray = arrayOf(file1, file2) + every { context.cacheDir } returns mockk() + every { context.cacheDir.listFiles() } returns fileArray + + mockedRepository.sortNativeCrashes(true) + verify { mockedRepository["getNativeFiles"](any() as FilenameFilter) } + verify { mockedRepository["getNativeCrashFiles"]() } + } + + @Test + fun `test sortNativeCrashes catches an exception and returns the unordered list`() { + val file1: File = mockk() + val file2: File? = null + val mockedRepository = spyk(repository, recordPrivateCalls = true) + every { file1.lastModified() } returns 1 + every { mockedRepository["getNativeCrashFiles"]() } returns arrayOf(file1, file2) + val result = mockedRepository.sortNativeCrashes(true) + verify { logger.logError("Failed sorting native crashes.", any()) } + assertEquals(result[0], file1) + assertEquals(result[1], null) + } + + @Test + fun `test sortNativeCrashes not by oldest`() { + val file1: File = mockk() + val file2: File = mockk() + val mockedRepository = spyk(repository, recordPrivateCalls = true) + every { file1.lastModified() } returns 1 + every { file2.lastModified() } returns 2 + every { mockedRepository["getNativeCrashFiles"]() } returns arrayOf(file1, file2) + val result = mockedRepository.sortNativeCrashes(false) + assert(result[0].lastModified() > result[1].lastModified()) + } + + @Test + fun `test errorFileForCrash when file does not exist`() { + val mockedRepository = spyk(repository, recordPrivateCalls = true) + val file1: File = mockk() + every { file1.absolutePath } returns "path.path" + every { file1.exists() } returns false + val result = mockedRepository.errorFileForCrash(file1) + assertEquals(result, null) + } + + @Test + fun `test errorFileForCrash when there is an error file`() { + val mockedRepository = spyk(repository, recordPrivateCalls = true) + val file1: File = mockk() + val file2: File = mockk() + every { file1.absolutePath } returns "path.path" + every { mockedRepository["companionFileForCrash"](file1, ".error") } returns file2 + val result = mockedRepository.errorFileForCrash(file1) + assert(result != null) + } + + @Test + fun `test mapFileForCrash when file does not exist`() { + val mockedRepository = spyk(repository, recordPrivateCalls = true) + val file1: File = mockk() + every { file1.absolutePath } returns "path.path" + every { file1.exists() } returns false + val result = mockedRepository.mapFileForCrash(file1) + assertEquals(result, null) + } + + @Test + fun `test mapFileForCrash when there is an error file`() { + val mockedRepository = spyk(repository, recordPrivateCalls = true) + val file1: File = mockk() + val file2: File = mockk() + every { file1.absolutePath } returns "path.path" + every { mockedRepository["companionFileForCrash"](file1, ".map") } returns file2 + val result = mockedRepository.mapFileForCrash(file1) + assert(result != null) + } + + @Test + fun `test deleteFiles when native crash is null`() { + val crashFile: File = mockk() + val errorFile: File = mockk() + val mapFile: File = mockk() + + every { crashFile.delete() } returns false + every { errorFile.delete() } returns false + every { mapFile.delete() } returns false + every { crashFile.absolutePath } returns "path" + repository.deleteFiles(crashFile, errorFile, mapFile, null) + + verify { errorFile.delete() } + verify { mapFile.delete() } + verify { logger.logWarning("Failed to delete native crash file {crashFilePath=path}") } + } + + @Test + fun `test deleteFiles when native crash is not null`() { + val crashFile: File = mockk() + val errorFile: File = mockk() + val mapFile: File = mockk() + val nativeCrash: NativeCrashData = mockk() + + every { crashFile.delete() } returns false + every { errorFile.delete() } returns false + every { mapFile.delete() } returns false + every { crashFile.absolutePath } returns "path" + every { nativeCrash.sessionId } returns "1" + every { nativeCrash.nativeCrashId } returns "10" + repository.deleteFiles(crashFile, errorFile, mapFile, nativeCrash) + + val msg = "Failed to delete native crash file {sessionId=" + nativeCrash.sessionId + + ", crashId=" + nativeCrash.nativeCrashId + + ", crashFilePath=" + crashFile.absolutePath + "}" + verify { errorFile.delete() } + verify { mapFile.delete() } + verify { logger.logWarning(msg) } + } + + @Test + fun `test deleteFiles when native crash delete() is true`() { + val crashFile: File = mockk() + val errorFile: File = mockk() + val mapFile: File = mockk() + + every { crashFile.delete() } returns true + every { errorFile.delete() } returns false + every { mapFile.delete() } returns false + every { crashFile.absolutePath } returns "path" + + repository.deleteFiles(crashFile, errorFile, mapFile, null) + verify { crashFile.delete() } + verify { errorFile.delete() } + verify { mapFile.delete() } + verify { logger.logDebug(any() as String) } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceTest.kt new file mode 100644 index 0000000000..86b82df8b2 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/EmbraceNdkServiceTest.kt @@ -0,0 +1,583 @@ +package io.embrace.android.embracesdk.ndk + +import android.content.Context +import android.content.res.Resources +import android.os.Build +import android.util.Base64 +import com.google.common.util.concurrent.MoreExecutors +import com.google.gson.Gson +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.comms.delivery.DeliveryService +import io.embrace.android.embracesdk.comms.delivery.EmbraceDeliveryService +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeDeviceArchitecture +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import io.embrace.android.embracesdk.internal.ApkToolsConfig +import io.embrace.android.embracesdk.internal.SharedObjectLoader +import io.embrace.android.embracesdk.internal.crash.CrashFileMarker +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.EventMessage +import io.embrace.android.embracesdk.payload.NativeCrashData +import io.embrace.android.embracesdk.payload.NativeCrashMetadata +import io.embrace.android.embracesdk.session.ActivityService +import io.embrace.android.embracesdk.session.EmbraceSessionProperties +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.spyk +import io.mockk.unmockkAll +import io.mockk.verify +import io.mockk.verifyOrder +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import java.io.File +import java.util.concurrent.ExecutorService + +internal class EmbraceNdkServiceTest { + + companion object { + private lateinit var embraceNdkService: TestEmbraceNdkService + private lateinit var context: Context + private lateinit var metadataService: MetadataService + private lateinit var configService: ConfigService + private lateinit var activityService: FakeActivityService + private lateinit var localConfig: LocalConfig + private lateinit var mockDeliveryService: EmbraceDeliveryService + private lateinit var userService: UserService + private lateinit var sessionProperties: EmbraceSessionProperties + private lateinit var appFramework: Embrace.AppFramework + private lateinit var sharedObjectLoader: SharedObjectLoader + private lateinit var logger: InternalEmbraceLogger + private lateinit var delegate: NdkServiceDelegate.NdkDelegate + private lateinit var repository: EmbraceNdkServiceRepository + private lateinit var resources: Resources + private lateinit var gson: Gson + private val deviceArchitecture = FakeDeviceArchitecture() + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(ExecutorService::class) + mockkStatic(Uuid::class) + mockkStatic(Embrace::class) + context = mockk(relaxed = true) + metadataService = mockk(relaxed = true) + configService = mockk(relaxed = true) + activityService = FakeActivityService() + localConfig = mockk(relaxed = true) + mockDeliveryService = mockk() + userService = mockk(relaxed = true) + sessionProperties = mockk(relaxed = true) + appFramework = mockk() + sharedObjectLoader = mockk() + logger = InternalEmbraceLogger() + delegate = mockk(relaxed = true) + repository = mockk(relaxUnitFun = true) + resources = mockk(relaxUnitFun = true) + gson = Gson() + + val appInfo = AppInfo(appFramework = Embrace.AppFramework.NATIVE.value) + every { metadataService.getAppInfo() } returns appInfo + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @After + fun after() { + clearAllMocks( + answers = false, + staticMocks = false, + objectMocks = false + ) + + val file = File(context.cacheDir.toString() + "/ndk") + if (file.exists()) { + file.delete() + } + } + + private fun initializeService() { + embraceNdkService = TestEmbraceNdkService( + context, + metadataService, + activityService, + mockDeliveryService, + userService, + sessionProperties, + appFramework, + sharedObjectLoader, + logger, + repository, + delegate, + MoreExecutors.newDirectExecutorService(), + MoreExecutors.newDirectExecutorService() + ) + } + + @Test + fun `test updateSessionId where installSignals was not executed and isInstalled false`() { + enableNdk(false) + initializeService() + embraceNdkService.updateSessionId("sessionId") + verify(exactly = 0) { delegate._updateSessionId("sessionId") } + } + + @Test + fun `test updateSessionId where installSignals was executed and isInstalled true`() { + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + initializeService() + embraceNdkService.updateSessionId("sessionId") + verify(exactly = 1) { delegate._updateSessionId("sessionId") } + } + + @Test + fun `test onBackground doesn't run _updateAppState when _updateMetaData was not executed and isInstalled false`() { + enableNdk(false) + initializeService() + embraceNdkService.onBackground(0L) + verify(exactly = 0) { delegate._updateAppState("background") } + } + + @Test + fun `test onBackground runs _updateAppState when _updateMetaData was executed and isInstalled true`() { + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + initializeService() + embraceNdkService.onBackground(0L) + verify(exactly = 1) { delegate._updateAppState("background") } + } + + @Test + fun `test onSessionPropertiesUpdate where _updateMetaData was not executed and isInstalled false`() { + enableNdk(false) + initializeService() + embraceNdkService.onSessionPropertiesUpdate(sessionProperties.get()) + verify(exactly = 0) { delegate._updateMetaData(any()) } + } + + @Test + fun `test onSessionPropertiesUpdate where _updateMetaData was executed and isInstalled true`() { + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + initializeService() + embraceNdkService.onSessionPropertiesUpdate(sessionProperties.get()) + val newDeviceMetaData = + NativeCrashMetadata( + metadataService.getAppInfo(), + metadataService.getDeviceInfo(), + userService.getUserInfo(), + sessionProperties.get().toMap() + ) + + verify { delegate._updateMetaData(newDeviceMetaData.toJson()) } + } + + @Test + fun `test initialization with unity id and ndk enabled runs installSignals and updateDeviceMetaData`() { + every { Uuid.getEmbUuid() } returns "unityId" + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + appFramework = Embrace.AppFramework.UNITY + initializeService() + assertTrue(activityService.listeners.contains(embraceNdkService)) + + val reportBasePath = context.cacheDir.absolutePath + "/ndk" + val markerFilePath = context.cacheDir.absolutePath + "/" + CrashFileMarker.CRASH_MARKER_FILE_NAME + verify(exactly = 1) { + delegate._installSignalHandlers( + reportBasePath, + markerFilePath, + any(), + "null", + metadataService.getAppState(), + embraceNdkService.getUnityCrashId(), + Build.VERSION.SDK_INT, + deviceArchitecture.is32BitDevice, + ApkToolsConfig.IS_DEVELOPER_LOGGING_ENABLED + ) + } + + val newDeviceMetaData = NativeCrashMetadata( + metadataService.getAppInfo(), + metadataService.getDeviceInfo(), + userService.getUserInfo(), + sessionProperties.get().toMap() + ).toJson() + + verify(exactly = 1) { delegate._updateMetaData(newDeviceMetaData) } + assertEquals(embraceNdkService.getUnityCrashId(), Uuid.getEmbUuid()) + } + + @Test + fun `test metadata is updated after installation of the signal handler`() { + every { Uuid.getEmbUuid() } returns "uuid" + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + + val appInfo = AppInfo(appFramework = Embrace.AppFramework.NATIVE.value) + every { metadataService.getAppInfo() } returns appInfo + every { metadataService.getLightweightAppInfo() } returns appInfo + val deviceInfo = DeviceInfo(jailbroken = false) + every { metadataService.getDeviceInfo() } returns deviceInfo + every { metadataService.getLightweightDeviceInfo() } returns deviceInfo + + val metaData = NativeCrashMetadata( + metadataService.getLightweightAppInfo(), + metadataService.getLightweightDeviceInfo(), + userService.getUserInfo(), + sessionProperties.get().toMap() + ).toJson() + + initializeService() + assertTrue(activityService.listeners.contains(embraceNdkService)) + + val reportBasePath = context.cacheDir.absolutePath + "/ndk" + val markerFilePath = context.cacheDir.absolutePath + "/" + CrashFileMarker.CRASH_MARKER_FILE_NAME + + verifyOrder { + metadataService.getLightweightAppInfo() + metadataService.getLightweightDeviceInfo() + + delegate._installSignalHandlers( + reportBasePath, + markerFilePath, + metaData, + "null", + metadataService.getAppState(), + "uuid", + Build.VERSION.SDK_INT, + deviceArchitecture.is32BitDevice, + ApkToolsConfig.IS_DEVELOPER_LOGGING_ENABLED + ) + + metadataService.getAppInfo() + metadataService.getDeviceInfo() + + delegate._updateMetaData(metaData) + } + } + + @Test + fun `test getUnityCrashId`() { + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + appFramework = Embrace.AppFramework.UNITY + every { Uuid.getEmbUuid() } returns "unityId" + initializeService() + val uuid = embraceNdkService.getUnityCrashId() + assertEquals(uuid, "unityId") + } + + @Test + fun `test initialization with ndk disabled doesn't run _installSignalHandlers and _updateMetaData`() { + enableNdk(false) + initializeService() + val reportBasePath = context.cacheDir.absolutePath + "/ndk" + val markerFilePath = context.cacheDir.absolutePath + "/crash_file_marker" + + verify(exactly = 0) { + delegate._installSignalHandlers( + reportBasePath, + markerFilePath, + "{}", + "null", + metadataService.getAppState(), + embraceNdkService.getUnityCrashId(), + Build.VERSION.SDK_INT, + deviceArchitecture.is32BitDevice, + ApkToolsConfig.IS_DEVELOPER_LOGGING_ENABLED + ) + } + + val newDeviceMetaData = + NativeCrashMetadata( + metadataService.getAppInfo(), + metadataService.getDeviceInfo(), + userService.getUserInfo(), + sessionProperties.get().toMap() + ) + + verify(exactly = 0) { delegate._updateMetaData(newDeviceMetaData.toJson()) } + } + + @Test + fun `test testCrash where isCpp is true`() { + initializeService() + embraceNdkService.testCrash(true) + verify { delegate._testNativeCrash_CPP() } + verify(exactly = 0) { delegate._testNativeCrash_C() } + } + + @Test + fun `test testCrash where isCpp is false`() { + initializeService() + embraceNdkService.testCrash(false) + verify { delegate._testNativeCrash_C() } + verify(exactly = 0) { delegate._testNativeCrash_CPP() } + } + + @Test + fun `test onUserInfoUpdate where _updateMetaData was not executed and isInstalled false`() { + enableNdk(false) + initializeService() + embraceNdkService.onUserInfoUpdate() + verify(exactly = 0) { delegate._updateMetaData(any()) } + } + + @Test + fun `test onUserInfoUpdate where _updateMetaData was executed and isInstalled true`() { + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + initializeService() + embraceNdkService.onUserInfoUpdate() + val newDeviceMetaData = + NativeCrashMetadata( + metadataService.getAppInfo(), + metadataService.getDeviceInfo(), + userService.getUserInfo(), + sessionProperties.get().toMap() + ) + + verify { delegate._updateMetaData(newDeviceMetaData.toJson()) } + } + + @Test + fun `test onForeground runs _updateAppState when _updateMetaData was executed and isInstalled true`() { + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + initializeService() + embraceNdkService.onForeground(true, 1, 10) + verify(exactly = 1) { delegate._updateAppState("active") } + } + + @Test + fun `test onForeground doesn't run _updateAppState when _updateMetaData was not executed and isInstalled false`() { + enableNdk(false) + initializeService() + embraceNdkService.onForeground(true, 1, 100) + verify(exactly = 0) { delegate._updateAppState("active") } + } + + @Test + fun `test checkForNativeCrash does nothing if there are no matchingFiles`() { + every { repository.sortNativeCrashes(false) } returns listOf() + initializeService() + val result = embraceNdkService.checkForNativeCrash() + assertNull(result) + verify { repository.sortNativeCrashes(false) } + verify(exactly = 0) { delegate._getCrashReport(any()) } + verify(exactly = 0) { repository.errorFileForCrash(any()) } + verify(exactly = 0) { repository.mapFileForCrash(any()) } + verify(exactly = 0) { + repository.deleteFiles( + any(), + any(), + any(), + any() as NativeCrashData + ) + } + verify(exactly = 0) { mockDeliveryService.sendEventAsync(any() as EventMessage) } + } + + @Test + fun `test getSymbolsForCurrentArch`() { + mockkStatic(Base64::class) + val resourceId = 10 + val nativeSymbolsJson = "{\"symbols\":{\"arm64-v8a\":{\"symbol1\":\"test\"}}}" + + enableNdk(true) + every { context.resources } returns resources + every { context.packageName } returns "package-name" + every { resources.getString(resourceId) } returns "result" + every { + resources.getIdentifier( + "emb_ndk_symbols", + "string", + "package-name" + ) + } returns resourceId + every { resources.getString(resourceId) } returns nativeSymbolsJson + every { + Base64.decode( + nativeSymbolsJson, + Base64.DEFAULT + ) + } returns nativeSymbolsJson.encodeToByteArray() + initializeService() + + val result = embraceNdkService.getSymbolsForCurrentArch() + assert(result != null) + assert(result?.containsKey("symbol1") ?: false) + assert(result?.getOrDefault("symbol1", "") == "test") + } + + @Test + fun `test checkForNativeCrash catches an exception if _getCrashReport returns an empty string`() { + val crashFile = File.createTempFile("test", "test") + every { repository.sortNativeCrashes(false) } returns listOf(crashFile) + every { delegate._getCrashReport(any()) } returns "" + initializeService() + val crashData = embraceNdkService.checkForNativeCrash() + assertNull(crashData) + } + + @Test + fun `test checkForNativeCrash catches an exception if _getCrashReport returns invalid json syntax`() { + val crashFile = File.createTempFile("test", "test") + every { Uuid.getEmbUuid() } returns "unityId" + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + + val json = "{\n" + + " \"sid\": [\n" + + " {\n" + + " }\n" + + " ]\n" + + "}" + every { repository.sortNativeCrashes(false) } returns listOf(crashFile) + every { delegate._getCrashReport(any()) } returns json + + initializeService() + val crashData = embraceNdkService.checkForNativeCrash() + assertNull(crashData) + } + + @Test + fun `test checkForNativeCrash when a native crash was captured`() { + val crashFile: File = File.createTempFile("test", "test") + val errorFile: File = File.createTempFile("test", "test") + val mapFile: File = File.createTempFile("test", "test") + + every { Uuid.getEmbUuid() } returns "unityId" + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns true + every { repository.errorFileForCrash(crashFile) } returns errorFile + every { repository.mapFileForCrash(crashFile) } returns mapFile + + every { delegate._getCrashReport(any()) } returns getNativeCrashRaw() + every { repository.sortNativeCrashes(false) } returns listOf(crashFile) + + appFramework = Embrace.AppFramework.UNITY + initializeService() + val mockedNdkService = spyk(embraceNdkService, recordPrivateCalls = true) + every { mockedNdkService.getSymbolsForCurrentArch() } returns mockk() + + val result = mockedNdkService.checkForNativeCrash() + assertNotNull(result) + + verify { mockedNdkService["getNativeCrashErrors"](any() as NativeCrashData, errorFile) } + verify(exactly = 1) { repository.sortNativeCrashes(false) } + verify(exactly = 1) { delegate._getCrashReport(any()) } + verify(exactly = 1) { repository.errorFileForCrash(crashFile) } + verify(exactly = 1) { repository.mapFileForCrash(crashFile) } + verify(exactly = 1) { + repository.deleteFiles( + crashFile, + errorFile, + mapFile, + any() as NativeCrashData + ) + } + } + + @Test + fun `test checkForNativeCrash when there is no native crash does not execute crash files logic`() { + every { repository.sortNativeCrashes(false) } returns listOf() + + appFramework = Embrace.AppFramework.UNITY + initializeService() + + val result = embraceNdkService.checkForNativeCrash() + assertNull(result) + + verify(exactly = 1) { repository.sortNativeCrashes(false) } + verify(exactly = 0) { delegate._getCrashReport(any()) } + verify(exactly = 0) { repository.errorFileForCrash(any()) } + verify(exactly = 0) { repository.mapFileForCrash(any()) } + verify(exactly = 0) { mockDeliveryService.sendEventAsync(any() as EventMessage) } + verify(exactly = 0) { + repository.deleteFiles( + any() as File, + any() as File, + any() as File, + any() as NativeCrashData + ) + } + verify(exactly = 0) { mockDeliveryService.sendEventAsync(any() as EventMessage) } + } + + @Test + fun `test initialization does not does not install signals and create directories if loadEmbraceNative is false`() { + enableNdk(true) + every { sharedObjectLoader.loadEmbraceNative() } returns false + initializeService() + val mockedNdkService = spyk(embraceNdkService, recordPrivateCalls = false) + verify(exactly = 0) { mockedNdkService["installSignals"]() } + verify(exactly = 0) { mockedNdkService["createCrashReportDirectory"]() } + } + + private fun getNativeCrashRaw() = ResourceReader.readResourceAsText("native_crash_raw.txt") + + private fun enableNdk(enabled: Boolean) { + every { configService.autoDataCaptureBehavior } returns fakeAutoDataCaptureBehavior(localCfg = { + LocalConfig("", enabled, SdkLocalConfig()) + }) + } + + private class TestEmbraceNdkService( + context: Context, + metadataService: MetadataService, + activityService: ActivityService, + deliveryService: DeliveryService, + userService: UserService, + sessionProperties: EmbraceSessionProperties, + appFramework: Embrace.AppFramework, + sharedObjectLoader: SharedObjectLoader, + logger: InternalEmbraceLogger, + repository: EmbraceNdkServiceRepository, + delegate: NdkServiceDelegate.NdkDelegate, + cleanCacheExecutorService: ExecutorService, + ndkStartupExecutorService: ExecutorService + ) : EmbraceNdkService( + context, + metadataService, + activityService, + configService, + deliveryService, + userService, + sessionProperties, + appFramework, + sharedObjectLoader, + logger, + repository, + delegate, + cleanCacheExecutorService, + ndkStartupExecutorService, + deviceArchitecture + ) { + override fun createCrashReportDirectory() { + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/NativeModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/NativeModuleImplTest.kt new file mode 100644 index 0000000000..ccc81be2da --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/ndk/NativeModuleImplTest.kt @@ -0,0 +1,27 @@ +package io.embrace.android.embracesdk.ndk + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.fakes.fakeEmbraceSessionProperties +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeDeliveryModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +internal class NativeModuleImplTest { + + @Test + fun testDefaultImplementations() { + val module = NativeModuleImpl( + FakeCoreModule(), + FakeEssentialServiceModule(), + FakeDeliveryModule(), + fakeEmbraceSessionProperties(), + FakeWorkerThreadModule() + ) + assertNotNull(module.ndkService) + assertNull(module.nativeThreadSamplerService) + assertNull(module.nativeThreadSamplerInstaller) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequestTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequestTest.kt new file mode 100644 index 0000000000..3bae98b4d7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/EmbraceNetworkRequestTest.kt @@ -0,0 +1,178 @@ +package io.embrace.android.embracesdk.network + +import io.embrace.android.embracesdk.internal.TraceparentGenerator +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import org.junit.Assert.assertEquals +import org.junit.Test + +private const val URL = "http://google.com" +private const val START_TIME = 1600000000000 +private const val END_TIME = 1600000000243 +private const val BYTES_SENT = 509L +private const val BYTES_RECEIVED = 210L +private const val RESPONSE_CODE = 304 +private const val TRACE_ID = "trace-id" +private const val ERR_TYPE = "err_type" +private const val ERR_MSG = "err_msg" +private val httpMethod = HttpMethod.GET +private val traceParent = TraceparentGenerator.generateW3CTraceparent() + +@Suppress("DEPRECATION") +internal class EmbraceNetworkRequestTest { + + @Test + fun testFromCompletedRequest1() { + val request = EmbraceNetworkRequest.fromCompletedRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + RESPONSE_CODE + ) + verifyDefaultCompletedRequest(request) + } + + @Test + fun testFromCompletedRequest2() { + val request = EmbraceNetworkRequest.fromCompletedRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + RESPONSE_CODE, + TRACE_ID + ) + verifyDefaultCompletedRequest(request) + assertEquals(TRACE_ID, request.traceId) + } + + @Test + fun testFromCompletedRequest3() { + val captureData = NetworkCaptureData(null, null, null, null, null) + val request = EmbraceNetworkRequest.fromCompletedRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + RESPONSE_CODE, + TRACE_ID, + captureData + ) + verifyDefaultCompletedRequest(request) + assertEquals(TRACE_ID, request.traceId) + assertEquals(captureData, request.networkCaptureData) + } + + @Test + fun testFromCompletedRequest4() { + val captureData = NetworkCaptureData(null, null, null, null, null) + val request = EmbraceNetworkRequest.fromCompletedRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + RESPONSE_CODE, + TRACE_ID, + traceParent, + captureData + ) + verifyDefaultCompletedRequest(request) + assertEquals(TRACE_ID, request.traceId) + assertEquals(traceParent, request.w3cTraceparent) + assertEquals(captureData, request.networkCaptureData) + } + + @Test + fun testFromIncompleteRequest1() { + val request = EmbraceNetworkRequest.fromIncompleteRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + ERR_TYPE, + ERR_MSG + ) + verifyDefaultIncompleteRequest(request) + } + + @Test + fun testFromIncompleteRequest2() { + val request = EmbraceNetworkRequest.fromIncompleteRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + ERR_TYPE, + ERR_MSG, + TRACE_ID + ) + verifyDefaultIncompleteRequest(request) + assertEquals(TRACE_ID, request.traceId) + } + + @Test + fun testFromIncompleteRequest3() { + val captureData = NetworkCaptureData(null, null, null, null, null) + val request = EmbraceNetworkRequest.fromIncompleteRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + ERR_TYPE, + ERR_MSG, + TRACE_ID, + captureData + ) + verifyDefaultIncompleteRequest(request) + assertEquals(TRACE_ID, request.traceId) + assertEquals(captureData, request.networkCaptureData) + } + + @Test + fun testFromIncompleteRequest4() { + val captureData = NetworkCaptureData(null, null, null, null, null) + val request = EmbraceNetworkRequest.fromIncompleteRequest( + URL, + httpMethod, + START_TIME, + END_TIME, + ERR_TYPE, + ERR_MSG, + TRACE_ID, + traceParent, + captureData + ) + verifyDefaultIncompleteRequest(request) + assertEquals(TRACE_ID, request.traceId) + assertEquals(traceParent, request.w3cTraceparent) + assertEquals(captureData, request.networkCaptureData) + } + + private fun verifyDefaultCompletedRequest(request: EmbraceNetworkRequest) { + assertEquals(URL, request.url) + assertEquals(httpMethod.name, request.httpMethod) + assertEquals(START_TIME, request.startTime) + assertEquals(END_TIME, request.endTime) + assertEquals(BYTES_SENT, request.bytesSent) + assertEquals(BYTES_RECEIVED, request.bytesReceived) + assertEquals(RESPONSE_CODE, request.responseCode) + } + + private fun verifyDefaultIncompleteRequest(request: EmbraceNetworkRequest) { + assertEquals(URL, request.url) + assertEquals(httpMethod.name, request.httpMethod) + assertEquals(START_TIME, request.startTime) + assertEquals(END_TIME, request.endTime) + assertEquals(ERR_TYPE, request.errorType) + assertEquals(ERR_MSG, request.errorMessage) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt new file mode 100644 index 0000000000..3f74de50d8 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt @@ -0,0 +1,218 @@ +package io.embrace.android.embracesdk.network.http + +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.Companion.TRACEPARENT_HEADER_NAME +import io.embrace.android.embracesdk.config.remote.NetworkSpanForwardingRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeNetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.mockk.CapturingSlot +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import java.io.IOException +import java.util.concurrent.TimeoutException +import javax.net.ssl.HttpsURLConnection + +internal class EmbraceUrlConnectionOverrideTest { + + private lateinit var mockEmbrace: Embrace + private lateinit var fakeConfigService: ConfigService + private lateinit var mockConnection: HttpsURLConnection + private lateinit var capturedEmbraceNetworkRequest: CapturingSlot + private lateinit var remoteNetworkSpanForwardingConfig: NetworkSpanForwardingRemoteConfig + private lateinit var embraceUrlConnectionOverride: EmbraceUrlConnectionOverride + private lateinit var embraceUrlConnectionOverrideUnwrapped: EmbraceUrlConnectionOverride + + @Before + fun setup() { + mockEmbrace = mockk(relaxed = true) + capturedEmbraceNetworkRequest = slot() + remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 0f) + fakeConfigService = FakeConfigService( + networkSpanForwardingBehavior = fakeNetworkSpanForwardingBehavior( + remoteConfig = { remoteNetworkSpanForwardingConfig } + ) + ) + every { mockEmbrace.recordNetworkRequest(capture(capturedEmbraceNetworkRequest)) } answers { } + every { mockEmbrace.configService } answers { fakeConfigService } + + mockConnection = createMockConnection() + embraceUrlConnectionOverride = EmbraceUrlConnectionOverride(mockConnection, true, mockEmbrace) + embraceUrlConnectionOverrideUnwrapped = EmbraceUrlConnectionOverride(mockConnection, false, mockEmbrace) + } + + @Test + fun `completed network call logged exactly once if connection connected with wrapped output stream`() { + executeRequest() + verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(HttpMethod.POST.name, httpMethod) + assertEquals(HTTP_OK, responseCode) + assertEquals(1L, bytesSent) + assertEquals(100L, bytesReceived) + assertNull(errorType) + } + } + + @Test + fun `completed network call logged exactly once if connection connected with unwrapped output stream`() { + executeRequest(embraceOverride = embraceUrlConnectionOverrideUnwrapped) + verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(HttpMethod.POST.name, httpMethod) + assertEquals(HTTP_OK, responseCode) + assertEquals(0L, bytesSent) + assertEquals(100L, bytesReceived) + assertNull(errorType) + } + } + + @Test + fun `incomplete network call logged exactly once and response data not accessed if connection connected`() { + executeRequest(exceptionOnInputStream = true) + verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } + verify(exactly = 0) { mockConnection.responseCode } + verify(exactly = 0) { mockConnection.contentLength } + verify(exactly = 0) { mockConnection.headerFields } + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(HttpMethod.POST.name, httpMethod) + assertNull(responseCode) + assertEquals(null, bytesSent) + assertEquals(null, bytesReceived) + assertEquals(IO_ERROR, errorType) + } + } + + @Test + fun `disconnect called with uninitialized connection results in error request capture and no response access`() { + embraceUrlConnectionOverride.disconnect() + verifyIncompleteRequestLogged() + } + + @Test + fun `incomplete network request logged when there's a failure in accessing the response content length`() { + every { mockConnection.contentLength } answers { throw TimeoutException() } + executeRequest() + verifyIncompleteRequestLogged(errorType = TIMEOUT_ERROR, noResponseAccess = false) + } + + @Test + fun `incomplete network request logged when there's a failure in accessing the response code`() { + every { mockConnection.responseCode } answers { throw TimeoutException() } + executeRequest() + verifyIncompleteRequestLogged(errorType = TIMEOUT_ERROR, noResponseAccess = false) + } + + @Test + fun `incomplete network request logged when there's a failure in accessing the response headers`() { + every { mockConnection.headerFields } answers { throw TimeoutException() } + executeRequest() + verifyIncompleteRequestLogged(errorType = TIMEOUT_ERROR, noResponseAccess = false) + } + + @Test + fun `complete network request logged when network data capture is off even if reading request body throws exception`() { + every { (mockConnection.outputStream as CountingOutputStream).requestBody } answers { throw NullPointerException() } + executeRequest() + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(HTTP_OK, responseCode) + assertNull(errorType) + } + } + + @Test + fun `check traceheaders are not forwarded by default`() { + executeRequest() + assertNull(capturedEmbraceNetworkRequest.captured.w3cTraceparent) + assertEquals(HTTP_OK, capturedEmbraceNetworkRequest.captured.responseCode) + } + + @Test + fun `check traceheaders are not forwarded on errors by default`() { + executeRequest(exceptionOnInputStream = true) + assertNull(capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(IO_ERROR, capturedEmbraceNetworkRequest.captured.errorType) + assertNull(capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check traceheaders are forwarded if feature flag is on`() { + remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + executeRequest() + assertEquals(HTTP_OK, capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) + } + + @Test + fun `check traceheaders are forwarded on errors if feature flag is on`() { + remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + executeRequest(exceptionOnInputStream = true) + assertNull(capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) + assertEquals(IO_ERROR, capturedEmbraceNetworkRequest.captured.errorType) + } + + private fun createMockConnection(): HttpsURLConnection { + val connection: HttpsURLConnection = mockk(relaxed = true) + val mockOutputStream: CountingOutputStream = mockk(relaxed = true) + every { mockOutputStream.requestBody } answers { ByteArray(1) } + every { connection.outputStream } answers { mockOutputStream } + every { connection.getRequestProperty(TRACEPARENT_HEADER_NAME) } answers { TRACEPARENT } + every { connection.requestMethod } answers { HttpMethod.POST.name } + every { connection.responseCode } answers { HTTP_OK } + every { connection.contentLength } answers { 100 } + every { connection.headerFields } answers { + mapOf( + Pair("Content-Encoding", listOf("gzip")), + Pair("Content-Length", listOf("100")), + Pair("myHeader", listOf("myValue")) + ) + } + return connection + } + + private fun executeRequest( + embraceOverride: EmbraceUrlConnectionOverride = embraceUrlConnectionOverride, + exceptionOnInputStream: Boolean = false + ) { + with(embraceOverride) { + connect() + outputStream?.write(8) + if (exceptionOnInputStream) { + every { mockConnection.inputStream } answers { throw IOException() } + assertThrows(IOException::class.java) { inputStream } + } else { + inputStream + headerFields + responseCode + } + disconnect() + } + } + + private fun verifyIncompleteRequestLogged(errorType: String = "UnknownState", noResponseAccess: Boolean = true) { + if (noResponseAccess) { + verify(exactly = 0) { mockConnection.responseCode } + verify(exactly = 0) { mockConnection.contentLength } + verify(exactly = 0) { mockConnection.headerFields } + } + verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } + assertNull(capturedEmbraceNetworkRequest.captured.responseCode) + assertEquals(errorType, capturedEmbraceNetworkRequest.captured.errorType) + } + + companion object { + private const val TRACEPARENT = "00-3c72a77a7b51af6fb3778c06d4c165ce-4c1d710fffc88e35-01" + private const val HTTP_OK = 200 + private val IO_ERROR = checkNotNull(IOException::class.java.canonicalName) + private val TIMEOUT_ERROR = checkNotNull(TimeoutException::class.java.canonicalName) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt new file mode 100644 index 0000000000..dc05ec5a62 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt @@ -0,0 +1,118 @@ +package io.embrace.android.embracesdk.network.http + +import android.os.Build.VERSION_CODES.TIRAMISU +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.Companion.TRACEPARENT_HEADER_NAME +import io.embrace.android.embracesdk.config.remote.NetworkSpanForwardingRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeNetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.mockk.CapturingSlot +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.net.URL + +@Config(sdk = [TIRAMISU]) +@RunWith(AndroidJUnit4::class) +internal class EmbraceUrlStreamHandlerTest { + private lateinit var mockEmbrace: Embrace + private lateinit var fakeConfigService: ConfigService + private lateinit var capturedEmbraceNetworkRequest: CapturingSlot + private lateinit var remoteNetworkSpanForwardingConfig: NetworkSpanForwardingRemoteConfig + + @Before + fun setup() { + mockEmbrace = mockk(relaxed = true) + capturedEmbraceNetworkRequest = slot() + remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 0f) + fakeConfigService = FakeConfigService( + networkSpanForwardingBehavior = fakeNetworkSpanForwardingBehavior( + remoteConfig = { remoteNetworkSpanForwardingConfig } + ) + ) + every { mockEmbrace.recordNetworkRequest(capture(capturedEmbraceNetworkRequest)) } answers { } + every { mockEmbrace.configService } answers { fakeConfigService } + every { mockEmbrace.generateW3cTraceparent() } answers { TRACEPARENT } + } + + @Test + fun `check traceheader is not injected into http request by default`() { + val url = URL( + "http", + "embrace.io", + 1881, + "insecure.txt", + EmbraceHttpUrlStreamHandler( + httpUrlStreamHandler, + mockEmbrace + ) + ) + val connection = checkNotNull(url.openConnection()) + assertNull(connection.getRequestProperty(TRACEPARENT_HEADER_NAME)) + } + + @Test + fun `check traceheader is not injected into https request by default`() { + val url = URL( + "https", + "embrace.io", + 1881, + "secure.txt", + EmbraceHttpsUrlStreamHandler( + httpsUrlStreamHandler, + mockEmbrace + ) + ) + val connection = checkNotNull(url.openConnection()) + assertNull(connection.getRequestProperty(TRACEPARENT_HEADER_NAME)) + } + + @Test + fun `check traceheader is injected into http request if feature flag is on`() { + remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + val url = URL( + "http", + "embrace.io", + 1881, + "insecure.txt", + EmbraceHttpUrlStreamHandler( + httpUrlStreamHandler, + mockEmbrace + ) + ) + val connection = checkNotNull(url.openConnection()) + assertEquals(TRACEPARENT, connection.getRequestProperty(TRACEPARENT_HEADER_NAME)) + } + + @Test + fun `check traceheader is injected into https request if feature flag is on`() { + remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + val url = URL( + "https", + "embrace.io", + 1881, + "secure.txt", + EmbraceHttpsUrlStreamHandler( + httpsUrlStreamHandler, + mockEmbrace + ) + ) + val connection = checkNotNull(url.openConnection()) + assertEquals(TRACEPARENT, connection.getRequestProperty(TRACEPARENT_HEADER_NAME)) + } + + companion object { + private const val TRACEPARENT = "00-3c72a77a7b51af6fb3778c06d4c165ce-4c1d710fffc88e35-01" + private val httpUrlStreamHandler = EmbraceUrlStreamHandlerFactory.newUrlStreamHandler("com.android.okhttp.HttpHandler") + private val httpsUrlStreamHandler = EmbraceUrlStreamHandlerFactory.newUrlStreamHandler("com.android.okhttp.HttpsHandler") + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureServiceTest.kt new file mode 100644 index 0000000000..cb30213edf --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkCaptureServiceTest.kt @@ -0,0 +1,217 @@ +package io.embrace.android.embracesdk.network.logging + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.BaseUrlLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.remote.NetworkCaptureRuleRemoteConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.fakeNetworkBehavior +import io.embrace.android.embracesdk.fakes.fakeSdkEndpointBehavior +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.prefs.EmbracePreferencesService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.AfterClass +import org.junit.Assert +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +internal class EmbraceNetworkCaptureServiceTest { + + companion object { + private val metadataService: FakeAndroidMetadataService = FakeAndroidMetadataService() + private val mockRemoteLogger: EmbraceRemoteLogger = mockk(relaxed = true) + private val configService: ConfigService = mockk(relaxed = true) + private val mockPreferenceService: EmbracePreferencesService = mockk(relaxed = true) + private lateinit var mockLocalConfig: LocalConfig + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockLocalConfig = + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"base_urls\": {\"data\": \"https://data.emb-api.com\"}}", + EmbraceSerializer() + ) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + private var cfg: RemoteConfig = RemoteConfig() + + @Before + fun setUp() { + clearAllMocks() + metadataService.setActiveSessionId("session-123") + every { configService.networkBehavior } returns fakeNetworkBehavior { cfg } + every { configService.sdkEndpointBehavior } returns fakeSdkEndpointBehavior { BaseUrlLocalConfig() } + } + + @Test + fun testUrlMatch() { + val regex = "httpbin.org/*".toRegex() + val url = "https://httpbin.org/get" + Assert.assertTrue(regex.containsMatchIn(url)) + } + + @Test + fun `test no capture rules`() { + val result = getService().getNetworkCaptureRules("url", "GET") + Assert.assertEquals(0, result.size) + } + + @Test + fun `test no capture for URL`() { + val rule = getDefaultRule(urlRegex = "embrace.io/*") + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + val result = getService().getNetworkCaptureRules("url", "GET") + Assert.assertEquals(0, result.size) + } + + @Test + fun `test capture rule doesn't capture Embrace endpoints`() { + val rule = getDefaultRule(urlRegex = "https://a-o0o0o.data.emb-api.com") + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + val result = getService().getNetworkCaptureRules("https://a-o0o0o.data.emb-api.com", "GET") + Assert.assertEquals(0, result.size) + } + + @Test + fun `test capture rule expires in`() { + val rule = getDefaultRule(expiresIn = 0) + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + val result = getService().getNetworkCaptureRules("https://embrace.io/changelog", "GET") + Assert.assertEquals(0, result.size) + } + + @Test + fun `test capture rule maxCount discount 1`() { + val rule = getDefaultRule() + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + every { mockPreferenceService.isNetworkCaptureRuleOver(any()) } returns false + val result = getService().getNetworkCaptureRules("https://embrace.io/changelog", "GET") + Assert.assertEquals(1, result.size) + every { mockPreferenceService.isNetworkCaptureRuleOver(any()) } returns true + val emptyRule = getService().getNetworkCaptureRules("https://embrace.io/changelog", "GET") + Assert.assertEquals(0, emptyRule.size) + } + + @Test + fun `test capture rule maxCount is over`() { + val rule = getDefaultRule() + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + every { mockPreferenceService.isNetworkCaptureRuleOver(any()) } returns true + val emptyRule = getService().getNetworkCaptureRules("https://embrace.io/changelog", "GET") + Assert.assertEquals(0, emptyRule.size) + } + + @Test + fun `test capture rule matches URL and method `() { + val rule = getDefaultRule( + method = "GET, POST", + urlRegex = "embrace.io/*" + ) + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + val result = getService().getNetworkCaptureRules("https://embrace.io/changelog", "GET") + Assert.assertTrue(result.isNotEmpty()) + } + + @Test + fun `test capture rule doesnt match URL and method `() { + val rule = getDefaultRule( + method = "POST", + urlRegex = "embrace.io/*" + ) + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + val result = getService().getNetworkCaptureRules("https://embrace.io/changelog", "GET") + Assert.assertTrue(result.isEmpty()) + } + @Test + fun `test capture rule duration`() { + // capture calls that exceeds 5000ms + val rule = getDefaultRule(duration = 5000) + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + + val service = getService() + // duration = 2000ms shouldn't be captured + service.logNetworkCapturedData( + "https://embrace.io/changelog", "GET", 200, 0, 2000, + mockk(relaxed = true) + ) + verify(exactly = 0) { mockRemoteLogger.logNetwork(any()) } + + // duration = 6000ms should be captured + service.logNetworkCapturedData( + "https://embrace.io/changelog", "GET", 200, 0, 6000, + mockk(relaxed = true) + ) + verify(exactly = 1) { mockRemoteLogger.logNetwork(any()) } + } + + @Test + fun `test capture rule status codes `() { + val rule = getDefaultRule(statusCodes = setOf(200, 404)) + cfg = RemoteConfig(networkCaptureRules = setOf(rule)) + + val service = getService() + service.logNetworkCapturedData( + "https://embrace.io/changelog", "GET", 200, 0, 2000, + mockk(relaxed = true) + ) + verify(exactly = 1) { mockRemoteLogger.logNetwork(any()) } + + service.logNetworkCapturedData( + "https://embrace.io/changelog", "GET", 404, 0, 2000, + mockk(relaxed = true) + ) + verify(exactly = 2) { mockRemoteLogger.logNetwork(any()) } + + service.logNetworkCapturedData( + "https://embrace.io/changelog", "GET", 500, 0, 2000, + mockk(relaxed = true) + ) + verify(exactly = 2) { mockRemoteLogger.logNetwork(any()) } + } + + private fun getService() = EmbraceNetworkCaptureService( + metadataService, + mockPreferenceService, + mockRemoteLogger, + configService, + EmbraceSerializer() + ) + + private fun getDefaultRule( + id: String = "123", + duration: Long = 0, + expiresIn: Long = 123, + method: String = "GET, POST", + maxSize: Long = 102400L, + maxCount: Int = 5, + statusCodes: Set = setOf(200, 404), + urlRegex: String = "embrace.io/*" + ) = + NetworkCaptureRuleRemoteConfig( + id = id, + duration = duration, + method = method, + urlRegex = urlRegex, + expiresIn = expiresIn, + maxSize = maxSize, + maxCount = maxCount, + statusCodes = statusCodes + ) +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt new file mode 100644 index 0000000000..6b4eb3e1e5 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt @@ -0,0 +1,252 @@ +package io.embrace.android.embracesdk.network.logging + +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.config.local.DomainLocalConfig +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.NetworkLocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.fakeNetworkBehavior +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.payload.NetworkSessionV2.DomainCount +import io.embrace.android.embracesdk.session.MemoryCleanerService +import io.embrace.android.embracesdk.utils.at +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +internal class EmbraceNetworkLoggingServiceTest { + private lateinit var service: EmbraceNetworkLoggingService + + companion object { + private lateinit var configService: ConfigService + private lateinit var localConfig: LocalConfig + private lateinit var memoryCleanerService: MemoryCleanerService + private lateinit var metadataService: FakeAndroidMetadataService + private lateinit var logger: InternalEmbraceLogger + private lateinit var networkCaptureService: EmbraceNetworkCaptureService + private lateinit var cfg: SdkLocalConfig + + @BeforeClass + @JvmStatic + fun beforeClass() { + configService = mockk(relaxed = true) { + every { networkBehavior } returns fakeNetworkBehavior( + localCfg = { cfg } + ) + } + localConfig = mockk(relaxed = true) + memoryCleanerService = mockk(relaxed = true) + logger = InternalEmbraceLogger() + metadataService = FakeAndroidMetadataService() + networkCaptureService = mockk(relaxed = true) + } + + @AfterClass + fun tearDown() { + unmockkAll() + } + } + + @Before + fun setUp() { + cfg = SdkLocalConfig(networking = NetworkLocalConfig()) + + clearAllMocks( + answers = false, + objectMocks = false, + constructorMocks = false, + staticMocks = false + ) + + service = + EmbraceNetworkLoggingService( + configService, + logger, + networkCaptureService + ) + } + + @Test + fun `test getNetworkCallsForSession only uses session between start and end time`() { + logNetworkCall("www.example1.com", 100, 200) + logNetworkCall("www.example2.com", 200, 300) + logNetworkCall("www.example3.com", 300, 400) + logNetworkCall("www.example4.com", 400, 500) + + val result = service.getNetworkCallsForSession(200, 301) + + // test use only session calls + assertEquals(2, result.requests.size) + assertEquals("www.example2.com", result.requests.at(0)?.url) + assertEquals("www.example3.com", result.requests.at(1)?.url) + } + + @Test + fun `test getNetworkCallsForSession over limit`() { + every { configService.networkBehavior.getNetworkCaptureLimit() }.returns( + 2 + ) + + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit2.com") + logNetworkCall("www.overLimit2.com") + logNetworkCall("www.overLimit3.com") + + val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + + // overLimit1 has 4 calls. The limit is 2. + val expectedOverLimit = DomainCount(4, 2) + assertEquals(1, result.requestCounts.size) + + assertEquals(expectedOverLimit, result.requestCounts["overLimit1.com"]) + assertNull(result.requestCounts["overLimit2.com"]) + assertNull(result.requestCounts["overLimit3.com"]) + } + + @Test + fun `test getNetworkCallsForSession merged limits`() { + cfg = SdkLocalConfig( + networking = NetworkLocalConfig( + domains = listOf(DomainLocalConfig("overLimit1.com", 2)) + ) + ) + + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit1.com") + logNetworkCall("www.overLimit2.com") + logNetworkCall("www.overLimit2.com") + logNetworkCall("www.overLimit3.com") + + val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + + // overLimit1 has 4 calls. The local limit is 2. + val expectedOverLimit = DomainCount(4, 2) + assertEquals(1, result.requestCounts.size) + + assertEquals(expectedOverLimit, result.requestCounts["overLimit1.com"]) + assertNull(result.requestCounts["overLimit2.com"]) + assertNull(result.requestCounts["overLimit3.com"]) + } + + @Test + fun logNetworkErrorTest() { + val url = "192.168.0.40:8080/test" + val httpMethod = "GET" + val startTime = 10000L + val endTime = 20000L + + service.logNetworkError( + url, + httpMethod, + startTime, + endTime, + "test", + "test", + null, + null, + null + ) + + val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + + assertEquals(url, result.requests.at(0)?.url) + } + + @Test + fun `test logNetworkCall sends the network body if necessary`() { + service.logNetworkCall( + "www.example.com", + "GET", + 200, + 10000L, + 20000L, + 1000L, + 1000L, + null, + null, + mockk(relaxed = true) + ) + + verify(exactly = 1) { + networkCaptureService.logNetworkCapturedData( + any(), + any(), + any(), + any(), + any(), + any() + ) + } + } + + @Test + fun `test logNetworkCall doesn't send the network body if null`() { + service.logNetworkCall( + "www.example.com", + "GET", + 200, + 10000L, + 20000L, + 1000L, + 1000L, + null, + null, + null + ) + + verify(exactly = 0) { + networkCaptureService.logNetworkCapturedData( + any(), + any(), + any(), + any(), + any(), + any() + ) + } + } + + @Test + fun cleanCollections() { + logNetworkCall("192.168.0.40:8080/test") + logNetworkCall("192.168.0.40:8080/test") + logNetworkCall("www.example.com") + logNetworkCall("www.example.com") + + service.cleanCollections() + + val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + + assertEquals(0, result.requests.size) + assertEquals(0, result.requestCounts.size) + } + + private fun logNetworkCall(url: String, startTime: Long = 100, endTime: Long = 200) { + service.logNetworkCall( + url, + "GET", + 200, + startTime, + endTime, + 1000L, + 1000L, + null, + null, + null + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManagerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManagerTest.kt new file mode 100644 index 0000000000..dd0a9245eb --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/NetworkCaptureEncryptionManagerTest.kt @@ -0,0 +1,149 @@ +package io.embrace.android.embracesdk.network.logging + +import android.util.Base64 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.mockk.mockk +import io.mockk.unmockkAll +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import java.security.KeyFactory +import java.security.spec.PKCS8EncodedKeySpec + +@RunWith(AndroidJUnit4::class) +internal class NetworkCaptureEncryptionManagerTest { + + private lateinit var networkCaptureEncryptionManager: NetworkCaptureEncryptionManager + + companion object { + private lateinit var mockLocalConfig: LocalConfig + + /** + * Setup before all tests get executed. Create mocks here. + */ + @BeforeClass + @JvmStatic + fun setupBeforeAll() { + mockLocalConfig = mockk() + } + + /** + * Setup after all tests get executed. Un-mock all here. + */ + @AfterClass + @JvmStatic + fun tearDownAfterAll() { + unmockkAll() + } + } + + @Before + fun setup() { + networkCaptureEncryptionManager = + NetworkCaptureEncryptionManager() + } + + @Test + fun `test encrypt and decrypt correctly`() { + val textToEncrypt = "text to encrypt" + val encryptedText = networkCaptureEncryptionManager.encrypt(textToEncrypt, sPublicKey) + + assertEquals(textToEncrypt, decrypt(encryptedText!!)) + } + + @Test + fun `test encrypt exception`() { + val textToEncrypt = "text to encrypt" + val encryptedText = networkCaptureEncryptionManager.encrypt(textToEncrypt, "12345") + + assertEquals(encryptedText, null) + } + + @Test + fun `test encrypt and decrypt long payload correctly`() { + val encryptedText = networkCaptureEncryptionManager.encrypt(encryptedPayload, sPublicKey) + + assertNotNull(decrypt(encryptedText!!)) + } + + private fun decrypt(data: String): String? { + val privateKey = KeyFactory.getInstance("RSA") + .generatePrivate(PKCS8EncodedKeySpec(Base64.decode(sPrivateKey, Base64.DEFAULT))) + return networkCaptureEncryptionManager.decrypt(data, privateKey) + } + + private val encryptedPayload = + "onilxHAND1nF2t21dktOG16FeLzICtqwSeW9IM5X4sFnuM+ixvI0mGGTuXzCTFAmISh+H0zwQte9\n" + + "OC3/+FilKZuLWGTxVHulaExzMj8tjRk6+gelfFyA+V9jaad7MQUfQGMFbs7GTVX3RbLgJtmp7giF" + + "\nThSOHmbYEw5l0C2IuHBF6O/xEIsdlMEce2fVHp8iU29KCXr8OyH5tBbXM5yb9jIqbolIiyOk2rIR" + + "\nclh/wPRQ4pUDxyLefJcnHSEh24Fj/7X3K2PvmQaKsVHMkrrVj6wc/bmrIVWCdrB54Hl+3GiJFWPi" + + "\nkTCUvBBwv8HkJP/CL3rzHf+KoilQ9DgocRUO5lSmoFyb7I+Z7iSPz7ybpW93N/b+VJ5hau+3aVUD" + + "\nH1A9fkegMXlGxJRPoJ0qT5b8zMKewBk5KAYVMpAIaXCEiAgJmiYvbquCsJ6aVHWFDgGqiEw7HcJB" + + "\nBj3INb6NVw6SY+hsLwKv3XIVwxZd+38ofUrtj1ZnH1KvxnvRFHc+aL40VneTxct0Oei4Do+dZENJ" + + "\ngSpd0d3FYJbrfjytS5ghQUcXlZUWNk787rb6NFxb3a3qtmB/bWZT15BR+vmdMSg6Xe5i//nJXT68" + + "\n134V+JdBOLxvKZ+6CqBoqSlor+rnT6lIaN9ZlPmjLKgXoAw73IxzgttpabQ+MvY4Tz6V9lCG6AsV" + + "\ngw0+D64mrhBIETXo7y2RwH3/s0VRVIbKDdFmmHDMczPLkpI5ucDSdZvwzWl1CJea1o9lnPTHzgXt" + + "\nHBRcfom9k7q8BpYzF1RNhgBjMtPQbA5Cia5hZ3ZqBfA1fuWCc27hDR8zNoRcaAl9ldxKmqruhKKX" + + "\nfa5+GWuteSrA0x3mC58yEKHtN0prRj9x7DQ1UGqMktqcep9JsL9BkQ7NqWyyg6ID4ZIarwaL1Ivr" + + "\nQqTPVzvr8ZJ8ZQkbDsu570EDUvPC6ZFKZbsq+SyYd9EXJeftmhC7qFoXKzV1Ii8Sm3kL4PveM7/T" + + "\nBRonOlkj6wpwL0Uk/aViv3iCRDeGwgesg0tCMws7sidn8GXMwM18o0+eF0qb7bKlH6yYYK4iWlrI" + + "\nPD2Bp9gb8gwrbSI2injJeG+jrUx7bT0dYJ+crHtqBJ/kY+b6OfzvXP6DxuKN+DEn71nOOO6FEEp2" + + "\nsdMqOkEfM0/1Vi/3pBL7oauMHhERXo/SJ+sT+MOCj18dtHHtXF0UZ0o8BLYvpQ0KR54iaj03ffuN" + + "\ndQk+uAysGdeXQZLq76U3bVyhRjvnivNt085qUMG1Gw672+A74ABCfq8q0nNoTPQPO9DaVc2ddYt1" + + "\nfcwf3bUcfJoy44zX00is5RJMDhbtxLs+8UP+IXslhaY3KE0WBI3tZTUNoYQy5WuCNrEhPve0obb8" + + "\nobMx+d4+0wLJ2cpXPZDyBRY0f8thYGjkblObAw8hq5Jt2ubat5Q1wS3Hlpm46VCcmT992iwYHPkq" + + "\nrNJw3cB5tai384+c6nFFLQPjEAEiUm8KOpMPgbojfR+DEKmBUfBy248VgKfTOgyxCwtPUTflEJNR" + + "\nofRMYqqDQy2KdIaORtc6dUozwK2ReJNT2Ha+W3idcNgTbug89TavGwomowqmcM8B6qfxB7Tmz5YA" + + "\nRdGXGfTK7YNf8/t3CEexJ+XGyhAXirBI8VklW15MIALspTy7y24TvvaINS6+W2bzAa4Rut6fYklI" + + "\nDh8sATxOCK4aRVZoShhbIJLBGkexdOF72kY+6HtDd5/oBtM3H7Smxjku2Q+VaELspi1Pe0xZlCSe" + + "\nmej/Yux36VzftCCfzKl/JfDV+jgIC315pp6JQLEw3ushK0vHVtasrbu2O209victMjRhJ5LFZccl" + + "\nnAhvyDMuuPzgwIiljfHLa4JhQj9MHe8cOBEsRRY+NnFvdbgXASTLc+fXEZt2eGtOMNSF03Fdw3s6" + + "\nCp4isGCX8eL0RVLENMFJVhCC+NxWLZMgUi4rWqsQp5Hm5GrrdmAbSlEMhAtHezKszsS+Ch45kGPO" + + "\nqEJHl1jfSO7NR6o2ecZUND7z1UEXCOJuLYVZxBe1q+n2Uh5FNY3uh3+xKPa6zNILr0Ew8/92mFdL" + + "\nIExHmCZLnbVbL1yLva54t2CZrmRMBI7JkRo2xogL4COSL0XVmTddUxtpjMnHPXADnlM7Q38Xa83h" + + "\nUpHOBHQTpHy0l7+qROeTE5pvw7v3vxoczl+VCy2GUabF2WgfjrPmooNZkgWfiKMf9kjSIVw2OCDA" + + "\nAzSYGgdgENmsf8tEY6W7ff775EvtbR58+V/HosSWu6B5n+K8PIWU/GMyX36CevUVtQPUmyso/" + + "\nSBjTwoK/Jezjun1cnbxcYFv62l0wv20mG+HSaSGofKb1CarM6pOzxSwvD2VxaBNleiyyXXp8MzGI" + + "\nmJuhjVRmqfN3AUCiLK4D6SJz+bGHq5U2dw==\n" + + private val sPublicKey: String = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuAZAv5tzK9Ab/DsVpNaY\n" + + "iuslKQsOHjz4N4haZLT8VaVIrlVjtkd5nPrVgEKStQf6PKnQ+1C0Tp069b6aPUkG\n" + + "22UL96nCKQ1eCIwRUT+Da7ac2YVuL21+HTs1KxLEWgN7qGy1uYNonrpsiY3XqzDv\n" + + "YMo65oFzbBV+yctuGHDFaulULJiLL8cE3/Rg3T0RfHK+C5/PqC8FBj6kn3FP9FZJ\n" + + "M4cty3nzbNWknj8r7+ikmOwma6CHEZz2u1gwPhIchNxNKuUF+4vxcBre9V/96LYO\n" + + "jSOGSDJmJN6ehUJjUpu7YSuGCki8YoLHAyoD/mYy7N/hYSeZwHiNjM+r44lZHNQT\n" + + "pwIDAQAB" + + private val sPrivateKey: String = + "MIIEpQIBAAKCAQEAuAZAv5tzK9Ab/DsVpNaYiuslKQsOHjz4N4haZLT8VaVIrlVj\n" + + "tkd5nPrVgEKStQf6PKnQ+1C0Tp069b6aPUkG22UL96nCKQ1eCIwRUT+Da7ac2YVu\n" + + "L21+HTs1KxLEWgN7qGy1uYNonrpsiY3XqzDvYMo65oFzbBV+yctuGHDFaulULJiL\n" + + "L8cE3/Rg3T0RfHK+C5/PqC8FBj6kn3FP9FZJM4cty3nzbNWknj8r7+ikmOwma6CH\n" + + "EZz2u1gwPhIchNxNKuUF+4vxcBre9V/96LYOjSOGSDJmJN6ehUJjUpu7YSuGCki8\n" + + "YoLHAyoD/mYy7N/hYSeZwHiNjM+r44lZHNQTpwIDAQABAoIBAQCP8ww5Fd9cmVka\n" + + "0BkZLWh72n7iASzVCHpd7kJPXqe4Uydsf40VLAn8etYBk5HxHEFprKi1viadDC7v\n" + + "xl4erH45pmxbGiawOC2jX/W36Yfi/SDqoo5TeUHamdL4U6DWjLzxPcBVUm7HIyr9\n" + + "2r+mwQuvWeIDJ6XjGVlpfsErSyOSgYh/XTCa1hawSRT5PAfOifOKMwF0Yqr5fXZY\n" + + "BWTjm3M/PV+kamF118/Y+DZeEr6liG/OyIeWGcnv0IqxxxDukMbqZLuSZo9t1wEZ\n" + + "lTganTsS5avNfrMuL29YQjdzmU2p471ZM381Z+a5sqD/T/+mpnv05Ae8DvgMJo7c\n" + + "EE68r305AoGBAORFzo4iGNIVsCjRpSkMeQXN4UOG3Tv3orKJaMlas101lMQwHmu6\n" + + "Ui8PqUK5rz8ziZvLyPJjo8lSbsVYO/RH9BxwJOIBxHbQTYTfSQyBeW5dom3LNN1k\n" + + "xEXiGPqQcn/rGPYVCJffImQYE2UbMad6ju0qg2YHW0CWecTE5zC9nRl9AoGBAM5g\n" + + "iTiK8/UL7yusph6lTxiNnDZztEskITE7xY5mda4VBkckRMO8J6hEyLMnbWvi645M\n" + + "ckCoeTRklwJ4AdaSzKXdbFywIxVkB+ciYJ76juC7pIRmgaNaKgx0og1wtzc4e9KQ\n" + + "Uch+2MIHkgfPQrY/vBmS+rBqE4cK/H8LLqpSbgrzAoGBAKQtLLz++vkGDjedaHsY\n" + + "dGZfR3eIpM8/cK2VtF61NDGCmudrcEWssPUV/3d1EvyStZLuwyzJyv+9oNugdSZh\n" + + "JcnaQjymZsXJVSeOa/xplotxHqR2tSPSGHPmhG6ZuzATR1WdlRudqR9yTWi3YUQC\n" + + "Go+qtuyHt/LBBv0lXN2qUjYFAoGAUw4oy1eonJrj8zi1VioDLgd3sbZY/dCZhx3e\n" + + "ANQdUiTl9OWUww1LDH46I1efwsZ9NDRx2rGyrbI5z+WKH9fOgoYdISRFyksKnyuH\n" + + "pRODQtBhgmNakuord/3MZgpRweh6dKBeOYlLJLM1Qu1XlM8LnWM4fp0CJNv4CAzx\n" + + "B9zKqp8CgYEAysNuHbhUCkr0ZuUjPaKmZegxjyiqkrahNSZJjVQJfmicTwJBlqAv\n" + + "Z10/PLpJ7dXNS1DdHT5hLXczCd/9BA9EsmiU6Ny/TshHw2BcSBLfQBS7KYEh/+cQ\n" + + "D256kKbCmQKlNYs3bZEuAcVsrBabIPNIfxMzc+bdxUsW+fTw8DP8zzE=" +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/networking/EmbraceUrlAdapterTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/networking/EmbraceUrlAdapterTest.kt new file mode 100644 index 0000000000..614408b460 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/networking/EmbraceUrlAdapterTest.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.networking + +import com.google.gson.GsonBuilder +import io.embrace.android.embracesdk.comms.api.EmbraceUrl +import io.embrace.android.embracesdk.comms.api.EmbraceUrlAdapter +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class EmbraceUrlAdapterTest { + + @Test + fun `test EmbraceUrl serialization`() { + val embraceUrl = EmbraceUrl.getUrl("http://fake.url") + + val gson = + GsonBuilder().registerTypeAdapter(EmbraceUrl::class.java, EmbraceUrlAdapter()).create() + val jsonStr = gson.toJson(embraceUrl, EmbraceUrl::class.java) + val serialized = gson.fromJson(jsonStr, EmbraceUrl::class.java) + + assertEquals(embraceUrl.toString(), serialized.toString()) + } + + @Test + fun `test null EmbraceUrl serialization`() { + val gson = + GsonBuilder().registerTypeAdapter(EmbraceUrl::class.java, EmbraceUrlAdapter()).create() + val jsonStr = gson.toJson(null, EmbraceUrl::class.java) + val serialized = gson.fromJson(jsonStr, EmbraceUrl::class.java) + + assertNull(serialized) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessageTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessageTest.kt new file mode 100644 index 0000000000..adf1a68caf --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityMessageTest.kt @@ -0,0 +1,61 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData +import io.opentelemetry.api.trace.StatusCode +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class BackgroundActivityMessageTest { + + private val backgroundActivity = BackgroundActivity("fake-activity", 0, "") + private val userInfo = UserInfo("fake-user-id") + private val appInfo = AppInfo("fake-app-id") + private val deviceInfo = DeviceInfo("fake-manufacturer") + private val breadcrumbs = Breadcrumbs( + customBreadcrumbs = listOf(CustomBreadcrumb("fake-breadcrumb", 1)) + ) + private val spans = listOf(EmbraceSpanData("fake-span-id", "", "", "", 0, 0, StatusCode.OK)) + private val perfInfo = PerformanceInfo(DiskUsage(1, 2)) + + private val info = BackgroundActivityMessage( + backgroundActivity, + userInfo, + appInfo, + deviceInfo, + perfInfo, + breadcrumbs, + spans + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("bg_activity_message_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("bg_activity_message_expected.json") + val obj = Gson().fromJson(json, BackgroundActivityMessage::class.java) + assertNotNull(obj) + + assertEquals(backgroundActivity.startTime, obj.backgroundActivity.startTime) + assertEquals(userInfo, obj.userInfo) + assertEquals(appInfo, obj.appInfo) + assertEquals(deviceInfo, obj.deviceInfo) + assertEquals(perfInfo, obj.performanceInfo) + assertEquals(breadcrumbs, obj.breadcrumbs) + assertEquals(spans, obj.spans) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", BackgroundActivityMessage::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityTest.kt new file mode 100644 index 0000000000..30faa188dd --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BackgroundActivityTest.kt @@ -0,0 +1,81 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class BackgroundActivityTest { + + private val info = BackgroundActivity( + sessionId = "fake-session-id", + startTime = 123456789L, + appState = "foreground", + endTime = 987654321L, + number = 5, + messageType = "fake-message-type", + lastHeartbeatTime = 123456780L, + isColdStart = true, + eventIds = listOf("fake-event-id"), + infoLogIds = listOf("fake-info-id"), + warningLogIds = listOf("fake-warn-id"), + errorLogIds = listOf("fake-err-id"), + infoLogsAttemptedToSend = 1, + warnLogsAttemptedToSend = 2, + errorLogsAttemptedToSend = 3, + exceptionError = ExceptionError(false), + crashReportId = "fake-crash-id", + endType = BackgroundActivity.LifeEventType.BKGND_STATE, + startType = BackgroundActivity.LifeEventType.BKGND_STATE, + properties = mapOf("fake-key" to "fake-value"), + unhandledExceptions = 1, + user = UserInfo("fake-user-id", "fake-user-name") + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("bg_activity_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("bg_activity_expected.json") + val obj = Gson().fromJson(json, BackgroundActivity::class.java) + assertNotNull(obj) + + with(obj) { + assertEquals("fake-session-id", sessionId) + assertEquals(123456789L, startTime) + assertEquals(987654321L, endTime) + assertEquals(5, number) + assertEquals("foreground", appState) + assertEquals("fake-message-type", messageType) + assertEquals(123456780L, lastHeartbeatTime) + assertTrue(checkNotNull(isColdStart)) + assertEquals(listOf("fake-event-id"), eventIds) + assertEquals(listOf("fake-info-id"), infoLogIds) + assertEquals(listOf("fake-warn-id"), warningLogIds) + assertEquals(listOf("fake-err-id"), errorLogIds) + assertEquals(1, infoLogsAttemptedToSend) + assertEquals(2, warnLogsAttemptedToSend) + assertEquals(3, errorLogsAttemptedToSend) + assertEquals("fake-crash-id", crashReportId) + assertEquals(BackgroundActivity.LifeEventType.BKGND_STATE, endType) + assertEquals(BackgroundActivity.LifeEventType.BKGND_STATE, startType) + assertEquals(1, unhandledExceptions) + assertEquals(ExceptionError(false), exceptionError) + assertEquals(mapOf("fake-key" to "fake-value"), properties) + } + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", BackgroundActivity::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BreadcrumbsTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BreadcrumbsTest.kt new file mode 100644 index 0000000000..0fe5e55acb --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/BreadcrumbsTest.kt @@ -0,0 +1,75 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class BreadcrumbsTest { + + private val info = Breadcrumbs( + viewBreadcrumbs = listOf(ViewBreadcrumb("View", 1600000000)), + tapBreadcrumbs = + listOf( + TapBreadcrumb( + null, + "Tap", + 1600000000, + TapBreadcrumb.TapBreadcrumbType.TAP + ) + ), + customBreadcrumbs = listOf(CustomBreadcrumb("Custom", 1600000000)), + webViewBreadcrumbs = listOf(WebViewBreadcrumb("WebView", 1600000000)), + fragmentBreadcrumbs = listOf(FragmentBreadcrumb("Fragment", 1600000000, 1600005000)), + rnActionBreadcrumbs = listOf( + RnActionBreadcrumb( + "RnAction", + 1600000000, + 1600005000, + emptyMap(), + 0, + "output" + ) + ), + pushNotifications = listOf( + PushNotificationBreadcrumb( + "PushNotification", + "body", + "from", + "id", + null, + null, + 1600000000 + ) + ) + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("breadcrumbs_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("breadcrumbs_expected.json") + val obj = Gson().fromJson(json, Breadcrumbs::class.java) + assertNotNull(obj) + assertNotNull(obj.viewBreadcrumbs?.single()) + assertNotNull(obj.customBreadcrumbs?.single()) + assertNotNull(obj.fragmentBreadcrumbs?.single()) + assertNotNull(obj.tapBreadcrumbs?.single()) + assertNotNull(obj.rnActionBreadcrumbs?.single()) + assertNotNull(obj.pushNotifications?.single()) + assertNotNull(obj.webViewBreadcrumbs?.single()) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", Breadcrumbs::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/CustomBreadcrumbTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/CustomBreadcrumbTest.kt new file mode 100644 index 0000000000..c43f3d30a7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/CustomBreadcrumbTest.kt @@ -0,0 +1,37 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class CustomBreadcrumbTest { + + private val info = CustomBreadcrumb( + "test", + 1600000000 + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("custom_breadcrumb_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("custom_breadcrumb_expected.json") + val obj = Gson().fromJson(json, CustomBreadcrumb::class.java) + assertEquals("test", obj.message) + assertEquals(1600000000, obj.getStartTime()) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", CustomBreadcrumb::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/EmbraceEventMessageTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/EmbraceEventMessageTest.kt new file mode 100644 index 0000000000..723338e81e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/EmbraceEventMessageTest.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.LogExceptionType +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class EmbraceEventMessageTest { + + private val eventComplete = Event( + eventId = "eventId", + sessionId = "sessionId", + messageId = "messageId", + name = "test", + timestamp = 1111L, + type = EmbraceEvent.Type.WARNING_LOG, + logExceptionType = LogExceptionType.NONE.value, + screenshotTaken = false, + appState = "active", + customProperties = mapOf("Float" to 1, "String" to "TestString"), + sessionProperties = mapOf() + ) + + private val eventMessage = EventMessage( + event = eventComplete, + performanceInfo = PerformanceInfo( + diskUsage = DiskUsage(appDiskUsage = null, deviceDiskFree = 3862863872L), + powerSaveModeIntervals = listOf(PowerModeInterval(startTime = 1679580212117L)), + ), + userInfo = UserInfo(personas = setOf("first_day")), + version = 13 + ) + + @Test + fun testMandatoryValues() { + assertNotNull(eventMessage.event) + } + + @Test + fun testSerialization() { + val data = ResourceReader.readResourceAsText("eventmessage_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(eventMessage) + assertEquals(data, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("eventmessage_expected.json") + val obj = Gson().fromJson(json, EventMessage::class.java) + assertEquals("eventId", obj.event.eventId) + assertEquals("sessionId", obj.event.sessionId) + assertEquals("messageId", obj.event.messageId) + + assertEquals(13, obj.version) + assertEquals(3862863872L, obj.performanceInfo?.diskUsage?.deviceDiskFree) + assertEquals(1679580212117L, obj.performanceInfo?.powerSaveModeIntervals?.get(0)?.startTime) + assertEquals(1, obj.userInfo?.personas?.size) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/ExceptionErrorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/ExceptionErrorTest.kt new file mode 100644 index 0000000000..66ccb9f284 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/ExceptionErrorTest.kt @@ -0,0 +1,59 @@ +package io.embrace.android.embracesdk.payload + +import io.embrace.android.embracesdk.fakes.FakeClock +import io.mockk.unmockkAll +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.Test + +internal class ExceptionErrorTest { + + companion object { + private lateinit var exceptionError: ExceptionError + private lateinit var clock: FakeClock + + @BeforeClass + @JvmStatic + fun beforeClass() { + clock = FakeClock() + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @Test + fun `test addException with strict mode disabled has a limit of 5 exceptions`() { + exceptionError = ExceptionError(false) + val throwable = Throwable("exceptions") + exceptionError.addException(throwable, "state", clock) + exceptionError.addException(throwable, "state", clock) + exceptionError.addException(throwable, "state", clock) + exceptionError.addException(throwable, "state", clock) + exceptionError.addException(throwable, "state", clock) + exceptionError.addException(throwable, "state", clock) + + assertEquals(exceptionError.exceptionErrors.size, 5) + assertEquals(exceptionError.occurrences, 6) + } + + @Test + fun `test addException with strict mode enabled has a limit of 50 exceptions`() { + exceptionError = ExceptionError(true) + val throwable = Throwable("exceptions") + + repeat(50) { + exceptionError.addException(throwable, "state", clock) + } + + assertEquals(exceptionError.exceptionErrors.size, 50) + assertEquals(exceptionError.occurrences, 50) + exceptionError.addException(throwable, "state", clock) + assertEquals(exceptionError.exceptionErrors.size, 50) + assertEquals(exceptionError.occurrences, 51) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataErrorTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataErrorTest.kt new file mode 100644 index 0000000000..b4d721bcbd --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataErrorTest.kt @@ -0,0 +1,38 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class NativeCrashDataErrorTest { + + private val info = + NativeCrashDataError( + 5, + 2 + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("native_crash_data_error_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("native_crash_data_error_expected.json") + val obj = Gson().fromJson(json, NativeCrashDataError::class.java) + assertEquals(5, obj.number) + assertEquals(2, obj.context) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", NativeCrashDataError::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataTest.kt new file mode 100644 index 0000000000..3a0bf6af1c --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashDataTest.kt @@ -0,0 +1,63 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class NativeCrashDataTest { + + private val info = NativeCrashData( + "report_id", + "sid", + 1610000000000, + "app_state", + NativeCrashMetadata( + AppInfo(), + DeviceInfo(), + UserInfo(), + emptyMap() + ), + 2, + "crash", + mapOf("key" to "value"), + listOf( + NativeCrashDataError( + 5, + 2 + ) + ), + "map" + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("native_crash_data_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("native_crash_data_expected.json") + val obj = Gson().fromJson(json, NativeCrashData::class.java) + assertEquals("report_id", obj.nativeCrashId) + assertEquals("sid", obj.sessionId) + assertEquals(1610000000000, obj.timestamp) + assertEquals("app_state", obj.appState) + assertNotNull(obj.metadata) + assertEquals(2, obj.unwindError) + assertNotNull(obj.getCrash()) + assertEquals(mapOf("key" to "value"), obj.symbols) + assertEquals(1, obj.errors?.size) + assertEquals("map", obj.map) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", NativeCrashData::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashMetadataTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashMetadataTest.kt new file mode 100644 index 0000000000..c9aca0eb88 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashMetadataTest.kt @@ -0,0 +1,52 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class NativeCrashMetadataTest { + + private val info = NativeCrashMetadata( + AppInfo("1.0"), + DeviceInfo("samsung"), + UserInfo("123"), + mapOf("key" to "value"), + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("native_crash_metadata_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("native_crash_metadata_expected.json") + val obj = Gson().fromJson(json, NativeCrashMetadata::class.java) + verifyInfoPopulated(obj) + } + + @Test + fun testToJsonImpl() { + val json = info.toJson() + val obj = Gson().fromJson(json, NativeCrashMetadata::class.java) + verifyInfoPopulated(obj) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", NativeCrashMetadata::class.java) + assertNotNull(info) + } + + private fun verifyInfoPopulated(obj: NativeCrashMetadata) { + assertEquals("1.0", obj.appInfo.appVersion) + assertEquals("samsung", obj.deviceInfo.manufacturer) + assertEquals("123", obj.userInfo.userId) + assertEquals("value", checkNotNull(obj.sessionProperties)["key"]) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashTest.kt new file mode 100644 index 0000000000..a7a94afe61 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeCrashTest.kt @@ -0,0 +1,53 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class NativeCrashTest { + + private val info = NativeCrash( + "id", + "crashMessage", + mapOf("key" to "value"), + listOf( + NativeCrashDataError( + 5, + 2 + ) + ), + 2, + "map" + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("native_crash_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("native_crash_expected.json") + val obj = Gson().fromJson(json, NativeCrash::class.java) + assertEquals("id", obj.id) + assertEquals("crashMessage", obj.crashMessage) + assertEquals(mapOf("key" to "value"), obj.symbols) + assertEquals(2, obj.unwindError) + assertEquals("map", obj.map) + + val err = obj.errors?.single() + assertEquals(5, err?.number) + assertEquals(2, err?.context) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", NativeCrash::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeSymbolsTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeSymbolsTest.kt new file mode 100644 index 0000000000..33ce478303 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/NativeSymbolsTest.kt @@ -0,0 +1,66 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class NativeSymbolsTest { + + private val armv7Symbols = mapOf( + "libfoo-armeabi-v7a.so" to "0x1234", + "libbar-armeabi-v7a.so" to "0x5678" + ) + + private val x86Symbols = mapOf( + "libfoo-x86.so" to "0x1234", + "libbar-x86.so" to "0x5678" + ) + + private val symbols = NativeSymbols( + mutableMapOf( + "armeabi-v7a" to armv7Symbols, + "x86" to x86Symbols + ) + ) + + @Test + fun testGetInvalidArch() { + assertEquals(emptyMap(), symbols.getSymbolByArchitecture(null)) + assertEquals(emptyMap(), symbols.getSymbolByArchitecture("")) + assertEquals(emptyMap(), symbols.getSymbolByArchitecture("foo")) + } + + @Test + fun testGetSymbolByArchitecture() { + assertEquals(armv7Symbols, symbols.getSymbolByArchitecture("arm64-v8a")) + assertEquals(armv7Symbols, symbols.getSymbolByArchitecture("armeabi-v7a")) + assertEquals(x86Symbols, symbols.getSymbolByArchitecture("x86")) + assertEquals(x86Symbols, symbols.getSymbolByArchitecture("x86_64")) + } + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("native_symbols_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(symbols) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("native_symbols_expected.json") + val obj = Gson().fromJson(json, NativeSymbols::class.java) + assertEquals(armv7Symbols, obj.getSymbolByArchitecture("arm64-v8a")) + assertEquals(armv7Symbols, obj.getSymbolByArchitecture("armeabi-v7a")) + assertEquals(x86Symbols, obj.getSymbolByArchitecture("x86")) + assertEquals(x86Symbols, obj.getSymbolByArchitecture("x86_64")) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", NativeSymbols::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumbTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumbTest.kt new file mode 100644 index 0000000000..2b77584ead --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/RnActionBreadcrumbTest.kt @@ -0,0 +1,45 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class RnActionBreadcrumbTest { + + private val info = RnActionBreadcrumb( + "my_action", + 1600000000, + 1600000100, + mapOf("key" to "value"), + 104, + "test" + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("rn_action_breadcrumb_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("rn_action_breadcrumb_expected.json") + val obj = Gson().fromJson(json, RnActionBreadcrumb::class.java) + assertEquals("my_action", obj.name) + assertEquals(1600000000, obj.getStartTime()) + assertEquals(1600000100, obj.endTime) + assertEquals(mapOf("key" to "value"), obj.properties) + assertEquals(104, obj.bytesSent) + assertEquals("test", obj.output) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", RnActionBreadcrumb::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/SessionMessageTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/SessionMessageTest.kt new file mode 100644 index 0000000000..d25be9f2fb --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/SessionMessageTest.kt @@ -0,0 +1,72 @@ +package io.embrace.android.embracesdk.payload + +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData +import io.opentelemetry.api.trace.StatusCode +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class SessionMessageTest { + + private val session = fakeSession() + private val userInfo = UserInfo("fake-user-id", "fake-user-name") + private val appInfo = AppInfo("fake-app-version") + private val deviceInfo = DeviceInfo("fake-manufacturer") + private val performanceInfo = PerformanceInfo(DiskUsage(150923409L, 509209823L)) + private val breadcrumbs = Breadcrumbs( + customBreadcrumbs = listOf(CustomBreadcrumb("Hi", 109234098234)) + ) + private val spans = listOf( + EmbraceSpanData( + "fake-span-id", + "", + null, + "", + 0, + 0, + StatusCode.OK, + emptyList() + ) + ) + + private val info = SessionMessage( + session, + userInfo, + appInfo, + deviceInfo, + performanceInfo, + breadcrumbs, + spans + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("session_message_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("session_message_expected.json") + val obj = Gson().fromJson(json, SessionMessage::class.java) + assertNotNull(obj) + assertEquals(session, obj.session) + assertEquals(userInfo, obj.userInfo) + assertEquals(appInfo, obj.appInfo) + assertEquals(deviceInfo, obj.deviceInfo) + assertEquals(performanceInfo, obj.performanceInfo) + assertEquals(breadcrumbs, obj.breadcrumbs) + assertEquals(spans, obj.spans) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", SessionMessage::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/TapBreadcrumbTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/TapBreadcrumbTest.kt new file mode 100644 index 0000000000..94708a55e9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/payload/TapBreadcrumbTest.kt @@ -0,0 +1,42 @@ +package io.embrace.android.embracesdk.payload + +import android.util.Pair +import com.google.gson.Gson +import io.embrace.android.embracesdk.ResourceReader +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +internal class TapBreadcrumbTest { + + private val info = TapBreadcrumb( + Pair(0f, 0f), + "tappedElementName", + 1600000000, + TapBreadcrumb.TapBreadcrumbType.TAP + ) + + @Test + fun testSerialization() { + val expectedInfo = ResourceReader.readResourceAsText("tap_breadcrumb_expected.json") + .filter { !it.isWhitespace() } + val observed = Gson().toJson(info) + assertEquals(expectedInfo, observed) + } + + @Test + fun testDeserialization() { + val json = ResourceReader.readResourceAsText("tap_breadcrumb_expected.json") + val obj = Gson().fromJson(json, TapBreadcrumb::class.java) + assertEquals("0,0", obj.location) + assertEquals("tappedElementName", obj.tappedElementName) + assertEquals(1600000000, obj.getStartTime()) + assertEquals(TapBreadcrumb.TapBreadcrumbType.TAP, obj.type) + } + + @Test + fun testEmptyObject() { + val info = Gson().fromJson("{}", TapBreadcrumb::class.java) + assertNotNull(info) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesServiceTest.kt new file mode 100644 index 0000000000..6dbfd22afc --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/prefs/EmbracePreferencesServiceTest.kt @@ -0,0 +1,316 @@ +package io.embrace.android.embracesdk.prefs + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.concurrency.BlockableExecutorService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class EmbracePreferencesServiceTest { + + private lateinit var prefs: SharedPreferences + private lateinit var service: EmbracePreferencesService + private lateinit var fakeClock: FakeClock + + private val executorService = BlockableExecutorService() + private val context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + prefs = PreferenceManager.getDefaultSharedPreferences(context) + fakeClock = FakeClock() + service = EmbracePreferencesService( + executorService, + lazy { prefs }, + fakeClock, + EmbraceSerializer() + ) + } + + /** + * Asserts that the startup state preference is updated when creating a PreferenceService. + */ + @Test + @Throws(InterruptedException::class) + fun testStartupStatePersistence() { + // move through two states: startup in progress + startup complete. + assertEquals( + "startup_entered", + service.sdkStartupStatus + ) + + service.applicationStartupComplete() + + // assert the entered startup and completed startup preferences were set + assertEquals( + "startup_completed", + service.sdkStartupStatus + ) + } + + @Test + fun `test app version is saved`() { + assertNull(service.appVersion) + + val appVersion = "1.1" + service.appVersion = appVersion + assertEquals(appVersion, service.appVersion) + } + + @Test + fun `test OS version is saved`() { + assertNull(service.osVersion) + + val osVersion = "12.1" + service.osVersion = osVersion + assertEquals(osVersion, service.osVersion) + } + + @Test + fun `test install date is saved`() { + assertNull(service.installDate) + + val installDate = 20221229L + service.installDate = installDate + assertEquals(installDate, service.installDate) + + service.installDate = -1 + assertNull(service.installDate) + } + + @Test + fun `test device identifier is saved`() { + val deviceIdentifier = "android" + service.deviceIdentifier = deviceIdentifier + assertEquals(deviceIdentifier, service.deviceIdentifier) + } + + @Test + fun `test device identifier is created`() { + assertNotNull(service.deviceIdentifier) + } + + @Test + fun `test sdk disabled is saved`() { + assertFalse(service.sdkDisabled) + service.sdkDisabled = true + assertTrue(service.sdkDisabled) + } + + @Test + fun `test user payer is saved`() { + assertFalse(service.userPayer) + service.userPayer = true + assertTrue(service.userPayer) + } + + @Test + fun `test user identifier is saved`() { + assertNull(service.userIdentifier) + + val userIdentifier = "userId" + service.userIdentifier = userIdentifier + assertEquals(userIdentifier, service.userIdentifier) + + service.userIdentifier = null + assertNull(service.userIdentifier) + } + + @Test + fun `test user email is saved`() { + assertNull(service.userEmailAddress) + + val email = "example@embrace.io" + service.userEmailAddress = email + assertEquals(email, service.userEmailAddress) + } + + @Test + fun `test user personas is saved`() { + assertNull(service.userPersonas) + + val list = setOf("persona1", "persona2") + service.userPersonas = list + assertEquals(list, service.userPersonas) + } + + @Test + fun `test permanent session properties are saved`() { + assertNull(service.permanentSessionProperties) + + val map = mapOf("property1" to "1", "property2" to "2") + service.permanentSessionProperties = map + assertEquals(map, service.permanentSessionProperties) + + service.permanentSessionProperties = null + assertNull(service.permanentSessionProperties) + } + + @Test + fun `test deprecated custom personas`() { + assertNull(service.customPersonas) + } + + @Test + fun `test username is saved`() { + assertNull(service.username) + + val username = "username" + service.username = username + assertEquals(username, service.username) + } + + @Test + fun `test last config fetch date is saved`() { + assertNull(service.lastConfigFetchDate) + service.lastConfigFetchDate = 1234L + assertEquals(1234L, service.lastConfigFetchDate) + } + + @Test + fun `test user message needs retry is saved`() { + assertFalse(service.userMessageNeedsRetry) + service.userMessageNeedsRetry = true + assertTrue(service.userMessageNeedsRetry) + } + + @Test + fun `test session number is saved`() { + assertEquals(0, service.sessionNumber) + + service.sessionNumber = 1234 + assertEquals(1234, service.sessionNumber) + + service.sessionNumber = -1 + assertEquals(0, service.sessionNumber) + } + + @Test + fun `test java script bundle url is saved`() { + assertNull(service.javaScriptBundleURL) + + val url = "http://url.com" + service.javaScriptBundleURL = url + assertEquals(url, service.javaScriptBundleURL) + } + + @Test + fun `test java script patch number is saved`() { + assertNull(service.javaScriptPatchNumber) + + val patchNumber = "1234" + service.javaScriptPatchNumber = patchNumber + assertEquals(patchNumber, service.javaScriptPatchNumber) + } + + @Test + fun `test react native embrace sdk version is saved`() { + assertNull(service.reactNativeVersionNumber) + + val version = "1.1.1" + service.rnSdkVersion = version + assertEquals(version, service.rnSdkVersion) + } + + @Test + fun `test react native version is saved`() { + assertNull(service.reactNativeVersionNumber) + + val version = "1.1.1" + service.reactNativeVersionNumber = version + assertEquals(version, service.reactNativeVersionNumber) + } + + @Test + fun `test unity version is saved`() { + assertNull(service.unityVersionNumber) + + val version = "1.1.1" + service.unityVersionNumber = version + assertEquals(version, service.unityVersionNumber) + } + + @Test + fun `test unity build number is saved`() { + assertNull(service.unityBuildIdNumber) + + val buildNumber = "10" + service.unityBuildIdNumber = buildNumber + assertEquals(buildNumber, service.unityBuildIdNumber) + } + + @Test + fun `test unity sdk version is saved`() { + assertNull(service.unitySdkVersionNumber) + + val version = "1.1.1" + service.unitySdkVersionNumber = version + assertEquals(version, service.unitySdkVersionNumber) + } + + @Test + fun `test is jail broken is saved`() { + assertNull(service.jailbroken) + service.jailbroken = true + assertTrue(service.jailbroken!!) + } + + @Test + fun `test screen resolution is saved`() { + assertNull(service.screenResolution) + val resolution = "1000x2000" + service.screenResolution = resolution + assertEquals(resolution, service.screenResolution) + } + + @Test + fun `test dart sdk version is saved`() { + assertNull(service.dartSdkVersion) + + val version = "2.1.2" + service.dartSdkVersion = version + assertEquals(version, service.dartSdkVersion) + } + + @Test + fun `test flutter sdk version is saved`() { + assertNull(service.embraceFlutterSdkVersion) + + val version = "3.1.2" + service.embraceFlutterSdkVersion = version + assertEquals(version, service.embraceFlutterSdkVersion) + } + + @Test + fun `test background activity enabled is saved`() { + assertFalse(service.backgroundActivityEnabled) + + val expected = true + service.backgroundActivityEnabled = true + assertEquals(expected, service.backgroundActivityEnabled) + } + + @Test + fun `test is users first day`() { + assertFalse(service.isUsersFirstDay()) + + service.installDate = 0L + fakeClock.setCurrentTime(PreferencesService.DAY_IN_MS + 1) + assertFalse(service.isUsersFirstDay()) + + fakeClock.setCurrentTime(PreferencesService.DAY_IN_MS - 1) + assertTrue(service.isUsersFirstDay()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/registry/ServiceRegistryTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/registry/ServiceRegistryTest.kt new file mode 100644 index 0000000000..d1d897ac33 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/registry/ServiceRegistryTest.kt @@ -0,0 +1,81 @@ +package io.embrace.android.embracesdk.registry + +import io.embrace.android.embracesdk.config.ConfigListener +import io.embrace.android.embracesdk.config.ConfigService +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeMemoryCleanerService +import io.embrace.android.embracesdk.session.ActivityListener +import io.embrace.android.embracesdk.session.MemoryCleanerListener +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.Closeable + +internal class ServiceRegistryTest { + + @Test + fun testServiceRegistration() { + val registry = ServiceRegistry() + val service = FakeService() + val obj = "test_obj" + registry.registerServices(service, obj) + + val expected = listOf(service) + assertEquals(expected, registry.closeables) + assertEquals(expected, registry.activityListeners) + assertEquals(expected, registry.memoryCleanerListeners) + assertEquals(expected, registry.configListeners) + } + + @Test + fun testListeners() { + val registry = ServiceRegistry() + val service = FakeService() + registry.registerService(service) + val expected = listOf(service) + + val activityService = FakeActivityService() + registry.registerActivityListeners(activityService) + assertEquals(expected, activityService.listeners) + + val memoryCleanerService = FakeMemoryCleanerService() + registry.registerMemoryCleanerListeners(memoryCleanerService) + assertEquals(expected, memoryCleanerService.listeners) + + val configService = FakeConfigService() + registry.registerConfigListeners(configService) + assertEquals(expected, configService.listeners.toList()) + + assertFalse(service.closed) + registry.close() + assertTrue(service.closed) + } + + @Test(expected = IllegalStateException::class) + fun testClosedRegistration() { + val registry = ServiceRegistry() + registry.closeRegistration() + registry.registerService(FakeService()) + } + + private class FakeService : + Closeable, + ConfigListener, + MemoryCleanerListener, + ActivityListener { + + var closed = false + + override fun close() { + closed = true + } + + override fun cleanCollections() { + } + + override fun onConfigChange(configService: ConfigService) { + } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesTest.kt new file mode 100644 index 0000000000..2d492574ce --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/EmbraceCrashSamplesTest.kt @@ -0,0 +1,73 @@ +package io.embrace.android.embracesdk.samples + +import io.embrace.android.embracesdk.Embrace +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.AfterClass +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +internal class EmbraceCrashSamplesTest { + + companion object { + private lateinit var crashSampleTest: EmbraceCrashSamples + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(Embrace::class) + crashSampleTest = EmbraceCrashSamples + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @Before + fun setup() { + every { Embrace.getInstance().configService } returns mockk(relaxed = true) + } + + @Test + fun `test isSdkStarted with isStarted false throws EmbraceNotInitializedException`() { + every { Embrace.getInstance().isStarted } returns false + assertThrows(EmbraceSampleCodeException::class.java) { crashSampleTest.isSdkStarted() } + } + + @Test + fun `test checkAnrDetectionEnabled throws EmbraceAnrDisabledException if isAnrCaptureEnabled is false`() { + every { Embrace.getInstance().configService?.anrBehavior?.isAnrCaptureEnabled() } returns false + assertThrows(EmbraceSampleCodeException::class.java) { crashSampleTest.checkAnrDetectionEnabled() } + } + + @Test + fun `test checkNdkDetectionEnabled throws EmbraceNotInitializedException if Embrace isStarted is false`() { + every { Embrace.getInstance().isStarted } returns false + assertThrows(EmbraceSampleCodeException::class.java) { crashSampleTest.checkNdkDetectionEnabled() } + } + + @Test + fun `test checkNdkDetectionEnabled with isNdkEnabled false throws EmbraceNdkDisabledException`() { + every { Embrace.getInstance().isStarted } returns true + assertThrows(EmbraceSampleCodeException::class.java) { crashSampleTest.checkNdkDetectionEnabled() } + } + + @Test + fun `test throwJvmException actually throws EmbraceNotInitializedException if Embrace isStarted is false`() { + every { Embrace.getInstance().isStarted } returns false + assertThrows(EmbraceSampleCodeException::class.java) { crashSampleTest.throwJvmException() } + } + + @Test + fun `test throwJvmException actually throws EmbraceNotInitializedException if Embrace is initialized`() { + every { Embrace.getInstance().isStarted } returns true + assertThrows(EmbraceSampleCodeException::class.java) { crashSampleTest.throwJvmException() } + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/VersionTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/VersionTest.kt new file mode 100644 index 0000000000..1a3a58a61e --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/samples/VersionTest.kt @@ -0,0 +1,81 @@ +package io.embrace.android.embracesdk.samples + +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class VersionTest { + + @Test + fun `compare versions`() { + assertTrue( + ComparableVersion("5.2.1-beta01") < ComparableVersion( + "5.2.1" + ) + ) + assertTrue( + ComparableVersion("5.2.1") > ComparableVersion( + "5.2.1-beta01" + ) + ) + assertTrue( + ComparableVersion("5.2.1").compareTo( + ComparableVersion("5.2.1") + ) == 0 + ) + assertTrue( + ComparableVersion("5.3.0") > ComparableVersion( + "5.2.1-beta01" + ) + ) + assertTrue( + ComparableVersion("5.2.1-beta01") < ComparableVersion( + "5.2.1-beta03" + ) + ) + assertTrue( + ComparableVersion("5.2.1-beta01") < ComparableVersion( + "5.2.1-beta3" + ) + ) + assertTrue( + ComparableVersion("5.1.0") < ComparableVersion( + "5.2.1-beta3" + ) + ) + assertTrue( + ComparableVersion("4.1.0-alpha1") < ComparableVersion( + "5.2.1-beta3" + ) + ) + assertTrue( + ComparableVersion("4.1.0-alpha") < ComparableVersion( + "5.2.1-beta" + ) + ) + assertTrue( + ComparableVersion("5.1.0-snapshot") < ComparableVersion( + "5.2.1-beta3" + ) + ) + assertTrue( + ComparableVersion("5.1.0-SNAPSHOT") < ComparableVersion( + "5.2.1-beta3" + ) + ) + assertTrue( + ComparableVersion("5.1.0") > ComparableVersion( + "5.1.0-SNAPSHOT" + ) + ) + assertTrue( + ComparableVersion("5.1.0") > ComparableVersion( + "5.1.0-beta" + ) + ) + assertTrue( + ComparableVersion("5.1.0") > ComparableVersion( + "5.1.0-alpha" + ) + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceActivityServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceActivityServiceTest.kt new file mode 100644 index 0000000000..ca591bab61 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceActivityServiceTest.kt @@ -0,0 +1,321 @@ +package io.embrace.android.embracesdk.session + +import android.app.Activity +import android.app.Application +import android.content.ComponentCallbacks2 +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Bundle +import android.os.Looper +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import io.embrace.android.embracesdk.capture.memory.MemoryService +import io.embrace.android.embracesdk.capture.orientation.OrientationService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +internal class EmbraceActivityServiceTest { + + private lateinit var activityService: EmbraceActivityService + + companion object { + private lateinit var mockLooper: Looper + private lateinit var mockLifeCycleOwner: LifecycleOwner + private lateinit var mockLifecycle: Lifecycle + private lateinit var mockApplication: Application + private lateinit var mockMemoryService: MemoryService + private lateinit var mockOrientationService: OrientationService + private val fakeClock = FakeClock() + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockLooper = mockk() + mockLifeCycleOwner = mockk() + mockLifecycle = mockk(relaxed = true) + mockkStatic(Looper::class) + mockkStatic(ProcessLifecycleOwner::class) + mockApplication = mockk(relaxed = true) + mockMemoryService = mockk() + mockOrientationService = mockk() + + fakeClock.setCurrentTime(1234) + every { mockApplication.registerActivityLifecycleCallbacks(any()) } returns Unit + every { Looper.getMainLooper() } returns mockLooper + every { mockLooper.thread } returns Thread.currentThread() + every { ProcessLifecycleOwner.get() } returns mockLifeCycleOwner + every { mockLifeCycleOwner.lifecycle } returns mockLifecycle + every { mockLifecycle.addObserver(any()) } returns Unit + } + + @AfterClass + fun tearDown() { + unmockkAll() + } + } + + @Before + fun before() { + clearAllMocks( + answers = false, + objectMocks = false, + constructorMocks = false, + staticMocks = false + ) + + activityService = EmbraceActivityService( + mockApplication, + mockOrientationService, + fakeClock + ) + activityService.setMemoryService(mockMemoryService) + } + + @Test + fun `test we are adding lifecycle observer on constructor`() { + verify { mockApplication.registerActivityLifecycleCallbacks(activityService) } + verify { mockApplication.applicationContext.registerComponentCallbacks(activityService) } + } + + @Test + fun `test on activity created updates state and orientation`() { + val mockActivity = mockk() + every { mockActivity.localClassName } returns "localClassName" + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + val mockResources = mockk() + val orientation = 1 + val mockConfiguration = Configuration() + mockConfiguration.orientation = orientation + every { mockActivity.isFinishing } returns false + + every { mockActivity.resources } returns mockResources + every { mockResources.configuration } returns mockConfiguration + val bundle = Bundle() + + activityService.onActivityCreated(mockActivity, bundle) + + assertEquals(mockActivity, activityService.foregroundActivity) + verify { mockOrientationService.onOrientationChanged(orientation) } + verify { mockActivityListener.onActivityCreated(mockActivity, bundle) } + } + + @Test + fun `test on activity started with no listeners`() { + val mockActivity = mockk() + every { mockActivity.localClassName } returns "localClassName" + + activityService.onActivityStarted(mockActivity) + every { mockActivity.isFinishing } returns false + + assertEquals(mockActivity, activityService.foregroundActivity) + } + + @Test + fun `test on activity started with activity listener`() { + val mockActivity = mockk() + every { mockActivity.localClassName } returns "localClassName" + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + every { mockActivity.isFinishing } returns false + + activityService.onActivityStarted(mockActivity) + + verify { mockActivityListener.onView(mockActivity) } + assertEquals(mockActivity, activityService.foregroundActivity) + } + + @Test + fun `verify on activity resumed for a StartupActivity does not trigger listeners`() { + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + activityService.onActivityResumed(TestStartupActivity()) + + verify { mockActivityListener wasNot Called } + } + + @Test + fun `verify on activity resumed for a non StartupActivity does trigger listeners`() { + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + activityService.onActivityResumed(TestNonStartupActivity()) + + verify { mockActivityListener.applicationStartupComplete() } + } + + @Test + fun `verify on activity stopped triggers listeners`() { + val mockActivity = mockk() + every { mockActivity.localClassName } returns "localClassName" + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + activityService.onActivityStopped(mockActivity) + + verify { mockActivityListener.onViewClose(mockActivity) } + } + + @Test + fun `verify on activity foreground for cold start triggers listeners`() { + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + activityService.onForeground() + + verify { mockActivityListener.onForeground(true, fakeClock.now(), fakeClock.now()) } + } + + @Test + fun `verify on activity foreground called twice is not a cold start`() { + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + with(activityService) { + onForeground() + // repeat so it's not a cold start + onForeground() + } + + verify { mockActivityListener.onForeground(true, fakeClock.now(), fakeClock.now()) } + verify { mockActivityListener.onForeground(true, fakeClock.now(), fakeClock.now()) } + } + + @Test + fun `verify on activity background triggers listeners`() { + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + activityService.onBackground() + + verify { mockActivityListener.onBackground(any()) } + } + + @Test + fun `verify on trim on level not running low does not do anything`() { + activityService.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE) + + verify { mockMemoryService wasNot Called } + } + + @Test + fun `verify on trim on level running low triggers memory warning`() { + activityService.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) + + verify { mockMemoryService.onMemoryWarning() } + } + + @Test + fun `verify isInBackground returns true by default`() { + assertTrue(activityService.isInBackground) + } + + @Test + fun `verify isInBackground returns false if it was previously on foreground`() { + activityService.onForeground() + + assertFalse(activityService.isInBackground) + } + + @Test + fun `verify isInBackground returns true if it was previously on background`() { + activityService.onBackground() + + assertTrue(activityService.isInBackground) + } + + @Test + fun `get foreground activity for existing current activity`() { + val mockActivity = mockk() + every { mockActivity.localClassName } returns "localClassName" + every { mockActivity.isFinishing } returns false + activityService.updateStateWithActivity(mockActivity) + + assertEquals(mockActivity, activityService.foregroundActivity) + } + + @Test + fun `get foreground activity for an activity that is finishing should return absent`() { + val mockActivity = mockk() + every { mockActivity.localClassName } returns "localClassName" + every { mockActivity.isFinishing } returns true + activityService.updateStateWithActivity(mockActivity) + + assertNull(activityService.foregroundActivity) + } + + @Test + fun `get foreground activity for a non existing activity should return absent`() { + activityService.updateStateWithActivity(null) + assertNull(activityService.foregroundActivity) + } + + @Test + fun `verify a listener is added`() { + // assert empty list first + assertEquals(0, activityService.listeners.size) + + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + + assertEquals(1, activityService.listeners.size) + } + + @Test + fun `verify if listener is already present, then it does not add anything`() { + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + // add it for a 2nd time + activityService.addListener(mockActivityListener) + + assertEquals(1, activityService.listeners.size) + } + + @Test + fun `verify a listener is added with priority`() { + val mockActivityListener = mockk() + val mockActivityListener2 = mockk() + activityService.addListener(mockActivityListener) + + activityService.addListener(mockActivityListener2) + + assertEquals(2, activityService.listeners.size) + assertEquals(mockActivityListener2, activityService.listeners[1]) + } + + @Test + fun `verify close cleans everything`() { + // add a listener first, so we then check that listener have been cleared + val mockActivityListener = mockk() + activityService.addListener(mockActivityListener) + every { mockApplication.unregisterActivityLifecycleCallbacks(activityService) } returns Unit + + activityService.close() + + verify { mockApplication.applicationContext.unregisterComponentCallbacks(activityService) } + verify { mockApplication.unregisterActivityLifecycleCallbacks(activityService) } + assertTrue(activityService.listeners.isEmpty()) + } + + @io.embrace.android.embracesdk.annotation.StartupActivity + private class TestStartupActivity : Activity() + + private class TestNonStartupActivity : Activity() +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityServiceTest.kt new file mode 100644 index 0000000000..37fd9daf7f --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceBackgroundActivityServiceTest.kt @@ -0,0 +1,322 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.FakeBreadcrumbService +import io.embrace.android.embracesdk.FakeDeliveryService +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.capture.user.EmbraceUserService +import io.embrace.android.embracesdk.capture.user.UserService +import io.embrace.android.embracesdk.concurrency.BlockableExecutorService +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import io.embrace.android.embracesdk.fakes.fakeSpansBehavior +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.internal.OpenTelemetryClock +import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.BackgroundActivity +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import java.util.concurrent.TimeUnit + +internal class EmbraceBackgroundActivityServiceTest { + + private lateinit var service: EmbraceBackgroundActivityService + private lateinit var clock: FakeClock + private lateinit var performanceInfoService: PerformanceInfoService + private lateinit var metadataService: MetadataService + private lateinit var breadcrumbService: FakeBreadcrumbService + private lateinit var activityService: FakeActivityService + private lateinit var eventService: EventService + private lateinit var remoteLogger: EmbraceRemoteLogger + private lateinit var userService: UserService + private lateinit var exceptionService: EmbraceInternalErrorService + private lateinit var deliveryService: FakeDeliveryService + private lateinit var ndkService: NdkService + private lateinit var configService: FakeConfigService + private lateinit var localConfig: LocalConfig + private lateinit var spansService: EmbraceSpansService + private lateinit var blockableExecutorService: BlockableExecutorService + private lateinit var spansRemoteConfig: SpansRemoteConfig + + @Before + fun init() { + clock = FakeClock(10000L) + performanceInfoService = mockk() + metadataService = FakeAndroidMetadataService() + breadcrumbService = FakeBreadcrumbService() + activityService = FakeActivityService(isInBackground = true) + eventService = mockk() + remoteLogger = mockk() + exceptionService = mockk() + deliveryService = FakeDeliveryService() + ndkService = mockk(relaxed = true) + userService = EmbraceUserService( + FakePreferenceService(backgroundActivityEnabled = true), + mockk() + ) + spansService = EmbraceSpansService(clock = OpenTelemetryClock(embraceClock = clock)) + spansRemoteConfig = SpansRemoteConfig(pctEnabled = 100f) + configService = FakeConfigService( + backgroundActivityCaptureEnabled = true, + spansBehavior = fakeSpansBehavior { spansRemoteConfig } + ) + configService.addListener(spansService) + configService.updateListeners() + localConfig = spyk( + LocalConfig.buildConfig( + "GrCPU", + false, + "{\"background_activity\": {\"max_background_activity_seconds\": 3600}}", + EmbraceSerializer() + ) + ) + blockableExecutorService = BlockableExecutorService() + + every { eventService.findEventIdsForSession(any(), any()) } returns listOf() + every { remoteLogger.findInfoLogIds(any(), any()) } returns listOf() + every { remoteLogger.findWarningLogIds(any(), any()) } returns listOf() + every { remoteLogger.findErrorLogIds(any(), any()) } returns listOf() + every { remoteLogger.getInfoLogsAttemptedToSend() } returns 0 + every { remoteLogger.getWarnLogsAttemptedToSend() } returns 0 + every { remoteLogger.getErrorLogsAttemptedToSend() } returns 0 + every { remoteLogger.getUnhandledExceptionsSent() } returns 0 + every { exceptionService.currentExceptionError } returns mockk() + every { + performanceInfoService.getSessionPerformanceInfo( + any(), + any(), + any(), + null + ) + } returns mockk() + } + + @Test + fun `test that the service listens to activity events`() { + this.service = createService() + assertEquals(service, activityService.listeners.single()) + } + + @Test + fun `test background activity state when going to the background`() { + this.service = createService() + + service.onBackground(clock.now()) + + assertNotNull(service.backgroundActivity) + assertEquals( + BackgroundActivity.LifeEventType.BKGND_STATE, + service.backgroundActivity?.startType + ) + + assertEquals(service.backgroundActivity?.sessionId, metadataService.activeSessionId) + } + + @Test + fun `test background activity state when going to the foreground`() { + this.service = createService() + + val timestamp = 1669392000L + + service.onForeground(true, 123, timestamp) + + assertNull(service.backgroundActivity) + + assertEquals(2, deliveryService.saveBackgroundActivityInvokedCount) + assertEquals(1, deliveryService.sendBackgroundActivitiesInvokedCount) + } + + @Test + fun `background activity is not started whn the service initializes in the foreground`() { + activityService.isInBackground = false + this.service = createService() + assertNull(service.backgroundActivity) + } + + @Test + fun `activity is cached on start capture`() { + this.service = createService() + assertNotNull(deliveryService.lastSavedBackgroundActivity) + } + + @Test + fun `activity is cached when going to the foreground regardless of time limit`() { + val startTime = clock.now() + + this.service = createService() + assertNotNull(deliveryService.lastSavedBackgroundActivity) + assertEquals(1, deliveryService.saveBackgroundActivityInvokedCount) + + clock.setCurrentTime(startTime + 1000) + service.onForeground(true, 500, startTime + 1000) + assertEquals(2, deliveryService.saveBackgroundActivityInvokedCount) + + clock.setCurrentTime(startTime + 2000) + service.onBackground(startTime + 2000) + assertEquals(3, deliveryService.saveBackgroundActivityInvokedCount) + } + + @Test + fun `activity is cached on start capture when the service started in foreground`() { + activityService.isInBackground = false + this.service = createService() + + assertNull(deliveryService.lastSavedBackgroundActivity) + + service.onBackground(clock.now()) + + assertNotNull(deliveryService.lastSavedBackgroundActivity) + } + + @Test + fun `calling save() persists the background activity in cache`() { + activityService.isInBackground = false // start the service in foreground + val startTime = clock.now() + clock.setCurrentTime(startTime) + + this.service = createService() + assertEquals(0, deliveryService.saveBackgroundActivityInvokedCount) + + // start capturing background activity 10 seconds later, activity is first cached + service.onBackground(startTime + 10 * 1000) + assertEquals(1, deliveryService.saveBackgroundActivityInvokedCount) + + // elapse another 10 seconds to get around the 5 seconds limitation + clock.setCurrentTime(startTime + 20 * 1000) + service.save() + + assertEquals(2, deliveryService.saveBackgroundActivityInvokedCount) + } + + @Test + fun `save() does not persist to disk if the activity was cached within the last 5 seconds`() { + activityService.isInBackground = false // start the service in foreground + + this.service = createService() + assertEquals(0, deliveryService.saveBackgroundActivityInvokedCount) + + service.onBackground(clock.now()) + assertEquals(1, deliveryService.saveBackgroundActivityInvokedCount) + + // save() will not persist to disk since the last time was less than 5 seconds ago + service.save() + assertEquals(1, deliveryService.saveBackgroundActivityInvokedCount) + } + + @Test + fun `NDK session id is updated when NDK is enabled`() { + configService = FakeConfigService( + autoDataCaptureBehavior = fakeAutoDataCaptureBehavior( + localCfg = { LocalConfig("", true, SdkLocalConfig()) } + ) + ) + this.service = createService() + + verify { + ndkService.updateSessionId(checkNotNull(service.backgroundActivity).sessionId) + } + } + + @Test + fun `NDK session id is not updated when NDK is not enabled`() { + this.service = createService() + + verify(exactly = 0) { + ndkService.updateSessionId(any()) + } + } + + @Test + fun `saving will persist the current completed spans but will not flush`() { + service = createService() + val now = TimeUnit.MILLISECONDS.toNanos(clock.now()) + spansService.initializeService(now, now + 10L) + assertEquals(1, spansService.completedSpans()?.size) + // move time ahead so the save will actually persist the new background activity message + clock.tick(6000) + service.save() + assertNotNull(deliveryService.lastSavedBackgroundActivity) + assertEquals(2, deliveryService.saveBackgroundActivityInvokedCount) + assertEquals(1, deliveryService.lastSavedBackgroundActivity?.spans?.size) + assertEquals(1, spansService.completedSpans()?.size) + } + + @Test + fun `crash will save and flush the current completed spans`() { + // Prevent background thread from overwriting deliveryService.lastSavedBackgroundActivity + blockableExecutorService.blockingMode = true + service = createService() + val now = TimeUnit.MILLISECONDS.toNanos(clock.now()) + spansService.initializeService(now, now + 10L) + service.handleCrash("crashId") + assertNotNull(deliveryService.lastSavedBackgroundActivity) + + // there should be 2 completed spans: session span and the sdk init span + assertEquals(1, deliveryService.saveBackgroundActivityInvokedCount) + assertEquals(2, deliveryService.lastSavedBackgroundActivity?.spans?.size) + assertEquals(0, spansService.completedSpans()?.size) + } + + @Test + fun `foregrounding will flush the current completed spans`() { + service = createService() + val now = TimeUnit.MILLISECONDS.toNanos(clock.now()) + spansService.initializeService(now, now + 10L) + service.onForeground(false, now, clock.now()) + assertNotNull(deliveryService.lastSavedBackgroundActivity) + + // there should be 2 completed spans: session span and the sdk init span + assertEquals(2, deliveryService.lastSavedBackgroundActivity?.spans?.size) + assertEquals(0, spansService.completedSpans()?.size) + } + + @Test + fun `sending background activity will flush the current completed spans`() { + service = createService() + val now = TimeUnit.MILLISECONDS.toNanos(clock.now()) + spansService.initializeService(now, now + 10L) + service.sendBackgroundActivity() + assertNotNull(deliveryService.lastSentBackgroundActivity) + + // there should be 2 completed spans: session span and the sdk init span + assertEquals(2, deliveryService.lastSentBackgroundActivity?.spans?.size) + assertEquals(0, spansService.completedSpans()?.size) + } + + private fun createService(): EmbraceBackgroundActivityService { + return EmbraceBackgroundActivityService( + performanceInfoService, + metadataService, + breadcrumbService, + activityService, + eventService, + remoteLogger, + userService, + exceptionService, + deliveryService, + configService, + ndkService, + clock, + spansService, + lazy { blockableExecutorService } + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerServiceTest.kt new file mode 100644 index 0000000000..fcab3fa187 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceMemoryCleanerServiceTest.kt @@ -0,0 +1,99 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.fakes.FakeMemoryCleanerListener +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.utils.at +import io.mockk.clearAllMocks +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +internal class EmbraceMemoryCleanerServiceTest { + + companion object { + private lateinit var exceptionService: EmbraceInternalErrorService + + @BeforeClass + @JvmStatic + fun beforeClass() { + exceptionService = mockk(relaxed = true) + } + + @JvmStatic + @AfterClass + fun tearDown() { + unmockkAll() + } + } + + private lateinit var service: EmbraceMemoryCleanerService + + @Before + fun setUp() { + clearAllMocks( + answers = false, + objectMocks = false, + constructorMocks = false, + staticMocks = false + ) + + service = EmbraceMemoryCleanerService() + } + + @Test + fun `test cleanServicesCollections clears service listeners`() { + val listener = FakeMemoryCleanerListener() + val listener2 = FakeMemoryCleanerListener() + + service.addListener(listener) + service.addListener(listener2) + + service.cleanServicesCollections(exceptionService) + + assertEquals(1, listener.callCount) + assertEquals(1, listener2.callCount) + } + + @Test + fun `test cleanServicesCollections clear listeners and catch exception`() { + val listener1 = FakeMemoryCleanerListener() + val listener2 = object : MemoryCleanerListener { + override fun cleanCollections() = throw NullPointerException() + } + val listener3 = FakeMemoryCleanerListener() + + service.addListener(listener1) + service.addListener(listener2) + service.addListener(listener3) + + service.cleanServicesCollections(exceptionService) + + assertEquals(1, listener1.callCount) + assertEquals(1, listener3.callCount) + } + + @Test + fun `test cleanServicesCollections clears Embrace public API`() { + service.cleanServicesCollections(exceptionService) + verify(exactly = 1) { exceptionService.resetExceptionErrorObject() } + } + + @Test + fun addListener() { + val listener = FakeMemoryCleanerListener() + val listener2 = FakeMemoryCleanerListener() + + service.addListener(listener) + service.addListener(listener) + service.addListener(listener2) + + assertEquals(2, service.listeners.size) + assertEquals(listener, service.listeners.at(0)) + assertEquals(listener2, service.listeners.at(1)) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceSessionServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceSessionServiceTest.kt new file mode 100644 index 0000000000..4710865d43 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/EmbraceSessionServiceTest.kt @@ -0,0 +1,526 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.FakeDeliveryService +import io.embrace.android.embracesdk.config.remote.SpansRemoteConfig +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeSpansBehavior +import io.embrace.android.embracesdk.internal.OpenTelemetryClock +import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.Session +import io.embrace.android.embracesdk.payload.Session.SessionLifeEventType +import io.embrace.android.embracesdk.payload.SessionMessage +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ExecutorService + +internal class EmbraceSessionServiceTest { + + private lateinit var service: EmbraceSessionService + private lateinit var configService: FakeConfigService + private lateinit var deliveryService: FakeDeliveryService + private lateinit var spansService: EmbraceSpansService + + companion object { + + private val activityService = FakeActivityService() + private val mockNdkService: NdkService = mockk(relaxUnitFun = true) + private val mockSession: Session = mockk(relaxed = true) + private val mockSessionMessage: SessionMessage = mockk(relaxed = true) + private val mockSessionHandler: SessionHandler = mockk(relaxed = true) + private val mockSessionProperties: EmbraceSessionProperties = mockk(relaxed = true) + private val clock = FakeClock() + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(ExecutorService::class) + every { mockSessionMessage.session } returns mockSession + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + @Before + fun before() { + deliveryService = FakeDeliveryService() + spansService = EmbraceSpansService(clock = OpenTelemetryClock(embraceClock = clock)) + configService = FakeConfigService( + spansBehavior = fakeSpansBehavior { SpansRemoteConfig(pctEnabled = 100f) } + ) + configService.addListener(spansService) + configService.updateListeners() + } + + @After + fun after() { + clearAllMocks(answers = false) + } + + @Test + fun `initializing service should detect early sessions and start a STATE session`() { + initializeSessionService(ndkEnabled = true, isActivityInBackground = false) + + assertNotNull(deliveryService.lastSentCachedSession) + + // verify that a STATE session is started + verify { + mockSessionHandler.onSessionStarted( + /* automatically detecting a cold start */ true, + Session.SessionLifeEventType.STATE, + any(), + mockSessionProperties, + any(), + any() + ) + } + } + + @Test + fun `start session successfully`() { + initializeSessionService() + val coldStart = /* same for false */ true + val type = /* could be any type */ Session.SessionLifeEventType.STATE + every { + mockSessionHandler.onSessionStarted( + coldStart, + type, + any(), + mockSessionProperties, + any(), + any() + ) + } returns mockSessionMessage + + val startTime = clock.now() + + service.startSession(coldStart, type, startTime) + + assertEquals(mockSession, service.getActiveSession()) + verify { + mockSessionHandler.onSessionStarted( + coldStart, + type, + startTime, + mockSessionProperties, + any(), + any() + ) + } + assertEquals(mockSession, service.getActiveSession()) + } + + @Test + fun `start session if not allowed then session handler will return a null session`() { + initializeSessionService() + val coldStart = /* same for false */ true + val type = /* could be any type */ Session.SessionLifeEventType.STATE + every { + mockSessionHandler.onSessionStarted( + coldStart, + type, + any(), + mockSessionProperties, + any(), + any() + ) + } returns null + + val startTime = clock.now() + service.startSession(coldStart, type, startTime) + + assertNull(service.getActiveSession()) + verify { + mockSessionHandler.onSessionStarted( + coldStart, + type, + startTime, + mockSessionProperties, + any(), + any() + ) + } + } + + @Test + fun `handle crash successfully`() { + initializeSessionService() + val crashId = "crash-id" + + // let's start session first so we have an active session + every { + mockSessionHandler.onSessionStarted( + true, + Session.SessionLifeEventType.STATE, + any(), + mockSessionProperties, + any(), + any() + ) + } returns mockSessionMessage + service.startSession(true, Session.SessionLifeEventType.STATE, clock.now()) + + service.handleCrash(crashId) + + verify { mockSessionHandler.onCrash(mockSession, crashId, mockSessionProperties, 0) } + } + + @Test + fun `on foreground starts state session successfully for cold start`() { + initializeSessionService() + val coldStart = true + val startTime = 123L + + service.onForeground(coldStart, startTime, 456) + assertNull(deliveryService.lastSentCachedSession) + + // verify that a STATE session is started + verify { + mockSessionHandler.onSessionStarted( + coldStart, + Session.SessionLifeEventType.STATE, + 456, + mockSessionProperties, + any(), + any() + ) + } + } + + @Test + fun `on foreground starts state session successfully for non cold start`() { + initializeSessionService() + val coldStart = false + val startTime = 123L + + service.onForeground(coldStart, startTime, 456) + + // verify that a STATE session is started + verify { + mockSessionHandler.onSessionStarted( + coldStart, + Session.SessionLifeEventType.STATE, + 456, + mockSessionProperties, + any(), + any() + ) + } + } + + @Test + fun `on background ends a state session for a previously existing session, with an sdkStarupDuration = 5`() { + initializeSessionService() + val sdkStartupDuration = 5L + service.setSdkStartupDuration(sdkStartupDuration) + // let's start session first so we have an active session + startDefaultSession() + + service.onBackground(456) + + // verify session is ended + verify { + mockSessionHandler.onSessionEnded( + Session.SessionLifeEventType.STATE, + mockSession, + mockSessionProperties, + sdkStartupDuration, + 456 + ) + } + // verify active session has been reset + assertNull(service.getActiveSession()) + } + + @Test + fun `trigger stateless end session successfully for activity in background`() { + initializeSessionService() + // let's start session first so we have an active session + startDefaultSession() + + service.triggerStatelessSessionEnd(Session.SessionLifeEventType.MANUAL) + + // verify session is ended + verify { + mockSessionHandler.onSessionEnded( + Session.SessionLifeEventType.MANUAL, + mockSession, + mockSessionProperties, + 0, + any() + ) + } + } + + @Test + fun `trigger stateless end session successfully for activity in foreground`() { + initializeSessionService(isActivityInBackground = false) + // let's start session first so we have an active session + startDefaultSession() + val endType = Session.SessionLifeEventType.MANUAL + + service.triggerStatelessSessionEnd(endType) + + // verify session is ended + verify { mockSessionHandler.onSessionEnded(endType, mockSession, mockSessionProperties, 0, any()) } + // verify that a MANUAL session is started + verify { + mockSessionHandler.onSessionStarted( + false, + endType, + any(), + mockSessionProperties, + any(), + any() + ) + } + } + + @Test + fun `trigger stateless end session for a STATE session end type should not do anything`() { + initializeSessionService() + service.triggerStatelessSessionEnd(Session.SessionLifeEventType.STATE) + + verify { mockSessionHandler wasNot Called } + } + + @Test + fun `close successfully`() { + initializeSessionService() + service.close() + + verify { mockSessionHandler.close() } + } + + @Test + fun `add property successfully`() { + initializeSessionService() + val key = "key" + val value = "value" + val permanent = true + val properties = mapOf() + every { mockSessionProperties.add(key, value, permanent) } returns true + every { mockSessionProperties.get() } returns properties + + val added = service.addProperty(key, value, permanent) + + assertTrue(added) + verify { mockSessionProperties.add(key, value, permanent) } + verify { mockNdkService.onSessionPropertiesUpdate(properties) } + } + + @Test + fun `if add property failed, then it should not notify ndk service`() { + initializeSessionService() + val key = "key" + val value = "value" + val permanent = true + every { mockSessionProperties.add(key, value, permanent) } returns false + + val added = service.addProperty(key, value, permanent) + + assertFalse(added) + verify { mockSessionProperties.add(key, value, permanent) } + verify { mockNdkService wasNot Called } + } + + @Test + fun `remove property successfully`() { + initializeSessionService() + val key = "key" + val properties = mapOf() + every { mockSessionProperties.remove(key) } returns true + every { mockSessionProperties.get() } returns properties + + val removed = service.removeProperty(key) + + assertTrue(removed) + verify { mockSessionProperties.remove(key) } + verify { mockNdkService.onSessionPropertiesUpdate(properties) } + } + + @Test + fun `if remove property failed, then it should not notify ndk service`() { + initializeSessionService() + val key = "key" + every { mockSessionProperties.remove(key) } returns false + + val removed = service.removeProperty(key) + + assertFalse(removed) + verify { mockSessionProperties.remove(key) } + verify { mockNdkService wasNot Called } + } + + @Test + fun `get embrace session properties`() { + val properties = mapOf() + every { mockSessionProperties.get() } returns properties + + initializeSessionService() + assertEquals(properties, service.getProperties()) + } + + @Test + fun `verify periodic caching`() { + initializeSessionService() + + service.onPeriodicCacheActiveSession() + + verify { + mockSessionHandler.getActiveSessionEndMessage( + /* either null active session or valid active session, same test */ null, + mockSessionProperties, + 0 + ) + } + + assertNotNull(deliveryService.lastSavedSession) + } + + @Test + fun `spanService that is not initialized will not result in any complete spans`() { + initializeSessionService() + assertNull(spansService.completedSpans()) + } + + @Test + fun `backgrounding flushes completed spans`() { + initializeSessionService() + startDefaultSession() + val now = clock.now() + spansService.initializeService(now, now + 5L) + assertEquals(1, spansService.completedSpans()?.size) + service.onBackground(now) + // expect 2 spans to be flushed: session span and sdk init span + verify { + mockSessionHandler.onSessionEnded( + endType = any(), + originSession = any(), + sessionProperties = any(), + sdkStartupDuration = any(), + endTime = any(), + completedSpans = match { + it.size == 2 + } + ) + } + assertEquals(0, spansService.completedSpans()?.size) + } + + @Test + fun `stateless session ends flushes completed spans`() { + listOf(SessionLifeEventType.MANUAL, SessionLifeEventType.TIMED).forEach { + before() + initializeSessionService() + startDefaultSession() + val now = clock.now() + spansService.initializeService(now, now + 5L) + assertEquals(1, spansService.completedSpans()?.size) + service.triggerStatelessSessionEnd(it) + // expect 2 spans to be flushed: session span and sdk init span + verify { + mockSessionHandler.onSessionEnded( + endType = any(), + originSession = any(), + sessionProperties = any(), + sdkStartupDuration = any(), + endTime = any(), + completedSpans = match { + it.size == 2 + } + ) + } + assertEquals(0, spansService.completedSpans()?.size) + after() + } + } + + @Test + fun `crash ending flushes completed spans`() { + initializeSessionService() + startDefaultSession() + val now = clock.now() + spansService.initializeService(now, now + 5L) + assertEquals(1, spansService.completedSpans()?.size) + service.handleCrash("crashId") + // expect 2 spans to be flushed: session span and sdk init span + verify { + mockSessionHandler.onCrash( + originSession = any(), + crashId = any(), + sessionProperties = any(), + sdkStartupDuration = any(), + completedSpans = match { + it.size == 2 + } + ) + } + assertEquals(0, spansService.completedSpans()?.size) + } + + @Test + fun `periodic caching caches completed spans but doesn't flush them`() { + initializeSessionService() + startDefaultSession() + val now = clock.now() + spansService.initializeService(now, now + 5L) + assertEquals(1, spansService.completedSpans()?.size) + service.onPeriodicCacheActiveSession() + assertEquals(1, spansService.completedSpans()?.size) + } + + private fun initializeSessionService( + ndkEnabled: Boolean = false, + isActivityInBackground: Boolean = true + ) { + activityService.isInBackground = isActivityInBackground + + service = EmbraceSessionService( + activityService, + mockNdkService, + mockSessionProperties, + mockk(relaxed = true), + mockSessionHandler, + deliveryService, + ndkEnabled, + clock, + spansService + ) + } + + private fun startDefaultSession() { + every { + mockSessionHandler.onSessionStarted( + true, + Session.SessionLifeEventType.STATE, + any(), + mockSessionProperties, + any(), + any() + ) + } returns mockSessionMessage + service.startSession(true, Session.SessionLifeEventType.STATE, clock.now()) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionHandlerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionHandlerTest.kt new file mode 100644 index 0000000000..321b1df150 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionHandlerTest.kt @@ -0,0 +1,616 @@ +package io.embrace.android.embracesdk.session + +import android.app.Activity +import io.embrace.android.embracesdk.FakeDeliveryService +import io.embrace.android.embracesdk.capture.PerformanceInfoService +import io.embrace.android.embracesdk.capture.connectivity.NetworkConnectivityService +import io.embrace.android.embracesdk.capture.crumbs.BreadcrumbService +import io.embrace.android.embracesdk.capture.thermalstate.NoOpThermalStatusService +import io.embrace.android.embracesdk.capture.webview.WebViewService +import io.embrace.android.embracesdk.comms.delivery.SessionMessageState +import io.embrace.android.embracesdk.config.local.LocalConfig +import io.embrace.android.embracesdk.config.local.SdkLocalConfig +import io.embrace.android.embracesdk.config.local.SessionLocalConfig +import io.embrace.android.embracesdk.config.remote.RemoteConfig +import io.embrace.android.embracesdk.config.remote.SessionRemoteConfig +import io.embrace.android.embracesdk.event.EmbraceRemoteLogger +import io.embrace.android.embracesdk.event.EventService +import io.embrace.android.embracesdk.fakes.FakeActivityService +import io.embrace.android.embracesdk.fakes.FakeAndroidMetadataService +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.FakeGatingService +import io.embrace.android.embracesdk.fakes.FakePreferenceService +import io.embrace.android.embracesdk.fakes.FakeUserService +import io.embrace.android.embracesdk.fakes.fakeAutoDataCaptureBehavior +import io.embrace.android.embracesdk.fakes.fakeDataCaptureEventBehavior +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.fakes.fakeSessionBehavior +import io.embrace.android.embracesdk.fixtures.testSpan +import io.embrace.android.embracesdk.internal.MessageType +import io.embrace.android.embracesdk.internal.StartupEventInfo +import io.embrace.android.embracesdk.internal.utils.Uuid +import io.embrace.android.embracesdk.logging.EmbraceInternalErrorService +import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.ndk.NdkService +import io.embrace.android.embracesdk.payload.Session +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.payload.UserInfo +import io.embrace.android.embracesdk.session.EmbraceSessionService.Companion.SESSION_CACHING_INTERVAL +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +internal class SessionHandlerTest { + + private lateinit var sessionHandler: SessionHandler + + companion object { + private val logger: InternalEmbraceLogger = InternalEmbraceLogger() + private val preferencesService: FakePreferenceService = FakePreferenceService() + private val mockUserService: FakeUserService = FakeUserService() + private val mockNetworkConnectivityService: NetworkConnectivityService = + mockk(relaxUnitFun = true) + private val mockBreadcrumbService: BreadcrumbService = mockk(relaxed = true) + private val activityService = FakeActivityService() + private val mockNdkService: NdkService = mockk(relaxUnitFun = true) + private val mockEventService: EventService = mockk(relaxed = true) + private val mockRemoteLogger: EmbraceRemoteLogger = mockk(relaxed = true) + private val mockExceptionService: EmbraceInternalErrorService = mockk(relaxed = true) + private val mockPerformanceInfoService: PerformanceInfoService = mockk(relaxed = true) + private val mockMemoryCleanerService: MemoryCleanerService = mockk(relaxUnitFun = true) + private val mockWebViewservice: WebViewService = mockk(relaxed = true) { + every { getCapturedData() } returns emptyList() + } + private val clock = FakeClock() + private val mockAutomaticSessionStopper: ScheduledExecutorService = mockk(relaxed = true) + private val mockSessionPeriodicCacheExecutorService: ScheduledExecutorService = mockk(relaxed = true) + private const val sessionUuid = "99fcae22-0db5-4b63-b49d-315eecce4889" + private const val now = 123L + private const val sessionNumber = 5 + private val mockSessionProperties: EmbraceSessionProperties = mockk(relaxed = true) + private val emptyMapSessionProperties: Map = emptyMap() + private val mockUserInfo: UserInfo = mockk() + private val mockAutomaticSessionStopperRunnable: Runnable = mockk() + private val mockPeriodicCachingRunnable: Runnable = mockk() + private var mockActiveSession: Session = mockk(relaxed = true) + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkStatic(ScheduledExecutorService::class) + mockkStatic(ExecutorService::class) + mockkStatic(Uuid::class) + + clock.setCurrentTime(now) + every { Uuid.getEmbUuid() } returns sessionUuid + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkAll() + } + } + + private lateinit var metadataService: FakeAndroidMetadataService + private lateinit var localConfig: LocalConfig + private lateinit var remoteConfig: RemoteConfig + private lateinit var sessionLocalConfig: SessionLocalConfig + private lateinit var deliveryService: FakeDeliveryService + private lateinit var gatingService: FakeGatingService + private lateinit var configService: FakeConfigService + + @Before + fun before() { + mockActiveSession = mockk(relaxed = true) + preferencesService.sessionNumber = sessionNumber + every { mockSessionProperties.get() } returns emptyMapSessionProperties + + metadataService = FakeAndroidMetadataService() + localConfig = LocalConfig( + appId = metadataService.getAppId(), + ndkEnabled = true, + sdkConfig = SdkLocalConfig() + ) + sessionLocalConfig = SessionLocalConfig() + remoteConfig = RemoteConfig() + configService = FakeConfigService( + autoDataCaptureBehavior = fakeAutoDataCaptureBehavior( + localCfg = { localConfig }, + remoteCfg = { remoteConfig } + ), + sessionBehavior = fakeSessionBehavior( + localCfg = { sessionLocalConfig }, + remoteCfg = { remoteConfig } + ), + dataCaptureEventBehavior = fakeDataCaptureEventBehavior( + remoteCfg = { remoteConfig } + ) + ) + gatingService = FakeGatingService(configService = configService) + deliveryService = FakeDeliveryService() + sessionHandler = SessionHandler( + logger, + configService, + preferencesService, + mockUserService, + mockNetworkConnectivityService, + metadataService, + gatingService, + mockBreadcrumbService, + activityService, + mockNdkService, + mockEventService, + mockRemoteLogger, + mockExceptionService, + mockPerformanceInfoService, + mockMemoryCleanerService, + deliveryService, + mockWebViewservice, + null, + NoOpThermalStatusService(), + null, + clock, + automaticSessionStopper = mockAutomaticSessionStopper, + sessionPeriodicCacheExecutorService = mockSessionPeriodicCacheExecutorService, + Executors.newSingleThreadExecutor() + ) + } + + @After + fun after() { + clearAllMocks(answers = false) + } + + @Test + fun `onSession started successfully`() { + val maxSessionSeconds = 60 + sessionLocalConfig = SessionLocalConfig(maxSessionSeconds = 60, asyncEnd = false) + mockUserService.obj = mockUserInfo + mockActiveSession = fakeSession() + + val screen = "screen" + every { mockBreadcrumbService.getLastViewBreadcrumbScreenName() } returns screen + val sessionStartType = Session.SessionLifeEventType.STATE + // this is needed so session handler creates automatic session stopper + + val sessionMessage = sessionHandler.onSessionStarted( + true, + sessionStartType, + now, + mockSessionProperties, + mockAutomaticSessionStopperRunnable, + mockPeriodicCachingRunnable + ) + + // verify record connection type + verify { mockNetworkConnectivityService.networkStatusOnSessionStarted(now) } + // verify active session is set + assertEquals(sessionUuid, metadataService.activeSessionId) + // verify session is being sanitized + assertEquals(1, gatingService.sessionMessagesFiltered.size) + // verify automatic session stopper has been scheduled + verify { + mockAutomaticSessionStopper.scheduleAtFixedRate( + mockAutomaticSessionStopperRunnable, + maxSessionSeconds.toLong(), + maxSessionSeconds.toLong(), + TimeUnit.SECONDS + ) + } + // verify periodic caching worker has been scheduled + verify { + mockSessionPeriodicCacheExecutorService.scheduleAtFixedRate( + mockPeriodicCachingRunnable, + 0, + SESSION_CACHING_INTERVAL.toLong(), + TimeUnit.SECONDS + ) + } + // verify session id gets updated if ndk enabled + verify { mockNdkService.updateSessionId(sessionUuid) } + // verify session is correctly built + with(checkNotNull(sessionMessage?.session)) { + assertEquals(sessionUuid, this.sessionId) + assertEquals(startTime, now) + assertEquals(sessionNumber + 1, number) + assertTrue(isColdStart) + assertEquals(sessionStartType, startType) + assertEquals(emptyMapSessionProperties, properties) + assertEquals("st", messageType) + assertEquals("foreground", appState) + assertEquals(mockUserInfo, user) + } + // verify session message is successfully built + with(checkNotNull(sessionMessage)) { + assertEquals(metadataService.getDeviceInfo(), deviceInfo) + assertEquals(metadataService.getAppInfo(), appInfo) + } + } + + @Test + fun `onSession if it's not allowed to start should not do anything`() { + remoteConfig = RemoteConfig( + disabledMessageTypes = setOf(MessageType.SESSION.name.toLowerCase(Locale.getDefault())) + ) + + val sessionMessage = sessionHandler.onSessionStarted( + true, + /* any event type */ Session.SessionLifeEventType.STATE, + now, + mockSessionProperties, + mockAutomaticSessionStopperRunnable, + mockPeriodicCachingRunnable + ) + + assertNull(sessionMessage) + verify { mockNetworkConnectivityService wasNot Called } + assertNull(metadataService.activeSessionId) + assertEquals(0, gatingService.sessionMessagesFiltered.size) + verify { mockAutomaticSessionStopper wasNot Called } + verify { mockSessionPeriodicCacheExecutorService wasNot Called } + verify { mockNdkService wasNot Called } + } + + @Test + fun `onSession started successfully with no preference service session number`() { + // return absent session number + preferencesService.sessionNumber = 0 + sessionLocalConfig = SessionLocalConfig(maxSessionSeconds = 5, asyncEnd = false) + every { mockBreadcrumbService.getLastViewBreadcrumbScreenName() } returns "screen" + val sessionStartType = Session.SessionLifeEventType.STATE + // this is needed so session handler creates automatic session stopper + + val sessionMessage = sessionHandler.onSessionStarted( + true, + sessionStartType, + now, + mockSessionProperties, + mockAutomaticSessionStopperRunnable, + mockPeriodicCachingRunnable + ) + + assertEquals(1, preferencesService.sessionNumber) + assertNotNull(sessionMessage) + assertNotNull(sessionMessage!!.session) + // no need to verify anything else because it's already verified in another test case + } + + @Test + fun `onSession started with no maximum session seconds should not start session automatic stopper`() { + every { mockBreadcrumbService.getLastViewBreadcrumbScreenName() } returns "screen" + val sessionStartType = Session.SessionLifeEventType.STATE + sessionLocalConfig = SessionLocalConfig(maxSessionSeconds = null) + + val sessionMessage = sessionHandler.onSessionStarted( + true, + sessionStartType, + now, + mockSessionProperties, + mockAutomaticSessionStopperRunnable, + mockPeriodicCachingRunnable + ) + + // verify automatic session stopper has not been scheduled + verify { mockAutomaticSessionStopper wasNot Called } + assertNotNull(sessionMessage) + assertNotNull(sessionMessage!!.session) + // no need to verify anything else because it's already verified in another test case + } + + @Test + fun `onSession started and resuming with no previous screen name but with foregroundActivity, it should force log view breadcrumb`() { + every { mockBreadcrumbService.getLastViewBreadcrumbScreenName() } returns null + val mockActivity: Activity = mockk() + // let's return a foreground activity + activityService.foregroundActivity = mockActivity + val activityClassName = "activity-class-name" + every { mockActivity.localClassName } returns activityClassName + val sessionStartType = Session.SessionLifeEventType.STATE + + val sessionMessage = sessionHandler.onSessionStarted( + true, + sessionStartType, + now, + mockSessionProperties, + mockAutomaticSessionStopperRunnable, + mockPeriodicCachingRunnable + ) + + // verify we are forcing log view with foreground activity class name + verify { mockBreadcrumbService.forceLogView(activityClassName, clock.now()) } + assertNotNull(sessionMessage) + assertNotNull(sessionMessage!!.session) + // no need to verify anything else because it's already verified in another test case + } + + @Test + fun `onSession ended successfully, with session duration less than 5 seconds, with cold start, with startup event info`() { + // since now=123, then duration will be less than 5 seconds + val startTime = 120L + every { mockActiveSession.startTime } returns startTime + every { mockActiveSession.isColdStart } returns true + val mockStartupEventInfo: StartupEventInfo = mockk(relaxed = true) + every { mockEventService.getStartupMomentInfo() } returns mockStartupEventInfo + + sessionHandler.onSessionEnded( + /* any type */ Session.SessionLifeEventType.STATE, + mockActiveSession, + mockSessionProperties, + /* any duration */ 2, + 1000 + ) + + assertEquals(deliveryService.lastSentSessions.single().second, SessionMessageState.END) + // verify cleaning is being performed + verify { + mockMemoryCleanerService.cleanServicesCollections( + mockExceptionService + ) + } + // verify we are sanitizing session message + assertEquals(1, gatingService.sessionMessagesFiltered.size) + // verify current session is removed from cache + verify { mockSessionProperties.clearTemporary() } + } + + @Test + fun `onSession not allowed to end because no active session available`() { + sessionHandler.onSessionEnded( + /* any type */ Session.SessionLifeEventType.STATE, + null, + mockSessionProperties, + /* any duration */ 2, + 1000 + ) + + verify { mockSessionPeriodicCacheExecutorService wasNot Called } + verify { mockAutomaticSessionStopper wasNot Called } + verify { mockMemoryCleanerService wasNot Called } + verify { mockSessionProperties wasNot Called } + + assertTrue(deliveryService.lastSentSessions.isEmpty()) + assertEquals(0, gatingService.sessionMessagesFiltered.size) + } + + @Test + fun `onSession not allowed to end because session control is disabled for MANUAL event type`() { + sessionHandler.onSessionEnded( + Session.SessionLifeEventType.MANUAL, + mockActiveSession, + mockSessionProperties, + /* any duration */ 2, + 1000 + ) + + verify { mockSessionPeriodicCacheExecutorService wasNot Called } + verify { mockAutomaticSessionStopper wasNot Called } + verify { mockMemoryCleanerService wasNot Called } + verify { mockSessionProperties wasNot Called } + assertTrue(deliveryService.lastSentSessions.isEmpty()) + assertEquals(0, gatingService.sessionMessagesFiltered.size) + } + + @Test + fun `onSession not allowed to end because session control is disabled for TIMED event type`() { + sessionHandler.onSessionEnded( + Session.SessionLifeEventType.TIMED, + mockActiveSession, + mockSessionProperties, + /* any duration */ 2, + 1000 + ) + + verify { mockSessionPeriodicCacheExecutorService wasNot Called } + verify { mockAutomaticSessionStopper wasNot Called } + verify { mockMemoryCleanerService wasNot Called } + verify { mockSessionProperties wasNot Called } + assertTrue(deliveryService.lastSentSessions.isEmpty()) + assertEquals(0, gatingService.sessionMessagesFiltered.size) + } + + @Test + fun `onSession not allowed to end MANUALLY because session duration is less than 5 seconds`() { + remoteConfig = RemoteConfig(sessionConfig = SessionRemoteConfig(isEnabled = true)) + // since now=123, then duration will be less than 5 seconds + val startTime = 120L + every { mockActiveSession.startTime } returns startTime + + sessionHandler.onSessionEnded( + Session.SessionLifeEventType.MANUAL, + mockActiveSession, + mockSessionProperties, + /* any duration */ 2, + 1000 + ) + + verify { mockSessionPeriodicCacheExecutorService wasNot Called } + verify { mockAutomaticSessionStopper wasNot Called } + verify { mockMemoryCleanerService wasNot Called } + verify { mockSessionProperties wasNot Called } + assertTrue(deliveryService.lastSentSessions.isEmpty()) + assertEquals(0, gatingService.sessionMessagesFiltered.size) + } + + @Test + fun `if session messages are disabled don't end session but end periodic cache and session automatic stopper`() { + remoteConfig = RemoteConfig( + disabledMessageTypes = setOf(MessageType.SESSION.name.toLowerCase(Locale.getDefault())) + ) + sessionHandler.scheduledFuture = mockk(relaxed = true) + + sessionHandler.onSessionEnded( + /* any type */ Session.SessionLifeEventType.STATE, + mockActiveSession, + mockSessionProperties, + /* any duration */ 2, + 1000 + ) + + // verify automatic session stopper was called + verify { sessionHandler.scheduledFuture?.cancel(false) } + verify { mockMemoryCleanerService wasNot Called } + verify { mockSessionProperties wasNot Called } + assertTrue(deliveryService.lastSentSessions.isEmpty()) + assertEquals(0, gatingService.sessionMessagesFiltered.size) + } + + @Test + fun `onCrash ended session successfully`() { + val crashId = "crash-id" + val startTime = 120L + val sdkStartupDuration = 2L + mockActiveSession = fakeSession().copy( + startTime = startTime, + isColdStart = true + ) + + sessionHandler.onCrash( + mockActiveSession, + crashId, + mockSessionProperties, + /* any duration */sdkStartupDuration + ) + + // when crashing, the following calls should not be made, this is because since we're + // about to crash we can save some time on not doing these // + verify { mockSessionPeriodicCacheExecutorService wasNot Called } + verify { mockAutomaticSessionStopper wasNot Called } + verify { mockMemoryCleanerService wasNot Called } + verify(exactly = 0) { mockSessionProperties.clearTemporary() } + assertNull(deliveryService.lastSavedSession) + assertEquals(1, gatingService.sessionMessagesFiltered.size) + + val session = checkNotNull(deliveryService.lastSentSessions.single().first.session) + + with(session) { + assertFalse(checkNotNull(isEndedCleanly)) + assertEquals("en", messageType) + assertEquals("foreground", appState) + assertEquals(emptyList(), eventIds) + assertEquals(emptyList(), infoLogIds) + assertEquals(emptyList(), warningLogIds) + assertEquals(emptyList(), errorLogIds) + assertEquals(emptyList(), networkLogIds) + assertEquals(0, infoLogsAttemptedToSend) + assertEquals(0, warnLogsAttemptedToSend) + assertEquals(0, errorLogsAttemptedToSend) + assertTrue(checkNotNull(exceptionError).exceptionErrors.isEmpty()) + assertEquals(now, lastHeartbeatTime) + assertEquals(mockSessionProperties.get(), properties) + assertEquals(Session.SessionLifeEventType.STATE, endType) + assertEquals(0, unhandledExceptions) + assertEquals(crashId, crashReportId) + assertEquals(now, endTime) + assertEquals(sdkStartupDuration, sdkStartupDuration) + assertEquals(0L, startupDuration) + assertEquals(0L, startupThreshold) + assertEquals(0, webViewInfo?.size) + } + } + + @Test + fun `onPeriodicCacheActiveSession caches session successfully`() { + val sessionMessage = sessionHandler.getActiveSessionEndMessage( + mockActiveSession, + mockSessionProperties, + /* any duration */2 + ) + + assertNotNull(sessionMessage) + + // when periodic caching, the following calls should not be made + verify { mockSessionPeriodicCacheExecutorService wasNot Called } + verify { mockAutomaticSessionStopper wasNot Called } + verify { mockMemoryCleanerService wasNot Called } + verify(exactly = 0) { mockSessionProperties.clearTemporary() } + assertTrue(deliveryService.lastSentSessions.isEmpty()) + assertEquals(0, gatingService.sessionMessagesFiltered.size) + } + + @Test + fun `onPeriodicCacheActiveSession does not cache if there is no active session`() { + val sessionMessage = sessionHandler.getActiveSessionEndMessage( + null, + mockSessionProperties, + /* any duration */2 + ) + + assertNull(sessionMessage) + + assertTrue(deliveryService.lastSentSessions.isEmpty()) + } + + @Test + fun `verify close stops everything successfully`() { + sessionHandler.scheduledFuture = mockk(relaxed = true) + sessionHandler.close() + verify { sessionHandler.scheduledFuture?.cancel(false) } + } + + @Test + fun `endSession includes completed spans in message`() { + sessionHandler.onSessionEnded( + endType = Session.SessionLifeEventType.STATE, + originSession = mockActiveSession, + sessionProperties = mockSessionProperties, + sdkStartupDuration = 1L, + endTime = 10L, + listOf(testSpan) + ) + + assertSpanInSessionMessage(deliveryService.lastSentSessions.single().first) + } + + @Test + fun `crashes includes completed spans in message`() { + sessionHandler.onCrash( + mockActiveSession, + "fakeCrashId", + mockSessionProperties, + 10L, + listOf(testSpan) + ) + + assertSpanInSessionMessage(deliveryService.lastSentSessions.single().first) + } + + @Test + fun `periodically cached sessions included currently completed spans`() { + val sessionMessage = sessionHandler.getActiveSessionEndMessage( + mockActiveSession, + mockSessionProperties, + 10L, + listOf(testSpan) + ) + + assertSpanInSessionMessage(sessionMessage) + } + + private fun assertSpanInSessionMessage(sessionMessage: SessionMessage?) { + assertNotNull(sessionMessage) + assertNotNull(sessionMessage?.spans) + assertEquals(1, sessionMessage?.spans?.size) + assertEquals(testSpan, sessionMessage?.spans!![0]) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionMessageSerializerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionMessageSerializerTest.kt new file mode 100644 index 0000000000..32c129d0c9 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionMessageSerializerTest.kt @@ -0,0 +1,48 @@ +package io.embrace.android.embracesdk.session + +import com.google.gson.Gson +import io.embrace.android.embracesdk.fakes.fakeSession +import io.embrace.android.embracesdk.fixtures.testSpan +import io.embrace.android.embracesdk.internal.EmbraceSerializer +import io.embrace.android.embracesdk.payload.AppInfo +import io.embrace.android.embracesdk.payload.Breadcrumbs +import io.embrace.android.embracesdk.payload.DeviceInfo +import io.embrace.android.embracesdk.payload.PerformanceInfo +import io.embrace.android.embracesdk.payload.SessionMessage +import io.embrace.android.embracesdk.payload.UserInfo +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +internal class SessionMessageSerializerTest { + + private lateinit var msg: SessionMessage + + @Before + fun setUp() { + msg = SessionMessage( + fakeSession(), + UserInfo(), + AppInfo(), + DeviceInfo(), + PerformanceInfo(), + Breadcrumbs(), + listOf(testSpan) + ) + } + + @Test + fun testSessionMessageSerializer() { + val gson = Gson() + val serializer = SessionMessageSerializer(EmbraceSerializer()) + + // message should be identical to JSON. + val expected = gson.toJson(msg, SessionMessage::class.java) + val observed = serializer.serialize(msg) + assertEquals(expected, observed) + + // JSON can be generated from cache + val cacheAttempt = serializer.serialize(msg) + assertEquals(expected, cacheAttempt) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionModuleImplTest.kt new file mode 100644 index 0000000000..3ab742e9d7 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/session/SessionModuleImplTest.kt @@ -0,0 +1,80 @@ +package io.embrace.android.embracesdk.session + +import io.embrace.android.embracesdk.FakeWorkerThreadModule +import io.embrace.android.embracesdk.concurrency.BlockableExecutorService +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.fakes.FakeConfigService +import io.embrace.android.embracesdk.fakes.fakeEmbraceSessionProperties +import io.embrace.android.embracesdk.fakes.injection.FakeAndroidServicesModule +import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule +import io.embrace.android.embracesdk.fakes.injection.FakeCustomerLogModule +import io.embrace.android.embracesdk.fakes.injection.FakeDataCaptureServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeDataContainerModule +import io.embrace.android.embracesdk.fakes.injection.FakeDeliveryModule +import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeNativeModule +import io.embrace.android.embracesdk.fakes.injection.FakeSdkObservabilityModule +import io.embrace.android.embracesdk.injection.InitModuleImpl +import io.embrace.android.embracesdk.injection.SessionModuleImpl +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +internal class SessionModuleImplTest { + + private val workerThreadModule = + FakeWorkerThreadModule( + scheduledExecutorProvider = ::BlockingScheduledExecutorService, + executorProvider = ::BlockableExecutorService + ) + + @Test + fun testDefaultImplementations() { + val module = SessionModuleImpl( + InitModuleImpl(), + FakeCoreModule(), + FakeAndroidServicesModule(), + FakeEssentialServiceModule(), + FakeNativeModule(), + FakeDataContainerModule(), + FakeDeliveryModule(), + fakeEmbraceSessionProperties(), + FakeDataCaptureServiceModule(), + FakeCustomerLogModule(), + FakeSdkObservabilityModule(), + workerThreadModule + ) + assertNotNull(module.sessionHandler) + assertNotNull(module.sessionService) + assertNull(module.backgroundActivityService) + } + + @Test + fun testEnabledBehaviors() { + val module = SessionModuleImpl( + InitModuleImpl(), + FakeCoreModule(), + FakeAndroidServicesModule(), + createEnabledBehavior(), + FakeNativeModule(), + FakeDataContainerModule(), + FakeDeliveryModule(), + fakeEmbraceSessionProperties(), + FakeDataCaptureServiceModule(), + FakeCustomerLogModule(), + FakeSdkObservabilityModule(), + workerThreadModule + ) + assertNotNull(module.sessionHandler) + assertNotNull(module.sessionService) + assertNotNull(module.backgroundActivityService) + } + + private fun createEnabledBehavior(): FakeEssentialServiceModule { + return FakeEssentialServiceModule( + configService = FakeConfigService( + backgroundActivityCaptureEnabled = true + ) + ) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ExecutorExtensionsTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ExecutorExtensionsTest.kt new file mode 100644 index 0000000000..f1b4c04019 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ExecutorExtensionsTest.kt @@ -0,0 +1,42 @@ +package io.embrace.android.embracesdk.utils + +import com.google.common.util.concurrent.MoreExecutors +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import java.util.concurrent.Callable + +internal class ExecutorExtensionsTest { + + @Test + fun testSubmitSafe() { + val executor = MoreExecutors.newDirectExecutorService() + assertEquals(1, executor.submitSafe(Callable { 1 })?.get()) + } + + @Test + fun testSubmitSafeClosed() { + val executor = MoreExecutors.newDirectExecutorService() + executor.shutdown() + assertNull(executor.submitSafe(Callable { 1 })) + } + + @Test + fun testEagerLazyLoadNormal() { + val executor = MoreExecutors.newDirectExecutorService() + assertEquals(1, executor.eagerLazyLoad(Callable { 1 }).value) + } + + @Test(expected = IllegalStateException::class) + fun testEagerLazyLoadException() { + val executor = MoreExecutors.newDirectExecutorService() + executor.eagerLazyLoad(Callable { error("Whoops") }).value + } + + @Test + fun testEagerLazyLoadClosed() { + val executor = MoreExecutors.newDirectExecutorService() + executor.shutdown() + assertEquals(1, executor.eagerLazyLoad(Callable { 1 }).value) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/JsonComparisonUtils.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/JsonComparisonUtils.kt new file mode 100644 index 0000000000..2d92510615 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/JsonComparisonUtils.kt @@ -0,0 +1,12 @@ +package io.embrace.android.embracesdk.utils + +import com.google.gson.JsonParser + +internal object JsonComparisonUtils { + + fun compareJson(expectedJson: String, actualJson: String): Boolean { + val expected = JsonParser.parseString(expectedJson) + val observed = JsonParser.parseString(actualJson) + return expected == observed + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ListExtensionsKtTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ListExtensionsKtTest.kt new file mode 100644 index 0000000000..762d823c34 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/ListExtensionsKtTest.kt @@ -0,0 +1,17 @@ +package io.embrace.android.embracesdk.utils + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class ListExtensionsKtTest { + + @Test + fun testSafeGet() { + val list = listOf("a", "b", "c") + assertEquals("a", list.at(0)) + assertEquals("c", list.at(2)) + assertNull(list.at(-1)) + assertNull(list.at(100)) + } +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/PropertyUtilsTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/PropertyUtilsTest.kt new file mode 100644 index 0000000000..a94451118c --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/utils/PropertyUtilsTest.kt @@ -0,0 +1,41 @@ +package io.embrace.android.embracesdk.utils + +import io.embrace.android.embracesdk.utils.PropertyUtils.sanitizeProperties +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class PropertyUtilsTest { + + @Test + fun testEmptyCase() { + assertEquals(emptyMap(), sanitizeProperties(null)) + assertEquals(emptyMap(), sanitizeProperties(emptyMap())) + } + + @Suppress("UNCHECKED_CAST") + @Test + fun testPropertyLimitExceeded() { + val input = (0..20).associateBy { "$it" } + val expected = (0..9).associateBy { "$it" } + assertEquals(expected, sanitizeProperties(input as Map?)) + } + + @Test + fun testNullValue() { + assertEquals("null", sanitizeProperties(mapOf("a" to null))["a"]) + } + + @Test + fun testSerializableValue() { + val obj = SerializableClass() + assertEquals(obj, sanitizeProperties(mapOf("a" to obj))["a"]) + } + + @Test + fun testUnserializableValue() { + assertEquals("not serializable", sanitizeProperties(mapOf("a" to UnSerializableClass()))["a"]) + } + + private class SerializableClass : java.io.Serializable + private class UnSerializableClass +} diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImplTest.kt new file mode 100644 index 0000000000..95676f0259 --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/worker/WorkerThreadModuleImplTest.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk.worker + +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame +import org.junit.Assert.fail +import org.junit.Test +import java.util.concurrent.RejectedExecutionException + +internal class WorkerThreadModuleImplTest { + + @Test + fun testModule() { + val module = WorkerThreadModuleImpl() + assertNotNull(module) + + assertNotNull(module.backgroundExecutor(ExecutorName.SESSION)) + val backgroundExecutor = module.backgroundExecutor(ExecutorName.SESSION_CACHE_EXECUTOR) + assertNotNull(backgroundExecutor) + assertNotNull(module.scheduledExecutor(ExecutorName.SESSION_CACHE_EXECUTOR)) + + // test caching + assertSame(backgroundExecutor, module.backgroundExecutor(ExecutorName.SESSION_CACHE_EXECUTOR)) + + // test shutting down module + module.close() + try { + module.backgroundExecutor(ExecutorName.SESSION).submit {} + fail("Should have thrown RejectedExecutionException") + } catch (ignored: RejectedExecutionException) { + } + } +} diff --git a/embrace-android-sdk/src/test/resources/anr_config.json b/embrace-android-sdk/src/test/resources/anr_config.json new file mode 100644 index 0000000000..8671c9e1d4 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/anr_config.json @@ -0,0 +1,4 @@ +{ + "capture_google": true, + "capture_unity_thread": true +} diff --git a/embrace-android-sdk/src/test/resources/anr_default_config_expected.txt b/embrace-android-sdk/src/test/resources/anr_default_config_expected.txt new file mode 100644 index 0000000000..70dbf951a3 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/anr_default_config_expected.txt @@ -0,0 +1,15 @@ +anr_pe_sc_extra_time=30000 +max_depth=100 +anr_pe_delay=5000 +min_duration=1000 +per_interval=80 +priority=0 +allow_early_capture=true +pct_bg_enabled=0 +pct_enabled=100 +per_session=5 +anr_pe_interval=1000 +interval=100 +main_thread_only=true +monitor_thread_priority=0 +timestamp=1499999999 diff --git a/embrace-android-sdk/src/test/resources/anr_interval_expected.json b/embrace-android-sdk/src/test/resources/anr_interval_expected.json new file mode 100644 index 0000000000..925a8607bc --- /dev/null +++ b/embrace-android-sdk/src/test/resources/anr_interval_expected.json @@ -0,0 +1,28 @@ +{ + "st": 150980980980, + "lk": 150980984980, + "en": 150980985980, + "v": "ui", + "se": { + "ticks": [ + { + "ts": 150980980980, + "threads": [ + { + "threadId": 13, + "state": "RUNNABLE", + "n": "my-thread", + "p": 5, + "tt": [ + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ] + } + ], + "o": 0, + "c": 0 + } + ] + }, + "c": 1 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/anr_override_config_expected.txt b/embrace-android-sdk/src/test/resources/anr_override_config_expected.txt new file mode 100644 index 0000000000..f57504a606 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/anr_override_config_expected.txt @@ -0,0 +1,17 @@ +anr_pe_sc_extra_time=12000 +black_list=java/util/Time\;kotlin/String\;test\=bar +max_depth=52 +anr_pe_delay=2000 +min_duration=502 +per_interval=59 +priority=3 +allow_early_capture=false +white_list=java/lang/Class\;kotlin/Boolean\;test\=foo\;about +pct_bg_enabled=1 +pct_enabled=5 +per_session=8 +anr_pe_interval=200 +interval=105 +main_thread_only=false +monitor_thread_priority=10 +timestamp=1499999999 diff --git a/embrace-android-sdk/src/test/resources/anr_tick_expected.json b/embrace-android-sdk/src/test/resources/anr_tick_expected.json new file mode 100644 index 0000000000..b37df32173 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/anr_tick_expected.json @@ -0,0 +1,17 @@ +{ + "ts": 156098234092, + "threads": [ + { + "threadId": 13, + "state": "RUNNABLE", + "n": "my-thread", + "p": 5, + "tt": [ + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ] + } + ], + "o": 2, + "c": 0 +} diff --git a/embrace-android-sdk/src/test/resources/api_request.json b/embrace-android-sdk/src/test/resources/api_request.json new file mode 100644 index 0000000000..dafe230ee4 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/api_request.json @@ -0,0 +1,16 @@ +{ + "contentType": "application/json", + "userAgent": "Embrace/a/1", + "contentEncoding": "application/json", + "accept": "application/json", + "acceptEncoding": "application/json", + "appId": "abcde", + "deviceId": "test_did", + "eventId": "test_eid", + "logId": "test_lid", + "url": { + "url": "https://google.com" + }, + "httpMethod": "GET", + "eTag": "d800f828fec4409dcabc7f5252e7ce71" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/app_config.json b/embrace-android-sdk/src/test/resources/app_config.json new file mode 100644 index 0000000000..735c43894a --- /dev/null +++ b/embrace-android-sdk/src/test/resources/app_config.json @@ -0,0 +1,3 @@ +{ + "report_disk_usage": false +} diff --git a/embrace-android-sdk/src/test/resources/app_info_expected.json b/embrace-android-sdk/src/test/resources/app_info_expected.json new file mode 100644 index 0000000000..c51203823b --- /dev/null +++ b/embrace-android-sdk/src/test/resources/app_info_expected.json @@ -0,0 +1,20 @@ +{ + "v": "1.0", + "f": 1, + "bi": "1234", + "bt": "release", + "fl": "demo", + "e": "prod", + "vu": false, + "vul": false, + "bv": "5ac7fe", + "ou": false, + "oul": false, + "sdk": "5.11.0", + "sdc": "5.10.0", + "rn": "fba09c9f", + "jsp": "53", + "rnv": "0.69.2", + "unv": "2019", + "ubg": "5092abc" +} diff --git a/embrace-android-sdk/src/test/resources/application_exit_info_local_config.json b/embrace-android-sdk/src/test/resources/application_exit_info_local_config.json new file mode 100644 index 0000000000..50a2db2852 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/application_exit_info_local_config.json @@ -0,0 +1,4 @@ +{ + "app_exit_info_traces_limit": 10, + "aei_enabled": true +} diff --git a/embrace-android-sdk/src/test/resources/application_exit_info_remote_config.json b/embrace-android-sdk/src/test/resources/application_exit_info_remote_config.json new file mode 100644 index 0000000000..940d08a477 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/application_exit_info_remote_config.json @@ -0,0 +1,5 @@ +{ + "app_exit_info_traces_limit": 100, + "pct_aei_enabled_v2": 100, + "aei_max_num": 50 +} diff --git a/embrace-android-sdk/src/test/resources/auto_data_capture_config.json b/embrace-android-sdk/src/test/resources/auto_data_capture_config.json new file mode 100644 index 0000000000..68bb2ac02c --- /dev/null +++ b/embrace-android-sdk/src/test/resources/auto_data_capture_config.json @@ -0,0 +1,6 @@ +{ + "memory_info": false, + "power_save_mode_info": false, + "network_connectivity_info": false, + "anr_info": false +} diff --git a/embrace-android-sdk/src/test/resources/background_activity_config.json b/embrace-android-sdk/src/test/resources/background_activity_config.json new file mode 100644 index 0000000000..081f07df07 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/background_activity_config.json @@ -0,0 +1,6 @@ +{ + "capture_enabled": true, + "manual_background_activity_limit": 15, + "min_background_activity_duration": 10000, + "max_cached_activities": 16 +} diff --git a/embrace-android-sdk/src/test/resources/base_url_config.json b/embrace-android-sdk/src/test/resources/base_url_config.json new file mode 100644 index 0000000000..6221100242 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/base_url_config.json @@ -0,0 +1,6 @@ +{ + "config": "https://config.example.com", + "data": "https://data.example.com", + "data_dev": "https://data-dev.example.com", + "images": "https://images.example.com" +} diff --git a/embrace-android-sdk/src/test/resources/bg_activity_config.json b/embrace-android-sdk/src/test/resources/bg_activity_config.json new file mode 100644 index 0000000000..09e96ee7fa --- /dev/null +++ b/embrace-android-sdk/src/test/resources/bg_activity_config.json @@ -0,0 +1,3 @@ +{ + "threshold": 0.5 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/bg_activity_expected.json b/embrace-android-sdk/src/test/resources/bg_activity_expected.json new file mode 100644 index 0000000000..efc925efa0 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/bg_activity_expected.json @@ -0,0 +1,36 @@ +{ + "id": "fake-session-id", + "st": 123456789, + "as": "foreground", + "et": 987654321, + "sn": 5, + "ty": "fake-message-type", + "ht": 123456780, + "cs": true, + "ss": [ + "fake-event-id" + ], + "il": [ + "fake-info-id" + ], + "wl": [ + "fake-warn-id" + ], + "el": [ + "fake-err-id" + ], + "lic": 1, + "lwc": 2, + "lec": 3, + "e": { + "c": 0, + "rep": [] + }, + "ri": "fake-crash-id", + "em": "bs", + "sm": "bs", + "sp": { + "fake-key": "fake-value" + }, + "ue": 1 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/bg_activity_message_expected.json b/embrace-android-sdk/src/test/resources/bg_activity_message_expected.json new file mode 100644 index 0000000000..6d3b99947b --- /dev/null +++ b/embrace-android-sdk/src/test/resources/bg_activity_message_expected.json @@ -0,0 +1,44 @@ +{ + "s": { + "id": "fake-activity", + "st": 0, + "as": "" + }, + "u": { + "id": "fake-user-id" + }, + "a": { + "v": "fake-app-id" + }, + "d": { + "dm": "fake-manufacturer" + }, + "p": { + "ds": { + "as": 1, + "fs": 2 + } + }, + "br": { + "cb": [ + { + "m": "fake-breadcrumb", + "ts": 1 + } + ] + }, + "spans": [ + { + "trace_id": "fake-span-id", + "span_id": "", + "parent_span_id": "", + "name": "", + "start_time_unix_nano": 0, + "end_time_unix_nano": 0, + "status": "OK", + "events": [], + "attributes": {} + } + ], + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/breadcrumb_custom.json b/embrace-android-sdk/src/test/resources/breadcrumb_custom.json new file mode 100644 index 0000000000..e29861ff18 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/breadcrumb_custom.json @@ -0,0 +1,27 @@ +{ + "s": { + "id": "fakeSessionId", + "st": 160000000000, + "sn": 1, + "ty": "st", + "as": "foreground", + "cs": true, + "sm": "s", + "sp": {} + }, + "br": { + "vb": [], + "tb": [], + "cb": [ + { + "m": "a breadcrumb", + "ts": 1577836801000 + } + ], + "wv": [], + "cv": [], + "rna": [], + "pn": [] + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/breadcrumb_empty.json b/embrace-android-sdk/src/test/resources/breadcrumb_empty.json new file mode 100644 index 0000000000..4d406bd097 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/breadcrumb_empty.json @@ -0,0 +1,22 @@ +{ + "s": { + "id": "fakeSessionId", + "st": 160000000000, + "sn": 1, + "ty": "st", + "as": "foreground", + "cs": true, + "sm": "s", + "sp": {} + }, + "br": { + "vb": [], + "tb": [], + "cb": [], + "wv": [], + "cv": [], + "rna": [], + "pn": [] + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/breadcrumb_fragment.json b/embrace-android-sdk/src/test/resources/breadcrumb_fragment.json new file mode 100644 index 0000000000..70005e28d5 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/breadcrumb_fragment.json @@ -0,0 +1,33 @@ +{ + "s": { + "id": "fakeSessionId", + "st": 160000000000, + "sn": 1, + "ty": "st", + "as": "foreground", + "cs": true, + "sm": "s", + "sp": {} + }, + "br": { + "vb": [], + "tb": [], + "cb": [], + "wv": [], + "cv": [ + { + "n": "b", + "st": 1577836803000, + "en": 1577836804000 + }, + { + "n": "a", + "st": 1577836801000, + "en": 1577836802000 + } + ], + "rna": [], + "pn": [] + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/breadcrumb_view.json b/embrace-android-sdk/src/test/resources/breadcrumb_view.json new file mode 100644 index 0000000000..ad3c4647ae --- /dev/null +++ b/embrace-android-sdk/src/test/resources/breadcrumb_view.json @@ -0,0 +1,33 @@ +{ + "s": { + "id": "fakeSessionId", + "st": 160000000000, + "sn": 1, + "ty": "st", + "as": "foreground", + "cs": true, + "sm": "s", + "sp": {} + }, + "br": { + "vb": [ + { + "vn": "viewB", + "st": 1577836802000, + "en": 1577836803000 + }, + { + "vn": "viewA", + "st": 1577836801000, + "en": 1577836802000 + } + ], + "tb": [], + "cb": [], + "wv": [], + "cv": [], + "rna": [], + "pn": [] + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/breadcrumb_view_custom.json b/embrace-android-sdk/src/test/resources/breadcrumb_view_custom.json new file mode 100644 index 0000000000..4cc4332b1d --- /dev/null +++ b/embrace-android-sdk/src/test/resources/breadcrumb_view_custom.json @@ -0,0 +1,38 @@ +{ + "s": { + "id": "fakeSessionId", + "st": 160000000000, + "sn": 1, + "ty": "st", + "as": "foreground", + "cs": true, + "sm": "s", + "sp": {} + }, + "br": { + "vb": [], + "tb": [], + "cb": [ + { + "m": "a breadcrumb", + "ts": 1577836804000 + } + ], + "wv": [], + "cv": [ + { + "n": "b", + "st": 1577836803000, + "en": 1577836804000 + }, + { + "n": "a", + "st": 1577836801000, + "en": 1577836802000 + } + ], + "rna": [], + "pn": [] + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/breadcrumb_webview.json b/embrace-android-sdk/src/test/resources/breadcrumb_webview.json new file mode 100644 index 0000000000..fbe9e1fbe8 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/breadcrumb_webview.json @@ -0,0 +1,31 @@ +{ + "s": { + "id": "fakeSessionId", + "st": 160000000000, + "sn": 1, + "ty": "st", + "as": "foreground", + "cs": true, + "sm": "s", + "sp": {} + }, + "br": { + "vb": [], + "tb": [], + "cb": [], + "wv": [ + { + "u": "https://example.com/path2", + "st": 1577836803000 + }, + { + "u": "https://example.com/path1", + "st": 1577836802000 + } + ], + "cv": [], + "rna": [], + "pn": [] + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/breadcrumbs_expected.json b/embrace-android-sdk/src/test/resources/breadcrumbs_expected.json new file mode 100644 index 0000000000..f223d26b14 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/breadcrumbs_expected.json @@ -0,0 +1,54 @@ +{ + "vb": [ + { + "vn": "View", + "st": 1600000000 + } + ], + "tb": [ + { + "tl": "0,0", + "tt": "Tap", + "ts": 1600000000, + "t": "s" + } + ], + "cb": [ + { + "m": "Custom", + "ts": 1600000000 + } + ], + "wv": [ + { + "u": "WebView", + "st": 1600000000 + } + ], + "cv": [ + { + "n": "Fragment", + "st": 1600000000, + "en": 1600005000 + } + ], + "rna": [ + { + "n": "RnAction", + "st": 1600000000, + "en": 1600005000, + "p": {}, + "pz": 0, + "o": "output" + } + ], + "pn": [ + { + "ti": "PushNotification", + "bd": "body", + "tp": "from", + "id": "id", + "ts": 1600000000 + } + ] +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/crash_expected.json b/embrace-android-sdk/src/test/resources/crash_expected.json new file mode 100644 index 0000000000..06ab0a9422 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/crash_expected.json @@ -0,0 +1,26 @@ +{ + "id": "123", + "ex": [ + { + "tt": [ + "stacktrace.line" + ], + "n": "java.lang.RuntimeException", + "m": "ExceptionMessage" + } + ], + "rep_js": [ + "js_exception" + ], + "th": [ + { + "threadId": 123, + "state": "RUNNABLE", + "n": "ReferenceHandler", + "p": 1, + "tt": [ + "stacktrace.line.thread" + ] + } + ] +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/crash_handler_config.json b/embrace-android-sdk/src/test/resources/crash_handler_config.json new file mode 100644 index 0000000000..010732bf7f --- /dev/null +++ b/embrace-android-sdk/src/test/resources/crash_handler_config.json @@ -0,0 +1,3 @@ +{ + "enabled": false +} diff --git a/embrace-android-sdk/src/test/resources/custom_breadcrumb_expected.json b/embrace-android-sdk/src/test/resources/custom_breadcrumb_expected.json new file mode 100644 index 0000000000..8c80bcfa29 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/custom_breadcrumb_expected.json @@ -0,0 +1,4 @@ +{ + "m": "test", + "ts": 1600000000 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/device_info_expected.json b/embrace-android-sdk/src/test/resources/device_info_expected.json new file mode 100644 index 0000000000..edd9ab100f --- /dev/null +++ b/embrace-android-sdk/src/test/resources/device_info_expected.json @@ -0,0 +1,15 @@ +{ + "dm": "samsung", + "do": "S20", + "da": "armeabi", + "jb": false, + "lc": "en-US", + "ms": 150982302, + "os": "android", + "ov": "10.2.1", + "oc": 29, + "sr": "1080x720", + "tz": "GMT+1", + "up": 150923, + "nc": 8 +} diff --git a/embrace-android-sdk/src/test/resources/disk_usage_expected.json b/embrace-android-sdk/src/test/resources/disk_usage_expected.json new file mode 100644 index 0000000000..513f4306a8 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/disk_usage_expected.json @@ -0,0 +1,4 @@ +{ + "as": 150982302, + "fs": 150923 +} diff --git a/embrace-android-sdk/src/test/resources/domain_config.json b/embrace-android-sdk/src/test/resources/domain_config.json new file mode 100644 index 0000000000..2a107a350a --- /dev/null +++ b/embrace-android-sdk/src/test/resources/domain_config.json @@ -0,0 +1,4 @@ +{ + "domain_name": "example-apis.com", + "domain_limit": 400 +} diff --git a/embrace-android-sdk/src/test/resources/empty_file.txt b/embrace-android-sdk/src/test/resources/empty_file.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/embrace-android-sdk/src/test/resources/event_expected.json b/embrace-android-sdk/src/test/resources/event_expected.json new file mode 100644 index 0000000000..8a9b3811a0 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/event_expected.json @@ -0,0 +1,15 @@ +{ "pr": { + "Float": 1, + "String": "TestString" + }, + "sp": {}, + "n": "test", + "li": "messageId", + "id": "eventId", + "si": "sessionId", + "t": "warning", + "ts": 1111, + "sc": false, + "st": "active", + "et": "none" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/eventmessage_expected.json b/embrace-android-sdk/src/test/resources/eventmessage_expected.json new file mode 100644 index 0000000000..33f68a53ab --- /dev/null +++ b/embrace-android-sdk/src/test/resources/eventmessage_expected.json @@ -0,0 +1,34 @@ +{ + "et": { + "pr": { + "Float": 1, + "String": "TestString" + }, + "sp": {}, + "n": "test", + "li": "messageId", + "id": "eventId", + "si": "sessionId", + "t": "warning", + "ts": 1111, + "sc": false, + "st": "active", + "et": "none" + }, + "u": { + "per": [ + "first_day" + ] + }, + "p": { + "ds": { + "fs": 3862863872 + }, + "lp": [ + { + "st": 1679580212117 + } + ] + }, + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/exception_error_info_expected.json b/embrace-android-sdk/src/test/resources/exception_error_info_expected.json new file mode 100644 index 0000000000..1832d92505 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/exception_error_info_expected.json @@ -0,0 +1,14 @@ +{ + "ts": 0, + "s": "STATE", + "ex": [ + { + "tt": [ + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ], + "n": "java.lang.IllegalStateException", + "m": "Whoops!" + } + ] +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/exception_info_expected.json b/embrace-android-sdk/src/test/resources/exception_info_expected.json new file mode 100644 index 0000000000..530c9e608e --- /dev/null +++ b/embrace-android-sdk/src/test/resources/exception_info_expected.json @@ -0,0 +1,8 @@ +{ + "tt": [ + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ], + "n": "java.lang.IllegalStateException", + "m": "Whoops!" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/expected_core_vital_repeated_elements_script.json b/embrace-android-sdk/src/test/resources/expected_core_vital_repeated_elements_script.json new file mode 100644 index 0000000000..ad6bfd1222 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/expected_core_vital_repeated_elements_script.json @@ -0,0 +1,85 @@ + +{ + "ts": 1111, + "u": "https://embrace.io/", + "vt": [ + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "CLS", + "st": 1111, + "d": 10, + "s": 0.1, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "CLS", + "st": 1111, + "d": 30, + "s": 0.1, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "CLS", + "st": 1111, + "d": 20, + "s": 0.1, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "LCP", + "st": 2222 + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "LCP", + "st": 2222 + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FCP", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FID", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FCP", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FID", + "st": 1111, + "d": 0, + "p": { + } + } + ] +} diff --git a/embrace-android-sdk/src/test/resources/expected_core_vital_script.json b/embrace-android-sdk/src/test/resources/expected_core_vital_script.json new file mode 100644 index 0000000000..5b8b351aa8 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/expected_core_vital_script.json @@ -0,0 +1,47 @@ + +{ + "ts": 1111, + "u": "https://embrace.io/", + "vt": [ + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "CLS", + "st": 1111, + "d": 10, + "s": 0.1, + "p": { + "value1": 0.003, + "value2": true, + "value3": [] + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "LCP", + "st": 2222, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FCP", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FID", + "st": 1111, + "d": 0, + "p": { + } + } + ] +} diff --git a/embrace-android-sdk/src/test/resources/expected_core_vital_script1.json b/embrace-android-sdk/src/test/resources/expected_core_vital_script1.json new file mode 100644 index 0000000000..010b129f81 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/expected_core_vital_script1.json @@ -0,0 +1,47 @@ + +{ + "ts": 2222, + "u": "https://embrace.io/", + "vt": [ + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "CLS", + "st": 1111, + "d": 10, + "s": 0.1, + "p": { + "value1": 0.003, + "value2": true, + "value3": [] + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "LCP", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FCP", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FID", + "st": 1111, + "d": 0, + "p": { + } + } + ] +} diff --git a/embrace-android-sdk/src/test/resources/expected_core_vital_script_repeated.json b/embrace-android-sdk/src/test/resources/expected_core_vital_script_repeated.json new file mode 100644 index 0000000000..3dac87ad99 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/expected_core_vital_script_repeated.json @@ -0,0 +1,47 @@ + +{ + "ts": 1111, + "u": "https://embrace.io/", + "vt": [ + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "CLS", + "st": 1111, + "d": 20, + "s": 0.1, + "p": { + "value1": 0.003, + "value2": true, + "value3": [] + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "LCP", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FCP", + "st": 1111, + "d": 0, + "p": { + } + }, + { + "key": "EMBRACE_METRIC", + "n": "layout-shift", + "t": "FID", + "st": 1111, + "d": 0, + "p": { + } + } + ] +} diff --git a/embrace-android-sdk/src/test/resources/fragment_breadcrumb_expected.json b/embrace-android-sdk/src/test/resources/fragment_breadcrumb_expected.json new file mode 100644 index 0000000000..a08ad02a87 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/fragment_breadcrumb_expected.json @@ -0,0 +1,5 @@ +{ + "n": "test", + "st": 1600000000, + "en": 1600001000 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/js_exception_expected.json b/embrace-android-sdk/src/test/resources/js_exception_expected.json new file mode 100644 index 0000000000..9d54b2e1f2 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/js_exception_expected.json @@ -0,0 +1,6 @@ +{ + "n": "java.lang.IllegalStateException", + "m": "Whoops!", + "t": "JsError", + "st": "foo(:20:21)" +} diff --git a/embrace-android-sdk/src/test/resources/local_network_config.json b/embrace-android-sdk/src/test/resources/local_network_config.json new file mode 100644 index 0000000000..eb722f87bc --- /dev/null +++ b/embrace-android-sdk/src/test/resources/local_network_config.json @@ -0,0 +1,11 @@ +{ + "default_capture_limit": 200, + "domains": [{ + "domain_name": "google.com", + "domain_limit": 80 + }], + "trace_id_header": "x-my-header-id", + "capture_request_content_length": true, + "disabled_url_patterns": ["[A-z]"], + "enable_native_monitoring": false +} diff --git a/embrace-android-sdk/src/test/resources/log_config.json b/embrace-android-sdk/src/test/resources/log_config.json new file mode 100644 index 0000000000..3e92caa003 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/log_config.json @@ -0,0 +1,6 @@ +{ + "max_length": 768, + "info_limit": 50, + "warn_limit": 200, + "error_limit": 500 +} diff --git a/embrace-android-sdk/src/test/resources/memory_warning_expected.json b/embrace-android-sdk/src/test/resources/memory_warning_expected.json new file mode 100644 index 0000000000..c234081407 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/memory_warning_expected.json @@ -0,0 +1,3 @@ +{ + "ts": 16098234098234 +} diff --git a/embrace-android-sdk/src/test/resources/metadata_appinfo_expected.json b/embrace-android-sdk/src/test/resources/metadata_appinfo_expected.json new file mode 100644 index 0000000000..15e54e0843 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/metadata_appinfo_expected.json @@ -0,0 +1,21 @@ +{ + "v": "1.0.0", + "f": 1, + "bi": "1234", + "bt": "debug", + "fl": "free", + "e": "UNKNOWN", + "vu": false, + "vul": false, + "bv": "10", + "ou": false, + "oul": false, + "sdk": "{versionName}", + "sdc": "{versionCode}", + "rn": null, + "jsp": null, + "rnv": null, + "unv": null, + "ubg": null, + "usv": null +} diff --git a/embrace-android-sdk/src/test/resources/metadata_react_native_appinfo_expected.json b/embrace-android-sdk/src/test/resources/metadata_react_native_appinfo_expected.json new file mode 100644 index 0000000000..cf1a3138ce --- /dev/null +++ b/embrace-android-sdk/src/test/resources/metadata_react_native_appinfo_expected.json @@ -0,0 +1,21 @@ +{ + "v": "1.0.0", + "f": 2, + "bi": "1234", + "bt": "debug", + "fl": "free", + "e": "UNKNOWN", + "vu": false, + "vul": false, + "bv": "10", + "ou": false, + "oul": false, + "sdk": "{versionName}", + "sdc": "{versionCode}", + "rn": "1234", + "jsp": null, + "rnv": null, + "unv": null, + "ubg": null, + "usv": null +} diff --git a/embrace-android-sdk/src/test/resources/native_crash_data_error_expected.json b/embrace-android-sdk/src/test/resources/native_crash_data_error_expected.json new file mode 100644 index 0000000000..a735005154 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/native_crash_data_error_expected.json @@ -0,0 +1,4 @@ +{ + "n": 5, + "c": 2 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/native_crash_data_expected.json b/embrace-android-sdk/src/test/resources/native_crash_data_expected.json new file mode 100644 index 0000000000..987366d8f3 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/native_crash_data_expected.json @@ -0,0 +1,24 @@ +{ + "report_id": "report_id", + "sid": "sid", + "ts": 1610000000000, + "state": "app_state", + "meta": { + "a": {}, + "d": {}, + "u": {}, + "sp": {} + }, + "ue": 2, + "crash": "crash", + "symbols": { + "key": "value" + }, + "errors": [ + { + "n": 5, + "c": 2 + } + ], + "map": "map" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/native_crash_expected.json b/embrace-android-sdk/src/test/resources/native_crash_expected.json new file mode 100644 index 0000000000..64b9db9866 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/native_crash_expected.json @@ -0,0 +1,15 @@ +{ + "id": "id", + "m": "crashMessage", + "sb": { + "key": "value" + }, + "er": [ + { + "n": 5, + "c": 2 + } + ], + "ue": 2, + "ma": "map" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/native_crash_metadata_expected.json b/embrace-android-sdk/src/test/resources/native_crash_metadata_expected.json new file mode 100644 index 0000000000..1e086aece6 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/native_crash_metadata_expected.json @@ -0,0 +1,14 @@ +{ + "a": { + "v": "1.0" + }, + "d": { + "dm": "samsung" + }, + "u": { + "id": "123" + }, + "sp": { + "key": "value" + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/native_crash_raw.txt b/embrace-android-sdk/src/test/resources/native_crash_raw.txt new file mode 100644 index 0000000000..a4fbf6d648 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/native_crash_raw.txt @@ -0,0 +1,53 @@ +{ + "meta": { + "a": { + "v": "1.0", + "f": 1, + "bi": "351E8925D6C7465F96AEFB99144432DB", + "bt": "debug", + "fl": null, + "e": "dev", + "vu": false, + "vul": false, + "bv": "1", + "ou": false, + "oul": false, + "sdk": "5.3.0-SNAPSHOT", + "sdc": "53", + "rn": "351E8925D6C7465F96AEFB99144432DB", + "jsp": null, + "rnv": null, + "unv": null, + "ubg": null + }, + "d": { + "dm": "Google", + "do": "sdk_gphone64_arm64", + "da": "arm64-v8a", + "jb": false, + "lc": "en_US", + "ms": "812531712", + "os": "Android OS", + "ov": "12", + "oc": "32", + "sr": "1080x2176", + "tz": "GMT", + "up": "122243039" + }, + "u": { + "id": "🔥", + "em": null, + "un": null, + "per": [ + "first_day" + ] + }, + "sp": {} + }, + "report_id": "EB1D08F2C1854054975A13E611127E98", + "v": "1", + "ts": 1656442874565, + "sid": "EB96C6A8AF09449A8547C7703CE6BDAE", + "state": "active", + "crash": "ewogICAgImVuIjogIlNJR1RSQVAiLAogICAgImVtIjogIlRyYWNlXC9icmVha3BvaW50IHRyYXAiLAogICAgImVjIjogMSwKICAgICJlZSI6IDAsCiAgICAiZXMiOiA1LAogICAgImZhIjogNDg2Mjc4MjYxMzgwLAogICAgImZyIjogWwogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvZGF0YVwvYXBwXC9+fkZhVnFSNHRfV3ZNWHRRRmdZNHdkM3c9PVwvY29tLm5pbWJsZS5tcnRvdGVtLnRlc3RuZGtjcmFzaGNhcHR1cmUtODlfNXBTamhyTDgzVDB4M2h3MUJrZz09XC9saWJcL2FybTY0XC9saWJuYXRpdmUtbGliLWNyYXNoLXRlc3Quc28iLAogICAgICAgICAgICAibWQiOiAiSmF2YV9jb21fbmltYmxlX21ydG90ZW1fdGVzdG5ka2NyYXNoY2FwdHVyZV9NYWluQWN0aXZpdHlfY3Jhc2gyIiwKICAgICAgICAgICAgImZhIjogNDg2Mjc4MjYxMzgwLAogICAgICAgICAgICAib2EiOiA0ODYyNzgyNjEzMDAsCiAgICAgICAgICAgICJtYSI6IDQ4NjI3ODE5NzI0OCwKICAgICAgICAgICAgImxuIjogNjQxMzIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIiIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MDU0ODE2OCwKICAgICAgICAgICAgIm9hIjogMCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAyOTc5NDAwCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA1MDkwMzIsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjk0MDI2NAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiX1pOM2FydDExaW50ZXJwcmV0ZXIzNEFydEludGVycHJldGVyVG9Db21waWxlZENvZGVCcmlkZ2VFUE5TXzZUaHJlYWRFUE5TXzlBcnRNZXRob2RFUE5TXzExU2hhZG93RnJhbWVFdFBOU182SlZhbHVlRSIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MDY1ODEzMiwKICAgICAgICAgICAgIm9hIjogNDg2OTgwNjU3ODE2LAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDMwODkzNjQKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIl9aTjNhcnQxMWludGVycHJldGVyNkRvQ2FsbElMYjBFTGIwRUVFYlBOU185QXJ0TWV0aG9kRVBOU182VGhyZWFkRVJOU18xMVNoYWRvd0ZyYW1lRVBLTlNfMTFJbnN0cnVjdGlvbkV0UE5TXzZKVmFsdWVFIiwKICAgICAgICAgICAgImZhIjogNDg2OTgxNzYxMTMyLAogICAgICAgICAgICAib2EiOiA0ODY5ODE3NjAzMDgsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogNDE5MjM2NAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiTXRlcnBJbnZva2VWaXJ0dWFsIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwNDY5NzQ0LAogICAgICAgICAgICAib2EiOiA0ODY5ODA0NjQzNjAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjkwMDk3NgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwNDg2MTY4LAogICAgICAgICAgICAib2EiOiAwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI5MTc0MDAKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIiIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MDE3OTc4OCwKICAgICAgICAgICAgIm9hIjogMCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAyNjExMDIwCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICJfWk4zYXJ0MTFpbnRlcnByZXRlcjMzQXJ0SW50ZXJwcmV0ZXJUb0ludGVycHJldGVyQnJpZGdlRVBOU182VGhyZWFkRVJLTlNfMjBDb2RlSXRlbURhdGFBY2Nlc3NvckVQTlNfMTFTaGFkb3dGcmFtZUVQTlNfNkpWYWx1ZUUiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODEyNTIyMjAsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4MTI1MjA2OCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAzNjgzNDUyCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICJfWk4zYXJ0MTFpbnRlcnByZXRlcjZEb0NhbGxJTGIwRUxiMEVFRWJQTlNfOUFydE1ldGhvZEVQTlNfNlRocmVhZEVSTlNfMTFTaGFkb3dGcmFtZUVQS05TXzExSW5zdHJ1Y3Rpb25FdFBOU182SlZhbHVlRSIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MTc2MTc2NCwKICAgICAgICAgICAgIm9hIjogNDg2OTgxNzYwMzA4LAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDQxOTI5OTYKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIk10ZXJwSW52b2tlVmlydHVhbCIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MDQ2OTc0NCwKICAgICAgICAgICAgIm9hIjogNDg2OTgwNDY0MzYwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI5MDA5NzYKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIiIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MDQ4NjE2OCwKICAgICAgICAgICAgIm9hIjogMCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAyOTE3NDAwCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODAxNzk3ODgsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjYxMTAyMAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiX1pOM2FydDExaW50ZXJwcmV0ZXIzM0FydEludGVycHJldGVyVG9JbnRlcnByZXRlckJyaWRnZUVQTlNfNlRocmVhZEVSS05TXzIwQ29kZUl0ZW1EYXRhQWNjZXNzb3JFUE5TXzExU2hhZG93RnJhbWVFUE5TXzZKVmFsdWVFIiwKICAgICAgICAgICAgImZhIjogNDg2OTgxMjUyMjIwLAogICAgICAgICAgICAib2EiOiA0ODY5ODEyNTIwNjgsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMzY4MzQ1MgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiX1pOM2FydDExaW50ZXJwcmV0ZXI2RG9DYWxsSUxiMEVMYjBFRUViUE5TXzlBcnRNZXRob2RFUE5TXzZUaHJlYWRFUk5TXzExU2hhZG93RnJhbWVFUEtOU18xMUluc3RydWN0aW9uRXRQTlNfNkpWYWx1ZUUiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODE3NjE3NjQsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4MTc2MDMwOCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiA0MTkyOTk2CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICJNdGVycEludm9rZUludGVyZmFjZSIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MTYzMDU0MCwKICAgICAgICAgICAgIm9hIjogNDg2OTgxNjI1NjI0LAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDQwNjE3NzIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIiIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MDQ4NjY4MCwKICAgICAgICAgICAgIm9hIjogMCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAyOTE3OTEyCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICJNdGVycEludm9rZVZpcnR1YWwiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA0NjY2NTYsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4MDQ2NDM2MCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAyODk3ODg4CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA0ODYxNjgsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjkxNzQwMAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwMTc5Nzg4LAogICAgICAgICAgICAib2EiOiAwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI2MTEwMjAKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIl9aTjNhcnQxMWludGVycHJldGVyMzNBcnRJbnRlcnByZXRlclRvSW50ZXJwcmV0ZXJCcmlkZ2VFUE5TXzZUaHJlYWRFUktOU18yMENvZGVJdGVtRGF0YUFjY2Vzc29yRVBOU18xMVNoYWRvd0ZyYW1lRVBOU182SlZhbHVlRSIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MTI1MjIyMCwKICAgICAgICAgICAgIm9hIjogNDg2OTgxMjUyMDY4LAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDM2ODM0NTIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIl9aTjNhcnQxMWludGVycHJldGVyNkRvQ2FsbElMYjBFTGIwRUVFYlBOU185QXJ0TWV0aG9kRVBOU182VGhyZWFkRVJOU18xMVNoYWRvd0ZyYW1lRVBLTlNfMTFJbnN0cnVjdGlvbkV0UE5TXzZKVmFsdWVFIiwKICAgICAgICAgICAgImZhIjogNDg2OTgxNzYxNzY0LAogICAgICAgICAgICAib2EiOiA0ODY5ODE3NjAzMDgsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogNDE5Mjk5NgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiTXRlcnBJbnZva2VEaXJlY3QiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODE3NTY3NzYsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4MTc1NTE5MiwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiA0MTg4MDA4CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA0ODY0MjQsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjkxNzY1NgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwMTc5Nzg4LAogICAgICAgICAgICAib2EiOiAwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI2MTEwMjAKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIl9aTjNhcnQxMWludGVycHJldGVyMzNBcnRJbnRlcnByZXRlclRvSW50ZXJwcmV0ZXJCcmlkZ2VFUE5TXzZUaHJlYWRFUktOU18yMENvZGVJdGVtRGF0YUFjY2Vzc29yRVBOU18xMVNoYWRvd0ZyYW1lRVBOU182SlZhbHVlRSIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MTI1MjIyMCwKICAgICAgICAgICAgIm9hIjogNDg2OTgxMjUyMDY4LAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDM2ODM0NTIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIl9aTjNhcnQxMWludGVycHJldGVyNkRvQ2FsbElMYjBFTGIwRUVFYlBOU185QXJ0TWV0aG9kRVBOU182VGhyZWFkRVJOU18xMVNoYWRvd0ZyYW1lRVBLTlNfMTFJbnN0cnVjdGlvbkV0UE5TXzZKVmFsdWVFIiwKICAgICAgICAgICAgImZhIjogNDg2OTgxNzYxNzY0LAogICAgICAgICAgICAib2EiOiA0ODY5ODE3NjAzMDgsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogNDE5Mjk5NgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiTXRlcnBJbnZva2VTdGF0aWMiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODUzODYxOTIsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4NTM4MjM3NiwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiA3ODE3NDI0CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA0ODY1NTIsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjkxNzc4NAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiTXRlcnBJbnZva2VJbnRlcmZhY2UiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODE2MjgwODgsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4MTYyNTYyNCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiA0MDU5MzIwCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA0ODY2ODAsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjkxNzkxMgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiTXRlcnBJbnZva2VTdGF0aWMiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODUzODQ1MDAsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4NTM4MjM3NiwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiA3ODE1NzMyCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA0ODY1NTIsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjkxNzc4NAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiTXRlcnBJbnZva2VWaXJ0dWFsIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwNDY2NjU2LAogICAgICAgICAgICAib2EiOiA0ODY5ODA0NjQzNjAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjg5Nzg4OAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwNDg2MTY4LAogICAgICAgICAgICAib2EiOiAwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI5MTc0MDAKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIk10ZXJwSW52b2tlU3RhdGljIiwKICAgICAgICAgICAgImZhIjogNDg2OTg1Mzg0NTAwLAogICAgICAgICAgICAib2EiOiA0ODY5ODUzODIzNzYsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogNzgxNTczMgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwNDg2NTUyLAogICAgICAgICAgICAib2EiOiAwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI5MTc3ODQKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIk10ZXJwSW52b2tlU3RhdGljIiwKICAgICAgICAgICAgImZhIjogNDg2OTg1MzgzMDAwLAogICAgICAgICAgICAib2EiOiA0ODY5ODUzODIzNzYsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogNzgxNDIzMgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwNDg2NTUyLAogICAgICAgICAgICAib2EiOiAwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI5MTc3ODQKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIiIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MDE3OTc4OCwKICAgICAgICAgICAgIm9hIjogMCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAyNjExMDIwCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICJhcnRRdWlja1RvSW50ZXJwcmV0ZXJCcmlkZ2UiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODAxNzYxMTYsCiAgICAgICAgICAgICJvYSI6IDQ4Njk4MDE3NDkzNiwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAyNjA3MzQ4CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvbGliNjRcL2xpYmFydC5zbyIsCiAgICAgICAgICAgICJtZCI6ICIiLAogICAgICAgICAgICAiZmEiOiA0ODY5ODA1NDg0NzYsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDQ4Njk3NzU2ODc2OCwKICAgICAgICAgICAgImxuIjogMjk3OTcwOAogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAibW8iOiAiXC9hcGV4XC9jb20uYW5kcm9pZC5hcnRcL2xpYjY0XC9saWJhcnQuc28iLAogICAgICAgICAgICAibWQiOiAiIiwKICAgICAgICAgICAgImZhIjogNDg2OTgwNTA5Njc2LAogICAgICAgICAgICAib2EiOiAwLAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDI5NDA5MDgKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIl9aTjNhcnQxMkludm9rZU1ldGhvZElMTlNfMTFQb2ludGVyU2l6ZUU4RUVFUDhfam9iamVjdFJLTlNfMzNTY29wZWRPYmplY3RBY2Nlc3NBbHJlYWR5UnVubmFibGVFUzNfUzNfUzNfbSIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MTExOTc2MCwKICAgICAgICAgICAgIm9hIjogNDg2OTgxMTE5MTQ4LAogICAgICAgICAgICAibWEiOiA0ODY5Nzc1Njg3NjgsCiAgICAgICAgICAgICJsbiI6IDM1NTA5OTIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgIm1vIjogIlwvYXBleFwvY29tLmFuZHJvaWQuYXJ0XC9saWI2NFwvbGliYXJ0LnNvIiwKICAgICAgICAgICAgIm1kIjogIiIsCiAgICAgICAgICAgICJmYSI6IDQ4Njk4MTExOTExMiwKICAgICAgICAgICAgIm9hIjogMCwKICAgICAgICAgICAgIm1hIjogNDg2OTc3NTY4NzY4LAogICAgICAgICAgICAibG4iOiAzNTUwMzQ0CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtbyI6ICJcL2FwZXhcL2NvbS5hbmRyb2lkLmFydFwvamF2YWxpYlwvYXJtNjRcL2Jvb3Qub2F0IiwKICAgICAgICAgICAgIm1kIjogIiIsCiAgICAgICAgICAgICJmYSI6IDE4NjY0MTU5OTIsCiAgICAgICAgICAgICJvYSI6IDAsCiAgICAgICAgICAgICJtYSI6IDE4NjU2ODI5NDQsCiAgICAgICAgICAgICJsbiI6IDczMzA0OAogICAgICAgIH0KICAgIF0KfQ==" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/native_symbols_expected.json b/embrace-android-sdk/src/test/resources/native_symbols_expected.json new file mode 100644 index 0000000000..a080bd0e16 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/native_symbols_expected.json @@ -0,0 +1,12 @@ +{ + "symbols": { + "armeabi-v7a": { + "libfoo-armeabi-v7a.so": "0x1234", + "libbar-armeabi-v7a.so": "0x5678" + }, + "x86": { + "libfoo-x86.so": "0x1234", + "libbar-x86.so": "0x5678" + } + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/network_config.json b/embrace-android-sdk/src/test/resources/network_config.json new file mode 100644 index 0000000000..0ad431a962 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/network_config.json @@ -0,0 +1,6 @@ +{ + "defaultCaptureLimit": 2000, + "domains": { + "google.com": 500 + } +} diff --git a/embrace-android-sdk/src/test/resources/orientation_expected.json b/embrace-android-sdk/src/test/resources/orientation_expected.json new file mode 100644 index 0000000000..aae3123535 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/orientation_expected.json @@ -0,0 +1,4 @@ +{ + "o": "p", + "ts": 12345678 +} diff --git a/embrace-android-sdk/src/test/resources/perf_info_expected.json b/embrace-android-sdk/src/test/resources/perf_info_expected.json new file mode 100644 index 0000000000..8e8fd039e8 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/perf_info_expected.json @@ -0,0 +1,15 @@ +{ + "ds": { + "as": 10000000, + "fs": 2000000 + }, + "mw": [], + "ns": [], + "anr": [], + "ga": [], + "aei": [], + "nst": [], + "lp": [], + "nr": {}, + "v": [] +} diff --git a/embrace-android-sdk/src/test/resources/public_key_config.json b/embrace-android-sdk/src/test/resources/public_key_config.json new file mode 100644 index 0000000000..30b8bc3e68 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/public_key_config.json @@ -0,0 +1,3 @@ +{ + "capture_public_key": "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuAZAv5tzK9Ab/DsVpNaYiuslKQsOHjz4N4haZLT8VaVIrlVjtkd5nPrVgEKStQf6PKnQ+1C0Tp069b6aPUkG22UL96nCKQ1eCIwRUT+Da7ac2YVuL21+HTs1KxLEWgN7qGy1uYNonrpsiY3XqzDvYMo65oFzbBV+yctuGHDFaulULJiLL8cE3/Rg3T0RfHK+C5/PqC8FBj6kn3FP9FZJM4cty3nzbNWknj8r7+ikmOwma6CHEZz2u1gwPhIchNxNKuUF+4vxcBre9V/96LYOjSOGSDJmJN6ehUJjUpu7YSuGCki8YoLHAyoD/mYy7N/hYSeZwHiNjM+r44lZHNQTpwIDAQAB-----END PUBLIC KEY-----" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/push_notification_breadcrumb_expected.json b/embrace-android-sdk/src/test/resources/push_notification_breadcrumb_expected.json new file mode 100644 index 0000000000..bd6cc4b353 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/push_notification_breadcrumb_expected.json @@ -0,0 +1,9 @@ +{ + "ti": "title", + "bd": "body", + "tp": "from", + "id": "id", + "pt": 1, + "te": "type", + "ts": 1600000000 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/remote_config_response.json b/embrace-android-sdk/src/test/resources/remote_config_response.json new file mode 100644 index 0000000000..38f601e5d4 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/remote_config_response.json @@ -0,0 +1,15 @@ +{ + "ls": 100, + "event_limits": {}, + "offset": 0, + "personas": [], + "session_control": { + "enable": true, + "async_end": false + }, + "threshold": 100, + "ui": { + "views": 100 + }, + "urlconnection_request_enabled": true +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/rn_action_breadcrumb_expected.json b/embrace-android-sdk/src/test/resources/rn_action_breadcrumb_expected.json new file mode 100644 index 0000000000..d1e5396b5f --- /dev/null +++ b/embrace-android-sdk/src/test/resources/rn_action_breadcrumb_expected.json @@ -0,0 +1,10 @@ +{ + "n": "my_action", + "st": 1600000000, + "en": 1600000100, + "p": { + "key": "value" + }, + "pz": 104, + "o": "test" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/session_config.json b/embrace-android-sdk/src/test/resources/session_config.json new file mode 100644 index 0000000000..ea7c886d2c --- /dev/null +++ b/embrace-android-sdk/src/test/resources/session_config.json @@ -0,0 +1,7 @@ +{ + "max_session_seconds": 120, + "async_end": true, + "error_log_strict_mode": true, + "send_full_for": ["crash"], + "components": ["breadcrumbs"] +} diff --git a/embrace-android-sdk/src/test/resources/session_expected.json b/embrace-android-sdk/src/test/resources/session_expected.json new file mode 100644 index 0000000000..0a157de043 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/session_expected.json @@ -0,0 +1,63 @@ +{ + "id": "fake-session-id", + "st": 123456789, + "sn": 5, + "ty": "fake-message-type", + "as": "foreground", + "cs": true, + "et": 987654321, + "ht": 123456780, + "tt": 16090292309, + "ce": true, + "tr": true, + "ss": [ + "fake-event-id" + ], + "il": [ + "fake-info-id" + ], + "wl": [ + "fake-warn-id" + ], + "el": [ + "fake-err-id" + ], + "nc": [ + "fake-network-id" + ], + "lic": 1, + "lwc": 2, + "lec": 3, + "e": { + "c": 0, + "rep": [] + }, + "ri": "fake-crash-id", + "em": "s", + "sm": "s", + "oc": [ + { + "o": "p", + "ts": 16092342200 + } + ], + "sp": { + "fake-key": "fake-value" + }, + "sd": 1223, + "sdt": 5000, + "si": 109, + "ue": 1, + "bf": {}, + "sb": { + "fake-native-key": "fake-native-value" + }, + "wvi_beta": [ + { + "t": "fake-webview-id", + "vt": [], + "u": "fake-url", + "ts": 16090292309 + } + ] +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/session_message_expected.json b/embrace-android-sdk/src/test/resources/session_message_expected.json new file mode 100644 index 0000000000..7234cd23de --- /dev/null +++ b/embrace-android-sdk/src/test/resources/session_message_expected.json @@ -0,0 +1,49 @@ +{ + "s": { + "id": "fakeSessionId", + "st": 160000000000, + "sn": 1, + "ty": "st", + "as": "foreground", + "cs": true, + "sm": "s", + "sp": {} + }, + "u": { + "id": "fake-user-id", + "em": "fake-user-name" + }, + "a": { + "v": "fake-app-version" + }, + "d": { + "dm": "fake-manufacturer" + }, + "p": { + "ds": { + "as": 150923409, + "fs": 509209823 + } + }, + "br": { + "cb": [ + { + "m": "Hi", + "ts": 109234098234 + } + ] + }, + "spans": [ + { + "trace_id": "fake-span-id", + "span_id": "", + "name": "", + "start_time_unix_nano": 0, + "end_time_unix_nano": 0, + "status": "OK", + "events": [], + "attributes": {} + } + ], + "v": 13 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/span_expected.json b/embrace-android-sdk/src/test/resources/span_expected.json new file mode 100644 index 0000000000..d2cb5d172a --- /dev/null +++ b/embrace-android-sdk/src/test/resources/span_expected.json @@ -0,0 +1,28 @@ +{ + "attributes": { + "emb.sequence_id": "3", + "emb.type": "PERFORMANCE" + }, + "end_time_unix_nano": 1681972471871000000, + "events": [ + { + "attributes": { + "test1": "value1", + "test2": "value2" + }, + "name": "start-time", + "time_unix_nano": 1681972471807000000 + }, + { + "attributes": {}, + "name": "end-span-event", + "time_unix_nano": 1681972471871000000 + } + ], + "name": "emb-sdk-init", + "parent_span_id": "0000000000000000", + "span_id": "342eb9c7f8cb54ff", + "start_time_unix_nano": 1681972471806000000, + "status": "OK", + "trace_id": "19bb482ec1c7e6b2f10fb89e0ccc85fa" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/startup_moment_config.json b/embrace-android-sdk/src/test/resources/startup_moment_config.json new file mode 100644 index 0000000000..2a45ccfd6c --- /dev/null +++ b/embrace-android-sdk/src/test/resources/startup_moment_config.json @@ -0,0 +1,3 @@ +{ + "automatically_end": false +} diff --git a/embrace-android-sdk/src/test/resources/startup_sampling_config.json b/embrace-android-sdk/src/test/resources/startup_sampling_config.json new file mode 100644 index 0000000000..1f39535c59 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/startup_sampling_config.json @@ -0,0 +1,7 @@ +{ + "pct_enabled": 5, + "sample_granularity": 200, + "sample_interval": 50, + "sampling_duration": 5000, + "stacktrace_length": 200 +} diff --git a/embrace-android-sdk/src/test/resources/startup_sampling_default_config_expected.txt b/embrace-android-sdk/src/test/resources/startup_sampling_default_config_expected.txt new file mode 100644 index 0000000000..ec0fa3a65a --- /dev/null +++ b/embrace-android-sdk/src/test/resources/startup_sampling_default_config_expected.txt @@ -0,0 +1,5 @@ +pct_enabled=100 +sample_interval=1000 +sample_granularity=100 +sampling_duration=10000 +stacktrace_length=100 diff --git a/embrace-android-sdk/src/test/resources/startup_sampling_override_config_expected.txt b/embrace-android-sdk/src/test/resources/startup_sampling_override_config_expected.txt new file mode 100644 index 0000000000..92898f1d64 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/startup_sampling_override_config_expected.txt @@ -0,0 +1,5 @@ +pct_enabled=50 +sample_interval=2000 +sample_granularity=200 +sampling_duration=20000 +stacktrace_length=60 diff --git a/embrace-android-sdk/src/test/resources/tap_breadcrumb_expected.json b/embrace-android-sdk/src/test/resources/tap_breadcrumb_expected.json new file mode 100644 index 0000000000..3cb7f3f5f4 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/tap_breadcrumb_expected.json @@ -0,0 +1,6 @@ +{ + "tl": "0,0", + "tt": "tappedElementName", + "ts": 1600000000, + "t": "s" +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/taps_config.json b/embrace-android-sdk/src/test/resources/taps_config.json new file mode 100644 index 0000000000..beb628652f --- /dev/null +++ b/embrace-android-sdk/src/test/resources/taps_config.json @@ -0,0 +1,3 @@ +{ + "capture_coordinates": false +} diff --git a/embrace-android-sdk/src/test/resources/test_screenshot.jpg b/embrace-android-sdk/src/test/resources/test_screenshot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b14fc82123fc324a9f1a9997e402157afb2a4cb6 GIT binary patch literal 81667 zcmeFZbyQs6l0SN&v7o_%OMnCd!QCMQ4;tJPNU-4U?iL_G(8gT?!9BPJcbDMOcn6y1 zb-wq`ojWt@&F{Up=AZl4>|Xoy?sN8$s#Cjm)u(Fr!~DZK@K`}cUIsuy0)Wqm5Ad)7 zL`%C{egps|C4d^+rC)2K`$bmG94K566Ijs)e1ios)%~JuNpI2O#iD zUJ2#T)e+^d_Orj5kEf}k7vg{#lzsn4Uyq=#d>=Xid<;}Rm`_MZ$;h8QXJmT8%)`saFCZu+^;%j+R!&~w zt%jzSwvMizso95*<`$M#PR=f_ZtfnQLBS!RVc`*x35j2ll2g8>rsd}47Zes1mz36g zudSh9_7>mL{#nw*-RnVp+oSlrm$+Wxh(ySIOEc7Abrb$tWLLSf2V5+kOGkZDk#XvC}=1sC}mIaHV$A?F+VS|^{`4hzld})SxtE=rjDkEUY10{F9 zl2WU7?isDs72M@bfqrS~qT95|$2*q-4qtmE9s6}fKU<|0mTie@+8j?G|D=z0Ry*)3 zsdAdAy`HRXZwv{u+ zv>WFsrfe$GFdA9E9pcyt6 zUhx5QWoFrabL&SXp$}hh8`tBC1?ZYsY2dPLD)6SFb3H5J<)1E-+rMV`?S5q56Q_HX zTP}J}EUgUJ%PZqcyzGalDp_jG&&cyldZEfPjCXN!2cu7Ke)CqV@Ox*;?wm2yZ)SR* zw+5k{<1KP0XW0vR03sL?ydq>aTfUKqwmFJ37f`W{Fl#4(YYHejn&>CgCgvO;cis>k zoVRJeZi3kIwjpbCl;QC`5k#SW0F+nrD%{2@_P_1NA6yL9JVO55LwM6{&n*?FL3Bo9 z%d44oSFt>sn-*P}Gxu3W`eS8Xp|C;eaN@^D=|SfKU|Ub>FXgs7$w$+zjF4A9W0sig&_`<<8t!nHc+=My5CO87CEa{d2f zt)hnxZdS;4i;+oB8uW@TXK8;GD)m<+7LAhxR|;n1sVCnKJwkaq65gNv5de42WJ{6} zUucKIF)W2&6+2co<0|M zvFCrGD$fFLme>-{1iY|~=*Ej?z^_72p0`RS2Xib7XNgK22t_Vd(M;KhQB?Jm6jYlF z%^K7^z5@~eo_A2^|NW*U}UsMR1QWy5b&mE($6d&FyR3n zTb_fX)-xB^XF5!3jW5(L%uBNIviB;rw))yB>&^(=wLAve6DdsRd#JkUcluVduBytd zuE}xJ*(yrKQ&4D0Ot4HDkU9qsEI`VUJFKmy8+RSJ9g*2z(@DFZB$|TGGiXvMn(wrT z`+JQ+`j+trV;*03UOq;#Ij&uYH4aekBKwOHME-aHg416cJ`zzBIV8>qXCRP2hpY%3 zChpHRF8M|K;Z@p(6t}%d^gpF2&A5y2pI%o*FIGd0>z$Yn5kV|~Bsy)-c#ujqawOCL zNX2`Y{!1N$y5V#WfVH0$UY+?-aJ4O(R%>& ztkc*}jptyXJu8-U$XW!ex0H1oM0?jxxMQnp3f`{$o4l;pz3|YuFKL7Ka|?d^ zCmRg$qAloKm=ocTd>tuz60=Ms)9KGeiC0A!3uURB)_oN(img(Nf>#AR*gi~M^1d%| zf@~S6KlFT)G^|mJhk0un7*7@6aA9)zN&Z~ye&}L z0r^(zNB>;V8|GKK^z-!10{|{y;Y(p!-k&PtH?FAgoqYhfg}dFpTU%F!vE;J62iMO~_Ke(}Y*4XsX)s0M4ZcF?PoaX_+-g8rW=jBxPBGyo@`ol@7(=Dt& za%jS34f;Aqh$P(9z30svb*7=3MDQ-$z07mvbyrf!ouJkvpKFJbGYq#{gxAupqO$J& zs^p^PmIQLUkKdS@!Lyw4|!-?Sx{l$Ta{?4bD{O` zo~-8kpQXfO>v&T^aW7=)MIL9v-zQAMdS`#7#m!u{HvAS<%h@nweI~i7xF9_`Gw>5W zfCs566rvew(Ys(WG6q>a03W|378>#o^@CYE#&!gctf!E$K`#b)30HTua4L<>t$91D z{8D$bL>~bC!r$Nn*aMJ$2~VwhUFp_K&VGH8a9w@IOmzmn4zufVFj!6B`ub_}2{nDJ z9NT*eog@Z6;oEJv#_=ezDZe_8wfwZJdL6h(s`QL+3@Upn@-s`(*L~}%N~C@50l=JG z8#!$B)&D83&`^5lM$Oq5mi-#8$Z_%EojH{tA`&4Cy%DmRt!ZiXafDl!QI3H@Z(`MHyBJT4nr1<%lZ5OxAvqRppO z!RhxF)5S6N92^2hGkcw=NE#-X7g!b2BD`C|>G#rIZ*uM+!j z45jdL$O^=w=g_uJyG!jZp5;ceaMv>BMgsp_Sw5dN1RZ(XN4U$S%A9W_q! zw<^jl9xUasqrBsmt=K(b#@p_!T1330n(y1=08j4-ny=k(lm)WGpkwVzn&0eme!6~_ zhWgOBjWv9zTOR}_4OzxusP)@ZBUrx;WLjey*AKiZ&ZIcA;|7LJlj zd!2y)?Q`CaRv$l_L9F%9R+0yRLEPX0sGRiM!`JEvZ*|%9qj1-|9(yxsR^6auq!<+u zu~dzwz&OOEPkQT0WljIX?_BC??Dm)u9zYD(dOv~Kg#@D`b?9WaFyq%54g z|MFp$IO2_$vgD$NU>}#LwOb!6W3U)YL!^lG$GK##&l3qq=-Z2&;1oyv@p~T}*)#FJ z8u#IDj=_ALVYF17u9GwqqS_FBt74RBqy%LjAvfSk)M%(*A1X^-R@#-gELCVHPbI}w z9Zt}hCCLCX*zK%9mu`bB9W=PcD>~t9eMyJC6keOuKf;Jl=kYCeTb zdu2)jI^*4X05H5iiLmp-o|4z)m+{Vw&pQN`Z>WZ9{QykGC;izG1MJ+mLJUEE^A$MC z_*{jT{pRfhAj9fG((7fT-31r>e)g;NxxT}%1dqP$^)l+ElpMqW7kPKa>_-JvxbBv$ zPGF+fOgONV>nb*RRmq(&8rLo229Q(JJOBx(Zt$bETdrIv_qs8jwGikqZ5hk$X;Pka zVv@80HA$g7BW$Mt@&Ld-vvk~id;lhrW?`5QKs=Ejg4L7$B4b<6ekFB8ICo&lk+ZV> zq11 zWL)&;^F^_0;9q^5(6=Au6YCg}HPFdH6Y#vPvdIp86`Gw% zN*kUWx$bff2ytABPir^rEe2UN8;}PTC4VqTdI0e36xw)ONnZRipc`nj@5KL2{u#(- zjz${SN&s_BZ+5N2s{{oUSA0;5w^S;-@7E&ng-TaYf?oc9F16rPw0ey7dL$ ziBH)c^}+0F)6wgnyNl838yYIqBxnSzed(Y@TR3bgxJ!Gi=GOtghFP|8iYl?z0a>F~ z=bbidgbEFQ05-58(|RJY3}34gw5a&&x#LU#YG~!`-^j71lPjP^#xoriE<%`vKpv=q zNEP`psm;63meswKEH%$C+x<=6?)}YP%zNBbL#7Eh%~TJ^~tE|9Z$5 z=wa?20{=Pn0PF_)-3%ZUhWPi|2jKSx&HcG}SEZT+(F5>LJK~Ox#o_L<4}fuETAAmJ_81pY8B$a>O!So+)(j*PM{SQ0-_pX0u`TuhGZ_@R@()4db z>3>Jg|8H~?BD?_m3%71HHJEXc^X0X;eWT4)QC=TChT%y65!qH@7X>2hN_=z)sA&{V z*B*KmAK5a-3hq1r+7ukP^YDYjM5ty7LoDf{&$x9aUBIDf`L{|V+DuP4 z^cZoGE=Ym`9Sl$4E)VtCt~I96oPl@|Eh zhEiFhTFX8^n32I$dP97Rp`wxfggm8jS2 ziC39M=M;u3A`8TiZp|(v$fq?aa8#6LOzxCZ9H>S-`t~ve#tJ2QDoQMeJ?=B+@T2@& zV7XhqYo|6p9Zn@kEXpZVh@cB3A+=NH)OrfhJ$HbkwWmKO3)-JU6k|vGE)*ZY z!rtGR(Z&k7Gw!sL_d3pvT>Ne!#`k#=)Em;oTX0TjXtQ|aWekf_Tf_PD)UCE*7SgVo zXwI<#4^h7YzzdalYmJ1LO8hO<+{dYKjH<_LlVa24FrMx#iQ!}O3VV+bZp_9ppQzC_ zmX?=JWQ3Up!zW6A6$Rj!shG9WrixZSUH*yA@&`kV5A^e~I9k&#HtcfYDy7T4pThhn zo3k0`tBH_uq(53Vuf~+;B+9jf?y;|)XBv}T{Sc_)gXU~4h_G7QJ&AGwTKJg~WqNg! z`P3b?1*w|EYLO1VX!(N6_VsGMTe_(k1b3PeA_5iMwHaC=0FidIH|~s!l5tl9zfj#s z0~hO7v86#1CIPpTcS62PZ^$wm)8X!166cTX>Q0~;8+8sI1i_H{|QngA1 zw98;LiJ~c^0{XdW<&`<(ffxhyImGH~>Qg7X$+MI0zmh=__@vxVFHLY|l`oN8BL}9h z?(w7-dA$Z$6&-VR5#oUCQI@P15?W--R=dB|7=GzKF9bKG^Jkqf;&nTFjsd6Fw-{er z%zSYiHTe9Jnc+9|f;(W`!KlUB#oEWVK}(%sLY#R|>cuVok8CU=HG@NFN0-C$eXNr5 z%m;K-?7@s%Q&EH)o}2smv5YM3tX4slMH-)Y-RaXh)&YWnVwG_M?myzF$oi`Lt-qn= zRKElMy2vLgKMuJPTQ=6?r`Ay=PnhPC&7Vr#CiVhK((iTLQ@WeWcjn%7G&FuO-;UMz zGfrj>M-`Q)M&85S*Dv&~JfcaQJh8J>zGY+8s%hBpD)FmE?0##EbdFAPjhP9IA4CcI zL4@QP*^|<@HNBCeqh z)z(0#PNnBT%9B=|x>DlS3SPE!!#BuYuT`e>HAy==jSxRqZDo7_3=vA{Rjj{gS7a`) zC~sA4asxg^5=FFd|5xZNdw?=Qeh(Fpm)82;_I;Xl_N2=! z>-kS=KI|fNbQCj}{MPoGsce3|;6W5Apf#FU#?#TB33-M%#$l5cdGdq>D}A0m%Q07A zQ5t)x{RGX?X=%2~Xl;J~8%3s!#qk!UU3_5GjvNt^ZRGHj%P5I1YHd`bZT%HuaM2hL zMouGqmEE`~Z)P^%9v5L_VS$C+=SP1eqbvt;q<30Geq)9Con5K%($av#`5<$d(~%r+ ziJg<38|Lz&Tut9{*;DMIJy{|oUgEUHpP#FEl-w|^-=zk)jF{XwfK$#2rg(i=^a3iK zqrRS)YBA|O#|8noT*B*-z(nphvon>@cdhWBDQfk)7Y)N)*`IDr1;^?=D zKo#l(u*idSLZqf?$6oHdhclbQB}s7QLCPH0b0+bmxZH-nM-Q!LNA&qMRcI=MuZ4B>GT1}ku}%C#_n_QKN)dC4SJpi)|aRqknMVsQGY%o!mci9G5hb_en za=F0-z8?fXZvDdKQg-Of#+B{%JGZ-`x$4?QKrFnL-U1M`fO2<*F`Y98x?7Lj#dgbQ zrZMuE;YgiZ37*?&qmVUN`hEYseG>v2i7`rtbiiNZu)`Z4fVUFV9_uX_u%~CH>X5=@ z-A|15gym-2EZ+GpyV2he@0Ym{od4rYc=LIg!zS{FZxd?+p*TG+J|&G&x5$e%QH8Qt zt-%#X9)PQL@oT-_$ty3vP(OSZFO|nT6jblv6WCB;(1&zNzgEdhT35krRA|LI72_*N z!Oyyl;JYV$&w3mZYjlSFxr*qS=V_ze%HS`~p6!OhaV#Az$9|2u*x1-s;$ogeF$B$T z$c=s4DS>I7?Icz}wRWW-JzyJi%!^jt7eor3+YN7$J3)t9-r5^WYmZ^%lPSt7T-J(S z)*%vHIxlrh(49hm3UB%|?WbvX4l6zV#9moZ`SHX3(aCY!*DG2CxFO1^=Z9A3-;h=n zTyf$%XZeBFOP(9NQcX`I*}tizk_W9?{{>H^x<76UDQ}TY$Lb77YVqg$73zs^DBiuI z|7vdb@a;HH@YL~4YwcIMr(cns{Il%^PCRbOfpfdCEUZbGcE#b_!=Uizpuv=WWgK(;8l(h`71U+x6pr$g@t2(C+wc4@&>Z$N`!6c7HWj=%(^V9~Xth$> zmV^WPFV7XRw;6|Ug`7WawsCK2R+czG5-d)Uyz)cQ(`i<D8!X?Y@Ei{$Lc2H(y7YZGDQ9tL@<6mSDFR2Lw46JUSl`a}ApvF^kNp6Hn?q&&5H=U}x5BKR+RCOs*DX$Ez zTB&tE`b^1T{>m^^I5XetV@p+}_&Kc=_^SUl@`wQX?;cjYdtiJWbC=?bq-whubK&s7neAN9L%d zlc(c<>hynAK=%6Fh;paxmFH~?(S*mQh1_rIjXd`F)+me+PYyTi%U*k5VEV+RD%vxv z$^omB?PY^ZEXS(WULb+6tt#G@1dZ(6Pa6DI_6=yO&&Fl!1TbxD#!pXk_{faWZO2dO zR;-m`^LWHlHUh?7wQ8ofWFY=^ov`7HXj_25X^;dt4bi0`izL+8$270A9`ZY$Z+btG z&J3Pp*R_Vz%ImdpsCfFgll|nxBql#WT^=azs>CH?S~s06^;Vy~tnk~gImyXWZg`v$ z;l}B2jF`Htgsq+Ua2m>bKO?y2f)A-E^lBI!XKP>cGBqk zEPQkqd>2=T-T!NQi)?7icGV&2i=vL_N7)LZ*4%P_`1&cPWFRQSgfR| zXq=iMdcP=0CU$a4Ptln~n?Jt)CAep}Iuvgm{g=URG_N?el^D}FVb_!Df^w3H@|4Ft z^fAlL?}+^QZ9t3-bS-Of_(LKrmhO?dtf!`K{qO2R{kYJ#B&dsWp?0qz2lCVrpOU-O zv4T%z-^{La0ub=R4gB?8$L+n}JqeXpox94> z-2*`9vv%*>0gsAB%s65npf(#y!aa-a4bK~Kdh6;Azcereb8&uZL7Mgl$kxrKM5H%Z zln6Ja3yp$i^nU3$N11FJ)MKNK5ePAVtY z2wr_um4_WRK@n3A!yzIf;39tcevV{wlyCPflwzFoy^;jZ3@lcH1Fl%p~~g4 zANk7{-(Yza+TAmk!pVcjQ{9ma;|jIJz&Sx8%f?mg{_Za5{kDv;_chI;lD#YMfGPD) zf4mLQcot{0_BW{Pk1U!=-qcp<4T{cBm5J8+Jza7&!l5YV;&0+7tLe|y{Jqs!`;xX4 zb;?g{402*GU%)iOw=S;_4Z;GSPMShXw>A!2Ea{MKs~h_$gZ!~ve%EioQDd2q>|hid z?(Lil7Mu$*FSoyQ@#y?k^t}L&YflLIW`4+D=r7uz2sa6Thwq7$@c;-xI!F(aeaWDM zh1O}^q!rYoc^4dVICnqhse_|$Keo+wtJciCoKl_2*UkCz!O#Q6oKh7{JF?bw?()TD zyz8tYQ}@u<0m(tiEg%-J&I4otMs~jEzhmCnoRz_Sf{D_Ul}mD54} zIO^yQK6?G-PWflM5uUNlJj_A!_zE0&>-#k>Z%NNkK|&`-PHK#vIx19sfw3tYv*S^# ztY}fU&#Uuau*=CTJEp`Ld{78%uz!viz1t78pT#m67t{J4Y}YbPwcy>Qkjp|0u=z;# zn{c2p;{`36TERiHmmZxlmF8*BNjDUG%5Dj@e97I{6d~;i-o#svP(yaIPVzx!cnzqRj9v>I%iQ@ZejxS&~LPg@0a#Un5>4BN?5)kT^b!7P4~XFGdmJytF50 z@iF?_4|M1^kJ~G>rnSiUHEElT@H&27+b8Fo^u?VYDI6t^6Gxp_>7i{KG5jicId9UE zw)0dHuWlI@-i`KQf>MwfGioU)E zixir>_ZfWva%WrI5XMt|wSGEQjuVeR#_<-)(23{sWitZz-N+3wERXIVX`PLviN{@^%9|brD_r?bdWLFI>uHe>>m7IpTCR@|Njlz5 z*23^;zsaqUVv>E0^*3=lj2wU>Okg@pzc~-_ua(A|iu)yJ+=_POW_w+r@50f(=A)UN zUdLng+`WLQ`|a8WgArx2F_x0ulRR6^Y`zmW0#Y+G!YuB=ZO^$}_t2s4UU4i!sq;Yw#yzb18(Z#Lhe{Bait-!ffT*R{jgcIk-@k{DULIzoU9#v~ zR>H9>Dw>lMlo&DdN}f{#x8taI!C7Pw7&XyU>+9s|ibI*ZLO7dlz`Z~f52UYTJ?_cD z+$sw+%#BcP@=JYvYV7p(l4gwCmVtD}2-LGd?%;ck!Z%FW2So$OYG!uJ)f#U>OHHpVo~89Z&Vaq*s>Zt+b1VDWeW01NGI#2X2%NJ7{Zt%~e$H-tOS8lx6|41i4`ZP;E%K zV>}Y=!_wnTSTc4-*mKKm82EnNhF`IztYh3Z3+ z<2OJs25&?>Xm;f9YbI~e^{Vi+f`*t1IxkjD_=JrEkK}|-La(D_KWor_!2FIsmx!g? zx+v1IwkP-t=)E$Fe+^+{6nt#$j(? zac*#2gw28|rpyP*4lz9@zCKLI9RckmP}{BU`rC%^^*f(q*n1DN)YR|`v~_e* z36ZR(G2Zm-MQ~ULQA(GdQ}q}jt`epo#c)fNMXy!&SjNh&R8n~{r zI^Rc|EEw?iuDTXKs|gf8O?jrP_iPUKk|r`@vw~Ouj$1ysC!yvugCnT}_-61HDdVF* z=Svu?<>pwF=dak(kx!d1UdF#*W-uXQdN0^HD59-@M!EnKhA<@i;)m<;ULHO^9~@xo z;Kr37)aXB)&4ZZ3lGX_PnrPs|`hH|0;$Btmys>EoyWf*+i8Vfit4e~_aKK&ugfdX! z^)gnfmo602%^AfPAKx!UWfjQLTa3N&6|AJ#fTz0Ud7IAglr^Po4b&_q2o958Jd#jx zBmuxyr_Od!aLkU0DbyFDKxhpKc7Wqr4+gpR2`GDl8Qv^0>#x7`&)NnE4$cdmL{m0Fi7 zB2LqL*9VP6syin<^0j}?7PVB!-BLw1RtLQImTZK&6zDHn`{C_|vMS8F(CQ}!eia

|_*l)(acM+gF)#xir6!K{!|l%~aL~=(uyuqMsMYWO-R6si9$Lt^z34M~~LY zPulq&fPsd5zquphTaYRWf_K|F?kez>4tNNaSIGYnmzewump~3qGfL%!d6w5t@J=h% zJ!dMEHgd;_4j5dg-l>{+lOVS##ZXQxtF8=WV^mhK+EpyhUNfXIlA5_-=X(YHD9;q{ zS|y*1jwzhi@BDyjkZ;BR;sB3Iv{~zl2q}{~F2B1d^Eri%`g9=nQz98DN0|}RESP4x zzP>(%b8St`(@4&c*GL+866na~h~TVDYGKqxX_R}&gkh*n)DH3GO7zwePg-b7p%ug@ zU9%x}M`ZU?DW&-w3boZe5mK1V1{il?ey~tqN57!8@q!+|a;{&gn^Zi;`UlFBZv}NV zFCF@F)QEu6Y!d(-KpNMmRYezcS$_Z?tr?^bkMhmx$`86mJYt+a8{0BOQIo~CCSt}X zGGUGeL_@?PfxHg|`^}c|wTJH6+WEy|kO^Z~QGw1o4%VYLQprB_5*&kquqK8UNj zmuaY?p`~va<5*nW7b$#eXh^v`>nmNp3rJi2pru~Ug@;0}#{{D5mKd)sSv^#&30#y% zBg;i-&pDK0Vw?vc1jb*wg0-U1wYvEZ<|tFX>^w*h?rX5f#y&mrVOW-OwRv}8lk!QW z+D)d7qlio`wkylIjb|-JW2yH#y9H&5WMpZ-g{;5c3c+@xgUQ+3m1Fkz^0}Yi#ZPm^ z?@6vtRa8_Qtt;xw%`)XtXg)^O1bqYfGg~o%G84PRh|_NFWDNNhihdsNFf6N-2yEZY zmRhELayu^VwI?Q9{^2ifO5xJ# zV2Pw1fx#OfK~oyH(;2(d?DE8_uw-Ker2ipNrzUVe@g!AECt&XpS%2N92?;V;$?-*j zr`AlE`g~CBVFQ^J#~_*)V-9+Rk~kwuc8*aDwj4v9k7aYvD-a1B2;847E)U(1Zk9sr zDQb^gNn>p4BAT^wBolHdxL`l+_474_`p8*6o8I@*5<1alA%HpHq`jBx&$M9aX`5zd zBecqdJ=!is&U|90)k!9{6Hz-b49asn^t1#F+WG+2ig?plUd-Zb1mh7a+5p@cRwAUIvIt4Ul=9>ytzG(% zLG`0Y4nwR>=v2(aF7wey0X#g645ib#wj67#xNvq?tptNNJ-3@!qo#sz4e-Ys-S% z3N5d%&mRC^ff*ei3`$?(Hfcb;dWvU-lUkH-RoEpy>RSXdn(1@~QsSv5uL_=p&;q)4 zP3U%UCs%GYXg)q(pGXyz1&3RCvI0I_u+(q!Sa5+L*x+^@>8-)yl2PaETzq+T2X-vz z#cfcpiZozi2`7g9Omw(z<5oe8BybkuzUJcr@ldVp_JMB%6*IS)bV=y_kQvcvP)VAs8&o?Qo;V2JZea-Dnq&MERQC3f1n&F!7EXFFB{X9 z+!b7~bf5SpP}m}X(M$o~(KCYyIZ<`W`B-mx(dx6T1HRw_b132wXBL@21WVmwRNucR zs?>dJh$OAwxHF{JvRp@0kP?5s>TO&~sKr zGfK_1xPAGNm2Q?r(>9mk5x5dvJVwc>V85Z^@ye=x@5}mKPT}0|PerHWoA7>>KWdUR@HrPG8zV)}DYpwTB5H%|M6jNGYq7f`!@j^f3A)ja zdy{yi%rcG0LN7J8ye7|Vs-4b`-F!~_B}uA;z6b@sS05n~l$Hc9)tEF@0l)+h0+=h{ z=|xuGd{W*93Wr~0CKS7jRbtRpPDqe88;9}W;79F0QPt?pi%3M`2|0Cja3IGU^=LAAmLkL)%2YjYfM}Jwde~iIuw7-P@d0mAK>!)me zeWpp$#FJd0RLtK&izyqEW8c%JZo4_8tyOJV30{Lx7f|>GQX+KbfGHc-RRsg8WxaMx z=DhR~EZp!l(LpSKEAdpXq|mZ@t-g$^)!O;6x)1{;#C^Swy2d_()am5 zYr=rx6PcEVHfFcK1nv6W%qFFEAWGd#;tPG-`O>lNmi+uQe3b+OGZKT`%3Ana@pDnc$6W>25Vl;1XF3}b*ZzUp zOx^wlwN=+o?JR2a{a!c7v#ATq(aTNh8UPvqQHlTxcsA8%(+<}%A)h?v zz7>xaRZCt|9zAFK@}`Oj3P!*&p6oZQe%E^Q6L3;@eRT+o+j{)I^8AF@!WZZ9UoBcq0(7*VWh>rqzUDzxU^oS2F*JKJ$+pqFG&khp?PIUZvMU_(whPVz4JlFF z8m^7)f8$5DxUwAl5v7cu%EJI`5HPMuLv9GglkhSo_7-XhWjVCe5aFm^VlXdexutYL z^B1cu)z(YSG-XgT8lz+twuu%-Wm}DWw0$l>%v;fAxFqXBvyvngmxn#~1!gt%p$XzX zQ{fI)N)ryDFD6^``-(tqJ==PF+wyUooDYdCxPERk2OP++tLv@kt#&qOIli0_7;1hz z2?BAe`%5S1Tj5Ic5I_NFemu!#t(a84mIlV2;AKizUm*OAk;XecZ~wVFd@Kk zXAj|J8N-3G&A_}RNYbLmm=h1iewEyGKjEE%Qf&Bzo`?wBmAS{_HLR}nF(%8WevRX> z1)QcqgNGRx64Yb=C00kQRG0bG$Rh3J@oYCY^5NGXCtKp|TSm|Rd$&U;+4o89nR#!U zW54D{J_n*?xi3is9GFC$l&j+VW4MtZbeoA5Y&{zgy`#yQ#A(ZvVG$eW;V zK2ut0+sSjzRSe4<<}lx`s-}8$pal4^qCl2=zAp(4Qbd=%K*2wfB{q5h)Te42#imNe zs9Jsa6@H-k<_8#|LgqB4YU8L^p^DveuMXw2#KqQjUeY@RYM3859R|q!_n|f>&^Ksm zxuwupx01Tg^BtBiSfs3kVZiUGA3p>thkwwVKij9d`epYAD{A%HZ18u^#Og{BnhPa7 z(wR@e?^7N$WdnvXqRUJIIak}l{zHF%p~%kGGvkoUyF_!p;w3320jBfZZ?}Pt;^gBq ztj{~9ifS4%E3!10o~g8^k`CNZmG!WS2twX=*M}+3oK)DJ2olJ@OwVT&ffW7fug3wr zrqz2z6nU_zC_m^bOQJW@eOGSFiSn==J)+jYsn@fQ<4i($+%hQJG<=Uu*@$CBn!~B#$MiTizjL z=AZ#>>P>q?W0GgSBMU!uXQI(t2q9cr{}izd3jyCq_ZuSY{w^@qvpCq>ai73zq~_I4 zK9fV*yZi*@jjX0)gRvu+0aWa0@TPs`xo^9HGtvpS5>73aFTUiJ>}iH#h^rY3 zxcmKRCuxRORam4vQEMw`Wt!-b9m)?sm))@dzST z;FVPHs{n@OV#=&U=wtQ*qV2qAQp98djF`CrtsZQ6+M2uG!9?7i&exJkadCzV(x4=m!aV8H@pe z$|aSl^4fHP%X=YCY+gnBCMZ&feq?Rn?CC)+l{eweumw>sh{iI3 z){bFsH_}_ijt&i9EhR8!ceVMhBd3F3217V6L z?REylDXZz!sCUD9;ffu5e=%fmh2eTaZg(aXiSZ&ECn_q+OOAYJf0k+dL*{@c;Kl%9 z;X3TmLygIIyGa?BMrrM*m&-A}pUR-=surX78;B@HptIi}0Okw3&~{*ww*yPh+Lu|S zL6O1hsqUi%A_X~3O;R#Z`5RcL=+Ln0x9MyWJ?FFK*Y0Z+=*A;vZ)x66nYQo|7`*-- zvTDi(I)+~_?^55h{D@5@=@yS@_Gu5*6BX7&#Y`h$lZS5~G$@)n+xx0|l`k{llSRR9G??k#QM?Yp z5FD#!QhivxR=;Ec5mHWG{^|Kyl9X&euAc{Ke`j-9@;di`4y(ggW~K3`(-i5c+Mk?D z4y+0>vn=}0e=>`xJjjSQjqyYfv3Tf|7Sx6()Qc%`tl395Q z=Ue*|miGj%h3^%WC)e+ki$q+{E>~Dk$Ziu*JyLo`P332O$*mF*R;fX@&R?QCVuY|u z9d3OS(R)tA(<=JIA0>Co`q7c{KQ5@z7V$i0xZQRoE%-mArex#2gUG@nJZ0}a5V~Zq zu1s6(1!OdHZr=8a!lLjerbsa<&8tVF9yF2O@*W!`KXs_Ud3vL+gIY#-4qTa-%8NtDv znqHYRS976nDn0L(INDWG?5QRsgWuTtd$s*FVg>06Cn7`T$8Dl6H2}Ga>lsFbgDIia zN<*n5e2{8kd=1c4Ba%PW_XHUwzzXIt{gPK4ka8;=rRq^~{rX3(#@|Ip-c(PvYeC1> zTWGK?CXC&^_^wk3OUG;CUlo!S2#@O*EpD_du z5$43JZzR3GBwkhDDhsSW{^Wa<=<36n)jqRhOwPMseuOYgaitxuU(Y;e5n1RP_)=`w zy|X)w?N%;d3-+J1WK@jZT%fQ;`7!pD;%=!0Y@0QWbSX-_)qz@f*Jj+!HE*oA+YaUo zG5e_~8(lI8lek81i~LXjSMZud9tqzCWr7?Jz;KV2ux7Ej^%CxoS`!p?I10?^9Un3ro_yv9DCI z*10p@YLIrUG9T{lMa{wrH~YIM!tSP>H>?db+li6aQTP1Hh)ij=A{00LUEyb{hhT%p z((02i!EB3YW4W5Zh4jFQ+!FfbfHYt(k|&Zkk{6>Cz-$Ak!s-xNoqKgL%)1KU6N8>J z8k^2&IB}jd;N1lIX+4JT9;Gm8_c>srkr6K|%UV#HCUA>-eR_?k^2GbaChO~k5|&L0 z;&4Q6Vpi!7Jh&9&8SkY{VPe6&3hzuzt?ID7o5ERcnc$${do+2(f1p^jO*&U7$Q4Z6 z;Gw@!shi!a6dsdNIaG#4&q!|8~ z(`?IC;ar(Z)?lY?c35VTXa85z)bZ0m`B*deElqR5u@MQB3xsn`g9$a)1|RDcg6$mG zZ?=_(k+(R5xX;qFt)p8! zyt%C?+zBW4+XG+`>P;0;Ga4(tN_+cQm~w7ht3ElIgSFy|JhpsSE|VsW1P7{Qv8Ct7 zD)faqefJa7|BJY{j*7D1`-KNV2I+1X5CoL&njs8QLJ&||X%G+)7+?rV=@L*{x>-}REEMl%}e%CKQ^&R#2{-w|Nt>R_?lml5G zE0*EhRWAbx<-B%~GRQy5n0xt^2=6ooIuzZeGY_ zqe!#gWh`6IEPXrj@(I1M_?^DBAilLsGb2Z2hh?bG)T6hqbug0FNo5-;=#1L}GN)Xh9FAe^@2abml#7W?{gE!3Ui#W@z zB_~g(+-ak0iQt7ob(rA~<<+eQ0IC;JD<-t~c-)vRp7~W$s3N!;Tn_ST`-0Z(3hed> zc`I1S#pHN^#A_b#+vZ>W6BJgH)!lI?U7n0|w))+9;W70zj9A znDuDA#=jBjkkHQsJ1#XcRxKU_`zFbp1VpTN*cAq)n8$C=UoP`|K1HSUMH~8)tDoCe z@~q$O>4${sn4S`Cc-ZyF;bw%vV?S&CtK-9`&ay-|pCy?46jT90hZ| zP{myy+^wuo$h;Ywm%15oKUWjK-*R)*Um$yp zqh^=BrH=Fdr5mrxJU+1{S+$`3CX*JA3CoOlwkLzTZ?lPKEwPlS){&iQC$T*{Zf-86 za3ZQ*Rpu;j5Nhm}$L#su$w>hNu#Cy9!YVVp$A6x*z2MWtFd zmI+5p?9a=K;sva1M8Ch0%CPE=6bTP0gua0j5!{u@LA14F6{ zQ{}F>%A&9%+cHwxY_oiPL4xVOmL98f7-tJ4dZ9D_lU8b7yC?#rr!54+1s1;thM>!> zuBO6HNBM4vlkat*@4^uie2SYQ7Mp7ytGA|N)y#ri8CndGs?m)#WSno<+3rWH#)YuG zW3#y1ngF>2H=IAYR$9@lQ$1-Oei0|(y}|s}cTZ>K>C~G;tCpJjnEMq3B28@it};$+ zmS;x_tdjp*ot|m$|Ay-1ni5-x95`c&gy%H`zm|T9^A3Dnrlh>4z%1E2cFAp+bzS@y zD3`Xr1;8@yn#TVHGS&9quB`|>9v#!JbCAU(z|D?0BN#MEy}&^us(9pv9m%r;)%}bi zkF{@=#DG9=K`ggHt`Lg$jmI#BX80KEzLSnr>nx-?UOX}qU-m<% zr8Ml5B_n8+b9xPvgqGJ#d5Ete{{-J4J&?Llx?`-E5bPG{O*s2M<-6zVB$8Txc&Y+L zz;3ezP(yBe0iZk?(^Tia5=Z2f+f8M>bKO$RdKxE7QPmyh(!?Z(26Dl^uxuR{st6+XeSXy9$o%1R| zSw0%%W!9bFh}Mb`H~Y}0`^4q}k%QyPLEOug|!(N-mHcRr2eGhpq|}2 z9jJSi)nQQEOZ~R5s_P4d63ugSkSvZ~WzB*7^LmNYbc)5tD))d*O#JxTdup7`ECR8? zHPlAt-D&WZezoY5^ifS>jjv4A)vWl+O-h>c6-xdZ=3^<;>rC0{^o2u#Mo;NaZ)E|M z6h56Yys#4m9N0@@@9v&wvJXqhbpOs_KzZN~NdV_H zc+j@OzMBr>zm?&e9{ra~TCow_rn}BV#4dT3cE?!&I?&@#i=KMh2gs0FVaRsUWlY%W z?8hcouS7$m1>*-e37xOIL9*!*E-N%{%4oCBAt0ILdclY2rAMp;-FsYKPmtpf@#)>x zuLNwhA^&7#Km3c44Xko%fvm~yC3^jNBKQCCL^7oT=|>92w`5z6UA-J&awxlgNPbKt zm>}-vmLde>#%yJMsg%WdA(_ZRFXqGEBVPOkvLcdBzLU-pQwG}mL7p8nRRoQ=65+`W z6_21Hh!CPt0bDurfDU2m5Ud8-&WV1QRX!0v-GXL_1(rDkQ3d$ zpj_B#yC1k{!j&X<`GvRn-P7lF%F+-4vg#3eEM-=p|IhY$Q@t38t)Xifug3nRpY_s! z_{nr;F35Q7hTV_+T5Cc6>j7eM%QNBgSfys$DbD9rqCFC$$Jn{4n%c>njJD^5C6>=7 z2DGK1>o>fOc_5tk9r^zVPWW#!Ajp8wi+^}oBNV=`yxU^dzBTcL-O8-RC6foo_D|SO z9&vd_q15Umkh8&Vg?AsC`LPD{Oj%RoH9Sb)k)!(}b*AB^Mh&G}cIKTY+zb#kz>(UR zg}*liPwSRQJ?MGO-JsKJ5GT~*l9m5%8uScErFf>n_a!hv#G5c;m#(2UfgrtSH2+bh z9;Z3Qq0BhmsR6_47@55(^>}8tZkw-?mhc9p92}4Aqh(@@=v=Y5!P;2lA;NKOy=&zppn_k zRYAuxf0|p;_3&qWmYU_p2j&<8-J7CYhP(r9g!YCprK{cUCB~|5g7cgv$BiBxMDLS1 zk*yxU+x*+T-2d`1Av;jn5ODJ9IB74aZk%V|&S~*foe{q;CvKnmJcY8h%tLMOj;n?K z?=g0_059lz<3H~4%S5;yTl}=QHf~WK9}b}wcRoLeT1WH&$?#j42Jo#x))KsLcLm8` zADB!dto6o5#PSf&d7VZ0crF`AV)3f%UAR7w>@Y}4eK%<|Tu#d>!P70mBgFxZH;E0~ z@py%G9^}m4NSsboU=sWD=ydfaj1-k)OW198) ztAe~N#gm9iO~#P^l6qQBq_ zdanmnMODmoc|Tshhl0)Y=KqRdh%s{=C=%(2a_{qved?yw*b>y~&&V}h8%jR!K)c3~ z)VA$TAPsBO1a`BN&1N4mg~v~0@0eO|N^>=aAVX+^M8yJ%q*y{-0SoXNklRFymhZGZ z+kfLVY}B#8$2Hqur`TU>Hrd8tgy~nUpMyHPh|P)il*RWgaD)&r=_y0ki(y(@#{D0} zd@_hunQ7@tbW8?0?(aeeJp+6R@GQOO%Hfbc8LrB;w3m!Xse-_JG4KuZvZw&uIG6sn zwz%Oa@xD?(w!>_UkeU`?-JQreIba!msjMTH;kLlRMm#6#TxfrF(d+1=_^r{w=w+bz zFt8~qJDwIC2&;bWCAD+OvP63fVL(_XnoMe?+2k*Hnc7~LjWoFI0RcwZqA$JW!}*{v zbk<9aXFb{`2eHFWG-iYbF55U8mKWJXa(oyoa9~X#+WCEjOigJB>!_|tbhdcP6C2=8aYJMgocfb z^yTv`apq_>Z6(mLZGjs{eVg>MLa!>mFD%|?gdY0Q3v|*4u0E-d8s~9QM0%_#b(Z-P zkjpY5Dhew;eO5}McKD*^hoGf(1V_yf2PHnz?YM{qU?qwAvPhwl4y)YtAJpqr>KhYQ z1aGuXGL}R{9<+B>Vy@6+zy0e^>`(ofa|c&jUy@ssRNd+mhA#Y)xC4I1_L5QGf)i&A&#rX-HqdB7@rQh*%GIE3YIzlUCi~;K)!Xf~ z!l2tQBr^!@Z(2QL`GN$Q+cWABy2^YDK1#N2i>tdnC4ym4OZU&~pW2?uK!0^85_4oU zlCv_Z;URn22_LJoPw9`Pu+zo3@>7mlrMig>tPwbV_!4TwBBFu|^D2i#tsIk#x9^?H z|HcmxTt?7N)Y`Hh88kPf>3(!_$hZNY8=LdMz=huaUD2MpGdFae4y6NDnO&-E%RR{t zs3{V>;}hfp48G0<-T*nPaa^r{Hzxr=Rm5=8P;=cfHj!=g{Gs1kb7OW@4J>G616SR4 z0`0>(weW1cGvnzZoPx9L>l+|YF^K#j*{8YbRceb;ZrA$-+j|cZYLk6FbWCX`43c=#_xA*z4N&Yd+h4YzP&9nnxp< zDGrQ}0Cl>xaXnI`t`ps);(V92%X@y2( zoU%r^F4|r!*zHQK3WI*0W4NIHV+O8KC;3(U0O-ORI8cn&mr4tbc&$|jbxtro*!cQ; zCI8j7;nNicai%4o9y-d3Bxm8!`FV+XQV?hTsv>tWcM&el5`umDbZ89c$B!p6ViUQ! zee}+{txYkxF@8ydH;@^=8xeQNEr$gD)}`7ZnL7m9yB%a7Gh0Ou33@@=bEn~Lq-HQTItpg@J*Cw5{A1&&P) zsl6<7QiqjzS}}^sC-jgI5ZtUELbPKb$CB0S8g^~c^n{XpMPN$M{m{?C#Gtz!&(@En zD#&muIMAor_GV=C1)@W^<6z_oI)eqvyt2>G`5~(?wRRjMZ%{Q2y>EGCIVhW7W}Z{U zDUy9#WnXc1GeFAWD3rrwrouhz17naDmHZV{xG_d*ZYmSg|J0^<)FBgfrMT)JUe`vC zI@>e0OO4{|<8on(o)%tbA^Q$Pd5zw-K*&uX1lPAKYR6cO(^{2B&0i(!LXv~yB}Now<$S^d3(m-NMd2T z;nDrKAXY0?2j}7&)9E?gb~4Ac1)E`E*K*gPzRi9VKm!D&rw*e--xpxg)-__1N{iu) z1EM^fHg<6VDq9SvrL2(lX3R6B8Rk)KBhmInnc;Vj9VV*p)dz!@t)xr=8XyhiP%%Pl z-4f$|d*$5evlZc>>$5<*HMN{91;W*LyKmp}LV8(ACJDLMc zwis-~&cQQy+GEsGeeVt@x`w?>T;+K;$F~Q3B{W`E9FTW0aHjVau|0Bhn0sq!ka~N4 zbeu~qN2;>NSWdXS=|`MBEbC07>G*;8r#bV+aLa}57q0TSy{~SbaKh>IIATw2uzHPa z*N$>ik86X}Ldp+<&HI_QTua@85;VOI9mtT9Hy=_ffBVQE1F?=M&+bTh2F$~L9kk_% z`gM6Sx^F>;XF85l$aQ2o@$}%ODaH6%wDa?6Kc?N3;|y-KxDECN&Vcm0v|D#NIlZy; z=Yk;sz`QJJwp+Qvl>sONR zw~J%Z@rd2FdwYh_o2`Wi`6+?6F2RpOZgLOf$;UwFqqC=DxL?G{dSfpcq1?c{wEh?9 zQAAbSgB`=Ctn!oMD^%T^JG%Ynb}=t*o4*KDi&K)sJ;cer1EjTN(oTvsikscB*K#g4 zx}U^((kbv=G?W!PGjH3}{sHQPTIMakyY}X2%k2Iyj>C_R6+$Mn#+=Jzj`B7Jx$UA1c6up-lMPR&B~UEvJ(rpg2l6+AA3N`&5IzNjLppNj`J zwgwpK)lvtj{d{oFO@-mF0z%LABan~HT$c9&M!9m=@F>VeLthtK=Q;@fq^ZuMt;pR7 zbTb-2;XxqZnE_G8&Nd$(TIh|XPJI&#`@su(afPGThG&7L;8!QA@~zNHnYys+2kgmN zIPx1$h}J{ z3R#UA5$1}w+^s)_>*(3ZCwi-o{lBt1>9rO6RL|-A9>}Wu-+6gsHTmX-g|WOvZI6S4 zRzJ~Vi&%9OAE$Fqfys}jc1Bz!2oAMjhu>b>R8h%QM{wPTp{2 z4A`!F6;~Q?9Ixt=_}wEi3?=08Q7tffouWYKiL9+SadED z>g!X05UNG{s>ce;O?f0jsnZ(OQC0D@`7L~ z*r4=YuUi}66b5c6tT`;JKJT0fKfGi1SYKfzwK853_pSCUbu?*h$lj z+7%2NbMtXuH8O~L{8&dl<;Tqyp-GIlP4+I#7I&zI2}XpZ`*6mE3Gv=@$2(xAjpj}+ z3F7z#E(QmJr?^X44Kh->V7%^-C`(nq3dc7KheA( zFKP~{le_7bP<9KH#FJ0Q2BR(6dH`qa-8qi9vUxJGe@Y_2!lAXu9zfn4qj`rr5i=Ks zi8!B9$E+8a?27OS!{W6MA696kf;dsq$kt_Pi_dsfRqikXX)rpQj2|-tNLn|qe{cH( z24nGTgh)Nw@4ZbKd=_a}kbrlb6J4iFY^2nYo9Qyh-B^*bZg7*Bc69Ltz=4xuq*77?FzIYk(M~t}%sc4)cG5~G*4^*$mTRTBUmsmn zuHJT>4-DXPH5Q!`9nm0k=4uo@@0F*$@d={nZYlz?L02hV;Ri-nzWc1An8~KLxW{#= zPpYHy&6a%2z7}yJ3l6_!F?0#NW z`rme(UwOv6es`{)lsHT1@*T?mz)5Q7|0&m1WQ!%o@R!?B>`9!X_}+Z^1U)2V-Tj#u z$a4#LVj2Fr$-e4cjJ^Hyza}?`m@h{$F$%!^lb(LoZy);KPfz0*e@MX7`isn4HC95{ zosE0vDqd{&J*0OsgNn{7#WB7)7_nc7JU}-sb`dvqA3(=@#RvvjZC_ubosyBF}NltV=2F3hVp)5WWKs7rN`lKE3{B_$wuPBz6sJ0+tpB@@m&fzvaU957$l)7`N_v-Mz#i>Uerehz|>O zRi+^eJ37Qt!&9*22Tl*KV0*M*Gn$loj>~*p{hS|#u2PwXAP+n{)r*WUy1nR9qUt8m z2iQGn4j@bB&KScw18WUVFC_Ttxo~$GLSou*a#I34Oz9QhbU_leB_F>NaUs<*#*f%{ z?pTWXd=zVc8P*SYC;H#N0f$5B{Lbl02EWp8E^^|X1gO5j8RLv{iE-E=VfAS)zdO78 z+e3?!XHxK~wDt=>U`yP-o+g69e^>|}jl7&I-pM*9sRzuPsy88CS$zvHEBqkFHEi`s zoK8PZNdo$~ga80%&<_V1I2s3*HKshBDbt_IBqNI^pP`;lakIMTP?_5VJ* z=K=^>hnIz?#YaSS8WLaiv}mWn z{J=V{nC&WxvHjmkivZ~ngYF$vVIL#xBD65+%^5=vRmVZ27n{vW=)f3TOv~Iew5aZ^ zxEc9QJLx;s`I*Yj1f<^XU53?nFq&{?oA-OqZY=%+#U7FR&jGNec!{4}gBcM*YXV_~ zAovPTombwgxmoAlW`?{a0og->^-Uo`*SekcqMgMGL>#h*FIBeQLBAiujl5R z%?h*e$GK6cM@2{PncB%Dh(0b`8x|o>xAYZ0ieJQK63N_i4XF%SPv!qI)6>GXx}=y6<2O#A`E<)=ootwG#!kPCX7ZutQQ z$m$9_R5z0Nae>bf=B^D=@ao{)K+m@>boo(CLKzC%KIb|g=f!d z;lF76r&!XT7}mcLB5uR+cm5PFGvp2H)jx!fGF6}9nFk$`xTb1#joq@Xk1TreLohYW z^)thY-9bQ4j4ZQtD}Ds%SU&pZ=3my~8m6_eAcRKY&y_t|wUbYGhe}1LLRdWFGo`jt zPrvivaz7cbi6#9>BKxG?2(CCfbExI2sVcWI7m4wFS~Y{^f#+&Yuf1GN;vkL@Ap`u< zcU!L_K&xU|An0?=Ik`G z(d1exJV!puQsA5{HcpZ~os#Og@$?a^jNd?s~UaJRG28@S^mTZko0T0G%oam*D z514)QVkzN}E-XLK)j`P!rv>3^(p#A1eY{wUcoQpXoX$t6{tna**_Zqe*U*ZJb)Q3P z)8GcjMaxwi)|vJ)jLl;-vG$`f#V!7C;T%k#J*u|-bVM<;XVf5l{MfZ3L6M=@ooOx2-AF#cs0uoNaj}#fa)4A zN=!L3ZQt2EYp?JKq?6t%6rZ0^V6{Ng^*7Ol@!5$YItKr2=++wiTo1~1TmLgTig6pfKgloC!mLFl&>EdneCAyLiGa9zu&bv{Q{Z!{NIzc-+#;c$jrR45vSo!t&Pu|$uwa#my3qR zZC>%*9|4N6 z{9?OV@7mxd>+DQRs$ym1_#~K;BJjR|O^aHrRis~O3ORW?eZ2bKBWEL>tdvpY?@TYxrMN? zK6f7|osUU-Rr<3ntGMf1CtEj1R1hGPJ%yD3M%2yZb^ zHmr2T`SsJOGg-PR!8KLKMMv8oR&Qlo4i*5P6wugtH~c7pa70s5NB|Z4V#w0s{WF|T zF|w?+leWuD69^dI>^xLX%~-`M&dGYHs#9KAYG)y0F(&}Xa3S4Zrmw;dkFOoGEt)xP zI)7T{=2Jb`GkW*`_8~ICou4?u;pyGj^yeaI~m+x9Npg*{n zOk52(cJm+ue|y0~KU);oP3-7D3$72rvkgwhQKlcV%<5}U^`k(*k}nQM3VGffK49sh z!fwB@_(AMrkO#QvB--f5%W6~5xAPlf4poA0PuD@Kqj-!DK#nY6`(=QJ0x}e~%PYyK zKKV~2%k!u=wrv$G9vtnYE?5y0{&Q*}6K}>mLY<#)FAWujSaZh3Y!}M!mxAPp4Ll2W zygg#AQ~gp16$yQ(72gAs7Hq^UCqN43xt0}(Q1+M)Ewm&)7?KfXcXq1@&}!VQyOCd$ zCxaW(1}WC6zXkZ*sN^@^-zfVRQn!Ig%(=&u0<{&fu5xy**KmANm7tdbd;49nzs|+b zTPg9?7rO~N8AC4*RMg##IvnX;RhWh?;NUf!4bR6!YWi}&j2x{`yq)MK;1Q?%Kyer5 z@M*S<5zcFtAYIGLHbz;3uWxnlx%sWw;i#0{RI!otLTx%jDc>!=nwZuPNz z{;WX1Hn@`RIqHy~69BzX!5FM+Py*w>?9ce06jiM`XA|b&sP1zZFUkqHn@aEW%Eu>| zQfal6mpG6NkK8-R)SV9a>{Tm7r&E2ap0@>liDGx&__4rBCyTO)HDtR}1rm!)cAF4# zCcS79Q74Or3YSh!(!y-&RLRg&GqTq19r8=A!$*AWq7qv$>}!{~y4z0A6zILjfZvkC zYEoO|+zeIjeK@q5UuV9_vnfPGG?v&IdemB8d6W{4;bnO-Tl$}U*jK(=!;?)4R>Hhm z^~Kd@EJ%AzYbpJ3|ET7w=(UpX)tvGdxTg|orY1R+Q|__rfR#&Qnn1@b*PhG(m&rfV zB1aE|6v2da$n`D#$*bw|nc_43`kWoZRr+jUSj)Y&6~eCZZHZ#%_9LMoQHz;R34Wg| zVt_&O6}22$xO*XUmM$xB1fr7Ll&zi$RAG_DG5YwAq(;a;ql|IG&?_-~EffX$!_{e# z;R}}J#ho5UW9Cw0;3Aq%#iARJ*2rr$Sy>*v&|mK{ugMhr-UOZ9&29}BKZ+TjUqYaF zlDnShj!Q`5#u_)s2UH@m$wX1|Jse2c!4~-nBPJlA*6)~VB4F1*ekhZGEne@GjMBlN zo$4%$bV;LWbdu9HcRtsSUGxN};S8o)(dSQSo5IQw*6YkbH(r=rQKrBfD5S88(lXmI zwOVt&67Im`VcXw*9Kcrd)7xCoX@)R;H+%sZyNAAiX z7z6J=gF3K*i->Xh_e?p^rH`8%P%W8kbSHVwlnKR)@*m+gv1tPCiw5`NQ;5DcxrTKNR7RJj_ATH^IQOd`!@acIo zx?Nsfe3iYZvT)<$zYI%M zDmD_y)Lhy?{e41BRrS`ff^nSGwve(D5AL#UrPyTKUBjgtSvjeur((icBH}xn#S1m= zYiabS_zkT@U05mfXnH2socK^ASuL3eJ-_Oc$MWyeDW+Xz(%peVbpDmRt!S=Vy543j zVr2zRo9kEb;n@_AJ}O+n{-R13Y{m%-`+A)v7Vw9@8V1u`&m;-me|~qRnwKX9_UzgrNxUk zdUG{Hk>RY%1mpcw!aTttqchtw=Vh%1`e@}?sFnMgJc+49#;bx=<3%O1Ph(TIY=P1P z%rU_yfFsyFr-kdJdxacIqI9f7=;iBB4X%X@!-?UXGh=bQKlks6vc z{Yw8EK`PxAxQOEmo-4r_%|`A+S63$}Qyf$AFe&;jwa%U>``Uo2kWv+<&{L zVy`QKvyE=bGAgiOmYD*(TH2Hx|EhGy)pkC@tL}uWPj}H*`JqhEo^e*N9LtU=VPmrI zgU!vyNgkIv4VI>g131c1ci;%N#=(PzM10{U6%6NSlN@{@PxY?!WgGVn)gIN5gOw*Z zeyeAJut6|@35?6vcrLyzQ{UxJvYWaGk6>_mb7<)}>bS^bu^9LK>?lywB=$p#crK=MfKB0dT^63ak>cm3D8EJ*o{KtG?$w&L6}b_^F;W&ItbC)9&lpYU~GjC%|vDm>Er; z{&rZ`o)T9H9@i*LvU0}IZ79E%rfHMAgRwfKsB}7VazwI~Q4W2iKe$lrACCoe(#w%w z>?v<FwT|h)>bUtO-W7*wegcX=L^>pEDt; zZEbCuSBQwe8~hxXkeV0-F5vy{>_?2TcNhfLuE=sRH9z=49%0xV2xyD9Fdke&tnJxK zj0aIc#$Nt{o2c%Mk3k8=#+vqTA^NODM0>yI8bI_P88*c zqj<2GnvWa{!?K_wm0gN?dtK=6K)EdgylwC(GCwnWWRqx7!7Cr5l2T3gTtB{qyuN+= zbL`g`zuMuO=i_NTa(5bp?o-XbccUw^y_63(jxE=p@T0_ds!y-a3Kld@JF$00Ey!r(md1_xW{) zjI&va-Cv;hx8$Ntf6L_;8ksz?a&uvJ)4*0pm@}Y%k85hJnR25QC>v44EpEq!28pgY$1@;g=Ce6O6R0US}Kbo}v zcn!^-iA}uoDSxdeP*)AjSH=s*TlLd3=w>`M!+JIv9 znvC(Ku~~hj(|V)oy^$)#=+aRiV%$EkROek7Fh|&r^Ow@|_PQf`Pa_0^o z^{Nw|KRJll6Ij|Eb4u*cCY`VF=#DQ>^6bB=1~e~%7^Nr%j z-1J6k79r~{u_vn%pA%~zofkYdjK4m8#uWh5`Hv4>?e0u!D|6Dz_05iEB1jh6OA`HK z3>*Ux-(AbXb?7JO`Br%R6jL@r!mmk@M8r}4%_d185vg^+33ZPU$akU#kp>X@deukY zSQWz^Ovh`Q>5=d=^;j-lw%2-|N{z-sptUjM^Z%u-|D(%AO$qF}f^(HCqS_DL47^Ki`iKs^w46PE*|!BasY_ z#O1Uzzp#Fu7M`zGsBv~b{#ybI_5bp{sllo|rGw#IM2m94uA5iY590?4&3Yd9?Qj^h z@2n~7*y7IotKecKFXjnS!=VQYX=A5>1lxJoY~|fUqZg(+t)kWH7U*7}aJ;rS3;aFnZ#?jo+_QCo17iT{^q>H*57xtCl>baaqDh#}iAC6>s zykC=wUw^`MSbDQw`Rb&?_nYa@B6|sMz2K&dn?Nacox}cY+#B>B;0i0noam0ru?mKZ zM?P{cs-hrZX7K^NeFb`Q2AaMco?bsB~68bf1H>2F7Hq)wGL6OKaxrK6}QaT%z{_ z@+j7}+***sjW}AfvNZCD6q7wYDid{wdISGz1EMKxYPaT&UWs;RF;WiFPaPdy zJ{THlOx08+Vsw=#>g2}rs8q)IuK^(fc80aT`$L^K+tP7ZI^(+oTwP9XGeB84tK~@i z`{zFR@BMC1)5cpCMwxzbSOwQ(e>6Mq@A;&-0fcxS{$=3c7#mKF`|6B;Fe`mqIXTB8Tr33l(;S3dev1x4V zQWlvCW81u~7}>u-?1Gg3bJzY7xm|!B#PCKDuz8>XHJR`@ZtcQ|MQ0myd?WMRt7h*< zZ(b=!LV>nK7>5*ILSB}IiGR-#`QTb5zR8M_wCct!dKD@5m8hUye3dIb|A={9hh?Hh za^meBDd8udt3HG_kz%st{_3>JDN{T9v6~ERvM&R4Br=u3(l@+? zMVokC9j+>3I48=TTEu2%b`%cP_9;S7NQSIu=s#rH>Hw8=j&wL>&W`$6Jk`hV(N?+; z-Vq=ej1a2cC_3y}C40J6>n;zY?GR4AOr~T$dP#&H$`cdm)<-^ah|8l>AYO-m5jR}M^t`%a2+ay zuQ=~=q&vt}H3-_M;SurNER>zC~gZWY-Z%htF}= zO`~3wvP;po)?eYVh!2@e+WLLj|${TTW&Dvm=bt^PtUS}R7I;8&Dn0JUB zcSv)?28spa!qBG9B^cb^dDeKjMhm3TPiOvIyNH3FzH5ABvpU5>)P|3KdoP6C!J`QX zZ*^qc+UI`$F)szTr09WAVfE#=J~Baxskvg(q=RziB|Kwh45#KKBQ&Ob#Mg8`jN=uAq=}!kaA|U?89MM3@ zjf*iAC+vt>jDc0z;u3Xo_cLl%yRBtW)3w${doHX{L@&wd!`#N);uJT~#+#+3@ZA1P zktS4DXeE3K&CrA&83bveJn#_=~$pFq4)I|^3twqHWxW9Je?7>h- z=B%qpU1q*2!2bxjOk3C7ye!@I0BTY}VXqva)kcfFpYIau8C6IMKEljp!_#_!pp>%7 z3!{%Y1J~p{e8avU-Oeic(5L91HyWtEFRqA&Pfb=0o!o%ma^+zF%%?|W2>-n=l=#`? zvc5hTk32Y#tf-QGT)h0Xe;{zcGEuvD%@oKb@HC*b3hZeLRy$o`vZrmb33g{H?I&or ziC6OZcApU3$bGBD|34Kk0f}~DnbG~R2$(~ued@2?D5hIIafz*6D9iZzTdHiCZoQsZ z!AYoBf12%upI1uGYFeL_6>% zy_j>o&`Gi1)WKlgFWk*-ZLmd=wV%3qT*OV^iS1r@F6T5Yc?dwkuzAUPA9Sq=^|8`SgNeOdz zri3f^OK%)WmisJg8~o1#qB6={eaVbZqTbr2#sU^GvOg9voh++WXIwj0;cp?e z>WybATA!&!zH>@-LT}QEZ+S;p|2w`!^wvJLA_(oFL5tmd`NsVPbm*N2U(wzmQ6_lF z30zd#PUpU)t}%X46HTZTajn~+@|s7jiUBW9nIoO7uk_}1RVF{zRPQXVq>OmiQ`mSM zz27T0peRRmV^Dtxo52sFtGFV@7cr}Cw2V3m zY|+E7280x<V-?o5!|813L` z@w=*VQI{^qL}Y$O?SKN0j*+7-od^zu9>y zP?_F9(S0e_^97Me+2+XYP(Vhj^VF5%hFzUpbb$>hG33Asn?5BAi$rR?9$(ny!+S^v zX0_wQ#UA3ku_@IYXkQ znVy>`2Fe=PAzRJ#G45-!5Yin3O0!+hgc>1r$>DxC%ftj%rPW*jG3YhG00=AO5By$v zj&*#7c;&pmmBF{(<$L2H3l3*soL5r^i}Ust}VhLz8Zl_>uMTxZ&@H@0m(VSnIWVC(&k@ zaLn0i7Jg;+EL0g;t~8D0O$&}EdsLhHnrsdzll2kHjVUcz)+*c;+3^-nFCZ9c(^q}r z&bh(*NiKj~czQbA3kgIQ-LIMwcYLU!r8d3t-5~4p-NXQC&UDMzH#PxJTp<5_C#_h~ z!MLx&M0LN`&Wig?xp77~A`{GieWHjW32SG_CTS06<(_*rT?VCElb~4HeaWT!7l?0t zD}HA@;jId2JzY-=^AO>%_Cd2T-ywxDx`(YLd5*Kj4QEE_#Q~@9)Lz6WIvBleMt`c| z^GufC`hE64PY2Fz0QhzwQ&DP_v-8b&qjgGo?dH#oUumPn_9e#JVY>%%jjh z2nSIl$teXl7VT4~Udx;GjttO2hz~73tlN{Ja{wr0Quv=x*>`XwthxbkJP7vxE(7HU z(E3uKq*IstFBVo>0FIhLLax>z$A3z}wV3tE0CU6H7+pW=q)YdAbg-ZM(g1^{DFM+= zCyPu1zOwiM&FZY1@)v}AD|S+r?s@Gq+ z?CT8WgWQ?YW(isEa47hPFR7Kp8Yf=5nR;daFt%H{#TB?sDF(Y%=Fz97{Fx#ShZ@9d$pZjNd z&LW~cg~7a4G=S`eU=HkD0SP@fy^s3h7)rJElJwf9i`c(8NnYDw9?OdH!BWH$ytH#_ zwz{*azPBkq-0@p9wktqZ4Uo4G{$F1I$^%MlL3sfAu6tSxL!3Fgqa2hxyuXsz`~QHP znZX_vUjfw?S&tm^#EovDL`wO?dmoNvMe40}rzx=TN{QIjlrW+^&-(f~iHSJ{U-yq&%1F|LqZ>|py`Hw?0UECLs zD(+@RH9YHwotWrR<4~g>x#KPv^T0QB$_l>#QnE9eEk7oVNj66J1$J)YP#_Mm(qM-Z zxk3+eHpM{wirZJ0g{H{`Q?3{IulOpA2(-WYZ2NS=Wb@%{<5gro(ORl1`b4mc=h zc|JZU>i9cMnFr?-K%u-s3ISGOr;VNU?&Pn=S{Lq5ucAA`W84BIww!<^;!{O(HvlPT zAS1_U6^_%=HlD}7sOX^PVN%fc4|eBjD&fJ+L@S5xBaCu}zSwKseRN;vsU`lU%B#Rf zq`8x6#c*AvS*TnaCA){`1TRF4 z#@K{!$yk9vCm0_Ci#0QXolLU2hMLJ`Wux#^j}yXlQIL>C3A)!^OdI^ke&r4jTklYg zl`L`g)6G9r>_(SuP(^fCy71Tm5jB$$XY zdansa7v0Q=8qBDJA?N-+`?vRg_Bs3a?APm@^M}6%Gxw}@ueI*a=epikWnC&p731|G z#pkBkWHbFhP$-)zoftqyaLE%HtD1XoX7IpB+FtDnm+6IPHcT19NB}vqKLK37&zU4K zES{iFRkWe6f4g4!cI@r-B2;gL=GU~*Fcx@F|5kHdeTzhCMPfYodT@-~SEL8{gKFCf z`+AirohB}Y$qH_ECsBKJYAHtQm)vWs?I%4dAU|kEyL|{Zadz@MY|WCx+B4f{&0X*g zw3dc9RQ}Z(tPXhJuuDXX$i<(QnBhO?dD@aCS4sDHwg|23ZrEZgC9YqqFFC>Ib7nbr68-p({k5Il%^ zT&lg68^Npzhm!vhg=yq@M&@0v8-4bzA#?fB^Jh+Vf#7=pt+~J8+&;*cZ28I6S|4W}IIXLQ%-Nw-^)5+3%{&GHv#UICKd zB*bx|NjCv7>536|JLVx7TYqTM?e5+f9h_Mb8sY`f4f)Qa2XIVEh|3)WI`*JZl3}8reBvwPMIU_IRLSCBQ2AI_0qed-wr1G?kmpQlN~#zKj&wc7do*WvJ{&KvdMy2b>&wL6dSb9`7~c6 z#^m5vHyy|F9$rpoNdF-r*HQ5UBXG~sTn7X!C?e!%xNTNf@S{1!jIA5bYeJE8Fu z8*PW^JHR)&(qYf2p^j_1ed6?T5))Z4DbtjtHMa=q0VGP6aUrUo$VLA&0iz6Ami(W2 z^cNKiQAmK^hb~EvXxI@wqCPXTk>lJe=_&llX*8Nn;2!tB_h*+5+N@wC_btOW^Q6>s~=UpjEQ ztcgAP2hH!(%mrK^?vc#X(L77#QLX~uww1+Ri01WOP(*)SJ@#lS>ML{|X2^fz@a8Ov zKR>zC;yoJ`Aro~@L)l%TAS)o<`Wn-L)0@CM#~0b2hS^(hQZyIm#5&Y6Ka{u>9=cvH zU7hh49r#60jfA}2$GUEa=R?#Z1C`7`*my2_N$i zt@lx|C6E`w+Y&_)`Ak;pc4LxVdYSk9J+gq6E1ZVuOJm3xeTzFl(%Ygd%l9L4g_ z-nL|6PaC4hJ{w?N*fsz0imLwBQ^v?qK`IoMZlp=Zd}OmaRFt4wD{{3q{^OIkG?I+G zPg!zb;;R7tmM6s7KaYDjf9TJnc}Xwxdr_BFg?F^UErle-Qq?{0yRY~4LvvQO=Q-r}*0yi8c6k;(tsVnL0>A)FWlK$o~@{yS)M1_mbCscBc`^U&jDW02AGpiF7GeW%I zl5Q;>=lW6HEdC#dn12p6TK^(+fOEzv<^`)kg~=%)Rz}4Y`u^p*H^J#M#l;#@v)!A(qw1FkcxC;LXu{iN*RcTDj+V}anHMP&gcDp=hb>=k~% zuse^L9OzvPTPQe=xSR7_e+-2DAzOtE+D!oM+bM1rj4q~|=_LRDWF~R#$Jf`&piru> z)xTkCB&F()6qS@t?ibB=Gr}~@;~LG%UN?DHMk?-7tVv4m`CC50I*PhUUprp(mTt** zsIv=RUI#5cHKoH0!_EQi0Z#HEohq{#^*)qCBr?KiDro zaP}e!2a1p<^XBfgDP{%W1q4FWn`?5qm1SLn?D``_-(3{y?5R6X$Jw`?>klL$?>(TV z?_J{F6-a;yPHISgezcK&ufZYt_vPP8Upt!LXn zs`|dG`-V>i2r`1(2x7!yK);#%7u1=@Ve|g+^1=Kg!P-Tc!Pp@$=mT-^kT{x`=$Y(^ ze)-v->^Y9nyY{f$Hfi<}odsKWgRt%-Hth)1&&96tn)G;WO4uX>YCqeFxIp+b}S zz$-L(AMpS1@9G4~L~1-k3tYr5dU6-I72j%p`k<%1|2>>fJ{jJ_)k|Y3>q~&dah$1- zhX~*%k`mZ=^2|+N3xX}9s*Qg%$b8{!7lO0@9fCb=IB-M&4Fc&NrG#iS_Y+L!~$Zr$EiVOXwS-1K52LK*}+ z(o~-`D<;p+w8L0N1u?h%isx+Z)J**RDbEICrBlC2IfgujSbVIN65X{UE<_hih6MWq z1Mp)tA)(^cFo!cw{92lc2E?qx#fGlzlaX21+!6Y zjag=rFLkR8oP7-dAQT+Bsq6lNN=ql-KlJp|;h}Y_s-^CTu6e0Rij7PJg?hNvoz}yn zfGPsD6@HWVQgT%KDwP7&0(=A3NbHH0QJguampyy!zMzQliun>X>j?&j9=B(gP)Cx}-?I$c>ldbNF92c5G5 zM`9JX_yI*xs*FBR7Li1pUakUoArBP|>;5RLZA*P{dna_Ok2Q3~VOVc$GUd^)5Z6h; zv+*M%Fcd^(yyuk(Izxk; zk{JMb$>_#l7#Fn2Nx6#Sn3+=!7#RqBjZme4i&UvzFP zZEb`|SRk>oa~c)js4ldT(tI!x5MdVHh-7IGSK|$1qgs1t8A-m47p*J}xm7oUwpVlS zFwR6sAgh4zHOAax;ibIlQSztGps_GbK;!c9=kM#d&-F*fXq{CcVy&kj3))4+fp@j_u@EBU;Aq0kjSdS)du4b9BEy zj%9cU)C*!RmCq#&krF_M5|TeC2>hm}@jqlkda|u<)^W0are_0pGI?|uHe?xe&G9$DbCZvPdX#R>CP8`U-h+a zXM^?=Bs;afhbiaSZ)$BCdTM!We-CuQOnRI)1^e|hxs!vttx-vhe8Ea@p*^M6F-fmM zGtcg4v_6KvaLgs3?)}rzw}o_=u$kZ&7;cupzPY`TVp{@1{h*PrG6c^QQ=Ra3o{z|d z-uY|0?xQb%-F$zmWI>Xd7SF(oc>-O_XWFKlCr|9)Pebh7`0DkfrI1eQS-FR*U?jhM zx?NMCQwM@|11i`wfg3N}wHVy|80T(29gYzY=USVuD!1#%RwT1dFg7LR889RKU#HLL93=kCx;WEux}rI# z+Iq0=r9MRGrvpc3J9YnhF<7do?jmMB`=ZmZ>TsIJ!*>Qvm~ZerN1hn%3gbFkczeM= z)@SyLhHkIiLJVN0#W${WpUI3hDweGVxhLFNE18XZCP%Gr*DF`+5V^~rKgm?M(Zbp+ zWTaGtOMF}PUQYSiB{jOh^M^|UJ^7o1Og?84V;V+4B_<{2rOf54isRLPJTNTYyuCZR zR6U%JA4c2|2d@Wpl9*eH$c_eC*5iisS0#9y`;Ox7_l!-cShN- zK63v--GAoED8L2IAZQkYryn;ZY#STt&b-N#z3Ma^qpRbs#n@<5OvZLNS&g%*{Jx|C zkGoiFDtq`YIdK0AKX|m!!L2~gyL_Ex&RX+hpzL*r^UrQ_J*HBDN&bMcVUqkz7-~{+ zszMdX*X9cAGj69@Nwcs;$kAOlDeeM=n(4s9;_;r`WVi0jGiFlN@O)QD!OZN{XjQJo zMVZg^-99%5C5uRhNc7${cc6IDiQz8)*!9HXq_}CWrO1Op6WB!4b>Offfo>T_tCqOu zWA6LH;eqKk^bwTEZTBlTRRm0W3WHi={7y_Rty)ZPW-)lZn+}xdh%ws}OaRRRO4Rq( z*jppziix{%?n4bhx&Hi1M9q!vmpSM}m`WAD;3!&fL`p=sjpe40Tr!o&n_L0dsW;M; zu5lP*aq??BJjN3=aDj^6fZvnPMxoQ$@u)o?xM{fjlZzKJwr}0!VI}P=9r|m zj1)NTieojqZ!{vo>0K`5tS)Pls7;54@t-t#_s{>a>+z;Z=}1`8a0EV|0jcKIr(cHO z=Z~hI+kFWDS@OQt-n?sFef8sncn&vlF&g)_Sua)IhbTtMeXT@5(p(tQR-T%Dtrrxs9!d-P z@fyJm+FnIVRFkAJr6CVc+M~-epVx-KULPKAr^bT4xp=>=>dh&xSN)?0qx$dIw2o$x z*HA&YKI({!I6bfSi#!8>2)4FQ$t`=bHqB~8DIn2yv(oDzK*nRFPv;(iYg#tf zlnxmJ0k~0NXn@bMO^N0g0{-uBK=nOYiaKT-pCt>lp@=ay^EVGrRj1{+qQQZj0Fdlq zj;7jhF+wp%?7N0C1ZjpO#g+;d>8j zU2~*r#)W2{v>O~kX8=X4Q;W&3Owag4CO+ihzzZv0qw^A5e5%r_ zIRbRff%Af9em+_oGqNAFw&hTRW$V^xieUX@;&tB&kIm@)7SBABxfU2PE8Xoe#|vAyk;3?;r{$ ztbLB)T6BB#TEV06k^W){rx5>JG`D@U+F;W4RI)q_Wn7hk31n=3Y`Z^g2>FdN(`MVp z*kF^V@A*5)xH->z^KPDdDkyNW8up}+`1mgo*?bQ0|kPl#BoB0I-L`pa~^-HTsny?>EXv9#eTDG7s$CNY+i zYL4-aFB7!;dY`gNvT9Zg_{_5rvl10en5^vAn#HrlgX{{M^rgJ)%?fB8^*l3%<2|e?URO{ykxXVy-IB` zO2O%?=#eUT$V1?5`9@na)pz2X^mSGffCxN!yzwqx@+0ONdC=bN$))ns=a>U zKZD(OXV6iY`njkz{Ck+;3C8Gk@7hVN+|BD1F`x=CS1;OPXm8<<`(>>D!g6RPCvE}h z@#}*xGS`VuUYoO7l_X1yjgj^nLmCQeOI%xh6n7uxkej`@*TPIOBr&6y%Szyhe-O?$ z1!L&hqAj?<{ts8^7B*mo=v9Ay;SsT6MMWvbjhMDJ8eTx{Bgg`?E27*W>AblOP-CMq zS58#J?z4(cPMr?@j=!I*0+_C3q0D!PWK8Eb122y$5gmDD%PR%v8urU%A_YUyoq8=Z zGe_0_{7mFfm&GQt`%T|+UMd$V-Nx`+-6Pdj%;oO4euXg-ohVo3*5yu%HU&I$u}L{f zP8y+vye4=)GY>(Gf`EJd1&#j0dMFOLyY`IKbElz6CBf!-f|4c}rQo=3NJUQAraW83 zk`0|kKcAQY2cV&wd+qd^a{ir~m$%i^yaDpuKb4YZ=+S$~cn0;yfat;*O}K)4g3Ox+|VX#Zy+I$?ippNGCz z%W3ADL9%u#hE+ER(D#p%A#I6^q0*tU($FE%T9-D4N{`>>Gf4xZp4OnvjTOTro3o3M zOXTk$f2o0Q&6v%T?j!hd?_W@1C`A+#RJuvU;!GuwdnWrr7 zuXTX}&q_WhM^YGYEf=ri?L?iIQIpTw`t#3(rTn0mE}$uLPTEmzUTaT}dJU~=6{1|C$@b{2>4aQ#Rf6(08;N!pXZ1-7vFb0e%J05hHKBRzSmu4E2*AcJb)NC2Jeqhq zHgD4@0cg8HYiS^{a>U)eP%~WUvD+v)vhBDEV6wV5!`ruYpZFNdsi%3}T*`g&F$c$4 z-&iq@_EYtcDJy4n5z7eRVyC~V$P=!^xtV7KdKsoj^nXR*DqC!*Q>u^Rau$wneG{-Y zhD4q?MyR4SCVrFO6GyNo&)s!39gXAP=vPWlfgHnq`_{eR_vDSQhAEH{F8Ha|3(zXb*bn>S}A=(6e3^!1*_nHG0EGVcBORa*)??SDBJ?;>5HvL%Tph^Y23W zOFqx6dqtFZbKlM1r)qUT4K{WV5R~*8)bSd}S&Zbw)dZERR#!}m!zEq~QMFPxqW;sX z;@}2i#(3E_5T}(4@vf6x zhJfaYtGhH$T8Xv6%lf-h%Bf2*#Ne+v%2gS58d?zf_YyJnh2+l z?X}WfB03`q$#K9<0SJ1Fo2AvJ;i&fM_fuV^%CY^9!^W$^ zl3?kgNnbxWqiEPwo;eJ^IRMTGrSN>Fbs{Qi5ECXzGA?rVbyxiLtK$>j2yf(`D#aB+ zHlLTe+%g63JQR35gK^5*-CV=?QwY34!Is5vw+?H|sJ%DCRZ)_xtOkym&ov*VlmHI( z^PnAf<{e1vnabo|PD|P#{!+>9Um=^f7b>lURI+98;x;X!UCg({HWXNK zra5RsUQAUA=l;i%Qyu^#+e>vfh{ZRGl^7Jv4WQJYY#A_Y>FUal?bGZchv-yKZq3<< zMY~_0O;IzG!OvT36Y0iT`%Ip2Rn(DRAeZ@u3C`NOsI6%7 z<%FG^McT^!0JDRVOI~+uDRDXBrUreg==&s@n7tPEHOME|Rco=fbH-d&FP~`wC? zSHrDOy7L$0&^?!DkHvhRo7qKruWiC|a4C#yVXPxU!&Mnq75c8vR#vR{hZ=#rQMA%! zZc@|_O0zuqhaY)``cLL(u-Sy}8vYKqd7gIS71Zt(;KaaR)rXkEe-MklbQ zQoOHg$j^=WmG=2RjUjBgLHMl@DuNg$Rl3nq`OU-FIckE3Di%A+J`n;X?qi}UO4;V` z@e7^hf4&!kLV8j=ZY-`edCOB|NU$P~1CIv0%hvTE1vgF$zrE+P2;0fBH8u0(*v}mW zYw^F^s5f;Ejt_-*Ov*rB3mmvSM67j!&5EFICuFNM!DLn5=8UGUrh)i8L-E5o=ucMC zuP@OKrh$H{B&3RB2jk(UF@$J_ci4nmsza_EKT}z0?^^@*Ue`yq?Y6aZ@YRL=m zyXzB&?Z=@5?`TCaDIV$$+C7VKv_!_lAz{}3iRy_|>LtQhOgCb?`l}Lx*$-Z|`kWY3 zt(3*oslN@fmj%lu^}ln#Xu^TuRVei+g^PDo7#$G!(pRw)6<9Y2>T6vAE^$5RF*u5B zyWvJav#%ZMaX@^R!-KwiroYyt#T_NAHOm2+02K9R!w7kq%s7bud{Ih1%^AB^B9wQA z9QiP%)7oe$`*|Vf%u3hbZ!qVxUM#PZriv=Q!aa;-%a{I@kRtta}@zRSW&uU@Uw&-CC%nksK{4*daE)jWVNuzIzk(Hmz^zSpQJ z8o6?s)Bn^{-h<+VlY;DOF2uAq#{e3Mh!5@~6 zRNb>avn5a}*QO_G-&khOw6j(_Yk5IbyK z2-j&7-VUfMyX>@7a^?J-Y6#QU3ij$TxoxT8g5we3aZdVVudcJpz%rRuUI$v-Uwp0C z1oF#RoaS2+fKUR^F3L{N-uH_=i2ZhSQ<`oang~l-+-BYgou7ibUJ+aW%+s<^H>QQTjkD6qii0_t>etHlJtPoRZ&=7zvuMHBucv zkHem*s_Tua3jLZaAGeWIQ8-a4m9l(cdbK-p_~GNQ4A=i?)N1_;_67P_*YxkoQR4ql z!GNFTWs*fYUHnJlo9N~#1>I*nVRoN1Ps7Z#`9#K_lLkORitO({ z%^MWEC}DO%{O||57e=q==Q?TlS}Xj^(03CrzzhHA$*C0)a>*5{ZJevsrVnKfD z)TykzKuwi_pwD@pNl|oW^wJ^KyLSSoTEPCZyT}!;P3_C8|HMfA+n-u=989$}W^Iv=Rt1%;hB||*OOrfrjWp^N)nt@*t=QDjd|rkFo!Kl%7Si2cxLl)Re*n3?? zapa$<18ic5eTg&l+R&c5Lj1?ErKfLdpR`hc`h6vw&&1SixcFbg|F3EADI?*|pjx<* zm%zWTlLPX}Qgs=o)%q~BZ~0Wm_bnooLeAT*9%BZb{`veaaoCe0$k)58{8lDct1L&uuyZHr-kmyVxiTmFaO)b zc<868JEK2ZG%h1|6_ZpD!ubA+oRHa&k=KoOZg<%5AUFWHZFOM-u>2%Y&q&VcqxQ@)$JFVbm1C=#fbmw=v zP6)%LzaTB3==VX-n0&;BUfc+Lb2(YEcUoJkp}I#a9!$!o|Kko-aGP)`XVRT9*^Pb2 zR}puSy^bp0LMX*YucAB$SSyn}_m4a%T6h(U1@qD5Df6(6DB&xJjFZWb1+P)c*U{r9L-SZ37(zOdqDS4$(DO{;;gKOv%ER z6~37+H7cq8@U(U(%e4r*^JgX^Xg7I(-yQD%S)06DAJz=atfQ%!OXYHmcP=#yu>T~3Rqs~hFzwl6n7aOYZaa4*W61H?Zq@nNzJ zME_{M?i}tuKR<~!KxAEC5Mjvwu3KfLq*RgU*=Z9OHe3oX{P5!01m{DEW6BhauKXOx z550eb=7U`PyTPf~s`X1we?^&~!wlG5}o zA0a=pS1y1%Vj`w@b}CV-d-{XVeg{(A-i|2Pv%nFzhR;M*?R2MyUc0|_>Pz$np__et z=;-M3yr}lZ2uj>}`;$%cXTfEOzEI*sHgpt7?2E5dO#Kg;ukRlZ8!n&!eKQTkT-SHtV&bGwdZ0wjc zNsDQGhDTj81eDBFWCKD?9vnGhtKU$A){w|W;zF5y)O_5M$5+a#`{1uOVVt4{NYlKAAh^LAW`3?8?KltB5{!lVh9)r?p+33UO0X}FQ-PJ@A5#rz%^PMbgA-!<-kpZ_oAPZp@|zn~*AjMn~$%qgSg zr&SGSe{BSo9>|qq(dL%{7Lxz3x+r#mR^T1z*8hU`&j7XmeZ1C@v^C^3s1Nw)1KK<= zdXiF|R-z(`o}A{X=BCW5J!5VVbs`S_U`8Ho7J_Hlj8-#=b%b%fxgA)HjS2m7@`U$N z3{AkGH%0Wj#!D0tNj}jbeGmCIxbUP-^V`w1$+8KB#;j2dMSMdsAEW33^~UDc5rg2r zAS2azz0qX~eLlp6m(dTf;l&c95FM~XsFju|W3{ z6+eF&d$@3v04l`uq$D=8R6!-1S}+Zm=!~Lj{q`>)RDSdewAHhIjFgZ8laeiL7|9*KTK8uomYIatK_|sk96y)(qh1W?U$lFThpDy!;b;CEJ!m!oaf%KDLjOnu8cGX3_ zzaX33opbWI*J9mgH_m&7jn!%quen8$&2U(j^=zYA?&l4nZx0Tye{ZdzQ~w6_F$50+ zV;eq1_rVH|>V~n9G>c+2{H|MO9>-6425?7RId*>x;stnpM_B6(4bLy|AG0$qVT@I* zMn1oSW4V&q*V4BannA0+wz^-5GY=aYzNdG>#O2;TXT9~k^J^B3SBuI@p&R*U7gRK> zYa$O!Oz&~mAaNDcrUtmf;O44a?Z>=JFPDaBnK4>jXzP}M>=@5;Rp_qzdkj#l8B zqviEhsty6P@py0-YDassWn&sBOWe8(IzTW2Qw~!Ew9BgOk3q$aCMB*x#)*o{iO!Q4 zZ7N>F*>ohzVeHIYCI+!&1N_a#w9F|W4<*TTD~B-;eX1RGd?a07Xmxer@ZoC%$5E9Y z-hBnCA9@8L?zL5CWA4`TT<-!B?XM{zcfZw)Vm*pB?Cf7roV@Yd^oFHIoH6UOeQaM^ zUaCp^P+@6U+P1qK(2-$RXKx=QId(~1aFhm80xc((e-JgjBy-84x8ahOS1`-V_HPeQ zr+3LiG*D&|q;o;QS@JZ;kMBeD)boZkMUk1}JK7CiUvv%HmNAWrcm$Voo{|X|K5w-( zF!tOqIBrbZi=nOcKJpr$eBKf_YEd8Ds;?Kgbe3V#t+ivNp@dk=MfDYfKa%4Kc6uG# zF2F6$)LFTno#-B?e21$W4{eg~p~=fgxJt(0-X=GEfwg^UySSkFMNsz_l#~=vuFt!; zfA|F|^71>G@X>BQS4XN+hll?aHd}UIB%GoX9Qwa0#e?8Qt#qt#-JP4m=mM==@5!q9 zUgFe%f)it0r+}AamzL8=QQP4&U8?2~$cYnQux=p`M7@!6ybO%q5FHlELV|Q51e`Zc zv;5dUw)cOZ>rKph;=|5%jniC642-tkqL}070y-?zoMIG+S<{<4*Mn2e(}Oe$yxUZT z^4Cr7nQOfgIujQLY{ol?yT6;SJhGn+u^nNZ->=&Be&Lv~DDIAaCzsDkCG}eEFrVqY zu@(}>S%J7XP&~740NNF;VL5k1H$QzV!V8K@xx3YqTy>;DAL2`WIqufeo!vpA{~NKX zS}eh~8XK_2sg3teISvHfHOx_$LsgRPI&u>=TsmQ@*;D zSIO1;=J0@aIW3$ZNq_rb7dVmj!3f#{&=7|)#CHuFT-m)lqnuAuqg)Q(M)HG>vnLxy z!;}mzxE>rwSNS~t_Wj|5e7~v2lD^Gcg64)gzH7WJD~Tc*5Ok}H_fNlxHn^wn*bf~l z#+wb=MW2H&l3Ut$UGGI)_-WUt;7GR9y$RZlTn+M3?FvkZ3b{?P@0kYNboWE6G z$M{_R{VO+G=e#bveqZaxL=}{yer^ul@UU97I`4P?otGNDvtf+YEkt2N7Y%BE-egUi zL*xH2Ni6vPYbB0j$gS@!wdA@2k){ooW&E!m_Eyswd%0)Y!qhf`e%p3-|NGGoSF<1` z4SrgTBALXvbMFKW1$r@i9Zk1YBwt+7lurpF2#r9MW~~vfN@bpm{S95{H#xe2zieDq zFlPH-vI4e=Ik>&c)=d3Dxfl*n6%j$2b34Y`Pnd56??cC{nwiLn-jHd<6t*fUWPQi^ zXnXq{$=3bWd?oDA(|S_m>u(c>SqO@ynm-S0Bl3 zSzL$|Vj0TI^%LX|IuANFY2!GPCy8|CiA>o133}N=+Hk54VE?n#>9JA_Lhi32cl@mn zQ<|@=TcUApzh0I>2hHe3Xwp>qdLMg}9m`dhn+)4w5~CsZAN5MtdO)PBm-;=_N{;g2ymKHAxb6x5TzR$9D*|WcvWOAM z{!W8=%ApmA7C{xEzQnyZP4c`zuI%RaL{7HCly=HRvW2cIvoLyqF71LqZRdj-Sr!yG z6+o5@Bye})V$&7r$BCO90n%47iKurezR6yi^|{RZbCXdNTfZujR&=#`WulcQITYfjCi=uc}l6c7e!85^~MzRk?GH3IzW0#3-q}Qr2`RRHSsR1%jF}#fpTTo7Lv4fj6GZCWB-D5%lx4Nf@Iy!l%w6mzaX6iAU7Fj z2%mgN%yHqrl%~#!e?M(g@=#SI0>`2+B6ITcUoy@HwzTYCY|FX66xEJ^t!+Vx%!CVihpt z9!I$szFlzthNR{FCIFQ`tlO{=I@8ZOZPz+Ta;1n0QZz$2%scwC6#GuK#k6YW;UmW#oTRtF3msM=a~12t6PNbY;a_ zT=HPtYG4NBqztN@y{dz5-vO`GU4iIku~8v4|5tdML^oFBUkM&h>Au(Cj;ZVpBSSa} z;W8#}ni^GUcLN6GcSe*;_Q2oH>J%r3wa)?curR(9c2|q(EmWxJdxUEPt8@2umN0?K zY<5qRVbE;^;7`+;6D8m&?XqOIPK=44f)l(RBpDcM>HO1XVy-d+p;q#ywVBZ>5Svx3E(C0e^MNW zJ9ebPgFBSYxZib++(II|{9_pnRBiKAyXF3a!@S*g2F_~B&X{q*6jp6yO4lIY?aYkZ z2jC6^Ik7^D(SF35Lz6M5u@J~tgM!5Ldi`5RshOJkG{|5t?-}Z*7$9N5{(OjP;;nI1 zXVAc(`p*SFtBk~3b=IP7bG>_D?_~#q%mEi=E_oF9C+u1iW@7K8PhnGc%-w*n@_T6! z89BQr1jMkR8p!4)ZrLH<;9spXkDuf?T+eZgZ)y1*!;`~Ht-k>q0DPVYWs4@8CZL{0 zM0H@8t!dB7pTUriMe54TwP%OyPMt8uvjw6l5iuQ7s>d2Qqm1|>;fwLwz)U7=iRYhJ zdxkpj&+#nUT3EW1y45XTMV0G|#OHij^H-aAuar@uD-z*1y&jx`exernYa*JB3U5l0 zB0=AAjaETeXKk#~{5#Ej1vN7;kfHP2jXaA@l5Ii3kJf${q?t9lpfXdNO!E0+_xsA5 z0lVN{3oYU~dB&8KG97T*teDa_m6US}xi#y17)oco1x!y>paN@oc1Z?ekbm?S^n90# zr+=n}$2jO&(5YL&x9Ssiq*5>@P`o79QC(L)p;`yzbw&sD;I3pIz6Kh$T5h#2yOq!9 zS{Xx^vBp)dP{y+%(m-qybhPr1tOCrqe`}W4$sd=}vKnOR4Sa9*-3EJahEYX$ zKHH>j?G8=>?2KXwv7=)LFdt4#9BL=#AZLy88kSa2UN<1MC0otBcSQ1^N7AC|5NXy)WS{}GK{{A!9qH-qH!KcX-0OSSGCA`!k z8-!^2eF$vUn>O<}ZK&B{_D;>qt5%MEnevD4{HV^Z#=`f`itX;X8C`i~ZkS)?KNraX zT%=IY7P>67Xz*j)u`m0oLM>4 zRkR8xSC6dE)0~YF8N=vKEVF)hF@}Zpul$hkt@PSR+4hD->VWQ#0+^`Es+mwNe;}ws zIK^M~eLf>0*NHuz0AX-xmLJa18%q?N<>L$6Q5mGiRsg0x@}!v#2)>`yVb9W==z!O{ zI1BrAtwWo`Ar(=-U0Vb3UbEwf3^|{7aYW{2+?K}ghKwZ|(ScWE(z70_-I`xp0Q&s` zHQey*Vj0y-t*X!>W^BuIY|{*v=ZfhDFgqqlfLFAo>IgBl{`2>X!PBXKMk@55k)q=v zn9!_+DUD_hQmP)b2i%qmUGiLqcvF(R4^WKz8(D_->>H!&N-VF#u$af`ysv~jgpvEa zr*6NB^O=QO!x-(sYrbpo-UqJ~JB;a#asnjEXFWV}f+ih(A=6R?m_g+d+vBHCKL7g< z_yFG1mTjM;uwOZLK~Gq!nJYausY$PRUtp59eq9{gxk9$bHyO+PnG5I~oweF?p14uE zu{uzDR)^L{Fsz#3(s2S}m!bAq-}t?ASD;|00S^(Ef)VdqSLGR z3FE3J*hAnmG-DMC0WboKbE>7KT)zWt`m-M~!*n&VdjkDSlYnS4kZGm;keNMU12yKIEm3!**ieH0SiDAq8RWWI`{tFHb;}s04F3=miRo2V5IqCpL)3O zf@jQA9KrMcx~ATKTRANv`b=^Re>N}~Vx-xXqMPJ+ey|xild_S0@IxFN4VbRU2rFVi z7z_PlOeF2)+q*)Hl-YeTG1ZOqh3w_)&D`YVJ2ns&at!{U&6pe1z?gVC8QS$NIOXmE zHz30x>P1{?3wk!6!`G9Ckypr%n))UeJ(JX<(gTw~P{Fm%E{4*t0-Z&RXD-TrrVhh7 zyO;O&%O`*hB2)->ND-QsLUpP!Elh0!grdo*qd;AcK#N(W-QGn)>Ja|hQ?g}=i(1rS zGwp=%2%**;OLgMz^SN^0R(}Ko{o)A?3!u?~7a)FZw3b4Ar;Fc$ zwqZQ5GAlEq9-Bwq|BU3_A&O)n+*cbIErh-qua4Dak4NufE-2qa{RNqp1L-@25C)tK z`Bv4rp|jzGTy^Ma4)pNFvzGV%4*@Vk^VOkJiqXeeXX}_dmKUlr*69T!G}}OvB8ym> z4H)|)De3)qnKF$QX>mK*-^UjNltEr})J^;eAbjJw#lIl(%{`%yfMW1bM!`a*(>%zL zCuelx9bOAi_T0p4iggOO>>$3+q5h*Ct1^hg6n!Yi1A~F$4$(~MQm#3kM+J($FP=^+ z(Fmb5=CZsF&WrL>9q>IwoUfMhHOYjqyFFOAXZAuQ{e`#kA>~_#uVc6YpW760;|CiP z_nV5M@x1=icsw3sy&*(~Wi?v`W*L zJj@Y(pYNkKL|1$tcEuf!N}TS>pV&(h9Z}cgAtXG9x_wv@X#A8qD)_ts4JHo)5B)E) zsD<2pU!0kDGL7?w)oKs~UkX)&{HrV@*HCPYhNW}%&Ow?{0cuEP2=86x>jtd6foT<< zFc6K?(IE5+{AYW8VN+434d;`pdiO-%7slg`r++FI16I9z0ad}QbGb=3OlFkA@v#OS z!Wi{U|C8Czx>dUTEfa^0k;dIKR;?t_$rD@WgFNH8d0m^qnaR&*&A+fU&fuBVa>@2H z#R`&Vtbpz4ST~85^$z8Y9~Z!#UxoMXDgHi&bYkP6H%84vWIx{&X}`4c))0LJsoVUW;N^ zOR6JUB_CXHa30e3_;sntXQxbsx12yDJh^d^^eQ|34rx2?Iz ztr7Z+ui)_ZDZjJvlh?_!=@CJG8?TaeKtY>M>xciW;6@WgmHj6QCepJ6|1K%yE|M@( zE5JiO-ncpD-l&NCX^(NmIQr;@t)n$+==U_cLBiQf)mS#52(3TlY1U&MBI)MqDSRvV zmPzJTfw4ch0oej4f24e`0-$VZ}u5hi{=>*2Mp^ij> z6I=}d_QBdUH;G~{r(e(GO}^%-yCC&k_{vetpT)>S-{hbn@ZOxS!T545X4L6X-Z-TW zct8RB+&~D~uEc9N0g{VDz;Z(OXFlMd0Gapvv>A3DNpP7vY|?>ycI2Sr?r&I64ZD{K z2Q&^WytO(Pihcdo0SHaov~%iey2T{f4dWUszs(l&h&PJMF;V~&AXW_u-e<>iqZ6Pq zYX>84MF!P2DsBEz6)u^weZn!_+8eI`^AmqGfltz!_>qi(8nt9%1w9w9y>C5UOxAP; zmws%#h3!VMQY?8~n=g(^5k?OMu9g4Ve$C8$qm>x}@%^V3>nj#tSU0>M?D*`9)oanH z+z1Fs218CZb_NfNe{4i_wyI zR*MZWrE}1M`xPae$+%^HvL1e!V^t>1gHQH{I(}FU+W{CSuTX{ldFM#|up5}X3vt{d zX9G31+H(*0dbg1gs!RNuy)^ZffYP*z@_)7W-eFCx+5UJqAfQx1dKVOxrt}_A0TC6D z5{eWl(t8UK2t|4aK?MbrCLl%W5SnxlLJz(7not9T_}yp9eeRh%bMM?UbLaf#`JF%V zXu{6kJMZ3mz3W};Q|vxL!CRB^&CHGO8)c3!NKTfU@A{;_s#S>;^HXW70@RO(EUGFi zo^`m6PjYp#zd8B(pjnR6F7y{dRvsg3#zMnl>g=`+^y!kLWJJ2k8eZ?4{f|`H&IlCP zqm-aCc^Hz`)XGMed1P4SooTVRlX9)Am-*bAn%PLXi&aSo2}NtI;t=ZD*r3^#4C>Ej zm3BjwGYdaGvY!@@SM%VZ%wl3ST?vqjjokN>XqQfd-9w;tOITHz+g1%~ej#f zR#%W;FU+_MGm!|EmG+Pk)pxW=xEjL2LvNlGPYQNtKX*}JTc*=ew?mbNR8SW>Yd-oQ zQZ6rVDy-V>-diq0d|JW89Fg2wvZdU?Or#SLpaIOwb{@KFquY(6BBpzJ)2yjmzqk zgcr2}d~!yvyO|`iiEdbwfaM}Q!k=GxM3yaQvg3Jm_{?_fx;WY;l(1xeQdWbc211ns zMV_g**Mpo3E?OMefO-CG*SL380COWu| z5)#hzDb#-!y8W4;oz}`Oim{*JN@`&z8A|aMXcC@!W@*OML73bpvWb#)NzKoL?zYu} z!?$In7SC=xKl2`Ua&c6>JAE7QQRnn*S7FSg0wkAvc@;wh#Ue(huemU%>~!!@4VD-^ zzE=M^sf{T)L5;}xvqC4~)IrHRc}e6#*^-jzZ3;2RMe&`B0F;Voalx&Xf^Q(oO$^(!U-eP7Cw;|TwVv=lDNz={bl<4 z4DghD;AE`+z=Q6UWmuey@zShDqrx5hn+qmlknFC-4TUFh#*~V3Uva7!4y}HK6A;Ae z>pxCk6lnPtPW5L04j07JGh~q`p@}#^&OZ|EU4Z_4uy30Ra)5oK7J*Aqyx zrhwB)kxWb_PG9P)hV4HL3$VCree4Vy(X-&_?7zT4Z3{6_j&jQ%n-rTW{a_g){pDcC z4@*Z9a%ea0rC2%QL8e=lYI*GgY}xq%n}Q^#g@wCl!_5(^ew>u=XM=+Blzv1ci;Svf zy0c=(qOo5-OmSoj&(ORGoqsv~koLwsZ@9>|tRqt02XuJK*M^M5U*A01EWm=FRMIOc zD%bGHPPyuM36AheB6-|W*MS6i9%pIgMt9ga?)2HDr#kB0Pi{dXon1tx2LME#?h52R zmZ5DTF1+~ANJu|ca>uMXd*G|`0LN^VZ>mJSL3#PQ?93x zI437LGxbT&nKNw~&QimSK4HyR$o!Zkm+OWFb&2#9`GT9lH%anM`U2!AGiy`TeD!%F zMfk3!kk;!3#*+Zz9wv*s;gAOM(SuFKIsN%Mh#<(E%d$!55vyecVZ7~X=%Gmj1H{b; z`>}nd57}uIqgnNUm0DZkyRVfj)wcj~1l@w1T5BzztxtXM`XGOFy5VZ_Ggu2Jt6S`Q z#iCvg!@-I(^{Ta@qE?y69pkrbDx1beAD|uWGyG`b%~XMkEMu0?n1N${vuj$3^7cD@ zGAKIq)5Z3#Y$eLKPWGqzHBRCxyuGY~+nKEH?UDev`E?s}Ep!1)R-DFF4vk7{@C**} zyAzG8cl(**Jv<{cd=;Ba6-+OQ@4fcrO#|imtOQfvosJwpihu^82oR|p^XA!v+XRcb zZdDct*K}T(&=m+Mpt7fNzCk##Rzg~X4P2ZhnKhJ%`z#O=(i4u`H1ogf=i1xv7EQh% z+~$LdkX3v>??>wmbN3E%Dsn{ zh~BCFnCcGsicx5sLMcW*Mm{2+aQVrdgpmhJw~qnK-8~tOXuQD*q}F(f{J9CB#-5Uc zW(by52fCxwwgxMWV&!3)1aB1Uc7vWccgsDtFo3!(vWHmL3MPCSS9f3>zf#8M9Uh#? zZNo}+Tv&w$2d|ObWfgv#4)&C&=jU62&A2C$jJ=@e*xBzA{uh|jWs5K; zHS3MXasbDF(?kWry{Jwy1|3q8e!R%|R!{cnr!EyXN}1SJ_~|CHv9v*)%w#LDK>x(h z^E``UWqbV`S-*>#6(x9!-n7P$M7QP2BUjm~*WdLF0hHMy!;GKkAWaXoO)FYoo*mGk zu#jePe;V{u>b0o17H>pU`OrpV=DbWzY+;heVk9jlmp0fb(cNpcX&pQtRbZ{E&m`0l zSMaY0k}!FA)!yT?egKm@QassW*f`HPbmVcyuZKJf`hbClnPaTN1j{d)bf4XgmKUjb z6{-~nBvH02xC>d({k!U0NZM(`XXDM(v4r%n<|+_zcL{+CpiIhw)pWv8`cW<;gxoA? z{BjT@)w;soX%OBJS#w65-Iq}q6-K7jD}_ONWG3CeS@?VCovZJHF5;E{m|qy!sV3B)`8QpwBtJbCYBm-gshITh?MMxho?z>rm{?M&le+Mg z*n7JVy2`3S{3N0j2OYtqVipEqpHCg4B#+zS1_zC7QRZ{?kZtJrk*d$>mW<2pJM)>E z5*94+*|XtogywwO2XkN^E7(3}^PVrgxZ}$j9K?5NwRslMTCo(&Sd`gxuOyv(UfH?N zL%1X?J{3X{q;LQjpUe`&m(pxzkAUlr^iGKXd!tX=e(m;Y1U^9Mk5m>Y49vh%7 zw`(VDg-^1B86xVzb z6xg;m3dg%Y3tzeoPKM=Yivq+F5HtNs-Ket;gK>QTLT{{F^!E^H55(37LVqWHv{<{OFEA7;3uXH{oXWmc${v5C(an|JMH z`1?3axMh565Pc08DC7trW)Hy=VUn9==J2iYk{2VgH!*Il;;Z*Y4rLx}Wsvd57hYuZ zjdtJ&Cn7Ws<_&beDa$HLqaCHl-UOXtWZmt%Q|BdjC&L!B`C;$n4FSyZoWtZ0+m~%) zwoC_rX%%P{lVNTm!X{xRghFBR`Eeqcu)7sTv?{|}D;3f9k^0GqR>gjEn>A_js=S9= zh>LR;=$Eup(|t~2WTUe#GG21dGK)Ot>T@6xwqKYVt~byCzRhI8t!dIHnv3B3#brw9 z(40Kj-4U3+Oshjl*8LdmO0+hoOrQq%<-zqBl5fl@uNS8c+a#ziS2Zf>ji@U_7l8d+u1A-0A3vR-U19n|{3Dd{^|3%o2HV3h5} zs5P-)A3!Jq70s~M35IDcwF?u@uu8`oMv{D%V(Hjj<;B6K|P^7WOuA70~TUD}q_coyYMUAJ_a7<(=NE*-DG zpsiYWNtGucFU44F4$fO!s}y$Qym&t*pk~?7A_^3!%DC7GiSautEGqjh>y^)}W;q6P ze!1WrgbFoz`CQL)S%A|R`PJ#vvKT1b4sVEPI$#SW0r~WP#NK;4)WBui!2f_i3DNhh%dB`sFr0z_wl z@EG?4q~LSpyNlgRJ8#G9binST?Vz(QKyAnQJ7?-mdh2)5G2Nui9g>{s@1=)uLgtIm z^2sQ5re}sz50uB!S)-$=)o_K|{Td`eHu7FLk)01q9{WJ&%Kxm9;vxg zVvk*YZnF7dW(h?>)4tZ&?JQu2&&%Nw>}i(mewg!{DAFR#4)OEEN}f3LP>xQN`dlNy z^jhlsjtHEp$I-U#nu87K+tpKE!hTOiR)S6bbn4-Z1vBV(Srbb`C!Sox@6&*0VYJ(( z0=@jrTU9OR8n=-ueJ6AuD-#*ClwjL$GBQ639IWHW0ikD>%xeYI=26gy?( zFP`55w1y$f453@@zLmNu6Lc|=G}_6Gi=01eYrV%%;qN;b;_EGd9=Tnc$Lxv3q>7 zNXobkou`5hN~k_|WRA55U#i4_y%_FFs%m=39;fh+R@z^dkh6HyQ}>YGnkFhW?=NOLh!U2OIa z4TK_Y_kvtK-z#)RpsdSWk@oo7@#x;#p!$leE8$9rWS_j&zA3?Rsn47Skr#Z=*r9O~ z00nOyAc?PLM6>h6$Gy1<@{0w~=C?Mjfcb>CwG;NDgkjvg@^*5McUps^OBLhlXi^KF zdbm7;%@)sF4NaXn*H$jjtDcNQTnd~7u1p|iUK>jh!6wMfefT*eX2u*!+}!sFZvC<= zwt8>&6y=NI`&JtkjS!v?e1qw@nam-a$Uye)yuah1$mFukDH}3mX%MH$?_wZo$5L0V zK-y0FqKQPZqaNg*vQ0RdH4$%i>ypEI6|4Cq-Ml=L9R!OhN_*sRcAbSyB97N<^WE7> zk&QnwHY9JnBX`aJn#tC;tRAPEMy*!4-`v`zp9dVYSwP7id>xd(ZCfdAC>lRG{?ePN zO5{QN)wVcKf$9ucKgodD{6q%s^?NWL0ZcZwsA^OacCl#!U%0>mX23q&kuf~e`>ywj z?Q^3s)B>BQ=!igLYEEBhMt4#%`rZYaVAVfA4=$m`&5E#Q9soZ!dsIqnYx zv})ftrt^%BQYiP<`2O~HyeIz6bbBx2e5PjCsO=Brs6UA3z6Hc{dHyV(ODbueQ5PV& z+F)1kX(g2pn(94vm6QK_2Z&%e(dW{>U0jqgjeaVH%regEw$c5_BfjV}LoN zjLn(bHrujU@T{md4`l0p(!ZHxAXxg zD_pw7sn|xs;Pb>a#Ky7boM-|SB6E=lZx$eC{THM&zGNp-o4AD$%v*2&;nEW>SJ`^R zQM((m$U_?-bN$k)kt7{J$=P{Kf_Df{|GNR#*xbY30^V);~D;Z zWx{%fyP>jtjf+G^C6ntFRTIQxp6Y`240$K@b;l2xmx-_V8Bz2z!SL^R zar{|lcGZaipbv~?Fm3IoFjOaP$#6%45j*V)h1*HP>A|UkBRDIJ$u~6I(Qd*hYwRYw z*|*l(pPYp!!*R|kgW1CahKr-=dKN`dmk(P%){`yJ6hc{a$wlx{H*v4}iB+OyT~Ad4 zA??zavOj9la=v19h>y*rb`{wi{UVw{4^zU3Ef?YAa$6|l_c(1{NG2?H&aBmi5(`5( z&mTKa%<~?>%6SLD=gf9%CR7Ev!4~1SFCa{*umH_K0CU9+Xy0^@G}~|08mC5y%{^5~ z<91C*lrhghRx|czbkzFC<%2_DT-lG|N30h^3PNIqj(l8aU(4;MhBXx};S@1C*|VDl zdOYlFI$utH0!w#e)%?t}dyB18vm+?>OjNleNS}5p002uSBE0+oK@NM(yijCSmHZXN zS69=p@B2I~7BR5|n$!BK&08*0I>=Dw-0WZtv}1)(hD+8%Zhdt~L|N&(Q}ewx*BOct z_rf2bXoKmM^o{2!Q}(9HthsHT#;q3YD#jI!bK=a6(FnB#Uw$#fS0yLfxZ3SY&%--3 z`&VZIxQFqa*d$%lj;s{=s9h$vOvbAE-NGLlWgmzz&15jym?;jvE#h<)`7Ss50Y@+?wbO zvddfz>bHd`@O{2OF{;CZ<3vcD<8Tgsl;dHuaIVyFO(K}w<@~J!VjC-S?9%QQ?N8Tj z3*Ixf2md_VY(KouW{rDHXM3f~+Ues+S){=CC1Wjx3JZI+E-j zZPjaD86)XJ)40`6PSoXGKp+_s<&G@|l)e?eXkUEsCRsTN`_i;pk|cV+SCJ&RB7+zw ziJ_}3;TQJHF}9@MX}ad@k7iuEV;q65(UIp^f38fDl+ha@N-z_>n7Wrii&GeRjscdkZ`Olun4;__syf~5e>HwlHFPsBA z`3(|J74AuWExCn=K|DJ?8jiT=wBGLW&a6&|6}v)3YS7GvxW0FasWfbQuXmb`?P8PS zS)B5e=*sJIs?xe#uFt9#RI`0tU2+B0YJx_T-*JiKF_1_;pB{V75%&m4N5y6%8-&(U zii+%*AAhb)h}lyiMvNCTk z??pel-u|6!vevx8ZKA;lt-OoXV&|O17kaaILw3>P+|y-yr8Oj5}{KnN)V1 z_nOW!{#t|@VaR%-%ezy}Y@8PAu;;~qXs?Jg?n#%ZdUELPunS{J@2p~K`g{+NHo%zr z|2(E(H(*SQh2}R(aQuyQ;!^vxhXM+;>kV?4l!S#ebqeTFH)#rPJIKz}Gi5zEaU2l& ziw-tM8M9%4DD@D=<9m*Puem^~`~Pyn`B%Ot`LiTnpCgVdUmYVn*=~&?({qUC&#rn~ zy}LwF-SJ%prMVCzxM+x8K1#6Fv8)OFPwRlHPnlF~!~X1>5IC7B`o3$E;KJl<{u!W9 z7)v>I0H71=A&)D4<)mi@pOPstb8+WtgIplMN}Lyy9#U+RiUU*2@-kIdN5~cyn!2ni zZR7iKKIHDrtHl+w!N#K1YDk*)l;Z-Bdo>kVKHJ;Zm&T5Hs58EK-dMqfL5D=Ld230qzcM<#Q+Y z)2_Fvo~mv~zhh<9%!5+3`OJn30Q87UdpAR(oT@6TNK73mRXN=`*-x5QSs`Bs6xiLo z<|!V+BT{BWRKg8f?4Mg+L-cmQIUU;NpU-@PE;iNn6%|cj>%Ku$?eYzZkuz52d9tj8 z8!GqWea(KxBch88n>jAS%(K;P7V0)FzpapRPJOc28mt_2CUz!P_AXH!f{AN2-+`(> zHB}#L`yBl)uBJ4(lOt5g;}t+Mfs%bomqSNm))}fQQPTGfj{+DUiX`6I`vHP31$9hh zVyFUPhE&m(n2aDH&QQBIqf%M#*z&9iqxqaY2sn#Z<<-sBo7#gw|h+H+|4R*IHp@N$ADAiloIAo2rUA*-~{ZRn?;W}_v z$@j~G7bd9~Y^58DKAmT&S0fXM^|pF`lH?{U2cW^ZUuD(jD2&yq;~245G4>V2MoIj^ z$4^Td2v!5rsT82xyrVZVsVLK$(xovm!(Xrm`aR642f{wGya0x*;gE(Pq(kXbzIdRNy#Fm@ zAHZ700%fSy0iXrD8UkM3{Q+W4`T>f_>k@4BbBxx6&vo>xCP1Cn!q6u4mh zm*4FYY(A_7_ca`N96-4KVnRZNszIne1)Exi8^5T*-^O$E=(qVsU)fZ%B& z93$$gR{Hyi{yw6eC9$|09Y5{vt8knNxL3Zee2pecyGgGNZq2)z6lIw8K=K2yCx;MJ_k;Q z^N%V+e?7#Pl<+M{pYGLc9>{IQ;f-O98XqJ1-W`971aP%~<3(ht%c5uDi@M?fG*Jy5 zQ#e)VGZRS^zwJgll9Lom7EjbrSmBlH8Spdk1HFPc^9-8uryHp_1K?S^qW%8w8IHf) zhU8CpgI^Q82$!U9e5@y&3j%%(W%1s(K``m~25G=!p1#k3A7oSU(>kDkcybw7m?APp zt5e^+XMB|uQzz894dcqQNjy2tt*k5hF!4ridW_(NvI1#m;ASKJ`P3KQ!^`QLzpTD+ z$7s1dhSi^`Y><72OrHmAlWggbox*4gk|Wu;6&AfUYA7RMCiT=+LyUA{Xj>uEwp zcaEA?ZZZ3?gM&`SY~ys^g6kayndFF)}h6$e&Yp?aebfa-VoY0%Vo?x74J8 zw>?xBdMTXf_M|!yLM7jqxhu_d?xnwJW@25X(|SBm@z^MGy|lc}C!vYsDYUqxg8^l> zGCE*k8RL-UkU3`&t3FyT$T_6-t=6Nc4O14Yv3FZ<9M_JN;*6^sd@WMZ4YhXb=G!^} z7~54$K1FzgEiVecx z;pwUy^m58<2e|&ZFX#@JVPPi55IO&_`h*{#9^~r{x^C=U{I15}j4w)9^6fC~8WmyW zMqwnH0~eV62adXQn?2Il;Y?46_mp|&MqTxnenH1=7K`xALdvPg_R2w+nn1Rf#>RF{ zzc}+X?u2)`aR5woNe}S@WbaSn>k3j_iG>Vc+p1c^&mUR0NoTCQcn0~V^X=Uh02Pk?aL1K zv|B5foEC$2nK{ZQz66^YtM}rbchCyzu8f>B{{V%ZINICHTpbY8f5hzft#k1`A{a{; zJyL&9d+6ZP{nO||nxw$=)iDpT)rsz854at6`isiaF~_gcw^zkP@G{-nc<1tlOQVUwJKHCl;cYakjup)?>jO(G_k`~ z2BXQAJPAg9U&I5Qvx6m|!{iyec$J zZdLJc?#N|X4J$acO{Jz`)GLA|>J-w?O&(oDuzKrZ0hr>anF*uZ)XG7_?xK~8$+3OW z)lGAeQW|QsV%HPv%f~nO$Jx@e3f3ncnHectMi>UB=dF=kPXb(GwhcrC-wSs$1jlA= zP+hIIdC>@GHtM?Y24M$X)N6S_Yx+D&m4duylF&skw)W-jEno9->^%rs$Mr*`%EV25>Le~ zyC_C?rN}Ht;(FSe-DYZe>P+7CtgBnI0Sr2ZY5oE42ob(4$M$x% zC%4@IFUppWQ38=iUsv23ih)#fZ9ugkhGy87+0|CV;k>7Kp%Yx&=DpZIh)rKugPQft zYAOX;y?y_j(6O_riE$gd|4c5uFn@hRw|(J(;>P805;UEGZ&XzNqhrBAsg3WdycjXr z)b{|EP=(I7!Z8EAdmtv(YjXBtMJ7(DXKz6d^L~7CHggl=4xkzo>C;8u8;==H7SbzL zXK~3nw^UWQUD0tZiuEwIed0Cpp17#UaAs2FdEP`$4#LW_-%CWI@Xpd`5H#i6&diO9 z&C8x>!PfW(nvt=AUr?LR{I1k!j9&F}-CZg=wzu14{xV_(d5}n&Wt7kCPB?Wef0&?j zW$??p?4^@zpt&G?w*gTSzq#4HTGD^~6@22ocYgPaI~&ItO;?dDQo=p@qX2Gbz-^|-wKQDSOPM|l-5b6@z@;O}>bJF0 z_wZ)E*oSv(cb!rVPb*HCq~)V5KN>O$5M1k9^^~%5dTN`K5w=(rd&*=fST%?Uvz~x> znYwzjSM(34y-nVFZc+55wJOvj-=VRLcZ8|v!33noXc=B>0xq()^=FRV;WODf0V;j> zsH<5@ZxfTsLFtAx)!!x@+Irf~MoF%sfeuh_#ZeCIMe2l3JS-4GSP()Iq^>Kqt_e*R zO&KnV>FpVtc97Rz4C(OT!C*N|e6|bfdvl7m{2wpb-GX~GJwIW-QZf8?KC63J!f)pX zXfEB#&A~yuK2M2ad{{6=V^3c3BJ+N<(4~twySk_cdllmzjUw_EcLm<82fted4;JgM zPxz;kehHFvLz#sjQFNYZ$wzdo!ZA}-^*=!BZzGDVzih0tL@-~Ra~OD7uYC-tqwA20 zQamy2EUrj9s?mG06hp0Cb7;@*pFEAe`{W;L;@oOa|H?k&ANRBwpWpUuS z46XqaV@;K+J*qA6sW%qhEPwA~1L9+x(-OxR{9qsb0~7=cQ}XjxVxR2l#u6<;_f9)OPIWrX2qm!$bNyKfxf70<(f%Lt~z@^*S0 zIQ?QNS$lemS5wX1^SLu^i%FuL@h1M_|FDKdnJB5$jcqO$;d*x9xP?y!o-98iyjwDq!LAseIm}cr&87hgQ+SJ~}>((aLILZ0pWo-NPHTh9qC# zr5+^?cu+#I&@bDJ|swZy>;(NtuXb@NS=AQpvSB3z?ggH^ZN;|~4`)g_6ptD{-iT>F$oHa(GMJPBG z9^=n6-I`?UHHz+V`(nxdsI*6XiSp9>89gRWw7S2F{({g1!#%9ind~|TGtOT^)h}=S zk-%J&O|j!g$lfBkvlM>^ZwS$T`M_*$&RH{-3XSHLp^{hY-LX5IZUE-COWgcwY_C;ku7I%RN zH>I$ z(zi&Ns_dC6jcDl(#d3{?z62%ZP`io&sQyY%ifER4K- z&5NtEbK_RSha#IJ$i-Z_R?X=t!lm|AmN=&tFo2>x;ccFPG|!Gt>JE#Vgy6}8A6X@& z*tEv1J`j1w#6zRHyqauurlN7dPh}2y1^&(GV~okxw!>?Si4LIJ%{gb`wvG~8@`TeO zAoE5K*3-ci{GXu-SNI4vs=w8qOC6VF&L{ImScNY?BPJQ!yYbV*Uoi>(J(DK*|FB;C z6Iq&};TZG8o>;2*S?8Q^#wS_GE0{qBB)A1_ZE&IN>kK#r{}f$N{ms;U3a+5!1F+8b z!(v5=0nybnx$6v9RRt6BZu#$jmExZI3B;jbi;R&K@%xPBQ^6Q#G^kA)>X9XPc|QG6 zMR}qtg;OnY8Fhcm*Gdl`KW)7V^&*~7W#s-qM6I%FK&?mtXem2>;EuUl5F0=cKmyz-V63SxO>*Q5CtUbCrU<_Iy*{{=s0u0(D}b0Y5v+{@@qaL`G@M;e``Tg17^qO zf(fB42AJ~xif`6O3=+LG=d1J>(GV4ql*z#65+smn0I3De?@SdZGAJ(%GsM1Kh;q2s zderr)*?hkK{(7q4OA!jpJyz#@VOiU|b=`U?ot*KipmY9@4f^L92Tl_tP5!(Xk{0zy z)K_dq%jngM2UM4zz0uu)E)l8Oc_zg7^7D{C)bI{Sd;(=^tW=3b=mW`2n1NHvY-klS z4_nybQ&Ak8`>YNBb+XUzSVG|^O0FtZ#!_ygk_wQYixRBlj1{Mh5sLFCSIU9n-N2DB zUT?SgX@{}p^nG=>P_LQyb!$Y8nwL=O3K)QUGC^x+TQjKre}X9wJBrDfH2Vyx+PHbH zsOAldaUDg`TRA?3vfmllSqWfnE{LneLW_BS}A)2jZJXc#*Nw(u)LMYXNoxU;bJjWd%)nPu8(61o!`TfHlc`TG_6*i?65`Hqa^2F%vR82uEXH}|q0CWbekvucLbQ`+hIQ_D zsvczt`iFrRjWgzFX~wXz<2h9qc+?x2Hbka;;(R7+Dyk5HMUL^Zj<>ZB#F_x1AqF4f zQZb@xNECUFv*Ilux5GAycD2&G=~rVO6gyhi<6=*@1+G;j@}Xl0^Y!T;(cMxeHHC7< z)|3R1Y(W+pL){*GobloSb}*F{f1LDMm8MF;`lUWE;T9q+R0sLA_yLmbj)?-!O4=(H zCslyE(GZ3FGAdyC1wptg{3L`)SFS}>uy#tA4KN#Z@KcL_bOBL*Mw<@3<@fk0NPCgCP5Tg8ozRU@nT6`a1*yzO(1$%s^05csC z5Hn%3J#txJzYr-a?Vn+OQCP)+z;Cb?kzLohD79AaW$NF}PKH_ircOL-!_HkDriD=n zKP2ll)JmU9300e{@nX=rnnm{@S=$jr?%sAO&Fj*e4QI`ivR-D>{@Ci_-6T@_C#z38%eza-PPUPk%sH^8sQ_|8}KO8xxgCkKSee9VhUy`k%M z2``CSC#^yzcoLGYMZCF9>gr6z8fSU_5zlH1u=drrj^T4iD(OjENOzLK_ug_vFZ&sL z8pmc!GHI@Bu9I%49Lh9BH5R5Bahu}z>NIGlaEAsd{Io$>gn_sN-+b&V_d>QYzVpI- z0)eFh757T*kTA3Mj5Bo#ZTz?ifpSzv`H3;#WV97tR$xCGGbBc}Rf8^RZ@h%NS}U0E zLAM*}!BG6Y)uj<+EI$mk>0jLjNhs}M2T;lx8PuXb$CDHC9`FF$(aLA zV*qD)X(|U5asYi1>RkNw{g2X~kKMqM zRkN{hwqxc;u|8c+@B5b$2i{r9XnS#x;F+QAsG~qkUWbvv&Wr=taPYI}+GTxw|JU)8 z=w+0t!b{1Cdx_qibRn3y`SaCnf;n#-m6r>@&u1sLl^8#EdP553>EISO7IJDWB5~60 zL}8KmcP#dUl4DvSqB|!E=#Ywv-VWU5C+!d7dng(vqu#u7yM{346uNZ=%S_s$@E_Y` znCQ!&n|!Z6KV0cHJ2hTjYyVnlVqn>Qn(%?y$@lxo{95nprAmSuF2HCo9nC`HRjODr zjB3&DM?=FNwa!e>)Isaw+_%%jhsvDU)%FGNR@q|AvEXKSr9_i@*ycR^CFGPNT9(U- zz4wksmsQ0GKbOfX#g!J87Ew*}C4RaiGzdf>NWs%?58f0#67=#5Kw?e^|%u534~tErHn7{Y}0SHf(8^ zx4A?TO(ij98#1$l7xO`@M$F|)7{6JXlOgw{bqVKytbWnDL1#bikr%OPT^iF zvCV@V2DV;j?3>ZY0B#=eTKSu7Ys>jdg>R29+zXO{5gY}sK%2ZboR>o+`92*}GDoR+-`(}cPHNS{< zl{~2;)1fEidzkV(=F-+7Nafm*+1;S;&LmpTLSHXB@Ir<|-&!QTmI3VJrf&4Py<0?= zNTv)ei05DYQccTr!a zZ~k;xvr@?;)GBmH0MSt*P<&gQlktMmZ!lE7JlF!Hw5Qz;cRX3O|`PAFB@D4ZUp%iP7(mlJx4hx%} z&BXWRfY^=8GCL6-@d!)Miuc7qO#)Kmuq`434EH9VfTBnacRDKis>bpepuUmFOG zrTV!Lrdg_;dLch?QTZDL=TQW^t8wQS3SwU)Tcmz~rUCHE-Quv0ROPJC>zal(Urvg@ zhH`VdRD`+8*_u$Sc~L|4)@lp9?H80xNhQhEogd^seHK3BZpr^>!a#E3oKkCEi=wJ$ z_Sg#8&nNktU(=#^kX+9eH&n2?+Wz#IjIiCe2LXn%B+x$z4pt7I{T>H~j&w$E&Ow#8 zWUR}c@#@8+OUw@;kGXA!uU$$gi7@UHiM8tz zEx8Dd(jH;Z0hUEN53UJcw0CDTc|~QiJX7yl_UQ;B<9y}BJNM(rS^dv-xVLM6Z{qsj zZO8EsG^749to#=53I9Z>_$`k8&p0OGpFqO@jD7tU`}iG?__t~v|C-PLF+AeG<{baK z`;zdVxv2gX&G`TB-2R@w5&jd?vcKne|LkoDe~B|G#Yv`u{f`{kI+a zpJUnY@&C8I2EWJu-+KH<|IPV4zw?p)**V$&W`Dou|G)L~`$q}K|IIb~|I)U<$N%5@ z{`o!r|CZ0g@A?04eg6;so992j$N%5<`5pN;zkhy@|G(}1)BkUN{|x+_pZ~vm{2%{+ D^O5Kh literal 0 HcmV?d00001 diff --git a/embrace-android-sdk/src/test/resources/thread_info_expected.json b/embrace-android-sdk/src/test/resources/thread_info_expected.json new file mode 100644 index 0000000000..3f068a5b41 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/thread_info_expected.json @@ -0,0 +1,10 @@ +{ + "threadId": 13, + "state": "RUNNABLE", + "n": "my-thread", + "p": 5, + "tt": [ + "java.base/java.lang.Thread.getStackTrace(Thread.java:1602)", + "io.embrace.android.embracesdk.ThreadInfoTest.testThreadInfoSerialization(ThreadInfoTest.kt:18)" + ] +} diff --git a/embrace-android-sdk/src/test/resources/ui_config.json b/embrace-android-sdk/src/test/resources/ui_config.json new file mode 100644 index 0000000000..5c6aa8a3c6 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/ui_config.json @@ -0,0 +1,7 @@ +{ + "breadcrumbs": 80, + "taps": 50, + "views": 200, + "web_views": 500, + "fragments": 300 +} diff --git a/embrace-android-sdk/src/test/resources/user_info_expected.json b/embrace-android-sdk/src/test/resources/user_info_expected.json new file mode 100644 index 0000000000..bfe30bd8d7 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/user_info_expected.json @@ -0,0 +1,8 @@ +{ + "id": "123", + "em": "fake@example.com", + "un": "joebloggs", + "per": [ + "first_day" + ] +} diff --git a/embrace-android-sdk/src/test/resources/view_breadcrumb_expected.json b/embrace-android-sdk/src/test/resources/view_breadcrumb_expected.json new file mode 100644 index 0000000000..e11fa2647c --- /dev/null +++ b/embrace-android-sdk/src/test/resources/view_breadcrumb_expected.json @@ -0,0 +1,5 @@ +{ + "vn": "screen", + "st": 1600000000, + "en": 1700000000 +} \ No newline at end of file diff --git a/embrace-android-sdk/src/test/resources/view_config.json b/embrace-android-sdk/src/test/resources/view_config.json new file mode 100644 index 0000000000..335832df21 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/view_config.json @@ -0,0 +1,3 @@ +{ + "enable_automatic_activity_capture": false +} diff --git a/embrace-android-sdk/src/test/resources/web_view_config.json b/embrace-android-sdk/src/test/resources/web_view_config.json new file mode 100644 index 0000000000..ca02cfbb0e --- /dev/null +++ b/embrace-android-sdk/src/test/resources/web_view_config.json @@ -0,0 +1,4 @@ +{ + "capture_query_params": false, + "enable": false +} diff --git a/embrace-android-sdk/src/test/resources/webview_breadcrumb_expected.json b/embrace-android-sdk/src/test/resources/webview_breadcrumb_expected.json new file mode 100644 index 0000000000..1262780284 --- /dev/null +++ b/embrace-android-sdk/src/test/resources/webview_breadcrumb_expected.json @@ -0,0 +1,4 @@ +{ + "u": "url", + "st": 1600000000 +} \ No newline at end of file diff --git a/embrace-lint/.gitignore b/embrace-lint/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/embrace-lint/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/embrace-lint/README.md b/embrace-lint/README.md new file mode 100644 index 0000000000..a6140f07d9 --- /dev/null +++ b/embrace-lint/README.md @@ -0,0 +1,5 @@ +# Embrace Lint checks + +This module contains custom Lint checks that run as part of Embrace's local build process. +For further detail on how to write Lint checks, please see the +[Android Tools docs](https://googlesamples.github.io/android-custom-lint-rules/index.html). \ No newline at end of file diff --git a/embrace-lint/build.gradle b/embrace-lint/build.gradle new file mode 100644 index 0000000000..8663c9b5d2 --- /dev/null +++ b/embrace-lint/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "java-library" + id "kotlin" +} + +apply plugin: "com.android.lint" + +import io.embrace.gradle.Versions + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + compileOnly "com.android.tools.lint:lint-api:${Versions.lint}" + + testCompileOnly "com.android.tools.lint:lint-api:${Versions.lint}" + testImplementation "com.android.tools.lint:lint-tests:${Versions.lint}" + testImplementation "junit:junit:${Versions.junit}" +} diff --git a/embrace-lint/src/main/java/io/embrace/android/lint/EmbraceLintRegistry.kt b/embrace-lint/src/main/java/io/embrace/android/lint/EmbraceLintRegistry.kt new file mode 100644 index 0000000000..d737fd3164 --- /dev/null +++ b/embrace-lint/src/main/java/io/embrace/android/lint/EmbraceLintRegistry.kt @@ -0,0 +1,23 @@ +package io.embrace.android.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.android.tools.lint.detector.api.Issue + +/** + * Container for all the lint issues that this module should scan for. + */ +@Suppress("UnstableApiUsage") +class EmbraceLintRegistry : IssueRegistry() { + + override val issues: List = listOf(EmbracePublicApiPackageRule.ISSUE) + + override val api: Int = CURRENT_API + + override val vendor: Vendor = Vendor( + vendorName = "Embrace", + feedbackUrl = "https://embrace.io", + contact = "support@embrace.io" + ) +} diff --git a/embrace-lint/src/main/java/io/embrace/android/lint/EmbracePublicApiPackageRule.kt b/embrace-lint/src/main/java/io/embrace/android/lint/EmbracePublicApiPackageRule.kt new file mode 100644 index 0000000000..631ed9763e --- /dev/null +++ b/embrace-lint/src/main/java/io/embrace/android/lint/EmbracePublicApiPackageRule.kt @@ -0,0 +1,59 @@ +package io.embrace.android.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UClass + +/** + * Checks for classes in the io.embrace.android.embracesdk package and warns against + * adding new ones. + */ +@Suppress("UnstableApiUsage") +class EmbracePublicApiPackageRule : Detector(), Detector.UastScanner { + + override fun getApplicableUastTypes(): List> { + return listOf(UClass::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitClass(node: UClass) { + val packageName = context.uastFile?.packageName + if (packageName == RESTRICTED_PACKAGE_NAME) { + context.report( + issue = ISSUE, + location = context.getNameLocation(node), + message = ERR_MSG, + ) + } + } + } + } + + companion object { + const val RESTRICTED_PACKAGE_NAME = "io.embrace.android.embracesdk" + const val ERR_MSG = "Don't put classes in the $RESTRICTED_PACKAGE_NAME package unless " + + "they're part of the public API. Please move the new class to an appropriate " + + "package or (if you're adding to the public API) suppress this error " + + "via the lint baseline file." + + val ISSUE = Issue.create( + id = "EmbracePublicApiPackageRule", + briefDescription = "Use of default package", + explanation = ERR_MSG, + category = Category.CORRECTNESS, + priority = 5, + severity = Severity.ERROR, + implementation = Implementation( + EmbracePublicApiPackageRule::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/embrace-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/embrace-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry new file mode 100644 index 0000000000..7f9fd057d9 --- /dev/null +++ b/embrace-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry @@ -0,0 +1 @@ +io.embrace.android.lint.EmbraceLintRegistry \ No newline at end of file diff --git a/embrace-lint/src/test/kotlin/io/embrace/android/lint/EmbracePublicApiPackageRuleTest.kt b/embrace-lint/src/test/kotlin/io/embrace/android/lint/EmbracePublicApiPackageRuleTest.kt new file mode 100644 index 0000000000..a3f1bdc073 --- /dev/null +++ b/embrace-lint/src/test/kotlin/io/embrace/android/lint/EmbracePublicApiPackageRuleTest.kt @@ -0,0 +1,74 @@ +package io.embrace.android.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class EmbracePublicApiPackageRuleTest : LintDetectorTest() { + + override fun getDetector(): Detector = EmbracePublicApiPackageRule() + + override fun getIssues(): MutableList = mutableListOf(EmbracePublicApiPackageRule.ISSUE) + + @Test + fun testRestrictedPackageJava() { + lint().files( + java( + """ + package io.embrace.android.embracesdk; + + public class MyClass {} + """ + ) + ) + .run() + .expectErrorCount(1) + } + + @Test + fun testRestrictedPackageKotlin() { + lint().files( + kotlin( + """ + package io.embrace.android.embracesdk + + class MyClass + """ + ) + ) + .run() + .expectErrorCount(1) + } + + @Test + fun testAllowedPackageJava() { + lint().files( + java( + """ + package com.example.allowed; + + public class MyClass {} + """ + ) + ) + .run() + .expectClean() + } + + @Test + fun testAllowedPackageKotlin() { + lint().files( + kotlin( + """ + package com.example.allowed + + class MyClass + """ + ) + ) + .run() + .expectClean() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..b56bf24e82 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. + +# https://developer.android.com/build/optimize-your-build#increase-the-jvm-heap-size +org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +org.gradle.parallel=true +version = 6.0.0-SNAPSHOT +android.useAndroidX=true +# Enable adding baseline-prof.txt files to AAR artifacts. +android.experimental.enableArtProfiles=true + +# Disable buildconfig generation by default. This can be overridden on a per-module basis +android.defaults.buildfeatures.buildconfig = false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 GIT binary patch literal 54708 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2girk4u zvO<3q)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^ShTtO;VyD{dezY;XD@Rwl_9#j4Uo!1W&ZHVe0H>f=h#9k>~KUj^iUJ%@wU{Xuy z3FItk0<;}6D02$u(RtEY#O^hrB>qgxnOD^0AJPGC9*WXw_$k%1a%-`>uRIeeAIf3! zbx{GRnG4R$4)3rVmg63gW?4yIWW_>;t3>4@?3}&ct0Tk}<5ljU>jIN1 z&+mzA&1B6`v(}i#vAzvqWH~utZzQR;fCQGLuCN|p0hey7iCQ8^^dr*hi^wC$bTk`8M(JRKtQuXlSf$d(EISvuY0dM z7&ff;p-Ym}tT8^MF5ACG4sZmAV!l;0h&Mf#ZPd--_A$uv2@3H!y^^%_&Iw$*p79Uc5@ZXLGK;edg%)6QlvrN`U7H@e^P*0Atd zQB%>4--B1!9yeF(3vk;{>I8+2D;j`zdR8gd8dHuCQ_6|F(5-?gd&{YhLeyq_-V--4 z(SP#rP=-rsSHJSHDpT1{dMAb7-=9K1-@co_!$dG^?c(R-W&a_C5qy2~m3@%vBGhgnrw|H#g9ABb7k{NE?m4xD?;EV+fPdE>S2g$U(&_zGV+TPvaot>W_ zf8yY@)yP8k$y}UHVgF*uxtjW2zX4Hc3;W&?*}K&kqYpi%FHarfaC$ETHpSoP;A692 zR*LxY1^BO1ry@7Hc9p->hd==U@cuo*CiTnozxen;3Gct=?{5P94TgQ(UJoBb`7z@BqY z;q&?V2D1Y%n;^Dh0+eD)>9<}=A|F5{q#epBu#sf@lRs`oFEpkE%mrfwqJNFCpJC$| zy6#N;GF8XgqX(m2yMM2yq@TxStIR7whUIs2ar$t%Avh;nWLwElVBSI#j`l2$lb-!y zK|!?0hJ1T-wL{4uJhOFHp4?@28J^Oh61DbeTeSWub(|dL-KfxFCp0CjQjV`WaPW|U z=ev@VyC>IS@{ndzPy||b3z-bj5{Y53ff}|TW8&&*pu#?qs?)#&M`ACfb;%m+qX{Or zb+FNNHU}mz!@!EdrxmP_6eb3Cah!mL0ArL#EA1{nCY-!jL8zzz7wR6wAw(8K|IpW; zUvH*b1wbuRlwlUt;dQhx&pgsvJcUpm67rzkNc}2XbC6mZAgUn?VxO6YYg=M!#e=z8 zjX5ZLyMyz(VdPVyosL0}ULO!Mxu>hh`-MItnGeuQ;wGaU0)gIq3ZD=pDc(Qtk}APj z#HtA;?idVKNF)&0r|&w#l7DbX%b91b2;l2=L8q#}auVdk{RuYn3SMDo1%WW0tD*62 zaIj65Y38;?-~@b82AF!?Nra2;PU)t~qYUhl!GDK3*}%@~N0GQH7zflSpfP-ydOwNe zOK~w((+pCD&>f!b!On);5m+zUBFJtQ)mV^prS3?XgPybC2%2LiE5w+S4B|lP z+_>3$`g=%P{IrN|1Oxz30R{kI`}ZL!r|)RS@8Do;ZD3_=PbBrrP~S@EdsD{V+`!4v z{MSF}j!6odl33rA+$odIMaK%ersg%xMz>JQ^R+!qNq$5S{KgmGN#gAApX*3ib)TDsVVi>4ypIX|Ik4d6E}v z=8+hs9J=k3@Eiga^^O|ESMQB-O6i+BL*~*8coxjGs{tJ9wXjGZ^Vw@j93O<&+bzAH z9+N^ALvDCV<##cGoo5fX;wySGGmbH zHsslio)cxlud=iP2y=nM>v8vBn*hJ0KGyNOy7dr8yJKRh zywBOa4Lhh58y06`5>ESYXqLt8ZM1axd*UEp$wl`APU}C9m1H8-ModG!(wfSUQ%}rT3JD*ud~?WJdM}x>84)Cra!^J9wGs6^G^ze~eV(d&oAfm$ z_gwq4SHe=<#*FN}$5(0d_NumIZYaqs|MjFtI_rJb^+ZO?*XQ*47mzLNSL7~Nq+nw8 zuw0KwWITC43`Vx9eB!0Fx*CN9{ea$xjCvtjeyy>yf!ywxvv6<*h0UNXwkEyRxX{!e$TgHZ^db3r;1qhT)+yt@|_!@ zQG2aT`;lj>qjY`RGfQE?KTt2mn=HmSR>2!E38n8PlFs=1zsEM}AMICb z86Dbx(+`!hl$p=Z)*W~+?_HYp+CJacrCS-Fllz!7E>8*!E(yCh-cWbKc7)mPT6xu= zfKpF3I+p%yFXkMIq!ALiXF89-aV{I6v+^k#!_xwtQ*Nl#V|hKg=nP=fG}5VB8Ki7) z;19!on-iq&Xyo#AowvpA)RRgF?YBdDc$J8*)2Wko;Y?V6XMOCqT(4F#U2n1jg*4=< z8$MfDYL|z731iEKB3WW#kz|c3qh7AXjyZ}wtSg9xA(ou-pLoxF{4qk^KS?!d3J0!! zqE#R9NYGUyy>DEs%^xW;oQ5Cs@fomcrsN}rI2Hg^6y9kwLPF`K3llX00aM_r)c?ay zevlHA#N^8N+AI=)vx?4(=?j^ba^{umw140V#g58#vtnh8i7vRs*UD=lge;T+I zl1byCNr5H%DF58I2(rk%8hQ;zuCXs=sipbQy?Hd;umv4!fav@LE4JQ^>J{aZ=!@Gc~p$JudMy%0{=5QY~S8YVP zaP6gRqfZ0>q9nR3p+Wa8icNyl0Zn4k*bNto-(+o@-D8cd1Ed7`}dN3%wezkFxj_#_K zyV{msOOG;n+qbU=jBZk+&S$GEwJ99zSHGz8hF1`Xxa^&l8aaD8OtnIVsdF0cz=Y)? zP$MEdfKZ}_&#AC)R%E?G)tjrKsa-$KW_-$QL}x$@$NngmX2bHJQG~77D1J%3bGK!- zl!@kh5-uKc@U4I_Er;~epL!gej`kdX>tSXVFP-BH#D-%VJOCpM(-&pOY+b#}lOe)Z z0MP5>av1Sy-dfYFy%?`p`$P|`2yDFlv(8MEsa++Qv5M?7;%NFQK0E`Ggf3@2aUwtBpCoh`D}QLY%QAnJ z%qcf6!;cjOTYyg&2G27K(F8l^RgdV-V!~b$G%E=HP}M*Q*%xJV3}I8UYYd)>*nMvw zemWg`K6Rgy+m|y!8&*}=+`STm(dK-#b%)8nLsL&0<8Zd^|# z;I2gR&e1WUS#v!jX`+cuR;+yi(EiDcRCouW0AHNd?;5WVnC_Vg#4x56#0FOwTH6_p z#GILFF0>bb_tbmMM0|sd7r%l{U!fI0tGza&?65_D7+x9G zf3GA{c|mnO(|>}y(}%>|2>p0X8wRS&Eb0g)rcICIctfD_I9Wd+hKuEqv?gzEZBxG-rG~e!-2hqaR$Y$I@k{rLyCccE}3d)7Fn3EvfsEhA|bnJ374&pZDq&i zr(9#eq(g8^tG??ZzVk(#jU+-ce`|yiQ1dgrJ)$|wk?XLEqv&M+)I*OZ*oBCizjHuT zjZ|mW=<1u$wPhyo#&rIO;qH~pu4e3X;!%BRgmX%?&KZ6tNl386-l#a>ug5nHU2M~{fM2jvY*Py< zbR&^o&!T19G6V-pV@CB)YnEOfmrdPG%QByD?=if99ihLxP6iA8$??wUPWzptC{u5H z38Q|!=IW`)5Gef4+pz|9fIRXt>nlW)XQvUXBO8>)Q=$@gtwb1iEkU4EOWI4`I4DN5 zTC-Pk6N>2%7Hikg?`Poj5lkM0T_i zoCXfXB&}{TG%IB)ENSfI_Xg3=lxYc6-P059>oK;L+vGMy_h{y9soj#&^q5E!pl(Oq zl)oCBi56u;YHkD)d`!iOAhEJ0A^~T;uE9~Yp0{E%G~0q|9f34F!`P56-ZF{2hSaWj zio%9RR%oe~he22r@&j_d(y&nAUL*ayBY4#CWG&gZ8ybs#UcF?8K#HzziqOYM-<`C& z1gD?j)M0bp1w*U>X_b1@ag1Fx=d*wlr zEAcpmI#5LtqcX95LeS=LXlzh*l;^yPl_6MKk)zPuTz_p8ynQ5;oIOUAoPED=+M6Q( z8YR!DUm#$zTM9tbNhxZ4)J0L&Hpn%U>wj3z<=g;`&c_`fGufS!o|1%I_sA&;14bRC z3`BtzpAB-yl!%zM{Aiok8*X%lDNrPiAjBnzHbF0=Ua*3Lxl(zN3Thj2x6nWi^H7Jlwd2fxIvnI-SiC%*j z2~wIWWKT^5fYipo-#HSrr;(RkzzCSt?THVEH2EPvV-4c#Gu4&1X% z<1zTAM7ZM(LuD@ZPS?c30Ur`;2w;PXPVevxT)Ti25o}1JL>MN5i1^(aCF3 zbp>RI?X(CkR9*Hnv!({Ti@FBm;`Ip%e*D2tWEOc62@$n7+gWb;;j}@G()~V)>s}Bd zw+uTg^ibA(gsp*|&m7Vm=heuIF_pIukOedw2b_uO8hEbM4l=aq?E-7M_J`e(x9?{5 zpbgu7h}#>kDQAZL;Q2t?^pv}Y9Zlu=lO5e18twH&G&byq9XszEeXt$V93dQ@Fz2DV zs~zm*L0uB`+o&#{`uVYGXd?)Fv^*9mwLW4)IKoOJ&(8uljK?3J`mdlhJF1aK;#vlc zJdTJc2Q>N*@GfafVw45B03)Ty8qe>Ou*=f#C-!5uiyQ^|6@Dzp9^n-zidp*O`YuZ|GO28 zO0bqi;)fspT0dS2;PLm(&nLLV&&=Ingn(0~SB6Fr^AxPMO(r~y-q2>gRWv7{zYW6c zfiuqR)Xc41A7Eu{V7$-yxYT-opPtqQIJzMVkxU)cV~N0ygub%l9iHT3eQtB>nH0c` zFy}Iwd9vocxlm!P)eh0GwKMZ(fEk92teSi*fezYw3qRF_E-EcCh-&1T)?beW?9Q_+pde8&UW*(avPF4P}M#z*t~KlF~#5TT!&nu z>FAKF8vQl>Zm(G9UKi4kTqHj`Pf@Z@Q(bmZkseb1^;9k*`a9lKXceKX#dMd@ds`t| z2~UPsbn2R0D9Nm~G*oc@(%oYTD&yK)scA?36B7mndR9l*hNg!3?6>CR+tF1;6sr?V zzz8FBrZ@g4F_!O2igIGZcWd zRe_0*{d6cyy9QQ(|Ct~WTM1pC3({5qHahk*M*O}IPE6icikx48VZ?!0Oc^FVoq`}eu~ zpRq0MYHaBA-`b_BVID}|oo-bem76;B2zo7j7yz(9JiSY6JTjKz#+w{9mc{&#x}>E? zSS3mY$_|scfP3Mo_F5x;r>y&Mquy*Q1b3eF^*hg3tap~%?@ASeyodYa=dF&k=ZyWy z3C+&C95h|9TAVM~-8y(&xcy0nvl}6B*)j0FOlSz%+bK-}S4;F?P`j55*+ZO0Ogk7D z5q30zE@Nup4lqQoG`L%n{T?qn9&WC94%>J`KU{gHIq?n_L;75kkKyib;^?yXUx6BO zju%DyU(l!Vj(3stJ>!pMZ*NZFd60%oSAD1JUXG0~2GCXpB0Am(YPyhzQda-e)b^+f zzFaEZdVTJRJXPJo%w z$?T;xq^&(XjmO>0bNGsT|1{1UqGHHhasPC;H!oX52(AQ7h9*^npOIRdQbNrS0X5#5G?L4V}WsAYcpq-+JNXhSl)XbxZ)L@5Q+?wm{GAU z9a7X8hAjAo;4r_eOdZfXGL@YpmT|#qECEcPTQ;nsjIkQ;!0}g?T>Zr*Fg}%BZVA)4 zCAzvWr?M&)KEk`t9eyFi_GlPV9a2kj9G(JgiZadd_&Eb~#DyZ%2Zcvrda_A47G&uW z^6TnBK|th;wHSo8ivpScU?AM5HDu2+ayzExMJc@?4{h-c`!b($ExB`ro#vkl<;=BA z961c*n(4OR!ebT*7UV7sqL;rZ3+Z)BYs<1I|9F|TOKebtLPxahl|ZXxj4j!gjj!3*+iSb5Zni&EKVt$S{0?2>A}d@3PSF3LUu)5 z*Y#a1uD6Y!$=_ghsPrOqX!OcIP`IW};tZzx1)h_~mgl;0=n zdP|Te_7)~R?c9s>W(-d!@nzQyxqakrME{Tn@>0G)kqV<4;{Q?Z-M)E-|IFLTc}WQr z1Qt;u@_dN2kru_9HMtz8MQx1aDYINH&3<+|HA$D#sl3HZ&YsjfQBv~S>4=u z7gA2*X6_cI$2}JYLIq`4NeXTz6Q3zyE717#>RD&M?0Eb|KIyF;xj;+3#DhC-xOj~! z$-Kx#pQ)_$eHE3Zg?V>1z^A%3jW0JBnd@z`kt$p@lch?A9{j6hXxt$(3|b>SZiBxOjA%LsIPii{=o(B`yRJ>OK;z_ELTi8xHX)il z--qJ~RWsZ%9KCNuRNUypn~<2+mQ=O)kd59$Lul?1ev3c&Lq5=M#I{ zJby%%+Top_ocqv!jG6O6;r0Xwb%vL6SP{O(hUf@8riADSI<|y#g`D)`x^vHR4!&HY`#TQMqM`Su}2(C|KOmG`wyK>uh@3;(prdL{2^7T3XFGznp{-sNLLJH@mh* z^vIyicj9yH9(>~I-Ev7p=yndfh}l!;3Q65}K}()(jp|tC;{|Ln1a+2kbctWEX&>Vr zXp5=#pw)@-O6~Q|><8rd0>H-}0Nsc|J6TgCum{XnH2@hFB09FsoZ_ow^Nv@uGgz3# z<6dRDt1>>-!kN58&K1HFrgjTZ^q<>hNI#n8=hP&pKAL4uDcw*J66((I?!pE0fvY6N zu^N=X8lS}(=w$O_jlE(;M9F={-;4R(K5qa=P#ZVW>}J&s$d0?JG8DZJwZcx3{CjLg zJA>q-&=Ekous)vT9J>fbnZYNUtvox|!Rl@e^a6ue_4-_v=(sNB^I1EPtHCFEs!>kK6B@-MS!(B zST${=v9q6q8YdSwk4}@c6cm$`qZ86ipntH8G~51qIlsYQ)+2_Fg1@Y-ztI#aa~tFD_QUxb zU-?g5B}wU@`tnc_l+B^mRogRghXs!7JZS=A;In1|f(1T(+xfIi zvjccLF$`Pkv2w|c5BkSj>>k%`4o6#?ygojkV78%zzz`QFE6nh{(SSJ9NzVdq>^N>X zpg6+8u7i(S>c*i*cO}poo7c9%i^1o&3HmjY!s8Y$5aO(!>u1>-eai0;rK8hVzIh8b zL53WCXO3;=F4_%CxMKRN^;ggC$;YGFTtHtLmX%@MuMxvgn>396~ zEp>V(dbfYjBX^!8CSg>P2c5I~HItbe(dl^Ax#_ldvCh;D+g6-%WD|$@S6}Fvv*eHc zaKxji+OG|_KyMe2D*fhP<3VP0J1gTgs6JZjE{gZ{SO-ryEhh;W237Q0 z{yrDobsM6S`bPMUzr|lT|99m6XDI$RzW4tQ$|@C2RjhBYPliEXFV#M*5G4;Kb|J8E z0IH}-d^S-53kFRZ)ZFrd2%~Sth-6BN?hnMa_PC4gdWyW3q-xFw&L^x>j<^^S$y_3_ zdZxouw%6;^mg#jG@7L!g9Kdw}{w^X9>TOtHgxLLIbfEG^Qf;tD=AXozE6I`XmOF=# zGt$Wl+7L<8^VI-eSK%F%dqXieK^b!Z3yEA$KL}X@>fD9)g@=DGt|=d(9W%8@Y@!{PI@`Nd zyF?Us(0z{*u6|X?D`kKSa}}Q*HP%9BtDEA^buTlI5ihwe)CR%OR46b+>NakH3SDbZmB2X>c8na&$lk zYg$SzY+EXtq2~$Ep_x<~+YVl<-F&_fbayzTnf<7?Y-un3#+T~ahT+eW!l83sofNt; zZY`eKrGqOux)+RMLgGgsJdcA3I$!#zy!f<$zL0udm*?M5w=h$Boj*RUk8mDPVUC1RC8A`@7PgoBIU+xjB7 z25vky+^7k_|1n1&jKNZkBWUu1VCmS}a|6_+*;fdUZAaIR4G!wv=bAZEXBhcjch6WH zdKUr&>z^P%_LIx*M&x{!w|gij?nigT8)Ol3VicXRL0tU}{vp2fi!;QkVc#I38op3O z=q#WtNdN{x)OzmH;)j{cor)DQ;2%m>xMu_KmTisaeCC@~rQwQTfMml7FZ_ zU2AR8yCY_CT$&IAn3n#Acf*VKzJD8-aphMg(12O9cv^AvLQ9>;f!4mjyxq_a%YH2+{~=3TMNE1 z#r3@ynnZ#p?RCkPK36?o{ILiHq^N5`si(T_cKvO9r3^4pKG0AgDEB@_72(2rvU^-; z%&@st2+HjP%H)u50t81p>(McL{`dTq6u-{JM|d=G1&h-mtjc2{W0%*xuZVlJpUSP-1=U6@5Q#g(|nTVN0icr-sdD~DWR=s}`$#=Wa zt5?|$`5`=TWZevaY9J9fV#Wh~Fw@G~0vP?V#Pd=|nMpSmA>bs`j2e{)(827mU7rxM zJ@ku%Xqhq!H)It~yXm=)6XaPk=$Rpk*4i4*aSBZe+h*M%w6?3&0>>|>GHL>^e4zR!o%aGzUn40SR+TdN%=Dbn zsRfXzGcH#vjc-}7v6yRhl{V5PhE-r~)dnmNz=sDt?*1knNZ>xI5&vBwrosF#qRL-Y z;{W)4W&cO0XMKy?{^d`Xh(2B?j0ioji~G~p5NQJyD6vouyoFE9w@_R#SGZ1DR4GnN z{b=sJ^8>2mq3W;*u2HeCaKiCzK+yD!^i6QhTU5npwO+C~A#5spF?;iuOE>o&p3m1C zmT$_fH8v+5u^~q^ic#pQN_VYvU>6iv$tqx#Sulc%|S7f zshYrWq7IXCiGd~J(^5B1nGMV$)lo6FCTm1LshfcOrGc?HW7g>pV%#4lFbnt#94&Rg{%Zbg;Rh?deMeOP(du*)HryI zCdhO$3|SeaWK<>(jSi%qst${Z(q@{cYz7NA^QO}eZ$K@%YQ^Dt4CXzmvx~lLG{ef8 zyckIVSufk>9^e_O7*w2z>Q$8me4T~NQDq=&F}Ogo#v1u$0xJV~>YS%mLVYqEf~g*j zGkY#anOI9{(f4^v21OvYG<(u}UM!-k;ziH%GOVU1`$0VuO@Uw2N{$7&5MYjTE?Er) zr?oZAc~Xc==KZx-pmoh9KiF_JKU7u0#b_}!dWgC>^fmbVOjuiP2FMq5OD9+4TKg^2 z>y6s|sQhI`=fC<>BnQYV433-b+jBi+N6unz%6EQR%{8L#=4sktI>*3KhX+qAS>+K#}y5KnJ8YuOuzG(Ea5;$*1P$-9Z+V4guyJ#s) zRPH(JPN;Es;H72%c8}(U)CEN}Xm>HMn{n!d(=r*YP0qo*^APwwU5YTTeHKy#85Xj< zEboiH=$~uIVMPg!qbx~0S=g&LZ*IyTJG$hTN zv%2>XF``@S9lnLPC?|myt#P)%7?%e_j*aU4TbTyxO|3!h%=Udp;THL+^oPp<6;TLlIOa$&xeTG_a*dbRDy+(&n1T=MU z+|G5{2UprrhN^AqODLo$9Z2h(3^wtdVIoSk@}wPajVgIoZipRft}^L)2Y@mu;X-F{LUw|s7AQD-0!otW#W9M@A~08`o%W;Bq-SOQavG*e-sy8) zwtaucR0+64B&Pm++-m56MQ$@+t{_)7l-|`1kT~1s!swfc4D9chbawUt`RUOdoxU|j z$NE$4{Ysr@2Qu|K8pD37Yv&}>{_I5N49a@0<@rGHEs}t zwh_+9T0oh@ptMbjy*kbz<&3>LGR-GNsT8{x1g{!S&V7{5tPYX(GF>6qZh>O&F)%_I zkPE-pYo3dayjNQAG+xrI&yMZy590FA1unQ*k*Zfm#f9Z5GljOHBj-B83KNIP1a?<^1vOhDJkma0o- zs(TP=@e&s6fRrU(R}{7eHL*(AElZ&80>9;wqj{|1YQG=o2Le-m!UzUd?Xrn&qd8SJ0mmEYtW;t(;ncW_j6 zGWh4y|KMK^s+=p#%fWxjXo434N`MY<8W`tNH-aM6x{@o?D3GZM&+6t4V3I*3fZd{a z0&D}DI?AQl{W*?|*%M^D5{E>V%;=-r&uQ>*e)cqVY52|F{ptA*`!iS=VKS6y4iRP6 zKUA!qpElT5vZvN}U5k-IpeNOr6KF`-)lN1r^c@HnT#RlZbi(;yuvm9t-Noh5AfRxL@j5dU-X37(?S)hZhRDbf5cbhDO5nSX@WtApyp` zT$5IZ*4*)h8wShkPI45stQH2Y7yD*CX^Dh@B%1MJSEn@++D$AV^ttKXZdQMU`rxiR z+M#45Z2+{N#uR-hhS&HAMFK@lYBWOzU^Xs-BlqQDyN4HwRtP2$kks@UhAr@wlJii%Rq?qy25?Egs z*a&iAr^rbJWlv+pYAVUq9lor}#Cm|D$_ev2d2Ko}`8kuP(ljz$nv3OCDc7zQp|j6W zbS6949zRvj`bhbO(LN3}Pq=$Ld3a_*9r_24u_n)1)}-gRq?I6pdHPYHgIsn$#XQi~ z%&m_&nnO9BKy;G%e~fa7i9WH#MEDNQ8WCXhqqI+oeE5R7hLZT_?7RWVzEGZNz4*Po ze&*a<^Q*ze72}UM&$c%FuuEIN?EQ@mnILwyt;%wV-MV+|d%>=;3f0(P46;Hwo|Wr0 z>&FS9CCb{?+lDpJMs`95)C$oOQ}BSQEv0Dor%-Qj0@kqlIAm1-qSY3FCO2j$br7_w zlpRfAWz3>Gh~5`Uh?ER?@?r0cXjD0WnTx6^AOFii;oqM?|M9QjHd*GK3WwA}``?dK15`ZvG>_nB2pSTGc{n2hYT6QF^+&;(0c`{)*u*X7L_ zaxqyvVm$^VX!0YdpSNS~reC+(uRqF2o>jqIJQkC&X>r8|mBHvLaduM^Mh|OI60<;G zDHx@&jUfV>cYj5+fAqvv(XSmc(nd@WhIDvpj~C#jhZ6@M3cWF2HywB1yJv2#=qoY| zIiaxLsSQa7w;4YE?7y&U&e6Yp+2m(sb5q4AZkKtey{904rT08pJpanm->Z75IdvW^ z!kVBy|CIUZn)G}92_MgoLgHa?LZJDp_JTbAEq8>6a2&uKPF&G!;?xQ*+{TmNB1H)_ z-~m@CTxDry_-rOM2xwJg{fcZ41YQDh{DeI$4!m8c;6XtFkFyf`fOsREJ`q+Bf4nS~ zKDYs4AE7Gugv?X)tu4<-M8ag{`4pfQ14z<(8MYQ4u*fl*DCpq66+Q1-gxNCQ!c$me zyTrmi7{W-MGP!&S-_qJ%9+e08_9`wWGG{i5yLJ;8qbt-n_0*Q371<^u@tdz|;>fPW zE=&q~;wVD_4IQ^^jyYX;2shIMiYdvIpIYRT>&I@^{kL9Ka2ECG>^l>Ae!GTn{r~o= z|I9=J#wNe)zYRqGZ7Q->L{dfewyC$ZYcLaoNormZ3*gfM=da*{heC)&46{yTS!t10 zn_o0qUbQOs$>YuY>YHi|NG^NQG<_@jD&WnZcW^NTC#mhVE7rXlZ=2>mZkx{bc=~+2 z{zVH=Xs0`*K9QAgq9cOtfQ^BHh-yr=qX8hmW*0~uCup89IJMvWy%#yt_nz@6dTS)L{O3vXye< zW4zUNb6d|Tx`XIVwMMgqnyk?c;Kv`#%F0m^<$9X!@}rI##T{iXFC?(ui{;>_9Din8 z7;(754q!Jx(~sb!6+6Lf*l{fqD7GW*v{>3wp+)@wq2abADBK!kI8To}7zooF%}g-z zJ1-1lp-lQI6w^bov9EfhpxRI}`$PTpJI3uo@ZAV729JJ2Hs68{r$C0U=!d$Bm+s(p z8Kgc(Ixf4KrN%_jjJjTx5`&`Ak*Il%!}D_V)GM1WF!k$rDJ-SudXd_Xhl#NWnET&e-P!rH~*nNZTzxj$?^oo3VWc-Ay^`Phze3(Ft!aNW-f_ zeMy&BfNCP^-FvFzR&rh!w(pP5;z1$MsY9Voozmpa&A}>|a{eu}>^2s)So>&kmi#7$ zJS_-DVT3Yi(z+ruKbffNu`c}s`Uo`ORtNpUHa6Q&@a%I%I;lm@ea+IbCLK)IQ~)JY zp`kdQ>R#J*i&Ljer3uz$m2&Un9?W=Ue|hHv?xlM`I&*-M;2{@so--0OAiraN1TLra z>EYQu#)Q@UszfJj&?kr%RraFyi*eG+HD_(!AWB;hPgB5Gd-#VDRxxv*VWMY0hI|t- zR=;TL%EKEg*oet7GtmkM zgH^y*1bfJ*af(_*S1^PWqBVVbejFU&#m`_69IwO!aRW>Rcp~+7w^ptyu>}WFYUf;) zZrgs;EIN9$Immu`$umY%$I)5INSb}aV-GDmPp!d_g_>Ar(^GcOY%2M)Vd7gY9llJR zLGm*MY+qLzQ+(Whs8-=ty2l)G9#82H*7!eo|B6B$q%ak6eCN%j?{SI9|K$u3)ORoz zw{bAGaWHrMb|X^!UL~_J{jO?l^}lI^|7jIn^p{n%JUq9{tC|{GM5Az3SrrPkuCt_W zq#u0JfDw{`wAq`tAJmq~sz`D_P-8qr>kmms>I|);7Tn zLl^n*Ga7l=U)bQmgnSo5r_&#Pc=eXm~W75X9Cyy0WDO|fbSn5 zLgpFAF4fa90T-KyR4%%iOq6$6BNs@3ZV<~B;7V=u zdlB8$lpe`w-LoS;0NXFFu@;^^bc?t@r3^XTe*+0;o2dt&>eMQeDit(SfDxYxuA$uS z**)HYK7j!vJVRNfrcokVc@&(ke5kJzvi};Lyl7@$!`~HM$T!`O`~MQ1k~ZH??fQr zNP)33uBWYnTntKRUT*5lu&8*{fv>syNgxVzEa=qcKQ86Vem%Lpae2LM=TvcJLs?`=o9%5Mh#k*_7zQD|U7;A%=xo^_4+nX{~b1NJ6@ z*=55;+!BIj1nI+)TA$fv-OvydVQB=KK zrGWLUS_Chm$&yoljugU=PLudtJ2+tM(xj|E>Nk?c{-RD$sGYNyE|i%yw>9gPItE{ zD|BS=M>V^#m8r?-3swQofD8j$h-xkg=F+KM%IvcnIvc)y zl?R%u48Jeq7E*26fqtLe_b=9NC_z|axW#$e0adI#r(Zsui)txQ&!}`;;Z%q?y2Kn! zXzFNe+g7+>>`9S0K1rmd)B_QVMD?syc3e0)X*y6(RYH#AEM9u?V^E0GHlAAR)E^4- zjKD+0K=JKtf5DxqXSQ!j?#2^ZcQoG5^^T+JaJa3GdFeqIkm&)dj76WaqGukR-*&`13ls8lU2ayVIR%;79HYAr5aEhtYa&0}l}eAw~qKjUyz4v*At z?})QplY`3cWB6rl7MI5mZx&#%I0^iJm3;+J9?RA(!JXjl?(XgmA-D#2cY-^?g1c*Q z3GVLh!8Jhe;QqecbMK#XIJxKMb=6dcs?1vbb?@ov-raj`hnYO92y8pv@>RVr=9Y-F zv`BK)9R6!m4Pfllu4uy0WBL+ZaUFFzbZZtI@J8{OoQ^wL-b$!FpGT)jYS-=vf~b-@ zIiWs7j~U2yI=G5;okQz%gh6}tckV5wN;QDbnu|5%%I(#)8Q#)wTq8YYt$#f9=id;D zJbC=CaLUyDIPNOiDcV9+=|$LE9v2;Qz;?L+lG{|g&iW9TI1k2_H;WmGH6L4tN1WL+ zYfSVWq(Z_~u~U=g!RkS|YYlWpKfZV!X%(^I3gpV%HZ_{QglPSy0q8V+WCC2opX&d@eG2BB#(5*H!JlUzl$DayI5_J-n zF@q*Fc-nlp%Yt;$A$i4CJ_N8vyM5fNN`N(CN53^f?rtya=p^MJem>JF2BEG|lW|E) zxf)|L|H3Oh7mo=9?P|Y~|6K`B3>T)Gw`0ESP9R`yKv}g|+qux(nPnU(kQ&&x_JcYg9+6`=; z-EI_wS~l{T3K~8}8K>%Ke`PY!kNt415_x?^3QOvX(QUpW&$LXKdeZM-pCI#%EZ@ta zv(q-(xXIwvV-6~(Jic?8<7ain4itN>7#AqKsR2y(MHMPeL)+f+v9o8Nu~p4ve*!d3 z{Lg*NRTZsi;!{QJknvtI&QtQM_9Cu%1QcD0f!Fz+UH4O#8=hvzS+^(e{iG|Kt7C#u zKYk7{LFc+9Il>d6)blAY-9nMd(Ff0;AKUo3B0_^J&ESV@4UP8PO0no7G6Gp_;Z;YnzW4T-mCE6ZfBy(Y zXOq^Of&?3#Ra?khzc7IJT3!%IKK8P(N$ST47Mr=Gv@4c!>?dQ-&uZihAL1R<_(#T8Y`Ih~soL6fi_hQmI%IJ5qN995<{<@_ z;^N8AGQE+?7#W~6X>p|t<4@aYC$-9R^}&&pLo+%Ykeo46-*Yc(%9>X>eZpb8(_p{6 zwZzYvbi%^F@)-}5%d_z^;sRDhjqIRVL3U3yK0{Q|6z!PxGp?|>!%i(!aQODnKUHsk^tpeB<0Qt7`ZBlzRIxZMWR+|+ z3A}zyRZ%0Ck~SNNov~mN{#niO**=qc(faGz`qM16H+s;Uf`OD1{?LlH!K!+&5xO%6 z5J80-41C{6)j8`nFvDaeSaCu_f`lB z_Y+|LdJX=YYhYP32M556^^Z9MU}ybL6NL15ZTV?kfCFfpt*Pw5FpHp#2|ccrz#zoO zhs=+jQI4fk*H0CpG?{fpaSCmXzU8bB`;kCLB8T{_3t>H&DWj0q0b9B+f$WG=e*89l zzUE)b9a#aWsEpgnJqjVQETpp~R7gn)CZd$1B8=F*tl+(iPH@s9jQtE33$dBDOOr=% ziOpR8R|1eLI?Rn*d+^;_U#d%bi$|#obe0(-HdB;K>=Y=mg{~jTA_WpChe8QquhF`N z>hJ}uV+pH`l_@d>%^KQNm*$QNJ(lufH>zv9M`f+C-y*;hAH(=h;kp@eL=qPBeXrAo zE7my75EYlFB30h9sdt*Poc9)2sNP9@K&4O7QVPQ^m$e>lqzz)IFJWpYrpJs)Fcq|P z5^(gnntu!+oujqGpqgY_o0V&HL72uOF#13i+ngg*YvPcqpk)Hoecl$dx>C4JE4DWp z-V%>N7P-}xWv%9Z73nn|6~^?w$5`V^xSQbZceV<_UMM&ijOoe{Y^<@3mLSq_alz8t zr>hXX;zTs&k*igKAen1t1{pj94zFB;AcqFwV)j#Q#Y8>hYF_&AZ?*ar1u%((E2EfZ zcRsy@s%C0({v=?8oP=DML`QsPgzw3|9|C22Y>;=|=LHSm7~+wQyI|;^WLG0_NSfrf zamq!5%EzdQ&6|aTP2>X=Z^Jl=w6VHEZ@=}n+@yeu^ke2Yurrkg9up3g$0SI8_O-WQu$bCsKc(juv|H;vz6}%7ONww zKF%!83W6zO%0X(1c#BM}2l^ddrAu^*`9g&1>P6m%x{gYRB)}U`40r>6YmWSH(|6Ic zH~QNgxlH*;4jHg;tJiKia;`$n_F9L~M{GiYW*sPmMq(s^OPOKm^sYbBK(BB9dOY`0 z{0!=03qe*Sf`rcp5Co=~pfQyqx|umPHj?a6;PUnO>EZGb!pE(YJgNr{j;s2+nNV(K zDi#@IJ|To~Zw)vqGnFwb2}7a2j%YNYxe2qxLk)VWJIux$BC^oII=xv-_}h@)Vkrg1kpKokCmX({u=lSR|u znu_fA0PhezjAW{#Gu0Mdhe8F4`!0K|lEy+<1v;$ijSP~A9w%q5-4Ft|(l7UqdtKao zs|6~~nmNYS>fc?Nc=yzcvWNp~B0sB5ForO5SsN(z=0uXxl&DQsg|Y?(zS)T|X``&8 z*|^p?~S!vk8 zg>$B{oW}%rYkgXepmz;iqCKY{R@%@1rcjuCt}%Mia@d8Vz5D@LOSCbM{%JU#cmIp! z^{4a<3m%-p@JZ~qg)Szb-S)k{jv92lqB(C&KL(jr?+#ES5=pUH$(;CO9#RvDdErmW z3(|f{_)dcmF-p*D%qUa^yYngNP&Dh2gq5hr4J!B5IrJ?ODsw@*!0p6Fm|(ebRT%l) z#)l22@;4b9RDHl1ys$M2qFc;4BCG-lp2CN?Ob~Be^2wQJ+#Yz}LP#8fmtR%o7DYzoo1%4g4D+=HonK7b!3nvL0f1=oQp93dPMTsrjZRI)HX-T}ApZ%B#B;`s? z9Kng{|G?yw7rxo(T<* z1+O`)GNRmXq3uc(4SLX?fPG{w*}xDCn=iYo2+;5~vhWUV#e5e=Yfn4BoS@3SrrvV9 zrM-dPU;%~+3&>(f3sr$Rcf4>@nUGG*vZ~qnxJznDz0irB(wcgtyATPd&gSuX^QK@+ z)7MGgxj!RZkRnMSS&ypR94FC$;_>?8*{Q110XDZ)L);&SA8n>72s1#?6gL>gydPs` zM4;ert4-PBGB@5E` zBaWT=CJUEYV^kV%@M#3(E8>g8Eg|PXg`D`;K8(u{?}W`23?JgtNcXkUxrH}@H_4qN zw_Pr@g%;CKkgP(`CG6VTIS4ZZ`C22{LO{tGi6+uPvvHkBFK|S6WO{zo1MeK$P zUBe}-)3d{55lM}mDVoU@oGtPQ+a<=wwDol}o=o1z*)-~N!6t09du$t~%MlhM9B5~r zy|zs^LmEF#yWpXZq!+Nt{M;bE%Q8z7L8QJDLie^5MKW|I1jo}p)YW(S#oLf(sWn~* zII>pocNM5#Z+-n2|495>?H?*oyr0!SJIl(}q-?r`Q;Jbqqr4*_G8I7agO298VUr9x z8ZcHdCMSK)ZO@Yr@c0P3{`#GVVdZ{zZ$WTO zuvO4ukug&& ze#AopTVY3$B>c3p8z^Yyo8eJ+(@FqyDWlR;uxy0JnSe`gevLF`+ZN6OltYr>oN(ZV z>76nIiVoll$rDNkck6_eh%po^u16tD)JXcii|#Nn(7=R9mA45jz>v}S%DeMc(%1h> zoT2BlF9OQ080gInWJ3)bO9j$ z`h6OqF0NL4D3Kz?PkE8nh;oxWqz?<3_!TlN_%qy*T7soZ>Pqik?hWWuya>T$55#G9 zxJv=G&=Tm4!|p1#!!hsf*uQe}zWTKJg`hkuj?ADST2MX6fl_HIDL7w`5Dw1Btays1 zz*aRwd&>4*H%Ji2bt-IQE$>sbCcI1Poble0wL`LAhedGRZp>%>X6J?>2F*j>`BX|P zMiO%!VFtr_OV!eodgp-WgcA-S=kMQ^zihVAZc!vdx*YikuDyZdHlpy@Y3i!r%JI85$-udM6|7*?VnJ!R)3Qfm4mMm~Z#cvNrGUy|i0u zb|(7WsYawjBK0u1>@lLhMn}@X>gyDlx|SMXQo|yzkg-!wIcqfGrA!|t<3NC2k` zq;po50dzvvHD>_mG~>W0iecTf@3-)<$PM5W@^yMcu@U;)(^eu@e4jAX7~6@XrSbIE zVG6v2miWY^g8bu5YH$c2QDdLkg2pU8xHnh`EUNT+g->Q8Tp4arax&1$?CH($1W&*} zW&)FQ>k5aCim$`Ph<9Zt?=%|pz&EX@_@$;3lQT~+;EoD(ho|^nSZDh*M0Z&&@9T+e zHYJ;xB*~UcF^*7a_T)9iV5}VTYKda8n*~PSy@>h7c(mH~2AH@qz{LMQCb+-enMhX} z2k0B1JQ+6`?Q3Lx&(*CBQOnLBcq;%&Nf<*$CX2<`8MS9c5zA!QEbUz1;|(Ua%CiuL zF2TZ>@t7NKQ->O#!;0s;`tf$veXYgq^SgG>2iU9tCm5&^&B_aXA{+fqKVQ*S9=58y zddWqy1lc$Y@VdB?E~_B5w#so`r552qhPR649;@bf63_V@wgb!>=ij=%ptnsq&zl8^ zQ|U^aWCRR3TnoKxj0m0QL2QHM%_LNJ(%x6aK?IGlO=TUoS%7YRcY{!j(oPcUq{HP=eR1>0o^(KFl-}WdxGRjsT);K8sGCkK0qVe{xI`# z@f+_kTYmLbOTxRv@wm2TNBKrl+&B>=VaZbc(H`WWLQhT=5rPtHf)#B$Q6m1f8We^)f6ylbO=t?6Y;{?&VL|j$VXyGV!v8eceRk zl>yOWPbk%^wv1t63Zd8X^Ck#12$*|yv`v{OA@2;-5Mj5sk#ptfzeX(PrCaFgn{3*hau`-a+nZhuJxO;Tis51VVeKAwFML#hF9g26NjfzLs8~RiM_MFl1mgDOU z=ywk!Qocatj1Q1yPNB|FW>!dwh=aJxgb~P%%7(Uydq&aSyi?&b@QCBiA8aP%!nY@c z&R|AF@8}p7o`&~>xq9C&X6%!FAsK8gGhnZ$TY06$7_s%r*o;3Y7?CenJUXo#V-Oag z)T$d-V-_O;H)VzTM&v8^Uk7hmR8v0)fMquWHs6?jXYl^pdM#dY?T5XpX z*J&pnyJ<^n-d<0@wm|)2SW9e73u8IvTbRx?Gqfy_$*LI_Ir9NZt#(2T+?^AorOv$j zcsk+t<#!Z!eC|>!x&#l%**sSAX~vFU0|S<;-ei}&j}BQ#ekRB-;c9~vPDIdL5r{~O zMiO3g0&m-O^gB}<$S#lCRxX@c3g}Yv*l)Hh+S^my28*fGImrl<-nbEpOw-BZ;WTHL zgHoq&ftG|~ouV<>grxRO6Z%{!O+j`Cw_4~BIzrjpkdA5jH40{1kDy|pEq#7`$^m*? zX@HxvW`e}$O$mJvm+65Oc4j7W@iVe)rF&-}R>KKz>rF&*Qi3%F0*tz!vNtl@m8L9= zyW3%|X}0KsW&!W<@tRNM-R>~~QHz?__kgnA(G`jWOMiEaFjLzCdRrqzKlP1vYLG`Y zh6_knD3=9$weMn4tBD|5=3a9{sOowXHu(z5y^RYrxJK z|L>TUvbDuO?3=YJ55N5}Kj0lC(PI*Te0>%eLNWLnawD54geX5>8AT(oT6dmAacj>o zC`Bgj-RV0m3Dl2N=w3e0>wWWG5!mcal`Xu<(1=2$b{k(;kC(2~+B}a(w;xaHPk^@V zGzDR|pt%?(1xwNxV!O6`JLCM!MnvpbLoHzKziegT_2LLWAi4}UHIo6uegj#WTQLet z9Dbjyr{8NAk+$(YCw~_@Az9N|iqsliRYtR7Q|#ONIV|BZ7VKcW$phH9`ZAlnMTW&9 zIBqXYuv*YY?g*cJRb(bXG}ts-t0*|HXId4fpnI>$9A?+BTy*FG8f8iRRKYRd*VF_$ zoo$qc+A(d#Lx0@`ck>tt5c$L1y7MWohMnZd$HX++I9sHoj5VXZRZkrq`v@t?dfvC} z>0h!c4HSb8%DyeF#zeU@rJL2uhZ^8dt(s+7FNHJeY!TZJtyViS>a$~XoPOhHsdRH* zwW+S*rIgW0qSPzE6w`P$Jv^5dsyT6zoby;@z=^yWLG^x;e557RnndY>ph!qCF;ov$ ztSW1h3@x{zm*IMRx|3lRWeI3znjpbS-0*IL4LwwkWyPF1CRpQK|s42dJ{ddA#BDDqio-Y+mF-XcP-z4bi zAhfXa2=>F0*b;F0ftEPm&O+exD~=W^qjtv&>|%(4q#H=wbA>7QorDK4X3~bqeeXv3 zV1Q<>_Fyo!$)fD`fd@(7(%6o-^x?&+s=)jjbQ2^XpgyYq6`}ISX#B?{I$a&cRcW?X zhx(i&HWq{=8pxlA2w~7521v-~lu1M>4wL~hDA-j(F2;9ICMg+6;Zx2G)ulp7j;^O_ zQJIRUWQam(*@?bYiRTKR<;l_Is^*frjr-Dj3(fuZtK{Sn8F;d*t*t{|_lnlJ#e=hx zT9?&_n?__2mN5CRQ}B1*w-2Ix_=CF@SdX-cPjdJN+u4d-N4ir*AJn&S(jCpTxiAms zzI5v(&#_#YrKR?B?d~ge1j*g<2yI1kp`Lx>8Qb;aq1$HOX4cpuN{2ti!2dXF#`AG{ zp<iD=Z#qN-yEwLwE7%8w8&LB<&6{WO$#MB-|?aEc@S1a zt%_p3OA|kE&Hs47Y8`bdbt_ua{-L??&}uW zmwE7X4Y%A2wp-WFYPP_F5uw^?&f zH%NCcbw_LKx!c!bMyOBrHDK1Wzzc5n7A7C)QrTj_Go#Kz7%+y^nONjnnM1o5Sw(0n zxU&@41(?-faq?qC^kO&H301%|F9U-Qm(EGd3}MYTFdO+SY8%fCMTPMU3}bY7ML1e8 zrdOF?E~1uT)v?UX(XUlEIUg3*UzuT^g@QAxEkMb#N#q0*;r zF6ACHP{ML*{Q{M;+^4I#5bh#c)xDGaIqWc#ka=0fh*_Hlu%wt1rBv$B z%80@8%MhIwa0Zw$1`D;Uj1Bq`lsdI^g_18yZ9XUz2-u6&{?Syd zHGEh-3~HH-vO<)_2^r|&$(q7wG{@Q~un=3)Nm``&2T99L(P+|aFtu1sTy+|gwL*{z z)WoC4rsxoWhz0H$rG|EwhDT z0zcOAod_k_Ql&Y`YV!#&Mjq{2ln|;LMuF$-G#jX_2~oNioTHb4GqFatn@?_KgsA7T z(ouy$cGKa!m}6$=C1Wmb;*O2p*@g?wi-}X`v|QA4bNDU*4(y8*jZy-Ku)S3iBN(0r ztfLyPLfEPqj6EV}xope=?b0Nyf*~vDz-H-Te@B`{ib?~F<*(MmG+8zoYS77$O*3vayg#1kkKN+Bu9J9;Soev<%2S&J zr8*_PKV4|?RVfb#SfNQ;TZC$8*9~@GR%xFl1 z3MD?%`1PxxupvVO>2w#8*zV<-!m&Lis&B>)pHahPQ@I_;rY~Z$1+!4V1jde&L8y0! zha7@F+rOENF{~0$+a~oId0R|_!PhO=8)$>LcO)ca6YeOQs?ZG;`4O`x=Pd??Bl?Qf zgkaNj7X5@3_==zlQ-u6?omteA!_e-6gfDtw6CBnP2o1wo-7U!Y@89rU1HFb|bIr!I z=qIz=AW(}L^m z=I9RiS{DRtTYS6jsnvt1zs)W;kSVFOK|WMyZ@dxs+8{*W9-aTmS79J4R{Cis>EIqS zw+~gJqwz)(!z>)KDyhS{lM*xQ-8mNvo$A=IwGu+iS564tgX`|MeEuis!aN-=7!L&e zhNs;g1MBqDyx{y@AI&{_)+-?EEg|5C*!=OgD#$>HklRVU+R``HYZZq5{F9C0KKo!d z$bE2XC(G=I^YUxYST+Hk>0T;JP_iAvCObcrPV1Eau865w6d^Wh&B?^#h2@J#!M2xp zLGAxB^i}4D2^?RayxFqBgnZ-t`j+~zVqr+9Cz9Rqe%1a)c*keP#r54AaR2*TH^}7j zmJ48DN);^{7+5|+GmbvY2v#qJy>?$B(lRlS#kyodlxA&Qj#9-y4s&|eq$5} zgI;4u$cZWKWj`VU%UY#SH2M$8?PjO-B-rNPMr=8d=-D(iLW#{RWJ}@5#Z#EK=2(&LvfW&{P4_jsDr^^rg9w#B7h`mBwdL9y)Ni;= zd$jFDxnW7n-&ptjnk#<0zmNNt{;_30vbQW!5CQ7SuEjR1be!vxvO53!30iOermrU1 zXhXaen8=4Q(574KO_h$e$^1khO&tQL59=)Dc^8iPxz8+tC3`G$w|yUzkGd%Wg4(3u zJ<&7r^HAaEfG?F8?2I64j4kPpsNQk7qBJa9_hFT;*j;A%H%;QI@QWqJaiOl=;u>G8 zG`5Ow4K5ifd=OS|7F;EFc1+GzLld0RCQxG>Fn?~5Wl5VHJ=$DeR-2zwBgzSrQsGG0 zBqrILuB+_SgLxh~S~^QNHWW(2P;Z?d!Rd1lnEM=z23xPzyrbO_L0k43zruDkrJO*D zlzN(peBMLji`xfgYUirul-7c#3t(*=x6A^KSU-L|$(0pp9A*43#=Q!cu%9ZHP!$J| zSk8k=Z8cl811Vvn(4p8xx+EdKQV(sjC4_mEvlWeuIfwEVcF2LiC{H!oW)LSW=0ul| zT?$5PCc(pf-zKzUH`p7I7coVvCK;Dv-3_c?%~bPz`#ehbfrSrFf{RAz0I5e*W1S)kTW{0gf5X2v2k=S=W{>pr44tQ?o` zih8gE29VGR_SL~YJtcA)lRLozPg!<3Mh(`Hp)5{bclb)reTScXzJ>7{?i^yR@{(^% z#=$BYXPIX%fhgsofP-T`3b<5#V(TTS)^$vlhV&Kn=(LXOTAADIR1v8UqmW5c`n`S% zC8SOW$e?>&0dwKD%Jt{+67PfCLnqX0{8K^(q_^^2#puPYPkJsyXWMa~?V?p5{flYi z-1!uqI2x%puPG)r7b8y+Pc0Z5C%aA6`Q1_?W9k!YbiVVJVJwGLL?)P0M&vo{^IgEE zrX3eTgrJl_AeXYmiciYX9OP?NPN%-7Ji%z3U`-iXX=T~OI0M=ek|5IvIsvXM$%S&v zKw{`Kj(JVc+Pp^?vLKEyoycfnk)Hd>et78P^Z*{#rBY~_>V7>{gtB$0G99nbNBt+r zyXvEg_2=#jjK+YX1A>cj5NsFz9rjB_LB%hhx4-2I73gr~CW_5pD=H|e`?#CQ2)p4& z^v?Dlxm-_j6bO5~eeYFZGjW3@AGkIxY=XB*{*ciH#mjQ`dgppNk4&AbaRYKKY-1CT z>)>?+ME)AcCM7RRZQsH5)db7y!&jY-qHp%Ex9N|wKbN$!86i>_LzaD=f4JFc6Dp(a z%z>%=q(sXlJ=w$y^|tcTy@j%AP`v1n0oAt&XC|1kA`|#jsW(gwI0vi3a_QtKcL+yh z1Y=`IRzhiUvKeZXH6>>TDej)?t_V8Z7;WrZ_7@?Z=HRhtXY+{hlY?x|;7=1L($?t3 z6R$8cmez~LXopZ^mH9=^tEeAhJV!rGGOK@sN_Zc-vmEr;=&?OBEN)8aI4G&g&gdOb zfRLZ~dVk3194pd;=W|Z*R|t{}Evk&jw?JzVERk%JNBXbMDX82q~|bv%!2%wFP9;~-H?={C1sZ( zuDvY5?M8gGX*DyN?nru)UvdL|Rr&mXzgZ;H<^KYvzIlet!aeFM@I?JduKj=!(+ zM7`37KYhd*^MrKID^Y1}*sZ#6akDBJyKna%xK%vLlBqzDxjQ3}jx8PBOmXkvf@B{@ zc#J;~wQ<6{B;``j+B!#7s$zONYdXunbuKvl@zvaWq;`v2&iCNF2=V9Kl|77-mpCp= z2$SxhcN=pZ?V{GW;t6s)?-cNPAyTi&8O0QMGo#DcdRl#+px!h3ayc*(VOGR95*Anj zL0YaiVN2mifzZ){X+fl`Z^P=_(W@=*cIe~BJd&n@HD@;lRmu8cx7K8}wPbIK)GjF> zQGQ2h#21o6b2FZI1sPl}9_(~R|2lE^h}UyM5A0bJQk2~Vj*O)l-4WC4$KZ>nVZS|d zZv?`~2{uPYkc?254B9**q6tS|>We?uJ&wK3KIww|zzSuj>ncI4D~K z1Y6irVFE{?D-|R{!rLhZxAhs+Ka9*-(ltIUgC;snNek4_5xhO}@+r9Sl*5=7ztnXO zAVZLm$Kdh&rqEtdxxrE9hw`aXW1&sTE%aJ%3VL3*<7oWyz|--A^qvV3!FHBu9B-Jj z4itF)3dufc&2%V_pZsjUnN=;s2B9<^Zc83>tzo)a_Q$!B9jTjS->%_h`ZtQPz@{@z z5xg~s*cz`Tj!ls3-hxgnX}LDGQp$t7#d3E}>HtLa12z&06$xEQfu#k=(4h{+p%aCg zzeudlLc$=MVT+|43#CXUtRR%h5nMchy}EJ;n7oHfTq6wN6PoalAy+S~2l}wK;qg9o zcf#dX>ke;z^13l%bwm4tZcU1RTXnDhf$K3q-cK576+TCwgHl&?9w>>_(1Gxt@jXln zt3-Qxo3ITr&sw1wP%}B>J$Jy>^-SpO#3e=7iZrXCa2!N69GDlD{97|S*og)3hG)Lk zuqxK|PkkhxV$FP45%z*1Z?(LVy+ruMkZx|(@1R(0CoS6`7FWfr4-diailmq&Q#ehn zc)b&*&Ub;7HRtFVjL%((d$)M=^6BV@Kiusmnr1_2&&aEGBpbK7OWs;+(`tRLF8x?n zfKJB3tB^F~N`_ak3^exe_3{=aP)3tuuK2a-IriHcWv&+u7p z_yXsd6kyLV@k=(QoSs=NRiKNYZ>%4wAF;2#iu1p^!6>MZUPd;=2LY~l2ydrx10b#OSAlltILY%OKTp{e{ zzNogSk~SJBqi<_wRa#JqBW8Ok=6vb%?#H(hG}Dv98{JST5^SSh>_GQ@UK-0J`6l#E za}X#ud0W?cp-NQE@jAx>NUv65U~%YYS%BC0Cr$5|2_A)0tW;(nqoGJUHG5R`!-{1M-4T{<^pOE!Dvyuu1x7?Wt#YIgq zA$Vwj`St+M#ZxJXXGkepIF6`xL&XPu^qiFlZcX+@fOAdQ9d(h{^xCiAWJ0Ixp~3&E z(WwdT$O$7ez?pw>Jf{`!T-205_zJv+y~$w@XmQ;CiL8d*-x_z~0@vo4|3xUermJ;Q z9KgxjkN8Vh)xZ2xhX0N@{~@^d@BLoYFW%Uys83=`15+YZ%KecmWXjVV2}YbjBonSh zVOwOfI7^gvlC~Pq$QDHMQ6_Pd10OV{q_Zai^Yg({5XysuT`3}~3K*8u>a2FLBQ%#_YT6$4&6(?ZGwDE*C-p8>bM?hj*XOIoj@C!L5) zH1y!~wZ^dX5N&xExrKV>rEJJjkJDq*$K>qMi`Lrq08l4bQW~!Fbxb>m4qMHu6weTiV6_9(a*mZ23kr9AM#gCGE zBXg8#m8{ad@214=#w0>ylE7qL$4`xm!**E@pw484-VddzN}DK2qg&W~?%hcv3lNHx zg(CE<2)N=p!7->aJ4=1*eB%fbAGJcY65f3=cKF4WOoCgVelH$qh0NpIka5J-6+sY* zBg<5!R=I*5hk*CR@$rY6a8M%yX%o@D%{q1Jn=8wAZ;;}ol>xFv5nXvjFggCQ_>N2} zXHiC~pCFG*oEy!h_sqF$^NJIpQzXhtRU`LR0yU;MqrYUG0#iFW4mbHe)zN&4*Wf)G zV6(WGOq~OpEoq##E{rC?!)8ygAaAaA0^`<8kXmf%uIFfNHAE|{AuZd!HW9C^4$xW; zmIcO#ti!~)YlIU4sH(h&s6}PH-wSGtDOZ+%H2gAO(%2Ppdec9IMViuwwWW)qnqblH9xe1cPQ@C zS4W|atjGDGKKQAQlPUVUi1OvGC*Gh2i&gkh0up%u-9ECa7(Iw}k~0>r*WciZyRC%l z7NX3)9WBXK{mS|=IK5mxc{M}IrjOxBMzFbK59VI9k8Yr$V4X_^wI#R^~RFcme2)l!%kvUa zJ{zpM;;=mz&>jLvON5j>*cOVt1$0LWiV>x)g)KKZnhn=%1|2E|TWNfRQ&n?vZxQh* zG+YEIf33h%!tyVBPj>|K!EB{JZU{+k`N9c@x_wxD7z~eFVw%AyU9htoH6hmo0`%kb z55c#c80D%0^*6y|9xdLG$n4Hn%62KIp`Md9Jhyp8)%wkB8<%RlPEwC&FL z;hrH(yRr(Ke$%TZ09J=gGMC3L?bR2F4ZU!}pu)*8@l(d9{v^^(j>y+GF*nGran5*M z{pl5ig0CVsG1etMB8qlF4MDFRkLAg4N=l{Sc*F>K_^AZQc{dSXkvonBI)qEN1*U&? zKqMr?Wu)q9c>U~CZUG+-ImNrU#c`bS?RpvVgWXqSsOJrCK#HNIJ+k_1Iq^QNr(j|~ z-rz67Lf?}jj^9Ik@VIMBU2tN{Ts>-O%5f?=T^LGl-?iC%vfx{}PaoP7#^EH{6HP!( zG%3S1oaiR;OmlKhLy@yLNns`9K?60Zg7~NyT0JF(!$jPrm^m_?rxt~|J2)*P6tdTU z25JT~k4RH9b_1H3-y?X4=;6mrBxu$6lsb@xddPGKA*6O`Cc^>Ul`f9c&$SHFhHN!* zjj=(Jb`P}R%5X@cC%+1ICCRh1^G&u548#+3NpYTVr54^SbFhjTuO-yf&s%r4VIU!lE!j(JzHSc9zRD_fw@CP0pkL(WX6 zn+}LarmQP9ZGF9So^+jr<(LGLlOxGiCsI^SnuC{xE$S;DA+|z+cUk=j^0ipB(WTZ} zR0osv{abBd)HOjc(SAV&pcP@37SLnsbtADj?bT#cPZq|?W1Ar;4Vg5m!l{@{TA~|g zXYOeU`#h-rT@(#msh%%kH>D=`aN}2Rysez?E@R6|@SB(_gS0}HC>83pE`obNA9vsH zSu^r>6W-FSxJA}?oTuH>-y9!pQg|*<7J$09tH=nq4GTx+5($$+IGlO^bptmxy#=)e zuz^beIPpUB_YK^?eb@gu(D%pJJwj3QUk6<3>S>RN^0iO|DbTZNheFX?-jskc5}Nho zf&1GCbE^maIL$?i=nXwi)^?NiK`Khb6A*kmen^*(BI%Kw&Uv4H;<3ib-2UwG{7M&* zn$qyi8wD9cKOuxWhRmFupwLuFn!G5Vj6PZ#GCNJLlTQuQ?bqAYd7Eva5YR~OBbIim zf(6yXS4pei1Bz4w4rrB6Ke~gKYErlC=l9sm*Zp_vwJe7<+N&PaZe|~kYVO%uChefr%G4-=0eSPS{HNf=vB;p~ z5b9O1R?WirAZqcdRn9wtct>$FU2T8p=fSp;E^P~zR!^C!)WHe=9N$5@DHk6(L|7s@ zcXQ6NM9Q~fan1q-u8{ez;RADoIqwkf4|6LfsMZK6h{ZUGYo>vD%JpY<@w;oIN-*sK zxp4@+d{zxe>Z-pH#_)%|d(AC`fa!@Jq)5K8hd71!;CEG|ZI{I2XI`X~n|ae;B!q{I zJDa#T+fRviR&wAN^Sl{z8Ar1LQOF&$rDs18h0{yMh^pZ#hG?c5OL8v07qRZ-Lj5(0 zjFY(S4La&`3IjOT%Jqx4z~08($iVS;M10d@q~*H=Py)xnKt(+G-*o33c7S3bJ8cmwgj45` zU|b7xCoozC!-7CPOR194J-m9N*g`30ToBo!Io?m>T)S{CusNZx0J^Hu6hOmvv;0~W zFHRYJgyRhP1sM_AQ%pkD!X-dPu_>)`8HunR4_v$4T78~R<})-@K2LBt03PBLnjHzuYY)AK?>0TJe9 zmmOjwSL%CTaLYvYlJ~|w?vc*R+$@vEAYghtgGhZ2LyF+UdOn+v^yvD9R%xbU$fUjK{{VQ4VL&&UqAFa>CZuX4kX zJ)njewLWfKXneB+r}Y$`ezzwDoRT3r{9(@=I3-z>8tT)n3whDyi(r*lAnxQJefj_x z-8lc=r!Vua{b}v;LT)oXW>~6Q03~RAp~R}TZq9sGbeUBMS)?ZrJqiu|E&ZE)uN1uL zXcAj3#aEz zzbcCF)+;Hia#OGBvOatkPQfE{*RtBlO1QFVhi+3q0HeuFa*p+Dj)#8Mq9yGtIx%0A znV5EmN(j!&b%kNz4`Vr-)mX_?$ng&M^a6loFO(G3SA!~eBUEY!{~>C|Ht1Q4cw)X5~dPiEYQJNg?B2&P>bU7N(#e5cr8qc7A{a7J9cdMcRx)N|?;$L~O|E)p~ zIC}oi3iLZKb>|@=ApsDAfa_<$0Nm<3nOPdr+8Y@dnb|u2S<7CUmTGKd{G57JR*JTo zb&?qrusnu}jb0oKHTzh42P00C{i^`v+g=n|Q6)iINjWk4mydBo zf0g=ikV*+~{rIUr%MXdz|9ebUP)<@zR8fgeR_rChk0<^^3^?rfr;-A=x3M?*8|RPz z@}DOF`aXXuZGih9PyAbp|DULSw8PJ`54io)ga6JG@Hgg@_Zo>OfJ)8+TIfgqu%877 z@aFykK*+|%@rSs-t*oAzH6Whyr=TpuQ}B0ptSsMg9p8@ZE5A6LfMk1qdsf8T^zkdC3rUhB$`s zBdanX%L3tF7*YZ4^A8MvOvhfr&B)QOWCLJ^02kw5;P%n~5e`sa6MG{E2N^*2ZX@ge zI2>ve##O?I}sWX)UqK^_bRz@;5HWp5{ziyg?QuEjXfMP!j zpr(McSAQz>ME?M-3NSoCn$91#_iNnULp6tD0NN7Z0s#G~-~xWZFWN-%KUVi^yz~-` zn;AeGvjLJ~{1p#^?$>zM4vu=3mjBI$(_tC~NC0o@6<{zS_*3nGfUsHr3Gdgn%XedF zQUP=j5Mb>9=#f7aPl;cm$=I0u*WP}aVE!lCYw2Ht{Z_j9mp1h>dHGKkEZP6f^6O@J zndJ2+rWjxp|3#<2oO=8v!oHMX{|Vb|^G~pU_A6=ckBQvt>o+dpgYy(D=VCj65GE&jJj{&-*iq?z)PHNee&-@Mie~#LD*={ex8h(-)<@|55 zUr(}L?mz#;d|mrD%zrh<-*=;5*7K$B`zPjJ%m2pwr*G6tf8tN%a

_x$+l{{cH8$W#CT literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..c6ac1e7296 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jul 22 17:43:50 PDT 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..642055e666 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -breadcrumbType d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..f9553162f1 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/scripts/release.gradle b/scripts/release.gradle new file mode 100644 index 0000000000..ec36d3c8f5 --- /dev/null +++ b/scripts/release.gradle @@ -0,0 +1,106 @@ +apply plugin: "maven-publish" +apply plugin: "signing" + +// load credentials from local properties if present +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localProperties.load(new FileInputStream(localPropertiesFile)) +} + +// create component with single publication variant +// https://developer.android.com/studio/publish-library/configure-pub-variants#single-pub-var +android { + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } +} + +// https://developer.android.com/studio/publish-library/upload-library +publishing { + publications { + + // create a single release publication + release(MavenPublication) { + groupId = "io.embrace" + artifactId = project.name + version = project.version + + afterEvaluate { + from components.release + } + + // append some license metadata to the POM. + pom { + name = project.name + description = "Embrace Android SDK" + url = "https://github.com/embrace-io/embrace-android-sdk" + licenses { + license { + name = "Embrace license" + url = "https://embrace.io/docs/embrace-software-notice/" + } + } + developers { + developer { + id = "dev1" + name = "Embrace" + email = "support@embrace.io" + } + } + scm { + connection = "scm:git:github.com/embrace-io/embrace-android-sdk.git" + developerConnection = "scm:git:ssh://github.com/embrace-io/embrace-android-sdk.git" + url = "https://github.com/embrace-io/embrace-android-sdk/tree/main" + } + } + } + } + + // configure repositories where the publication can be hosted + repositories { + // beta releases + maven { + credentials { + username System.getenv("MAVEN_QA_USER") + password System.getenv("MAVEN_QA_PASSWORD") + } + name = "Qa" + url = "https://repo.embrace.io/repository/beta" + } + // the android-testing maven repository is used for publishing snapshots + maven { + credentials { + username System.getenv("MAVEN_QA_USER") + password System.getenv("MAVEN_QA_PASSWORD") + } + name = "Snapshot" + url = "https://repo.embrace.io/repository/android-testing" + } + + // sonatype repo that provides path to publishing on maven central + maven { + credentials { + username System.getenv("SONATYPE_USERNAME") ?: localProperties.ossrhUsername + password System.getenv("SONATYPE_PASSWORD") ?: localProperties.ossrhPassword + } + name = "sonatype" + url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2" + } + } +} + +allprojects { + ext."signing.keyId" = System.getenv("mavenSigningKeyId") ?: localProperties.getProperty("signing.keyId") + ext."signing.secretKeyRingFile" = System.getenv("mavenSigningKeyRingFile") ?: localProperties.getProperty("signing.secretKeyRingFile") + ext."signing.password" = System.getenv("mavenSigningKeyPassword") ?: localProperties.getProperty("signing.password") +} + +signing { + sign publishing.publications.release +} + +project.tasks.withType(Sign)*.enabled = !project.version.endsWith("-SNAPSHOT") diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..2d8c71f671 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +include ":embrace-android-sdk", + ":embrace-android-okhttp3", + ":embrace-android-fcm", + ":embrace-android-compose", + ":embrace-lint", + ":test-server" diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000..142261afdf --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,24 @@ +sonar.projectKey=embrace-android-sdk +sonar.projectName=Embrace Android SDK + +# ===================================================== +# Meta-data for the project +# ===================================================== + +sonar.links.homepage=https://github.com/embrace-io/embrace-android-sdk +sonar.links.ci=https://github.com/embrace-io/embrace-android-sdk/actions +sonar.links.scm=https://github.com/embrace-io/embrace-android-sdk +sonar.links.issue=https://github.com/embrace-io/embrace-android-sdk/issues + +# ===================================================== +# Properties that will be shared amongst all modules +# ===================================================== + +# SQ standard properties +sonar.sources=embrace-android-sdk/src/main +sonar.tests=embrace-android-sdk/src/test +sonar.java.binaries=. +sonar.java.source=1.8 +sonar.c.file.suffixes=- +sonar.cpp.file.suffixes=- +sonar.objc.file.suffixes=- diff --git a/test-server/.gitignore b/test-server/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/test-server/.gitignore @@ -0,0 +1 @@ +/build diff --git a/test-server/README.md b/test-server/README.md new file mode 100644 index 0000000000..0743d0e5c7 --- /dev/null +++ b/test-server/README.md @@ -0,0 +1,124 @@ +## Testing + +### Basic Lambda types + +These are important objects that will come up later for interacting with the TestServer. The order displayed is the order they would be invoked for a given request. + +``` +where "T" is the object (`Session`, `Event` etc) in the Request body, and "R" is the object (`Config` or Unit for the rest) in the Response body + +typealias RequestFilter = (T, EmbraceMockConnectionImpl?) -> Boolean +typealias ResponseLogic = (T, EmbraceMockConnectionImpl) -> Response +typealias OnRequestCallback = (T, Response, EmbraceMockConnectionImpl) -> Unit +``` + +#### RequestFilter + +`RequestFilter` is used to indicate which request you would like this to be applied to. The way this should be implemented is, in your lambda, inspect the Request object and/or the connection to see if it meets the criteria you are testing for, and if it does return `true`, and if it doesn't, return `false` + +**In almost all cases, a RequestFilter of `null` will match ALL requests for an endpoint** + +#### ResponseLogic + +`ResponseLogic` is used to generate Responses given a Request. The TestServer will invoke the `ResponseLogic` instance with the request it passed from the `UrlConnection` and set the values in the returned Response object on the `UrlConnection` + +#### OnRequestCallback + +`OnRequestCallback` is invoked after a request goes out and a response is generated, but before that response is passed to the connection, so the SDK has not received the response yet. This callback is designed to be where you can put assertations, probably assertations confirming that the SDK sent out the proper request object. + + +### `TestServer` + +``` +fun getEndpointLogic(endpointType: EndpointType): EndpointLogic +fun getEndpointTesting(endpointType: EndpointType): EndpointTesting + +val receivedRequests: List> +``` + +`TestServer` is an in process mock server that receives the raw `UrlConnection` object and injects/mocks/sets a response based on internal set of "logic" that it carries. + +This class is composed of multiple `Endpoint` instances, one each for "Config", "Events", "Sessions" and "Images". Each Endpoint has a corresponding `EndpointType`, which is used to fetch the instance. logic is registered on a per-endpoint basis, and the `TestServer` will delegate the responsibility of processing a request to an `Endpoint` based on the URL of the request. + +to fetch the "Events" `Endpoint` from `TestServer` call; + +``` +// to retrieve the endpoint logic +EndpointLogic endpoint = mServer.getEndpointLogic(EndpointType.Events) + +//to retrieve the config testing methods +EndpointTesting endpoint = mServer.getEndpointTesting(EndpointType.Events) +``` + +`receivedRequests` will return all the requests received for all the Endpoints in one List + +#### `Endpoint` + +`Endpoint` is a single implementation, but is broken up into 2 interfaces, `EndpointLogic` which contains setters for response "logic" and `EndpointTesting` which contains methods and callbacks to expose the internal state of the server, useful for writing tests. No huge reason for doing this, I just thought it would make it simpler + +#### `EndpointLogic` + +``` +fun addRequestResponseLogic(filter: RequestFilter?, logic: ResponseLogic) +fun clearRequestResponseLogic() +``` + +`addRequestResponseLogic` will add a conditional response for an Endpoint. First the Endpoint will check if the incoming request gets `true` from your `RequestFilter` and if so, it will generate a Response based on the value returned by the `ResponseLogic`. **TestServer will check for a match starting with the most recent entry** and working backward chronologically from there, and will only generate a response based on the first match. So, even if you have multiple matching `RequestFilter`s for a given request, it will only use the most recent one's `ResponseLogic` to generate a Response + +Each endpoint has default "happy" logic. basically for Config it's to just return a default config object and the rest to return a 200 regardless of the request body. `clearRequestResponseLog()` will remove all of the logic set during the test and revert back to that default logic; the same behavior you would get if you had not added any logic in the first place + + +#### `EndpointTesting` + +``` +val receivedRequests: List> +fun onRequestFinished(requestFilter: RequestFilter? = null, onRequestCallback: OnRequestCallback? = null) +fun onRequestFinishedBlocking(requestFilter: RequestFilter? = null, onRequestCallback: OnRequestCallback? = null) +fun clearRequestFilter() +``` + +`receivedRequests` is a list of all the requests received during the test run for the endpoint The current request will already be added to this by the time an `OnRequestReceived` callback is invoked, keep that in mind if you are using this field in the body of an `OnRequestReceived` lambda. Also, from java this is transformed into a method, `getReceivedRequests()` as part of the standard interop + +`onRequestFinished()` allows you to register a filter and a callback for a request. **This callback will only fire once**, after a match is received and the functions are invoked, it will be removed from the TestServer. + +> I couldn't quite decide what the default behavior should be, or if there should be an option to leave it up indefinitely or return a boolean in the `OnRequestCallback` which indicates if it should be unregistered or not...idk, lots of possibilities with this and the behavior I chose is just based on personal preference. Lmk if you want this changed in the future + +`onRequestFinishedBlocking()` take the filter and callback and **block the thread until a match is received**. This kind of method is super useful if the completion of a test hinges on checking what the body of a network request looks like, or making sure a request went out. you can make this the last method in a test, and it will either fail or continue and complete the test depending on what happens. + +There is a "timeout` that can be set by calling `TestServer.defaultTimeout`. This method will block only as long as the `defaultTimeout` period, and if a match hasn't been found **the test will fail** + +`clearRequestFilter()` removes all these filters + +### Base Test Classes + +Tests should extend 1 of the 2 current Base Test classes. The classes will do all the messy resetting and rewiring necessary for the `TestServer` to work, and stepping outside of them will take some work to get running + +`BaseTest` just does basic setup of the testing environment, you will need to call `Embrace.start()` yourself to start the SDK + +`BaseEmbraceStarted` extends `BaseTest` but also starts the SDK and fires the `OnCreate` and `OnStart` lifecycle methods so a Session is already underway by the time your test starts running + +#### `BaseTest`/`EmbraceContext ` + +`BaseTest`/`EmbraceContext` +``` +fun setBuildInfo(buildInfo: BuildInfo) +fun setLocalConfig(localConfig: LocalConfig) + +fun triggerLifecycleEvent(event: Lifecycle.Event) +fun triggerOnLowMemory() + +fun sendForeground() +fun sendBackground() +``` + +We have full control over mocking the activity lifecycle. This applies both for callbacks registered to `Context.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks)` as well as the generated `LifecycleObserver`s registered via `ProcessLifecycleOwner.get().getLifecycle().addObserver(LifecycleObserver)`. There aren't really any guardrails on this to prevent setting the lifecycles out of order or anything, so it is possible that the Android internals will not like it if you call lifecycle methods out of order. If you are simply trying to send the application into the foreground or the background, you should use `sendForeground()` or `sendBackground()` instead + +> The hacking I did to get override the observer registered with `ProcessLifecycleOwner` seems solid, but hasn't been tested in a wide variety of situations, so I could see it breaking. Lmk if it does + +`setBuildInfo(BuildInfo)` and `setLocalConfig(LocalConfig)` both inject the objects into the `resources` folder which will be fetched on startup, so **these methods must be called before `Embrace.start()` to have an effect`** + +Also, All of these methods are available in the `EmbraceContext` instance, calling them there will have the exact same effect as calling them on `BaseTest` + +`sendForeground()` will trigger the `ON_START` lifecycle event to occur. This also includes all preceeding lifecycle events. For example, if the test has not called triggerLifecycleEvent() yet and this method is invoked, it will call `triggerLifecycleEvent(Lifecycle.Event.ON_CREATE)`, then call `triggerLifecycleEvent(Lifecycle.Event.ON_START)`. + +`sendBackground()` will trigger the `ON_STOP` lifecycle event to occur. Much like the previous method, all preceeding lifecycle events will be triggered diff --git a/test-server/build.gradle b/test-server/build.gradle new file mode 100644 index 0000000000..698fc87ab5 --- /dev/null +++ b/test-server/build.gradle @@ -0,0 +1,40 @@ +plugins { + id "internal-embrace-plugin" +} + +embraceOptions { + apiBinaryCompatChecks.set(false) +} + +android { + namespace = "io.embrace.android.embracesdk.testserver" + + defaultConfig { + minSdkVersion 21 + versionCode 52 + versionName version + consumerProguardFiles "consumer-rules.pro" + } + + sourceSets { + androidTest.java.srcDirs += "src/androidTest/kotlin" + main.java.srcDirs += "src/main/kotlin" + } +} + +dependencies { + androidTestImplementation project(":embrace-android-sdk") + implementation "androidx.test:core:1.4.0" + implementation "androidx.test:runner:1.4.0" + implementation "androidx.test:rules:1.4.0" + implementation "androidx.test.ext:junit:1.1.3" + implementation "androidx.test.espresso:espresso-core:3.4.0" + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.annotation:annotation:1.2.0" + implementation "androidx.lifecycle:lifecycle-common:2.5.1" + implementation "androidx.lifecycle:lifecycle-extensions:2.0.0" + implementation "com.google.code.gson:gson:2.9.0" + implementation "com.squareup.okhttp3:mockwebserver:4.9.0" + + compileOnly project(":embrace-android-sdk") +} diff --git a/test-server/consumer-rules.pro b/test-server/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test-server/lint-baseline.xml b/test-server/lint-baseline.xml new file mode 100644 index 0000000000..3e726e59ba --- /dev/null +++ b/test-server/lint-baseline.xml @@ -0,0 +1,491 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-server/proguard-rules.pro b/test-server/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/test-server/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/test-server/src/main/AndroidManifest.xml b/test-server/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..03717b1fdb --- /dev/null +++ b/test-server/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test-server/src/main/kotlin/androidx/lifecycle/MockReportFragment.kt b/test-server/src/main/kotlin/androidx/lifecycle/MockReportFragment.kt new file mode 100644 index 0000000000..a9ac9b2350 --- /dev/null +++ b/test-server/src/main/kotlin/androidx/lifecycle/MockReportFragment.kt @@ -0,0 +1,22 @@ +package androidx.lifecycle + +/** + * Used to Mock the ReportFragment as part of mocking the activity lifecycle callbacks + */ +public class MockReportFragment : ReportFragment() { + + internal var embraceProcessListener: ActivityInitializationListener? = null + + internal override fun setProcessListener(processListener: ActivityInitializationListener?) { + this.embraceProcessListener = processListener + } + + public fun onLifecycleEvent(event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_CREATE -> embraceProcessListener?.onCreate() + Lifecycle.Event.ON_START -> embraceProcessListener?.onStart() + Lifecycle.Event.ON_RESUME -> embraceProcessListener?.onResume() + else -> {} // do nothing + } + } +} diff --git a/test-server/src/main/kotlin/androidx/lifecycle/ProcessLifecycleOwnerAccess.kt b/test-server/src/main/kotlin/androidx/lifecycle/ProcessLifecycleOwnerAccess.kt new file mode 100644 index 0000000000..7faac1a25a --- /dev/null +++ b/test-server/src/main/kotlin/androidx/lifecycle/ProcessLifecycleOwnerAccess.kt @@ -0,0 +1,18 @@ +package androidx.lifecycle + +import android.app.Activity +import android.content.Context + +/** + * Used to access Package-Private methods in ProcessLifecycleOwner + */ +public object ProcessLifecycleOwnerAccess { + + public fun attach(context: Context) { + ProcessLifecycleOwner.init(context) + } + + public fun get(activity: Activity): ReportFragment { + return ReportFragment.get(activity) + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/ActivityServiceHooks.java b/test-server/src/main/kotlin/io/embrace/android/embracesdk/ActivityServiceHooks.java new file mode 100644 index 0000000000..757cabbb7b --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/ActivityServiceHooks.java @@ -0,0 +1,13 @@ +package io.embrace.android.embracesdk; + +import io.embrace.android.embracesdk.session.ActivityListener; + +/** + * Provides hooks into the activity service that aren't accessible via Kotlin. + */ +public class ActivityServiceHooks { + + static void addListener(ActivityListener listener) { + Embrace.getImpl().getActivityService().addListener(listener); + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/BaseTest.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/BaseTest.kt new file mode 100644 index 0000000000..61deffe848 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/BaseTest.kt @@ -0,0 +1,342 @@ +package io.embrace.android.embracesdk + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.FileObserver +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ProcessLifecycleOwnerAccess +import androidx.test.platform.app.InstrumentationRegistry +import com.google.gson.Gson +import io.embrace.android.embracesdk.utils.BitmapFactory +import io.embrace.android.embracesdk.utils.JsonValidator +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import java.io.File +import java.io.IOException +import java.net.HttpURLConnection +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPInputStream + +/** + * The default Base test class, which all tests using TestServer should inherit from. This + * class will reset the Embrace instance as well as the TestServer before each individual test + * is run + */ +public open class BaseTest { + + protected lateinit var failedApiCallsFilePath: String + public lateinit var mContext: EmbraceContext + protected val gson: Gson = Gson() + public val testServer: TestServer = TestServer() + private var fileObserver: EmbraceFileObserver? = null + + @SuppressLint("VisibleForTests") + @Before + public fun beforeEach() { + testServer.start(getDefaultNetworkResponses()) + + if (Looper.myLooper() == null) { + Looper.prepare() + } + + mContext = + EmbraceContext(InstrumentationRegistry.getInstrumentation().context.applicationContext) + + failedApiCallsFilePath = mContext.cacheDir.absolutePath + "/emb_failed_api_calls.json" + + // attach our mock context to the ProcessLifecycleOwner, this will give us control over the + // activity/application lifecycle for callbacks registered with the ProcessLifecycleOwner + Handler(Looper.getMainLooper()).post { + ProcessLifecycleOwnerAccess.attach(mContext) + } + clearCacheFolder() + + Embrace.setImpl(EmbraceImpl()) + + setLocalConfig() + refreshScreenshotBitmap() + } + + @After + public fun afterEach() { + Log.e("TestServer", "Stop Embrace") + Embrace.getImpl().stop() + testServer.stop() + fileObserver?.stopWatching() + } + + private fun clearCacheFolder() { + mContext.cacheDir.deleteRecursively() + } + + private fun getDefaultNetworkResponses(): Map { + val config = ConfigHooks.getConfig() + val configMockResponse = TestServerResponse(HttpURLConnection.HTTP_OK, gson.toJson(config)) + + return mapOf( + EmbraceEndpoint.LOGGING.url to TestServerResponse(HttpURLConnection.HTTP_OK), + EmbraceEndpoint.EVENTS.url to TestServerResponse(HttpURLConnection.HTTP_OK), + EmbraceEndpoint.SESSIONS.url to TestServerResponse(HttpURLConnection.HTTP_OK), + EmbraceEndpoint.SCREENSHOT.url to TestServerResponse(HttpURLConnection.HTTP_OK), + EmbraceEndpoint.CONFIG.url to configMockResponse + ) + } + + private fun setLocalConfig() { + val baseUrl = testServer.getBaseUrl() + mContext.appId = "default-test-app-Id" + mContext.sdkConfig = ConfigHooks.getSdkConfig(baseUrl) + } + + /** + * trigger a specific Lifecycle event to fire in the application. For example, passing in Lifecycle.Event.ON_CREATE + * will trigger `ActivityLifecycleCallbacks.onCreate()` callbacks as well as those registered with the + * ProcessLifecycleOwner. + * + * Be careful, this method uses a good deal of internal Android code, so if you call lifecycle methods + * out of their normal order, you may get weird behavior or silent failures. If you would just like to + * send the application to the foreground or the background, you should call sendForeground() or sendBackground() + */ + public fun triggerLifecycleEvent(event: Lifecycle.Event) { + mContext.triggerActivityLifecycleEvent(event) + } + + public fun triggerOnLowMemory() { + mContext.triggerOnLowMemory() + } + + /** + * send the Application to the Foreground by triggering the ON_START lifecycle callbacks. + * This method will "catch up" your current lifecycle state to ON_START by calling all methods + * inbetween. For example, if you do not have current lifecycle state, + * we will trigger ON_CREATE and then trigger ON_START when this method is invoked + */ + public fun sendForeground() { + mContext.sendForeground() + } + + /** + * send the Application to the Background by triggering the ON_STOP lifecycle callbacks. This method will "catch up" + * your current lifecycle state to ON_STOP by calling all methods inbetween. For example, if your current + * lifecycle state is ON_RESUME, we will trigger ON_PAUSE and then trigger ON_START when this method is invoked + */ + public fun sendBackground() { + mContext.sendBackground() + } + + public fun getScreenshot(): Bitmap { + val bitmap = Bitmap.createBitmap( + mContext.screenshotBitmap.width, + mContext.screenshotBitmap.height, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + canvas.setBitmap(bitmap) + return bitmap + } + + private fun refreshScreenshotBitmap() { + mContext.screenshotBitmap = BitmapFactory.newRandomBitmap(BITMAP_WIDTH, BITMAP_HEIGHT) + } + + /** + * Starts the Embrace SDK and simulates the application coming into the foreground (which + * starts a session). + */ + public fun startEmbraceInForeground() { + Log.e("TestServer", "Start Embrace") + Embrace.getInstance().start(mContext) + assertTrue(Embrace.getInstance().isStarted) + Log.e("TestServer", "initialize lifecycle to start session") + sendForeground() + Embrace.getInstance().addBreadcrumb("a message") + Embrace.getInstance().setUserEmail("user@email.com") + Embrace.getInstance().setUserIdentifier("some id") + Embrace.getInstance().setUsername("John Doe") + + validateInitializationRequests() + } + + /** + * Consume requests done when app starts: + * - Remote Config request + * - Session + * - Startup Moment Start Event + * - Startup Moment End Event + * It needs to be done with a for because the order of the requests can be different between runs. + */ + private fun validateInitializationRequests() { + var isStartupStartEventValidated = false + + (0 until TOTAL_REQUESTS_AT_INIT).forEach { _ -> + waitForRequest { request -> + when (request.path?.substringBefore("?")) { + EmbraceEndpoint.EVENTS.url -> { + Assert.assertEquals("POST", request.method) + if (!isStartupStartEventValidated) { + isStartupStartEventValidated = true + validateMessageAgainstGoldenFile( + request, + "moment-startup-start-event.json" + ) + } else { + validateMessageAgainstGoldenFile( + request, + "moment-startup-late-event.json" + ) + } + } + EmbraceEndpoint.SESSIONS.url -> { + Assert.assertEquals("POST", request.method) + validateMessageAgainstGoldenFile(request, "session-start.json") + } + EmbraceEndpoint.CONFIG.url -> { + Assert.assertEquals("GET", request.method) + } + else -> fail("Unexpected Request call. ${request.path}") + } + println("REQUEST: ${request.path}") + } + } + } + + /** + * Blocks the current thread until the SDK makes a HTTP request of the expected type. + * + * For example, if you call [sendForeground] the SDK will send a session and this method + * will get the request. You can then write assertions against that request. + * + * If the request is not received within a reasonable amount of time this method will + * fail the test. + */ + public fun waitForRequest(action: (response: RecordedRequest) -> Unit = {}) { + val request = testServer.takeRequest() + request?.let(action) ?: fail( + "Expected request not sent after configured timeout. " + + "The SDK probably either failed to send the data or crashed - check Logcat for clues." + ) + } + + public fun waitForFailedRequest( + endpoint: EmbraceEndpoint, + request: () -> Unit, + action: () -> Unit, + validate: (file: File) -> Unit + ) { + val startSignal = CountDownLatch(1) + val file = File(failedApiCallsFilePath) + + fileObserver = EmbraceFileObserver(failedApiCallsFilePath, FileObserver.ALL_EVENTS) + fileObserver?.startWatching(startSignal) + + testServer.addResponse( + endpoint, + TestServerResponse(HttpURLConnection.HTTP_BAD_REQUEST) + ) + + request() + action() + startSignal.await(500, TimeUnit.MILLISECONDS) + validate(file) + } + + public fun validateMessageAgainstGoldenFile( + request: RecordedRequest, + goldenFileName: String + ) { + try { + val requestBody = readCompressedRequestBody(request) + val goldenFileIS = mContext.assets.open("golden-files/$goldenFileName") + + val msg by lazy { + val observedOutput = writeOutputToDisk(requestBody, goldenFileName, ".observed") + val expected = + mContext.assets.open("golden-files/$goldenFileName").bufferedReader().readText() + val expectedOutput = writeOutputToDisk(expected, goldenFileName, ".expected") + + "Request ${request.path} differs from golden-files/$goldenFileName. Please check " + + "logcat for further details. You can also compare the difference" + + " on https://www.jsondiff.com/ by pulling the expected/observed files with adb:\n" + + "adb pull ${expectedOutput.absolutePath}\n" + + "adb pull ${observedOutput.absolutePath}\n" + + "observed: $requestBody" + } + assertTrue(msg, JsonValidator.areEquals(goldenFileIS, requestBody)) + } catch (e: IOException) { + fail("Failed to validate request against golden file. ${e.stackTraceToString()}") + } + } + + private fun readCompressedRequestBody(request: RecordedRequest): String { + return try { + val data = GZIPInputStream(request.body.inputStream()) + data.use { String(it.readBytes()) } + } catch (exc: IOException) { + throw IllegalStateException( + "Failed to uncompress inputstream of request body. The SDK probably didn't send a " + + "request - check Logcat for any crashes in the process.", + exc + ) + } + } + + private fun writeOutputToDisk( + requestBody: String, + goldenFilename: String, + suffix: String + ): File { + val dir = File(mContext.externalCacheDir, "test_failure").apply { mkdir() } + return File(dir, "${goldenFilename}$suffix").apply { + writeText(requestBody) + } + } + + /** + * Reads the file with the failed api call to validate failedApiContent is present + * + * @param failedApiContent the content that was intended to send in the api call + * @param failedCallFileName file name that contains our failed api request + */ + public fun readFileContent(failedApiContent: String, failedCallFileName: String) { + val failedApiFilePath = + mContext.cacheDir.path + "/emb_" + failedCallFileName + val failedApiFile = File(failedApiFilePath) + val failedApiJsonString: String = + failedApiFile.reader().use { it.readText() } + assertTrue(failedApiJsonString.contains(failedApiContent)) + } + + /** + * Reads the file that contains all the failed api request to get the one we need to validate + */ + public fun readFile(file: File, failedApiCall: String) { + try { + assertTrue(file.exists() && !file.isDirectory) + val jsonString: String = file.reader().use { it.readText() } + assertTrue(jsonString.contains(failedApiCall)) + } catch (e: IOException) { + fail("IOException error: ${e.message}") + } + } +} + +public const val TOTAL_REQUESTS_AT_INIT: Int = 4 +public const val BITMAP_HEIGHT: Int = 100 +public const val BITMAP_WIDTH: Int = 100 + +public enum class EmbraceEndpoint(public val url: String) { + CONFIG("/v2/config"), + SCREENSHOT("/v1/screenshot"), + SESSIONS("/v1/log/sessions"), + EVENTS("/v1/log/events"), + LOGGING("/v1/log/logging") +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/BuildInfoHooks.java b/test-server/src/main/kotlin/io/embrace/android/embracesdk/BuildInfoHooks.java new file mode 100644 index 0000000000..faac006760 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/BuildInfoHooks.java @@ -0,0 +1,21 @@ +package io.embrace.android.embracesdk; + +import java.util.HashMap; +import java.util.Map; + +import io.embrace.android.embracesdk.internal.BuildInfo; + +class BuildInfoHooks { + + private static final BuildInfo buildInfo = new BuildInfo("default test build id", "default test build type", "default test build flavor"); + + static Map getResourceValues(String appId, String sdkConfig) { + Map map = new HashMap<>(); + map.put(BuildInfo.BUILD_INFO_BUILD_ID.hashCode(), buildInfo.getBuildId()); + map.put(BuildInfo.BUILD_INFO_BUILD_FLAVOR.hashCode(), buildInfo.getBuildFlavor()); + map.put(BuildInfo.BUILD_INFO_BUILD_TYPE.hashCode(), buildInfo.getBuildType()); + map.put("emb_app_id".hashCode(), appId); + map.put("emb_sdk_config".hashCode(), sdkConfig); + return map; + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/ConfigHooks.java b/test-server/src/main/kotlin/io/embrace/android/embracesdk/ConfigHooks.java new file mode 100644 index 0000000000..b5a2783f74 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/ConfigHooks.java @@ -0,0 +1,106 @@ +package io.embrace.android.embracesdk; + +import android.util.Base64; + +import com.google.gson.Gson; + +import java.nio.charset.StandardCharsets; + +import io.embrace.android.embracesdk.config.local.BaseUrlLocalConfig; +import io.embrace.android.embracesdk.config.local.NetworkLocalConfig; +import io.embrace.android.embracesdk.config.local.SdkLocalConfig; +import io.embrace.android.embracesdk.config.remote.RemoteConfig; +import io.embrace.android.embracesdk.config.remote.SessionRemoteConfig; +import io.embrace.android.embracesdk.config.remote.WebViewVitals; + +/** + * Provides hooks into SessionConfig that aren't accessible via Kotlin. + */ +public class ConfigHooks { + + static RemoteConfig getConfig() { + return new RemoteConfig( + null, + null, + null, + null, + null, + null, + null, + null, + null, + getSessionConfig(), + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + getWebViewVitals() + ); + } + + static SessionRemoteConfig getSessionConfig() { + return new SessionRemoteConfig(true, + false, + null, + null); + } + + static WebViewVitals getWebViewVitals() { + return new WebViewVitals(100f, 100); + } + + static BaseUrlLocalConfig getBaseUrlConfig(String baseUrl) { + return new BaseUrlLocalConfig(baseUrl, + baseUrl, + baseUrl, + baseUrl); + } + + static NetworkLocalConfig getNetworkConfig() { + // With this config in true, an error related with openConnection reflection method not found + // was being thrown on this test: + // io.embrace.android.embracesdk.LogMessageTest.logHandledExceptionTest + return new NetworkLocalConfig(null, + null, + null, + null, + null, + false); + } + + static String getSdkConfig(String baseUrl) { + SdkLocalConfig sdkConfig = new SdkLocalConfig( + null, + null, + null, + null, + null, + null, + null, + null, + null, + getNetworkConfig(), + null, + null, + null, + null, + getBaseUrlConfig(baseUrl), + null, + null, + null, + null + ); + + String json = new Gson().toJson(sdkConfig); + return Base64.encodeToString( + json.getBytes(StandardCharsets.UTF_8), + Base64.DEFAULT + ); + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceContext.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceContext.kt new file mode 100644 index 0000000000..2c96ae7f16 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceContext.kt @@ -0,0 +1,214 @@ +package io.embrace.android.embracesdk + +import android.app.Application +import android.content.ComponentCallbacks +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.MockReportFragment +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwnerAccess +import io.embrace.android.embracesdk.internal.MockActivity +import io.embrace.android.embracesdk.internal.PauseProcessListener +import io.embrace.android.embracesdk.utils.BitmapFactory +import io.embrace.android.embracesdk.utils.FailureLatch +import org.junit.Assert.assertEquals + +/** + * The super mocked Context. This is a very useful class for injecting Resources, including those + * that BuildInfo and LocalConfig are read from, as well as mocking Activity Lifecycle Events + */ +public class EmbraceContext(public val context: Context) : Application() { + public val activity: MockActivity + public val handler: Handler = Handler(Looper.getMainLooper()) + + private var currentState: Lifecycle.Event? = null + private var pauseProcessListener: PauseProcessListener + + private val activityLifecycleCallbacks = mutableListOf() + private val componentCallbacks = mutableListOf() + + public var appId: String? = null + public var sdkConfig: String? = null + public var screenshotBitmap: Bitmap = BitmapFactory.newRandomBitmap(100, 100) + + init { + attachBaseContext(context) + pauseProcessListener = PauseProcessListener() + ProcessLifecycleOwner.get().lifecycle.addObserver(pauseProcessListener) + activity = MockActivity(this) + } + + override fun onCreate() { + super.onCreate() + } + + public fun triggerOnLowMemory() { + componentCallbacks.forEach { it.onLowMemory() } + } + + public fun triggerActivityLifecycleEvent(state: Lifecycle.Event) { + val bundle = Bundle() + if (activity.baseContext == null) { + activity.setContext(context) + } + + val latch = FailureLatch(2000) + val pauseLatch = FailureLatch(3000) + + // invoke Activity callbacks on the Main Thread, like it would in a real application. + // + // there is some funky behavior in the ProcessObserver where it has a fixed delay calling + // the onPause callbacks. blocking using the pauseLatch until the callback is actually invoked + // is the only way we can run this without arbitrary Thread.Sleep() calls + handler.post { + invokeCallback(state, bundle, pauseLatch, latch) + } + latch.await() + + if (state == Lifecycle.Event.ON_PAUSE) { + pauseLatch.await() + } + + currentState = state + } + + private fun invokeCallback( + state: Lifecycle.Event, + bundle: Bundle, + pauseLatch: FailureLatch, + latch: FailureLatch + ) { + when (state) { + Lifecycle.Event.ON_CREATE -> activityLifecycleCallbacks.forEach { + it.onActivityCreated(activity, bundle) + } + Lifecycle.Event.ON_START -> activityLifecycleCallbacks.forEach { + it.onActivityStarted(activity) + } + Lifecycle.Event.ON_RESUME -> activityLifecycleCallbacks.forEach { + it.onActivityResumed(activity) + } + Lifecycle.Event.ON_PAUSE -> { + pauseProcessListener.onPauseCallback = { pauseLatch.countDown() } + activityLifecycleCallbacks.forEach { + it.onActivityPaused(activity) + } + } + Lifecycle.Event.ON_STOP -> activityLifecycleCallbacks.forEach { + it.onActivityStopped(activity) + } + Lifecycle.Event.ON_DESTROY -> activityLifecycleCallbacks.forEach { + it.onActivityDestroyed(activity) + } + Lifecycle.Event.ON_ANY -> + throw IllegalArgumentException("ON_ANY must not been send by anybody") // copy from the Android SDK code + } + (ProcessLifecycleOwnerAccess.get(activity) as MockReportFragment).onLifecycleEvent(state) + latch.countDown() + } + + public fun sendForeground() { + currentState + when (currentState) { + null, Lifecycle.Event.ON_CREATE, Lifecycle.Event.ON_DESTROY -> { + while (currentState != Lifecycle.Event.ON_START) { + currentState.nextEvent()?.let { triggerActivityLifecycleEvent(it) } + } + } + Lifecycle.Event.ON_STOP -> { + triggerActivityLifecycleEvent(Lifecycle.Event.ON_START) + } + else -> error("Cannot send App to foreground when current state is: $currentState") + } + assertEquals("Failed to send application to Foreground", Lifecycle.Event.ON_START, currentState) + } + + override fun getPackageManager(): PackageManager { + return FakePackageManager(this) + } + + public fun sendBackground() { + var currentState = currentState + when (currentState) { + Lifecycle.Event.ON_START, Lifecycle.Event.ON_RESUME, Lifecycle.Event.ON_PAUSE -> { + while (currentState != Lifecycle.Event.ON_STOP) { + currentState = currentState.nextEvent() + if (currentState != null) { + triggerActivityLifecycleEvent(currentState) + } + } + } + Lifecycle.Event.ON_STOP, Lifecycle.Event.ON_DESTROY -> { + error("Application is already in the background, current state = $currentState") + } + null, Lifecycle.Event.ON_CREATE -> { + error("Application has not entered foreground, call sendForeground() first") + } + else -> {} + } + assertEquals("Failed to send application to Background", currentState, Lifecycle.Event.ON_STOP) + } + + override fun getResources(): Resources { + return EmbraceResources(super.getResources(), appId, sdkConfig) + } + + override fun getApplicationContext(): Context { + return this + } + + override fun registerActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) { + callback?.let { activityLifecycleCallbacks.add(it) } + } + + override fun registerComponentCallbacks(callback: ComponentCallbacks) { + componentCallbacks.add(callback) + } + + override fun unregisterActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) { + activityLifecycleCallbacks.remove(callback) + super.unregisterActivityLifecycleCallbacks(callback) + } + + public fun Lifecycle.Event?.nextEvent(): Lifecycle.Event? { + return when (this) { + null -> Lifecycle.Event.ON_CREATE + Lifecycle.Event.ON_CREATE -> Lifecycle.Event.ON_START + Lifecycle.Event.ON_START -> Lifecycle.Event.ON_RESUME + Lifecycle.Event.ON_RESUME -> Lifecycle.Event.ON_PAUSE + Lifecycle.Event.ON_PAUSE -> Lifecycle.Event.ON_STOP + Lifecycle.Event.ON_STOP -> Lifecycle.Event.ON_DESTROY + Lifecycle.Event.ON_DESTROY -> Lifecycle.Event.ON_CREATE + Lifecycle.Event.ON_ANY -> null + } + } +} + +@Suppress("DEPRECATION") +public class EmbraceResources( + resources: Resources, + appId: String?, + sdkConfig: String? +) : Resources( + resources.assets, + resources.displayMetrics, + resources.configuration +) { + + private val mapping = BuildInfoHooks.getResourceValues(appId, sdkConfig) + + @SuppressWarnings("DiscouragedApi") + override fun getIdentifier(name: String?, defType: String?, defPackage: String?): Int { + return name.hashCode() + } + + override fun getString(id: Int): String { + return mapping[id] ?: "" + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceFileObserver.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceFileObserver.kt new file mode 100644 index 0000000000..878f6cacfc --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/EmbraceFileObserver.kt @@ -0,0 +1,22 @@ +package io.embrace.android.embracesdk + +import android.os.FileObserver +import java.util.concurrent.CountDownLatch + +@Suppress("DEPRECATION") +public class EmbraceFileObserver( + path: String, + mask: Int, +) : FileObserver(path, mask) { + + private lateinit var startSignal: CountDownLatch + + public fun startWatching(startSignal: CountDownLatch) { + this.startSignal = startSignal + startWatching() + } + + override fun onEvent(p0: Int, p1: String?) { + startSignal.countDown() + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/FakePackageManager.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/FakePackageManager.kt new file mode 100644 index 0000000000..2ae01d2300 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/FakePackageManager.kt @@ -0,0 +1,460 @@ +package io.embrace.android.embracesdk + +import android.content.ComponentName +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.ChangedPackages +import android.content.pm.FeatureInfo +import android.content.pm.InstrumentationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.content.pm.PermissionGroupInfo +import android.content.pm.PermissionInfo +import android.content.pm.ProviderInfo +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.content.pm.SharedLibraryInfo +import android.content.pm.VersionedPackage +import android.content.res.Resources +import android.content.res.XmlResourceParser +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.os.UserHandle + +internal class FakePackageManager(private val embraceContext: EmbraceContext) : PackageManager() { + + override fun getPackageInfo(packageName: String, flags: Int): PackageInfo { + return PackageInfo().apply { + this.packageName = embraceContext.packageName + versionName = "1.1.2" + versionCode = 5 + } + } + + override fun getPackageInfo(versionedPackage: VersionedPackage, flags: Int): PackageInfo { + throw UnsupportedOperationException() + } + + override fun currentToCanonicalPackageNames(packageNames: Array): Array { + throw UnsupportedOperationException() + } + + override fun canonicalToCurrentPackageNames(packageNames: Array): Array { + throw UnsupportedOperationException() + } + + override fun getLaunchIntentForPackage(packageName: String): Intent? { + throw UnsupportedOperationException() + } + + override fun getLeanbackLaunchIntentForPackage(packageName: String): Intent? { + throw UnsupportedOperationException() + } + + override fun getPackageGids(packageName: String): IntArray { + throw UnsupportedOperationException() + } + + override fun getPackageGids(packageName: String, flags: Int): IntArray { + throw UnsupportedOperationException() + } + + override fun getPackageUid(packageName: String, flags: Int): Int { + throw UnsupportedOperationException() + } + + override fun getPermissionInfo(permName: String, flags: Int): PermissionInfo { + throw UnsupportedOperationException() + } + + override fun queryPermissionsByGroup( + permissionGroup: String?, + flags: Int + ): MutableList { + throw UnsupportedOperationException() + } + + override fun getPermissionGroupInfo(groupName: String, flags: Int): PermissionGroupInfo { + throw UnsupportedOperationException() + } + + override fun getAllPermissionGroups(flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun getApplicationInfo(packageName: String, flags: Int): ApplicationInfo { + throw UnsupportedOperationException() + } + + override fun getActivityInfo(component: ComponentName, flags: Int): ActivityInfo { + throw UnsupportedOperationException() + } + + override fun getReceiverInfo(component: ComponentName, flags: Int): ActivityInfo { + throw UnsupportedOperationException() + } + + override fun getServiceInfo(component: ComponentName, flags: Int): ServiceInfo { + throw UnsupportedOperationException() + } + + override fun getProviderInfo(component: ComponentName, flags: Int): ProviderInfo { + throw UnsupportedOperationException() + } + + override fun getInstalledPackages(flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun getPackagesHoldingPermissions( + permissions: Array, + flags: Int + ): MutableList { + throw UnsupportedOperationException() + } + + override fun checkPermission(permName: String, packageName: String): Int { + throw UnsupportedOperationException() + } + + override fun isPermissionRevokedByPolicy(permName: String, packageName: String): Boolean { + throw UnsupportedOperationException() + } + + override fun addPermission(info: PermissionInfo): Boolean { + throw UnsupportedOperationException() + } + + override fun addPermissionAsync(info: PermissionInfo): Boolean { + throw UnsupportedOperationException() + } + + override fun removePermission(permName: String) { + throw UnsupportedOperationException() + } + + override fun checkSignatures(packageName1: String, packageName2: String): Int { + throw UnsupportedOperationException() + } + + override fun checkSignatures(uid1: Int, uid2: Int): Int { + throw UnsupportedOperationException() + } + + override fun getPackagesForUid(uid: Int): Array? { + throw UnsupportedOperationException() + } + + override fun getNameForUid(uid: Int): String? { + throw UnsupportedOperationException() + } + + override fun getInstalledApplications(flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun isInstantApp(): Boolean { + throw UnsupportedOperationException() + } + + override fun isInstantApp(packageName: String): Boolean { + throw UnsupportedOperationException() + } + + override fun getInstantAppCookieMaxBytes(): Int { + throw UnsupportedOperationException() + } + + override fun getInstantAppCookie(): ByteArray { + throw UnsupportedOperationException() + } + + override fun clearInstantAppCookie() { + throw UnsupportedOperationException() + } + + override fun updateInstantAppCookie(cookie: ByteArray?) { + throw UnsupportedOperationException() + } + + override fun getSystemSharedLibraryNames(): Array? { + throw UnsupportedOperationException() + } + + override fun getSharedLibraries(flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun getChangedPackages(sequenceNumber: Int): ChangedPackages? { + throw UnsupportedOperationException() + } + + override fun getSystemAvailableFeatures(): Array { + throw UnsupportedOperationException() + } + + override fun hasSystemFeature(featureName: String): Boolean { + throw UnsupportedOperationException() + } + + override fun hasSystemFeature(featureName: String, version: Int): Boolean { + throw UnsupportedOperationException() + } + + override fun resolveActivity(intent: Intent, flags: Int): ResolveInfo? { + throw UnsupportedOperationException() + } + + override fun queryIntentActivities(intent: Intent, flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun queryIntentActivityOptions( + caller: ComponentName?, + specifics: Array?, + intent: Intent, + flags: Int + ): MutableList { + throw UnsupportedOperationException() + } + + override fun queryBroadcastReceivers(intent: Intent, flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun resolveService(intent: Intent, flags: Int): ResolveInfo? { + throw UnsupportedOperationException() + } + + override fun queryIntentServices(intent: Intent, flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun queryIntentContentProviders(intent: Intent, flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun resolveContentProvider(authority: String, flags: Int): ProviderInfo? { + throw UnsupportedOperationException() + } + + override fun queryContentProviders( + processName: String?, + uid: Int, + flags: Int + ): MutableList { + throw UnsupportedOperationException() + } + + override fun getInstrumentationInfo(className: ComponentName, flags: Int): InstrumentationInfo { + throw UnsupportedOperationException() + } + + override fun queryInstrumentation( + targetPackage: String, + flags: Int + ): MutableList { + throw UnsupportedOperationException() + } + + override fun getDrawable( + packageName: String, + resid: Int, + appInfo: ApplicationInfo? + ): Drawable? { + throw UnsupportedOperationException() + } + + override fun getActivityIcon(activityName: ComponentName): Drawable { + throw UnsupportedOperationException() + } + + override fun getActivityIcon(intent: Intent): Drawable { + throw UnsupportedOperationException() + } + + override fun getActivityBanner(activityName: ComponentName): Drawable? { + throw UnsupportedOperationException() + } + + override fun getActivityBanner(intent: Intent): Drawable? { + throw UnsupportedOperationException() + } + + override fun getDefaultActivityIcon(): Drawable { + throw UnsupportedOperationException() + } + + override fun getApplicationIcon(info: ApplicationInfo): Drawable { + throw UnsupportedOperationException() + } + + override fun getApplicationIcon(packageName: String): Drawable { + throw UnsupportedOperationException() + } + + override fun getApplicationBanner(info: ApplicationInfo): Drawable? { + throw UnsupportedOperationException() + } + + override fun getApplicationBanner(packageName: String): Drawable? { + throw UnsupportedOperationException() + } + + override fun getActivityLogo(activityName: ComponentName): Drawable? { + throw UnsupportedOperationException() + } + + override fun getActivityLogo(intent: Intent): Drawable? { + throw UnsupportedOperationException() + } + + override fun getApplicationLogo(info: ApplicationInfo): Drawable? { + throw UnsupportedOperationException() + } + + override fun getApplicationLogo(packageName: String): Drawable? { + throw UnsupportedOperationException() + } + + override fun getUserBadgedIcon(drawable: Drawable, user: UserHandle): Drawable { + throw UnsupportedOperationException() + } + + override fun getUserBadgedDrawableForDensity( + drawable: Drawable, + user: UserHandle, + badgeLocation: Rect?, + badgeDensity: Int + ): Drawable { + throw UnsupportedOperationException() + } + + override fun getUserBadgedLabel(label: CharSequence, user: UserHandle): CharSequence { + throw UnsupportedOperationException() + } + + override fun getText( + packageName: String, + resid: Int, + appInfo: ApplicationInfo? + ): CharSequence? { + throw UnsupportedOperationException() + } + + override fun getXml( + packageName: String, + resid: Int, + appInfo: ApplicationInfo? + ): XmlResourceParser? { + throw UnsupportedOperationException() + } + + override fun getApplicationLabel(info: ApplicationInfo): CharSequence { + throw UnsupportedOperationException() + } + + override fun getResourcesForActivity(activityName: ComponentName): Resources { + throw UnsupportedOperationException() + } + + override fun getResourcesForApplication(app: ApplicationInfo): Resources { + throw UnsupportedOperationException() + } + + override fun getResourcesForApplication(packageName: String): Resources { + throw UnsupportedOperationException() + } + + override fun verifyPendingInstall(id: Int, verificationCode: Int) { + throw UnsupportedOperationException() + } + + override fun extendVerificationTimeout( + id: Int, + verificationCodeAtTimeout: Int, + millisecondsToDelay: Long + ) { + throw UnsupportedOperationException() + } + + override fun setInstallerPackageName(targetPackage: String, installerPackageName: String?) { + throw UnsupportedOperationException() + } + + override fun getInstallerPackageName(packageName: String): String? { + throw UnsupportedOperationException() + } + + override fun addPackageToPreferred(packageName: String) { + throw UnsupportedOperationException() + } + + override fun removePackageFromPreferred(packageName: String) { + throw UnsupportedOperationException() + } + + override fun getPreferredPackages(flags: Int): MutableList { + throw UnsupportedOperationException() + } + + override fun addPreferredActivity( + filter: IntentFilter, + match: Int, + set: Array?, + activity: ComponentName + ) { + throw UnsupportedOperationException() + } + + override fun clearPackagePreferredActivities(packageName: String) { + throw UnsupportedOperationException() + } + + override fun getPreferredActivities( + outFilters: MutableList, + outActivities: MutableList, + packageName: String? + ): Int { + throw UnsupportedOperationException() + } + + override fun setComponentEnabledSetting( + componentName: ComponentName, + newState: Int, + flags: Int + ) { + throw UnsupportedOperationException() + } + + override fun getComponentEnabledSetting(componentName: ComponentName): Int { + throw UnsupportedOperationException() + } + + override fun setApplicationEnabledSetting(packageName: String, newState: Int, flags: Int) { + throw UnsupportedOperationException() + } + + override fun getApplicationEnabledSetting(packageName: String): Int { + throw UnsupportedOperationException() + } + + override fun isSafeMode(): Boolean { + throw UnsupportedOperationException() + } + + override fun setApplicationCategoryHint(packageName: String, categoryHint: Int) { + throw UnsupportedOperationException() + } + + override fun getPackageInstaller(): PackageInstaller { + throw UnsupportedOperationException() + } + + override fun canRequestPackageInstalls(): Boolean { + throw UnsupportedOperationException() + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/TestServer.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/TestServer.kt new file mode 100644 index 0000000000..0ba55ca047 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/TestServer.kt @@ -0,0 +1,79 @@ +package io.embrace.android.embracesdk + +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import java.net.HttpURLConnection +import java.util.concurrent.TimeUnit + +/** + * Server used to mock responses of different endpoints. + * It wraps MockWebServer and uses it to act as a mock server. + * To work properly, the sdk needs to set its baseUrl to the one returned by this class. + */ +public class TestServer { + private lateinit var mockWebServer: MockWebServer + private lateinit var mockNetworkResponses: MutableMap + + public fun start(responses: Map) { + mockWebServer = MockWebServer() + mockNetworkResponses = responses.toMutableMap() + setDispatcher() + mockWebServer.start() + } + + public fun stop() { + mockWebServer.shutdown() + } + + /** + * Gets the baseUrl that is needed to be set in the SDK so that it works with this test server. + */ + public fun getBaseUrl(): String = mockWebServer.url("").toString().removeSuffix("/") + + /** + * Gets the next request to be consumed. If there is no request, it will return null + * after REQUEST_TIMEOUT_SECONDS. + */ + public fun takeRequest(): RecordedRequest? { + return mockWebServer.takeRequest(REQUEST_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS) + } + + /** + * Adds a mocked response to be returned when calling a specific endpoint. + */ + public fun addResponse(endpoint: EmbraceEndpoint, response: TestServerResponse) { + mockNetworkResponses[endpoint.url] = response + } + + /** + * Returns a specific response for each endpoint. + */ + private fun setDispatcher() { + mockWebServer.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + val endpoint = request.path?.substringBefore("?") + val testServerResponse = + mockNetworkResponses[endpoint] + ?: TestServerResponse(HttpURLConnection.HTTP_NOT_FOUND) + + return testServerResponse.toMockWebServerResponse() + } + } + } +} + +/** + * Mock network response to be delivered when calling an endpoint. + */ +public data class TestServerResponse(val statusCode: Int, val body: String = "") { + public fun toMockWebServerResponse(): MockResponse { + + return MockResponse().setResponseCode(statusCode).also { + if (body.isNotEmpty()) it.setBody(body) + } + } +} + +public const val REQUEST_TIMEOUT_MILLISECONDS: Long = 60000L diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockActivity.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockActivity.kt new file mode 100644 index 0000000000..02743b9a64 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockActivity.kt @@ -0,0 +1,33 @@ +@file:Suppress("DEPRECATION") + +package io.embrace.android.embracesdk.internal + +import android.app.FragmentManager +import android.content.Context +import android.view.Window +import androidx.appcompat.app.AppCompatActivity +import io.embrace.android.embracesdk.EmbraceContext + +/** + * Used to Mock an Activity instance, which will return the Mocked FragmentManager + */ +public class MockActivity(private val context: EmbraceContext) : AppCompatActivity() { + private val fragmentManager = MockFragmentManager(this) + private val mockView = MockView(context) + public fun setContext(context: Context) { + this.attachBaseContext(context) + } + + @Deprecated("Deprecated in Java") + override fun getFragmentManager(): FragmentManager { + return fragmentManager + } + + override fun getLocalClassName(): String { + return "io.embrace.android.embracesdk.TestActivity" + } + + override fun getWindow(): Window { + return MockWindow(context, mockView) + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockFragmentManager.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockFragmentManager.kt new file mode 100644 index 0000000000..17a3fe6fea --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockFragmentManager.kt @@ -0,0 +1,122 @@ +@file:Suppress("DEPRECATION") + +package io.embrace.android.embracesdk.internal + +import android.app.Activity +import android.app.Fragment +import android.app.FragmentManager +import android.app.FragmentTransaction +import android.os.Bundle +import androidx.lifecycle.MockReportFragment +import androidx.lifecycle.ReportFragment +import java.io.FileDescriptor +import java.io.PrintWriter + +/** + * Used to mock a FragmentManager instance in order to inject a Mock ReportFragment + */ +public class MockFragmentManager(private val activity: Activity) : FragmentManager() { + public var reportFragment: ReportFragment = MockReportFragment().apply { + this.activity + } + + override fun saveFragmentInstanceState(f: Fragment?): Fragment.SavedState { + TODO("Not yet implemented") + } + + override fun findFragmentById(id: Int): Fragment { + TODO("Not yet implemented") + } + + override fun getFragments(): MutableList { + TODO("Not yet implemented") + } + + override fun beginTransaction(): FragmentTransaction { + TODO("Not yet implemented") + } + + override fun putFragment(bundle: Bundle?, key: String?, fragment: Fragment?) { + TODO("Not yet implemented") + } + + override fun removeOnBackStackChangedListener(listener: OnBackStackChangedListener?) { + TODO("Not yet implemented") + } + + override fun getFragment(bundle: Bundle?, key: String?): Fragment { + TODO("Not yet implemented") + } + + override fun unregisterFragmentLifecycleCallbacks(cb: FragmentLifecycleCallbacks?) { + TODO("Not yet implemented") + } + + override fun getPrimaryNavigationFragment(): Fragment { + TODO("Not yet implemented") + } + + override fun getBackStackEntryCount(): Int { + TODO("Not yet implemented") + } + + override fun isDestroyed(): Boolean { + TODO("Not yet implemented") + } + + override fun getBackStackEntryAt(index: Int): BackStackEntry { + TODO("Not yet implemented") + } + + override fun executePendingTransactions(): Boolean { + TODO("Not yet implemented") + } + + override fun popBackStackImmediate(): Boolean { + TODO("Not yet implemented") + } + + override fun popBackStackImmediate(name: String?, flags: Int): Boolean { + TODO("Not yet implemented") + } + + override fun popBackStackImmediate(id: Int, flags: Int): Boolean { + TODO("Not yet implemented") + } + + override fun findFragmentByTag(tag: String?): Fragment { + if (tag == "androidx.lifecycle.LifecycleDispatcher.report_fragment_tag") { + return reportFragment + } else { + return Fragment() + } + } + + override fun addOnBackStackChangedListener(listener: OnBackStackChangedListener?) { + TODO("Not yet implemented") + } + + override fun dump(prefix: String?, fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + TODO("Not yet implemented") + } + + override fun isStateSaved(): Boolean { + TODO("Not yet implemented") + } + + override fun popBackStack() { + TODO("Not yet implemented") + } + + override fun popBackStack(name: String?, flags: Int) { + TODO("Not yet implemented") + } + + override fun popBackStack(id: Int, flags: Int) { + TODO("Not yet implemented") + } + + override fun registerFragmentLifecycleCallbacks(cb: FragmentLifecycleCallbacks?, recursive: Boolean) { + TODO("Not yet implemented") + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockView.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockView.kt new file mode 100644 index 0000000000..d093cea9fa --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockView.kt @@ -0,0 +1,20 @@ +package io.embrace.android.embracesdk.internal + +import android.graphics.Canvas +import android.view.View +import io.embrace.android.embracesdk.EmbraceContext + +public class MockView(public val context: EmbraceContext) : View(context) { + + init { + right = context.screenshotBitmap.width + left = 0 + top = 0 + bottom = context.screenshotBitmap.height + } + + @SuppressWarnings("MissingSuperCall") + override fun draw(canvas: Canvas?) { + canvas?.setBitmap(context.screenshotBitmap) + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockWindow.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockWindow.kt new file mode 100644 index 0000000000..5dcee4386f --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/MockWindow.kt @@ -0,0 +1,113 @@ +package io.embrace.android.embracesdk.internal + +import android.content.Context +import android.content.res.Configuration +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.view.InputQueue +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.SurfaceHolder +import android.view.View +import android.view.ViewGroup +import android.view.Window + +public class MockWindow(context: Context, public val mockView: View) : Window(context) { + override fun superDispatchTrackballEvent(event: MotionEvent?): Boolean = false + + override fun setNavigationBarColor(color: Int) {} + + override fun onConfigurationChanged(newConfig: Configuration?) {} + + override fun peekDecorView(): View? = null + + override fun setFeatureDrawableUri(featureId: Int, uri: Uri?) {} + + override fun setVolumeControlStream(streamType: Int) {} + + override fun setBackgroundDrawable(drawable: Drawable?) {} + + override fun takeKeyEvents(get: Boolean) {} + + override fun getNavigationBarColor(): Int = 0 + + override fun superDispatchGenericMotionEvent(event: MotionEvent?): Boolean = false + + override fun superDispatchKeyEvent(event: KeyEvent?): Boolean = false + + override fun getLayoutInflater(): LayoutInflater = LayoutInflater.from(context) + + override fun performContextMenuIdentifierAction(id: Int, flags: Int): Boolean = false + + override fun setStatusBarColor(color: Int) {} + + override fun togglePanel(featureId: Int, event: KeyEvent?) {} + + override fun performPanelIdentifierAction(featureId: Int, id: Int, flags: Int): Boolean = false + + override fun closeAllPanels() {} + + override fun superDispatchKeyShortcutEvent(event: KeyEvent?): Boolean = false + + override fun superDispatchTouchEvent(event: MotionEvent?): Boolean = false + + override fun setDecorCaptionShade(decorCaptionShade: Int) {} + + override fun takeInputQueue(callback: InputQueue.Callback?) {} + + override fun setResizingCaptionDrawable(drawable: Drawable?) {} + + override fun performPanelShortcut(featureId: Int, keyCode: Int, event: KeyEvent?, flags: Int): Boolean = false + + override fun setFeatureDrawable(featureId: Int, drawable: Drawable?) {} + + override fun saveHierarchyState(): Bundle? = null + + override fun addContentView(view: View?, params: ViewGroup.LayoutParams?) {} + + override fun invalidatePanelMenu(featureId: Int) {} + + override fun setTitle(title: CharSequence?) {} + + override fun setChildDrawable(featureId: Int, drawable: Drawable?) {} + + override fun closePanel(featureId: Int) {} + + override fun restoreHierarchyState(savedInstanceState: Bundle?) {} + + override fun onActive() {} + + override fun getDecorView(): View = mockView + + override fun setTitleColor(textColor: Int) {} + + override fun setContentView(layoutResID: Int) {} + + override fun setContentView(view: View?) {} + + override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) {} + + override fun getVolumeControlStream(): Int = 0 + + override fun getCurrentFocus(): View? = null + + override fun getStatusBarColor(): Int = 0 + + override fun isShortcutKey(keyCode: Int, event: KeyEvent?): Boolean = false + + override fun setFeatureDrawableAlpha(featureId: Int, alpha: Int) {} + + override fun isFloating(): Boolean = false + + override fun setFeatureDrawableResource(featureId: Int, resId: Int) {} + + override fun setFeatureInt(featureId: Int, value: Int) {} + + override fun setChildInt(featureId: Int, value: Int) {} + + override fun takeSurface(callback: SurfaceHolder.Callback2?) {} + + override fun openPanel(featureId: Int, event: KeyEvent?) {} +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/PauseProcessListener.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/PauseProcessListener.kt new file mode 100644 index 0000000000..75789cc86b --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/internal/PauseProcessListener.kt @@ -0,0 +1,21 @@ +@file:Suppress("DEPRECATION") + +package io.embrace.android.embracesdk.internal + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent + +/** + * Simple callback that can be registered for ON_PAUSE events in the LifecycleProcessOwner + */ +public class PauseProcessListener : LifecycleObserver { + + public var onPauseCallback: (() -> Unit)? = null + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public fun onPauseCalled() { + onPauseCallback?.invoke() + onPauseCallback = null + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/BitmapFactory.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/BitmapFactory.kt new file mode 100644 index 0000000000..25841f02f1 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/BitmapFactory.kt @@ -0,0 +1,28 @@ +package io.embrace.android.embracesdk.utils + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import java.io.ByteArrayOutputStream +import java.util.Random + +public object BitmapFactory { + public fun newRandomBitmap(width: Int, height: Int): Bitmap { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val paint = Paint() + paint.setColor(Math.abs(Random().nextInt())) + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) + return bitmap + } +} + +public fun Bitmap.compress(): ByteArray { + val stream = ByteArrayOutputStream() + try { + compress(Bitmap.CompressFormat.JPEG, 70, stream) + } catch (e: OutOfMemoryError) { + return ByteArray(0) + } + return stream.toByteArray() +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/FailureLatch.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/FailureLatch.kt new file mode 100644 index 0000000000..375c8c3164 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/FailureLatch.kt @@ -0,0 +1,40 @@ +package io.embrace.android.embracesdk.utils + +import android.os.Handler +import android.os.Looper +import org.junit.Assert.fail +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * important to run fail() calls on the testing thread. JUnit doesn't always pick up on test failures + * from background threads, which request might be coming in on, so you could end up with false positives + */ +public class FailureLatch( + public var timeout: Long, + public val testingHandler: Handler = Handler(Looper.getMainLooper()) +) : CountDownLatch(1) { + public var timedOut: Boolean = false + + @Volatile + private var finished = false + + override fun countDown() { + finished = true + super.countDown() + } + + @Throws(InterruptedException::class) + override fun await() { + if (finished) { + return + } + // this should never finish. Either FailureLatch#countDown() is called, and the + // timeoutRunnable is removed from the handler, or the timeoutRunnable executes, failing + // the test + this.await(timeout, TimeUnit.MILLISECONDS) + if (!finished) { + fail("timed out. More than ${timeout}ms have elapsed") + } + } +} diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/JsonValidator.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/JsonValidator.kt new file mode 100644 index 0000000000..ef762ddc72 --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/JsonValidator.kt @@ -0,0 +1,146 @@ +package io.embrace.android.embracesdk.utils + +import android.util.Log +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import java.io.InputStream +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets + +/** + * Performs a comparison between two Json sources. + */ +internal object JsonValidator { + + /** + * Compares a Json obtained from an InputStream and another one from a String. + */ + fun areEquals(expected: InputStream?, observed: String): Boolean { + val expectedText = InputStreamReader(expected, StandardCharsets.UTF_8).readText() + return areJsonStringEquals(expectedText, observed) + } + + /** + * Compares two json strings. + */ + private fun areJsonStringEquals(expected: String, observed: String): Boolean { + val expectedJson = JsonParser.parseString(expected) + val observedJson = JsonParser.parseString(observed) + Log.i( + JsonValidator.javaClass.simpleName, + "Expected JSON: $expectedJson. \n Actual JSON: $observedJson" + ) + + return areJsonElementsEquals(expectedJson, observedJson) + } + + private fun areJsonElementsEquals( + jsonElement1: JsonElement, + jsonElement2: JsonElement + ): Boolean { + when { + jsonElement1.isIgnored() || jsonElement2.isIgnored() -> return true + jsonElement1.isJsonObject && jsonElement2.isJsonObject -> { + return areJsonObjectsEquals(jsonElement1.asJsonObject, jsonElement2.asJsonObject) + } + jsonElement1.isJsonArray && jsonElement2.isJsonArray -> { + return areJsonArraysEquals(jsonElement1.asJsonArray, jsonElement2.asJsonArray) + } + jsonElement1.isJsonPrimitive && jsonElement2.isJsonPrimitive -> { + return areJsonPrimitiveEquals( + jsonElement1.asJsonPrimitive, jsonElement2.asJsonPrimitive + ) + } + jsonElement1.isJsonNull && jsonElement2.isJsonNull -> { + return true + } + else -> return false + } + } + + /** + * Two JsonObjects are equals if each element in the EntrySet is equal to the one + * in the other JsonObject. + */ + private fun areJsonObjectsEquals(jsonObject1: JsonObject, jsonObject2: JsonObject): Boolean { + val entrySet1 = jsonObject1.entrySet() + val entrySet2 = jsonObject2.entrySet() + + if (entrySet1 != null && entrySet2 != null && entrySet2.size == entrySet1.size) { + entrySet1.forEachIndexed { _, entry -> + try { + Log.e(JsonValidator.javaClass.simpleName, "entry.key " + entry.key) + jsonObject2.get(entry.key) + } catch (ex: Exception) { + Log.e(JsonValidator.javaClass.simpleName, "entry.key " + entry.key) + } + try { + if (!areJsonElementsEquals(entry.value, jsonObject2.get(entry.key))) { + Log.e( + JsonValidator.javaClass.simpleName, "Match failed for key: ${entry.key}" + ) + return false + } + } catch (ex: Exception) { + Log.e(JsonValidator.javaClass.simpleName, "entry.key " + entry.key) + return false + } + } + return true + } else { + Log.e(JsonValidator.javaClass.simpleName, "Different entry set size.") + Log.e(JsonValidator.javaClass.simpleName, "expected jsonObject1: " + jsonObject1.size() + " " + jsonObject1) + Log.e(JsonValidator.javaClass.simpleName, "observed jsonObject2: " + jsonObject2.size() + " " + jsonObject2) + return false + } + } + + /** + * Two JsonArrays are equal if each element in the array is equal to the one in the other array. + * Arrays ordered in a different way are considered different. + */ + private fun areJsonArraysEquals(jsonArray1: JsonArray, jsonArray2: JsonArray): Boolean { + if (jsonArray1.size() != jsonArray2.size()) { + Log.e(JsonValidator.javaClass.simpleName, "Different arrays size.") + return false + } + + jsonArray1.forEachIndexed { index, entry -> + if (!areJsonElementsEquals(entry, jsonArray2.get(index))) { + Log.e( + JsonValidator.javaClass.simpleName, "Different array value at position: $index." + ) + return false + } + } + return true + } + + /** + * Compares two JsonPrimitives. + */ + private fun areJsonPrimitiveEquals( + jsonPrimitive1: JsonPrimitive, + jsonPrimitive2: JsonPrimitive + ): Boolean { + val arePrimitiveEquals = jsonPrimitive1 == jsonPrimitive2 + if (!arePrimitiveEquals) { + Log.e( + JsonValidator.javaClass.simpleName, + "Different values. Expected: ${jsonPrimitive1.asJsonPrimitive} " + + "Actual: ${jsonPrimitive2.asJsonPrimitive}" + ) + } + return arePrimitiveEquals + } +} + +public fun JsonElement.isIgnored(): Boolean = + this.isJsonPrimitive && + this.asJsonPrimitive.isString && + this.asJsonPrimitive.asString == IGNORE_KEYWORD + +private const val IGNORE_KEYWORD = "__EMBRACE_TEST_IGNORE__" diff --git a/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/Mutable.kt b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/Mutable.kt new file mode 100644 index 0000000000..b9ef308fab --- /dev/null +++ b/test-server/src/main/kotlin/io/embrace/android/embracesdk/utils/Mutable.kt @@ -0,0 +1,6 @@ +package io.embrace.android.embracesdk.utils + +/** + * Simple wrapper class for accessing mutable fields across Threads during tests + */ +public class Mutable(@JvmField public var value: T)