diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67fe01bbc0..bf59ab8565 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,24 @@ jobs: working-directory: OneSignalSDK run: | ./gradlew detekt --console=plain + # Explicit paths: a bare `apiCheck` silently skips any module that stops registering the task. + - name: "[Public API] Binary compatibility check" + id: api_check + working-directory: OneSignalSDK + run: | + ./gradlew \ + :OneSignal:apiCheck \ + :OneSignal:core:apiCheck \ + :OneSignal:notifications:apiCheck \ + :OneSignal:in-app-messages:apiCheck \ + :OneSignal:location:apiCheck \ + --console=plain + - name: "[Public API] Explain a failure" + if: failure() && steps.api_check.outcome == 'failure' + run: | + echo "If apiCheck failed, the public API changed. Review the diff it printed." + echo "If the change is intended, run './gradlew apiDump' in OneSignalSDK and commit the updated api/*.api files." + echo "If the new lines are internal (not customer or wrapper API), add them to ignoredPackages or ignoredClasses in OneSignalSDK/build.gradle instead." test: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d8c995c958..c134363a88 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ local.properties OneSignalSDK/local.properties examples/demo/local.properties +google-services.json # Google services google-services.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 049ac834b6..7d5c8f4020 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,29 @@ flowchart TD The only manual actions in Flow B are **Run workflow** and **merge the bump PR**; everything in between is automated. +### Public API changes + +The published modules track their public API in committed dumps under `/api/*.api`, generated by the [binary compatibility validator](https://github.com/Kotlin/binary-compatibility-validator). CI runs `apiCheck` alongside Spotless and Detekt, so any change to the public surface fails the build until the dump is regenerated. The point is that a breaking change has to appear as a reviewable diff rather than be caught by eye. + +If you change the public API on purpose, regenerate and commit the dumps: + +```bash +cd OneSignalSDK +./gradlew apiDump +``` + +Then read the resulting diff before committing it. A removed or altered line is a breaking change for customers. An added line is usually safe, with one important exception: a new method on an interface customers implement (listeners such as `INotificationClickListener` or `IUserJwtInvalidatedListener`) breaks every existing implementation. + +Two things about the dumps are worth knowing up front: + +- They cover customer and wrapper-SDK API only. Kotlin has no cross-module "internal", so `com.onesignal.**.internal.*` and most of `com.onesignal.common.*` compile as public; `apiValidation` in `OneSignalSDK/build.gradle` excludes them so internal refactors don't trip `apiCheck`. `OneSignalWrapper`, `IHostPermissionPrompt`, and `debug.internal.logging.Logging` stay in because the wrapper SDKs (Flutter, React Native, Cordova, Capacitor, Unity, .NET) call them, so treat them as public API. If you add a new internal package, or a new class at the root of `com.onesignal.common`, add it to `ignoredPackages` or `ignoredClasses` rather than dumping it. +- `in-app-messages.api` and `location.api` are empty because their customer-facing interfaces live in `core`. That is expected, not a misconfiguration. `notifications.api` also records manifest components (receivers, activities, HMS/ADM services). +- `Logging.setLoggerTelemetry` takes a KMP type, so a KMP submodule bump (Flow B above) that changes `ILogTelemetryRemote` will fail `apiCheck`. Run `apiDump` on the bump PR if so. + +The validator hooks into the `kotlin-android` plugin, and CI calls each module's `apiCheck` by path. If a module moves to AGP 9's built-in Kotlin, its task stops being registered and CI fails with "task not found" rather than skipping it. See [BCV#312](https://github.com/Kotlin/binary-compatibility-validator/issues/312). + +The eventual replacement is the [ABI validation built into the Kotlin Gradle plugin](https://kotlinlang.org/docs/gradle-binary-compatibility-validation.html) (`kotlin { abiValidation { } }`, experimental since Kotlin 2.2). It keeps the same `api/*.api` dump format, so the move is mostly porting the `apiValidation` filters. It needs Kotlin 2.2+, so it is planned alongside the AGP 9 move for v6. + #### Before Submitting A Bug Report Before creating bug reports, please check this list of steps to follow. diff --git a/OneSignalSDK/build.gradle b/OneSignalSDK/build.gradle index 6a29fc4016..baa5bd7a49 100644 --- a/OneSignalSDK/build.gradle +++ b/OneSignalSDK/build.gradle @@ -10,7 +10,7 @@ buildscript { ] androidGradlePluginVersion = '8.8.2' detektVersion = '1.21.0' - googleServicesGradlePluginVersion = '4.3.10' + googleServicesGradlePluginVersion = '4.4.2' huaweiAgconnectVersion = '1.9.1.304' huaweiHMSPushVersion = '6.3.0.304' huaweiHMSLocationVersion = '4.0.0.300' @@ -26,6 +26,9 @@ buildscript { startupVersion = '1.1.1' ktlintVersion = '0.50.0' // Used by Spotless for Kotlin formatting (compatible with Kotlin 1.7.10) spotlessVersion = '6.25.0' + // BCV hooks into 'kotlin-android', so AGP 9's built-in Kotlin stops it registering tasks. + // See https://github.com/Kotlin/binary-compatibility-validator/issues/312 + binaryCompatibilityValidatorVersion = '0.16.3' tdunningJsonForTest = '1.0' // DO NOT upgrade for tests, using an old version so it matches AOSP sharedRepos = { @@ -44,7 +47,8 @@ buildscript { "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion", "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion", "com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion", - "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin:0.32.0" + "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin:0.32.0", + "org.jetbrains.kotlinx:binary-compatibility-validator:$binaryCompatibilityValidatorVersion" ] } @@ -54,6 +58,58 @@ buildscript { } } +apply plugin: 'org.jetbrains.kotlinx.binary-compatibility-validator' + +// Tracks the public API of the published modules as committed `api/*.api` dumps, so a breaking +// change has to show up as a reviewable diff instead of being spotted by eye. +apiValidation { + // Not published, so they have no customer-facing surface to protect. + ignoredProjects += ['app', 'testhelpers'] + // :kmp is a git submodule; its API is owned by OneSignal-KMP-SDK. + ignoredProjects += ['kmp'] + + // Public only because Kotlin has no cross-module internal. Subpackages are ignored too. + ignoredPackages += [ + 'com.onesignal.internal', + 'com.onesignal.core.internal', + // Not all of debug.internal: wrapper SDKs call debug.internal.logging.Logging. + 'com.onesignal.debug.internal.crash', + 'com.onesignal.debug.internal.logging.logger', + 'com.onesignal.inAppMessages.internal', + 'com.onesignal.location.internal', + 'com.onesignal.notifications.internal', + 'com.onesignal.session.internal', + 'com.onesignal.user.internal', + 'com.onesignal.common.consistency', + 'com.onesignal.common.events', + 'com.onesignal.common.exceptions', + 'com.onesignal.common.modeling', + 'com.onesignal.common.modules', + 'com.onesignal.common.services', + 'com.onesignal.common.threading', + ] + // Wrapper SDKs call OneSignalWrapper and IHostPermissionPrompt, so the rest of + // com.onesignal.common is ignored class by class. Nested classes need their own entry. + ignoredClasses += [ + 'com.onesignal.common.AndroidUtils', + 'com.onesignal.common.AndroidUtils$SchemaType', + 'com.onesignal.common.AndroidUtils$SchemaType$Companion', + 'com.onesignal.common.DateUtils', + 'com.onesignal.common.DeviceUtils', + 'com.onesignal.common.IDManager', + 'com.onesignal.common.InputGuardKt', + 'com.onesignal.common.JSONObjectExtensionsKt', + 'com.onesignal.common.JSONUtils', + 'com.onesignal.common.NetworkUtils', + 'com.onesignal.common.NetworkUtils$ResponseStatusType', + 'com.onesignal.common.OneSignalUtils', + 'com.onesignal.common.PIIHasher', + 'com.onesignal.common.RootToolsInternalMethods', + 'com.onesignal.common.TimeUtils', + 'com.onesignal.common.ViewUtils', + ] +} + allprojects { repositories { google() diff --git a/OneSignalSDK/detekt/detekt-baseline-core.xml b/OneSignalSDK/detekt/detekt-baseline-core.xml index 8786322cfa..0326ea2c58 100644 --- a/OneSignalSDK/detekt/detekt-baseline-core.xml +++ b/OneSignalSDK/detekt/detekt-baseline-core.xml @@ -260,6 +260,9 @@ MagicNumber:SubscriptionModel.kt$SubscriptionStatus.FIREBASE_FCM_ERROR_IOEXCEPTION_OTHER$11 MagicNumber:SubscriptionModel.kt$SubscriptionStatus.FIREBASE_FCM_ERROR_IOEXCEPTION_SERVICE_NOT_AVAILABLE$9 MagicNumber:SubscriptionModel.kt$SubscriptionStatus.FIREBASE_FCM_ERROR_MISC_EXCEPTION$12 + MagicNumber:SubscriptionModel.kt$SubscriptionStatus.FIREBASE_FCM_FID_DEFAULT_APP_MISSING$32 + MagicNumber:SubscriptionModel.kt$SubscriptionStatus.FIREBASE_FCM_FID_REGISTER_API_UNAVAILABLE$33 + MagicNumber:SubscriptionModel.kt$SubscriptionStatus.FIREBASE_FCM_FID_REGISTRATION_FAILED$34 MagicNumber:SubscriptionModel.kt$SubscriptionStatus.FIREBASE_FCM_INIT_ERROR$8 MagicNumber:SubscriptionModel.kt$SubscriptionStatus.HMS_API_EXCEPTION_OTHER$27 MagicNumber:SubscriptionModel.kt$SubscriptionStatus.HMS_ARGUMENTS_INVALID$26 diff --git a/OneSignalSDK/gradle.properties b/OneSignalSDK/gradle.properties index e75aa8eeec..1ad83a5087 100644 --- a/OneSignalSDK/gradle.properties +++ b/OneSignalSDK/gradle.properties @@ -52,3 +52,6 @@ onesignalPublishSharedKmp = true # This is the name of the SDK to use when building your project. # This will be fed from the GitHub Actions workflow. SDK_VERSION=5.10.2 + +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/OneSignalSDK/onesignal/api/OneSignal.api b/OneSignalSDK/onesignal/api/OneSignal.api new file mode 100644 index 0000000000..e69de29bb2 diff --git a/OneSignalSDK/onesignal/core/api/core.api b/OneSignalSDK/onesignal/core/api/core.api new file mode 100644 index 0000000000..0a26d1da1c --- /dev/null +++ b/OneSignalSDK/onesignal/core/api/core.api @@ -0,0 +1,627 @@ +public final class com/onesignal/Continue { + public static final field INSTANCE Lcom/onesignal/Continue; + public static final fun none ()Lkotlin/coroutines/Continuation; + public static final fun with (Ljava/util/function/Consumer;)Lkotlin/coroutines/Continuation; + public static final fun with (Ljava/util/function/Consumer;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/Continuation; + public static synthetic fun with$default (Ljava/util/function/Consumer;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlin/coroutines/Continuation; +} + +public final class com/onesignal/ContinueResult { + public fun (ZLjava/lang/Object;Ljava/lang/Throwable;)V + public final fun getData ()Ljava/lang/Object; + public final fun getThrowable ()Ljava/lang/Throwable; + public final fun isSuccess ()Z +} + +public final class com/onesignal/ErrorCode : java/lang/Enum { + public static final field BACKEND_ERROR Lcom/onesignal/ErrorCode; + public static final field INVALID_ARGUMENT Lcom/onesignal/ErrorCode; + public static final field NOT_INITIALIZED Lcom/onesignal/ErrorCode; + public static final field STORAGE_LOCKED Lcom/onesignal/ErrorCode; + public static final field UNKNOWN Lcom/onesignal/ErrorCode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getSource ()Lcom/onesignal/ErrorSource; + public static fun valueOf (Ljava/lang/String;)Lcom/onesignal/ErrorCode; + public static fun values ()[Lcom/onesignal/ErrorCode; +} + +public final class com/onesignal/ErrorSource : java/lang/Enum { + public static final field BACKEND Lcom/onesignal/ErrorSource; + public static final field CLIENT Lcom/onesignal/ErrorSource; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/onesignal/ErrorSource; + public static fun values ()[Lcom/onesignal/ErrorSource; +} + +public abstract interface class com/onesignal/IOneSignal { + public abstract fun addUserJwtInvalidatedListener (Lcom/onesignal/IUserJwtInvalidatedListener;)V + public abstract fun getConsentGiven ()Z + public abstract fun getConsentGiven (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getConsentRequired ()Z + public abstract fun getConsentRequired (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getDebug ()Lcom/onesignal/debug/IDebugManager; + public abstract fun getDisableGMSMissingPrompt ()Z + public abstract fun getDisableGMSMissingPrompt (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getInAppMessages ()Lcom/onesignal/inAppMessages/IInAppMessagesManager; + public abstract fun getInAppMessages (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getLocation ()Lcom/onesignal/location/ILocationManager; + public abstract fun getLocation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getNotifications ()Lcom/onesignal/notifications/INotificationsManager; + public abstract fun getNotifications (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getSdkVersion ()Ljava/lang/String; + public abstract fun getSession ()Lcom/onesignal/session/ISessionManager; + public abstract fun getSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getUser ()Lcom/onesignal/user/IUserManager; + public abstract fun getUser (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun initWithContext (Landroid/content/Context;Ljava/lang/String;)Z + public abstract fun initWithContext (Landroid/content/Context;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun initWithContextSuspend (Landroid/content/Context;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun isInitialized ()Z + public abstract fun login (Ljava/lang/String;)V + public abstract fun login (Ljava/lang/String;Lcom/onesignal/OneSignalUserProfile;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun login (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun loginSuspend (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun logout ()V + public abstract fun logoutSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun removeUserJwtInvalidatedListener (Lcom/onesignal/IUserJwtInvalidatedListener;)V + public abstract fun setConsentGiven (Z)V + public abstract fun setConsentGiven (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun setConsentRequired (Z)V + public abstract fun setConsentRequired (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun setDisableGMSMissingPrompt (Z)V + public abstract fun setDisableGMSMissingPrompt (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun updateUserJwt (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun updateUserJwtSuspend (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class com/onesignal/IOneSignal$DefaultImpls { + public static synthetic fun initWithContextSuspend$default (Lcom/onesignal/IOneSignal;Landroid/content/Context;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static fun login (Lcom/onesignal/IOneSignal;Ljava/lang/String;)V + public static synthetic fun login$default (Lcom/onesignal/IOneSignal;Ljava/lang/String;Lcom/onesignal/OneSignalUserProfile;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun login$default (Lcom/onesignal/IOneSignal;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V + public static synthetic fun loginSuspend$default (Lcom/onesignal/IOneSignal;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public abstract interface class com/onesignal/IUserJwtInvalidatedListener { + public abstract fun onUserJwtInvalidated (Lcom/onesignal/UserJwtInvalidatedEvent;)V +} + +public final class com/onesignal/InitData : com/onesignal/OneSignalResultData { + public fun toMap ()Ljava/util/Map; + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/LoginData : com/onesignal/OneSignalResultData { + public final fun getEmailSubscriptionId ()Ljava/lang/String; + public final fun getExternalId ()Ljava/lang/String; + public final fun getOnesignalId ()Ljava/lang/String; + public final fun getSmsSubscriptionId ()Ljava/lang/String; + public fun toMap ()Ljava/util/Map; + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/LogoutData : com/onesignal/OneSignalResultData { + public fun toMap ()Ljava/util/Map; + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/OneSignal { + public static final field INSTANCE Lcom/onesignal/OneSignal; + public static final fun addUserJwtInvalidatedListener (Lcom/onesignal/IUserJwtInvalidatedListener;)V + public static final fun getConsentGiven ()Z + public static final fun getConsentGivenSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getConsentRequired ()Z + public static final fun getConsentRequiredSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getDebug ()Lcom/onesignal/debug/IDebugManager; + public static final fun getDisableGMSMissingPrompt ()Z + public static final fun getDisableGMSMissingPromptSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getInAppMessages ()Lcom/onesignal/inAppMessages/IInAppMessagesManager; + public static final fun getInAppMessagesSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getLocation ()Lcom/onesignal/location/ILocationManager; + public static final fun getLocationSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getNotifications ()Lcom/onesignal/notifications/INotificationsManager; + public static final fun getNotificationsSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getSdkVersion ()Ljava/lang/String; + public final fun getServices ()Lcom/onesignal/common/services/IServiceProvider; + public static final fun getSession ()Lcom/onesignal/session/ISessionManager; + public static final fun getSessionSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getUser ()Lcom/onesignal/user/IUserManager; + public static final fun getUserSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun initWithContext (Landroid/content/Context;Ljava/lang/String;)V + public static final fun initWithContext (Landroid/content/Context;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun initWithContextSuspend (Landroid/content/Context;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun initWithContextSuspend$default (Landroid/content/Context;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun isInitialized ()Z + public static final fun login (Ljava/lang/String;)V + public static final fun login (Ljava/lang/String;Lcom/onesignal/OneSignalUserProfile;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun login (Ljava/lang/String;Lcom/onesignal/OneSignalUserProfile;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun login (Ljava/lang/String;Ljava/lang/String;)V + public static synthetic fun login$default (Ljava/lang/String;Lcom/onesignal/OneSignalUserProfile;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun login$default (Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V + public static final fun loginSuspend (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun loginSuspend$default (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun logout ()V + public static final fun logoutSuspend (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun removeUserJwtInvalidatedListener (Lcom/onesignal/IUserJwtInvalidatedListener;)V + public static final fun setConsentGiven (Z)V + public static final fun setConsentGivenSuspend (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun setConsentRequired (Z)V + public static final fun setConsentRequiredSuspend (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun setDisableGMSMissingPrompt (Z)V + public static final fun setDisableGMSMissingPromptSuspend (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun updateUserJwt (Ljava/lang/String;Ljava/lang/String;)V + public static final fun updateUserJwtSuspend (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class com/onesignal/OneSignalError { + public synthetic fun (Ljava/util/List;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCause ()Ljava/lang/Throwable; + public final fun getDetails ()Ljava/util/List; + public final fun getFirst ()Lcom/onesignal/OneSignalError$Detail; + public final fun toList ()Ljava/util/List; + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/OneSignalError$Detail { + public synthetic fun (Lcom/onesignal/ErrorCode;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Lcom/onesignal/ErrorSource;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getBackendCode ()Ljava/lang/String; + public final fun getCode ()Lcom/onesignal/ErrorCode; + public final fun getHttpStatus ()Ljava/lang/Integer; + public final fun getMessage ()Ljava/lang/String; + public final fun getMeta ()Ljava/util/Map; + public final fun getSource ()Lcom/onesignal/ErrorSource; + public final fun toMap ()Ljava/util/Map; + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/OneSignalResult { + public synthetic fun (Lcom/onesignal/OneSignalResultData;Lcom/onesignal/OneSignalError;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getData ()Lcom/onesignal/OneSignalResultData; + public final fun getError ()Lcom/onesignal/OneSignalError; + public final fun isSuccess ()Z + public final fun toMap ()Ljava/util/Map; + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/OneSignalResultData { + public abstract fun toMap ()Ljava/util/Map; +} + +public final class com/onesignal/OneSignalUserProfile { + public static final field Companion Lcom/onesignal/OneSignalUserProfile$Companion; + public synthetic fun (Lcom/onesignal/OneSignalUserProfile$Builder;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun builder ()Lcom/onesignal/OneSignalUserProfile$Builder; + public fun equals (Ljava/lang/Object;)Z + public final fun getAliases ()Ljava/util/Map; + public final fun getEmail ()Ljava/lang/String; + public final fun getSms ()Ljava/lang/String; + public final fun getTags ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/OneSignalUserProfile$Builder { + public fun ()V + public final fun build ()Lcom/onesignal/OneSignalUserProfile; + public final fun setAliases (Ljava/util/Map;)Lcom/onesignal/OneSignalUserProfile$Builder; + public final fun setEmail (Ljava/lang/String;)Lcom/onesignal/OneSignalUserProfile$Builder; + public final fun setSms (Ljava/lang/String;)Lcom/onesignal/OneSignalUserProfile$Builder; + public final fun setTags (Ljava/util/Map;)Lcom/onesignal/OneSignalUserProfile$Builder; +} + +public final class com/onesignal/OneSignalUserProfile$Companion { + public final fun builder ()Lcom/onesignal/OneSignalUserProfile$Builder; +} + +public final class com/onesignal/SyncJobService : android/app/job/JobService { + public fun ()V + public fun onStartJob (Landroid/app/job/JobParameters;)Z + public fun onStopJob (Landroid/app/job/JobParameters;)Z +} + +public final class com/onesignal/UpdateUserJwtData : com/onesignal/OneSignalResultData { + public fun toMap ()Ljava/util/Map; + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/UserJwtInvalidatedEvent { + public fun (Ljava/lang/String;)V + public final fun getExternalId ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/common/IHostPermissionPrompt { + public abstract fun request (Ljava/lang/String;Lcom/onesignal/common/IHostPermissionPrompt$Callback;)Z +} + +public abstract interface class com/onesignal/common/IHostPermissionPrompt$Callback { + public abstract fun onResult (Z)V +} + +public final class com/onesignal/common/OneSignalWrapper { + public static final field INSTANCE Lcom/onesignal/common/OneSignalWrapper; + public static final fun getHostPermissionPrompt ()Lcom/onesignal/common/IHostPermissionPrompt; + public static final fun getSdkType ()Ljava/lang/String; + public static final fun getSdkVersion ()Ljava/lang/String; + public static final fun setHostPermissionPrompt (Lcom/onesignal/common/IHostPermissionPrompt;)V + public static final fun setSdkType (Ljava/lang/String;)V + public static final fun setSdkVersion (Ljava/lang/String;)V +} + +public final class com/onesignal/core/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 SDK_VERSION Ljava/lang/String; + public fun ()V +} + +public final class com/onesignal/core/activities/PermissionsActivity : androidx/activity/ComponentActivity { + public fun ()V + public fun onRequestPermissionsResult (I[Ljava/lang/String;[I)V +} + +public final class com/onesignal/core/services/SyncJobService : android/app/job/JobService { + public fun ()V + public fun onStartJob (Landroid/app/job/JobParameters;)Z + public fun onStopJob (Landroid/app/job/JobParameters;)Z +} + +public abstract interface class com/onesignal/debug/IDebugManager { + public abstract fun addLogListener (Lcom/onesignal/debug/ILogListener;)V + public abstract fun getAlertLevel ()Lcom/onesignal/debug/LogLevel; + public abstract fun getLogLevel ()Lcom/onesignal/debug/LogLevel; + public abstract fun removeLogListener (Lcom/onesignal/debug/ILogListener;)V + public abstract fun setAlertLevel (Lcom/onesignal/debug/LogLevel;)V + public abstract fun setLogLevel (Lcom/onesignal/debug/LogLevel;)V +} + +public abstract interface class com/onesignal/debug/ILogListener { + public abstract fun onLogEvent (Lcom/onesignal/debug/OneSignalLogEvent;)V +} + +public final class com/onesignal/debug/LogLevel : java/lang/Enum { + public static final field Companion Lcom/onesignal/debug/LogLevel$Companion; + public static final field DEBUG Lcom/onesignal/debug/LogLevel; + public static final field ERROR Lcom/onesignal/debug/LogLevel; + public static final field FATAL Lcom/onesignal/debug/LogLevel; + public static final field INFO Lcom/onesignal/debug/LogLevel; + public static final field NONE Lcom/onesignal/debug/LogLevel; + public static final field VERBOSE Lcom/onesignal/debug/LogLevel; + public static final field WARN Lcom/onesignal/debug/LogLevel; + public static final fun fromInt (I)Lcom/onesignal/debug/LogLevel; + public static final fun fromString (Ljava/lang/String;)Lcom/onesignal/debug/LogLevel; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/onesignal/debug/LogLevel; + public static fun values ()[Lcom/onesignal/debug/LogLevel; +} + +public final class com/onesignal/debug/LogLevel$Companion { + public final fun fromInt (I)Lcom/onesignal/debug/LogLevel; + public final fun fromString (Ljava/lang/String;)Lcom/onesignal/debug/LogLevel; +} + +public final class com/onesignal/debug/OneSignalLogEvent { + public fun (Lcom/onesignal/debug/LogLevel;Ljava/lang/String;)V + public final fun component1 ()Lcom/onesignal/debug/LogLevel; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Lcom/onesignal/debug/LogLevel;Ljava/lang/String;)Lcom/onesignal/debug/OneSignalLogEvent; + public static synthetic fun copy$default (Lcom/onesignal/debug/OneSignalLogEvent;Lcom/onesignal/debug/LogLevel;Ljava/lang/String;ILjava/lang/Object;)Lcom/onesignal/debug/OneSignalLogEvent; + public fun equals (Ljava/lang/Object;)Z + public final fun getEntry ()Ljava/lang/String; + public final fun getLevel ()Lcom/onesignal/debug/LogLevel; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/onesignal/debug/internal/logging/Logging { + public static final field INSTANCE Lcom/onesignal/debug/internal/logging/Logging; + public final fun addListener (Lcom/onesignal/debug/ILogListener;)V + public static final fun atLogLevel (Lcom/onesignal/debug/LogLevel;)Z + public static final fun debug (Ljava/lang/String;Ljava/lang/Throwable;)V + public static synthetic fun debug$default (Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V + public static final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V + public static synthetic fun error$default (Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V + public static final fun fatal (Ljava/lang/String;Ljava/lang/Throwable;)V + public static synthetic fun fatal$default (Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V + public final fun getApplicationService ()Lcom/onesignal/core/internal/application/IApplicationService; + public static final fun getLogLevel ()Lcom/onesignal/debug/LogLevel; + public static final fun getVisualLogLevel ()Lcom/onesignal/debug/LogLevel; + public static final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V + public static synthetic fun info$default (Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V + public static final fun log (Lcom/onesignal/debug/LogLevel;Ljava/lang/String;)V + public static final fun log (Lcom/onesignal/debug/LogLevel;Ljava/lang/String;Ljava/lang/Throwable;)V + public final fun removeListener (Lcom/onesignal/debug/ILogListener;)V + public final fun setApplicationService (Lcom/onesignal/core/internal/application/IApplicationService;)V + public static final fun setLogLevel (Lcom/onesignal/debug/LogLevel;)V + public final fun setLoggerTelemetry (Lcom/onesignal/logger/ILogTelemetryRemote;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun setLoggerTelemetry$default (Lcom/onesignal/debug/internal/logging/Logging;Lcom/onesignal/logger/ILogTelemetryRemote;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun setVisualLogLevel (Lcom/onesignal/debug/LogLevel;)V + public static final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V + public static synthetic fun verbose$default (Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V + public static final fun warn (Ljava/lang/String;Ljava/lang/Throwable;)V + public static synthetic fun warn$default (Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessage { + public abstract fun getMessageId ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageClickEvent { + public abstract fun getMessage ()Lcom/onesignal/inAppMessages/IInAppMessage; + public abstract fun getResult ()Lcom/onesignal/inAppMessages/IInAppMessageClickResult; +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageClickListener { + public abstract fun onClick (Lcom/onesignal/inAppMessages/IInAppMessageClickEvent;)V +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageClickResult { + public abstract fun getActionId ()Ljava/lang/String; + public abstract fun getClosingMessage ()Z + public abstract fun getUrl ()Ljava/lang/String; + public abstract fun getUrlTarget ()Lcom/onesignal/inAppMessages/InAppMessageActionUrlType; +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageDidDismissEvent { + public abstract fun getMessage ()Lcom/onesignal/inAppMessages/IInAppMessage; +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageDidDisplayEvent { + public abstract fun getMessage ()Lcom/onesignal/inAppMessages/IInAppMessage; +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageLifecycleListener { + public abstract fun onDidDismiss (Lcom/onesignal/inAppMessages/IInAppMessageDidDismissEvent;)V + public abstract fun onDidDisplay (Lcom/onesignal/inAppMessages/IInAppMessageDidDisplayEvent;)V + public abstract fun onWillDismiss (Lcom/onesignal/inAppMessages/IInAppMessageWillDismissEvent;)V + public abstract fun onWillDisplay (Lcom/onesignal/inAppMessages/IInAppMessageWillDisplayEvent;)V +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageWillDismissEvent { + public abstract fun getMessage ()Lcom/onesignal/inAppMessages/IInAppMessage; +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessageWillDisplayEvent { + public abstract fun getMessage ()Lcom/onesignal/inAppMessages/IInAppMessage; +} + +public abstract interface class com/onesignal/inAppMessages/IInAppMessagesManager { + public abstract fun addClickListener (Lcom/onesignal/inAppMessages/IInAppMessageClickListener;)V + public abstract fun addLifecycleListener (Lcom/onesignal/inAppMessages/IInAppMessageLifecycleListener;)V + public abstract fun addTrigger (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun addTriggers (Ljava/util/Map;)V + public abstract fun clearTriggers ()V + public abstract fun getPaused ()Z + public abstract fun removeClickListener (Lcom/onesignal/inAppMessages/IInAppMessageClickListener;)V + public abstract fun removeLifecycleListener (Lcom/onesignal/inAppMessages/IInAppMessageLifecycleListener;)V + public abstract fun removeTrigger (Ljava/lang/String;)V + public abstract fun removeTriggers (Ljava/util/Collection;)V + public abstract fun setPaused (Z)V +} + +public final class com/onesignal/inAppMessages/InAppMessageActionUrlType : java/lang/Enum { + public static final field BROWSER Lcom/onesignal/inAppMessages/InAppMessageActionUrlType; + public static final field Companion Lcom/onesignal/inAppMessages/InAppMessageActionUrlType$Companion; + public static final field IN_APP_WEBVIEW Lcom/onesignal/inAppMessages/InAppMessageActionUrlType; + public static final field REPLACE_CONTENT Lcom/onesignal/inAppMessages/InAppMessageActionUrlType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public fun toString ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lcom/onesignal/inAppMessages/InAppMessageActionUrlType; + public static fun values ()[Lcom/onesignal/inAppMessages/InAppMessageActionUrlType; +} + +public final class com/onesignal/inAppMessages/InAppMessageActionUrlType$Companion { + public final fun fromString (Ljava/lang/String;)Lcom/onesignal/inAppMessages/InAppMessageActionUrlType; +} + +public abstract interface class com/onesignal/location/ILocationManager { + public abstract fun isShared ()Z + public abstract fun requestPermission (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun setShared (Z)V +} + +public final class com/onesignal/notifications/BackgroundImageLayout { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getBodyTextColor ()Ljava/lang/String; + public final fun getImage ()Ljava/lang/String; + public final fun getTitleTextColor ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/notifications/IActionButton { + public abstract fun getIcon ()Ljava/lang/String; + public abstract fun getId ()Ljava/lang/String; + public abstract fun getText ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/notifications/IDisplayableMutableNotification : com/onesignal/notifications/IDisplayableNotification, com/onesignal/notifications/IMutableNotification { +} + +public abstract interface class com/onesignal/notifications/IDisplayableNotification : com/onesignal/notifications/INotification { + public abstract fun display ()V +} + +public abstract interface class com/onesignal/notifications/IMutableNotification : com/onesignal/notifications/INotification { + public abstract fun setExtender (Landroidx/core/app/NotificationCompat$Extender;)V +} + +public abstract interface class com/onesignal/notifications/INotification { + public abstract fun getActionButtons ()Ljava/util/List; + public abstract fun getAdditionalData ()Lorg/json/JSONObject; + public abstract fun getAndroidNotificationId ()I + public abstract fun getBackgroundImageLayout ()Lcom/onesignal/notifications/BackgroundImageLayout; + public abstract fun getBigPicture ()Ljava/lang/String; + public abstract fun getBody ()Ljava/lang/String; + public abstract fun getCollapseId ()Ljava/lang/String; + public abstract fun getFromProjectNumber ()Ljava/lang/String; + public abstract fun getGroupKey ()Ljava/lang/String; + public abstract fun getGroupMessage ()Ljava/lang/String; + public abstract fun getGroupedNotifications ()Ljava/util/List; + public abstract fun getLargeIcon ()Ljava/lang/String; + public abstract fun getLaunchURL ()Ljava/lang/String; + public abstract fun getLedColor ()Ljava/lang/String; + public abstract fun getLockScreenVisibility ()I + public abstract fun getNotificationId ()Ljava/lang/String; + public abstract fun getPriority ()I + public abstract fun getRawPayload ()Ljava/lang/String; + public abstract fun getSentTime ()J + public abstract fun getSmallIcon ()Ljava/lang/String; + public abstract fun getSmallIconAccentColor ()Ljava/lang/String; + public abstract fun getSound ()Ljava/lang/String; + public abstract fun getTemplateId ()Ljava/lang/String; + public abstract fun getTemplateName ()Ljava/lang/String; + public abstract fun getTitle ()Ljava/lang/String; + public abstract fun getTtl ()I +} + +public abstract interface class com/onesignal/notifications/INotificationClickEvent { + public abstract fun getNotification ()Lcom/onesignal/notifications/INotification; + public abstract fun getResult ()Lcom/onesignal/notifications/INotificationClickResult; + public abstract fun toJSONObject ()Lorg/json/JSONObject; +} + +public abstract interface class com/onesignal/notifications/INotificationClickListener { + public abstract fun onClick (Lcom/onesignal/notifications/INotificationClickEvent;)V +} + +public abstract interface class com/onesignal/notifications/INotificationClickResult { + public abstract fun getActionId ()Ljava/lang/String; + public abstract fun getUrl ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/notifications/INotificationLifecycleListener { + public abstract fun onWillDisplay (Lcom/onesignal/notifications/INotificationWillDisplayEvent;)V +} + +public abstract interface class com/onesignal/notifications/INotificationReceivedEvent { + public abstract fun getContext ()Landroid/content/Context; + public abstract fun getNotification ()Lcom/onesignal/notifications/IDisplayableMutableNotification; + public abstract fun getRestoring ()Z + public abstract fun preventDefault ()V + public abstract fun preventDefault (Z)V +} + +public final class com/onesignal/notifications/INotificationReceivedEvent$DefaultImpls { + public static fun getRestoring (Lcom/onesignal/notifications/INotificationReceivedEvent;)Z +} + +public abstract interface class com/onesignal/notifications/INotificationServiceExtension { + public abstract fun onNotificationReceived (Lcom/onesignal/notifications/INotificationReceivedEvent;)V +} + +public abstract interface class com/onesignal/notifications/INotificationWillDisplayEvent { + public abstract fun getNotification ()Lcom/onesignal/notifications/IDisplayableNotification; + public abstract fun preventDefault ()V + public abstract fun preventDefault (Z)V +} + +public abstract interface class com/onesignal/notifications/INotificationsManager { + public abstract fun addClickListener (Lcom/onesignal/notifications/INotificationClickListener;)V + public abstract fun addForegroundLifecycleListener (Lcom/onesignal/notifications/INotificationLifecycleListener;)V + public abstract fun addPermissionObserver (Lcom/onesignal/notifications/IPermissionObserver;)V + public abstract fun clearAllNotifications ()V + public abstract fun getCanRequestPermission ()Z + public abstract fun getPermission ()Z + public abstract fun removeClickListener (Lcom/onesignal/notifications/INotificationClickListener;)V + public abstract fun removeForegroundLifecycleListener (Lcom/onesignal/notifications/INotificationLifecycleListener;)V + public abstract fun removeGroupedNotifications (Ljava/lang/String;)V + public abstract fun removeNotification (I)V + public abstract fun removePermissionObserver (Lcom/onesignal/notifications/IPermissionObserver;)V + public abstract fun requestPermission (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class com/onesignal/notifications/IPermissionObserver { + public abstract fun onNotificationPermissionChange (Z)V +} + +public abstract interface class com/onesignal/session/ISessionManager { + public abstract fun addOutcome (Ljava/lang/String;)V + public abstract fun addOutcomeWithValue (Ljava/lang/String;F)V + public abstract fun addUniqueOutcome (Ljava/lang/String;)V +} + +public abstract interface class com/onesignal/user/IUserManager { + public abstract fun addAlias (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun addAliases (Ljava/util/Map;)V + public abstract fun addEmail (Ljava/lang/String;)V + public abstract fun addObserver (Lcom/onesignal/user/state/IUserStateObserver;)V + public abstract fun addSms (Ljava/lang/String;)V + public abstract fun addTag (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun addTags (Ljava/util/Map;)V + public abstract fun getExternalId ()Ljava/lang/String; + public abstract fun getOnesignalId ()Ljava/lang/String; + public abstract fun getPushSubscription ()Lcom/onesignal/user/subscriptions/IPushSubscription; + public abstract fun getTags ()Ljava/util/Map; + public abstract fun removeAlias (Ljava/lang/String;)V + public abstract fun removeAliases (Ljava/util/Collection;)V + public abstract fun removeEmail (Ljava/lang/String;)V + public abstract fun removeObserver (Lcom/onesignal/user/state/IUserStateObserver;)V + public abstract fun removeSms (Ljava/lang/String;)V + public abstract fun removeTag (Ljava/lang/String;)V + public abstract fun removeTags (Ljava/util/Collection;)V + public abstract fun setLanguage (Ljava/lang/String;)V + public abstract fun trackEvent (Ljava/lang/String;Ljava/util/Map;)V +} + +public final class com/onesignal/user/IUserManager$DefaultImpls { + public static synthetic fun trackEvent$default (Lcom/onesignal/user/IUserManager;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V +} + +public abstract interface class com/onesignal/user/state/IUserStateObserver { + public abstract fun onUserStateChange (Lcom/onesignal/user/state/UserChangedState;)V +} + +public final class com/onesignal/user/state/UserChangedState { + public fun (Lcom/onesignal/user/state/UserState;)V + public final fun getCurrent ()Lcom/onesignal/user/state/UserState; + public final fun toJSONObject ()Lorg/json/JSONObject; +} + +public final class com/onesignal/user/state/UserState { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun getExternalId ()Ljava/lang/String; + public final fun getOnesignalId ()Ljava/lang/String; + public final fun toJSONObject ()Lorg/json/JSONObject; +} + +public abstract interface class com/onesignal/user/subscriptions/IEmailSubscription : com/onesignal/user/subscriptions/ISubscription { + public abstract fun getEmail ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/user/subscriptions/IPushSubscription : com/onesignal/user/subscriptions/ISubscription { + public abstract fun addObserver (Lcom/onesignal/user/subscriptions/IPushSubscriptionObserver;)V + public abstract fun getOptedIn ()Z + public abstract fun getToken ()Ljava/lang/String; + public abstract fun optIn ()V + public abstract fun optOut ()V + public abstract fun removeObserver (Lcom/onesignal/user/subscriptions/IPushSubscriptionObserver;)V +} + +public abstract interface class com/onesignal/user/subscriptions/IPushSubscriptionObserver { + public abstract fun onPushSubscriptionChange (Lcom/onesignal/user/subscriptions/PushSubscriptionChangedState;)V +} + +public abstract interface class com/onesignal/user/subscriptions/ISmsSubscription : com/onesignal/user/subscriptions/ISubscription { + public abstract fun getNumber ()Ljava/lang/String; +} + +public abstract interface class com/onesignal/user/subscriptions/ISubscription { + public abstract fun getId ()Ljava/lang/String; +} + +public final class com/onesignal/user/subscriptions/PushSubscriptionChangedState { + public fun (Lcom/onesignal/user/subscriptions/PushSubscriptionState;Lcom/onesignal/user/subscriptions/PushSubscriptionState;)V + public final fun getCurrent ()Lcom/onesignal/user/subscriptions/PushSubscriptionState; + public final fun getPrevious ()Lcom/onesignal/user/subscriptions/PushSubscriptionState; + public final fun toJSONObject ()Lorg/json/JSONObject; +} + +public final class com/onesignal/user/subscriptions/PushSubscriptionState { + public fun (Ljava/lang/String;Ljava/lang/String;Z)V + public final fun getId ()Ljava/lang/String; + public final fun getOptedIn ()Z + public final fun getToken ()Ljava/lang/String; + public final fun toJSONObject ()Lorg/json/JSONObject; +} + diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt index 6b38326c82..1fbcd7837e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt @@ -250,6 +250,16 @@ interface IOneSignal { jwtBearerToken: String? = null, ) + /** + * Login with [externalId] plus [profile] as one Create User upsert (anonymous is not merged). Email/SMS create or transfer; a foreign alias fails it. + * Same id is a no-op once a backend ID exists. Backend failure leaves this identity local; INVALID_ARGUMENT and NOT_INITIALIZED change nothing. + */ + suspend fun login( + externalId: String?, + profile: OneSignalUserProfile, + jwtBearerToken: String? = null, + ): OneSignalResult + /** * Logout the current user (suspend version). */ diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt index ed049b5506..928a52c0a2 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt @@ -424,6 +424,18 @@ object OneSignal { oneSignal.loginSuspend(externalId, jwtBearerToken) } + /** + * Login with [externalId] plus [profile] as one Create User upsert (anonymous is not merged). Email/SMS create or transfer; a foreign alias fails it. + * Same id is a no-op once a backend ID exists. Backend failure leaves this identity local; INVALID_ARGUMENT and NOT_INITIALIZED change nothing. + */ + @JvmStatic + @JvmOverloads + suspend fun login( + externalId: String?, + profile: OneSignalUserProfile, + jwtBearerToken: String? = null, + ): OneSignalResult = oneSignal.login(externalId, profile, jwtBearerToken) + /** * Logout the current user without blocking the calling thread. * Suspends until the SDK is initialized if initialization is in progress. diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalError.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalError.kt new file mode 100644 index 0000000000..1e0af31854 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalError.kt @@ -0,0 +1,301 @@ +package com.onesignal + +import com.onesignal.common.toList +import com.onesignal.common.toMap +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.json.JSONArray +import org.json.JSONObject +import java.util.Collections +import java.util.LinkedHashMap + +/** Whether the SDK produced a failure locally or OneSignal's backend returned it. */ +enum class ErrorSource { + CLIENT, + BACKEND, +} + +/** + * The catalog of failure codes shared by every OneSignal SDK. + * + * An enum rather than a sealed hierarchy so that Java callers get a native `switch` and the + * wrapper bridges get a trivial name-to-string marshal. The backend half of the catalog is + * deliberately *not* modelled here; see [BACKEND_ERROR]. + * + * New codes will be added in later releases, so a `when` or `switch` over this enum should keep an + * `else` branch. + */ +enum class ErrorCode(val source: ErrorSource) { + /** [IOneSignal.initWithContextSuspend] was never called, or it failed. */ + NOT_INITIALIZED(ErrorSource.CLIENT), + + /** + * Device storage was locked, so the SDK could not read or write its own preferences. + * Transient: the same call generally succeeds once the device is unlocked. + */ + STORAGE_LOCKED(ErrorSource.CLIENT), + + /** A caller-supplied argument failed validation before any request was made. */ + INVALID_ARGUMENT(ErrorSource.CLIENT), + + /** OneSignal rejected the request. The catalog code is on [OneSignalError.Detail.backendCode]. */ + BACKEND_ERROR(ErrorSource.BACKEND), + + /** No more specific code applies. Callers should surface [OneSignalError.Detail.message]. */ + UNKNOWN(ErrorSource.CLIENT), +} + +/** + * Describes why a OneSignal call failed. + * + * One request can fail for several reasons at once, so [details] is a list of [Detail]. Everything + * the SDK raises locally has exactly one reason, which [first] reads without the indexing + * ceremony. + * + * On the wire this is the list itself, sitting under the envelope's `error` key: + * + * ```json + * { "success": false, "data": null, + * "error": [ { "code": "STORAGE_LOCKED", "source": "CLIENT", "backendCode": null, "message": "..." } ] } + * ``` + * + * Construct through [of] or [fromWire]. The internal companion is still reachable from Java, so the + * never-empty invariant is enforced in `init`, not by visibility. + */ +class OneSignalError private constructor( + details: List, + /** + * The throwable behind the failure, when there was one. + * + * Deliberately absent from [toList]: a stack trace cannot cross the wrapper bridges, and the + * wire schema has to stay identical across every SDK. This exists so that native Kotlin and + * Java callers do not lose the stack when the suspend APIs report a failure instead of + * throwing it. + */ + val cause: Throwable?, +) { + /** + * Why the call failed. Never empty. + * + * Copied so a caller holding the original list cannot empty it afterwards, and unmodifiable so + * the copy itself cannot be emptied either. Java sees a plain `List` and `clear()` is one + * keystroke away from `get()`; both would leave [first] throwing. + */ + val details: List = Collections.unmodifiableList(details.toList()) + + init { + // [first] is documented as always safe to read, and the wire projection of an empty error + // would claim failure while explaining nothing. Every factory, including Java callers of the + // JVM-public companion, passes through here. + require(this.details.isNotEmpty()) { "OneSignalError requires at least one Detail." } + } + + /** + * A single reason a call failed. + * + * Nested rather than top-level so the name cannot collide with `kotlin.Error`, which is + * auto-imported everywhere, or shadow `java.lang.Error` in a Java file that imports it. + */ + class Detail private constructor( + /** A stable code, safe to branch on. Never localized. */ + val code: ErrorCode, + /** Backend catalog code (e.g. `user-1`). Present only for BACKEND_ERROR. */ + val backendCode: String?, + /** HTTP status of the backend response, when there was one. */ + val httpStatus: Int?, + /** A human-readable description intended for logs and diagnostics, not for end users. */ + val message: String?, + /** Who the failure came from. Kept on UNKNOWN so a backend failure is not re-attributed as client. */ + val source: ErrorSource, + /** Backend `meta` object, when present (e.g. `conflicting_aliases`). */ + val meta: Map?, + ) { + /** Projects this reason onto the cross-SDK wire shape consumed by the wrapper bridges. */ + fun toMap(): Map = + mapOf( + KEY_CODE to code.name, + KEY_SOURCE to source.name, + KEY_BACKEND_CODE to backendCode, + KEY_HTTP_STATUS to httpStatus, + KEY_MESSAGE to message, + KEY_META to meta, + ) + + override fun toString(): String = + "Detail(code=$code, source=$source, backendCode=$backendCode, httpStatus=$httpStatus, message=$message, meta=$meta)" + + internal companion object { + // Private because `const val` in an internal companion still compiles to a public + // static field, which would leak the wire keys into the customer-facing API surface. + private const val KEY_CODE = "code" + private const val KEY_SOURCE = "source" + private const val KEY_BACKEND_CODE = "backendCode" + private const val KEY_HTTP_STATUS = "httpStatus" + private const val KEY_MESSAGE = "message" + private const val KEY_META = "meta" + + /** Rebuilds a reason from its wire shape. Unrecognized codes become [ErrorCode.UNKNOWN]. */ + fun fromMap(map: Map<*, *>): Detail { + val code = codeOf(map[KEY_CODE] as? String) + return Detail( + code = code, + backendCode = optionalString(map[KEY_BACKEND_CODE]), + httpStatus = (map[KEY_HTTP_STATUS] as? Number)?.toInt(), + message = map[KEY_MESSAGE] as? String, + source = sourceOf(map[KEY_SOURCE] as? String) ?: code.source, + meta = optionalMeta(map[KEY_META]), + ) + } + + fun of( + code: ErrorCode, + backendCode: String? = null, + httpStatus: Int? = null, + message: String? = null, + source: ErrorSource = code.source, + meta: Map? = null, + ): Detail = + Detail( + code, + backendCode, + httpStatus, + message, + source, + meta?.let { Collections.unmodifiableMap(LinkedHashMap(it)) }, + ) + + private fun codeOf(name: String?): ErrorCode = ErrorCode.entries.firstOrNull { it.name == name } ?: ErrorCode.UNKNOWN + + private fun sourceOf(name: String?): ErrorSource? = ErrorSource.entries.firstOrNull { it.name == name } + + private fun optionalString(value: Any?): String? = (value as? String)?.takeIf { it.isNotEmpty() } + + private fun optionalMeta(value: Any?): Map? { + val map = + when (value) { + is Map<*, *> -> + value.entries.mapNotNull { (key, entry) -> + (key as? String)?.let { it to entry } + }.toMap() + is JSONObject -> value.toMap() + else -> return null + } + return map.takeIf { it.isNotEmpty() }?.let { Collections.unmodifiableMap(LinkedHashMap(it)) } + } + } + } + + /** The first reason, which is the only one for every failure the SDK raises locally. */ + val first: Detail + get() = details.first() + + /** Projects this error onto the cross-SDK wire shape consumed by the wrapper bridges. */ + fun toList(): List> = details.map { it.toMap() } + + override fun toString(): String = "OneSignalError(details=$details)" + + internal companion object { + private val json = + Json { + ignoreUnknownKeys = true + isLenient = false + } + + /** Builds a single-reason error, which is the shape of everything the SDK raises locally. */ + fun of( + code: ErrorCode, + message: String? = null, + backendCode: String? = null, + httpStatus: Int? = null, + meta: Map? = null, + cause: Throwable? = null, + ): OneSignalError = OneSignalError(listOf(Detail.of(code, backendCode, httpStatus, message, meta = meta)), cause) + + /** Builds a multi-reason error. [reasons] must not be empty. */ + fun of( + reasons: List, + cause: Throwable? = null, + ): OneSignalError = OneSignalError(reasons, cause) + + /** Parses a backend `errors[]` body. Unreadable bodies become one reason with [httpStatus]. */ + fun fromBackendResponse( + httpStatus: Int?, + body: String?, + fallbackMessage: String? = null, + ): OneSignalError { + parseBackendErrors(httpStatus, body)?.let { return OneSignalError(it, cause = null) } + val message = body?.takeIf { it.isNotBlank() } ?: fallbackMessage + return of(ErrorCode.BACKEND_ERROR, message = message, httpStatus = httpStatus) + } + + /** Rebuilds an error from its wire shape. Unreadable entries become UNKNOWN rather than dropping the failure. */ + fun fromWire(raw: Any?): OneSignalError { + val reasons = + when (raw) { + is List<*> -> raw.map { reasonOf(it) } + // org.json is what a bridge naturally parses with, and JSONArray is not a + // java.util.List. Convert rather than treating the whole array as one reason. + is JSONArray -> raw.toList().orEmpty().map { reasonOf(it) } + else -> listOf(reasonOf(raw)) + } + return OneSignalError(reasons.ifEmpty { listOf(Detail.of(ErrorCode.UNKNOWN)) }, cause = null) + } + + private fun reasonOf(raw: Any?): Detail = + when (raw) { + is Map<*, *> -> Detail.fromMap(raw) + is JSONObject -> Detail.fromMap(raw.toMap()) + else -> Detail.of(ErrorCode.UNKNOWN, message = raw?.toString()) + } + + private fun parseBackendErrors( + httpStatus: Int?, + body: String?, + ): List? { + if (body.isNullOrBlank()) return null + return try { + val errors = (json.parseToJsonElement(body) as? JsonObject)?.get("errors") as? JsonArray + val details = + errors?.mapNotNull { element -> + val item = element as? JsonObject ?: return@mapNotNull null + Detail.of( + code = ErrorCode.BACKEND_ERROR, + backendCode = item.optionalString("code"), + httpStatus = httpStatus, + message = item.optionalString("title"), + meta = (item["meta"] as? JsonObject)?.toPlainMap()?.takeIf { it.isNotEmpty() }, + ) + } + details?.takeIf { it.isNotEmpty() } + } catch (_: Exception) { + null + } + } + + private fun JsonObject.optionalString(key: String): String? = + (this[key] as? JsonPrimitive) + ?.takeIf { it !is JsonNull } + ?.content + ?.takeIf { it.isNotEmpty() } + + private fun JsonObject.toPlainMap(): Map = entries.associate { it.key to it.value.toPlain() } + + private fun JsonElement.toPlain(): Any? = + when (this) { + is JsonNull -> null + is JsonPrimitive -> + when { + isString -> content + content.equals("true", ignoreCase = true) -> true + content.equals("false", ignoreCase = true) -> false + else -> content.toLongOrNull() ?: content.toDoubleOrNull() ?: content + } + is JsonObject -> toPlainMap() + is JsonArray -> map { it.toPlain() } + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalResult.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalResult.kt new file mode 100644 index 0000000000..a9f1248203 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalResult.kt @@ -0,0 +1,135 @@ +package com.onesignal + +import com.onesignal.common.toMap +import org.json.JSONObject + +/** + * The outcome of an asynchronous OneSignal call: either a [data] payload or an [error], never both + * and never neither. + * + * Every SDK returns the same envelope, so a wrapper can handle results uniformly regardless of the + * platform underneath it: + * + * ```json + * { "success": true, "data": { }, "error": null } + * { "success": false, "data": null, "error": [ { "code": "STORAGE_LOCKED", ... } ] } + * ``` + * + * The presence of [error] is what defines the outcome; [isSuccess] and the wire-level `success` + * flag are both derived from it, so the two can never disagree. + * + * Construct through [success], [failure], or [fromMap]. The internal companion is still reachable + * from Java, so the either/or invariant is enforced in `init`, not by visibility. + * + * From Kotlin, inside a coroutine: + * ```kotlin + * val result = OneSignal.login("user-123", OneSignalUserProfile.builder().setEmail("a@b.co").build()) + * if (result.isSuccess) println(result.data?.onesignalId) else println(result.error?.first?.code) + * ``` + * + * From Java the generated accessors read naturally: + * ```java + * if (result.isSuccess()) { result.getData(); } else { result.getError().getDetails(); } + * ``` + */ +class OneSignalResult private constructor( + /** The payload on success, `null` on failure. */ + val data: T?, + /** The failure detail on failure, `null` on success. */ + val error: OneSignalError?, +) { + init { + // [isSuccess] reads error while callers read data, so neither or both leaves the two disagreeing. + require((data == null) != (error == null)) { "OneSignalResult carries exactly one of data or error." } + } + + /** `true` when the call completed successfully. Equivalent to `error == null`. */ + val isSuccess: Boolean + get() = error == null + + /** Projects the envelope onto the cross-SDK wire shape consumed by the wrapper bridges. */ + fun toMap(): Map = + mapOf( + KEY_SUCCESS to isSuccess, + KEY_DATA to data?.toMap(), + KEY_ERROR to error?.toList(), + ) + + override fun toString(): String = if (isSuccess) "OneSignalResult(success, data=$data)" else "OneSignalResult(failure, error=$error)" + + internal companion object { + // Private because `const val` in an internal companion still compiles to a public static + // field, which would leak the wire keys into the customer-facing API surface. + private const val KEY_SUCCESS = "success" + private const val KEY_DATA = "data" + private const val KEY_ERROR = "error" + + fun success(data: T): OneSignalResult = OneSignalResult(data, null) + + fun failure(error: OneSignalError): OneSignalResult = OneSignalResult(null, error) + + fun failure( + code: ErrorCode, + message: String? = null, + backendCode: String? = null, + httpStatus: Int? = null, + cause: Throwable? = null, + ): OneSignalResult = failure(OneSignalError.of(code, message, backendCode, httpStatus, cause = cause)) + + /** + * Rebuilds an envelope from its wire shape, delegating payload parsing to [dataParser]. + * + * The incoming `success` flag is deliberately ignored: [error] is the single source of + * truth, which keeps a malformed producer from yielding a result that claims success while + * carrying an error. Unrecognized keys are ignored so a newer producer can add fields + * without breaking an older consumer. + * + * Neither `error` nor `data` is read with a cast that doubles as its own presence check. + * Such a cast reports a value the parser could not type as a value that was never sent, + * which turns a failure into an empty success and an unreadable payload into a blank one. + * + * A payload that is present but unreadable (neither a [Map] nor a [JSONObject]) is reported + * as a failure. That contradicts the producer, which was reporting success, and it is still + * the lesser harm: the envelope's whole purpose is that [isSuccess] tells a caller whether + * [data] can be trusted, and a fabricated payload is indistinguishable at the call site from + * one the producer really sent. A caller handed a failure retries or reports it; a caller + * handed a blank payload logs someone in as nobody. + */ + fun fromMap( + map: Map, + dataParser: (Map<*, *>) -> T?, + ): OneSignalResult { + val reasons = map[KEY_ERROR] + // JSONObject.NULL is org.json's JSON null, a live object, so `!= null` would treat a + // successful envelope's `"error": null` as a present error. + if (!reasons.isJsonAbsent()) { + return failure(OneSignalError.fromWire(reasons)) + } + + val payload = map[KEY_DATA] + return when { + payload.isJsonAbsent() -> parsed(dataParser, emptyMap()) + payload is Map<*, *> -> parsed(dataParser, payload) + payload is JSONObject -> parsed(dataParser, payload.toMap()) + else -> failure(ErrorCode.UNKNOWN, unreadablePayload(payload)) + } + } + + private fun parsed( + dataParser: (Map<*, *>) -> T?, + payload: Map<*, *>, + ): OneSignalResult { + val data = dataParser(payload) ?: return failure(ErrorCode.UNKNOWN, MISSING_REQUIRED_FIELDS) + return success(data) + } + + // The type is enough to diagnose the producer. The payload itself is identity data, and an + // error message is somewhere it would be logged. + private fun unreadablePayload(payload: Any?): String = + "Result data could not be read: expected a map but received ${payload?.let { it::class.simpleName }}." + + private const val MISSING_REQUIRED_FIELDS = "Result data was missing required fields." + + private fun Any?.isJsonAbsent(): Boolean = this === null || this === JSONObject.NULL + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalResultData.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalResultData.kt new file mode 100644 index 0000000000..c6e99a16af --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalResultData.kt @@ -0,0 +1,110 @@ +package com.onesignal + +/** + * Implemented by every payload that can travel inside a [OneSignalResult]. + * + * Sealed so a new member is never a silent source-compatible break for external implementors — this + * contract exists so the envelope can project onto the wire shape without knowing which payload it + * is carrying, and is not intended to be implemented outside the SDK. + */ +sealed interface OneSignalResultData { + /** Projects this payload onto the cross-SDK wire shape consumed by the wrapper bridges. */ + fun toMap(): Map +} + +/* + * The payloads themselves. + * + * Several of these start with no fields. That is intentional, and it is why none of them is a + * `data class` or an `object`: a regular class can gain fields without a singleton blocking per-call + * state or a generated copy/equals changing under customers. The constructors are internal, which + * Java still sees as public: new fields change those JVM signatures, so they are not supported API. + * + * Field names here are also the wire keys, so they are additive-only: never renamed, never removed. + */ + +/** The payload returned by a successful login. */ +class LoginData internal constructor( + /** The OneSignal ID the external ID is now associated with. */ + val onesignalId: String, + /** The external ID that was logged in. */ + val externalId: String, + /** ID of the email subscription for the address passed in, if the backend returned one. */ + val emailSubscriptionId: String? = null, + /** ID of the SMS subscription for the number passed in, if the backend returned one. */ + val smsSubscriptionId: String? = null, +) : OneSignalResultData { + override fun toMap(): Map = + buildMap { + put(KEY_ONESIGNAL_ID, onesignalId) + put(KEY_EXTERNAL_ID, externalId) + emailSubscriptionId?.let { put(KEY_EMAIL_SUBSCRIPTION_ID, it) } + smsSubscriptionId?.let { put(KEY_SMS_SUBSCRIPTION_ID, it) } + } + + override fun toString(): String = + "LoginData(onesignalId=$onesignalId, externalId=$externalId, emailSubscriptionId=$emailSubscriptionId, smsSubscriptionId=$smsSubscriptionId)" + + internal companion object { + // Private because `const val` in an internal companion still compiles to a public static + // field, which would leak the wire keys into the customer-facing API surface. + private const val KEY_ONESIGNAL_ID = "onesignalId" + private const val KEY_EXTERNAL_ID = "externalId" + private const val KEY_EMAIL_SUBSCRIPTION_ID = "emailSubscriptionId" + private const val KEY_SMS_SUBSCRIPTION_ID = "smsSubscriptionId" + + /** Null when [onesignalId] or [externalId] is missing, empty, or not a string. */ + fun fromMap(map: Map<*, *>): LoginData? { + val onesignalId = map[KEY_ONESIGNAL_ID] as? String + val externalId = map[KEY_EXTERNAL_ID] as? String + if (onesignalId.isNullOrEmpty() || externalId.isNullOrEmpty()) return null + return LoginData( + onesignalId, + externalId, + emailSubscriptionId = optionalString(map[KEY_EMAIL_SUBSCRIPTION_ID]), + smsSubscriptionId = optionalString(map[KEY_SMS_SUBSCRIPTION_ID]), + ) + } + + private fun optionalString(value: Any?): String? = (value as? String)?.takeIf { it.isNotEmpty() } + } +} + +/** The payload returned by a successful logout. Carries no fields yet. */ +class LogoutData internal constructor() : OneSignalResultData { + override fun toMap(): Map = emptyMap() + + override fun toString(): String = "LogoutData()" + + internal companion object { + fun fromMap( + @Suppress("UNUSED_PARAMETER") map: Map<*, *>, + ): LogoutData = LogoutData() + } +} + +/** The payload returned by a successful user JWT update. Carries no fields yet. */ +class UpdateUserJwtData internal constructor() : OneSignalResultData { + override fun toMap(): Map = emptyMap() + + override fun toString(): String = "UpdateUserJwtData()" + + internal companion object { + fun fromMap( + @Suppress("UNUSED_PARAMETER") map: Map<*, *>, + ): UpdateUserJwtData = UpdateUserJwtData() + } +} + +/** The payload returned by a successful initialization. Carries no fields yet. */ +class InitData internal constructor() : OneSignalResultData { + override fun toMap(): Map = emptyMap() + + override fun toString(): String = "InitData()" + + internal companion object { + fun fromMap( + @Suppress("UNUSED_PARAMETER") map: Map<*, *>, + ): InitData = InitData() + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalUserProfile.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalUserProfile.kt new file mode 100644 index 0000000000..f35b058a75 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalUserProfile.kt @@ -0,0 +1,99 @@ +package com.onesignal + +import com.onesignal.common.OneSignalUtils +import com.onesignal.common.hasNullByte +import java.util.Collections + +/** + * Profile applied at login. Email/SMS create or transfer that address onto this user; they do not replace other subscriptions. + * Build one with [builder]. There is no public constructor, so adding a field never breaks compiled callers. + */ +// Takes the Builder so the synthetic accessor's JVM signature stays fixed as fields are added. +class OneSignalUserProfile private constructor(builder: Builder) { + val email: String? = builder.email?.takeIf { it.isNotBlank() } + val sms: String? = builder.sms?.takeIf { it.isNotBlank() } + val tags: Map = Collections.unmodifiableMap(builder.tags.toMap()) + val aliases: Map = Collections.unmodifiableMap(builder.aliases.toMap()) + + internal val hasFields: Boolean + get() = + !email.isNullOrBlank() || + !sms.isNullOrBlank() || + tags.isNotEmpty() || + aliases.isNotEmpty() + + internal fun validationError(): String? = + when { + email != null && !OneSignalUtils.isValidEmail(email) -> "Invalid email address" + sms != null && !OneSignalUtils.isValidPhoneNumber(sms) -> "Invalid SMS number" + // Java callers can put nulls in these maps despite the non-null types. + tags.any { (key, value) -> isBadKey(key) || (value as String?) == null } -> "Tag keys must be non-empty and values non-null" + aliases.any { (label, id) -> isBadKey(label) || isBadKey(id) } -> "Alias labels and IDs must be non-empty" + else -> null + } + + private fun isBadKey(value: String?) = value.isNullOrEmpty() || hasNullByte(value) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is OneSignalUserProfile) return false + return email == other.email && + sms == other.sms && + tags == other.tags && + aliases == other.aliases + } + + override fun hashCode(): Int { + var result = email?.hashCode() ?: 0 + result = 31 * result + (sms?.hashCode() ?: 0) + result = 31 * result + tags.hashCode() + result = 31 * result + aliases.hashCode() + return result + } + + override fun toString(): String = + "OneSignalUserProfile(email=${redact(email)}, sms=${redact(sms)}, tags=${tags.size}, aliases=${aliases.size})" + + /** Builds a [OneSignalUserProfile]. Unset fields stay empty. */ + class Builder { + internal var email: String? = null + private set + internal var sms: String? = null + private set + internal var tags: Map = emptyMap() + private set + internal var aliases: Map = emptyMap() + private set + + /** Email to create or transfer onto this user at login. */ + fun setEmail(email: String?) = apply { this.email = email } + + /** SMS to create or transfer onto this user at login. Must be E.164 (e.g. +14155552671). */ + fun setSms(sms: String?) = apply { this.sms = sms } + + /** Sets tags to apply at login. The map is copied. */ + fun setTags(tags: Map) = apply { this.tags = tags.toMap() } + + /** Sets aliases to apply at login. The map is copied. */ + fun setAliases(aliases: Map) = apply { this.aliases = aliases.toMap() } + + /** Builds the profile. */ + fun build() = OneSignalUserProfile(this) + } + + companion object { + /** Returns a new builder. */ + @JvmStatic + fun builder() = Builder() + + /** SDK-internal Kotlin shorthand; the mangled JVM name keeps it out of the public API. */ + internal operator fun invoke( + email: String? = null, + sms: String? = null, + tags: Map = emptyMap(), + aliases: Map = emptyMap(), + ) = Builder().setEmail(email).setSms(sms).setTags(tags).setAliases(aliases).build() + + private fun redact(value: String?): String = if (value == null) "null" else "" + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/IHostPermissionPrompt.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/IHostPermissionPrompt.kt new file mode 100644 index 0000000000..44c25134e0 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/IHostPermissionPrompt.kt @@ -0,0 +1,22 @@ +package com.onesignal.common + +/** + * Shows the Android permission dialog on the host's own activity, for wrappers whose + * activity is not a `ComponentActivity` and so cannot host an activity result launcher. + */ +interface IHostPermissionPrompt { + /** Receives the user's answer. May be called from any thread. */ + fun interface Callback { + /** Call once after [request] returns true, and do not call it when [request] returns false. */ + fun onResult(granted: Boolean) + } + + /** + * Return false, and do not call [Callback], when no activity can prompt. + * Otherwise call [Callback.onResult] once, even if the host goes away first. + */ + fun request( + androidPermission: String, + callback: Callback, + ): Boolean +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/OneSignalWrapper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/OneSignalWrapper.kt index be119ab5b9..45a8a0fad8 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/OneSignalWrapper.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/OneSignalWrapper.kt @@ -1,5 +1,6 @@ package com.onesignal.common +// Wrapper SDKs call this from Java, C# bindings, and Unity JNI by name. Tracked by apiCheck. object OneSignalWrapper { /** * The type of the wrapper SDK. @@ -12,4 +13,12 @@ object OneSignalWrapper { */ @JvmStatic var sdkVersion: String? = null + + /** + * Set on activity attach and cleared on detach by wrappers whose host activity cannot + * host an activity result launcher. Consulted only when `PermissionsActivity` is missing. + */ + @JvmStatic + @Volatile + var hostPermissionPrompt: IHostPermissionPrompt? = null } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/InFlightResult.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/InFlightResult.kt new file mode 100644 index 0000000000..f644d8c38f --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/InFlightResult.kt @@ -0,0 +1,51 @@ +package com.onesignal.common.threading + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +/** + * One caller runs [block]. Later callers wait for that result instead of running it again. + */ +class InFlightResult { + private val gate = Mutex() + private var current: CompletableDeferred? = null + + /** One caller runs [block]. Later callers wait for that result. */ + suspend fun share( + fallback: T, + block: suspend () -> T, + ): T { + val (deferred, owner) = + gate.withLock { + val existing = current + if (existing != null) { + existing to false + } else { + CompletableDeferred().also { current = it } to true + } + } + if (!owner) { + return deferred.await() + } + return try { + val result = block() + deferred.complete(result) + result + } finally { + // A cancelled owner cannot wait on a held lock, which would leave [current] stuck. + withContext(NonCancellable) { + if (!deferred.isCompleted) { + deferred.complete(fallback) + } + gate.withLock { + if (current === deferred) { + current = null + } + } + } + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt index 8118671035..614903b8f2 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt @@ -16,9 +16,11 @@ import com.onesignal.core.internal.config.impl.FeatureFlagsRefreshService import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.DatabaseProvider +import com.onesignal.core.internal.device.IConnectivityMonitor import com.onesignal.core.internal.device.IDeviceService import com.onesignal.core.internal.device.IFidEnv import com.onesignal.core.internal.device.IInstallIdService +import com.onesignal.core.internal.device.impl.ConnectivityMonitor import com.onesignal.core.internal.device.impl.DeviceService import com.onesignal.core.internal.device.impl.FidEnvService import com.onesignal.core.internal.device.impl.InstallIdService @@ -34,6 +36,7 @@ import com.onesignal.core.internal.language.impl.LanguageContext import com.onesignal.core.internal.operations.IOperationRepo import com.onesignal.core.internal.operations.impl.OperationModelStore import com.onesignal.core.internal.operations.impl.OperationRepo +import com.onesignal.core.internal.operations.impl.OperationRetryTrigger import com.onesignal.core.internal.permissions.IRequestPermissionService import com.onesignal.core.internal.permissions.impl.RequestPermissionService import com.onesignal.core.internal.preferences.IPreferencesService @@ -69,6 +72,7 @@ internal class CoreModule : IModule { .provides() builder.register().provides() builder.register().provides() + builder.register().provides() builder.register