From c7756acb5b8d41790ed4cc2389d2f1469031c217 Mon Sep 17 00:00:00 2001 From: jinliu Date: Mon, 5 Jan 2026 11:22:27 -0500 Subject: [PATCH 01/19] fix: IAM with NOT_EQUAL_TO trigger always shows up (#2509) Co-authored-by: AR Abdul Azeez --- .../triggers/impl/TriggerController.kt | 8 +- .../triggers/impl/TriggerControllerTests.kt | 1082 +++++++++++++++++ 2 files changed, 1086 insertions(+), 4 deletions(-) create mode 100644 OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerControllerTests.kt diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerController.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerController.kt index bb1496ae30..f6b7082840 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerController.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerController.kt @@ -59,12 +59,12 @@ internal class TriggerController( } val operatorType = trigger.operatorType + // If trigger is type of NOT_EQUAL_TO, true if only there exists a trigger with the same key and different value. + // In that case, do not return early so it will evaluate later. val deviceValue = triggers[trigger.property] - ?: // If we don't have a local value for this trigger, can only be true in two cases; - // 1. If operator is Not Exists - // 2. Checking to make sure the key doesn't equal a specific value, other than null of course. - return if (operatorType == Trigger.OSTriggerOperator.NOT_EXISTS) true else operatorType == Trigger.OSTriggerOperator.NOT_EQUAL_TO && trigger.value != null + ?: // If we don't have a local value for this trigger, can only be true if operator is Not Exists + return operatorType == Trigger.OSTriggerOperator.NOT_EXISTS // We have local value at this point, we can evaluate existence checks if (operatorType == Trigger.OSTriggerOperator.EXISTS) { diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerControllerTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerControllerTests.kt new file mode 100644 index 0000000000..3ee79ca08e --- /dev/null +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/triggers/impl/TriggerControllerTests.kt @@ -0,0 +1,1082 @@ +package com.onesignal.inAppMessages.internal.triggers.impl + +import com.onesignal.common.modeling.ModelChangedArgs +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.inAppMessages.internal.InAppMessage +import com.onesignal.inAppMessages.internal.Trigger +import com.onesignal.inAppMessages.internal.state.InAppStateService +import com.onesignal.inAppMessages.internal.triggers.ITriggerHandler +import com.onesignal.inAppMessages.internal.triggers.TriggerModel +import com.onesignal.inAppMessages.internal.triggers.TriggerModelStore +import com.onesignal.mocks.MockHelper +import com.onesignal.session.internal.session.ISessionService +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.json.JSONArray +import org.json.JSONObject + +private class Mocks { + val triggerModelStore = mockk(relaxed = true) + val inAppStateService = mockk(relaxed = true) + val sessionService = mockk(relaxed = true) + val time = MockHelper.time(100) + val dynamicTriggerController = DynamicTriggerController(inAppStateService, sessionService, time) + + val triggerController = TriggerController(triggerModelStore, dynamicTriggerController) + + fun createTrigger( + id: String = "trigger-id", + kind: Trigger.OSTriggerKind = Trigger.OSTriggerKind.CUSTOM, + property: String? = "property-key", + operator: Trigger.OSTriggerOperator = Trigger.OSTriggerOperator.EQUAL_TO, + value: Any? = "value", + ): Trigger { + val json = JSONObject() + json.put("id", id) + json.put("kind", kind.toString()) + if (property != null) { + json.put("property", property) + } + json.put("operator", operator.toString()) + if (value != null) { + json.put("value", value) + } + return Trigger(json) + } + + fun createInAppMessage( + messageId: String = "message-id", + triggers: List> = emptyList(), + ): InAppMessage { + val json = JSONObject() + json.put("id", messageId) + json.put("variants", JSONObject().apply { + put("all", JSONObject().apply { + put("en", "variant-id") + }) + }) + val triggersJson = JSONArray() + triggers.forEach { andConditions -> + val andConditionsJson = JSONArray() + andConditions.forEach { trigger -> + andConditionsJson.put(trigger.toJSONObject()) + } + triggersJson.put(andConditionsJson) + } + json.put("triggers", triggersJson) + return InAppMessage(json, time) + } + + fun createTriggerModel(key: String, value: Any): TriggerModel { + val model = TriggerModel() + model.key = key + model.value = value + return model + } +} + +class TriggerControllerTests : FunSpec({ + lateinit var mocks: Mocks + + beforeAny { + Logging.logLevel = LogLevel.NONE + mocks = Mocks() + } + + context("evaluateMessageTriggers") { + test("returns true when message has no triggers") { + // Given + val message = mocks.createInAppMessage(triggers = emptyList()) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true when single AND condition is satisfied") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger1))) + mocks.triggerController.triggers["key1"] = "value1" + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false when single AND condition is not satisfied") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger1))) + mocks.triggerController.triggers["key1"] = "different-value" + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true when all triggers in AND condition are satisfied") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + ) + val trigger2 = mocks.createTrigger( + id = "trigger2", + property = "key2", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger1, trigger2))) + mocks.triggerController.triggers["key1"] = "value1" + mocks.triggerController.triggers["key2"] = 20 + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false when one trigger in AND condition is not satisfied") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + ) + val trigger2 = mocks.createTrigger( + id = "trigger2", + property = "key2", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger1, trigger2))) + mocks.triggerController.triggers["key1"] = "value1" + mocks.triggerController.triggers["key2"] = 5 // Less than 10 + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true when first OR condition is satisfied") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + ) + val trigger2 = mocks.createTrigger( + id = "trigger2", + property = "key2", + value = "value2", + ) + val message = mocks.createInAppMessage( + triggers = listOf( + listOf(trigger1), + listOf(trigger2), + ), + ) + mocks.triggerController.triggers["key1"] = "value1" + // key2 is not set, so second OR condition fails + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true when second OR condition is satisfied") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + ) + val trigger2 = mocks.createTrigger( + id = "trigger2", + property = "key2", + value = "value2", + ) + val message = mocks.createInAppMessage( + triggers = listOf( + listOf(trigger1), + listOf(trigger2), + ), + ) + // key1 is not set, so first OR condition fails + mocks.triggerController.triggers["key2"] = "value2" + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false when all OR conditions fail") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + ) + val trigger2 = mocks.createTrigger( + id = "trigger2", + property = "key2", + value = "value2", + ) + val message = mocks.createInAppMessage( + triggers = listOf( + listOf(trigger1), + listOf(trigger2), + ), + ) + mocks.triggerController.triggers["key1"] = "different-value" + mocks.triggerController.triggers["key2"] = "different-value" + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns false for UNKNOWN trigger kind") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + kind = Trigger.OSTriggerKind.UNKNOWN, + property = "key1", + value = "value1", + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + mocks.triggerController.triggers["key1"] = "value1" + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("delegates to dynamicTriggerController for non-CUSTOM triggers") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + kind = Trigger.OSTriggerKind.SESSION_TIME, + property = null, + value = 100, // 100 seconds + operator = Trigger.OSTriggerOperator.GREATER_THAN, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + // Set up dependencies so dynamicTriggerShouldFire returns true + // Required time: 100 * 1000 = 100000ms + // Current session time should be > 100000ms (200000ms = 200 seconds) + val mockSessionService = mockk(relaxed = true) + val mockTime = MockHelper.time(200000) // 200 seconds > 100 seconds + every { mockSessionService.startTime } returns 0L + val dynamicController = DynamicTriggerController(mocks.inAppStateService, mockSessionService, mockTime) + val triggerController = TriggerController(mocks.triggerModelStore, dynamicController) + + // When + val result = triggerController.evaluateMessageTriggers(message) + + // Then + // The result should be true because session time (200s) > required time (100s) + // This verifies that TriggerController delegates to DynamicTriggerController + result shouldBe true + } + } + + context("evaluateTrigger - Custom triggers") { + test("returns true for EXISTS operator when key exists") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.EXISTS, + value = null, + ) + mocks.triggerController.triggers["key1"] = "any-value" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for EXISTS operator when key does not exist") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.EXISTS, + value = null, + ) + // key1 is not in triggers + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true for NOT_EXISTS operator when key does not exist") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.NOT_EXISTS, + value = null, + ) + // key1 is not in triggers + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for NOT_EXISTS operator when key exists") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.NOT_EXISTS, + value = null, + ) + mocks.triggerController.triggers["key1"] = "any-value" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns false for NOT_EQUAL_TO operator when key does not exist") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.NOT_EQUAL_TO, + value = null, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true for NOT_EQUAL_TO operator when key exists with different value") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.NOT_EQUAL_TO, + value = "value1", + ) + mocks.triggerController.triggers["key1"] = "value2" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for NOT_EQUAL_TO operator when key exists with same value") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.NOT_EQUAL_TO, + value = "value1", + ) + mocks.triggerController.triggers["key1"] = "value1" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true for CONTAINS operator when collection contains value") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.CONTAINS, + value = "item1", + ) + mocks.triggerController.triggers["key1"] = listOf("item1", "item2", "item3") + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for CONTAINS operator when collection does not contain value") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.CONTAINS, + value = "item4", + ) + mocks.triggerController.triggers["key1"] = listOf("item1", "item2", "item3") + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns false for CONTAINS operator when value is not a collection") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + operator = Trigger.OSTriggerOperator.CONTAINS, + value = "item1", + ) + mocks.triggerController.triggers["key1"] = "not-a-collection" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + } + + context("evaluateTrigger - String operators") { + test("returns true for EQUAL_TO operator with matching strings") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + operator = Trigger.OSTriggerOperator.EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = "value1" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for EQUAL_TO operator with non-matching strings") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + operator = Trigger.OSTriggerOperator.EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = "different-value" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true for NOT_EQUAL_TO operator with different strings") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + operator = Trigger.OSTriggerOperator.NOT_EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = "different-value" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for NOT_EQUAL_TO operator with matching strings") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "value1", + operator = Trigger.OSTriggerOperator.NOT_EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = "value1" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + } + + context("evaluateTrigger - Numeric operators") { + test("returns true for EQUAL_TO operator with matching numbers") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = 10 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true for GREATER_THAN operator when device value is greater") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN, + ) + mocks.triggerController.triggers["key1"] = 20 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for GREATER_THAN operator when device value is less") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN, + ) + mocks.triggerController.triggers["key1"] = 5 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true for LESS_THAN operator when device value is less") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.LESS_THAN, + ) + mocks.triggerController.triggers["key1"] = 5 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for LESS_THAN operator when device value is greater") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.LESS_THAN, + ) + mocks.triggerController.triggers["key1"] = 20 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + + test("returns true for GREATER_THAN_OR_EQUAL_TO operator when device value is greater") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN_OR_EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = 20 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true for GREATER_THAN_OR_EQUAL_TO operator when device value is equal") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN_OR_EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = 10 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true for LESS_THAN_OR_EQUAL_TO operator when device value is less") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.LESS_THAN_OR_EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = 5 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true for LESS_THAN_OR_EQUAL_TO operator when device value is equal") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.LESS_THAN_OR_EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = 10 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + } + + context("evaluateTrigger - Flex matching") { + test("returns true for EQUAL_TO with number and string conversion") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "5", + operator = Trigger.OSTriggerOperator.EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = 5 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true for EQUAL_TO with string and number conversion") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 5, + operator = Trigger.OSTriggerOperator.EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = "5" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true for NOT_EQUAL_TO with number and string conversion") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = "5", + operator = Trigger.OSTriggerOperator.NOT_EQUAL_TO, + ) + mocks.triggerController.triggers["key1"] = 10 + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns true for numeric comparison with string device value") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN, + ) + mocks.triggerController.triggers["key1"] = "20" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe true + } + + test("returns false for numeric comparison with invalid string device value") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + value = 10, + operator = Trigger.OSTriggerOperator.GREATER_THAN, + ) + mocks.triggerController.triggers["key1"] = "not-a-number" + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.evaluateMessageTriggers(message) + + // Then + result shouldBe false + } + } + + context("isTriggerOnMessage") { + test("returns true when message contains trigger key by property") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.isTriggerOnMessage(message, listOf("key1")) + + // Then + result shouldBe true + } + + test("returns true when message contains trigger key by triggerId") { + // Given + val trigger = mocks.createTrigger( + id = "trigger-id-1", + property = "key1", + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.isTriggerOnMessage(message, listOf("trigger-id-1")) + + // Then + result shouldBe true + } + + test("returns false when message does not contain trigger key") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + property = "key1", + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.isTriggerOnMessage(message, listOf("different-key")) + + // Then + result shouldBe false + } + + test("returns false when message has null triggers") { + // Given + val message = mocks.createInAppMessage(triggers = emptyList()) + // Manually set triggers to null using reflection or create a message without triggers + // For now, we'll test with empty triggers which should return false + + // When + val result = mocks.triggerController.isTriggerOnMessage(message, listOf("key1")) + + // Then + result shouldBe false + } + + test("returns true when any of multiple trigger keys match") { + // Given + val trigger1 = mocks.createTrigger( + id = "trigger1", + property = "key1", + ) + val trigger2 = mocks.createTrigger( + id = "trigger2", + property = "key2", + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger1, trigger2))) + + // When + val result = mocks.triggerController.isTriggerOnMessage(message, listOf("key3", "key1")) + + // Then + result shouldBe true + } + } + + context("messageHasOnlyDynamicTriggers") { + test("returns false when message has no triggers") { + // Given + val message = mocks.createInAppMessage(triggers = emptyList()) + + // When + val result = mocks.triggerController.messageHasOnlyDynamicTriggers(message) + + // Then + result shouldBe false + } + + test("returns true when message has only SESSION_TIME triggers") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + kind = Trigger.OSTriggerKind.SESSION_TIME, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.messageHasOnlyDynamicTriggers(message) + + // Then + result shouldBe true + } + + test("returns true when message has only TIME_SINCE_LAST_IN_APP triggers") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + kind = Trigger.OSTriggerKind.TIME_SINCE_LAST_IN_APP, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.messageHasOnlyDynamicTriggers(message) + + // Then + result shouldBe true + } + + test("returns false when message has CUSTOM trigger") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + kind = Trigger.OSTriggerKind.CUSTOM, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.messageHasOnlyDynamicTriggers(message) + + // Then + result shouldBe false + } + + test("returns false when message has UNKNOWN trigger") { + // Given + val trigger = mocks.createTrigger( + id = "trigger1", + kind = Trigger.OSTriggerKind.UNKNOWN, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(trigger))) + + // When + val result = mocks.triggerController.messageHasOnlyDynamicTriggers(message) + + // Then + result shouldBe false + } + + test("returns false when message has mixed dynamic and custom triggers") { + // Given + val dynamicTrigger = mocks.createTrigger( + id = "trigger1", + kind = Trigger.OSTriggerKind.SESSION_TIME, + ) + val customTrigger = mocks.createTrigger( + id = "trigger2", + kind = Trigger.OSTriggerKind.CUSTOM, + ) + val message = mocks.createInAppMessage(triggers = listOf(listOf(dynamicTrigger, customTrigger))) + + // When + val result = mocks.triggerController.messageHasOnlyDynamicTriggers(message) + + // Then + result shouldBe false + } + } + + context("Model store change handlers") { + test("onModelAdded adds trigger and fires event") { + // Given + val model = mocks.createTriggerModel("key1", "value1") + val mockHandler = mockk(relaxed = true) + mocks.triggerController.subscribe(mockHandler) + + // When + mocks.triggerController.onModelAdded(model, "tag") + + // Then + mocks.triggerController.triggers["key1"] shouldBe "value1" + verify { mockHandler.onTriggerChanged("key1") } + } + + test("onModelUpdated updates trigger and fires event") { + // Given + val model = mocks.createTriggerModel("key1", "new-value") + val args = ModelChangedArgs(model, "path", "property", "old-value", "new-value") + val mockHandler = mockk(relaxed = true) + mocks.triggerController.subscribe(mockHandler) + + // When + mocks.triggerController.onModelUpdated(args, "tag") + + // Then + mocks.triggerController.triggers["key1"] shouldBe "new-value" + verify { mockHandler.onTriggerChanged("key1") } + } + + test("onModelRemoved removes trigger") { + // Given + val model = mocks.createTriggerModel("key1", "value1") + mocks.triggerController.triggers["key1"] = "value1" + + // When + mocks.triggerController.onModelRemoved(model, "tag") + + // Then + mocks.triggerController.triggers.containsKey("key1") shouldBe false + } + } + + context("Event subscription") { + test("subscribe adds handler to dynamicTriggerController") { + // Given + val mockHandler = mockk(relaxed = true) + val mockDynamicController = spyk(mocks.dynamicTriggerController) + val triggerController = TriggerController(mocks.triggerModelStore, mockDynamicController) + + // When + triggerController.subscribe(mockHandler) + + // Then + verify { mockDynamicController.subscribe(mockHandler) } + } + + test("unsubscribe removes handler from dynamicTriggerController") { + // Given + val mockHandler = mockk(relaxed = true) + val mockDynamicController = spyk(mocks.dynamicTriggerController) + val triggerController = TriggerController(mocks.triggerModelStore, mockDynamicController) + + // When + triggerController.unsubscribe(mockHandler) + + // Then + verify { mockDynamicController.unsubscribe(mockHandler) } + } + + test("hasSubscribers delegates to dynamicTriggerController") { + // Given + val mockHandler = mockk(relaxed = true) + val mockDynamicController = spyk(mocks.dynamicTriggerController) + every { mockDynamicController.hasSubscribers } returns true + val triggerController = TriggerController(mocks.triggerModelStore, mockDynamicController) + + // When + val result = triggerController.hasSubscribers + + // Then + result shouldBe true + } + } +}) From 2c43b9dd025db6076032fe6c684caaa8c7beead5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:08:11 -0500 Subject: [PATCH 02/19] chore: Release SDK v5.4.2-beta1 (#2513) Co-authored-by: github-actions[bot] --- Examples/OneSignalDemo/gradle.properties | 2 +- OneSignalSDK/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/OneSignalDemo/gradle.properties b/Examples/OneSignalDemo/gradle.properties index 18113c902d..e64953b594 100644 --- a/Examples/OneSignalDemo/gradle.properties +++ b/Examples/OneSignalDemo/gradle.properties @@ -17,4 +17,4 @@ android.enableJetifier=false # 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.4.0 \ No newline at end of file +SDK_VERSION=5.4.2-beta1 \ No newline at end of file diff --git a/OneSignalSDK/gradle.properties b/OneSignalSDK/gradle.properties index 51c56cce11..825d93690a 100644 --- a/OneSignalSDK/gradle.properties +++ b/OneSignalSDK/gradle.properties @@ -39,4 +39,4 @@ android.useAndroidX = 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.4.0 +SDK_VERSION=5.4.2-beta1 From a4ba2a8a1826997928a3e6db25acebbe6e0323d9 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 6 Jan 2026 16:00:24 -0800 Subject: [PATCH 03/19] fix: send receive receipt even when preventDefault is called (#2512) --- .../generation/impl/NotificationGenerationProcessor.kt | 2 +- .../generation/NotificationGenerationProcessorTests.kt | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt index d05e907f24..5014500409 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt @@ -233,9 +233,9 @@ internal class NotificationGenerationProcessor( // Notification channel disable or not displayed // save notification as dismissed to avoid user re-enabling channel and notification being displayed due to restore markNotificationAsDismissed(notificationJob) - return } + // Always call notificationReceived, regardless of wasDisplayed _lifecycleService.notificationReceived(notificationJob) } diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt index 154b879a11..d740fe13da 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt @@ -168,6 +168,10 @@ class NotificationGenerationProcessorTests : FunSpec({ mocks.notificationGenerationProcessor.processNotificationData(mocks.context, 1, mocks.notificationPayload, false, 1111) // Then + // notificationReceived should be called + coVerify(exactly = 1) { + mocks.notificationLifecycleService.notificationReceived(any()) + } } test("processNotificationData should display notification when external callback takes longer than 30 seconds") { @@ -209,6 +213,10 @@ class NotificationGenerationProcessorTests : FunSpec({ mocks.notificationGenerationProcessor.processNotificationData(mocks.context, 1, mocks.notificationPayload, false, 1111) // Then + // notificationReceived should be called + coVerify(exactly = 1) { + mocks.notificationLifecycleService.notificationReceived(any()) + } } test("processNotificationData should display notification when foreground callback takes longer than 30 seconds") { From a827295adb9870e0f9c2b34b598121b2d2a05752 Mon Sep 17 00:00:00 2001 From: jinliu Date: Thu, 8 Jan 2026 12:00:31 -0500 Subject: [PATCH 04/19] fix: crash around com.onesignal.SyncJobService (#2515) --- .../core/src/main/AndroidManifest.xml | 7 +++++ .../main/java/com/onesignal/SyncJobService.kt | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/SyncJobService.kt diff --git a/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml b/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml index 69421766c8..285ce5c588 100644 --- a/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml +++ b/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml @@ -10,6 +10,13 @@ android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false" /> + + + + diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/SyncJobService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/SyncJobService.kt new file mode 100644 index 0000000000..26b264f4e8 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/SyncJobService.kt @@ -0,0 +1,31 @@ +package com.onesignal + +import android.app.job.JobParameters +import android.app.job.JobService + +/** + * Temporary shim for old OneSignal v4 jobs pointing to com.onesignal.SyncJobService. + * + * The v4 SyncJobService was used to run background sync; v5 has moved to a new architecture, + * but scheduled jobs from the old service can still be lingering in JobScheduler on devices + * that previously had v4 installed. When those old jobs fire, the system tries to instantiate + * com.onesignal.SyncJobService. Since the class isn’t in the v5 SDK anymore, we will get a + * ClassNotFoundException and a crash. + * + * Providing a no-op implementation with the same fully qualified name lets those old jobs run + * once, do nothing, and finish, which clears them out. This is intentionally a no-op and + * finishes immediately. + */ +class SyncJobService : JobService() { + + override fun onStartJob(params: JobParameters?): Boolean { + // Job finishes immediately, nothing to do. + jobFinished(params, false) + return false // No more work on a background thread. + } + + override fun onStopJob(params: JobParameters?): Boolean { + // Don't reschedule the job. + return false + } +} From 359e8bec4c0ce064bc8d0597ccf0519ca6828962 Mon Sep 17 00:00:00 2001 From: Sherwin Heydarbeygi Date: Fri, 23 Jan 2026 15:27:02 -0800 Subject: [PATCH 05/19] ci: remove "Auto-Generated Release Notes" header from create-release-pr workflow (#2526) --- .github/workflows/create-release-pr.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index b1badcf805..ce11e837bb 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -110,9 +110,6 @@ jobs: - name: Generate Release Notes from PR Titles id: generate_notes run: | - echo "## πŸ”– Auto-Generated Release Notes" > pr_body.md - echo "" >> pr_body.md - if [[ "$VERSION" == *"alpha"* ]]; then CHANNEL="alpha" elif [[ "$VERSION" == *"beta"* ]]; then From cbb373fab48f5f1e9f8616fb390f89f483ac2cda Mon Sep 17 00:00:00 2001 From: abdulraqeeb33 Date: Mon, 26 Jan 2026 10:44:01 -0500 Subject: [PATCH 06/19] chore: Restrict test coverage just for the lines of code changed (#2527) Co-authored-by: AR Abdul Azeez --- OneSignalSDK/coverage/checkCoverage.sh | 168 ++++++++++++++++++++++--- 1 file changed, 149 insertions(+), 19 deletions(-) diff --git a/OneSignalSDK/coverage/checkCoverage.sh b/OneSignalSDK/coverage/checkCoverage.sh index 1f0814c1f9..26708f0af0 100755 --- a/OneSignalSDK/coverage/checkCoverage.sh +++ b/OneSignalSDK/coverage/checkCoverage.sh @@ -2,6 +2,7 @@ # Diff Coverage Check Script # This script generates coverage reports and checks diff coverage against the base branch +# Only checks coverage for newly added/modified lines (not entire files) # Uses a manual coverage check that reliably matches JaCoCo paths to git diff paths # # Usage: @@ -25,7 +26,10 @@ SKIP_COVERAGE_CHECK=${SKIP_COVERAGE_CHECK:-false} # Set to 'true' to bypass cov # Get script directory and project root SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# PROJECT_ROOT is the OneSignalSDK directory (where build reports are) PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# REPO_ROOT is the git repository root (parent of OneSignalSDK) +REPO_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)" # Paths relative to project root COVERAGE_REPORT="$PROJECT_ROOT/build/reports/jacoco/merged/jacocoMergedReport.xml" @@ -42,7 +46,7 @@ if [ "$SKIP_COVERAGE_CHECK" = "true" ]; then BYPASS_REASON="SKIP_COVERAGE_CHECK environment variable set" elif [ -n "$GITHUB_EVENT_NAME" ] && [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then # Check commit messages for bypass keyword - cd "$PROJECT_ROOT" + cd "$REPO_ROOT" COMMIT_MESSAGES=$(git log --format=%B origin/main..HEAD 2>/dev/null || git log --format=%B "$BASE_BRANCH"..HEAD 2>/dev/null || echo "") if echo "$COMMIT_MESSAGES" | grep -qiE "\[skip coverage\]|\[bypass coverage\]|\[no coverage\]"; then BYPASS_REASON="Commit message contains [skip coverage] keyword" @@ -71,8 +75,13 @@ echo -e "${YELLOW}[2/3] Checking diff coverage against $BASE_BRANCH...${NC}" echo -e "${YELLOW}Threshold: ${COVERAGE_THRESHOLD}%${NC}\n" # Get changed files (run from project root) -cd "$PROJECT_ROOT" -CHANGED_FILES=$(git diff --name-only "$BASE_BRANCH"...HEAD 2>/dev/null | grep -E '\.(kt|java)$' || true) +# Include committed changes, staged changes, and unstaged changes +cd "$REPO_ROOT" +COMMITTED_FILES=$(git diff --name-only "$BASE_BRANCH"...HEAD 2>/dev/null | grep -E '\.(kt|java)$' || true) +STAGED_FILES=$(git diff --cached --name-only 2>/dev/null | grep -E '\.(kt|java)$' || true) +UNSTAGED_FILES=$(git diff --name-only 2>/dev/null | grep -E '\.(kt|java)$' || true) +# Combine all, remove duplicates, and filter to OneSignalSDK files +CHANGED_FILES=$(echo -e "$COMMITTED_FILES\n$STAGED_FILES\n$UNSTAGED_FILES" | grep -E '^OneSignalSDK/' | sort -u || true) if [ -z "$CHANGED_FILES" ]; then echo -e "${BLUE}No Kotlin/Java files changed${NC}\n" @@ -91,17 +100,93 @@ else export COVERAGE_REPORT export GENERATE_MARKDOWN export MARKDOWN_REPORT + export BASE_BRANCH + export REPO_ROOT python3 << PYEOF import xml.etree.ElementTree as ET import re import sys import os +import subprocess coverage_report = os.environ.get('COVERAGE_REPORT') threshold = int(os.environ.get('COVERAGE_THRESHOLD', '80')) changed_files_str = """$CHANGED_FILES""" generate_markdown = os.environ.get('GENERATE_MARKDOWN', 'false').lower() == 'true' markdown_report = os.environ.get('MARKDOWN_REPORT', 'diff_coverage.md') +base_branch = os.environ.get('BASE_BRANCH', 'origin/main') +repo_root_env = os.environ.get('REPO_ROOT') + +def get_changed_lines(file_path, project_root): + """Get line numbers of added/modified lines from git diff""" + try: + # First try to get diff from committed changes + result = subprocess.run( + ['git', 'diff', '--unified=0', base_branch + '...HEAD', '--', file_path], + capture_output=True, + text=True, + cwd=project_root + ) + + # If no committed changes, check staged changes + if result.returncode != 0 or not result.stdout.strip(): + result = subprocess.run( + ['git', 'diff', '--cached', '--unified=0', '--', file_path], + capture_output=True, + text=True, + cwd=project_root + ) + + # If no staged changes, check unstaged changes + if result.returncode != 0 or not result.stdout.strip(): + result = subprocess.run( + ['git', 'diff', '--unified=0', '--', file_path], + capture_output=True, + text=True, + cwd=project_root + ) + + # If still nothing, try alternative base branch format + if result.returncode != 0 or not result.stdout.strip(): + result = subprocess.run( + ['git', 'diff', '--unified=0', base_branch, 'HEAD', '--', file_path], + capture_output=True, + text=True, + cwd=project_root + ) + + if result.returncode != 0 or not result.stdout.strip(): + return None + + changed_lines = set() + current_new_line = None + + for line in result.stdout.split('\n'): + # Parse unified diff format + # @@ -old_start,old_count +new_start,new_count @@ + match = re.match(r'@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@', line) + if match: + current_new_line = int(match.group(1)) + count = int(match.group(2)) if match.group(2) else 1 + # The count tells us how many lines are in this hunk + # We'll track them as we see + lines + elif line.startswith('+') and not line.startswith('+++'): + # Added/modified line (starts with +) + if current_new_line is not None: + changed_lines.add(current_new_line) + current_new_line += 1 + elif line.startswith('-') and not line.startswith('---'): + # Deleted line - don't add to changed_lines, don't increment current_new_line + pass + elif line.startswith(' '): + # Context line (unchanged, starts with space) - increment current_new_line + if current_new_line is not None: + current_new_line += 1 + + return changed_lines if changed_lines else None + except Exception as e: + # Silently fail and return None - we'll fall back to checking all lines + return None try: tree = ET.parse(coverage_report) @@ -110,6 +195,22 @@ except Exception as e: print(f"Error parsing coverage report: {e}") sys.exit(1) +# Get repository root - prefer environment variable, then try to detect from coverage report path +if repo_root_env: + project_root = repo_root_env +else: + # Fallback: try to detect from coverage report path + # Coverage report is in OneSignalSDK/build/..., so go up two levels to get repo root + detected_root = os.path.dirname(os.path.dirname(coverage_report)) if '/build/' in coverage_report else os.path.dirname(coverage_report) + # Look for OneSignalSDK in the path and go one level up + parts = coverage_report.split('/') + if 'OneSignalSDK' in parts: + idx = parts.index('OneSignalSDK') + project_root = '/'.join(parts[:idx]) + else: + # Fallback: assume we're in repo root + project_root = os.getcwd() + changed_files = [f.strip() for f in changed_files_str.split('\n') if f.strip()] total_uncovered = 0 @@ -119,7 +220,7 @@ files_checked = [] markdown_output = [] if generate_markdown: - markdown_output.append("## Diff Coverage Report\n") + markdown_output.append("## Diff Coverage Report (Changed Lines Only)\n") markdown_output.append(f"**Threshold:** {threshold}%\n\n") markdown_output.append("### Changed Files Coverage\n\n") @@ -141,6 +242,9 @@ for changed_file in changed_files: filename = match.group(3) package_name = package_path.replace('/', '/') + # Get changed line numbers for this file + changed_lines = get_changed_lines(changed_file, project_root) + # Find in coverage report found = False for package in root.findall(f'.//package[@name="{package_name}"]'): @@ -149,26 +253,38 @@ for changed_file in changed_files: files_checked.append(filename) lines = sourcefile.findall('line') - file_total = len([l for l in lines if int(l.get('mi', 0)) > 0 or int(l.get('ci', 0)) > 0]) - file_covered = len([l for l in lines if int(l.get('ci', 0)) > 0]) - file_uncovered = len([l for l in lines if l.get('ci') == '0' and int(l.get('mi', 0)) > 0]) + + # Filter to only changed lines if we have that info + if changed_lines is not None and len(changed_lines) > 0: + # Only check lines that were added/modified + relevant_lines = [l for l in lines if int(l.get('nr', 0)) in changed_lines] + else: + # Fallback: check all lines if we can't get changed lines + relevant_lines = lines + + # Count only executable lines (mi > 0 means instructions exist) + file_total = len([l for l in relevant_lines if int(l.get('mi', 0)) > 0 or int(l.get('ci', 0)) > 0]) + file_covered = len([l for l in relevant_lines if int(l.get('ci', 0)) > 0]) + file_uncovered = len([l for l in relevant_lines if l.get('ci') == '0' and int(l.get('mi', 0)) > 0]) if file_total > 0: total_lines += file_total total_uncovered += file_uncovered - coverage_pct = (file_covered / file_total * 100) + coverage_pct = (file_covered / file_total * 100) if file_total > 0 else 100 if generate_markdown: status = "βœ…" if coverage_pct >= threshold else "❌" - markdown_output.append(f"- {status} **{filename}**: {file_covered}/{file_total} lines ({coverage_pct:.1f}%)") + changed_info = f" ({len(changed_lines)} changed lines)" if changed_lines else " (all lines - could not determine changed lines)" + markdown_output.append(f"- {status} **{filename}**: {file_covered}/{file_total} changed lines ({coverage_pct:.1f}%){changed_info}") if coverage_pct < threshold: files_below_threshold.append((filename, coverage_pct, file_uncovered)) - markdown_output.append(f" - ⚠️ Below threshold: {file_uncovered} uncovered lines") + markdown_output.append(f" - ⚠️ Below threshold: {file_uncovered} uncovered changed lines") else: status = "βœ“" if coverage_pct >= threshold else "βœ—" color = "" if coverage_pct >= threshold else "\033[0;31m" reset = "\033[0m" if color else "" - print(f" {color}{status}{reset} {filename}: {file_covered}/{file_total} lines ({coverage_pct:.1f}%)") + changed_info = f" ({len(changed_lines)} changed lines)" if changed_lines else " (all lines - could not determine changed lines)" + print(f" {color}{status}{reset} {filename}: {file_covered}/{file_total} changed lines ({coverage_pct:.1f}%){changed_info}") if coverage_pct < threshold: files_below_threshold.append((filename, coverage_pct, file_uncovered)) break @@ -185,14 +301,14 @@ if total_lines > 0: overall_coverage = ((total_lines - total_uncovered) / total_lines * 100) if generate_markdown: - markdown_output.append(f"\n### Overall Coverage\n") - markdown_output.append(f"**{total_lines - total_uncovered}/{total_lines}** lines covered ({overall_coverage:.1f}%)\n") + markdown_output.append(f"\n### Overall Coverage (Changed Lines Only)\n") + markdown_output.append(f"**{total_lines - total_uncovered}/{total_lines}** changed lines covered ({overall_coverage:.1f}%)\n") if files_below_threshold: markdown_output.append(f"\n### ❌ Coverage Check Failed\n") markdown_output.append(f"Files below {threshold}% threshold:\n") for filename, pct, uncovered in files_below_threshold: - markdown_output.append(f"- **{filename}**: {pct:.1f}% ({uncovered} uncovered lines)\n") + markdown_output.append(f"- **{filename}**: {pct:.1f}% ({uncovered} uncovered changed lines)\n") # Write markdown file with open(markdown_report, 'w') as f: @@ -206,15 +322,15 @@ if total_lines > 0: else: sys.exit(0) else: - print(f"\n Overall: {(total_lines - total_uncovered)}/{total_lines} lines covered ({overall_coverage:.1f}%)") + print(f"\n Overall: {(total_lines - total_uncovered)}/{total_lines} changed lines covered ({overall_coverage:.1f}%)") if files_below_threshold: print(f"\n Files below {threshold}% threshold:") for filename, pct, uncovered in files_below_threshold: - print(f" β€’ {filename}: {pct:.1f}% ({uncovered} uncovered lines)") + print(f" β€’ {filename}: {pct:.1f}% ({uncovered} uncovered changed lines)") sys.exit(1) else: - print(f"\n βœ“ All files meet {threshold}% threshold") + print(f"\n βœ“ All files meet {threshold}% threshold for changed lines") sys.exit(0) elif files_checked: # Files were found but had no executable lines @@ -279,13 +395,27 @@ echo -e "${YELLOW}[3/3] Generating HTML coverage report...${NC}" # Try to generate HTML report using diff-cover if available, otherwise skip if python3 -m diff_cover.diff_cover_tool --version &>/dev/null 2>&1; then # Try diff-cover for HTML report (may not work due to path issues, but worth trying) - cd "$PROJECT_ROOT" - python3 -m diff_cover.diff_cover_tool "build/reports/jacoco/merged/jacocoMergedReport.xml" \ + cd "$REPO_ROOT" + # Check if there are uncommitted changes - if so, we need to handle them differently + STAGED_COUNT=$(git diff --cached --name-only 2>/dev/null | grep -E '\.(kt|java)$' | wc -l | tr -d ' ') + UNSTAGED_COUNT=$(git diff --name-only 2>/dev/null | grep -E '\.(kt|java)$' | wc -l | tr -d ' ') + + if [ "$STAGED_COUNT" -gt 0 ] || [ "$UNSTAGED_COUNT" -gt 0 ]; then + # There are uncommitted changes - diff-cover won't see them with --compare-branch + # So we'll note this in the output + echo -e "${YELLOW} Note: HTML report shows committed changes only${NC}" + echo -e "${YELLOW} Uncommitted changes are checked in the console output above${NC}" + fi + + python3 -m diff_cover.diff_cover_tool "$PROJECT_ROOT/build/reports/jacoco/merged/jacocoMergedReport.xml" \ --compare-branch="$BASE_BRANCH" \ --format html:"$HTML_REPORT" 2>&1 | grep -v "No lines with coverage" || true if [ -f "$HTML_REPORT" ]; then echo -e "${GREEN}βœ“ HTML report generated: $HTML_REPORT${NC}" + if [ "$STAGED_COUNT" -gt 0 ] || [ "$UNSTAGED_COUNT" -gt 0 ]; then + echo -e "${YELLOW} Note: Report shows committed changes only (uncommitted changes shown in console)${NC}" + fi echo -e "${BLUE} Open it in your browser to see detailed coverage${NC}\n" else echo -e "${YELLOW} HTML report generation had issues (non-fatal)${NC}\n" From 696dc54fd62e18c297837faf686bce26af6a1d09 Mon Sep 17 00:00:00 2001 From: jinliu Date: Mon, 26 Jan 2026 11:45:04 -0500 Subject: [PATCH 07/19] chore: merge 5.3.0 beta into main and resolve conflict (#2525) --- .../java/com/onesignal/common/JSONUtils.kt | 55 + .../operations/impl/OperationModelStore.kt | 3 + .../java/com/onesignal/user/IUserManager.kt | 11 + .../java/com/onesignal/user/UserModule.kt | 8 + .../onesignal/user/internal/UserManager.kt | 15 + .../ICustomEventBackendService.kt | 24 + .../customEvents/ICustomEventController.kt | 8 + .../impl/CustomEventBackendService.kt | 53 + .../impl/CustomEventController.kt | 32 + .../customEvents/impl/CustomEventMetadata.kt | 39 + .../operations/TrackCustomEventOperation.kt | 85 ++ .../executors/CustomEventOperationExecutor.kt | 71 ++ .../com/onesignal/common/JSONUtilsTests.kt | 945 ++++++++++++++++++ .../user/internal/UserManagerTests.kt | 53 +- .../backend/CustomEventBackendServiceTests.kt | 86 ++ .../CustomEventOperationExecutorTests.kt | 66 ++ .../java/com/onesignal/mocks/MockHelper.kt | 7 + 17 files changed, 1556 insertions(+), 5 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt index 75ba75db7c..3169fd3d16 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt @@ -163,4 +163,59 @@ object JSONUtils { `object` } } + + /** + * Check if an object is JSON-serializable. + * Recursively check each item if object is a map or a list. + */ + fun isValidJsonObject(value: Any?): Boolean { + return when (value) { + null, + is Boolean, + is Number, + is String, + is JSONObject, + is JSONArray, + -> true + is Map<*, *> -> value.keys.all { it is String } && value.values.all { isValidJsonObject(it) } + is List<*> -> value.all { isValidJsonObject(it) } + else -> false + } + } + + /** + * Recursively convert a JSON-serializable map into a JSON-compatible format, handling + * nested Maps and Lists appropriately. + */ + fun mapToJson(map: Map): JSONObject { + val json = JSONObject() + for ((key, value) in map) { + json.put(key, convertToJson(value)) + } + return json + } + + /** + * Recursively converts maps and lists into JSON-compatible objects, transforming maps with + * String keys into JSON objects, lists into JSON arrays, and leaving primitive values unchanged to support safe JSON serialization. + */ + fun convertToJson(value: Any): Any { + return when (value) { + is Map<*, *> -> { + val subMap = + value.entries + .filter { it.key is String } + .associate { + it.key as String to convertToJson(it.value!!) + } + mapToJson(subMap) + } + is List<*> -> { + val array = JSONArray() + value.forEach { array.put(convertToJson(it!!)) } + array + } + else -> value + } + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt index 1fa25fe631..c3d6e45910 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt @@ -14,11 +14,13 @@ import com.onesignal.user.internal.operations.RefreshUserOperation import com.onesignal.user.internal.operations.SetAliasOperation import com.onesignal.user.internal.operations.SetPropertyOperation import com.onesignal.user.internal.operations.SetTagOperation +import com.onesignal.user.internal.operations.TrackCustomEventOperation import com.onesignal.user.internal.operations.TrackPurchaseOperation import com.onesignal.user.internal.operations.TrackSessionEndOperation import com.onesignal.user.internal.operations.TrackSessionStartOperation import com.onesignal.user.internal.operations.TransferSubscriptionOperation import com.onesignal.user.internal.operations.UpdateSubscriptionOperation +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor @@ -60,6 +62,7 @@ internal class OperationModelStore(prefs: IPreferencesService) : ModelStore TrackSessionStartOperation() UpdateUserOperationExecutor.TRACK_SESSION_END -> TrackSessionEndOperation() UpdateUserOperationExecutor.TRACK_PURCHASE -> TrackPurchaseOperation() + CustomEventOperationExecutor.CUSTOM_EVENT -> TrackCustomEventOperation() else -> throw Exception("Unrecognized operation: $operationName") } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt index 2b71ca11de..7ebf37f14f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt @@ -166,4 +166,15 @@ interface IUserManager { * Remove an observer from the user state. */ fun removeObserver(observer: IUserStateObserver) + + /** + * Tracks a custom event performed by the current user + * + * @param name for the custom event + * @param properties an optional property dictionary, must be serializable into a JSON Object + */ + fun trackEvent( + name: String, + properties: Map? = null, + ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt index f8dc3c4959..be55228756 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt @@ -16,9 +16,14 @@ import com.onesignal.user.internal.backend.impl.SubscriptionBackendService import com.onesignal.user.internal.backend.impl.UserBackendService import com.onesignal.user.internal.builduser.IRebuildUserService import com.onesignal.user.internal.builduser.impl.RebuildUserService +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import com.onesignal.user.internal.customEvents.ICustomEventController +import com.onesignal.user.internal.customEvents.impl.CustomEventBackendService +import com.onesignal.user.internal.customEvents.impl.CustomEventController import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.migrations.RecoverConfigPushSubscription import com.onesignal.user.internal.migrations.RecoverFromDroppedLoginBug +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor @@ -71,6 +76,9 @@ internal class UserModule : IModule { builder.register().provides() builder.register().provides() builder.register().provides() + builder.register().provides() + builder.register().provides() + builder.register().provides() builder.register().provides() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt index 934d233183..60f322b805 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt @@ -1,6 +1,7 @@ package com.onesignal.user.internal import com.onesignal.common.IDManager +import com.onesignal.common.JSONUtils import com.onesignal.common.OneSignalUtils import com.onesignal.common.events.EventProducer import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler @@ -10,6 +11,7 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.IUserManager import com.onesignal.user.internal.backend.IdentityConstants +import com.onesignal.user.internal.customEvents.ICustomEventController import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.properties.PropertiesModel @@ -25,6 +27,7 @@ internal open class UserManager( private val _subscriptionManager: ISubscriptionManager, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, + private val _customEventController: ICustomEventController, private val _languageContext: ILanguageContext, ) : IUserManager, ISingletonModelStoreChangeHandler { override val onesignalId: String @@ -244,6 +247,18 @@ internal open class UserManager( changeHandlersNotifier.unsubscribe(observer) } + override fun trackEvent( + name: String, + properties: Map?, + ) { + if (!JSONUtils.isValidJsonObject(properties)) { + Logging.log(LogLevel.ERROR, "Custom event properties are not JSON-serializable") + return + } + + _customEventController.sendCustomEvent(name, properties) + } + override fun onModelReplaced( model: IdentityModel, tag: String, diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt new file mode 100644 index 0000000000..92474635ab --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt @@ -0,0 +1,24 @@ +package com.onesignal.user.internal.customEvents + +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata + +/** + * The backend service for custom events. + */ +interface ICustomEventBackendService { + /** + * Send an custom event to the backend and return the response. + * + * @param customEvent The custom event to send up. + */ + suspend fun sendCustomEvent( + appId: String, + onesignalId: String, + externalId: String?, + timestamp: Long, + eventName: String, + eventProperties: String?, + metadata: CustomEventMetadata, + ): ExecutionResponse +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt new file mode 100644 index 0000000000..35c307539c --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt @@ -0,0 +1,8 @@ +package com.onesignal.user.internal.customEvents + +interface ICustomEventController { + fun sendCustomEvent( + name: String, + properties: Map?, + ) +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt new file mode 100644 index 0000000000..096fa67456 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt @@ -0,0 +1,53 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.common.DateUtils +import com.onesignal.common.exceptions.BackendException +import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import org.json.JSONArray +import org.json.JSONObject +import java.util.TimeZone + +internal class CustomEventBackendService( + private val httpClient: IHttpClient, +) : ICustomEventBackendService { + override suspend fun sendCustomEvent( + appId: String, + onesignalId: String, + externalId: String?, + timestamp: Long, + eventName: String, + eventProperties: String?, + metadata: CustomEventMetadata, + ): ExecutionResponse { + val body = JSONObject() + body.put("name", eventName) + body.put("onesignal_id", onesignalId) + externalId?.let { body.put("external_id", it) } + body.put( + "timestamp", + DateUtils.iso8601Format().apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format( + timestamp, + ), + ) + + val payload = eventProperties?.let { JSONObject(it) } ?: JSONObject() + + payload.put("os_sdk", metadata.toJSONObject()) + + body.put("payload", payload) + val jsonObject = JSONObject().put("events", JSONArray().put(body)) + + val response = httpClient.post("apps/$appId/custom_events", jsonObject) + + if (!response.isSuccess) { + throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) + } + + return ExecutionResponse(ExecutionResult.SUCCESS) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt new file mode 100644 index 0000000000..05142fe784 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt @@ -0,0 +1,32 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.common.JSONUtils +import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.operations.IOperationRepo +import com.onesignal.core.internal.time.ITime +import com.onesignal.user.internal.customEvents.ICustomEventController +import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.operations.TrackCustomEventOperation + +class CustomEventController( + private val identityModelStore: IdentityModelStore, + private val configModelStore: ConfigModelStore, + private val time: ITime, + private val opRepo: IOperationRepo, +) : ICustomEventController { + override fun sendCustomEvent( + name: String, + properties: Map?, + ) { + val op = + TrackCustomEventOperation( + configModelStore.model.appId, + identityModelStore.model.onesignalId, + identityModelStore.model.externalId, + time.currentTimeMillis, + name, + properties?.let { JSONUtils.mapToJson(it).toString() }, + ) + opRepo.enqueue(op) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt new file mode 100644 index 0000000000..cd14d6a909 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt @@ -0,0 +1,39 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.common.putSafe +import org.json.JSONException +import org.json.JSONObject + +class CustomEventMetadata( + val deviceType: String?, + val sdk: String?, + val appVersion: String?, + val type: String?, + val deviceModel: String?, + val deviceOS: String?, +) { + @Throws(JSONException::class) + fun toJSONObject(): JSONObject { + val json = JSONObject() + json.putSafe(SDK, sdk) + json.putSafe(APP_VERSION, appVersion) + json.putSafe(TYPE, type) + json.putSafe(DEVICE_TYPE, deviceType) + json.putSafe(DEVICE_MODEL, deviceModel) + json.putSafe(DEVICE_OS, deviceOS) + return json + } + + override fun toString(): String { + return toJSONObject().toString() + } + + companion object { + private const val DEVICE_TYPE = "device_type" + private const val SDK = "sdk" + private const val APP_VERSION = "app_version" + private const val TYPE = "type" + private const val DEVICE_MODEL = "device_model" + private const val DEVICE_OS = "device_os" + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt new file mode 100644 index 0000000000..73313f97ec --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt @@ -0,0 +1,85 @@ +package com.onesignal.user.internal.operations + +import com.onesignal.common.IDManager +import com.onesignal.core.internal.operations.GroupComparisonType +import com.onesignal.core.internal.operations.Operation +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor + +class TrackCustomEventOperation() : Operation(CustomEventOperationExecutor.CUSTOM_EVENT) { + /** + * The OneSignal appId the custom event was created. + */ + var appId: String + get() = getStringProperty(::appId.name) + private set(value) { + setStringProperty(::appId.name, value) + } + + /** + * The OneSignal ID the custom event was created under. This ID *may* be locally generated + * and can be checked via [IDManager.isLocalId] to ensure correct processing. + */ + var onesignalId: String + get() = getStringProperty(::onesignalId.name) + private set(value) { + setStringProperty(::onesignalId.name, value) + } + + /** + * The optional external ID of current logged-in user. Must be unique for the [appId]. + */ + var externalId: String? + get() = getOptStringProperty(::externalId.name) + private set(value) { + setOptStringProperty(::externalId.name, value) + } + + /** + * The timestamp when the custom event was created. + */ + var timeStamp: Long + get() = getLongProperty(::timeStamp.name) + private set(value) { + setLongProperty(::timeStamp.name, value) + } + + /** + * The name for the custom event. + */ + var eventName: String + get() = getStringProperty(::eventName.name) + set(value) { + setAnyProperty(::eventName.name, value) + } + + /** + * The nullable properties for the custom event. + */ + var eventProperties: String? + get() = getOptStringProperty(::eventProperties.name) + set(value) { + setOptStringProperty(::eventProperties.name, value) + } + + override val createComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$eventName" + override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$eventName" + + override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE + override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) + override val applyToRecordId: String get() = onesignalId + + constructor(appId: String, onesignalId: String, externalId: String?, timeStamp: Long, eventName: String, eventProperties: String?) : this() { + this.appId = appId + this.onesignalId = onesignalId + this.externalId = externalId + this.timeStamp = timeStamp + this.eventName = eventName + this.eventProperties = eventProperties + } + + override fun translateIds(map: Map) { + if (map.containsKey(onesignalId)) { + onesignalId = map[onesignalId]!! + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt new file mode 100644 index 0000000000..2e1046e6c6 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt @@ -0,0 +1,71 @@ +package com.onesignal.user.internal.operations.impl.executors + +import android.os.Build +import com.onesignal.common.AndroidUtils +import com.onesignal.common.NetworkUtils +import com.onesignal.common.OneSignalUtils +import com.onesignal.common.exceptions.BackendException +import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.device.IDeviceService +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.core.internal.operations.IOperationExecutor +import com.onesignal.core.internal.operations.Operation +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata +import com.onesignal.user.internal.operations.TrackCustomEventOperation + +internal class CustomEventOperationExecutor( + private val customEventBackendService: ICustomEventBackendService, + private val applicationService: IApplicationService, + private val deviceService: IDeviceService, +) : IOperationExecutor { + override val operations: List + get() = listOf(CUSTOM_EVENT) + + private val eventMetadataJson: CustomEventMetadata by lazy { + CustomEventMetadata( + deviceType = deviceService.deviceType.name, + sdk = OneSignalUtils.sdkVersion, + appVersion = AndroidUtils.getAppVersion(applicationService.appContext), + type = "AndroidPush", + deviceModel = Build.MODEL, + deviceOS = Build.VERSION.RELEASE, + ) + } + + override suspend fun execute(operations: List): ExecutionResponse { + val operation = operations.first() + + try { + when (operation) { + is TrackCustomEventOperation -> { + customEventBackendService.sendCustomEvent( + operation.appId, + operation.onesignalId, + operation.externalId, + operation.timeStamp, + operation.eventName, + operation.eventProperties, + eventMetadataJson, + ) + } + } + } catch (ex: BackendException) { + val responseType = NetworkUtils.getResponseStatusType(ex.statusCode) + + return when (responseType) { + NetworkUtils.ResponseStatusType.RETRYABLE -> + ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) + else -> + ExecutionResponse(ExecutionResult.FAIL_NORETRY) + } + } + + return ExecutionResponse(ExecutionResult.SUCCESS) + } + + companion object { + const val CUSTOM_EVENT = "custom-event" + } +} diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt new file mode 100644 index 0000000000..1320369ca4 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt @@ -0,0 +1,945 @@ +package com.onesignal.common + +import android.os.Bundle +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import io.mockk.every +import io.mockk.mockk +import org.json.JSONArray +import org.json.JSONObject + +class JSONUtilsTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + } + + context("wrapInJsonArray") { + test("should wrap a JSONObject in a JSONArray") { + // Given + val jsonObject = JSONObject().apply { + put("key", "value") + } + + // When + val result = JSONUtils.wrapInJsonArray(jsonObject) + + // Then + result.length() shouldBe 1 + result.getJSONObject(0).getString("key") shouldBe "value" + } + + test("should handle null JSONObject") { + // Given + val jsonObject: JSONObject? = null + + // When + val result = JSONUtils.wrapInJsonArray(jsonObject) + + // Then + result.length() shouldBe 1 + result.isNull(0) shouldBe true + } + + test("should wrap empty JSONObject") { + // Given + val jsonObject = JSONObject() + + // When + val result = JSONUtils.wrapInJsonArray(jsonObject) + + // Then + result.length() shouldBe 1 + result.getJSONObject(0).length() shouldBe 0 + } + } + + context("bundleAsJSONObject") { + test("should convert Bundle to JSONObject") { + // Given + val bundle = mockk(relaxed = true) + val keySet = setOf("stringKey", "intKey", "boolKey") + every { bundle.keySet() } returns keySet + every { bundle["stringKey"] } returns "stringValue" + every { bundle["intKey"] } returns 42 + every { bundle["boolKey"] } returns true + + // When + val result = JSONUtils.bundleAsJSONObject(bundle) + + // Then + result.getString("stringKey") shouldBe "stringValue" + result.getInt("intKey") shouldBe 42 + result.getBoolean("boolKey") shouldBe true + } + + test("should handle empty Bundle") { + // Given + val bundle = mockk(relaxed = true) + every { bundle.keySet() } returns emptySet() + + // When + val result = JSONUtils.bundleAsJSONObject(bundle) + + // Then + result.length() shouldBe 0 + } + + test("should handle Bundle with null values") { + // Given + val bundle = mockk(relaxed = true) + val keySet = setOf("key1", "key2") + every { bundle.keySet() } returns keySet + every { bundle["key1"] } returns "value1" + every { bundle["key2"] } returns null + + // When + val result = JSONUtils.bundleAsJSONObject(bundle) + + // Then + result.getString("key1") shouldBe "value1" + result.isNull("key2") shouldBe true + } + } + + context("jsonStringToBundle") { + test("should return null for invalid JSON string") { + // Given + val invalidJson = "{invalid json}" + + // When + val result = JSONUtils.jsonStringToBundle(invalidJson) + + // Then + result shouldBe null + } + + test("should return null for empty string") { + // Given + val emptyString = "" + + // When + val result = JSONUtils.jsonStringToBundle(emptyString) + + // Then + result shouldBe null + } + } + + context("newStringMapFromJSONObject") { + test("should convert JSONObject to Map") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("key2", "value2") + put("key3", 123) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 3 + result["key1"] shouldBe "value1" + result["key2"] shouldBe "value2" + result["key3"] shouldBe "123" + } + + test("should handle null values as empty string") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("key2", JSONObject.NULL) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result["key1"] shouldBe "value1" + result["key2"] shouldBe "" + } + + test("should omit nested JSONObjects") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("nested", JSONObject().apply { put("inner", "value") }) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 1 + result["key1"] shouldBe "value1" + result.containsKey("nested") shouldBe false + } + + test("should omit JSONArrays") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("array", JSONArray().put("item1").put("item2")) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 1 + result["key1"] shouldBe "value1" + result.containsKey("array") shouldBe false + } + + test("should handle empty JSONObject") { + // Given + val jsonObject = JSONObject() + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 0 + } + } + + context("newStringSetFromJSONArray") { + test("should convert JSONArray to Set") { + // Given + val jsonArray = JSONArray().apply { + put("item1") + put("item2") + put("item3") + } + + // When + val result = JSONUtils.newStringSetFromJSONArray(jsonArray) + + // Then + result.size shouldBe 3 + result shouldBe setOf("item1", "item2", "item3") + } + + test("should handle empty JSONArray") { + // Given + val jsonArray = JSONArray() + + // When + val result = JSONUtils.newStringSetFromJSONArray(jsonArray) + + // Then + result.size shouldBe 0 + } + + test("should handle JSONArray with duplicate values") { + // Given + val jsonArray = JSONArray().apply { + put("item1") + put("item2") + put("item1") + } + + // When + val result = JSONUtils.newStringSetFromJSONArray(jsonArray) + + // Then + result.size shouldBe 2 + result shouldBe setOf("item1", "item2") + } + } + + context("toUnescapedEUIDString") { + test("should unescape forward slashes in external_user_id") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "user/123") + put("other_key", "value") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"user/123\"" + result shouldNotContain "user\\/123" + } + + test("should handle JSON without external_user_id") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("key2", "value2") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "key1" + result shouldContain "value1" + } + + test("should handle external_user_id without slashes") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "user123") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"user123\"" + } + + test("should handle multiple escaped slashes") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "user/123/456") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"user/123/456\"" + result shouldNotContain "\\/" + } + + test("should handle empty external_user_id") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"\"" + } + } + + context("compareJSONArrays") { + test("should return true for equal JSONArrays") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().apply { + put("item1") + put("item2") + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe true + } + + test("should return true for both null arrays") { + // When + val result = JSONUtils.compareJSONArrays(null, null) + + // Then + result shouldBe true + } + + test("should return false when one array is null") { + // Given + val array1 = JSONArray().put("item1") + + // When + val result1 = JSONUtils.compareJSONArrays(array1, null) + val result2 = JSONUtils.compareJSONArrays(null, array1) + + // Then + result1 shouldBe false + result2 shouldBe false + } + + test("should return false for arrays of different sizes") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().put("item1") + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe false + } + + test("should return false for arrays with different items") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().apply { + put("item1") + put("item3") + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe false + } + + test("should handle arrays with different order but same items") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().apply { + put("item2") + put("item1") + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe true + } + + test("should handle arrays with numbers") { + // Given + val array1 = JSONArray().apply { + put(1) + put(2) + } + val array2 = JSONArray().apply { + put(1) + put(2) + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe true + } + } + + context("normalizeType") { + test("should convert Int to Long") { + // Given + val intValue = 42 + + // When + val result = JSONUtils.normalizeType(intValue) + + // Then + result shouldBe 42L + } + + test("should convert Float to Double") { + // Given + val floatValue = 3.14f + + // When + val result = JSONUtils.normalizeType(floatValue) + + // Then + // Float to Double conversion has precision differences, so use approximate comparison + result shouldNotBe null + val doubleValue = result as? Number + doubleValue shouldNotBe null + val difference = kotlin.math.abs(doubleValue!!.toDouble() - 3.14) + (difference < 0.0001) shouldBe true + } + + test("should return other types unchanged") { + // Given + val stringValue = "test" + val boolValue = true + val longValue = 100L + + // When + val stringResult = JSONUtils.normalizeType(stringValue) + val boolResult = JSONUtils.normalizeType(boolValue) + val longResult = JSONUtils.normalizeType(longValue) + + // Then + stringResult shouldBe "test" + boolResult shouldBe true + longResult shouldBe 100L + } + } + + context("isValidJsonObject") { + test("should return true for primitive types") { + // Then + JSONUtils.isValidJsonObject(null) shouldBe true + JSONUtils.isValidJsonObject(true) shouldBe true + JSONUtils.isValidJsonObject(false) shouldBe true + JSONUtils.isValidJsonObject(42) shouldBe true + JSONUtils.isValidJsonObject(3.14) shouldBe true + JSONUtils.isValidJsonObject("string") shouldBe true + } + + test("should return true for JSONObject and JSONArray") { + // Given + val jsonObject = JSONObject() + val jsonArray = JSONArray() + + // Then + JSONUtils.isValidJsonObject(jsonObject) shouldBe true + JSONUtils.isValidJsonObject(jsonArray) shouldBe true + } + + test("should return true for valid Map with String keys") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to 42, + "key3" to true, + ) + + // When + val result = JSONUtils.isValidJsonObject(map) + + // Then + result shouldBe true + } + + test("should return false for Map with non-String keys") { + // Given + val map = mapOf( + 1 to "value1", + 2 to "value2", + ) + + // When + val result = JSONUtils.isValidJsonObject(map) + + // Then + result shouldBe false + } + + test("should return true for valid List") { + // Given + val list = listOf("item1", "item2", 42, true) + + // When + val result = JSONUtils.isValidJsonObject(list) + + // Then + result shouldBe true + } + + test("should return true for nested valid structures") { + // Given + val nestedMap = mapOf( + "key1" to "value1", + "key2" to mapOf( + "nestedKey" to "nestedValue", + ), + "key3" to listOf("item1", "item2"), + ) + + // When + val result = JSONUtils.isValidJsonObject(nestedMap) + + // Then + result shouldBe true + } + + test("should return false for nested invalid structures") { + // Given + val invalidMap = mapOf( + "key1" to "value1", + "key2" to mapOf( + 1 to "invalid", // non-String key + ), + ) + + // When + val result = JSONUtils.isValidJsonObject(invalidMap) + + // Then + result shouldBe false + } + + test("should return false for non-JSON types") { + // Then + JSONUtils.isValidJsonObject(Any()) shouldBe false + JSONUtils.isValidJsonObject(Exception()) shouldBe false + JSONUtils.isValidJsonObject(Thread.currentThread()) shouldBe false + } + + test("should return true for List containing valid nested structures") { + // Given + val list = listOf( + "string", + 42, + mapOf("key" to "value"), + listOf("nested", "items"), + ) + + // When + val result = JSONUtils.isValidJsonObject(list) + + // Then + result shouldBe true + } + + test("should return false for List containing invalid types") { + // Given + val list = listOf( + "string", + Any(), // invalid type + ) + + // When + val result = JSONUtils.isValidJsonObject(list) + + // Then + result shouldBe false + } + } + + context("mapToJson") { + test("should convert simple map to JSONObject") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to 42, + "key3" to true, + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getString("key1") shouldBe "value1" + result.getInt("key2") shouldBe 42 + result.getBoolean("key3") shouldBe true + } + + test("should convert empty map to empty JSONObject") { + // Given + val map = emptyMap() + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.length() shouldBe 0 + } + + test("should convert map with nested map") { + // Given + val map = mapOf( + "key1" to "value1", + "nested" to mapOf( + "nestedKey1" to "nestedValue1", + "nestedKey2" to 100, + ), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getString("key1") shouldBe "value1" + val nested = result.getJSONObject("nested") + nested.getString("nestedKey1") shouldBe "nestedValue1" + nested.getInt("nestedKey2") shouldBe 100 + } + + test("should convert map with list values") { + // Given + val map = mapOf( + "key1" to listOf("item1", "item2", "item3"), + "key2" to listOf(1, 2, 3), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + val array1 = result.getJSONArray("key1") + array1.length() shouldBe 3 + array1.getString(0) shouldBe "item1" + array1.getString(1) shouldBe "item2" + array1.getString(2) shouldBe "item3" + + val array2 = result.getJSONArray("key2") + array2.length() shouldBe 3 + array2.getInt(0) shouldBe 1 + array2.getInt(1) shouldBe 2 + array2.getInt(2) shouldBe 3 + } + + test("should convert map with deeply nested structures") { + // Given + val map = mapOf( + "level1" to mapOf( + "level2" to mapOf( + "level3" to "deepValue", + ), + ), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + val level1 = result.getJSONObject("level1") + val level2 = level1.getJSONObject("level2") + level2.getString("level3") shouldBe "deepValue" + } + + test("should convert map with list containing maps") { + // Given + val map = mapOf( + "items" to listOf( + mapOf("name" to "item1", "value" to 10), + mapOf("name" to "item2", "value" to 20), + ), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + val array = result.getJSONArray("items") + array.length() shouldBe 2 + val item1 = array.getJSONObject(0) + item1.getString("name") shouldBe "item1" + item1.getInt("value") shouldBe 10 + val item2 = array.getJSONObject(1) + item2.getString("name") shouldBe "item2" + item2.getInt("value") shouldBe 20 + } + + test("should handle null values") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to JSONObject.NULL, + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getString("key1") shouldBe "value1" + result.isNull("key2") shouldBe true + } + + test("should handle different number types") { + // Given + val map = mapOf( + "int" to 42, + "long" to 100L, + "double" to 3.14, + "float" to 2.5f, + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getInt("int") shouldBe 42 + result.getLong("long") shouldBe 100L + result.getDouble("double") shouldBe 3.14 + // Float precision may differ, so check approximately + (kotlin.math.abs(result.getDouble("float") - 2.5) < 0.0001) shouldBe true + } + } + + context("convertToJson") { + test("should return primitive values unchanged") { + // Then + JSONUtils.convertToJson("string") shouldBe "string" + JSONUtils.convertToJson(42) shouldBe 42 + JSONUtils.convertToJson(true) shouldBe true + JSONUtils.convertToJson(false) shouldBe false + JSONUtils.convertToJson(3.14) shouldBe 3.14 + } + + test("should convert Map to JSONObject") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to 42, + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + (result is JSONObject) shouldBe true + val jsonObject = result as JSONObject + jsonObject.getString("key1") shouldBe "value1" + jsonObject.getInt("key2") shouldBe 42 + } + + test("should convert List to JSONArray") { + // Given + val list = listOf("item1", "item2", "item3") + + // When + val result = JSONUtils.convertToJson(list) + + // Then + (result is JSONArray) shouldBe true + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 3 + jsonArray.getString(0) shouldBe "item1" + jsonArray.getString(1) shouldBe "item2" + jsonArray.getString(2) shouldBe "item3" + } + + test("should convert nested Map recursively") { + // Given + val map = mapOf( + "outer" to mapOf( + "inner" to "value", + ), + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + val inner = jsonObject.getJSONObject("outer") + inner.getString("inner") shouldBe "value" + } + + test("should convert List containing Maps") { + // Given + val list = listOf( + mapOf("key1" to "value1"), + mapOf("key2" to "value2"), + ) + + // When + val result = JSONUtils.convertToJson(list) + + // Then + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 2 + val item1 = jsonArray.getJSONObject(0) + item1.getString("key1") shouldBe "value1" + val item2 = jsonArray.getJSONObject(1) + item2.getString("key2") shouldBe "value2" + } + + test("should convert Map containing List") { + // Given + val map = mapOf( + "items" to listOf("a", "b", "c"), + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + val array = jsonObject.getJSONArray("items") + array.length() shouldBe 3 + array.getString(0) shouldBe "a" + array.getString(1) shouldBe "b" + array.getString(2) shouldBe "c" + } + + test("should convert empty List to empty JSONArray") { + // Given + val list = emptyList() + + // When + val result = JSONUtils.convertToJson(list) + + // Then + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 0 + } + + test("should convert empty Map to empty JSONObject") { + // Given + val map = emptyMap() + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + jsonObject.length() shouldBe 0 + } + + test("should handle List with mixed types") { + // Given + val list = listOf("string", 42, true, 3.14) + + // When + val result = JSONUtils.convertToJson(list) + + // Then + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 4 + jsonArray.getString(0) shouldBe "string" + jsonArray.getInt(1) shouldBe 42 + jsonArray.getBoolean(2) shouldBe true + jsonArray.getDouble(3) shouldBe 3.14 + } + + test("should filter out non-String keys from Map") { + // Given + val map = mapOf( + "validKey" to "value1", + 123 to "value2", // non-String key should be filtered + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + jsonObject.length() shouldBe 1 + jsonObject.getString("validKey") shouldBe "value1" + jsonObject.has("123") shouldBe false + } + + test("should handle deeply nested structures") { + // Given + val structure = mapOf( + "level1" to listOf( + mapOf( + "level2" to listOf( + mapOf("level3" to "deepValue"), + ), + ), + ), + ) + + // When + val result = JSONUtils.convertToJson(structure) + + // Then + val jsonObject = result as JSONObject + val level1Array = jsonObject.getJSONArray("level1") + val level1Item = level1Array.getJSONObject(0) + val level2Array = level1Item.getJSONArray("level2") + val level2Item = level2Array.getJSONObject(0) + level2Item.getString("level3") shouldBe "deepValue" + } + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt index 41e836eb9b..ada9f00f68 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt @@ -4,7 +4,9 @@ import com.onesignal.core.internal.language.ILanguageContext import com.onesignal.mocks.MockHelper import com.onesignal.user.internal.subscriptions.ISubscriptionManager import com.onesignal.user.internal.subscriptions.SubscriptionList +import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.mockk.every @@ -26,7 +28,7 @@ class UserManagerTests : FunSpec({ every { languageContext.language = capture(languageSlot) } answers { } val userManager = - UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), languageContext) + UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), MockHelper.customEventController(), languageContext) // When userManager.setLanguage("new-language") @@ -44,7 +46,7 @@ class UserManagerTests : FunSpec({ } val userManager = - UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext()) + UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.customEventController(), MockHelper.languageContext()) // When val externalId = userManager.externalId @@ -63,7 +65,7 @@ class UserManagerTests : FunSpec({ } val userManager = - UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext()) + UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.customEventController(), MockHelper.languageContext()) // When val alias1 = userManager.aliases["my-alias-key1"] @@ -102,7 +104,7 @@ class UserManagerTests : FunSpec({ } val userManager = - UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext()) + UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.customEventController(), MockHelper.languageContext()) // When val tag1 = propertiesModelStore.model.tags["my-tag-key1"] @@ -141,7 +143,7 @@ class UserManagerTests : FunSpec({ it.tags["my-tag-key1"] = "my-tag-value1" } - val userManager = UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext()) + val userManager = UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.customEventController(), MockHelper.languageContext()) // When val tagSnapshot1 = userManager.getTags() @@ -173,6 +175,7 @@ class UserManagerTests : FunSpec({ mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), + MockHelper.customEventController(), MockHelper.languageContext(), ) @@ -191,4 +194,44 @@ class UserManagerTests : FunSpec({ verify(exactly = 1) { mockSubscriptionManager.addSmsSubscription("+15558675309") } verify(exactly = 1) { mockSubscriptionManager.removeSmsSubscription("+15558675309") } } + + test("custom event controller sends various types of properties") { + // Given + val customEventController = MockHelper.customEventController() + + val userManager = + UserManager(mockk(), MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), customEventController, MockHelper.languageContext()) + + val eventName = "eventName" + val properties = + mapOf( + "key1" to "value1", + "key2" to 2, + "key3" to 5.123, + "key4" to mapOf("key4-1" to "value4-1"), + "key5" to mapOf("key5-1" to mapOf("key5-1-1" to 0)), + ) + + // When + // should be able to handle any of the map structures above + shouldNotThrow { + userManager.trackEvent( + eventName, + properties, + ) + } + + // Then + // ensure the controller call sendCustomEvent() with the correct name and properties + verify(exactly = 1) { + customEventController.sendCustomEvent( + withArg { + it.shouldBeEqual(eventName) + }, + withArg { + it.shouldBeEqual(properties) + }, + ) + } + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt new file mode 100644 index 0000000000..924d7f9f3f --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt @@ -0,0 +1,86 @@ +package com.onesignal.user.internal.backend + +import com.onesignal.common.DateUtils +import com.onesignal.core.internal.http.HttpResponse +import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.user.internal.customEvents.impl.CustomEventBackendService +import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.json.JSONObject +import java.util.TimeZone + +class CustomEventBackendServiceTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + } + + val metadata = + CustomEventMetadata( + "Android", + "sdk", + "1.0", + "type", + "deviceModel", + "deviceOS", + ) + + test("track event") { + // Given + val spyHttpClient = mockk() + coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(202, "") + val customEventBackendService = CustomEventBackendService(spyHttpClient) + + // When + val properties = JSONObject().put("proKey1", "proVal1").toString() + + val response = + customEventBackendService.sendCustomEvent( + appId = "appId", + onesignalId = "onesignalId", + externalId = null, + timestamp = 1, + eventName = "event-name", + eventProperties = properties, + metadata = metadata, + ) + + // Then + response.result shouldBe ExecutionResult.SUCCESS + coVerify { + spyHttpClient.post( + "apps/appId/custom_events", + withArg { + val eventsObject = it.getJSONArray("events").getJSONObject(0) + val eventMap = mutableMapOf() + for (key in eventsObject.keys()) { + eventMap[key] = eventsObject.get(key) + } + + eventMap["name"] shouldBe "event-name" + eventMap["onesignal_id"] shouldBe "onesignalId" + eventMap["external_id"] shouldBe null + eventMap["timestamp"] shouldBe + DateUtils + .iso8601Format() + .apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format( + 1, + ) + + val payload = eventMap["payload"] as JSONObject + payload.getJSONObject("os_sdk").toString() shouldBeEqual metadata.toJSONObject().toString() + payload.getString("proKey1") shouldBeEqual "proVal1" + }, + ) + } + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt new file mode 100644 index 0000000000..044d4c3726 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt @@ -0,0 +1,66 @@ +package com.onesignal.user.internal.operations + +import android.content.Context +import android.os.Build +import com.onesignal.common.OneSignalUtils +import com.onesignal.core.internal.device.IDeviceService +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.core.internal.operations.Operation +import com.onesignal.mocks.MockHelper +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import org.json.JSONObject + +class CustomEventOperationExecutorTests : FunSpec({ + test("execution of track event operation") { + // Given + val mockCustomEventBackendService = mockk() + coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS) + + val mockApplicationService = MockHelper.applicationService() + val mockContext = mockk(relaxed = true) + every { mockApplicationService.appContext } returns mockContext + val mockDeviceService = MockHelper.deviceService() + every { mockDeviceService.deviceType } returns IDeviceService.DeviceType.Android + + val deviceMode = Build.MODEL + val deviceOS = Build.VERSION.RELEASE + val properties = JSONObject().put("key", "value").toString() + + val customEventOperationExecutor = + CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService) + val operations = listOf(TrackCustomEventOperation("appId", "onesignalId", null, 1, "event-name", properties)) + + // When + val response = customEventOperationExecutor.execute(operations) + + // Then + response.result shouldBe ExecutionResult.SUCCESS + coVerify(exactly = 1) { + mockCustomEventBackendService.sendCustomEvent( + "appId", + "onesignalId", + null, + 1, + "event-name", + properties, + withArg { + it.sdk shouldBe OneSignalUtils.sdkVersion + it.appVersion?.shouldBeEqual("0") + it.type?.shouldBeEqual(("AndroidPush")) + it.deviceType?.shouldBeEqual(("Android")) + it.deviceModel shouldBe deviceMode + it.deviceOS shouldBe deviceOS + }, + ) + } + } +}) diff --git a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt index d8fa8ed86a..500b736e79 100644 --- a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt @@ -8,6 +8,7 @@ import com.onesignal.core.internal.language.ILanguageContext import com.onesignal.core.internal.time.ITime import com.onesignal.session.internal.session.SessionModel import com.onesignal.session.internal.session.SessionModelStore +import com.onesignal.user.internal.customEvents.ICustomEventController import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.properties.PropertiesModel @@ -126,4 +127,10 @@ object MockHelper { every { deviceService.deviceType } returns IDeviceService.DeviceType.Android return deviceService } + + fun customEventController(): ICustomEventController { + val controller = mockk() + every { controller.sendCustomEvent(any(), any()) } just runs + return controller + } } From 7636afce2421a0e38f0d058fdbefe21f0fcf174c Mon Sep 17 00:00:00 2001 From: jinliu Date: Mon, 26 Jan 2026 11:57:05 -0500 Subject: [PATCH 08/19] chore: improve testing with injected coroutine dispatchers (#2524) Co-authored-by: AR Abdul Azeez --- .../threading/CoroutineDispatcherProvider.kt | 23 + .../threading/DefaultDispatcherProvider.kt | 26 + .../core/internal/startup/StartupService.kt | 8 +- .../outcomes/impl/OutcomeEventsRepository.kt | 16 +- .../internal/startup/StartupServiceTests.kt | 190 ++-- .../onesignal/internal/OneSignalImpTests.kt | 72 +- .../outcomes/OutcomeEventsRepositoryTests.kt | 873 +++++++++--------- .../data/impl/NotificationRepository.kt | 32 +- .../onesignal/mocks/TestDispatcherProvider.kt | 65 ++ 9 files changed, 745 insertions(+), 560 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt create mode 100644 OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt new file mode 100644 index 0000000000..8b57c5659d --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt @@ -0,0 +1,23 @@ +package com.onesignal.common.threading + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job + +/** + * Provider interface for coroutine dispatchers. + * This allows for proper dependency injection and easier testing. + */ +interface CoroutineDispatcherProvider { + val io: CoroutineDispatcher + val default: CoroutineDispatcher + + /** + * Launch a coroutine on the IO dispatcher. + */ + fun launchOnIO(block: suspend () -> Unit): Job + + /** + * Launch a coroutine on the Default dispatcher. + */ + fun launchOnDefault(block: suspend () -> Unit): Job +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt new file mode 100644 index 0000000000..8ca50d5b5e --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt @@ -0,0 +1,26 @@ +package com.onesignal.common.threading + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job + +/** + * Production implementation of [CoroutineDispatcherProvider] that uses OneSignalDispatchers. + * + * This delegates to the existing scopes in OneSignalDispatchers to avoid creating duplicate scopes. + * The OneSignalDispatchers already maintains IOScope and DefaultScope with SupervisorJob, + * so we reuse those instead of creating new ones. + */ +class DefaultDispatcherProvider : CoroutineDispatcherProvider { + override val io: CoroutineDispatcher = OneSignalDispatchers.IO + override val default: CoroutineDispatcher = OneSignalDispatchers.Default + + override fun launchOnIO(block: suspend () -> Unit): Job { + // Delegate to OneSignalDispatchers which already has IOScope with SupervisorJob + return OneSignalDispatchers.launchOnIO(block) + } + + override fun launchOnDefault(block: suspend () -> Unit): Job { + // Delegate to OneSignalDispatchers which already has DefaultScope with SupervisorJob + return OneSignalDispatchers.launchOnDefault(block) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt index 9d1c112d64..6d209734f9 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt @@ -1,18 +1,20 @@ package com.onesignal.core.internal.startup import com.onesignal.common.services.ServiceProvider -import com.onesignal.common.threading.OneSignalDispatchers +import com.onesignal.common.threading.CoroutineDispatcherProvider +import com.onesignal.common.threading.DefaultDispatcherProvider internal class StartupService( private val services: ServiceProvider, + private val dispatchers: CoroutineDispatcherProvider = DefaultDispatcherProvider(), ) { fun bootstrap() { services.getAllServices().forEach { it.bootstrap() } } - // schedule to start all startable services using OneSignal dispatcher + // schedule to start all startable services using the provided dispatcher fun scheduleStart() { - OneSignalDispatchers.launchOnDefault { + dispatchers.launchOnDefault { services.getAllServices().forEach { it.start() } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt index 8a9b7ecb66..16c4f2d32f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt @@ -1,6 +1,7 @@ package com.onesignal.session.internal.outcomes.impl import android.content.ContentValues +import com.onesignal.common.threading.OneSignalDispatchers import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.OneSignalDbContract import com.onesignal.debug.internal.logging.Logging @@ -9,7 +10,7 @@ import com.onesignal.session.internal.influence.InfluenceChannel import com.onesignal.session.internal.influence.InfluenceType import com.onesignal.session.internal.influence.InfluenceType.Companion.fromString import com.onesignal.session.internal.outcomes.migrations.RemoveInvalidSessionTimeRecords -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONException @@ -17,12 +18,13 @@ import java.util.Locale internal class OutcomeEventsRepository( private val _databaseProvider: IDatabaseProvider, + private val ioDispatcher: CoroutineDispatcher = OneSignalDispatchers.IO, ) : IOutcomeEventsRepository { /** * Delete event from the DB */ override suspend fun deleteOldOutcomeEvent(event: OutcomeEventParams) { - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { _databaseProvider.os.delete( OutcomeEventsTable.TABLE_NAME, OutcomeEventsTable.COLUMN_NAME_TIMESTAMP + " = ?", @@ -36,7 +38,7 @@ internal class OutcomeEventsRepository( * For offline mode and contingency of errors */ override suspend fun saveOutcomeEvent(eventParams: OutcomeEventParams) { - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { var notificationIds = JSONArray() var iamIds = JSONArray() var notificationInfluenceType = InfluenceType.UNATTRIBUTED @@ -101,7 +103,7 @@ internal class OutcomeEventsRepository( */ override suspend fun getAllEventsToSend(): List { val events: MutableList = ArrayList() - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { RemoveInvalidSessionTimeRecords.run(_databaseProvider) _databaseProvider.os.query(OutcomeEventsTable.TABLE_NAME) { cursor -> if (cursor.moveToFirst()) { @@ -248,7 +250,7 @@ internal class OutcomeEventsRepository( override suspend fun saveUniqueOutcomeEventParams(eventParams: OutcomeEventParams) { Logging.debug("OutcomeEventsCache.saveUniqueOutcomeEventParams(eventParams: $eventParams)") - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { val outcomeName = eventParams.outcomeId val cachedUniqueOutcomes: MutableList = ArrayList() val directBody = eventParams.outcomeSource?.directBody @@ -283,7 +285,7 @@ internal class OutcomeEventsRepository( ): List { val uniqueInfluences: MutableList = ArrayList() - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { try { for (influence in influences) { val availableInfluenceIds = JSONArray() @@ -333,7 +335,7 @@ internal class OutcomeEventsRepository( val notificationTableName = OneSignalDbContract.NotificationTable.TABLE_NAME val notificationIdColumnName = OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { val whereStr = "NOT EXISTS(" + "SELECT NULL FROM " + notificationTableName + " n " + diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt index 7416b2910c..820ed4fe37 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt @@ -4,8 +4,7 @@ import com.onesignal.common.services.ServiceBuilder import com.onesignal.common.services.ServiceProvider import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.mocks.IOMockHelper -import com.onesignal.mocks.IOMockHelper.awaitIO +import com.onesignal.mocks.TestDispatcherProvider import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.comparables.shouldBeLessThan @@ -14,7 +13,12 @@ import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +@OptIn(ExperimentalCoroutinesApi::class) class StartupServiceTests : FunSpec({ fun setupServiceProvider( bootstrapServices: List, @@ -27,111 +31,127 @@ class StartupServiceTests : FunSpec({ serviceBuilder.register(reg).provides() return serviceBuilder.build() } - - listener(IOMockHelper) + val testDispatcher = StandardTestDispatcher() + val dispatcherProvider = TestDispatcherProvider(testDispatcher) beforeAny { Logging.logLevel = LogLevel.NONE } test("bootstrap with no IBootstrapService dependencies is a no-op") { - // Given - val startupService = StartupService(setupServiceProvider(listOf(), listOf())) + runTest(testDispatcher.scheduler) { + // Given + val startupService = StartupService(setupServiceProvider(listOf(), listOf()), dispatcherProvider) - // When - startupService.bootstrap() + // When + startupService.bootstrap() - // Then + // Then + } } test("bootstrap will call all IBootstrapService dependencies successfully") { - // Given - val mockBootstrapService1 = mockk(relaxed = true) - val mockBootstrapService2 = mockk(relaxed = true) + runTest(testDispatcher.scheduler) { + // Given + val mockBootstrapService1 = mockk(relaxed = true) + val mockBootstrapService2 = mockk(relaxed = true) - val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf())) + val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider) - // When - startupService.bootstrap() + // When + startupService.bootstrap() - // Then - verify(exactly = 1) { mockBootstrapService1.bootstrap() } - verify(exactly = 1) { mockBootstrapService2.bootstrap() } + // Then + verify(exactly = 1) { mockBootstrapService1.bootstrap() } + verify(exactly = 1) { mockBootstrapService2.bootstrap() } + } } test("bootstrap will propagate exception when an IBootstrapService throws an exception") { - // Given - val exception = Exception("SOMETHING BAD") - - val mockBootstrapService1 = mockk() - every { mockBootstrapService1.bootstrap() } throws exception - val mockBootstrapService2 = spyk() - - val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf())) - - // When - val actualException = - shouldThrowUnit { - startupService.bootstrap() - } - - // Then - actualException shouldBe exception - verify(exactly = 1) { mockBootstrapService1.bootstrap() } - verify(exactly = 0) { mockBootstrapService2.bootstrap() } + runTest(testDispatcher.scheduler) { + // Given + val exception = Exception("SOMETHING BAD") + + val mockBootstrapService1 = mockk() + every { mockBootstrapService1.bootstrap() } throws exception + val mockBootstrapService2 = spyk() + + val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider) + + // When + val actualException = + shouldThrowUnit { + startupService.bootstrap() + } + + // Then + actualException shouldBe exception + verify(exactly = 1) { mockBootstrapService1.bootstrap() } + verify(exactly = 0) { mockBootstrapService2.bootstrap() } + } } test("startup will call all IStartableService dependencies successfully after a short delay") { - // Given - val mockStartupService1 = mockk(relaxed = true) - val mockStartupService2 = mockk(relaxed = true) - - val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2))) - - // When - startupService.scheduleStart() - - // Then - wait deterministically for both services to start using IOMockHelper - awaitIO() - verify(exactly = 1) { mockStartupService1.start() } - verify(exactly = 1) { mockStartupService2.start() } + runTest(testDispatcher.scheduler) { + // Given + val mockStartupService1 = mockk(relaxed = true) + val mockStartupService2 = mockk(relaxed = true) + + val startupService = StartupService( + setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2)), + dispatcherProvider + ) + + // When + startupService.scheduleStart() + + // Then - wait deterministically for both services to start using advanceUntilIdle + advanceUntilIdle() + verify(exactly = 1) { mockStartupService1.start() } + verify(exactly = 1) { mockStartupService2.start() } + } } test("scheduleStart does not block main thread") { - // Given - val mockStartableService1 = mockk(relaxed = true) - val mockStartableService2 = spyk() - val mockStartableService3 = spyk() - // Only service1 and service2 are scheduled - service3 is NOT scheduled - val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2))) - - // When - scheduleStart() is async, so it doesn't block - val startTime = System.currentTimeMillis() - startupService.scheduleStart() - val scheduleTime = System.currentTimeMillis() - startTime - - // This should execute immediately since scheduleStart() doesn't block - // service3 is NOT part of scheduled services, so this is a direct call - mockStartableService3.start() - val immediateTime = System.currentTimeMillis() - startTime - - // Then - verify scheduleStart() returned quickly (non-blocking) - // Should return in < 50ms (proving it doesn't wait for services to start) - scheduleTime shouldBeLessThan 50L - immediateTime shouldBeLessThan 50L - - // Verify service3 was called immediately (proving main thread wasn't blocked) - verify(exactly = 1) { mockStartableService3.start() } - - // Wait deterministically for async execution using IOMockHelper - awaitIO() - - // Verify scheduled services were called - verify(exactly = 1) { mockStartableService1.start() } - verify(exactly = 1) { mockStartableService2.start() } - - // The key assertion: scheduleStart() returned immediately without blocking, - // allowing service3.start() to be called synchronously before scheduled services - // complete. This proves scheduleStart() is non-blocking. + runTest(testDispatcher.scheduler) { + // Given + val mockStartableService1 = mockk(relaxed = true) + val mockStartableService2 = spyk() + val mockStartableService3 = spyk() + // Only service1 and service2 are scheduled - service3 is NOT scheduled + val startupService = StartupService( + setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2)), + dispatcherProvider + ) + + // When - scheduleStart() is async, so it doesn't block + val startTime = System.currentTimeMillis() + startupService.scheduleStart() + val scheduleTime = System.currentTimeMillis() - startTime + + // This should execute immediately since scheduleStart() doesn't block + // service3 is NOT part of scheduled services, so this is a direct call + mockStartableService3.start() + val immediateTime = System.currentTimeMillis() - startTime + + // Then - verify scheduleStart() returned quickly (non-blocking) + // Should return in < 50ms (proving it doesn't wait for services to start) + scheduleTime shouldBeLessThan 50L + immediateTime shouldBeLessThan 50L + + // Verify service3 was called immediately (proving main thread wasn't blocked) + verify(exactly = 1) { mockStartableService3.start() } + + // Wait deterministically for async execution using advanceUntilIdle + advanceUntilIdle() + + // Verify scheduled services were called + verify(exactly = 1) { mockStartableService1.start() } + verify(exactly = 1) { mockStartableService2.start() } + + // The key assertion: scheduleStart() returned immediately without blocking, + // allowing service3.start() to be called synchronously before scheduled services + // complete. This proves scheduleStart() is non-blocking. + } } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt index d660fa2525..08cfc6d79f 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt @@ -2,41 +2,51 @@ package com.onesignal.internal import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging +import com.onesignal.mocks.TestDispatcherProvider import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest class OneSignalImpTests : FunSpec({ beforeAny { Logging.logLevel = LogLevel.NONE } + val testDispatcher = StandardTestDispatcher() + val dispatcherProvider = TestDispatcherProvider(testDispatcher) + test("attempting login before initWithContext throws exception") { - // Given - val os = OneSignalImp() + runTest(testDispatcher.scheduler) { + // Given + val os = OneSignalImp() - // When - val exception = - shouldThrowUnit { - os.login("login-id") - } + // When + val exception = + shouldThrowUnit { + os.login("login-id") + } - // Then - exception.message shouldBe "Must call 'initWithContext' before 'login'" + // Then + exception.message shouldBe "Must call 'initWithContext' before 'login'" + } } test("attempting logout before initWithContext throws exception") { - // Given - val os = OneSignalImp() + runTest(testDispatcher.scheduler) { + // Given + val os = OneSignalImp() - // When - val exception = - shouldThrowUnit { - os.logout() - } + // When + val exception = + shouldThrowUnit { + os.logout() + } - // Then - exception.message shouldBe "Must call 'initWithContext' before 'logout'" + // Then + exception.message shouldBe "Must call 'initWithContext' before 'logout'" + } } // Comprehensive tests for deprecated properties that should work before and after initialization @@ -44,7 +54,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then os.consentRequired shouldBe false @@ -52,7 +62,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When os.consentRequired = true @@ -69,7 +79,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then - should not throw os.consentRequired = false @@ -82,7 +92,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then os.consentGiven shouldBe false @@ -90,7 +100,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When os.consentGiven = true @@ -107,7 +117,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then - should not throw os.consentGiven = true @@ -120,7 +130,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then os.disableGMSMissingPrompt shouldBe false @@ -128,7 +138,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When os.disableGMSMissingPrompt = true @@ -145,7 +155,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then - should not throw os.disableGMSMissingPrompt = true @@ -157,7 +167,7 @@ class OneSignalImpTests : FunSpec({ context("property consistency tests") { test("all properties maintain state correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When - set all properties to true os.consentRequired = true @@ -182,7 +192,7 @@ class OneSignalImpTests : FunSpec({ test("properties are independent of each other") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When - set only consentRequired to true os.consentRequired = true @@ -218,7 +228,7 @@ class OneSignalImpTests : FunSpec({ // waitForInit() would timeout after 30 seconds and log a warning (not throw) // Given - a fresh OneSignalImp instance - val oneSignalImp = OneSignalImp() + val oneSignalImp = OneSignalImp(dispatcherProvider.io) // The timeout behavior is built into waitUntilInitInternal() // which uses withTimeout() to wait for up to 30 seconds (or 4.8 seconds on main thread) @@ -236,7 +246,7 @@ class OneSignalImpTests : FunSpec({ // until initialization completes (per PR #2412) // Given - val oneSignalImp = OneSignalImp() + val oneSignalImp = OneSignalImp(dispatcherProvider.io) // We can verify the wait behavior by checking: // 1. The suspendCompletion (CompletableDeferred) is properly initialized diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt index e7fad98ac3..f1cd47491c 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt @@ -4,6 +4,7 @@ import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.DatabaseMockHelper +import com.onesignal.mocks.TestDispatcherProvider import com.onesignal.session.internal.influence.Influence import com.onesignal.session.internal.influence.InfluenceChannel import com.onesignal.session.internal.influence.InfluenceType @@ -19,6 +20,9 @@ import io.kotest.matchers.shouldNotBe import io.mockk.verify import io.mockk.verifyAll import io.mockk.verifySequence +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.runTest import org.json.JSONArray @RobolectricTest @@ -27,481 +31,512 @@ class OutcomeEventsRepositoryTests : FunSpec({ Logging.logLevel = LogLevel.NONE } + // avoids initialization happening too early (before Robolectric’s environment exists). + lateinit var testDispatcher: TestDispatcher + lateinit var dispatcherProvider: TestDispatcherProvider + + beforeTest { + testDispatcher = StandardTestDispatcher() + dispatcherProvider = TestDispatcherProvider(testDispatcher) + } + test("delete outcome event should use the timestamp to delete row from database") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.deleteOldOutcomeEvent(OutcomeEventParams("outcomeId", null, 0f, 0, 1111)) - - // Then - verify(exactly = 1) { - mockDatabasePair.second.delete( - OutcomeEventsTable.TABLE_NAME, - withArg { - it.contains(OutcomeEventsTable.COLUMN_NAME_TIMESTAMP) - }, - withArg { it.contains("1111") }, - ) + runTest(dispatcherProvider.io) { + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.deleteOldOutcomeEvent(OutcomeEventParams("outcomeId", null, 0f, 0, 1111)) + + // Then + verify(exactly = 1) { + mockDatabasePair.second.delete( + OutcomeEventsTable.TABLE_NAME, + withArg { + it.contains(OutcomeEventsTable.COLUMN_NAME_TIMESTAMP) + }, + withArg { it.contains("1111") }, + ) + } } } test("save outcome event should insert row into database") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveOutcomeEvent(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId2", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1")), - OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), - ), - .2f, - 0, - 2222, - ), - ) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId3", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), - null, - ), - .4f, - 0, - 3333, - ), - ) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId4", - OutcomeSource( - null, - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1").put("iamId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveOutcomeEvent(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId2", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1")), + OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), + ), + .2f, + 0, + 2222, ), - .6f, - 0, - 4444, - ), - ) - - // Then - verifySequence { - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe 0f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 1111L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "unattributed" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray().toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "unattributed" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray().toString() - }, ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId2" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .2f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 2222L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() - }, - ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId3" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .4f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 3333L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\"]").toString() - }, + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId3", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), + null, + ), + .4f, + 0, + 3333, + ), ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId4" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .6f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 4444L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() - }, + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId4", + OutcomeSource( + null, + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1").put("iamId2")), + ), + .6f, + 0, + 4444, + ), ) + + // Then + verifySequence { + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe 0f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 1111L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "unattributed" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray().toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "unattributed" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray().toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId2" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .2f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 2222L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId3" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .4f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 3333L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\"]").toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId4" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .6f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 4444L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() + }, + ) + } } } test("get events should retrieve return empty list when database is empty") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - // When - val events = outcomeEventsRepository.getAllEventsToSend() + // When + val events = outcomeEventsRepository.getAllEventsToSend() - // Then - events.count() shouldBe 0 + // Then + events.count() shouldBe 0 + } } test("get events should retrieve return an item per row in database") { - // Given - val records = - listOf( - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId1", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.2f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 1111L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 1L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "unattributed", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "unattributed", - ), - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId2", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.4f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 2222L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 2L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "indirect", - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId1\",\"notificationId2\"]", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "indirect", - OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId1\",\"iamId2\"]", - ), - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId3", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.6f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 3333L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 3L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "direct", - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId3\"]", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "direct", - OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId3\"]", - ), - ) - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME, records) - - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - val events = outcomeEventsRepository.getAllEventsToSend() - - // Then - events.count() shouldBe 3 - events[0].outcomeId shouldBe "outcomeId1" - events[0].weight shouldBe 0.2f - events[0].timestamp shouldBe 1111L - events[0].sessionTime shouldBe 1L - events[0].outcomeSource shouldNotBe null - events[0].outcomeSource!!.directBody shouldBe null - events[0].outcomeSource!!.indirectBody shouldBe null - events[1].outcomeId shouldBe "outcomeId2" - events[1].weight shouldBe 0.4f - events[1].timestamp shouldBe 2222L - events[1].sessionTime shouldBe 2L - events[1].outcomeSource shouldNotBe null - events[1].outcomeSource!!.directBody shouldBe null - events[1].outcomeSource!!.indirectBody shouldNotBe null - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.length() shouldBe 2 - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(0) shouldBe "notificationId1" - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(1) shouldBe "notificationId2" - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.length() shouldBe 2 - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId1" - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(1) shouldBe "iamId2" - events[2].outcomeId shouldBe "outcomeId3" - events[2].weight shouldBe 0.6f - events[2].timestamp shouldBe 3333L - events[2].sessionTime shouldBe 3L - events[2].outcomeSource shouldNotBe null - events[2].outcomeSource!!.indirectBody shouldBe null - events[2].outcomeSource!!.directBody shouldNotBe null - events[2].outcomeSource!!.directBody!!.notificationIds!!.length() shouldBe 1 - events[2].outcomeSource!!.directBody!!.notificationIds!!.getString(0) shouldBe "notificationId3" - events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.length() shouldBe 1 - events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId3" + runTest(dispatcherProvider.io) { + // Given + val records = + listOf( + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId1", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.2f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 1111L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 1L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "unattributed", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "unattributed", + ), + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId2", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.4f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 2222L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 2L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "indirect", + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId1\",\"notificationId2\"]", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "indirect", + OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId1\",\"iamId2\"]", + ), + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId3", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.6f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 3333L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 3L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "direct", + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId3\"]", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "direct", + OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId3\"]", + ), + ) + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + val events = outcomeEventsRepository.getAllEventsToSend() + + // Then + events.count() shouldBe 3 + events[0].outcomeId shouldBe "outcomeId1" + events[0].weight shouldBe 0.2f + events[0].timestamp shouldBe 1111L + events[0].sessionTime shouldBe 1L + events[0].outcomeSource shouldNotBe null + events[0].outcomeSource!!.directBody shouldBe null + events[0].outcomeSource!!.indirectBody shouldBe null + events[1].outcomeId shouldBe "outcomeId2" + events[1].weight shouldBe 0.4f + events[1].timestamp shouldBe 2222L + events[1].sessionTime shouldBe 2L + events[1].outcomeSource shouldNotBe null + events[1].outcomeSource!!.directBody shouldBe null + events[1].outcomeSource!!.indirectBody shouldNotBe null + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.length() shouldBe 2 + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(0) shouldBe "notificationId1" + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(1) shouldBe "notificationId2" + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.length() shouldBe 2 + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId1" + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(1) shouldBe "iamId2" + events[2].outcomeId shouldBe "outcomeId3" + events[2].weight shouldBe 0.6f + events[2].timestamp shouldBe 3333L + events[2].sessionTime shouldBe 3L + events[2].outcomeSource shouldNotBe null + events[2].outcomeSource!!.indirectBody shouldBe null + events[2].outcomeSource!!.directBody shouldNotBe null + events[2].outcomeSource!!.directBody!!.notificationIds!!.length() shouldBe 1 + events[2].outcomeSource!!.directBody!!.notificationIds!!.getString(0) shouldBe "notificationId3" + events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.length() shouldBe 1 + events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId3" + } } test("save unique outcome should insert no rows when no influences") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.COLUMN_NAME_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.COLUMN_NAME_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) - // Then - verify(exactly = 0) { mockDatabasePair.second.insert(CachedUniqueOutcomeTable.COLUMN_NAME_NAME, null, any()) } + // Then + verify(exactly = 0) { mockDatabasePair.second.insert(CachedUniqueOutcomeTable.COLUMN_NAME_NAME, null, any()) } + } } test("save unique outcome should insert 1 row for each unique influence when direct notification and indiract iam") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1")), - OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1")), + OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" + }, + ) + } } } test("save unique outcome should insert 1 row for each unique influence when direct iam and indiract notifications") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(null, JSONArray().put("iamId1")), - OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(null, JSONArray().put("iamId1")), + OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2")), + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + } } } test("save unique outcome should insert 1 row for each unique influence when direct notification and iam") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), - null, + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), + null, + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + } } } test("save unique outcome should insert 1 row for each unique influence when indirect notification and iam") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - null, - OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2"), JSONArray().put("iamId1").put("iamId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + null, + OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2"), JSONArray().put("iamId1").put("iamId2")), + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" + }, + ) + } } } test("retrieve non-cached influence should return full list when there are no cached unique influences") { - // Given - val records = listOf>() - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - val influences = - outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( - "outcomeId1", - listOf( - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), - ), - ) - - // Then - influences.count() shouldBe 2 + runTest(dispatcherProvider.io) { + // Given + val records = listOf>() + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + val influences = + outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( + "outcomeId1", + listOf( + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), + ), + ) + + // Then + influences.count() shouldBe 2 + } } test("retrieve non-cached influence should filter out an influence when there are is a matching influence") { - // Given - val records = listOf(mapOf(CachedUniqueOutcomeTable.COLUMN_NAME_NAME to "outcomeId1")) - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - val influences = - outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( - "outcomeId1", - listOf( - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), - ), - ) - - // Then - influences.count() shouldBe 0 + runTest(dispatcherProvider.io) { + // Given + val records = listOf(mapOf(CachedUniqueOutcomeTable.COLUMN_NAME_NAME to "outcomeId1")) + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + val influences = + outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( + "outcomeId1", + listOf( + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), + ), + ) + + // Then + influences.count() shouldBe 0 + } } test("clear unique influence should delete out an influence when there are is a matching influence") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.cleanCachedUniqueOutcomeEventNotifications() - - // Then - verifyAll { - mockDatabasePair.second.delete(CachedUniqueOutcomeTable.TABLE_NAME, any(), any()) + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.cleanCachedUniqueOutcomeEventNotifications() + + // Then + verifyAll { + mockDatabasePair.second.delete(CachedUniqueOutcomeTable.TABLE_NAME, any(), any()) + } } } }) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt index 66c750e3c0..d1ad883e12 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt @@ -4,6 +4,8 @@ import android.app.NotificationManager import android.content.ContentValues import android.provider.BaseColumns import android.text.TextUtils +import com.onesignal.common.threading.CoroutineDispatcherProvider +import com.onesignal.common.threading.DefaultDispatcherProvider import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.OneSignalDbContract @@ -14,7 +16,6 @@ import com.onesignal.notifications.internal.common.NotificationHelper import com.onesignal.notifications.internal.data.INotificationQueryHelper import com.onesignal.notifications.internal.data.INotificationRepository import com.onesignal.notifications.internal.limiting.INotificationLimitManager -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONException @@ -24,6 +25,7 @@ internal class NotificationRepository( private val _databaseProvider: IDatabaseProvider, private val _time: ITime, private val _badgeCountUpdater: IBadgeCountUpdater, + private val dispatchers: CoroutineDispatcherProvider = DefaultDispatcherProvider(), ) : INotificationRepository { /** * Deletes notifications with created timestamps older than 7 days @@ -31,7 +33,7 @@ internal class NotificationRepository( * 1. NotificationTable.TABLE_NAME */ override suspend fun deleteExpiredNotifications() { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val whereStr: String = OneSignalDbContract.NotificationTable.COLUMN_NAME_CREATED_TIME.toString() + " < ?" val sevenDaysAgoInSeconds: String = java.lang.String.valueOf( @@ -48,7 +50,7 @@ internal class NotificationRepository( } override suspend fun markAsDismissedForOutstanding() { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val appContext = _applicationService.appContext val notificationManager: NotificationManager = NotificationHelper.getNotificationManager(appContext) val retColumn = arrayOf(OneSignalDbContract.NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID) @@ -79,7 +81,7 @@ internal class NotificationRepository( } override suspend fun markAsDismissedForGroup(group: String) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val appContext = _applicationService.appContext val notificationManager: NotificationManager = NotificationHelper.getNotificationManager(appContext) @@ -124,7 +126,7 @@ internal class NotificationRepository( override suspend fun markAsDismissed(androidId: Int): Boolean { var didDismiss: Boolean = false - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { didDismiss = internalMarkAsDismissed(androidId) } @@ -159,7 +161,7 @@ internal class NotificationRepository( var result = false - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val retColumn = arrayOf(OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID) val whereArgs = arrayOf(id!!) @@ -188,7 +190,7 @@ internal class NotificationRepository( androidId: Int, groupId: String, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { // There currently isn't a visible notification from for this group_id. // Save the group summary notification id so it can be updated later. val values = ContentValues() @@ -218,7 +220,7 @@ internal class NotificationRepository( expireTime: Long, jsonPayload: String, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { Logging.debug("Saving Notification id=$id") try { @@ -302,7 +304,7 @@ internal class NotificationRepository( summaryGroup: String?, clearGroupOnSummaryClick: Boolean, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { var whereStr: String var whereArgs: Array? = null if (summaryGroup != null) { @@ -358,7 +360,7 @@ internal class NotificationRepository( override suspend fun getGroupId(androidId: Int): String? { var groupId: String? = null - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, // retColumn @@ -378,7 +380,7 @@ internal class NotificationRepository( override suspend fun getAndroidIdFromCollapseKey(collapseKey: String): Int? { var androidId: Int? = null - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, // retColumn @@ -402,7 +404,7 @@ internal class NotificationRepository( notificationsToMakeRoomFor: Int, maxNumberOfNotificationsInt: Int, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val maxNumberOfNotificationsString = maxNumberOfNotificationsInt.toString() try { @@ -437,7 +439,7 @@ internal class NotificationRepository( override suspend fun listNotificationsForGroup(summaryGroup: String): List { val listOfNotifications = mutableListOf() - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val whereArgs = arrayOf(summaryGroup) _databaseProvider.os.query( @@ -512,7 +514,7 @@ internal class NotificationRepository( val whereArgs = if (isGroupless) null else arrayOf(group) - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { // Order by timestamp in descending and limit to 1 _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, @@ -538,7 +540,7 @@ internal class NotificationRepository( override suspend fun listNotificationsForOutstanding(excludeAndroidIds: List?): List { val listOfNotifications = mutableListOf() - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val dbQuerySelection = _queryHelper.recentUninteractedWithNotificationsWhere() if (excludeAndroidIds != null) { diff --git a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt new file mode 100644 index 0000000000..2288659dc8 --- /dev/null +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt @@ -0,0 +1,65 @@ +package com.onesignal.mocks + +import com.onesignal.common.threading.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher + +/** + * Test implementation of [CoroutineDispatcherProvider] for unit tests. + * Uses a [TestDispatcher] for deterministic testing. + * + * Usage in tests: + * ``` + * test("my test") { + * val testDispatcher = StandardTestDispatcher() + * val dispatcherProvider = TestDispatcherProvider(testDispatcher) + * + * runTest(testDispatcher.scheduler) { + * val service = MyService(dispatcherProvider) + * service.doWork() + * + * // Option 1: Advance until all pending coroutines complete + * advanceUntilIdle() + * + * // Option 2: Advance virtual time by a specific amount (e.g., 100ms) + * // advanceTimeBy(100) + * + * // Option 3: Run only coroutines scheduled at current time + * // runCurrent() + * + * // Make assertions + * } + * } + * ``` + * + * Methods to control execution: + * - [advanceUntilIdle()] - Runs all pending coroutines until there's nothing left to execute + * - [advanceTimeBy(delayTime)] - Advances virtual time by the specified amount and runs + * coroutines scheduled for that time period + * - [runCurrent()] - Runs only the coroutines that are scheduled to run at the current + * virtual time (doesn't advance time) + * - [currentTime] - Property to check the current virtual time + */ +class TestDispatcherProvider( + private val testDispatcher: TestDispatcher = StandardTestDispatcher() +) : CoroutineDispatcherProvider { + override val io: CoroutineDispatcher = testDispatcher + override val default: CoroutineDispatcher = testDispatcher + + private val scope: CoroutineScope by lazy { + CoroutineScope(SupervisorJob() + testDispatcher) + } + + override fun launchOnIO(block: suspend () -> Unit): Job { + return scope.launch { block() } + } + + override fun launchOnDefault(block: suspend () -> Unit): Job { + return scope.launch { block() } + } +} From 5cab18b6a6fb77feb4f9a12b08942dde8e56b80e Mon Sep 17 00:00:00 2001 From: Sherwin Heydarbeygi Date: Mon, 26 Jan 2026 12:52:50 -0800 Subject: [PATCH 09/19] ci: remove extra "Channels" line that gets prepended to the release notes (#2529) --- .github/workflows/publish-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index cb58cbc996..b8fa1f2888 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -163,8 +163,6 @@ jobs: CHANNEL="current" fi - echo -e "Channels: $CHANNEL\n\n$(cat release_notes.md)" > release_notes.md - git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" From 8a1ff87ba8a5d9b9ab20599faf2721909eda6c79 Mon Sep 17 00:00:00 2001 From: jinliu Date: Mon, 26 Jan 2026 17:31:24 -0500 Subject: [PATCH 10/19] Revert "chore: improve testing with injected coroutine dispatchers" (#2530) --- .../threading/CoroutineDispatcherProvider.kt | 23 - .../threading/DefaultDispatcherProvider.kt | 26 - .../core/internal/startup/StartupService.kt | 8 +- .../outcomes/impl/OutcomeEventsRepository.kt | 16 +- .../internal/startup/StartupServiceTests.kt | 190 ++-- .../onesignal/internal/OneSignalImpTests.kt | 72 +- .../outcomes/OutcomeEventsRepositoryTests.kt | 873 +++++++++--------- .../data/impl/NotificationRepository.kt | 32 +- .../onesignal/mocks/TestDispatcherProvider.kt | 65 -- 9 files changed, 560 insertions(+), 745 deletions(-) delete mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt delete mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt delete mode 100644 OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt deleted file mode 100644 index 8b57c5659d..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.onesignal.common.threading - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Job - -/** - * Provider interface for coroutine dispatchers. - * This allows for proper dependency injection and easier testing. - */ -interface CoroutineDispatcherProvider { - val io: CoroutineDispatcher - val default: CoroutineDispatcher - - /** - * Launch a coroutine on the IO dispatcher. - */ - fun launchOnIO(block: suspend () -> Unit): Job - - /** - * Launch a coroutine on the Default dispatcher. - */ - fun launchOnDefault(block: suspend () -> Unit): Job -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt deleted file mode 100644 index 8ca50d5b5e..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.onesignal.common.threading - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Job - -/** - * Production implementation of [CoroutineDispatcherProvider] that uses OneSignalDispatchers. - * - * This delegates to the existing scopes in OneSignalDispatchers to avoid creating duplicate scopes. - * The OneSignalDispatchers already maintains IOScope and DefaultScope with SupervisorJob, - * so we reuse those instead of creating new ones. - */ -class DefaultDispatcherProvider : CoroutineDispatcherProvider { - override val io: CoroutineDispatcher = OneSignalDispatchers.IO - override val default: CoroutineDispatcher = OneSignalDispatchers.Default - - override fun launchOnIO(block: suspend () -> Unit): Job { - // Delegate to OneSignalDispatchers which already has IOScope with SupervisorJob - return OneSignalDispatchers.launchOnIO(block) - } - - override fun launchOnDefault(block: suspend () -> Unit): Job { - // Delegate to OneSignalDispatchers which already has DefaultScope with SupervisorJob - return OneSignalDispatchers.launchOnDefault(block) - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt index 6d209734f9..9d1c112d64 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt @@ -1,20 +1,18 @@ package com.onesignal.core.internal.startup import com.onesignal.common.services.ServiceProvider -import com.onesignal.common.threading.CoroutineDispatcherProvider -import com.onesignal.common.threading.DefaultDispatcherProvider +import com.onesignal.common.threading.OneSignalDispatchers internal class StartupService( private val services: ServiceProvider, - private val dispatchers: CoroutineDispatcherProvider = DefaultDispatcherProvider(), ) { fun bootstrap() { services.getAllServices().forEach { it.bootstrap() } } - // schedule to start all startable services using the provided dispatcher + // schedule to start all startable services using OneSignal dispatcher fun scheduleStart() { - dispatchers.launchOnDefault { + OneSignalDispatchers.launchOnDefault { services.getAllServices().forEach { it.start() } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt index 16c4f2d32f..8a9b7ecb66 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt @@ -1,7 +1,6 @@ package com.onesignal.session.internal.outcomes.impl import android.content.ContentValues -import com.onesignal.common.threading.OneSignalDispatchers import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.OneSignalDbContract import com.onesignal.debug.internal.logging.Logging @@ -10,7 +9,7 @@ import com.onesignal.session.internal.influence.InfluenceChannel import com.onesignal.session.internal.influence.InfluenceType import com.onesignal.session.internal.influence.InfluenceType.Companion.fromString import com.onesignal.session.internal.outcomes.migrations.RemoveInvalidSessionTimeRecords -import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONException @@ -18,13 +17,12 @@ import java.util.Locale internal class OutcomeEventsRepository( private val _databaseProvider: IDatabaseProvider, - private val ioDispatcher: CoroutineDispatcher = OneSignalDispatchers.IO, ) : IOutcomeEventsRepository { /** * Delete event from the DB */ override suspend fun deleteOldOutcomeEvent(event: OutcomeEventParams) { - withContext(ioDispatcher) { + withContext(Dispatchers.IO) { _databaseProvider.os.delete( OutcomeEventsTable.TABLE_NAME, OutcomeEventsTable.COLUMN_NAME_TIMESTAMP + " = ?", @@ -38,7 +36,7 @@ internal class OutcomeEventsRepository( * For offline mode and contingency of errors */ override suspend fun saveOutcomeEvent(eventParams: OutcomeEventParams) { - withContext(ioDispatcher) { + withContext(Dispatchers.IO) { var notificationIds = JSONArray() var iamIds = JSONArray() var notificationInfluenceType = InfluenceType.UNATTRIBUTED @@ -103,7 +101,7 @@ internal class OutcomeEventsRepository( */ override suspend fun getAllEventsToSend(): List { val events: MutableList = ArrayList() - withContext(ioDispatcher) { + withContext(Dispatchers.IO) { RemoveInvalidSessionTimeRecords.run(_databaseProvider) _databaseProvider.os.query(OutcomeEventsTable.TABLE_NAME) { cursor -> if (cursor.moveToFirst()) { @@ -250,7 +248,7 @@ internal class OutcomeEventsRepository( override suspend fun saveUniqueOutcomeEventParams(eventParams: OutcomeEventParams) { Logging.debug("OutcomeEventsCache.saveUniqueOutcomeEventParams(eventParams: $eventParams)") - withContext(ioDispatcher) { + withContext(Dispatchers.IO) { val outcomeName = eventParams.outcomeId val cachedUniqueOutcomes: MutableList = ArrayList() val directBody = eventParams.outcomeSource?.directBody @@ -285,7 +283,7 @@ internal class OutcomeEventsRepository( ): List { val uniqueInfluences: MutableList = ArrayList() - withContext(ioDispatcher) { + withContext(Dispatchers.IO) { try { for (influence in influences) { val availableInfluenceIds = JSONArray() @@ -335,7 +333,7 @@ internal class OutcomeEventsRepository( val notificationTableName = OneSignalDbContract.NotificationTable.TABLE_NAME val notificationIdColumnName = OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID - withContext(ioDispatcher) { + withContext(Dispatchers.IO) { val whereStr = "NOT EXISTS(" + "SELECT NULL FROM " + notificationTableName + " n " + diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt index 820ed4fe37..7416b2910c 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt @@ -4,7 +4,8 @@ import com.onesignal.common.services.ServiceBuilder import com.onesignal.common.services.ServiceProvider import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.mocks.TestDispatcherProvider +import com.onesignal.mocks.IOMockHelper +import com.onesignal.mocks.IOMockHelper.awaitIO import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.comparables.shouldBeLessThan @@ -13,12 +14,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -@OptIn(ExperimentalCoroutinesApi::class) class StartupServiceTests : FunSpec({ fun setupServiceProvider( bootstrapServices: List, @@ -31,127 +27,111 @@ class StartupServiceTests : FunSpec({ serviceBuilder.register(reg).provides() return serviceBuilder.build() } - val testDispatcher = StandardTestDispatcher() - val dispatcherProvider = TestDispatcherProvider(testDispatcher) + + listener(IOMockHelper) beforeAny { Logging.logLevel = LogLevel.NONE } test("bootstrap with no IBootstrapService dependencies is a no-op") { - runTest(testDispatcher.scheduler) { - // Given - val startupService = StartupService(setupServiceProvider(listOf(), listOf()), dispatcherProvider) + // Given + val startupService = StartupService(setupServiceProvider(listOf(), listOf())) - // When - startupService.bootstrap() + // When + startupService.bootstrap() - // Then - } + // Then } test("bootstrap will call all IBootstrapService dependencies successfully") { - runTest(testDispatcher.scheduler) { - // Given - val mockBootstrapService1 = mockk(relaxed = true) - val mockBootstrapService2 = mockk(relaxed = true) + // Given + val mockBootstrapService1 = mockk(relaxed = true) + val mockBootstrapService2 = mockk(relaxed = true) - val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider) + val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf())) - // When - startupService.bootstrap() + // When + startupService.bootstrap() - // Then - verify(exactly = 1) { mockBootstrapService1.bootstrap() } - verify(exactly = 1) { mockBootstrapService2.bootstrap() } - } + // Then + verify(exactly = 1) { mockBootstrapService1.bootstrap() } + verify(exactly = 1) { mockBootstrapService2.bootstrap() } } test("bootstrap will propagate exception when an IBootstrapService throws an exception") { - runTest(testDispatcher.scheduler) { - // Given - val exception = Exception("SOMETHING BAD") - - val mockBootstrapService1 = mockk() - every { mockBootstrapService1.bootstrap() } throws exception - val mockBootstrapService2 = spyk() - - val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider) - - // When - val actualException = - shouldThrowUnit { - startupService.bootstrap() - } - - // Then - actualException shouldBe exception - verify(exactly = 1) { mockBootstrapService1.bootstrap() } - verify(exactly = 0) { mockBootstrapService2.bootstrap() } - } + // Given + val exception = Exception("SOMETHING BAD") + + val mockBootstrapService1 = mockk() + every { mockBootstrapService1.bootstrap() } throws exception + val mockBootstrapService2 = spyk() + + val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf())) + + // When + val actualException = + shouldThrowUnit { + startupService.bootstrap() + } + + // Then + actualException shouldBe exception + verify(exactly = 1) { mockBootstrapService1.bootstrap() } + verify(exactly = 0) { mockBootstrapService2.bootstrap() } } test("startup will call all IStartableService dependencies successfully after a short delay") { - runTest(testDispatcher.scheduler) { - // Given - val mockStartupService1 = mockk(relaxed = true) - val mockStartupService2 = mockk(relaxed = true) - - val startupService = StartupService( - setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2)), - dispatcherProvider - ) - - // When - startupService.scheduleStart() - - // Then - wait deterministically for both services to start using advanceUntilIdle - advanceUntilIdle() - verify(exactly = 1) { mockStartupService1.start() } - verify(exactly = 1) { mockStartupService2.start() } - } + // Given + val mockStartupService1 = mockk(relaxed = true) + val mockStartupService2 = mockk(relaxed = true) + + val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2))) + + // When + startupService.scheduleStart() + + // Then - wait deterministically for both services to start using IOMockHelper + awaitIO() + verify(exactly = 1) { mockStartupService1.start() } + verify(exactly = 1) { mockStartupService2.start() } } test("scheduleStart does not block main thread") { - runTest(testDispatcher.scheduler) { - // Given - val mockStartableService1 = mockk(relaxed = true) - val mockStartableService2 = spyk() - val mockStartableService3 = spyk() - // Only service1 and service2 are scheduled - service3 is NOT scheduled - val startupService = StartupService( - setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2)), - dispatcherProvider - ) - - // When - scheduleStart() is async, so it doesn't block - val startTime = System.currentTimeMillis() - startupService.scheduleStart() - val scheduleTime = System.currentTimeMillis() - startTime - - // This should execute immediately since scheduleStart() doesn't block - // service3 is NOT part of scheduled services, so this is a direct call - mockStartableService3.start() - val immediateTime = System.currentTimeMillis() - startTime - - // Then - verify scheduleStart() returned quickly (non-blocking) - // Should return in < 50ms (proving it doesn't wait for services to start) - scheduleTime shouldBeLessThan 50L - immediateTime shouldBeLessThan 50L - - // Verify service3 was called immediately (proving main thread wasn't blocked) - verify(exactly = 1) { mockStartableService3.start() } - - // Wait deterministically for async execution using advanceUntilIdle - advanceUntilIdle() - - // Verify scheduled services were called - verify(exactly = 1) { mockStartableService1.start() } - verify(exactly = 1) { mockStartableService2.start() } - - // The key assertion: scheduleStart() returned immediately without blocking, - // allowing service3.start() to be called synchronously before scheduled services - // complete. This proves scheduleStart() is non-blocking. - } + // Given + val mockStartableService1 = mockk(relaxed = true) + val mockStartableService2 = spyk() + val mockStartableService3 = spyk() + // Only service1 and service2 are scheduled - service3 is NOT scheduled + val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2))) + + // When - scheduleStart() is async, so it doesn't block + val startTime = System.currentTimeMillis() + startupService.scheduleStart() + val scheduleTime = System.currentTimeMillis() - startTime + + // This should execute immediately since scheduleStart() doesn't block + // service3 is NOT part of scheduled services, so this is a direct call + mockStartableService3.start() + val immediateTime = System.currentTimeMillis() - startTime + + // Then - verify scheduleStart() returned quickly (non-blocking) + // Should return in < 50ms (proving it doesn't wait for services to start) + scheduleTime shouldBeLessThan 50L + immediateTime shouldBeLessThan 50L + + // Verify service3 was called immediately (proving main thread wasn't blocked) + verify(exactly = 1) { mockStartableService3.start() } + + // Wait deterministically for async execution using IOMockHelper + awaitIO() + + // Verify scheduled services were called + verify(exactly = 1) { mockStartableService1.start() } + verify(exactly = 1) { mockStartableService2.start() } + + // The key assertion: scheduleStart() returned immediately without blocking, + // allowing service3.start() to be called synchronously before scheduled services + // complete. This proves scheduleStart() is non-blocking. } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt index 08cfc6d79f..d660fa2525 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt @@ -2,51 +2,41 @@ package com.onesignal.internal import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.mocks.TestDispatcherProvider import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest class OneSignalImpTests : FunSpec({ beforeAny { Logging.logLevel = LogLevel.NONE } - val testDispatcher = StandardTestDispatcher() - val dispatcherProvider = TestDispatcherProvider(testDispatcher) - test("attempting login before initWithContext throws exception") { - runTest(testDispatcher.scheduler) { - // Given - val os = OneSignalImp() + // Given + val os = OneSignalImp() - // When - val exception = - shouldThrowUnit { - os.login("login-id") - } + // When + val exception = + shouldThrowUnit { + os.login("login-id") + } - // Then - exception.message shouldBe "Must call 'initWithContext' before 'login'" - } + // Then + exception.message shouldBe "Must call 'initWithContext' before 'login'" } test("attempting logout before initWithContext throws exception") { - runTest(testDispatcher.scheduler) { - // Given - val os = OneSignalImp() + // Given + val os = OneSignalImp() - // When - val exception = - shouldThrowUnit { - os.logout() - } + // When + val exception = + shouldThrowUnit { + os.logout() + } - // Then - exception.message shouldBe "Must call 'initWithContext' before 'logout'" - } + // Then + exception.message shouldBe "Must call 'initWithContext' before 'logout'" } // Comprehensive tests for deprecated properties that should work before and after initialization @@ -54,7 +44,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When & Then os.consentRequired shouldBe false @@ -62,7 +52,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When os.consentRequired = true @@ -79,7 +69,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When & Then - should not throw os.consentRequired = false @@ -92,7 +82,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When & Then os.consentGiven shouldBe false @@ -100,7 +90,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When os.consentGiven = true @@ -117,7 +107,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When & Then - should not throw os.consentGiven = true @@ -130,7 +120,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When & Then os.disableGMSMissingPrompt shouldBe false @@ -138,7 +128,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When os.disableGMSMissingPrompt = true @@ -155,7 +145,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When & Then - should not throw os.disableGMSMissingPrompt = true @@ -167,7 +157,7 @@ class OneSignalImpTests : FunSpec({ context("property consistency tests") { test("all properties maintain state correctly") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When - set all properties to true os.consentRequired = true @@ -192,7 +182,7 @@ class OneSignalImpTests : FunSpec({ test("properties are independent of each other") { // Given - val os = OneSignalImp(dispatcherProvider.io) + val os = OneSignalImp() // When - set only consentRequired to true os.consentRequired = true @@ -228,7 +218,7 @@ class OneSignalImpTests : FunSpec({ // waitForInit() would timeout after 30 seconds and log a warning (not throw) // Given - a fresh OneSignalImp instance - val oneSignalImp = OneSignalImp(dispatcherProvider.io) + val oneSignalImp = OneSignalImp() // The timeout behavior is built into waitUntilInitInternal() // which uses withTimeout() to wait for up to 30 seconds (or 4.8 seconds on main thread) @@ -246,7 +236,7 @@ class OneSignalImpTests : FunSpec({ // until initialization completes (per PR #2412) // Given - val oneSignalImp = OneSignalImp(dispatcherProvider.io) + val oneSignalImp = OneSignalImp() // We can verify the wait behavior by checking: // 1. The suspendCompletion (CompletableDeferred) is properly initialized diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt index f1cd47491c..e7fad98ac3 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt @@ -4,7 +4,6 @@ import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.DatabaseMockHelper -import com.onesignal.mocks.TestDispatcherProvider import com.onesignal.session.internal.influence.Influence import com.onesignal.session.internal.influence.InfluenceChannel import com.onesignal.session.internal.influence.InfluenceType @@ -20,9 +19,6 @@ import io.kotest.matchers.shouldNotBe import io.mockk.verify import io.mockk.verifyAll import io.mockk.verifySequence -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.runTest import org.json.JSONArray @RobolectricTest @@ -31,512 +27,481 @@ class OutcomeEventsRepositoryTests : FunSpec({ Logging.logLevel = LogLevel.NONE } - // avoids initialization happening too early (before Robolectric’s environment exists). - lateinit var testDispatcher: TestDispatcher - lateinit var dispatcherProvider: TestDispatcherProvider - - beforeTest { - testDispatcher = StandardTestDispatcher() - dispatcherProvider = TestDispatcherProvider(testDispatcher) - } - test("delete outcome event should use the timestamp to delete row from database") { - runTest(dispatcherProvider.io) { - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - outcomeEventsRepository.deleteOldOutcomeEvent(OutcomeEventParams("outcomeId", null, 0f, 0, 1111)) - - // Then - verify(exactly = 1) { - mockDatabasePair.second.delete( - OutcomeEventsTable.TABLE_NAME, - withArg { - it.contains(OutcomeEventsTable.COLUMN_NAME_TIMESTAMP) - }, - withArg { it.contains("1111") }, - ) - } + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + outcomeEventsRepository.deleteOldOutcomeEvent(OutcomeEventParams("outcomeId", null, 0f, 0, 1111)) + + // Then + verify(exactly = 1) { + mockDatabasePair.second.delete( + OutcomeEventsTable.TABLE_NAME, + withArg { + it.contains(OutcomeEventsTable.COLUMN_NAME_TIMESTAMP) + }, + withArg { it.contains("1111") }, + ) } } test("save outcome event should insert row into database") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - outcomeEventsRepository.saveOutcomeEvent(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId2", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1")), - OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), - ), - .2f, - 0, - 2222, + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + outcomeEventsRepository.saveOutcomeEvent(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId2", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1")), + OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), ), - ) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId3", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), - null, - ), - .4f, - 0, - 3333, + .2f, + 0, + 2222, + ), + ) + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId3", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), + null, ), - ) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId4", - OutcomeSource( - null, - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1").put("iamId2")), - ), - .6f, - 0, - 4444, + .4f, + 0, + 3333, + ), + ) + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId4", + OutcomeSource( + null, + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1").put("iamId2")), ), + .6f, + 0, + 4444, + ), + ) + + // Then + verifySequence { + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe 0f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 1111L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "unattributed" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray().toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "unattributed" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray().toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId2" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .2f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 2222L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId3" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .4f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 3333L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\"]").toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId4" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .6f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 4444L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() + }, ) - - // Then - verifySequence { - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe 0f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 1111L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "unattributed" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray().toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "unattributed" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray().toString() - }, - ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId2" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .2f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 2222L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() - }, - ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId3" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .4f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 3333L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\"]").toString() - }, - ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId4" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .6f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 4444L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() - }, - ) - } } } test("get events should retrieve return empty list when database is empty") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - // When - val events = outcomeEventsRepository.getAllEventsToSend() + // When + val events = outcomeEventsRepository.getAllEventsToSend() - // Then - events.count() shouldBe 0 - } + // Then + events.count() shouldBe 0 } test("get events should retrieve return an item per row in database") { - runTest(dispatcherProvider.io) { - // Given - val records = - listOf( - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId1", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.2f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 1111L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 1L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "unattributed", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "unattributed", - ), - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId2", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.4f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 2222L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 2L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "indirect", - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId1\",\"notificationId2\"]", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "indirect", - OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId1\",\"iamId2\"]", - ), - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId3", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.6f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 3333L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 3L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "direct", - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId3\"]", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "direct", - OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId3\"]", - ), - ) - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME, records) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - val events = outcomeEventsRepository.getAllEventsToSend() - - // Then - events.count() shouldBe 3 - events[0].outcomeId shouldBe "outcomeId1" - events[0].weight shouldBe 0.2f - events[0].timestamp shouldBe 1111L - events[0].sessionTime shouldBe 1L - events[0].outcomeSource shouldNotBe null - events[0].outcomeSource!!.directBody shouldBe null - events[0].outcomeSource!!.indirectBody shouldBe null - events[1].outcomeId shouldBe "outcomeId2" - events[1].weight shouldBe 0.4f - events[1].timestamp shouldBe 2222L - events[1].sessionTime shouldBe 2L - events[1].outcomeSource shouldNotBe null - events[1].outcomeSource!!.directBody shouldBe null - events[1].outcomeSource!!.indirectBody shouldNotBe null - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.length() shouldBe 2 - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(0) shouldBe "notificationId1" - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(1) shouldBe "notificationId2" - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.length() shouldBe 2 - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId1" - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(1) shouldBe "iamId2" - events[2].outcomeId shouldBe "outcomeId3" - events[2].weight shouldBe 0.6f - events[2].timestamp shouldBe 3333L - events[2].sessionTime shouldBe 3L - events[2].outcomeSource shouldNotBe null - events[2].outcomeSource!!.indirectBody shouldBe null - events[2].outcomeSource!!.directBody shouldNotBe null - events[2].outcomeSource!!.directBody!!.notificationIds!!.length() shouldBe 1 - events[2].outcomeSource!!.directBody!!.notificationIds!!.getString(0) shouldBe "notificationId3" - events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.length() shouldBe 1 - events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId3" - } + // Given + val records = + listOf( + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId1", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.2f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 1111L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 1L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "unattributed", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "unattributed", + ), + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId2", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.4f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 2222L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 2L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "indirect", + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId1\",\"notificationId2\"]", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "indirect", + OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId1\",\"iamId2\"]", + ), + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId3", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.6f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 3333L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 3L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "direct", + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId3\"]", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "direct", + OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId3\"]", + ), + ) + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME, records) + + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + val events = outcomeEventsRepository.getAllEventsToSend() + + // Then + events.count() shouldBe 3 + events[0].outcomeId shouldBe "outcomeId1" + events[0].weight shouldBe 0.2f + events[0].timestamp shouldBe 1111L + events[0].sessionTime shouldBe 1L + events[0].outcomeSource shouldNotBe null + events[0].outcomeSource!!.directBody shouldBe null + events[0].outcomeSource!!.indirectBody shouldBe null + events[1].outcomeId shouldBe "outcomeId2" + events[1].weight shouldBe 0.4f + events[1].timestamp shouldBe 2222L + events[1].sessionTime shouldBe 2L + events[1].outcomeSource shouldNotBe null + events[1].outcomeSource!!.directBody shouldBe null + events[1].outcomeSource!!.indirectBody shouldNotBe null + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.length() shouldBe 2 + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(0) shouldBe "notificationId1" + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(1) shouldBe "notificationId2" + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.length() shouldBe 2 + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId1" + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(1) shouldBe "iamId2" + events[2].outcomeId shouldBe "outcomeId3" + events[2].weight shouldBe 0.6f + events[2].timestamp shouldBe 3333L + events[2].sessionTime shouldBe 3L + events[2].outcomeSource shouldNotBe null + events[2].outcomeSource!!.indirectBody shouldBe null + events[2].outcomeSource!!.directBody shouldNotBe null + events[2].outcomeSource!!.directBody!!.notificationIds!!.length() shouldBe 1 + events[2].outcomeSource!!.directBody!!.notificationIds!!.getString(0) shouldBe "notificationId3" + events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.length() shouldBe 1 + events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId3" } test("save unique outcome should insert no rows when no influences") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.COLUMN_NAME_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.COLUMN_NAME_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) - // Then - verify(exactly = 0) { mockDatabasePair.second.insert(CachedUniqueOutcomeTable.COLUMN_NAME_NAME, null, any()) } - } + // Then + verify(exactly = 0) { mockDatabasePair.second.insert(CachedUniqueOutcomeTable.COLUMN_NAME_NAME, null, any()) } } test("save unique outcome should insert 1 row for each unique influence when direct notification and indiract iam") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1")), - OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), - ), - .2f, - 0, - 2222, + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1")), + OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), ), + .2f, + 0, + 2222, + ), + ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" + }, ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" - }, - ) - } } } test("save unique outcome should insert 1 row for each unique influence when direct iam and indiract notifications") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(null, JSONArray().put("iamId1")), - OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2")), - ), - .2f, - 0, - 2222, + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(null, JSONArray().put("iamId1")), + OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2")), ), + .2f, + 0, + 2222, + ), + ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - } } } test("save unique outcome should insert 1 row for each unique influence when direct notification and iam") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), - null, - ), - .2f, - 0, - 2222, + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), + null, ), + .2f, + 0, + 2222, + ), + ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - } } } test("save unique outcome should insert 1 row for each unique influence when indirect notification and iam") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - null, - OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2"), JSONArray().put("iamId1").put("iamId2")), - ), - .2f, - 0, - 2222, + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + null, + OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2"), JSONArray().put("iamId1").put("iamId2")), ), + .2f, + 0, + 2222, + ), + ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" + }, ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" - }, - ) - } } } test("retrieve non-cached influence should return full list when there are no cached unique influences") { - runTest(dispatcherProvider.io) { - // Given - val records = listOf>() - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - val influences = - outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( - "outcomeId1", - listOf( - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), - ), - ) - - // Then - influences.count() shouldBe 2 - } + // Given + val records = listOf>() + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + val influences = + outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( + "outcomeId1", + listOf( + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), + ), + ) + + // Then + influences.count() shouldBe 2 } test("retrieve non-cached influence should filter out an influence when there are is a matching influence") { - runTest(dispatcherProvider.io) { - // Given - val records = listOf(mapOf(CachedUniqueOutcomeTable.COLUMN_NAME_NAME to "outcomeId1")) - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - val influences = - outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( - "outcomeId1", - listOf( - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), - ), - ) - - // Then - influences.count() shouldBe 0 - } + // Given + val records = listOf(mapOf(CachedUniqueOutcomeTable.COLUMN_NAME_NAME to "outcomeId1")) + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + val influences = + outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( + "outcomeId1", + listOf( + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), + ), + ) + + // Then + influences.count() shouldBe 0 } test("clear unique influence should delete out an influence when there are is a matching influence") { - runTest(dispatcherProvider.io) { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - - // When - outcomeEventsRepository.cleanCachedUniqueOutcomeEventNotifications() - - // Then - verifyAll { - mockDatabasePair.second.delete(CachedUniqueOutcomeTable.TABLE_NAME, any(), any()) - } + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + + // When + outcomeEventsRepository.cleanCachedUniqueOutcomeEventNotifications() + + // Then + verifyAll { + mockDatabasePair.second.delete(CachedUniqueOutcomeTable.TABLE_NAME, any(), any()) } } }) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt index d1ad883e12..66c750e3c0 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt @@ -4,8 +4,6 @@ import android.app.NotificationManager import android.content.ContentValues import android.provider.BaseColumns import android.text.TextUtils -import com.onesignal.common.threading.CoroutineDispatcherProvider -import com.onesignal.common.threading.DefaultDispatcherProvider import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.OneSignalDbContract @@ -16,6 +14,7 @@ import com.onesignal.notifications.internal.common.NotificationHelper import com.onesignal.notifications.internal.data.INotificationQueryHelper import com.onesignal.notifications.internal.data.INotificationRepository import com.onesignal.notifications.internal.limiting.INotificationLimitManager +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONException @@ -25,7 +24,6 @@ internal class NotificationRepository( private val _databaseProvider: IDatabaseProvider, private val _time: ITime, private val _badgeCountUpdater: IBadgeCountUpdater, - private val dispatchers: CoroutineDispatcherProvider = DefaultDispatcherProvider(), ) : INotificationRepository { /** * Deletes notifications with created timestamps older than 7 days @@ -33,7 +31,7 @@ internal class NotificationRepository( * 1. NotificationTable.TABLE_NAME */ override suspend fun deleteExpiredNotifications() { - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { val whereStr: String = OneSignalDbContract.NotificationTable.COLUMN_NAME_CREATED_TIME.toString() + " < ?" val sevenDaysAgoInSeconds: String = java.lang.String.valueOf( @@ -50,7 +48,7 @@ internal class NotificationRepository( } override suspend fun markAsDismissedForOutstanding() { - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { val appContext = _applicationService.appContext val notificationManager: NotificationManager = NotificationHelper.getNotificationManager(appContext) val retColumn = arrayOf(OneSignalDbContract.NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID) @@ -81,7 +79,7 @@ internal class NotificationRepository( } override suspend fun markAsDismissedForGroup(group: String) { - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { val appContext = _applicationService.appContext val notificationManager: NotificationManager = NotificationHelper.getNotificationManager(appContext) @@ -126,7 +124,7 @@ internal class NotificationRepository( override suspend fun markAsDismissed(androidId: Int): Boolean { var didDismiss: Boolean = false - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { didDismiss = internalMarkAsDismissed(androidId) } @@ -161,7 +159,7 @@ internal class NotificationRepository( var result = false - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { val retColumn = arrayOf(OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID) val whereArgs = arrayOf(id!!) @@ -190,7 +188,7 @@ internal class NotificationRepository( androidId: Int, groupId: String, ) { - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { // There currently isn't a visible notification from for this group_id. // Save the group summary notification id so it can be updated later. val values = ContentValues() @@ -220,7 +218,7 @@ internal class NotificationRepository( expireTime: Long, jsonPayload: String, ) { - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { Logging.debug("Saving Notification id=$id") try { @@ -304,7 +302,7 @@ internal class NotificationRepository( summaryGroup: String?, clearGroupOnSummaryClick: Boolean, ) { - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { var whereStr: String var whereArgs: Array? = null if (summaryGroup != null) { @@ -360,7 +358,7 @@ internal class NotificationRepository( override suspend fun getGroupId(androidId: Int): String? { var groupId: String? = null - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, // retColumn @@ -380,7 +378,7 @@ internal class NotificationRepository( override suspend fun getAndroidIdFromCollapseKey(collapseKey: String): Int? { var androidId: Int? = null - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, // retColumn @@ -404,7 +402,7 @@ internal class NotificationRepository( notificationsToMakeRoomFor: Int, maxNumberOfNotificationsInt: Int, ) { - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { val maxNumberOfNotificationsString = maxNumberOfNotificationsInt.toString() try { @@ -439,7 +437,7 @@ internal class NotificationRepository( override suspend fun listNotificationsForGroup(summaryGroup: String): List { val listOfNotifications = mutableListOf() - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { val whereArgs = arrayOf(summaryGroup) _databaseProvider.os.query( @@ -514,7 +512,7 @@ internal class NotificationRepository( val whereArgs = if (isGroupless) null else arrayOf(group) - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { // Order by timestamp in descending and limit to 1 _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, @@ -540,7 +538,7 @@ internal class NotificationRepository( override suspend fun listNotificationsForOutstanding(excludeAndroidIds: List?): List { val listOfNotifications = mutableListOf() - withContext(dispatchers.io) { + withContext(Dispatchers.IO) { val dbQuerySelection = _queryHelper.recentUninteractedWithNotificationsWhere() if (excludeAndroidIds != null) { diff --git a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt deleted file mode 100644 index 2288659dc8..0000000000 --- a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.onesignal.mocks - -import com.onesignal.common.threading.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher - -/** - * Test implementation of [CoroutineDispatcherProvider] for unit tests. - * Uses a [TestDispatcher] for deterministic testing. - * - * Usage in tests: - * ``` - * test("my test") { - * val testDispatcher = StandardTestDispatcher() - * val dispatcherProvider = TestDispatcherProvider(testDispatcher) - * - * runTest(testDispatcher.scheduler) { - * val service = MyService(dispatcherProvider) - * service.doWork() - * - * // Option 1: Advance until all pending coroutines complete - * advanceUntilIdle() - * - * // Option 2: Advance virtual time by a specific amount (e.g., 100ms) - * // advanceTimeBy(100) - * - * // Option 3: Run only coroutines scheduled at current time - * // runCurrent() - * - * // Make assertions - * } - * } - * ``` - * - * Methods to control execution: - * - [advanceUntilIdle()] - Runs all pending coroutines until there's nothing left to execute - * - [advanceTimeBy(delayTime)] - Advances virtual time by the specified amount and runs - * coroutines scheduled for that time period - * - [runCurrent()] - Runs only the coroutines that are scheduled to run at the current - * virtual time (doesn't advance time) - * - [currentTime] - Property to check the current virtual time - */ -class TestDispatcherProvider( - private val testDispatcher: TestDispatcher = StandardTestDispatcher() -) : CoroutineDispatcherProvider { - override val io: CoroutineDispatcher = testDispatcher - override val default: CoroutineDispatcher = testDispatcher - - private val scope: CoroutineScope by lazy { - CoroutineScope(SupervisorJob() + testDispatcher) - } - - override fun launchOnIO(block: suspend () -> Unit): Job { - return scope.launch { block() } - } - - override fun launchOnDefault(block: suspend () -> Unit): Job { - return scope.launch { block() } - } -} From fa3df7e858fb3c6f608a6fba1130c5e2a6c8ab79 Mon Sep 17 00:00:00 2001 From: jinliu Date: Tue, 27 Jan 2026 11:51:20 -0500 Subject: [PATCH 11/19] fix: end initialization early if device storage is locked (#2520) --- .../java/com/onesignal/common/AndroidUtils.kt | 22 ++++++++++ .../com/onesignal/internal/OneSignalImp.kt | 10 +++++ .../core/internal/application/SDKInitTests.kt | 40 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt index fe846f503a..c1d09cba71 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt @@ -10,6 +10,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Looper +import android.os.UserManager import android.text.TextUtils import androidx.annotation.Keep import androidx.core.app.NotificationManagerCompat @@ -41,6 +42,27 @@ object AndroidUtils { return hasToken && insetsAttached } + /** + * Retrieve whether the device user is accessible. + * + * On Android 7.0+ (API 24+), encrypted user data is inaccessible until the user unlocks + * the device for the first time after boot. This includes: + * * getSharedPreferences() + * * Any file-based storage in the default credential-encrypted context + * + * Apps that auto-run on boot or background services triggered early may hit this issue. + */ + fun isAndroidUserUnlocked(appContext: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + // Prior to API 24, the device booted into an unlocked state by default + return true + } + + val userManager = appContext.getSystemService(Context.USER_SERVICE) as? UserManager + // assume user is unlocked if the Android UserManager is null + return userManager?.isUserUnlocked ?: true + } + fun hasConfigChangeFlag( activity: Activity, configChangeFlag: Int, diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index 57495235a7..664cfd65ce 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -277,6 +277,16 @@ internal class OneSignalImp( context: Context, appId: String?, ): Boolean { + // Check whether current Android user is accessible. + // Return early if it is inaccessible, as we are unable to complete initialization without access + // to device storage like SharedPreferences. + if (!AndroidUtils.isAndroidUserUnlocked(context)) { + Logging.warn("initWithContext called when device storage is locked, no user data is accessible!") + initState = InitState.FAILED + notifyInitComplete() + return false + } + initEssentials(context) val startupService = bootstrapServices() diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt index 9418aa1f5a..7247848568 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt @@ -5,6 +5,7 @@ import android.content.ContextWrapper import android.content.SharedPreferences import androidx.test.core.app.ApplicationProvider.getApplicationContext import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import com.onesignal.common.AndroidUtils import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys.PREFS_LEGACY_APP_ID import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging @@ -14,6 +15,9 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.maps.shouldContain import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject import kotlinx.coroutines.runBlocking import java.util.concurrent.CountDownLatch @@ -98,6 +102,42 @@ class SDKInitTests : FunSpec({ } } + test("initWithContext returns gracefully when Android user is locked") { + // Given + val context = getApplicationContext() + val os = OneSignalImp() + + mockkObject(AndroidUtils) + every { AndroidUtils.isAndroidUserUnlocked(any()) } returns false + + // When + os.initWithContext(context, "appId") + + // Then + // returns gracefully but isInitialized should be false + os.isInitialized shouldBe false + + unmockkObject(AndroidUtils) + } + + test("initWithContext is successful when Android user is unlocked") { + // Given + val context = getApplicationContext() + val os = OneSignalImp() + + mockkObject(AndroidUtils) + every { AndroidUtils.isAndroidUserUnlocked(any()) } returns true + + // When + os.initWithContext(context, "appId") + + // Then + waitForInitialization(os) + os.isInitialized shouldBe true + + unmockkObject(AndroidUtils) + } + test("initWithContext with no appId succeeds when configModel has appId") { // Given // block SharedPreference before calling init From 6675028f2509bdede640e8fcfbd85316262e76eb Mon Sep 17 00:00:00 2001 From: Sherwin Heydarbeygi Date: Tue, 27 Jan 2026 13:02:18 -0800 Subject: [PATCH 12/19] Add Claude Code GitHub Workflow (#2532) --- .github/workflows/claude-code-review.yml | 44 +++++++++++++++++++++ .github/workflows/claude.yml | 50 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000000..4f6145beb8 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000000..79fe056478 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + From df936c698160fbac094ddda5e10b8a203bbdd4c8 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 27 Jan 2026 14:47:42 -0800 Subject: [PATCH 13/19] feat: IAMs now display when triggers added before first fetch (#2528) --- .../internal/InAppMessagesManager.kt | 47 +++ .../internal/InAppMessagesManagerTests.kt | 325 +++++++++++++++--- 2 files changed, 323 insertions(+), 49 deletions(-) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt index 2b68fe3457..5a23298c0b 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt @@ -116,6 +116,12 @@ internal class InAppMessagesManager( private val fetchIAMMutex = Mutex() private var lastTimeFetchedIAMs: Long? = null + // Tracks whether the first IAM fetch has completed since this cold start + private var hasCompletedFirstFetch: Boolean = false + + // Tracks trigger keys added early on cold start (before first fetch completes), for redisplay logic + private val earlySessionTriggers: MutableSet = java.util.Collections.synchronizedSet(mutableSetOf()) + private val identityModelChangeHandler = object : ISingletonModelStoreChangeHandler { override fun onModelReplaced( @@ -308,6 +314,26 @@ internal class InAppMessagesManager( if (newMessages != null) { this.messages = newMessages as MutableList + + // Apply isTriggerChanged for messages that match triggers added too early on cold start + synchronized(earlySessionTriggers) { + if (earlySessionTriggers.isNotEmpty()) { + Logging.verbose("InAppMessagesManager: Processing triggers added early on cold start: $earlySessionTriggers") + for (message in this.messages) { + val isMessageDisplayed = redisplayedInAppMessages.contains(message) + val isTriggerOnMessage = + _triggerController.isTriggerOnMessage(message, earlySessionTriggers) + if (isMessageDisplayed && isTriggerOnMessage) { + Logging.verbose("InAppMessagesManager: Setting isTriggerChanged=true for message ${message.messageId}") + message.isTriggerChanged = true + } + } + earlySessionTriggers.clear() + } + // Mark that first fetch has completed + hasCompletedFirstFetch = true + } + evaluateInAppMessages() } } @@ -565,6 +591,14 @@ internal class InAppMessagesManager( ) { Logging.debug("InAppMessagesManager.addTrigger(key: $key, value: $value)") + // Track triggers added early on cold start (before first fetch completes) for redisplay logic + synchronized(earlySessionTriggers) { + if (!hasCompletedFirstFetch) { + Logging.verbose("InAppMessagesManager: Tracking trigger added early on cold start: $key") + earlySessionTriggers.add(key) + } + } + var triggerModel = _triggerModelStore.get(key) if (triggerModel != null) { triggerModel.value = value @@ -588,11 +622,24 @@ internal class InAppMessagesManager( override fun removeTrigger(key: String) { Logging.debug("InAppMessagesManager.removeTrigger(key: $key)") + synchronized(earlySessionTriggers) { + if (!hasCompletedFirstFetch) { + earlySessionTriggers.remove(key) + } + } + _triggerModelStore.remove(key) } override fun clearTriggers() { Logging.debug("InAppMessagesManager.clearTriggers()") + + synchronized(earlySessionTriggers) { + if (!hasCompletedFirstFetch) { + earlySessionTriggers.clear() + } + } + _triggerModelStore.clear() } diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt index 397ea43152..418cce53cc 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesManagerTests.kt @@ -54,6 +54,8 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.json.JSONArray import org.json.JSONObject +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible private class Mocks { // mock default services needed for InAppMessagesManager @@ -111,24 +113,59 @@ private class Mocks { result } - // factory-style so every access returns a new message: - val testInAppMessage: InAppMessage - get() { - val json = JSONObject() - json.put("id", "test-message-id") - val variantsJson = JSONObject() - val allVariantJson = JSONObject() - allVariantJson.put("en", "variant-id-123") - variantsJson.put("all", allVariantJson) - json.put("variants", variantsJson) - json.put("triggers", JSONArray()) - return InAppMessage(json, time) - } - // factory-style so every access returns a new message: val testInAppMessagePreview: InAppMessage get() = InAppMessage(true, time) + // Helper function to create InAppMessage with custom triggers (factory-style, returns new message each call) + fun createInAppMessage( + id: String = "test-message-${System.nanoTime()}", // Unique ID by default + triggers: List> = emptyList() // List of (property, operator, value) + ): InAppMessage { + val json = JSONObject().apply { + put("id", id) + put("variants", JSONObject().apply { + put("all", JSONObject().apply { put("en", "variant-id-123") }) + }) + + if (triggers.isEmpty()) { + put("triggers", JSONArray()) + } else { + put("triggers", JSONArray().apply { + put(JSONArray().apply { + triggers.forEach { (property, operator, value) -> + put(JSONObject().apply { + put("id", "trigger-$property") + put("kind", "custom") + put("property", property) + put("operator", operator) + put("value", value) + }) + } + }) + }) + } + } + return InAppMessage(json, time) + } + + // Helper function to access private earlySessionTriggers field for testing using Kotlin reflection + fun getEarlySessionTriggers(manager: InAppMessagesManager): MutableSet { + val property = InAppMessagesManager::class.memberProperties + .first { it.name == "earlySessionTriggers" } + property.isAccessible = true + @Suppress("UNCHECKED_CAST") + return property.get(manager) as MutableSet + } + + // Helper function to access private hasCompletedFirstFetch field for testing using Kotlin reflection + fun getHasCompletedFirstFetch(manager: InAppMessagesManager): Boolean { + val property = InAppMessagesManager::class.memberProperties + .first { it.name == "hasCompletedFirstFetch" } + property.isAccessible = true + return property.get(manager) as Boolean + } + // Helper function to create InAppMessagesManager with all dependencies val inAppMessagesManager = InAppMessagesManager( applicationService, @@ -280,8 +317,8 @@ class InAppMessagesManagerTests : FunSpec({ test("start loads redisplayed messages from repository and resets display flag") { // Given - val message1 = mocks.testInAppMessage - val message2 = mocks.testInAppMessage + val message1 = mocks.createInAppMessage() + val message2 = mocks.createInAppMessage() message1.isDisplayedInSession = true message2.isDisplayedInSession = true val mockRepository = mocks.repository @@ -353,7 +390,7 @@ class InAppMessagesManagerTests : FunSpec({ // When iamManager.addLifecycleListener(mockListener) - iamManager.onMessageWillDisplay(mocks.testInAppMessage) + iamManager.onMessageWillDisplay(mocks.createInAppMessage()) // Then // Verify listener callback was called @@ -368,7 +405,7 @@ class InAppMessagesManagerTests : FunSpec({ // When iamManager.addLifecycleListener(mockListener) iamManager.removeLifecycleListener(mockListener) - iamManager.onMessageWillDisplay(mocks.testInAppMessage) + iamManager.onMessageWillDisplay(mocks.createInAppMessage()) // Then // Listener should not be called after removal @@ -378,7 +415,7 @@ class InAppMessagesManagerTests : FunSpec({ test("addClickListener subscribes listener") { // Given val mockListener = mocks.inAppMessageClickListener - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() val mockClickResult = mocks.inAppMessageClickResult val iamManager = mocks.inAppMessagesManager @@ -395,7 +432,7 @@ class InAppMessagesManagerTests : FunSpec({ test("removeClickListener unsubscribes listener") { // Given val mockListener = mockk(relaxed = true) - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() val mockClickResult = mocks.inAppMessageClickResult val iamManager = mocks.inAppMessagesManager @@ -569,8 +606,8 @@ class InAppMessagesManagerTests : FunSpec({ context("Session Lifecycle") { test("onSessionStarted resets redisplayed messages and fetches messages") { // Given - val message1 = mocks.testInAppMessage - val message2 = mocks.testInAppMessage + val message1 = mocks.createInAppMessage() + val message2 = mocks.createInAppMessage() val mockRywData = mocks.rywData val mockDeferred = mocks.rywDeferred val mockRepository = mocks.repository @@ -626,7 +663,7 @@ class InAppMessagesManagerTests : FunSpec({ mocks.inAppMessagesManager.addLifecycleListener(mocks.inAppMessageLifecycleListener) // When - mocks.inAppMessagesManager.onMessageWillDisplay(mocks.testInAppMessage) + mocks.inAppMessagesManager.onMessageWillDisplay(mocks.createInAppMessage()) awaitIO() // Then @@ -638,7 +675,7 @@ class InAppMessagesManagerTests : FunSpec({ // Given // When/Then - should not throw - mocks.inAppMessagesManager.onMessageWillDisplay(mocks.testInAppMessage) + mocks.inAppMessagesManager.onMessageWillDisplay(mocks.createInAppMessage()) // Verified by no exception being thrown when no listeners are subscribed } @@ -650,7 +687,7 @@ class InAppMessagesManagerTests : FunSpec({ coEvery { mocks.backend.sendIAMImpression(any(), any(), any(), any()) } just runs // When - mocks.inAppMessagesManager.onMessageWasDisplayed(mocks.testInAppMessage) + mocks.inAppMessagesManager.onMessageWasDisplayed(mocks.createInAppMessage()) awaitIO() // Then @@ -670,7 +707,7 @@ class InAppMessagesManagerTests : FunSpec({ test("onMessageWasDisplayed does not send duplicate impressions") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.pushSubscription.id } returns "subscription-id" coEvery { mocks.backend.sendIAMImpression(any(), any(), any(), any()) } just runs @@ -688,7 +725,7 @@ class InAppMessagesManagerTests : FunSpec({ mocks.inAppMessagesManager.addLifecycleListener(mocks.inAppMessageLifecycleListener) // When - mocks.inAppMessagesManager.onMessageWillDismiss(mocks.testInAppMessage) + mocks.inAppMessagesManager.onMessageWillDismiss(mocks.createInAppMessage()) awaitIO() // Then @@ -700,7 +737,7 @@ class InAppMessagesManagerTests : FunSpec({ // Given // When/Then - should not throw - mocks.inAppMessagesManager.onMessageWillDismiss(mocks.testInAppMessage) + mocks.inAppMessagesManager.onMessageWillDismiss(mocks.createInAppMessage()) // Verified by no exception being thrown when no listeners are subscribed } @@ -710,7 +747,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.inAppStateService.inAppMessageIdShowing } returns null // When - mocks.inAppMessagesManager.onMessageWasDismissed(mocks.testInAppMessage) + mocks.inAppMessagesManager.onMessageWasDismissed(mocks.createInAppMessage()) awaitIO() // Then @@ -731,7 +768,7 @@ class InAppMessagesManagerTests : FunSpec({ test("onTriggerConditionChanged makes redisplay messages available and re-evaluates") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" @@ -751,7 +788,7 @@ class InAppMessagesManagerTests : FunSpec({ test("onTriggerChanged makes redisplay messages available and re-evaluates") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" @@ -814,7 +851,7 @@ class InAppMessagesManagerTests : FunSpec({ coEvery { mocks.backend.sendIAMPageImpression(any(), any(), any(), any(), any()) } just runs // When - mocks.inAppMessagesManager.onMessagePageChanged(mocks.testInAppMessage, mockPage) + mocks.inAppMessagesManager.onMessagePageChanged(mocks.createInAppMessage(), mockPage) awaitIO() // Then @@ -837,7 +874,7 @@ class InAppMessagesManagerTests : FunSpec({ context("Error Handling") { test("onMessageWasDisplayed removes impression from set on backend failure") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.pushSubscription.id } returns "subscription-id" coEvery { mocks.backend.sendIAMImpression(any(), any(), any(), any()) @@ -857,7 +894,7 @@ class InAppMessagesManagerTests : FunSpec({ test("onMessagePageChanged removes page impression on backend failure") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() val mockPage = mockk(relaxed = true) every { mocks.pushSubscription.id } returns "subscription-id" every { mockPage.pageId } returns "page-id" @@ -879,7 +916,7 @@ class InAppMessagesManagerTests : FunSpec({ test("onMessageActionOccurredOnMessage removes click on backend failure") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() coEvery { mocks.backend.sendIAMClick(any(), any(), any(), any(), any(), any()) } throws BackendException(500, "Server error") @@ -939,7 +976,7 @@ class InAppMessagesManagerTests : FunSpec({ test("fetchMessagesWhenConditionIsMet evaluates messages when new messages are returned") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" @@ -959,7 +996,7 @@ class InAppMessagesManagerTests : FunSpec({ context("Message Queue and Display") { test("messages are not queued when paused") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" @@ -981,7 +1018,7 @@ class InAppMessagesManagerTests : FunSpec({ context("Message Evaluation") { test("messages are evaluated and queued when paused is set to false") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" @@ -1007,7 +1044,7 @@ class InAppMessagesManagerTests : FunSpec({ test("dismissed messages are not queued for display") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() every { mocks.userManager.onesignalId } returns "onesignal-id" every { mocks.applicationService.isInForeground } returns true every { mocks.pushSubscription.id } returns "subscription-id" @@ -1036,7 +1073,7 @@ class InAppMessagesManagerTests : FunSpec({ // Given // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then - wait for async operations @@ -1049,7 +1086,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.testOutcome.weight } returns weight // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then - wait for async operations @@ -1066,7 +1103,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.inAppMessageClickResult.tags } returns mockTags // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then - wait for async operations @@ -1083,7 +1120,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.inAppMessageClickResult.tags } returns mockTags // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then - wait for async operations @@ -1101,7 +1138,7 @@ class InAppMessagesManagerTests : FunSpec({ every { AndroidUtils.openURLInBrowser(any(), any()) } just runs // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then @@ -1120,7 +1157,7 @@ class InAppMessagesManagerTests : FunSpec({ every { OneSignalChromeTab.open(any(), any(), any()) } returns true // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then @@ -1133,7 +1170,7 @@ class InAppMessagesManagerTests : FunSpec({ // Given // When/Then - should not throw - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) } } context("Prompt Processing") { @@ -1149,7 +1186,7 @@ class InAppMessagesManagerTests : FunSpec({ every { mocks.inAppStateService.currentPrompt = any() } answers { currentPrompt = firstArg() } // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then @@ -1161,7 +1198,7 @@ class InAppMessagesManagerTests : FunSpec({ // Given // When - mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.testInAppMessage, mocks.inAppMessageClickResult) + mocks.inAppMessagesManager.onMessageActionOccurredOnMessage(mocks.createInAppMessage(), mocks.inAppMessageClickResult) awaitIO() // Then @@ -1172,7 +1209,7 @@ class InAppMessagesManagerTests : FunSpec({ context("Message Persistence") { test("onMessageWasDismissed persists message to repository") { // Given - val message = mocks.testInAppMessage + val message = mocks.createInAppMessage() coEvery { mocks.repository.saveInAppMessage(any()) } just runs every { mocks.inAppStateService.lastTimeInAppDismissed } returns 500L every { mocks.inAppStateService.currentPrompt } returns null @@ -1187,4 +1224,194 @@ class InAppMessagesManagerTests : FunSpec({ message.isTriggerChanged shouldBe false } } + + context("Early Trigger Tracking") { + test("triggers added before first fetch are tracked in earlySessionTriggers") { + // Given + val iamManager = mocks.inAppMessagesManager + every { mocks.triggerModelStore.get(any()) } returns null + every { mocks.triggerModelStore.add(any()) } answers {} + + // When + iamManager.addTrigger("trigger1", "value1") + iamManager.addTrigger("trigger2", "value2") + + // Then + // Verify triggers were added to the triggerModelStore + verify(exactly = 1) { mocks.triggerModelStore.add(match { it.key == "trigger1" && it.value == "value1" }) } + verify(exactly = 1) { mocks.triggerModelStore.add(match { it.key == "trigger2" && it.value == "value2" }) } + + // Verify triggers were tracked in earlySessionTriggers + val earlySessionTriggers = mocks.getEarlySessionTriggers(iamManager) + earlySessionTriggers.contains("trigger1") shouldBe true + earlySessionTriggers.contains("trigger2") shouldBe true + } + + test("messages matching early triggers get isTriggerChanged flag after first fetch") { + // Given + every { mocks.applicationService.isInForeground } returns true + every { mocks.pushSubscription.id } returns "subscription-id" + every { mocks.triggerModelStore.get(any()) } returns null + every { mocks.triggerModelStore.add(any()) } answers {} + + // Create test messages + // message1 has a trigger matching the early trigger we'll add + val message1 = mocks.createInAppMessage( + id = "message-1", + triggers = listOf(Triple("earlyTrigger", "equal", "value")) + ) + + // message2 has a different trigger that doesn't match + val message2 = mocks.createInAppMessage( + id = "message-2", + triggers = listOf(Triple("otherTrigger", "equal", "value")) + ) + + // Add message1 to redisplayedInAppMessages (simulate it was previously shown) + val redisplayedMessages = mutableListOf(message1) + coEvery { mocks.repository.listInAppMessages() } returns redisplayedMessages + + // Mock trigger controller to say message1 matches early trigger, message2 does not + every { mocks.triggerController.isTriggerOnMessage(message1, any>()) } returns true + every { mocks.triggerController.isTriggerOnMessage(message2, any>()) } returns false + every { mocks.triggerController.evaluateMessageTriggers(any()) } returns false + + // Mock backend to return both messages + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message1, message2) + + // Start the manager to load redisplayed messages + mocks.inAppMessagesManager.start() + awaitIO() + + // Add triggers before first fetch + mocks.inAppMessagesManager.addTrigger("earlyTrigger", "value") + + // Both messages start with isTriggerChanged = false + message1.isTriggerChanged shouldBe false + message2.isTriggerChanged shouldBe false + + // When - Trigger first fetch + mocks.inAppMessagesManager.onSessionStarted() + awaitIO() + + // Message 1 should have isTriggerChanged = true (matches early trigger and was redisplayed) + message1.isTriggerChanged shouldBe true + + // Message 2 should have isTriggerChanged = false (does not match early trigger) + message2.isTriggerChanged shouldBe false + } + + test("triggers added after first fetch are not tracked as early triggers") { + // Given + val message = mocks.createInAppMessage( + id = "message-1", + triggers = listOf(Triple("lateTrigger", "equal", "value")) + ) + + every { mocks.userManager.onesignalId } returns "onesignal-id" + every { mocks.applicationService.isInForeground } returns true + every { mocks.pushSubscription.id } returns "subscription-id" + every { mocks.configModelStore.model.appId } returns "test-app-id" + every { mocks.configModelStore.model.fetchIAMMinInterval } returns 0L + every { mocks.triggerModelStore.get(any()) } returns null + every { mocks.triggerModelStore.add(any()) } answers {} + + // Message is in redisplayedInAppMessages from the start + coEvery { mocks.repository.listInAppMessages() } returns mutableListOf(message) + + // Mock trigger controller to say message matches the late trigger + every { mocks.triggerController.isTriggerOnMessage(message, any>()) } returns true + every { mocks.triggerController.evaluateMessageTriggers(any()) } returns false + + // Mock first fetch to return the message + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + + mocks.inAppMessagesManager.start() + awaitIO() + + // Trigger first fetch (this sets hasCompletedFirstFetch = true) + mocks.inAppMessagesManager.onSessionStarted() + awaitIO() + + // Verify first fetch completed + mocks.getHasCompletedFirstFetch(mocks.inAppMessagesManager) shouldBe true + + // When - Add trigger AFTER first fetch (should NOT be tracked) + mocks.inAppMessagesManager.addTrigger("lateTrigger", "value") + + // Verify trigger was NOT added to earlySessionTriggers + val earlySessionTriggers = mocks.getEarlySessionTriggers(mocks.inAppMessagesManager) + earlySessionTriggers.contains("lateTrigger") shouldBe false + + // Mock second fetch to return the same message + coEvery { mocks.backend.listInAppMessages(any(), any(), any(), any()) } returns listOf(message) + + // Trigger second fetch + mocks.inAppMessagesManager.onSessionStarted() + awaitIO() + + // Then + // Message should NOT have isTriggerChanged because trigger was added after first fetch + message.isTriggerChanged shouldBe false + } + + test("earlySessionTriggers is cleared after first fetch") { + // Given + val mockTriggerModelStore = mocks.triggerModelStore + val mockBackend = mocks.backend + val mockRepository = mocks.repository + val mockTriggerController = mocks.triggerController + val iamManager = mocks.inAppMessagesManager + + every { mockTriggerModelStore.get(any()) } returns null + every { mockTriggerModelStore.add(any()) } answers {} + coEvery { mockRepository.listInAppMessages() } returns mutableListOf() + every { mockTriggerController.evaluateMessageTriggers(any()) } returns false + coEvery { mockBackend.listInAppMessages(any(), any(), any(), any()) } returns listOf(mocks.createInAppMessage()) + + every { mocks.pushSubscription.id } returns "test-sub-id" + every { mocks.configModelStore.model.appId } returns "test-app-id" + every { mocks.configModelStore.model.fetchIAMMinInterval } returns 0L + every { mocks.applicationService.isInForeground } returns true + + iamManager.start() + awaitIO() + + // Add triggers before first fetch + iamManager.addTrigger("trigger1", "value1") + iamManager.addTrigger("trigger2", "value2") + + // Verify triggers were tracked before first fetch + val earlySessionTriggersBeforeFetch = mocks.getEarlySessionTriggers(iamManager) + earlySessionTriggersBeforeFetch.size shouldBe 2 + earlySessionTriggersBeforeFetch.contains("trigger1") shouldBe true + earlySessionTriggersBeforeFetch.contains("trigger2") shouldBe true + + // When - Trigger first fetch + mocks.inAppMessagesManager.onSessionStarted() + awaitIO() + + // Verify earlySessionTriggers was cleared after first fetch + val earlySessionTriggersAfterFetch = mocks.getEarlySessionTriggers(iamManager) + earlySessionTriggersAfterFetch.size shouldBe 0 + + // Create a message for potential second fetch + val messageAfterClear = mocks.createInAppMessage() + + // Mock backend for second fetch + coEvery { mockBackend.listInAppMessages(any(), any(), any(), any()) } returns listOf(messageAfterClear) + + // Mock that message is in redisplayed and matches the cleared triggers + coEvery { mockRepository.listInAppMessages() } returns mutableListOf(messageAfterClear) + every { mockTriggerController.isTriggerOnMessage(messageAfterClear, any>()) } returns true + + // Trigger second fetch + mocks.inAppMessagesManager.onSessionStarted() + awaitIO() + + // Then + // Message should NOT have isTriggerChanged because earlySessionTriggers was cleared after first fetch + messageAfterClear.isTriggerChanged shouldBe false + } + } }) From 35c53e1c2affc715c1ac2b89a3f648a54f31161f Mon Sep 17 00:00:00 2001 From: jinliu Date: Fri, 30 Jan 2026 10:51:54 -0500 Subject: [PATCH 14/19] fix: custom events now handle null object within the event properties (#2537) Co-authored-by: AR Abdul Azeez --- .../java/com/onesignal/common/JSONUtils.kt | 10 +- .../java/com/onesignal/user/IUserManager.kt | 2 +- .../onesignal/user/internal/UserManager.kt | 2 +- .../customEvents/ICustomEventController.kt | 12 +- .../impl/CustomEventController.kt | 6 +- .../customEvents/impl/CustomEventMetadata.kt | 10 + .../operations/TrackCustomEventOperation.kt | 5 + .../com/onesignal/common/JSONUtilsTests.kt | 125 ++++++++- .../user/internal/UserManagerTests.kt | 1 + .../impl/CustomEventControllerTests.kt | 251 ++++++++++++++++++ 10 files changed, 414 insertions(+), 10 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/customEvents/impl/CustomEventControllerTests.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt index 3169fd3d16..0cf3b0bdd1 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt @@ -187,7 +187,7 @@ object JSONUtils { * Recursively convert a JSON-serializable map into a JSON-compatible format, handling * nested Maps and Lists appropriately. */ - fun mapToJson(map: Map): JSONObject { + fun mapToJson(map: Map): JSONObject { val json = JSONObject() for ((key, value) in map) { json.put(key, convertToJson(value)) @@ -198,21 +198,23 @@ object JSONUtils { /** * Recursively converts maps and lists into JSON-compatible objects, transforming maps with * String keys into JSON objects, lists into JSON arrays, and leaving primitive values unchanged to support safe JSON serialization. + * Null values are converted to JSONObject.NULL to preserve them in the JSON structure. */ - fun convertToJson(value: Any): Any { + fun convertToJson(value: Any?): Any? { return when (value) { + null -> JSONObject.NULL is Map<*, *> -> { val subMap = value.entries .filter { it.key is String } .associate { - it.key as String to convertToJson(it.value!!) + it.key as String to convertToJson(it.value) } mapToJson(subMap) } is List<*> -> { val array = JSONArray() - value.forEach { array.put(convertToJson(it!!)) } + value.forEach { array.put(convertToJson(it)) } array } else -> value diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt index 7ebf37f14f..be2dd9ed61 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt @@ -175,6 +175,6 @@ interface IUserManager { */ fun trackEvent( name: String, - properties: Map? = null, + properties: Map? = null, ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt index 60f322b805..328cb9da7d 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt @@ -249,7 +249,7 @@ internal open class UserManager( override fun trackEvent( name: String, - properties: Map?, + properties: Map?, ) { if (!JSONUtils.isValidJsonObject(properties)) { Logging.log(LogLevel.ERROR, "Custom event properties are not JSON-serializable") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt index 35c307539c..a72ad38f35 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt @@ -1,8 +1,18 @@ package com.onesignal.user.internal.customEvents +/** + * Interface for sending custom events to track user behavior. + */ interface ICustomEventController { + /** + * Sends a custom event with optional properties. + * + * @param name The name of the custom event + * @param properties Optional map of event properties. Can contain nested maps, lists, and null values. + * Properties will be converted to JSON format. + */ fun sendCustomEvent( name: String, - properties: Map?, + properties: Map?, ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt index 05142fe784..f63f69775c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt @@ -8,6 +8,10 @@ import com.onesignal.user.internal.customEvents.ICustomEventController import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.operations.TrackCustomEventOperation +/** + * Controller for custom events. Handles the creation and enqueueing of custom event operations + * for tracking user events with optional properties. + */ class CustomEventController( private val identityModelStore: IdentityModelStore, private val configModelStore: ConfigModelStore, @@ -16,7 +20,7 @@ class CustomEventController( ) : ICustomEventController { override fun sendCustomEvent( name: String, - properties: Map?, + properties: Map?, ) { val op = TrackCustomEventOperation( diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt index cd14d6a909..de64db7f09 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt @@ -4,6 +4,10 @@ import com.onesignal.common.putSafe import org.json.JSONException import org.json.JSONObject +/** + * Metadata for custom events containing device and SDK information. + * This metadata is included with custom events sent to the OneSignal backend. + */ class CustomEventMetadata( val deviceType: String?, val sdk: String?, @@ -12,6 +16,12 @@ class CustomEventMetadata( val deviceModel: String?, val deviceOS: String?, ) { + /** + * Converts the metadata to a JSONObject for serialization. + * + * @return JSONObject containing all metadata fields + * @throws JSONException if JSON serialization fails + */ @Throws(JSONException::class) fun toJSONObject(): JSONObject { val json = JSONObject() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt index 73313f97ec..b510a4fd3f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt @@ -5,6 +5,11 @@ import com.onesignal.core.internal.operations.GroupComparisonType import com.onesignal.core.internal.operations.Operation import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor +/** + * An [Operation] to track a single custom event with properties for the current user. + * This operation is enqueued when a user tracks a custom event and will be processed + * by the [CustomEventOperationExecutor] to send the event to the OneSignal backend. + */ class TrackCustomEventOperation() : Operation(CustomEventOperationExecutor.CUSTOM_EVENT) { /** * The OneSignal appId the custom event was created. diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt index 1320369ca4..d92ecd58ff 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt @@ -772,6 +772,7 @@ class JSONUtilsTests : FunSpec({ JSONUtils.convertToJson(true) shouldBe true JSONUtils.convertToJson(false) shouldBe false JSONUtils.convertToJson(3.14) shouldBe 3.14 + JSONUtils.convertToJson(null) shouldBe JSONObject.NULL } test("should convert Map to JSONObject") { @@ -887,18 +888,19 @@ class JSONUtilsTests : FunSpec({ test("should handle List with mixed types") { // Given - val list = listOf("string", 42, true, 3.14) + val list = listOf("string", 42, true, 3.14, null) // When val result = JSONUtils.convertToJson(list) // Then val jsonArray = result as JSONArray - jsonArray.length() shouldBe 4 + jsonArray.length() shouldBe 5 jsonArray.getString(0) shouldBe "string" jsonArray.getInt(1) shouldBe 42 jsonArray.getBoolean(2) shouldBe true jsonArray.getDouble(3) shouldBe 3.14 + jsonArray.get(4) shouldBe JSONObject.NULL } test("should filter out non-String keys from Map") { @@ -941,5 +943,124 @@ class JSONUtilsTests : FunSpec({ val level2Item = level2Array.getJSONObject(0) level2Item.getString("level3") shouldBe "deepValue" } + + test("should handle null values in maps") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to null, + "key3" to 42, + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + jsonObject.getString("key1") shouldBe "value1" + jsonObject.isNull("key2") shouldBe true + jsonObject.getInt("key3") shouldBe 42 + } + + test("should handle null values in nested objects") { + // Given + val map = mapOf( + "someObject" to mapOf( + "abc" to "123", + "nested" to mapOf( + "def" to "456", + ), + "ghi" to null, + ), + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + val someObject = jsonObject.getJSONObject("someObject") + someObject.getString("abc") shouldBe "123" + val nested = someObject.getJSONObject("nested") + nested.getString("def") shouldBe "456" + someObject.isNull("ghi") shouldBe true + } + + test("should handle null values in arrays") { + // Given + val map = mapOf( + "someArray" to listOf(1, 2), + "someMixedArray" to listOf(1, "2", mapOf("abc" to "123"), null), + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + val someArray = jsonObject.getJSONArray("someArray") + someArray.length() shouldBe 2 + someArray.getInt(0) shouldBe 1 + someArray.getInt(1) shouldBe 2 + + val someMixedArray = jsonObject.getJSONArray("someMixedArray") + someMixedArray.length() shouldBe 4 + someMixedArray.getInt(0) shouldBe 1 + someMixedArray.getString(1) shouldBe "2" + val nestedObj = someMixedArray.getJSONObject(2) + nestedObj.getString("abc") shouldBe "123" + someMixedArray.get(3) shouldBe JSONObject.NULL + } + + test("should handle complete example structure with nulls") { + // Given - matches the user's example structure + val map = mapOf( + "someNum" to 123, + "someFloat" to 3.14159, + "someString" to "abc", + "someBool" to true, + "someObject" to mapOf( + "abc" to "123", + "nested" to mapOf( + "def" to "456", + ), + "ghi" to null, + ), + "someArray" to listOf(1, 2), + "someMixedArray" to listOf(1, "2", mapOf("abc" to "123"), null), + "someNull" to null, + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + jsonObject.getInt("someNum") shouldBe 123 + jsonObject.getDouble("someFloat") shouldBe 3.14159 + jsonObject.getString("someString") shouldBe "abc" + jsonObject.getBoolean("someBool") shouldBe true + + val someObject = jsonObject.getJSONObject("someObject") + someObject.getString("abc") shouldBe "123" + val nested = someObject.getJSONObject("nested") + nested.getString("def") shouldBe "456" + someObject.isNull("ghi") shouldBe true + + val someArray = jsonObject.getJSONArray("someArray") + someArray.length() shouldBe 2 + someArray.getInt(0) shouldBe 1 + someArray.getInt(1) shouldBe 2 + + val someMixedArray = jsonObject.getJSONArray("someMixedArray") + someMixedArray.length() shouldBe 4 + someMixedArray.getInt(0) shouldBe 1 + someMixedArray.getString(1) shouldBe "2" + val nestedObj = someMixedArray.getJSONObject(2) + nestedObj.getString("abc") shouldBe "123" + someMixedArray.get(3) shouldBe JSONObject.NULL + + jsonObject.isNull("someNull") shouldBe true + } } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt index ada9f00f68..d4f3121b8a 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt @@ -210,6 +210,7 @@ class UserManagerTests : FunSpec({ "key3" to 5.123, "key4" to mapOf("key4-1" to "value4-1"), "key5" to mapOf("key5-1" to mapOf("key5-1-1" to 0)), + "key6" to null, ) // When diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/customEvents/impl/CustomEventControllerTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/customEvents/impl/CustomEventControllerTests.kt new file mode 100644 index 0000000000..01bb4b4018 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/customEvents/impl/CustomEventControllerTests.kt @@ -0,0 +1,251 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.common.JSONUtils +import com.onesignal.core.internal.operations.IOperationRepo +import com.onesignal.mocks.MockHelper +import com.onesignal.user.internal.operations.TrackCustomEventOperation +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import io.mockk.verify +import org.json.JSONObject + +class CustomEventControllerTests : FunSpec({ + test("should create and enqueue TrackCustomEventOperation with all fields") { + // Given + val appId = "test-app-id" + val onesignalId = "test-onesignal-id" + val externalId = "test-external-id" + val timestamp = 1234567890L + val eventName = "test-event" + val properties = mapOf( + "key1" to "value1", + "key2" to 42, + ) + + val configModelStore = MockHelper.configModelStore { + it.appId = appId + } + val identityModelStore = MockHelper.identityModelStore { + it.onesignalId = onesignalId + it.externalId = externalId + } + val time = MockHelper.time(timestamp) + val opRepo = mockk(relaxed = true) + every { opRepo.enqueue(any()) } just runs + + val controller = CustomEventController( + identityModelStore, + configModelStore, + time, + opRepo, + ) + + // When + controller.sendCustomEvent(eventName, properties) + + // Then + val operationSlot = slot() + verify(exactly = 1) { opRepo.enqueue(capture(operationSlot)) } + + val operation = operationSlot.captured + operation.appId shouldBe appId + operation.onesignalId shouldBe onesignalId + operation.externalId shouldBe externalId + operation.timeStamp shouldBe timestamp + operation.eventName shouldBe eventName + operation.eventProperties shouldBe JSONUtils.mapToJson(properties).toString() + } + + test("should handle null properties") { + // Given + val appId = "test-app-id" + val onesignalId = "test-onesignal-id" + val timestamp = 1234567890L + val eventName = "test-event" + + val configModelStore = MockHelper.configModelStore { + it.appId = appId + } + val identityModelStore = MockHelper.identityModelStore { + it.onesignalId = onesignalId + } + val time = MockHelper.time(timestamp) + val opRepo = mockk(relaxed = true) + every { opRepo.enqueue(any()) } just runs + + val controller = CustomEventController( + identityModelStore, + configModelStore, + time, + opRepo, + ) + + // When + controller.sendCustomEvent(eventName, null) + + // Then + val operationSlot = slot() + verify(exactly = 1) { opRepo.enqueue(capture(operationSlot)) } + + val operation = operationSlot.captured + operation.appId shouldBe appId + operation.onesignalId shouldBe onesignalId + operation.timeStamp shouldBe timestamp + operation.eventName shouldBe eventName + operation.eventProperties shouldBe null + } + + test("should convert properties with nested structures to JSON") { + // Given + val appId = "test-app-id" + val onesignalId = "test-onesignal-id" + val timestamp = 1234567890L + val eventName = "test-event" + val properties = mapOf( + "someNum" to 123, + "someFloat" to 3.14159, + "someString" to "abc", + "someBool" to true, + "someObject" to mapOf( + "abc" to "123", + "nested" to mapOf( + "def" to "456", + ), + "ghi" to null, + ), + "someArray" to listOf(1, 2), + "someMixedArray" to listOf(1, "2", mapOf("abc" to "123"), null), + "someNull" to null, + ) + + val configModelStore = MockHelper.configModelStore { + it.appId = appId + } + val identityModelStore = MockHelper.identityModelStore { + it.onesignalId = onesignalId + } + val time = MockHelper.time(timestamp) + val opRepo = mockk(relaxed = true) + every { opRepo.enqueue(any()) } just runs + + val controller = CustomEventController( + identityModelStore, + configModelStore, + time, + opRepo, + ) + + // When + controller.sendCustomEvent(eventName, properties) + + // Then + val operationSlot = slot() + verify(exactly = 1) { opRepo.enqueue(capture(operationSlot)) } + + val operation = operationSlot.captured + val jsonProperties = JSONObject(operation.eventProperties!!) + + jsonProperties.getInt("someNum") shouldBe 123 + jsonProperties.getDouble("someFloat") shouldBe 3.14159 + jsonProperties.getString("someString") shouldBe "abc" + jsonProperties.getBoolean("someBool") shouldBe true + + val someObject = jsonProperties.getJSONObject("someObject") + someObject.getString("abc") shouldBe "123" + val nested = someObject.getJSONObject("nested") + nested.getString("def") shouldBe "456" + someObject.isNull("ghi") shouldBe true + + val someArray = jsonProperties.getJSONArray("someArray") + someArray.length() shouldBe 2 + someArray.getInt(0) shouldBe 1 + someArray.getInt(1) shouldBe 2 + + val someMixedArray = jsonProperties.getJSONArray("someMixedArray") + someMixedArray.length() shouldBe 4 + someMixedArray.getInt(0) shouldBe 1 + someMixedArray.getString(1) shouldBe "2" + val arrayObj = someMixedArray.getJSONObject(2) + arrayObj.getString("abc") shouldBe "123" + someMixedArray.get(3) shouldBe JSONObject.NULL + + jsonProperties.isNull("someNull") shouldBe true + } + + test("should handle empty properties map") { + // Given + val appId = "test-app-id" + val onesignalId = "test-onesignal-id" + val timestamp = 1234567890L + val eventName = "test-event" + val properties = emptyMap() + + val configModelStore = MockHelper.configModelStore { + it.appId = appId + } + val identityModelStore = MockHelper.identityModelStore { + it.onesignalId = onesignalId + } + val time = MockHelper.time(timestamp) + val opRepo = mockk(relaxed = true) + every { opRepo.enqueue(any()) } just runs + + val controller = CustomEventController( + identityModelStore, + configModelStore, + time, + opRepo, + ) + + // When + controller.sendCustomEvent(eventName, properties) + + // Then + val operationSlot = slot() + verify(exactly = 1) { opRepo.enqueue(capture(operationSlot)) } + + val operation = operationSlot.captured + val jsonProperties = JSONObject(operation.eventProperties!!) + jsonProperties.length() shouldBe 0 + } + + test("should use current timestamp from time service") { + // Given + val appId = "test-app-id" + val onesignalId = "test-onesignal-id" + val timestamp = 1000L + val eventName = "test-event" + + val configModelStore = MockHelper.configModelStore { + it.appId = appId + } + val identityModelStore = MockHelper.identityModelStore { + it.onesignalId = onesignalId + } + val time = MockHelper.time(timestamp) + val opRepo = mockk(relaxed = true) + every { opRepo.enqueue(any()) } just runs + + val controller = CustomEventController( + identityModelStore, + configModelStore, + time, + opRepo, + ) + + // When + controller.sendCustomEvent(eventName, null) + + // Then + val operationSlot = slot() + verify(exactly = 1) { opRepo.enqueue(capture(operationSlot)) } + + val operation = operationSlot.captured + operation.timeStamp shouldBe timestamp + } +}) From 0198eddfe0aa4f52aa372b50bb038c1478d9a6f7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 11 Feb 2026 11:29:53 -0800 Subject: [PATCH 15/19] Rename sdk-actions to sdk-shared in workflows (#2547) Co-authored-by: Cursor --- .github/workflows/create-release-pr.yml | 2 +- .github/workflows/lint-pr-title.yml | 2 +- .github/workflows/publish-release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index ce11e837bb..1720874a11 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -70,7 +70,7 @@ jobs: fetch-tags: true - name: Setup Git User - uses: OneSignal/sdk-actions/.github/actions/setup-git-user@main + uses: OneSignal/sdk-shared/.github/actions/setup-git-user@main - name: Create release branch from base run: | diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml index 164f72fe56..fd44677d6d 100644 --- a/.github/workflows/lint-pr-title.yml +++ b/.github/workflows/lint-pr-title.yml @@ -10,5 +10,5 @@ on: jobs: call: - uses: OneSignal/sdk-actions/.github/workflows/lint-pr-title.yml@main + uses: OneSignal/sdk-shared/.github/workflows/lint-pr-title.yml@main secrets: inherit diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b8fa1f2888..d59c2d725a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -178,7 +178,7 @@ jobs: wrapper_prs: needs: publish - uses: OneSignal/sdk-actions/.github/workflows/create-wrapper-prs.yml@main + uses: OneSignal/sdk-shared/.github/workflows/create-wrapper-prs.yml@main secrets: GH_PUSH_TOKEN: ${{ secrets.GH_PUSH_TOKEN }} with: From 9d2b4b76c2d913bafd457caf3cd9bc839fdd09de Mon Sep 17 00:00:00 2001 From: abdulraqeeb33 Date: Fri, 13 Feb 2026 15:12:57 -0500 Subject: [PATCH 16/19] chore: Updating the example app (#2543) Co-authored-by: AR Abdul Azeez Co-authored-by: Nan Co-authored-by: Fadi George Co-authored-by: Cursor --- .../app/agconnect-services.json | 96 ++ Examples/OneSignalDemoV2/app/build.gradle.kts | 136 +++ .../OneSignalDemoV2/app/google-services.json | 30 + .../OneSignalDemoV2/app/proguard-rules.pro | 21 + .../app/src/huawei/AndroidManifest.xml | 17 + .../notification/HmsMessageServiceAppLevel.kt | 68 ++ .../app/src/main/AndroidManifest.xml | 72 ++ .../sdktest/application/MainApplication.kt | 145 +++ .../sdktest/data/model/InAppMessageType.kt | 43 + .../sdktest/data/model/NotificationType.kt | 24 + .../sdktest/data/network/OneSignalService.kt | 288 ++++++ .../data/repository/OneSignalRepository.kt | 243 +++++ .../sdktest/ui/components/ActionButton.kt | 177 ++++ .../sdktest/ui/components/Dialogs.kt | 660 ++++++++++++ .../sdktest/ui/components/ListComponents.kt | 220 ++++ .../sdktest/ui/components/LoadingOverlay.kt | 56 ++ .../sdktest/ui/components/LogView.kt | 232 +++++ .../sdktest/ui/components/SectionCard.kt | 88 ++ .../sdktest/ui/components/ToggleRow.kt | 62 ++ .../onesignal/sdktest/ui/main/MainActivity.kt | 44 + .../onesignal/sdktest/ui/main/MainScreen.kt | 461 +++++++++ .../sdktest/ui/main/MainViewModel.kt | 626 ++++++++++++ .../com/onesignal/sdktest/ui/main/Sections.kt | 495 +++++++++ .../sdktest/ui/secondary/SecondaryActivity.kt | 69 ++ .../com/onesignal/sdktest/ui/theme/Theme.kt | 98 ++ .../com/onesignal/sdktest/util/LogManager.kt | 111 ++ .../sdktest/util/SharedPreferenceUtil.kt | 72 ++ .../onesignal/sdktest/util/TooltipHelper.kt | 103 ++ .../ic_alert_octagon_white_48dp.png | Bin 0 -> 763 bytes .../res/drawable-hdpi/ic_alert_white_48dp.png | Bin 0 -> 1178 bytes .../res/drawable-hdpi/ic_bell_white_24dp.png | Bin 0 -> 635 bytes .../ic_brightness_percent_white_24dp.png | Bin 0 -> 772 bytes .../res/drawable-hdpi/ic_cart_white_24dp.png | Bin 0 -> 714 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 0 -> 1525 bytes .../ic_chevron_down_white_48dp.png | Bin 0 -> 588 bytes .../ic_chevron_up_white_48dp.png | Bin 0 -> 578 bytes .../res/drawable-hdpi/ic_email_white_48dp.png | Bin 0 -> 1017 bytes .../ic_gesture_tap_white_24dp.png | Bin 0 -> 798 bytes .../ic_human_greeting_white_24dp.png | Bin 0 -> 751 bytes .../res/drawable-hdpi/ic_image_white_24dp.png | Bin 0 -> 649 bytes .../ic_information_white_48dp.png | Bin 0 -> 1352 bytes .../ic_map_marker_white_24dp.png | Bin 0 -> 793 bytes .../drawable-hdpi/ic_message_white_48dp.png | Bin 0 -> 675 bytes .../drawable-hdpi/ic_newspaper_white_24dp.png | Bin 0 -> 576 bytes .../res/drawable-hdpi/ic_star_white_24dp.png | Bin 0 -> 764 bytes .../ic_alert_octagon_white_48dp.png | Bin 0 -> 621 bytes .../res/drawable-mdpi/ic_alert_white_48dp.png | Bin 0 -> 902 bytes .../res/drawable-mdpi/ic_bell_white_24dp.png | Bin 0 -> 508 bytes .../ic_brightness_percent_white_24dp.png | Bin 0 -> 646 bytes .../res/drawable-mdpi/ic_cart_white_24dp.png | Bin 0 -> 544 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 0 -> 994 bytes .../ic_chevron_down_white_48dp.png | Bin 0 -> 510 bytes .../ic_chevron_up_white_48dp.png | Bin 0 -> 500 bytes .../res/drawable-mdpi/ic_email_white_48dp.png | Bin 0 -> 757 bytes .../ic_gesture_tap_white_24dp.png | Bin 0 -> 578 bytes .../ic_human_greeting_white_24dp.png | Bin 0 -> 588 bytes .../res/drawable-mdpi/ic_image_white_24dp.png | Bin 0 -> 506 bytes .../ic_information_white_48dp.png | Bin 0 -> 940 bytes .../ic_map_marker_white_24dp.png | Bin 0 -> 604 bytes .../drawable-mdpi/ic_message_white_48dp.png | Bin 0 -> 537 bytes .../drawable-mdpi/ic_newspaper_white_24dp.png | Bin 0 -> 472 bytes .../res/drawable-mdpi/ic_star_white_24dp.png | Bin 0 -> 609 bytes .../drawable-nodpi/onesignal_rectangle.png | Bin 0 -> 18638 bytes .../res/drawable-nodpi/onesignal_square.png | Bin 0 -> 99328 bytes .../ic_alert_octagon_white_48dp.png | Bin 0 -> 994 bytes .../drawable-xhdpi/ic_alert_white_48dp.png | Bin 0 -> 1402 bytes .../res/drawable-xhdpi/ic_bell_white_24dp.png | Bin 0 -> 698 bytes .../ic_brightness_percent_white_24dp.png | Bin 0 -> 932 bytes .../res/drawable-xhdpi/ic_cart_white_24dp.png | Bin 0 -> 867 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 0 -> 1994 bytes .../ic_chevron_down_white_48dp.png | Bin 0 -> 668 bytes .../ic_chevron_up_white_48dp.png | Bin 0 -> 687 bytes .../drawable-xhdpi/ic_email_white_48dp.png | Bin 0 -> 1319 bytes .../ic_gesture_tap_white_24dp.png | Bin 0 -> 911 bytes .../ic_human_greeting_white_24dp.png | Bin 0 -> 873 bytes .../drawable-xhdpi/ic_image_white_24dp.png | Bin 0 -> 731 bytes .../ic_information_white_48dp.png | Bin 0 -> 1716 bytes .../ic_map_marker_white_24dp.png | Bin 0 -> 921 bytes .../drawable-xhdpi/ic_message_white_48dp.png | Bin 0 -> 786 bytes .../ic_newspaper_white_24dp.png | Bin 0 -> 621 bytes .../res/drawable-xhdpi/ic_star_white_24dp.png | Bin 0 -> 924 bytes .../ic_alert_octagon_white_48dp.png | Bin 0 -> 1504 bytes .../drawable-xxhdpi/ic_alert_white_48dp.png | Bin 0 -> 1860 bytes .../drawable-xxhdpi/ic_bell_white_24dp.png | Bin 0 -> 979 bytes .../ic_brightness_percent_white_24dp.png | Bin 0 -> 1213 bytes .../drawable-xxhdpi/ic_cart_white_24dp.png | Bin 0 -> 1218 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 0 -> 3041 bytes .../ic_chevron_down_white_48dp.png | Bin 0 -> 793 bytes .../ic_chevron_up_white_48dp.png | Bin 0 -> 813 bytes .../drawable-xxhdpi/ic_email_white_48dp.png | Bin 0 -> 1788 bytes .../ic_gesture_tap_white_24dp.png | Bin 0 -> 1319 bytes .../ic_human_greeting_white_24dp.png | Bin 0 -> 1248 bytes .../drawable-xxhdpi/ic_image_white_24dp.png | Bin 0 -> 975 bytes .../ic_information_white_48dp.png | Bin 0 -> 2625 bytes .../ic_map_marker_white_24dp.png | Bin 0 -> 1436 bytes .../drawable-xxhdpi/ic_message_white_48dp.png | Bin 0 -> 1024 bytes .../ic_newspaper_white_24dp.png | Bin 0 -> 736 bytes .../drawable-xxhdpi/ic_star_white_24dp.png | Bin 0 -> 1346 bytes .../ic_alert_octagon_white_48dp.png | Bin 0 -> 1848 bytes .../drawable-xxxhdpi/ic_alert_white_48dp.png | Bin 0 -> 2329 bytes .../drawable-xxxhdpi/ic_bell_white_24dp.png | Bin 0 -> 1269 bytes .../ic_brightness_percent_white_24dp.png | Bin 0 -> 1630 bytes .../drawable-xxxhdpi/ic_cart_white_24dp.png | Bin 0 -> 1571 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 0 -> 4124 bytes .../ic_chevron_down_white_48dp.png | Bin 0 -> 999 bytes .../ic_chevron_up_white_48dp.png | Bin 0 -> 1032 bytes .../drawable-xxxhdpi/ic_email_white_48dp.png | Bin 0 -> 2257 bytes .../ic_gesture_tap_white_24dp.png | Bin 0 -> 1624 bytes .../ic_human_greeting_white_24dp.png | Bin 0 -> 1620 bytes .../drawable-xxxhdpi/ic_image_white_24dp.png | Bin 0 -> 1210 bytes .../ic_information_white_48dp.png | Bin 0 -> 3520 bytes .../ic_map_marker_white_24dp.png | Bin 0 -> 1809 bytes .../ic_message_white_48dp.png | Bin 0 -> 1304 bytes .../ic_newspaper_white_24dp.png | Bin 0 -> 921 bytes .../drawable-xxxhdpi/ic_star_white_24dp.png | Bin 0 -> 1795 bytes .../res/drawable/ic_launcher_background.xml | 170 ++++ .../res/drawable/ic_launcher_foreground.xml | 34 + .../res/mipmap-hdpi/ic_onesignal_launcher.png | Bin 0 -> 5972 bytes .../res/mipmap-mdpi/ic_onesignal_launcher.png | Bin 0 -> 3989 bytes .../mipmap-xhdpi/ic_onesignal_launcher.png | Bin 0 -> 8639 bytes .../mipmap-xxhdpi/ic_onesignal_launcher.png | Bin 0 -> 13885 bytes .../mipmap-xxxhdpi/ic_onesignal_launcher.png | Bin 0 -> 20157 bytes .../app/src/main/res/values/colors.xml | 22 + .../app/src/main/res/values/strings.xml | 132 +++ .../app/src/main/res/values/styles.xml | 8 + Examples/OneSignalDemoV2/build.gradle.kts | 30 + .../OneSignalDemoV2/buildSrc/build.gradle.kts | 8 + .../buildSrc/src/main/kotlin/Dependencies.kt | 52 + .../buildSrc/src/main/kotlin/Versions.kt | 46 + Examples/OneSignalDemoV2/build_app_prompt.md | 945 ++++++++++++++++++ Examples/OneSignalDemoV2/gradle.properties | 21 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + Examples/OneSignalDemoV2/gradlew | 234 +++++ Examples/OneSignalDemoV2/gradlew.bat | 89 ++ Examples/OneSignalDemoV2/settings.gradle.kts | 2 + 136 files changed, 6555 insertions(+) create mode 100644 Examples/OneSignalDemoV2/app/agconnect-services.json create mode 100644 Examples/OneSignalDemoV2/app/build.gradle.kts create mode 100644 Examples/OneSignalDemoV2/app/google-services.json create mode 100644 Examples/OneSignalDemoV2/app/proguard-rules.pro create mode 100644 Examples/OneSignalDemoV2/app/src/huawei/AndroidManifest.xml create mode 100644 Examples/OneSignalDemoV2/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/AndroidManifest.xml create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/InAppMessageType.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/NotificationType.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ActionButton.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ListComponents.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LoadingOverlay.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LogView.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/SectionCard.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ToggleRow.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainActivity.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/theme/Theme.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/LogManager.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt create mode 100644 Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/TooltipHelper.kt create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_octagon_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_bell_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_brightness_percent_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_cart_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_checkbox_marked_circle_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_down_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_up_white_48dp.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_email_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_gesture_tap_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_human_greeting_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_image_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_information_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_map_marker_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_message_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_newspaper_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_octagon_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_bell_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_brightness_percent_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_cart_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_checkbox_marked_circle_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_chevron_down_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_chevron_up_white_48dp.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_email_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_gesture_tap_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_human_greeting_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_image_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_information_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_map_marker_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_message_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_newspaper_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-nodpi/onesignal_rectangle.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-nodpi/onesignal_square.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_alert_octagon_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_alert_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_bell_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_brightness_percent_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_cart_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_checkbox_marked_circle_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_down_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_up_white_48dp.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_email_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_gesture_tap_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_human_greeting_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_information_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_map_marker_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_message_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_newspaper_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_star_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_alert_octagon_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_alert_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_bell_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_brightness_percent_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_cart_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_checkbox_marked_circle_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_chevron_down_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_chevron_up_white_48dp.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_email_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_gesture_tap_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_human_greeting_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_information_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_map_marker_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_message_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_newspaper_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_alert_octagon_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_alert_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_bell_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_brightness_percent_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_cart_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_checkbox_marked_circle_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_chevron_down_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_chevron_up_white_48dp.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_email_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_gesture_tap_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_human_greeting_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_image_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_information_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_map_marker_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_message_white_48dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_newspaper_white_24dp.png create mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_star_white_24dp.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-hdpi/ic_onesignal_launcher.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-mdpi/ic_onesignal_launcher.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-xhdpi/ic_onesignal_launcher.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-xxhdpi/ic_onesignal_launcher.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-xxxhdpi/ic_onesignal_launcher.png create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/values/colors.xml create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/values/strings.xml create mode 100644 Examples/OneSignalDemoV2/app/src/main/res/values/styles.xml create mode 100644 Examples/OneSignalDemoV2/build.gradle.kts create mode 100644 Examples/OneSignalDemoV2/buildSrc/build.gradle.kts create mode 100644 Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Dependencies.kt create mode 100644 Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Versions.kt create mode 100644 Examples/OneSignalDemoV2/build_app_prompt.md create mode 100644 Examples/OneSignalDemoV2/gradle.properties create mode 100644 Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.jar create mode 100644 Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.properties create mode 100755 Examples/OneSignalDemoV2/gradlew create mode 100644 Examples/OneSignalDemoV2/gradlew.bat create mode 100644 Examples/OneSignalDemoV2/settings.gradle.kts diff --git a/Examples/OneSignalDemoV2/app/agconnect-services.json b/Examples/OneSignalDemoV2/app/agconnect-services.json new file mode 100644 index 0000000000..965d4a0f6c --- /dev/null +++ b/Examples/OneSignalDemoV2/app/agconnect-services.json @@ -0,0 +1,96 @@ +{ + "agcgw":{ + "backurl":"connect-dre.hispace.hicloud.com", + "url":"connect-dre.dbankcloud.cn", + "websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com", + "websocketurl":"connect-ws-dre.hispace.dbankcloud.cn" + }, + "agcgw_all":{ + "CN":"connect-drcn.dbankcloud.cn", + "CN_back":"connect-drcn.hispace.hicloud.com", + "DE":"connect-dre.dbankcloud.cn", + "DE_back":"connect-dre.hispace.hicloud.com", + "RU":"connect-drru.hispace.dbankcloud.ru", + "RU_back":"connect-drru.hispace.dbankcloud.cn", + "SG":"connect-dra.dbankcloud.cn", + "SG_back":"connect-dra.hispace.hicloud.com" + }, + "websocketgw_all":{ + "CN":"connect-ws-drcn.hispace.dbankcloud.cn", + "CN_back":"connect-ws-drcn.hispace.dbankcloud.com", + "DE":"connect-ws-dre.hispace.dbankcloud.cn", + "DE_back":"connect-ws-dre.hispace.dbankcloud.com", + "RU":"connect-ws-drru.hispace.dbankcloud.ru", + "RU_back":"connect-ws-drru.hispace.dbankcloud.cn", + "SG":"connect-ws-dra.hispace.dbankcloud.cn", + "SG_back":"connect-ws-dra.hispace.dbankcloud.com" + }, + "client":{ + "cp_id":"5190001000034239317", + "product_id":"388421841221340564", + "client_id":"1103097158011211392", + "client_secret":"14843C60CAFDCFD5E50025C14864697AFF55886BCF00558E8C817F141E0B4704", + "project_id":"388421841221340564", + "app_id":"107780279", + "api_key":"DAEDAN06wwm3fsiHbQaQzugegFDUc6lpsR9VZGRNoWEbjHpDphR5rSbobUr5/ohT1WlRTyIykjr4GzzEJ/jSxlziFmXF/8e56HAYiw==", + "package_name":"com.onesignal.sdktest" + }, + "oauth_client":{ + "client_id":"107780279", + "client_type":1 + }, + "app_info":{ + "app_id":"107780279", + "package_name":"com.onesignal.sdktest" + }, + "service":{ + "analytics":{ + "collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com", + "collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", + "collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", + "resource_id":"p1", + "channel_id":"" + }, + "edukit":{ + "edu_url":"edukit.edu.cloud.huawei.com.cn", + "dh_url":"edukit.edu.cloud.huawei.com.cn" + }, + "search":{ + "url":"https://search-dre.cloud.huawei.com" + }, + "cloudstorage":{ + "storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia", + "storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru", + "storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru", + "storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu", + "storage_url_de":"https://ops-dre.agcstorage.link", + "storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn", + "storage_url_sg":"https://ops-dra.agcstorage.link", + "storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn", + "storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn" + }, + "ml":{ + "mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" + } + }, + "region":"DE", + "configuration_version":"3.0", + "appInfos":[ + { + "package_name":"com.onesignal.sdktest", + "client":{ + "app_id":"107780279" + }, + "app_info":{ + "package_name":"com.onesignal.sdktest", + "app_id":"107780279" + }, + "oauth_client":{ + "client_type":1, + "client_id":"107780279" + } + } + ] +} \ No newline at end of file diff --git a/Examples/OneSignalDemoV2/app/build.gradle.kts b/Examples/OneSignalDemoV2/app/build.gradle.kts new file mode 100644 index 0000000000..3c16b29203 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/build.gradle.kts @@ -0,0 +1,136 @@ +plugins { + id(Plugins.androidApplication) + id(Plugins.kotlinAndroid) +} + +// Apply GMS or Huawei plugin based on build variant +// Check at configuration time, not when task graph is ready +val taskRequests = gradle.startParameter.taskRequests.toString().lowercase() +if (taskRequests.contains("gms")) { + apply(plugin = Plugins.googleServices) +} else if (taskRequests.contains("huawei")) { + apply(plugin = Plugins.huaweiAgconnect) +} + +// OneSignal SDK version - can be overridden via gradle property SDK_VERSION +val sdkVersion: String = rootProject.findProperty("SDK_VERSION") as? String ?: Versions.oneSignalSdk + +android { + namespace = AppConfig.applicationId + compileSdk = Versions.compileSdk + + defaultConfig { + minSdk = Versions.minSdk + targetSdk = Versions.targetSdk + versionCode = Versions.versionCode + versionName = Versions.versionName + multiDexEnabled = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.composeCompiler + } + + flavorDimensions += "default" + + productFlavors { + create("gms") { + dimension = "default" + applicationId = AppConfig.applicationId + } + create("huawei") { + dimension = "default" + minSdk = Versions.minSdk + applicationId = AppConfig.applicationId + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + debug { + isDebuggable = true + } + create("profileable") { + initWith(getByName("release")) + isDebuggable = false + isProfileable = true + isMinifyEnabled = false + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks += listOf("release") + } + } + + kotlinOptions { + jvmTarget = "1.8" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + packaging { + resources { + excludes += "androidsupportmultidexversion.txt" + } + } +} + +dependencies { + // Kotlin + implementation(Dependencies.kotlinStdlib) + implementation(Dependencies.coroutinesAndroid) + + // AndroidX + implementation(Dependencies.multidex) + implementation(Dependencies.appcompat) + implementation(Dependencies.coreKtx) + + // Compose BOM + implementation(platform(Dependencies.composeBom)) + implementation(Dependencies.composeUi) + implementation(Dependencies.composeUiGraphics) + implementation(Dependencies.composeUiToolingPreview) + implementation(Dependencies.composeMaterial3) + implementation(Dependencies.composeMaterialIcons) + implementation(Dependencies.composeRuntime) + implementation(Dependencies.composeRuntimeLivedata) + debugImplementation(Dependencies.composeUiTooling) + + // Activity & Lifecycle Compose + implementation(Dependencies.activityCompose) + implementation(Dependencies.lifecycleViewModelCompose) + implementation(Dependencies.lifecycleRuntimeCompose) + + // Lifecycle + implementation(Dependencies.lifecycleViewModelKtx) + implementation(Dependencies.lifecycleRuntimeKtx) + + // Google Play Services + implementation(Dependencies.playServicesLocation) + + // OneSignal - Google Play Builds + "gmsImplementation"("com.onesignal:OneSignal:$sdkVersion") + + // OneSignal - Huawei Builds + "huaweiImplementation"("com.onesignal:OneSignal:$sdkVersion") { + exclude(group = "com.google.android.gms", module = "play-services-gcm") + exclude(group = "com.google.android.gms", module = "play-services-analytics") + exclude(group = "com.google.android.gms", module = "play-services-location") + exclude(group = "com.google.firebase", module = "firebase-messaging") + } + "huaweiImplementation"(Dependencies.huaweiPush) + "huaweiImplementation"(Dependencies.huaweiLocation) +} diff --git a/Examples/OneSignalDemoV2/app/google-services.json b/Examples/OneSignalDemoV2/app/google-services.json new file mode 100644 index 0000000000..42c6c4e5cc --- /dev/null +++ b/Examples/OneSignalDemoV2/app/google-services.json @@ -0,0 +1,30 @@ +{ + "project_info": { + "project_number": "249481192614", + "firebase_url": "https://onesignaltest-e7802.firebaseio.com", + "project_id": "onesignaltest-e7802", + "storage_bucket": "onesignaltest-e7802.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:249481192614:android:9d39e24e24034b14", + "android_client_info": { + "package_name": "com.onesignal.sdktest" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyC2H_Z5NRhKVJsoG6dLwzSrH3aLNm7p3sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/Examples/OneSignalDemoV2/app/proguard-rules.pro b/Examples/OneSignalDemoV2/app/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/Examples/OneSignalDemoV2/app/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/Examples/OneSignalDemoV2/app/src/huawei/AndroidManifest.xml b/Examples/OneSignalDemoV2/app/src/huawei/AndroidManifest.xml new file mode 100644 index 0000000000..4cf0999869 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/huawei/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/Examples/OneSignalDemoV2/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.kt b/Examples/OneSignalDemoV2/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.kt new file mode 100644 index 0000000000..eed818c155 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.kt @@ -0,0 +1,68 @@ +package com.onesignal.sdktest.notification + +import android.os.Bundle +import android.util.Log +import com.huawei.hms.push.HmsMessageService +import com.huawei.hms.push.RemoteMessage +import com.onesignal.notifications.bridges.OneSignalHmsEventBridge + +/** + * HMS Message Service for handling Huawei Push notifications. + * This service forwards all push events to the OneSignal SDK. + * + * Note: This is the Huawei flavor-specific implementation. + */ +class HmsMessageServiceAppLevel : HmsMessageService() { + + companion object { + private const val TAG = "OneSignalHMS" + } + + /** + * When an app calls the getToken method to apply for a token from the server, + * if the server does not return the token during current method calling, + * the server can return the token through this method later. + * This method callback must be completed in 10 seconds. + * Otherwise, you need to start a new Job for callback processing. + */ + override fun onNewToken(token: String, bundle: Bundle) { + Log.d(TAG, "HmsMessageServiceAppLevel onNewToken refresh token: $token bundle: $bundle") + + // Forward event on to OneSignal SDK + OneSignalHmsEventBridge.onNewToken(this, token, bundle) + } + + @Deprecated("Deprecated in Java") + override fun onNewToken(token: String) { + Log.d(TAG, "HmsMessageServiceAppLevel onNewToken refresh token: $token") + + // Forward event on to OneSignal SDK + OneSignalHmsEventBridge.onNewToken(this, token) + } + + /** + * This method is called in the following cases: + * 1. "Data messages" - App process is alive when received. + * 2. "Notification Message" - foreground_show = false and app is in focus + * This method callback must be completed in 10 seconds. + * Start a new Job if more time is needed. + */ + override fun onMessageReceived(message: RemoteMessage) { + Log.d(TAG, "HMS onMessageReceived: $message") + Log.d(TAG, "HMS onMessageReceived.ttl: ${message.ttl}") + Log.d(TAG, "HMS onMessageReceived.data: ${message.data}") + + message.notification?.let { notification -> + Log.d(TAG, "HMS onMessageReceived.title: ${notification.title}") + Log.d(TAG, "HMS onMessageReceived.body: ${notification.body}") + Log.d(TAG, "HMS onMessageReceived.icon: ${notification.icon}") + Log.d(TAG, "HMS onMessageReceived.color: ${notification.color}") + Log.d(TAG, "HMS onMessageReceived.channelId: ${notification.channelId}") + Log.d(TAG, "HMS onMessageReceived.imageURL: ${notification.imageUrl}") + Log.d(TAG, "HMS onMessageReceived.tag: ${notification.tag}") + } + + // Forward event on to OneSignal SDK + OneSignalHmsEventBridge.onMessageReceived(this, message) + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/AndroidManifest.xml b/Examples/OneSignalDemoV2/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9d72e7e0ed --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt new file mode 100644 index 0000000000..2cfe743f3b --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt @@ -0,0 +1,145 @@ +package com.onesignal.sdktest.application + +import android.os.StrictMode +import androidx.multidex.MultiDexApplication +import com.onesignal.OneSignal +import com.onesignal.debug.LogLevel +import com.onesignal.sdktest.util.LogManager +import com.onesignal.sdktest.util.LogLevel as AppLogLevel +import com.onesignal.inAppMessages.IInAppMessageClickEvent +import com.onesignal.inAppMessages.IInAppMessageClickListener +import com.onesignal.inAppMessages.IInAppMessageDidDismissEvent +import com.onesignal.inAppMessages.IInAppMessageDidDisplayEvent +import com.onesignal.inAppMessages.IInAppMessageLifecycleListener +import com.onesignal.inAppMessages.IInAppMessageWillDismissEvent +import com.onesignal.inAppMessages.IInAppMessageWillDisplayEvent +import com.onesignal.notifications.IDisplayableNotification +import com.onesignal.notifications.INotificationClickEvent +import com.onesignal.notifications.INotificationClickListener +import com.onesignal.notifications.INotificationLifecycleListener +import com.onesignal.notifications.INotificationWillDisplayEvent +import com.onesignal.sdktest.R +import com.onesignal.sdktest.data.network.OneSignalService +import com.onesignal.sdktest.util.SharedPreferenceUtil +import com.onesignal.sdktest.util.TooltipHelper +import com.onesignal.user.state.IUserStateObserver +import com.onesignal.user.state.UserChangedState + +class MainApplication : MultiDexApplication() { + + companion object { + private const val TAG = "OneSignalExample" + private const val SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION = 2000L + } + + init { + // Run strict mode to surface any potential issues easier + StrictMode.enableDefaults() + } + + override fun onCreate() { + super.onCreate() + + OneSignal.Debug.logLevel = LogLevel.VERBOSE + + // Add SDK log listener BEFORE init to capture all SDK logs in UI + OneSignal.Debug.addLogListener { event -> + val level = when (event.level) { + LogLevel.VERBOSE, LogLevel.DEBUG -> AppLogLevel.DEBUG + LogLevel.INFO -> AppLogLevel.INFO + LogLevel.WARN -> AppLogLevel.WARN + LogLevel.ERROR, LogLevel.FATAL -> AppLogLevel.ERROR + LogLevel.NONE -> return@addLogListener + } + LogManager.log("SDK", event.entry, level) + } + + // Get or set the OneSignal App ID + var appId = SharedPreferenceUtil.getOneSignalAppId(this) + if (appId == null) { + appId = getString(R.string.onesignal_app_id) + SharedPreferenceUtil.cacheOneSignalAppId(this, appId) + } + + // Initialize OneSignal Service with app ID and REST API key + OneSignalService.setAppId(appId) + + // Initialize tooltip helper + TooltipHelper.init(this) + + // Set consent required before init (must be set before initWithContext) + OneSignal.consentRequired = SharedPreferenceUtil.getCachedConsentRequired(this) + OneSignal.consentGiven = SharedPreferenceUtil.getUserPrivacyConsent(this) + + // Initialize OneSignal on main thread (required) + OneSignal.initWithContext(this, appId) + LogManager.i(TAG, "OneSignal init completed") + + // Set up all OneSignal listeners + setupOneSignalListeners() + + // Note: Notification permission is automatically requested when MainActivity loads. + // This ensures the prompt appears after the user sees the app UI. + } + + private fun setupOneSignalListeners() { + OneSignal.InAppMessages.addLifecycleListener(object : IInAppMessageLifecycleListener { + override fun onWillDisplay(event: IInAppMessageWillDisplayEvent) { + LogManager.d(TAG, "onWillDisplayInAppMessage") + } + + override fun onDidDisplay(event: IInAppMessageDidDisplayEvent) { + LogManager.d(TAG, "onDidDisplayInAppMessage") + } + + override fun onWillDismiss(event: IInAppMessageWillDismissEvent) { + LogManager.d(TAG, "onWillDismissInAppMessage") + } + + override fun onDidDismiss(event: IInAppMessageDidDismissEvent) { + LogManager.d(TAG, "onDidDismissInAppMessage") + } + }) + + OneSignal.InAppMessages.addClickListener(object : IInAppMessageClickListener { + override fun onClick(event: IInAppMessageClickEvent) { + LogManager.d(TAG, "IInAppMessageClickListener.onClick") + } + }) + + OneSignal.Notifications.addClickListener(object : INotificationClickListener { + override fun onClick(event: INotificationClickEvent) { + LogManager.d(TAG, "INotificationClickListener.onClick fired with event: $event") + } + }) + + OneSignal.Notifications.addForegroundLifecycleListener(object : INotificationLifecycleListener { + override fun onWillDisplay(event: INotificationWillDisplayEvent) { + LogManager.d(TAG, "INotificationLifecycleListener.onWillDisplay fired with event: $event") + + val notification: IDisplayableNotification = event.notification + + // Prevent OneSignal from displaying the notification immediately on return. + // Spin up a new thread to mimic some asynchronous behavior. + event.preventDefault() + Thread { + try { + Thread.sleep(SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION) + } catch (ignored: InterruptedException) { + } + notification.display() + }.start() + } + }) + + OneSignal.User.addObserver(object : IUserStateObserver { + override fun onUserStateChange(state: UserChangedState) { + LogManager.i(TAG, "User state changed: onesignalId=${state.current.onesignalId}, externalId=${state.current.externalId}") + } + }) + + // Restore cached states + OneSignal.InAppMessages.paused = SharedPreferenceUtil.getCachedInAppMessagingPausedStatus(this) + OneSignal.Location.isShared = SharedPreferenceUtil.getCachedLocationSharedStatus(this) + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/InAppMessageType.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/InAppMessageType.kt new file mode 100644 index 0000000000..7491dc0524 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/InAppMessageType.kt @@ -0,0 +1,43 @@ +package com.onesignal.sdktest.data.model + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CropSquare +import androidx.compose.material.icons.filled.Fullscreen +import androidx.compose.material.icons.filled.VerticalAlignBottom +import androidx.compose.material.icons.filled.VerticalAlignTop +import androidx.compose.ui.graphics.vector.ImageVector + +/** + * Enum representing different types of in-app messages that can be triggered. + */ +enum class InAppMessageType( + val title: String, + val triggerKey: String, + val triggerValue: String, + val icon: ImageVector +) { + TOP_BANNER( + title = "TOP BANNER", + triggerKey = "iam_type", + triggerValue = "top_banner", + icon = Icons.Filled.VerticalAlignTop + ), + BOTTOM_BANNER( + title = "BOTTOM BANNER", + triggerKey = "iam_type", + triggerValue = "bottom_banner", + icon = Icons.Filled.VerticalAlignBottom + ), + CENTER_MODAL( + title = "CENTER MODAL", + triggerKey = "iam_type", + triggerValue = "center_modal", + icon = Icons.Filled.CropSquare + ), + FULL_SCREEN( + title = "FULL SCREEN", + triggerKey = "iam_type", + triggerValue = "full_screen", + icon = Icons.Filled.Fullscreen + ) +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/NotificationType.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/NotificationType.kt new file mode 100644 index 0000000000..b6ee86bf27 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/model/NotificationType.kt @@ -0,0 +1,24 @@ +package com.onesignal.sdktest.data.model + +/** + * Enum representing different types of push notifications that can be sent. + */ +enum class NotificationType( + val title: String, + val notificationTitle: String, + val notificationBody: String, + val bigPicture: String? = null, + val largeIcon: String? = null +) { + SIMPLE( + title = "Simple", + notificationTitle = "Simple Notification", + notificationBody = "This is a simple push notification" + ), + WITH_IMAGE( + title = "With Image", + notificationTitle = "Image Notification", + notificationBody = "This notification includes an image", + bigPicture = "https://media.onesignal.com/automated_push_templates/ratings_template.png" + ) +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt new file mode 100644 index 0000000000..8982aefc85 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt @@ -0,0 +1,288 @@ +package com.onesignal.sdktest.data.network + +import com.onesignal.OneSignal +import com.onesignal.sdktest.data.model.NotificationType +import com.onesignal.sdktest.util.LogManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URL + +/** + * OneSignal API service for testing purposes. + * Provides methods to send notifications and fetch user data via the REST API. + * + * Note: This approach is for testing purposes only. In production, notifications + * should be sent from your backend server. + */ +object OneSignalService { + + private const val TAG = "OneSignalService" + private const val ONESIGNAL_API_URL = "https://onesignal.com/api/v1/notifications" + private const val ONESIGNAL_API_BASE_URL = "https://api.onesignal.com" + + private var appId: String = "" + + fun setAppId(appId: String) { + this.appId = appId + } + + fun getAppId(): String = appId + + /** + * Send a notification to this device. + */ + suspend fun sendNotification(type: NotificationType): Boolean = withContext(Dispatchers.IO) { + val subscription = OneSignal.User.pushSubscription + + if (!subscription.optedIn) { + LogManager.w(TAG, "Cannot send notification - user not opted in") + return@withContext false + } + + val subscriptionId = subscription.id + if (subscriptionId.isNullOrEmpty()) { + LogManager.w(TAG, "Cannot send notification - no subscription ID") + return@withContext false + } + + try { + val notificationJson = JSONObject().apply { + put("app_id", appId) + put("include_subscription_ids", org.json.JSONArray().put(subscriptionId)) + put("headings", JSONObject().put("en", type.notificationTitle)) + put("contents", JSONObject().put("en", type.notificationBody)) + put("android_group", type.title) + put("android_led_color", "FF595CF2") + put("android_accent_color", "FF595CF2") + // Add large icon if available + type.largeIcon?.let { + put("large_icon", it) + LogManager.d(TAG, "Adding large_icon: $it") + } + // Add big picture if available + type.bigPicture?.let { + put("big_picture", it) + LogManager.d(TAG, "Adding big_picture: $it") + } + } + + LogManager.d(TAG, "Sending notification: ${notificationJson.toString(2)}") + LogManager.d(TAG, "Request URL: $ONESIGNAL_API_URL") + + val connection = (URL(ONESIGNAL_API_URL).openConnection() as HttpURLConnection).apply { + useCaches = false + connectTimeout = 30000 + readTimeout = 30000 + setRequestProperty("Accept", "application/vnd.onesignal.v1+json") + setRequestProperty("Content-Type", "application/json; charset=UTF-8") + requestMethod = "POST" + doOutput = true + doInput = true + } + + val outputBytes = notificationJson.toString().toByteArray(Charsets.UTF_8) + connection.setFixedLengthStreamingMode(outputBytes.size) + connection.outputStream.write(outputBytes) + + val responseCode = connection.responseCode + + if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_ACCEPTED || responseCode == HttpURLConnection.HTTP_CREATED) { + val response = connection.inputStream.bufferedReader().use { it.readText() } + LogManager.d(TAG, "Notification sent successfully: $response") + return@withContext true + } else { + val errorResponse = connection.errorStream?.bufferedReader()?.use { it.readText() } ?: "Unknown error" + LogManager.e(TAG, "Failed to send notification (HTTP $responseCode): $errorResponse") + LogManager.e(TAG, "Request body was: ${notificationJson.toString()}") + return@withContext false + } + } catch (e: Exception) { + LogManager.e(TAG, "Error sending notification", e) + return@withContext false + } + } + + /** + * Send a custom notification with title and body. + */ + suspend fun sendCustomNotification(title: String, body: String): Boolean = withContext(Dispatchers.IO) { + val subscription = OneSignal.User.pushSubscription + + if (!subscription.optedIn) { + LogManager.w(TAG, "Cannot send notification - user not opted in") + return@withContext false + } + + val subscriptionId = subscription.id + if (subscriptionId.isNullOrEmpty()) { + LogManager.w(TAG, "Cannot send notification - no subscription ID") + return@withContext false + } + + try { + val notificationJson = JSONObject().apply { + put("app_id", appId) + put("include_subscription_ids", org.json.JSONArray().put(subscriptionId)) + put("headings", JSONObject().put("en", title)) + put("contents", JSONObject().put("en", body)) + put("android_led_color", "FF595CF2") + put("android_accent_color", "FF595CF2") + } + + val connection = (URL(ONESIGNAL_API_URL).openConnection() as HttpURLConnection).apply { + useCaches = false + connectTimeout = 30000 + readTimeout = 30000 + setRequestProperty("Accept", "application/vnd.onesignal.v1+json") + setRequestProperty("Content-Type", "application/json; charset=UTF-8") + requestMethod = "POST" + doOutput = true + doInput = true + } + + val outputBytes = notificationJson.toString().toByteArray(Charsets.UTF_8) + connection.setFixedLengthStreamingMode(outputBytes.size) + connection.outputStream.write(outputBytes) + + val responseCode = connection.responseCode + + if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_ACCEPTED || responseCode == HttpURLConnection.HTTP_CREATED) { + val response = connection.inputStream.bufferedReader().use { it.readText() } + LogManager.d(TAG, "Custom notification sent successfully: $response") + return@withContext true + } else { + val errorResponse = connection.errorStream?.bufferedReader()?.use { it.readText() } ?: "Unknown error" + LogManager.e(TAG, "Failed to send custom notification (HTTP $responseCode): $errorResponse") + return@withContext false + } + } catch (e: Exception) { + LogManager.e(TAG, "Error sending custom notification", e) + return@withContext false + } + } + + /** + * Fetch user data from OneSignal API. + * Note: This endpoint does not require authentication. + * + * @param onesignalId The OneSignal user ID + * @return UserData object containing aliases, tags, emails, and SMS numbers, or null on error + */ + suspend fun fetchUser(onesignalId: String): UserData? = withContext(Dispatchers.IO) { + if (onesignalId.isEmpty()) { + LogManager.w(TAG, "Cannot fetch user - onesignalId is empty") + return@withContext null + } + + if (appId.isEmpty()) { + LogManager.w(TAG, "Cannot fetch user - appId not set") + return@withContext null + } + + try { + val url = "$ONESIGNAL_API_BASE_URL/apps/$appId/users/by/onesignal_id/$onesignalId" + LogManager.d(TAG, "Fetching user data from: $url") + + val connection = (URL(url).openConnection() as HttpURLConnection).apply { + useCaches = false + connectTimeout = 30000 + readTimeout = 30000 + setRequestProperty("Accept", "application/json") + requestMethod = "GET" + } + + val responseCode = connection.responseCode + + if (responseCode == HttpURLConnection.HTTP_OK) { + val response = connection.inputStream.bufferedReader().use { it.readText() } + LogManager.d(TAG, "User data fetched successfully, parsing response...") + try { + val userData = parseUserResponse(response) + LogManager.d(TAG, "Parsed user data: aliases=${userData.aliases.size}, tags=${userData.tags.size}, emails=${userData.emails.size}, sms=${userData.smsNumbers.size}") + return@withContext userData + } catch (e: Exception) { + LogManager.e(TAG, "Error parsing user response", e) + return@withContext null + } + } else { + val errorResponse = connection.errorStream?.bufferedReader()?.use { it.readText() } ?: "Unknown error" + LogManager.e(TAG, "Failed to fetch user (HTTP $responseCode): $errorResponse") + return@withContext null + } + } catch (e: Exception) { + LogManager.e(TAG, "Error fetching user", e) + return@withContext null + } + } + + private fun parseUserResponse(json: String): UserData { + val jsonObject = JSONObject(json) + + // Parse aliases from identity object (filter out external_id and onesignal_id) + val aliases = mutableMapOf() + if (jsonObject.has("identity")) { + val identity = jsonObject.getJSONObject("identity") + identity.keys().forEach { key -> + if (key != "external_id" && key != "onesignal_id") { + aliases[key] = identity.getString(key) + } + } + } + + // Parse external_id separately + val externalId = if (jsonObject.has("identity")) { + val identity = jsonObject.getJSONObject("identity") + if (identity.has("external_id")) identity.getString("external_id") else null + } else null + + // Parse tags from properties object + val tags = mutableMapOf() + if (jsonObject.has("properties")) { + val properties = jsonObject.getJSONObject("properties") + if (properties.has("tags")) { + val tagsObj = properties.getJSONObject("tags") + tagsObj.keys().forEach { key -> + tags[key] = tagsObj.getString(key) + } + } + } + + // Parse subscriptions for emails and SMS + val emails = mutableListOf() + val smsNumbers = mutableListOf() + if (jsonObject.has("subscriptions")) { + val subscriptions = jsonObject.getJSONArray("subscriptions") + for (i in 0 until subscriptions.length()) { + val subscription = subscriptions.getJSONObject(i) + val type = subscription.optString("type", "") + val token = subscription.optString("token", "") + + when (type) { + "Email" -> if (token.isNotEmpty()) emails.add(token) + "SMS" -> if (token.isNotEmpty()) smsNumbers.add(token) + } + } + } + + return UserData( + aliases = aliases, + tags = tags, + emails = emails, + smsNumbers = smsNumbers, + externalId = externalId + ) + } +} + +/** + * Data class representing user data fetched from the OneSignal API. + */ +data class UserData( + val aliases: Map, + val tags: Map, + val emails: List, + val smsNumbers: List, + val externalId: String? +) diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt new file mode 100644 index 0000000000..70696e54fd --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt @@ -0,0 +1,243 @@ +package com.onesignal.sdktest.data.repository + +import android.util.Log +import com.onesignal.OneSignal +import com.onesignal.sdktest.data.model.NotificationType +import com.onesignal.sdktest.data.network.OneSignalService +import com.onesignal.sdktest.data.network.UserData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Repository for all OneSignal SDK operations. + * All methods are suspend functions to be called from coroutines on background threads. + */ +class OneSignalRepository { + + companion object { + private const val TAG = "OneSignalRepository" + } + + // User operations + suspend fun loginUser(externalUserId: String) = withContext(Dispatchers.IO) { + Log.d(TAG, "Logging in user with externalUserId: $externalUserId") + OneSignal.login(externalUserId) + Log.d(TAG, "Logged in user with onesignalId: ${OneSignal.User.onesignalId}") + } + + suspend fun logoutUser() = withContext(Dispatchers.IO) { + Log.d(TAG, "Logging out user") + OneSignal.logout() + } + + // Alias operations + fun addAlias(label: String, id: String) { + Log.d(TAG, "Adding alias: $label -> $id") + OneSignal.User.addAlias(label, id) + } + + fun addAliases(aliases: Map) { + Log.d(TAG, "Adding aliases: $aliases") + OneSignal.User.addAliases(aliases) + } + + fun removeAlias(label: String) { + Log.d(TAG, "Removing alias: $label") + OneSignal.User.removeAlias(label) + } + + fun removeAliases(labels: Collection) { + Log.d(TAG, "Removing aliases: $labels") + if (labels.isNotEmpty()) { + OneSignal.User.removeAliases(labels) + } + } + + // Email operations + fun addEmail(email: String) { + Log.d(TAG, "Adding email: $email") + OneSignal.User.addEmail(email) + } + + fun removeEmail(email: String) { + Log.d(TAG, "Removing email: $email") + OneSignal.User.removeEmail(email) + } + + // SMS operations + fun addSms(smsNumber: String) { + Log.d(TAG, "Adding SMS: $smsNumber") + OneSignal.User.addSms(smsNumber) + } + + fun removeSms(smsNumber: String) { + Log.d(TAG, "Removing SMS: $smsNumber") + OneSignal.User.removeSms(smsNumber) + } + + // Tag operations + fun addTag(key: String, value: String) { + Log.d(TAG, "Adding tag: $key -> $value") + OneSignal.User.addTag(key, value) + } + + fun addTags(tags: Map) { + Log.d(TAG, "Adding tags: $tags") + OneSignal.User.addTags(tags) + } + + fun removeTag(key: String) { + Log.d(TAG, "Removing tag: $key") + OneSignal.User.removeTag(key) + } + + fun removeTags(keys: Collection) { + Log.d(TAG, "Removing tags: $keys") + if (keys.isNotEmpty()) { + OneSignal.User.removeTags(keys) + } + } + + fun getTags(): Map { + return OneSignal.User.getTags() + } + + // Trigger operations + fun addTrigger(key: String, value: String) { + Log.d(TAG, "Adding trigger: $key -> $value") + OneSignal.InAppMessages.addTrigger(key, value) + } + + fun addTriggers(triggers: Map) { + Log.d(TAG, "Adding triggers: $triggers") + OneSignal.InAppMessages.addTriggers(triggers) + } + + fun removeTrigger(key: String) { + Log.d(TAG, "Removing trigger: $key") + OneSignal.InAppMessages.removeTrigger(key) + } + + fun clearTriggers(keys: Collection) { + Log.d(TAG, "Clearing triggers: $keys") + if (keys.isNotEmpty()) { + OneSignal.InAppMessages.removeTriggers(keys) + } + } + + // Outcome operations + fun sendOutcome(name: String) { + Log.d(TAG, "Sending outcome: $name") + OneSignal.Session.addOutcome(name) + } + + fun sendUniqueOutcome(name: String) { + Log.d(TAG, "Sending unique outcome: $name") + OneSignal.Session.addUniqueOutcome(name) + } + + fun sendOutcomeWithValue(name: String, value: Float) { + Log.d(TAG, "Sending outcome with value: $name -> $value") + OneSignal.Session.addOutcomeWithValue(name, value) + } + + // Track Event + fun trackEvent(name: String, properties: Map?) { + Log.d(TAG, "Tracking event: $name with properties: $properties") + OneSignal.User.trackEvent(name, properties) + } + + // Push subscription + fun getPushSubscriptionId(): String? { + return OneSignal.User.pushSubscription.id + } + + fun isPushEnabled(): Boolean { + return OneSignal.User.pushSubscription.optedIn + } + + fun setPushEnabled(enabled: Boolean) { + Log.d(TAG, "Setting push enabled: $enabled") + if (enabled) { + OneSignal.User.pushSubscription.optIn() + } else { + OneSignal.User.pushSubscription.optOut() + } + } + + // In-App Messaging + fun isInAppMessagesPaused(): Boolean { + return OneSignal.InAppMessages.paused + } + + fun setInAppMessagesPaused(paused: Boolean) { + Log.d(TAG, "Setting in-app messages paused: $paused") + OneSignal.InAppMessages.paused = paused + } + + // Location + fun isLocationShared(): Boolean { + return OneSignal.Location.isShared + } + + fun setLocationShared(shared: Boolean) { + Log.d(TAG, "Setting location shared: $shared") + OneSignal.Location.isShared = shared + } + + suspend fun promptLocation() = withContext(Dispatchers.IO) { + Log.d(TAG, "Prompting for location permission") + OneSignal.Location.requestPermission() + } + + // Notifications + suspend fun promptPushPermission() = withContext(Dispatchers.IO) { + Log.d(TAG, "Prompting for push permission") + OneSignal.Notifications.requestPermission(true) + } + + fun hasNotificationPermission(): Boolean { + return OneSignal.Notifications.permission + } + + // Send notifications + suspend fun sendNotification(type: NotificationType): Boolean { + Log.d(TAG, "Sending notification: ${type.title}") + return OneSignalService.sendNotification(type) + } + + suspend fun sendCustomNotification(title: String, body: String): Boolean { + Log.d(TAG, "Sending custom notification: $title") + return OneSignalService.sendCustomNotification(title, body) + } + + // Privacy consent + fun setConsentRequired(required: Boolean) { + Log.d(TAG, "Setting consent required: $required") + OneSignal.consentRequired = required + } + + fun getConsentRequired(): Boolean { + return OneSignal.consentRequired + } + + fun setPrivacyConsent(granted: Boolean) { + Log.d(TAG, "Setting privacy consent: $granted") + OneSignal.consentGiven = granted + } + + fun getPrivacyConsent(): Boolean { + return OneSignal.consentGiven + } + + // OneSignal ID + fun getOneSignalId(): String? { + return OneSignal.User.onesignalId + } + + // Fetch user data from API + suspend fun fetchUser(onesignalId: String): UserData? = withContext(Dispatchers.IO) { + Log.d(TAG, "Fetching user data for: $onesignalId") + OneSignalService.fetchUser(onesignalId) + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ActionButton.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ActionButton.kt new file mode 100644 index 0000000000..b99c2bb004 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ActionButton.kt @@ -0,0 +1,177 @@ +package com.onesignal.sdktest.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.onesignal.sdktest.ui.theme.OneSignalRed + +private val ButtonShape = RoundedCornerShape(10.dp) + +/** + * Primary action button (full width, colored background). + */ +@Composable +fun PrimaryButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colorScheme.primary, + enabled: Boolean = true +) { + Button( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .height(44.dp), + colors = ButtonDefaults.buttonColors( + containerColor = backgroundColor, + disabledContainerColor = backgroundColor.copy(alpha = 0.4f) + ), + enabled = enabled, + shape = ButtonShape, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp + ) + ) { + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + letterSpacing = 0.8.sp + ) + ) + } +} + +/** + * Outline button (outlined primary style). + */ +@Composable +fun OutlineButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + val color = MaterialTheme.colorScheme.primary + OutlinedButton( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .height(44.dp), + colors = ButtonDefaults.outlinedButtonColors(contentColor = color), + enabled = enabled, + shape = ButtonShape, + border = BorderStroke(1.dp, if (enabled) color.copy(alpha = 0.5f) else color.copy(alpha = 0.2f)) + ) { + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + color = color, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + letterSpacing = 0.8.sp + ) + ) + } +} + +/** + * Destructive button (outlined red style). + */ +@Composable +fun DestructiveButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + OutlinedButton( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .height(44.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = OneSignalRed + ), + enabled = enabled, + shape = ButtonShape, + border = BorderStroke(1.dp, if (enabled) OneSignalRed.copy(alpha = 0.5f) else OneSignalRed.copy(alpha = 0.2f)) + ) { + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + color = OneSignalRed, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + letterSpacing = 0.8.sp + ) + ) + } +} + +/** + * Button with icon on the right side. + */ +@Composable +fun IconButton( + text: String, + icon: ImageVector, + onClick: () -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = OneSignalRed +) { + Button( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .height(44.dp), + colors = ButtonDefaults.buttonColors( + containerColor = backgroundColor + ), + shape = ButtonShape, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp + ) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + letterSpacing = 0.8.sp + ), + modifier = Modifier.padding(end = 8.dp) + ) + Icon( + imageVector = icon, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(18.dp) + ) + } + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt new file mode 100644 index 0000000000..8045664984 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt @@ -0,0 +1,660 @@ +package com.onesignal.sdktest.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.graphics.Color +import com.onesignal.sdktest.ui.theme.OneSignalRed +import org.json.JSONObject + +private val TextFieldShape = RoundedCornerShape(10.dp) + +@Composable +private fun dialogTextFieldColors() = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = Color(0xFFBDBDBD), + focusedBorderColor = OneSignalRed, + cursorColor = OneSignalRed, + focusedLabelColor = OneSignalRed +) + +/** + * Dialog for entering a single value. + */ +@Composable +fun SingleInputDialog( + title: String, + label: String, + onDismiss: () -> Unit, + onConfirm: (String) -> Unit, + keyboardType: KeyboardType = KeyboardType.Text +) { + var value by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + properties = DialogProperties(usePlatformDefaultWidth = false), + title = { + Text(title, style = MaterialTheme.typography.titleMedium) + }, + text = { + OutlinedTextField( + value = value, + onValueChange = { value = it }, + label = { Text(label) }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = keyboardType), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + }, + confirmButton = { + TextButton( + onClick = { onConfirm(value) }, + enabled = value.isNotBlank() + ) { + Text("Add") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + shape = RoundedCornerShape(16.dp) + ) +} + +/** + * Dialog for entering a key-value pair. + */ +@Composable +fun PairInputDialog( + title: String, + keyLabel: String = "Key", + valueLabel: String = "Value", + onDismiss: () -> Unit, + onConfirm: (String, String) -> Unit +) { + var key by remember { mutableStateOf("") } + var value by remember { mutableStateOf("") } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + shape = RoundedCornerShape(16.dp), + tonalElevation = 2.dp + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text(title, style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(20.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + OutlinedTextField( + value = key, + onValueChange = { key = it }, + label = { Text(keyLabel) }, + modifier = Modifier.weight(1f), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + OutlinedTextField( + value = value, + onValueChange = { value = it }, + label = { Text(valueLabel) }, + modifier = Modifier.weight(1f), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + } + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + TextButton( + onClick = { onConfirm(key, value) }, + enabled = key.isNotBlank() && value.isNotBlank() + ) { + Text("Add") + } + } + } + } + } +} + +/** + * Dialog for entering multiple key-value pairs. + */ +@Composable +fun MultiPairInputDialog( + title: String, + keyLabel: String = "Key", + valueLabel: String = "Value", + onDismiss: () -> Unit, + onConfirm: (List>) -> Unit +) { + var pairs by remember { mutableStateOf(listOf(Pair("", ""))) } + + val allValid = pairs.all { it.first.isNotBlank() && it.second.isNotBlank() } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + shape = RoundedCornerShape(16.dp), + tonalElevation = 2.dp + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text(title, style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(20.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 400.dp) + .verticalScroll(rememberScrollState()) + ) { + pairs.forEachIndexed { index, (key, value) -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = key, + onValueChange = { newKey -> + pairs = pairs.toMutableList().apply { + this[index] = Pair(newKey, this[index].second) + } + }, + label = { Text(keyLabel) }, + modifier = Modifier.weight(1f), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + OutlinedTextField( + value = value, + onValueChange = { newValue -> + pairs = pairs.toMutableList().apply { + this[index] = Pair(this[index].first, newValue) + } + }, + label = { Text(valueLabel) }, + modifier = Modifier.weight(1f), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + if (pairs.size > 1) { + IconButton( + onClick = { + pairs = pairs.toMutableList().apply { removeAt(index) } + } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Remove", + tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f) + ) + } + } + } + if (index < pairs.lastIndex) { + HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + TextButton( + onClick = { pairs = pairs + Pair("", "") }, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Icon(Icons.Default.Add, contentDescription = null) + Spacer(modifier = Modifier.width(4.dp)) + Text("Add Row") + } + } + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + TextButton( + onClick = { onConfirm(pairs) }, + enabled = allValid + ) { + Text("Add All") + } + } + } + } + } +} + +/** + * Dialog for selecting multiple items to remove. + */ +@Composable +fun MultiSelectRemoveDialog( + title: String, + items: List>, + onDismiss: () -> Unit, + onConfirm: (Collection) -> Unit +) { + var selectedKeys by remember { mutableStateOf(setOf()) } + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + properties = DialogProperties(usePlatformDefaultWidth = false), + title = { + Text(title, style = MaterialTheme.typography.titleMedium) + }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 400.dp) + .verticalScroll(rememberScrollState()) + ) { + items.forEach { (key, value) -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + selectedKeys = if (key in selectedKeys) { + selectedKeys - key + } else { + selectedKeys + key + } + } + .padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = key in selectedKeys, + onCheckedChange = { checked -> + selectedKeys = if (checked) { + selectedKeys + key + } else { + selectedKeys - key + } + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = key, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + }, + confirmButton = { + TextButton( + onClick = { onConfirm(selectedKeys) }, + enabled = selectedKeys.isNotEmpty() + ) { + Text("Remove (${selectedKeys.size})") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + shape = RoundedCornerShape(16.dp) + ) +} + +/** + * Dialog for login/switch user. + */ +@Composable +fun LoginDialog( + onDismiss: () -> Unit, + onConfirm: (String) -> Unit +) { + SingleInputDialog( + title = "Login User", + label = "External User Id", + onDismiss = onDismiss, + onConfirm = onConfirm + ) +} + +/** + * Dialog for outcome selection and input. + */ +@Composable +fun OutcomeDialog( + onDismiss: () -> Unit, + onSendNormal: (String) -> Unit, + onSendUnique: (String) -> Unit, + onSendWithValue: (String, Float) -> Unit +) { + var selectedType by remember { mutableStateOf(0) } + var outcomeName by remember { mutableStateOf("") } + var outcomeValue by remember { mutableStateOf("") } + + val outcomeTypes = listOf("Normal Outcome", "Unique Outcome", "Outcome with Value") + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + properties = DialogProperties(usePlatformDefaultWidth = false), + title = { + Text("Send Outcome", style = MaterialTheme.typography.titleMedium) + }, + text = { + Column { + outcomeTypes.forEachIndexed { index, type -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedType == index, + onClick = { selectedType = index } + ) + Text( + text = type, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 4.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = outcomeName, + onValueChange = { outcomeName = it }, + label = { Text("Outcome Name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + + if (selectedType == 2) { + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = outcomeValue, + onValueChange = { outcomeValue = it }, + label = { Text("Value") }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + } + } + }, + confirmButton = { + TextButton( + onClick = { + when (selectedType) { + 0 -> onSendNormal(outcomeName) + 1 -> onSendUnique(outcomeName) + 2 -> onSendWithValue(outcomeName, outcomeValue.toFloatOrNull() ?: 0f) + } + }, + enabled = outcomeName.isNotBlank() + ) { + Text("Send") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + shape = RoundedCornerShape(16.dp) + ) +} + +private fun isValidJsonObject(value: String?): Boolean { + if (value.isNullOrBlank()) return true + return try { + JSONObject(value) + true + } catch (e: Exception) { + false + } +} + +private fun parseJsonToMap(json: String): Map? { + if (json.isBlank()) return null + return try { + val jsonObject = JSONObject(json) + val map = mutableMapOf() + jsonObject.keys().forEach { key -> + map[key] = jsonObject.get(key) + } + map + } catch (e: Exception) { + null + } +} + +/** + * Dialog for track event with JSON validation. + */ +@Composable +fun TrackEventDialog( + onDismiss: () -> Unit, + onConfirm: (String, Map?) -> Unit +) { + var eventName by remember { mutableStateOf("") } + var eventValue by remember { mutableStateOf("") } + var showError by remember { mutableStateOf(false) } + + val isValueValid = isValidJsonObject(eventValue) + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + properties = DialogProperties(usePlatformDefaultWidth = false), + title = { + Text("Track Event", style = MaterialTheme.typography.titleMedium) + }, + text = { + Column { + OutlinedTextField( + value = eventName, + onValueChange = { eventName = it }, + label = { Text("Event Name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + isError = eventName.isBlank() && showError, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + + Spacer(modifier = Modifier.height(10.dp)) + + OutlinedTextField( + value = eventValue, + onValueChange = { + eventValue = it + showError = false + }, + label = { Text("Properties (JSON, optional)") }, + placeholder = { Text("{\"key\": \"value\"}") }, + modifier = Modifier.fillMaxWidth(), + singleLine = false, + minLines = 2, + maxLines = 4, + isError = !isValueValid && eventValue.isNotBlank(), + shape = TextFieldShape, + colors = dialogTextFieldColors(), + supportingText = if (!isValueValid && eventValue.isNotBlank()) { + { + Text( + text = "Invalid JSON format", + color = MaterialTheme.colorScheme.error + ) + } + } else null + ) + } + }, + confirmButton = { + TextButton( + onClick = { + showError = true + if (eventName.isNotBlank() && isValueValid) { + val properties = parseJsonToMap(eventValue) + onConfirm(eventName, properties) + } + }, + enabled = eventName.isNotBlank() && isValueValid + ) { + Text("Track") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + shape = RoundedCornerShape(16.dp) + ) +} + +/** + * Dialog for custom notification. + */ +@Composable +fun CustomNotificationDialog( + onDismiss: () -> Unit, + onConfirm: (String, String) -> Unit +) { + var title by remember { mutableStateOf("") } + var body by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + properties = DialogProperties(usePlatformDefaultWidth = false), + title = { + Text("Custom Notification", style = MaterialTheme.typography.titleMedium) + }, + text = { + Column { + OutlinedTextField( + value = title, + onValueChange = { title = it }, + label = { Text("Title") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = body, + onValueChange = { body = it }, + label = { Text("Body") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = TextFieldShape, + colors = dialogTextFieldColors() + ) + } + }, + confirmButton = { + TextButton( + onClick = { onConfirm(title, body) }, + enabled = title.isNotBlank() && body.isNotBlank() + ) { + Text("Send") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + shape = RoundedCornerShape(16.dp) + ) +} + +/** + * Tooltip info dialog. + */ +@Composable +fun TooltipDialog( + title: String, + description: String, + options: List>? = null, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + properties = DialogProperties(usePlatformDefaultWidth = false), + title = { + Text(title, style = MaterialTheme.typography.titleMedium) + }, + text = { + Column { + Text( + description, + style = MaterialTheme.typography.bodyMedium + ) + options?.let { opts -> + Spacer(modifier = Modifier.height(12.dp)) + opts.forEach { (name, desc) -> + Text( + text = "β€’ $name: $desc", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(vertical = 3.dp) + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("OK") + } + }, + shape = RoundedCornerShape(16.dp) + ) +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ListComponents.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ListComponents.kt new file mode 100644 index 0000000000..0a2d5ebccd --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ListComponents.kt @@ -0,0 +1,220 @@ +package com.onesignal.sdktest.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.onesignal.sdktest.ui.theme.DividerColor + +/** + * A row displaying a key-value pair with delete button. + */ +@Composable +fun PairItem( + key: String, + value: String, + onDelete: (() -> Unit)? = null, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = key, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (onDelete != null) { + IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Delete", + tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f), + modifier = Modifier.size(18.dp) + ) + } + } + } +} + +/** + * A row displaying a single value with delete button. + */ +@Composable +fun SingleItem( + value: String, + onDelete: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Delete", + tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f), + modifier = Modifier.size(18.dp) + ) + } + } +} + +/** + * Empty state text. + */ +@Composable +fun EmptyState( + text: String, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 20.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } +} + +/** + * Collapsible list section for emails/SMS (shows "X more available" when collapsed). + */ +@Composable +fun CollapsibleSingleList( + items: List, + emptyText: String, + onDelete: (String) -> Unit, + maxCollapsedItems: Int = 5, + modifier: Modifier = Modifier +) { + var expanded by remember { mutableStateOf(false) } + val shouldCollapse = items.size > maxCollapsedItems + val displayItems = if (shouldCollapse && !expanded) { + items.take(maxCollapsedItems) + } else { + items + } + + Column(modifier = modifier.fillMaxWidth()) { + if (items.isEmpty()) { + EmptyState(text = emptyText) + } else { + displayItems.forEachIndexed { index, item -> + SingleItem(value = item, onDelete = { onDelete(item) }) + if (index < displayItems.lastIndex) { + HorizontalDivider( + color = DividerColor, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + if (shouldCollapse) { + HorizontalDivider( + color = DividerColor, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = if (expanded) "Show less" else "${items.size - maxCollapsedItems} more", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary + ) + Icon( + imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + } + } + } + } +} + +/** + * List of key-value pairs with optional action buttons. + */ +@Composable +fun PairList( + items: List>, + emptyText: String, + onDelete: ((String) -> Unit)? = null, + modifier: Modifier = Modifier +) { + Column(modifier = modifier.fillMaxWidth()) { + if (items.isEmpty()) { + EmptyState(text = emptyText) + } else { + items.forEachIndexed { index, (key, value) -> + PairItem(key = key, value = value, onDelete = onDelete?.let { { it(key) } }) + if (index < items.lastIndex) { + HorizontalDivider( + color = DividerColor, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + } + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LoadingOverlay.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LoadingOverlay.kt new file mode 100644 index 0000000000..defe0ddbef --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LoadingOverlay.kt @@ -0,0 +1,56 @@ +package com.onesignal.sdktest.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.onesignal.sdktest.ui.theme.OneSignalRed + +/** + * Full screen loading overlay with a centered spinner. + */ +@Composable +fun LoadingOverlay( + isLoading: Boolean, + modifier: Modifier = Modifier +) { + if (isLoading) { + Box( + modifier = modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.25f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { /* Consume clicks */ } + ), + contentAlignment = Alignment.Center + ) { + Surface( + modifier = Modifier.size(56.dp), + shape = CircleShape, + color = Color.White, + shadowElevation = 4.dp + ) { + Box(contentAlignment = Alignment.Center) { + CircularProgressIndicator( + color = OneSignalRed, + strokeWidth = 3.dp, + modifier = Modifier.size(28.dp) + ) + } + } + } + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LogView.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LogView.kt new file mode 100644 index 0000000000..22b149b07f --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/LogView.kt @@ -0,0 +1,232 @@ +package com.onesignal.sdktest.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.onesignal.sdktest.util.LogEntry +import com.onesignal.sdktest.util.LogLevel +import com.onesignal.sdktest.util.LogManager + +private val LogBackground = Color(0xFF1A1B1E) +private val LogHeaderBackground = Color(0xFF1A1B1E) +private val LogDebugColor = Color(0xFF82AAFF) +private val LogInfoColor = Color(0xFFC3E88D) +private val LogWarnColor = Color(0xFFFFCB6B) +private val LogErrorColor = Color(0xFFFF5370) +private val LogTimestampColor = Color(0xFF676E7B) + +/** + * Collapsible log view that displays app logs at the top of the screen. + */ +@Composable +fun LogView( + modifier: Modifier = Modifier, + defaultExpanded: Boolean = true +) { + var isExpanded by remember { mutableStateOf(defaultExpanded) } + val logs = LogManager.logs + val listState = rememberLazyListState() + + // Auto-scroll to top when new logs arrive + LaunchedEffect(logs.size) { + if (logs.isNotEmpty()) { + listState.animateScrollToItem(0) + } + } + + Column( + modifier = modifier + .fillMaxWidth() + .testTag("log_view_container") + ) { + // Header with toggle + Row( + modifier = Modifier + .fillMaxWidth() + .background(LogHeaderBackground) + .clickable { isExpanded = !isExpanded } + .padding(horizontal = 14.dp, vertical = 10.dp) + .testTag("log_view_header"), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "LOGS", + color = Color.White.copy(alpha = 0.9f), + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + letterSpacing = 1.sp, + modifier = Modifier.testTag("log_view_title") + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = "(${logs.size})", + color = LogTimestampColor, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + modifier = Modifier + .testTag("log_view_count") + .semantics { contentDescription = "Log count: ${logs.size}" } + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Clear button + if (logs.isNotEmpty()) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Clear logs", + tint = LogTimestampColor, + modifier = Modifier + .clickable { LogManager.clear() } + .padding(4.dp) + .size(14.dp) + .testTag("log_view_clear_button") + ) + + Spacer(modifier = Modifier.width(10.dp)) + } + + Icon( + imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = if (isExpanded) "Collapse" else "Expand", + tint = Color.White.copy(alpha = 0.6f), + modifier = Modifier.size(18.dp) + ) + } + + // Log content + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(), + exit = shrinkVertically() + ) { + if (logs.isEmpty()) { + Text( + text = "No logs yet", + color = LogTimestampColor, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + modifier = Modifier + .fillMaxWidth() + .background(LogBackground) + .padding(14.dp) + .testTag("log_view_empty") + ) + } else { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .background(LogBackground) + .testTag("log_view_list") + ) { + items(logs.size) { index -> + LogEntryRow(entry = logs[index], index = index) + } + } + } + } + } +} + +@Composable +private fun LogEntryRow(entry: LogEntry, index: Int) { + val levelText = when (entry.level) { + LogLevel.DEBUG -> "D" + LogLevel.INFO -> "I" + LogLevel.WARN -> "W" + LogLevel.ERROR -> "E" + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 4.dp) + .testTag("log_entry_$index") + .semantics { + contentDescription = "Log $index: $levelText ${entry.message}" + }, + verticalAlignment = Alignment.Top + ) { + // Timestamp + Text( + text = entry.timestamp, + color = LogTimestampColor, + fontSize = 10.sp, + fontFamily = FontFamily.Monospace, + modifier = Modifier.testTag("log_entry_${index}_timestamp") + ) + + Spacer(modifier = Modifier.width(8.dp)) + + // Level indicator + Text( + text = levelText, + color = when (entry.level) { + LogLevel.DEBUG -> LogDebugColor + LogLevel.INFO -> LogInfoColor + LogLevel.WARN -> LogWarnColor + LogLevel.ERROR -> LogErrorColor + }, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + modifier = Modifier.testTag("log_entry_${index}_level") + ) + + Spacer(modifier = Modifier.width(8.dp)) + + // Message + Text( + text = entry.message, + color = Color.White.copy(alpha = 0.85f), + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .testTag("log_entry_${index}_message") + ) + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/SectionCard.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/SectionCard.kt new file mode 100644 index 0000000000..ab0a76f4b9 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/SectionCard.kt @@ -0,0 +1,88 @@ +package com.onesignal.sdktest.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * Reusable section card with title and optional info tooltip. + */ +@Composable +fun SectionCard( + title: String, + modifier: Modifier = Modifier, + showCard: Boolean = true, + onInfoClick: (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit +) { + Column(modifier = modifier.fillMaxWidth()) { + // Section header + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title.uppercase(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f) + ) + if (onInfoClick != null) { + IconButton( + onClick = onInfoClick, + modifier = Modifier.size(28.dp) + ) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Info", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp) + ) + } + } + } + + if (showCard) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + shape = MaterialTheme.shapes.medium, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + content = content + ) + } + } else { + Column( + modifier = Modifier.fillMaxWidth(), + content = content + ) + } + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ToggleRow.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ToggleRow.kt new file mode 100644 index 0000000000..d0ba154cdf --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/components/ToggleRow.kt @@ -0,0 +1,62 @@ +package com.onesignal.sdktest.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.onesignal.sdktest.ui.theme.OneSignalRed + +/** + * Reusable toggle row with label and optional description. + */ +@Composable +fun ToggleRow( + label: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + description: String? = null, + enabled: Boolean = true +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = if (enabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + } + ) + if (description != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = if (enabled) 1f else 0.4f + ) + ) + } + } + Spacer(modifier = Modifier.width(12.dp)) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.surface, + checkedTrackColor = OneSignalRed, + checkedBorderColor = OneSignalRed + ) + ) + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainActivity.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainActivity.kt new file mode 100644 index 0000000000..46aab10a58 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainActivity.kt @@ -0,0 +1,44 @@ +package com.onesignal.sdktest.ui.main + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import com.onesignal.sdktest.ui.theme.LightBackground +import com.onesignal.sdktest.ui.theme.OneSignalTheme + +class MainActivity : ComponentActivity() { + + private val viewModel: MainViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + OneSignalTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = LightBackground + ) { + MainScreen(viewModel = viewModel) + } + + // Observe toast messages + val toastMessage by viewModel.toastMessage.observeAsState() + LaunchedEffect(toastMessage) { + toastMessage?.let { + Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show() + viewModel.clearToast() + } + } + } + } + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt new file mode 100644 index 0000000000..05c2c04bf5 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt @@ -0,0 +1,461 @@ +package com.onesignal.sdktest.ui.main + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.onesignal.sdktest.R +import com.onesignal.sdktest.data.model.NotificationType +import com.onesignal.sdktest.ui.components.CustomNotificationDialog +import com.onesignal.sdktest.ui.components.LoadingOverlay +import com.onesignal.sdktest.ui.components.LoginDialog +import com.onesignal.sdktest.ui.components.LogView +import com.onesignal.sdktest.ui.components.MultiPairInputDialog +import com.onesignal.sdktest.ui.components.MultiSelectRemoveDialog +import com.onesignal.sdktest.ui.components.OutcomeDialog +import com.onesignal.sdktest.ui.components.PairInputDialog +import com.onesignal.sdktest.ui.components.PrimaryButton +import com.onesignal.sdktest.ui.components.SingleInputDialog +import com.onesignal.sdktest.ui.components.TooltipDialog +import com.onesignal.sdktest.ui.components.TrackEventDialog +import com.onesignal.sdktest.ui.secondary.SecondaryActivity +import com.onesignal.sdktest.ui.theme.OneSignalRed + +import com.onesignal.sdktest.util.TooltipHelper + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen(viewModel: MainViewModel) { + val context = LocalContext.current + + // Observe all LiveData as State + val appId by viewModel.appId.observeAsState("") + val pushSubscriptionId by viewModel.pushSubscriptionId.observeAsState() + val pushEnabled by viewModel.pushEnabled.observeAsState(false) + val hasNotificationPermission by viewModel.hasNotificationPermission.observeAsState(false) + val consentRequired by viewModel.consentRequired.observeAsState(false) + val privacyConsentGiven by viewModel.privacyConsentGiven.observeAsState(false) + val externalUserId by viewModel.externalUserId.observeAsState() + val aliases by viewModel.aliases.observeAsState(emptyList()) + val emails by viewModel.emails.observeAsState(emptyList()) + val smsNumbers by viewModel.smsNumbers.observeAsState(emptyList()) + val tags by viewModel.tags.observeAsState(emptyList()) + val triggers by viewModel.triggers.observeAsState(emptyList()) + val inAppMessagesPaused by viewModel.inAppMessagesPaused.observeAsState(true) + val locationShared by viewModel.locationShared.observeAsState(false) + val isLoading by viewModel.isLoading.observeAsState(false) + + // Dialog states + var showLoginDialog by remember { mutableStateOf(false) } + var showAddAliasDialog by remember { mutableStateOf(false) } + var showAddMultipleAliasDialog by remember { mutableStateOf(false) } + var showAddEmailDialog by remember { mutableStateOf(false) } + var showAddSmsDialog by remember { mutableStateOf(false) } + var showAddTagDialog by remember { mutableStateOf(false) } + var showAddMultipleTagDialog by remember { mutableStateOf(false) } + var showRemoveTagsDialog by remember { mutableStateOf(false) } + var showAddTriggerDialog by remember { mutableStateOf(false) } + var showAddMultipleTriggerDialog by remember { mutableStateOf(false) } + var showRemoveTriggersDialog by remember { mutableStateOf(false) } + var showOutcomeDialog by remember { mutableStateOf(false) } + var showTrackEventDialog by remember { mutableStateOf(false) } + var showCustomNotificationDialog by remember { mutableStateOf(false) } + var showTooltipDialog by remember { mutableStateOf(null) } + + // Auto prompt for notification permission + LaunchedEffect(Unit) { + viewModel.promptPush() + } + + Box(modifier = Modifier.fillMaxSize()) { + Scaffold( + topBar = { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.onesignal_rectangle), + contentDescription = "OneSignal Logo", + modifier = Modifier.height(24.dp), + colorFilter = ColorFilter.tint(Color.White) + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + "Sample App", + color = Color.White.copy(alpha = 0.7f), + fontSize = 14.sp, + fontWeight = FontWeight.Normal + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = OneSignalRed + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(MaterialTheme.colorScheme.background) + ) { + // Log view at top (fixed, not scrolling) + LogView(defaultExpanded = true) + + // Scrollable content + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(4.dp)) + + // === APP SECTION === + AppSection( + appId = appId, + consentRequired = consentRequired, + onConsentRequiredChange = { viewModel.setConsentRequired(it) }, + privacyConsentGiven = privacyConsentGiven, + onConsentChange = { viewModel.setPrivacyConsent(it) }, + onGetKeysClick = { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://onesignal.com"))) + } + ) + + // === USER SECTION === + UserSection( + externalUserId = externalUserId, + onLoginClick = { showLoginDialog = true }, + onLogoutClick = { viewModel.logoutUser() } + ) + + // === PUSH SECTION === + PushSection( + pushSubscriptionId = pushSubscriptionId, + pushEnabled = pushEnabled, + hasPermission = hasNotificationPermission, + onEnabledChange = { viewModel.setPushEnabled(it) }, + onPromptPush = { viewModel.promptPush() }, + onInfoClick = { showTooltipDialog = "push" } + ) + + // === SEND PUSH NOTIFICATION SECTION === + SendPushSection( + onSimpleClick = { viewModel.sendNotification(NotificationType.SIMPLE) }, + onImageClick = { viewModel.sendNotification(NotificationType.WITH_IMAGE) }, + onCustomClick = { showCustomNotificationDialog = true }, + onInfoClick = { showTooltipDialog = "sendPushNotification" } + ) + + // === IN-APP MESSAGING SECTION === + InAppMessagingSection( + isPaused = inAppMessagesPaused, + onPausedChange = { viewModel.setInAppMessagesPaused(it) }, + onInfoClick = { showTooltipDialog = "inAppMessaging" } + ) + + // === SEND IN-APP MESSAGE SECTION === + SendInAppMessageSection( + onSendMessage = { type -> + viewModel.sendInAppMessage(type.title, type.triggerKey, type.triggerValue) + }, + onInfoClick = { showTooltipDialog = "sendInAppMessage" } + ) + + // === ALIASES SECTION === + AliasesSection( + aliases = aliases, + onAddClick = { showAddAliasDialog = true }, + onAddMultipleClick = { showAddMultipleAliasDialog = true }, + onInfoClick = { showTooltipDialog = "aliases" } + ) + + // === EMAILS SECTION === + EmailsSection( + emails = emails, + onAddClick = { showAddEmailDialog = true }, + onRemove = { viewModel.removeEmail(it) }, + onInfoClick = { showTooltipDialog = "emails" } + ) + + // === SMS SECTION === + SmsSection( + smsNumbers = smsNumbers, + onAddClick = { showAddSmsDialog = true }, + onRemove = { viewModel.removeSms(it) }, + onInfoClick = { showTooltipDialog = "sms" } + ) + + // === TAGS SECTION === + TagsSection( + tags = tags, + onAddClick = { showAddTagDialog = true }, + onAddMultipleClick = { showAddMultipleTagDialog = true }, + onRemove = { viewModel.removeTag(it) }, + onRemoveSelected = { showRemoveTagsDialog = true }, + onInfoClick = { showTooltipDialog = "tags" } + ) + + // === OUTCOME EVENTS SECTION === + OutcomeSection( + onSendOutcome = { showOutcomeDialog = true }, + onInfoClick = { showTooltipDialog = "outcomes" } + ) + + // === TRIGGERS SECTION === + TriggersSection( + triggers = triggers, + onAddClick = { showAddTriggerDialog = true }, + onAddMultipleClick = { showAddMultipleTriggerDialog = true }, + onRemove = { viewModel.removeTrigger(it) }, + onRemoveSelected = { showRemoveTriggersDialog = true }, + onClearAll = { viewModel.clearTriggers() }, + onInfoClick = { showTooltipDialog = "triggers" } + ) + + // === TRACK EVENT SECTION === + TrackEventSection( + onTrackClick = { showTrackEventDialog = true }, + onInfoClick = { showTooltipDialog = "trackEvent" } + ) + + // === LOCATION SECTION === + LocationSection( + locationShared = locationShared, + onLocationSharedChange = { viewModel.setLocationShared(it) }, + onPromptLocation = { viewModel.promptLocation() }, + onInfoClick = { showTooltipDialog = "location" } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // === NEXT ACTIVITY BUTTON === + PrimaryButton( + text = "NEXT ACTIVITY", + onClick = { + context.startActivity(Intent(context, SecondaryActivity::class.java)) + } + ) + + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + + // Loading overlay + LoadingOverlay(isLoading = isLoading) + } + + // === DIALOGS === + if (showLoginDialog) { + LoginDialog( + onDismiss = { showLoginDialog = false }, + onConfirm = { userId -> + viewModel.loginUser(userId) + showLoginDialog = false + } + ) + } + + if (showAddAliasDialog) { + PairInputDialog( + title = "Add Alias", + keyLabel = "Label", + valueLabel = "ID", + onDismiss = { showAddAliasDialog = false }, + onConfirm = { key, value -> + viewModel.addAlias(key, value) + showAddAliasDialog = false + } + ) + } + + if (showAddMultipleAliasDialog) { + MultiPairInputDialog( + title = "Add Multiple Aliases", + keyLabel = "Label", + valueLabel = "ID", + onDismiss = { showAddMultipleAliasDialog = false }, + onConfirm = { pairs -> + viewModel.addAliases(pairs) + showAddMultipleAliasDialog = false + } + ) + } + + if (showAddEmailDialog) { + SingleInputDialog( + title = "Add Email", + label = "Email", + onDismiss = { showAddEmailDialog = false }, + onConfirm = { email -> + viewModel.addEmail(email) + showAddEmailDialog = false + } + ) + } + + if (showAddSmsDialog) { + SingleInputDialog( + title = "Add SMS", + label = "Phone Number", + onDismiss = { showAddSmsDialog = false }, + onConfirm = { sms -> + viewModel.addSms(sms) + showAddSmsDialog = false + } + ) + } + + if (showAddTagDialog) { + PairInputDialog( + title = "Add Tag", + onDismiss = { showAddTagDialog = false }, + onConfirm = { key, value -> + viewModel.addTag(key, value) + showAddTagDialog = false + } + ) + } + + if (showAddMultipleTagDialog) { + MultiPairInputDialog( + title = "Add Multiple Tags", + onDismiss = { showAddMultipleTagDialog = false }, + onConfirm = { pairs -> + viewModel.addTags(pairs) + showAddMultipleTagDialog = false + } + ) + } + + if (showRemoveTagsDialog && tags.isNotEmpty()) { + MultiSelectRemoveDialog( + title = "Remove Tags", + items = tags, + onDismiss = { showRemoveTagsDialog = false }, + onConfirm = { keys -> + viewModel.removeSelectedTags(keys) + showRemoveTagsDialog = false + } + ) + } + + if (showAddTriggerDialog) { + PairInputDialog( + title = "Add Trigger", + onDismiss = { showAddTriggerDialog = false }, + onConfirm = { key, value -> + viewModel.addTrigger(key, value) + showAddTriggerDialog = false + } + ) + } + + if (showAddMultipleTriggerDialog) { + MultiPairInputDialog( + title = "Add Multiple Triggers", + onDismiss = { showAddMultipleTriggerDialog = false }, + onConfirm = { pairs -> + viewModel.addTriggers(pairs) + showAddMultipleTriggerDialog = false + } + ) + } + + if (showRemoveTriggersDialog && triggers.isNotEmpty()) { + MultiSelectRemoveDialog( + title = "Remove Triggers", + items = triggers, + onDismiss = { showRemoveTriggersDialog = false }, + onConfirm = { keys -> + viewModel.removeSelectedTriggers(keys) + showRemoveTriggersDialog = false + } + ) + } + + if (showOutcomeDialog) { + OutcomeDialog( + onDismiss = { showOutcomeDialog = false }, + onSendNormal = { name -> + viewModel.sendOutcome(name) + showOutcomeDialog = false + }, + onSendUnique = { name -> + viewModel.sendUniqueOutcome(name) + showOutcomeDialog = false + }, + onSendWithValue = { name, value -> + viewModel.sendOutcomeWithValue(name, value) + showOutcomeDialog = false + } + ) + } + + if (showTrackEventDialog) { + TrackEventDialog( + onDismiss = { showTrackEventDialog = false }, + onConfirm = { name, properties -> + viewModel.trackEvent(name, properties) + showTrackEventDialog = false + } + ) + } + + if (showCustomNotificationDialog) { + CustomNotificationDialog( + onDismiss = { showCustomNotificationDialog = false }, + onConfirm = { title, body -> + viewModel.sendCustomNotification(title, body) + showCustomNotificationDialog = false + } + ) + } + + showTooltipDialog?.let { key -> + val tooltip = TooltipHelper.getTooltip(key) + if (tooltip != null) { + TooltipDialog( + title = tooltip.title, + description = tooltip.description, + options = tooltip.options?.map { it.name to it.description }, + onDismiss = { showTooltipDialog = null } + ) + } + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt new file mode 100644 index 0000000000..e65af736d2 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt @@ -0,0 +1,626 @@ +package com.onesignal.sdktest.ui.main + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.onesignal.OneSignal +import com.onesignal.notifications.IPermissionObserver +import com.onesignal.sdktest.data.model.NotificationType +import com.onesignal.sdktest.data.repository.OneSignalRepository +import com.onesignal.sdktest.util.LogManager +import com.onesignal.sdktest.util.SharedPreferenceUtil +import com.onesignal.user.state.IUserStateObserver +import com.onesignal.user.state.UserChangedState +import com.onesignal.user.subscriptions.IPushSubscriptionObserver +import com.onesignal.user.subscriptions.PushSubscriptionChangedState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MainViewModel(application: Application) : AndroidViewModel(application), IPushSubscriptionObserver, IPermissionObserver, IUserStateObserver { + + private val repository = OneSignalRepository() + + // App ID + private val _appId = MutableLiveData() + val appId: LiveData = _appId + + // Push Subscription + private val _pushSubscriptionId = MutableLiveData() + val pushSubscriptionId: LiveData = _pushSubscriptionId + + private val _pushEnabled = MutableLiveData() + val pushEnabled: LiveData = _pushEnabled + + // Notification Permission + private val _hasNotificationPermission = MutableLiveData() + val hasNotificationPermission: LiveData = _hasNotificationPermission + + // Consent Required + private val _consentRequired = MutableLiveData() + val consentRequired: LiveData = _consentRequired + + // Privacy Consent + private val _privacyConsentGiven = MutableLiveData() + val privacyConsentGiven: LiveData = _privacyConsentGiven + + // Aliases + private val _aliases = MutableLiveData>>() + val aliases: LiveData>> = _aliases + + // Emails + private val _emails = MutableLiveData>() + val emails: LiveData> = _emails + + // SMS numbers + private val _smsNumbers = MutableLiveData>() + val smsNumbers: LiveData> = _smsNumbers + + // Tags + private val _tags = MutableLiveData>>() + val tags: LiveData>> = _tags + + // Triggers + private val _triggers = MutableLiveData>>() + val triggers: LiveData>> = _triggers + + // In-App Messages Paused + private val _inAppMessagesPaused = MutableLiveData() + val inAppMessagesPaused: LiveData = _inAppMessagesPaused + + // Location Shared + private val _locationShared = MutableLiveData() + val locationShared: LiveData = _locationShared + + // Toast messages + private val _toastMessage = MutableLiveData() + val toastMessage: LiveData = _toastMessage + + // Loading state + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + // External User ID (for login state display) + private val _externalUserId = MutableLiveData() + val externalUserId: LiveData = _externalUserId + + // Local lists to track added items + private val aliasesList = mutableListOf>() + private val emailsList = mutableListOf() + private val smsNumbersList = mutableListOf() + private val tagsList = mutableListOf>() + private val triggersList = mutableListOf>() + + init { + LogManager.info("App initialized") + loadInitialState() + OneSignal.User.pushSubscription.addObserver(this) + OneSignal.Notifications.addPermissionObserver(this) + OneSignal.User.addObserver(this) + android.util.Log.d("MainViewModel", "init: observers registered, current onesignalId=${OneSignal.User.onesignalId}") + LogManager.debug("OneSignal ID: ${OneSignal.User.onesignalId ?: "not set"}") + } + + // IPermissionObserver + override fun onNotificationPermissionChange(permission: Boolean) { + _hasNotificationPermission.postValue(permission) + } + + // IUserStateObserver - called when user changes (login/logout) + override fun onUserStateChange(state: UserChangedState) { + android.util.Log.d("MainViewModel", "onUserStateChange fired: ${state.current.onesignalId}") + viewModelScope.launch(Dispatchers.Main) { + loadExistingAliases() + loadExistingTags() + refreshPushSubscription() + fetchUserDataFromApi() + } + } + + private fun loadInitialState() { + val context = getApplication() + + _appId.value = SharedPreferenceUtil.getOneSignalAppId(context) ?: "" + _consentRequired.value = repository.getConsentRequired() + _privacyConsentGiven.value = repository.getPrivacyConsent() + _inAppMessagesPaused.value = repository.isInAppMessagesPaused() + _locationShared.value = repository.isLocationShared() + + val externalId = OneSignal.User.externalId + _externalUserId.value = if (externalId.isEmpty()) null else externalId + + refreshPushSubscription() + loadExistingAliases() + loadExistingTags() + refreshEmails() + refreshSmsNumbers() + refreshTriggers() + + val onesignalId = OneSignal.User.onesignalId + if (!onesignalId.isNullOrEmpty()) { + fetchUserDataFromApi() + } + } + + fun fetchUserDataFromApi() { + val onesignalId = OneSignal.User.onesignalId + if (onesignalId.isNullOrEmpty()) { + _isLoading.value = false + return + } + + _isLoading.value = true + viewModelScope.launch(Dispatchers.IO) { + try { + val userData = repository.fetchUser(onesignalId) + withContext(Dispatchers.Main) { + if (userData != null) { + aliasesList.clear() + aliasesList.addAll(userData.aliases.map { Pair(it.key, it.value) }) + refreshAliases() + + tagsList.clear() + tagsList.addAll(userData.tags.map { Pair(it.key, it.value) }) + refreshTags() + + emailsList.clear() + emailsList.addAll(userData.emails) + refreshEmails() + + smsNumbersList.clear() + smsNumbersList.addAll(userData.smsNumbers) + refreshSmsNumbers() + + if (!userData.externalId.isNullOrEmpty()) { + _externalUserId.value = userData.externalId + SharedPreferenceUtil.cacheUserExternalUserId(getApplication(), userData.externalId) + } + + kotlinx.coroutines.delay(100) + } + _isLoading.value = false + } + } catch (e: Exception) { + android.util.Log.e("MainViewModel", "Error fetching user data", e) + withContext(Dispatchers.Main) { + logError("Failed to fetch user data: ${e.message}") + _isLoading.value = false + } + } + } + } + + private fun loadExistingAliases() { + aliasesList.clear() + refreshAliases() + } + + private fun loadExistingTags() { + val existingTags = repository.getTags() + tagsList.clear() + tagsList.addAll(existingTags.map { Pair(it.key, it.value) }) + refreshTags() + } + + fun refreshPushSubscription() { + _pushSubscriptionId.value = repository.getPushSubscriptionId() + _pushEnabled.value = repository.isPushEnabled() + _hasNotificationPermission.value = repository.hasNotificationPermission() + } + + private fun refreshAliases() { _aliases.value = aliasesList.toList() } + private fun refreshEmails() { _emails.value = emailsList.toList() } + private fun refreshSmsNumbers() { _smsNumbers.value = smsNumbersList.toList() } + private fun refreshTags() { _tags.value = tagsList.toList() } + private fun refreshTriggers() { _triggers.value = triggersList.toList() } + + // User operations + fun loginUser(externalUserId: String) { + _isLoading.value = true + viewModelScope.launch(Dispatchers.IO) { + repository.loginUser(externalUserId) + withContext(Dispatchers.Main) { + SharedPreferenceUtil.cacheUserExternalUserId(getApplication(), externalUserId) + _externalUserId.value = externalUserId + showToast("Logged in as: $externalUserId") + aliasesList.clear() + emailsList.clear() + smsNumbersList.clear() + triggersList.clear() + refreshAliases() + refreshEmails() + refreshSmsNumbers() + refreshTriggers() + loadExistingTags() + refreshPushSubscription() + // Loading stays on; onUserStateChange will call fetchUserDataFromApi() to dismiss it + } + } + } + + fun logoutUser() { + _isLoading.value = true + viewModelScope.launch(Dispatchers.IO) { + repository.logoutUser() + withContext(Dispatchers.Main) { + SharedPreferenceUtil.cacheUserExternalUserId(getApplication(), "") + _externalUserId.value = null + showToast("Logged out") + loadExistingAliases() + loadExistingTags() + refreshPushSubscription() + _isLoading.value = false + emailsList.clear() + smsNumbersList.clear() + triggersList.clear() + refreshEmails() + refreshSmsNumbers() + refreshTriggers() + } + } + } + + // Consent required + fun setConsentRequired(required: Boolean) { + repository.setConsentRequired(required) + SharedPreferenceUtil.cacheConsentRequired(getApplication(), required) + _consentRequired.value = required + showToast(if (required) "Consent required enabled" else "Consent required disabled") + } + + // Privacy consent + fun setPrivacyConsent(granted: Boolean) { + repository.setPrivacyConsent(granted) + SharedPreferenceUtil.cacheUserPrivacyConsent(getApplication(), granted) + _privacyConsentGiven.value = granted + showToast(if (granted) "Consent granted" else "Consent revoked") + } + + // Alias operations (single and batch) + fun addAlias(label: String, id: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.addAlias(label, id) + withContext(Dispatchers.Main) { + aliasesList.removeAll { it.first == label } + aliasesList.add(Pair(label, id)) + refreshAliases() + showToast("Alias added: $label") + } + } + } + + fun addAliases(pairs: List>) { + viewModelScope.launch(Dispatchers.IO) { + val map = pairs.associate { it.first to it.second } + repository.addAliases(map) + withContext(Dispatchers.Main) { + for ((label, id) in pairs) { + aliasesList.removeAll { it.first == label } + aliasesList.add(Pair(label, id)) + } + refreshAliases() + showToast("${pairs.size} alias(es) added") + } + } + } + + fun removeAlias(label: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.removeAlias(label) + withContext(Dispatchers.Main) { + aliasesList.removeAll { it.first == label } + refreshAliases() + showToast("Alias removed: $label") + } + } + } + + fun removeSelectedAliases(labels: Collection) { + viewModelScope.launch(Dispatchers.IO) { + repository.removeAliases(labels) + withContext(Dispatchers.Main) { + aliasesList.removeAll { it.first in labels } + refreshAliases() + showToast("${labels.size} alias(es) removed") + } + } + } + + // Email operations + fun addEmail(email: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.addEmail(email) + withContext(Dispatchers.Main) { + if (!emailsList.contains(email)) { + emailsList.add(email) + refreshEmails() + } + showToast("Email added: $email") + } + } + } + + fun removeEmail(email: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.removeEmail(email) + withContext(Dispatchers.Main) { + emailsList.remove(email) + refreshEmails() + showToast("Email removed: $email") + } + } + } + + // SMS operations + fun addSms(smsNumber: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.addSms(smsNumber) + withContext(Dispatchers.Main) { + if (!smsNumbersList.contains(smsNumber)) { + smsNumbersList.add(smsNumber) + refreshSmsNumbers() + } + showToast("SMS added: $smsNumber") + } + } + } + + fun removeSms(smsNumber: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.removeSms(smsNumber) + withContext(Dispatchers.Main) { + smsNumbersList.remove(smsNumber) + refreshSmsNumbers() + showToast("SMS removed: $smsNumber") + } + } + } + + // Tag operations (single and batch) + fun addTag(key: String, value: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.addTag(key, value) + withContext(Dispatchers.Main) { + loadExistingTags() + showToast("Tag added: $key") + } + } + } + + fun addTags(pairs: List>) { + viewModelScope.launch(Dispatchers.IO) { + val map = pairs.associate { it.first to it.second } + repository.addTags(map) + withContext(Dispatchers.Main) { + loadExistingTags() + showToast("${pairs.size} tag(s) added") + } + } + } + + fun removeTag(key: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.removeTag(key) + withContext(Dispatchers.Main) { + loadExistingTags() + showToast("Tag removed: $key") + } + } + } + + fun removeSelectedTags(keys: Collection) { + viewModelScope.launch(Dispatchers.IO) { + repository.removeTags(keys) + withContext(Dispatchers.Main) { + loadExistingTags() + showToast("${keys.size} tag(s) removed") + } + } + } + + // Trigger operations (single and batch) + fun addTrigger(key: String, value: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.addTrigger(key, value) + withContext(Dispatchers.Main) { + triggersList.removeAll { it.first == key } + triggersList.add(Pair(key, value)) + refreshTriggers() + showToast("Trigger added: $key") + } + } + } + + fun addTriggers(pairs: List>) { + viewModelScope.launch(Dispatchers.IO) { + val map = pairs.associate { it.first to it.second } + repository.addTriggers(map) + withContext(Dispatchers.Main) { + for ((key, value) in pairs) { + triggersList.removeAll { it.first == key } + triggersList.add(Pair(key, value)) + } + refreshTriggers() + showToast("${pairs.size} trigger(s) added") + } + } + } + + fun removeTrigger(key: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.removeTrigger(key) + withContext(Dispatchers.Main) { + triggersList.removeAll { it.first == key } + refreshTriggers() + showToast("Trigger removed: $key") + } + } + } + + fun removeSelectedTriggers(keys: Collection) { + viewModelScope.launch(Dispatchers.IO) { + repository.clearTriggers(keys) + withContext(Dispatchers.Main) { + triggersList.removeAll { it.first in keys } + refreshTriggers() + showToast("${keys.size} trigger(s) removed") + } + } + } + + fun clearTriggers() { + viewModelScope.launch(Dispatchers.IO) { + val keys = triggersList.map { it.first } + repository.clearTriggers(keys) + withContext(Dispatchers.Main) { + triggersList.clear() + refreshTriggers() + showToast("All triggers cleared") + } + } + } + + // Outcome operations + fun sendOutcome(name: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.sendOutcome(name) + withContext(Dispatchers.Main) { showToast("Outcome sent: $name") } + } + } + + fun sendUniqueOutcome(name: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.sendUniqueOutcome(name) + withContext(Dispatchers.Main) { showToast("Unique outcome sent: $name") } + } + } + + fun sendOutcomeWithValue(name: String, value: Float) { + viewModelScope.launch(Dispatchers.IO) { + repository.sendOutcomeWithValue(name, value) + withContext(Dispatchers.Main) { showToast("Outcome with value sent: $name = $value") } + } + } + + // Track Event + fun trackEvent(name: String, properties: Map?) { + viewModelScope.launch(Dispatchers.IO) { + repository.trackEvent(name, properties) + withContext(Dispatchers.Main) { + val message = if (!properties.isNullOrEmpty()) { + "Event tracked: $name with properties" + } else { + "Event tracked: $name" + } + showToast(message) + } + } + } + + // Push subscription + fun setPushEnabled(enabled: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + repository.setPushEnabled(enabled) + withContext(Dispatchers.Main) { + _pushEnabled.value = enabled + showToast(if (enabled) "Push enabled" else "Push disabled") + } + } + } + + fun promptPush() { + viewModelScope.launch(Dispatchers.Main) { + OneSignal.Notifications.requestPermission(true) + refreshPushSubscription() + } + } + + // In-App Messages + fun setInAppMessagesPaused(paused: Boolean) { + repository.setInAppMessagesPaused(paused) + SharedPreferenceUtil.cacheInAppMessagingPausedStatus(getApplication(), paused) + _inAppMessagesPaused.value = paused + showToast(if (paused) "In-app messages paused" else "In-app messages resumed") + } + + // Location + fun setLocationShared(shared: Boolean) { + repository.setLocationShared(shared) + SharedPreferenceUtil.cacheLocationSharedStatus(getApplication(), shared) + _locationShared.value = shared + showToast(if (shared) "Location sharing enabled" else "Location sharing disabled") + } + + fun promptLocation() { + viewModelScope.launch(Dispatchers.IO) { + repository.promptLocation() + withContext(Dispatchers.Main) { showToast("Location permission requested") } + } + } + + // Send notification + fun sendNotification(type: NotificationType) { + logDebug("Sending notification: ${type.title}") + viewModelScope.launch(Dispatchers.IO) { + val success = repository.sendNotification(type) + withContext(Dispatchers.Main) { + if (success) { + showToast("Notification sent: ${type.title}") + } else { + logError("Failed to send notification: ${type.title}") + showToast("Failed to send notification") + } + } + } + } + + fun sendCustomNotification(title: String, body: String) { + logDebug("Sending custom notification: $title") + viewModelScope.launch(Dispatchers.IO) { + val success = repository.sendCustomNotification(title, body) + withContext(Dispatchers.Main) { + if (success) { + showToast("Notification sent: $title") + } else { + logError("Failed to send notification: $title") + showToast("Failed to send notification") + } + } + } + } + + fun sendInAppMessage(title: String, triggerKey: String, triggerValue: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.addTrigger(triggerKey, triggerValue) + withContext(Dispatchers.Main) { + triggersList.removeAll { it.first == triggerKey } + triggersList.add(Pair(triggerKey, triggerValue)) + refreshTriggers() + showToast("Sent In-App Message: $title") + } + } + } + + private fun showToast(message: String) { + _toastMessage.value = message + LogManager.info(message) + } + + fun clearToast() { _toastMessage.value = null } + + // Logging utilities + private fun logError(message: String) = LogManager.error(message) + private fun logDebug(message: String) = LogManager.debug(message) + + override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) { + _pushSubscriptionId.postValue(state.current.id) + _pushEnabled.postValue(state.current.optedIn) + } + + override fun onCleared() { + super.onCleared() + OneSignal.User.pushSubscription.removeObserver(this) + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt new file mode 100644 index 0000000000..f672d322c1 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt @@ -0,0 +1,495 @@ +package com.onesignal.sdktest.ui.main + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.onesignal.sdktest.data.model.InAppMessageType +import com.onesignal.sdktest.ui.components.CollapsibleSingleList +import com.onesignal.sdktest.ui.components.DestructiveButton +import com.onesignal.sdktest.ui.components.OutlineButton +import com.onesignal.sdktest.ui.components.PairList +import com.onesignal.sdktest.ui.components.PrimaryButton +import com.onesignal.sdktest.ui.components.SectionCard +import com.onesignal.sdktest.ui.components.ToggleRow +import com.onesignal.sdktest.ui.theme.DividerColor +import com.onesignal.sdktest.ui.theme.OneSignalGreen +import com.onesignal.sdktest.ui.theme.OneSignalGreenLight +import com.onesignal.sdktest.ui.theme.OneSignalRed +import com.onesignal.sdktest.ui.theme.WarningBackground + +// === APP SECTION === +@Composable +fun AppSection( + appId: String, + consentRequired: Boolean, + onConsentRequiredChange: (Boolean) -> Unit, + privacyConsentGiven: Boolean, + onConsentChange: (Boolean) -> Unit, + onGetKeysClick: () -> Unit +) { + SectionCard(title = "App") { + // App ID + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "App ID", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = appId, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Guidance Banner + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = CardDefaults.cardColors(containerColor = WarningBackground), + shape = MaterialTheme.shapes.medium, + border = BorderStroke(1.dp, Color(0xFFFFE082).copy(alpha = 0.5f)), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Text( + "Add your own App ID, then rebuild to fully test all functionality.", + style = MaterialTheme.typography.bodySmall + ) + Text( + "Get your keys at onesignal.com", + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.SemiBold), + color = OneSignalRed, + modifier = Modifier + .padding(top = 4.dp) + .clickable { onGetKeysClick() } + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Privacy Consent Card + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + shape = MaterialTheme.shapes.medium, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + ToggleRow( + label = "Consent Required", + description = "Require consent before SDK processes data", + checked = consentRequired, + onCheckedChange = onConsentRequiredChange + ) + if (consentRequired) { + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + ToggleRow( + label = "Privacy Consent", + description = "Consent given for data collection", + checked = privacyConsentGiven, + onCheckedChange = onConsentChange + ) + } + } +} + +// === USER SECTION === +@Composable +fun UserSection( + externalUserId: String?, + onLoginClick: () -> Unit, + onLogoutClick: () -> Unit +) { + val isLoggedIn = !externalUserId.isNullOrEmpty() + + SectionCard(title = "User") { + // Status + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Status", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = if (isLoggedIn) "Logged In" else "Anonymous", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + color = if (isLoggedIn) OneSignalGreen else MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + // External ID + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "External ID", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = externalUserId ?: "β€”", + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + PrimaryButton( + text = if (isLoggedIn) "SWITCH USER" else "LOGIN USER", + onClick = onLoginClick + ) + + if (isLoggedIn) { + OutlineButton( + text = "LOGOUT USER", + onClick = onLogoutClick + ) + } +} + +// === PUSH SECTION === +@Composable +fun PushSection( + pushSubscriptionId: String?, + pushEnabled: Boolean, + hasPermission: Boolean, + onEnabledChange: (Boolean) -> Unit, + onPromptPush: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Push", onInfoClick = onInfoClick) { + // Push ID + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Push ID", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = pushSubscriptionId ?: "Not Available", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + color = if (pushSubscriptionId != null) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + } + ) + ) + } + + HorizontalDivider( + color = DividerColor, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + // Enabled Toggle + ToggleRow( + label = "Enabled", + checked = pushEnabled, + onCheckedChange = onEnabledChange, + enabled = hasPermission + ) + } + + if (!hasPermission) { + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton( + text = "PROMPT PUSH", + onClick = onPromptPush + ) + } +} + +// === SEND PUSH NOTIFICATION SECTION === +@Composable +fun SendPushSection( + onSimpleClick: () -> Unit, + onImageClick: () -> Unit, + onCustomClick: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Send Push Notification", showCard = false, onInfoClick = onInfoClick) { + PrimaryButton(text = "SIMPLE", onClick = onSimpleClick) + PrimaryButton(text = "WITH IMAGE", onClick = onImageClick) + PrimaryButton(text = "CUSTOM", onClick = onCustomClick) + } +} + +// === IN-APP MESSAGING SECTION === +@Composable +fun InAppMessagingSection( + isPaused: Boolean, + onPausedChange: (Boolean) -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "In-App Messaging", onInfoClick = onInfoClick) { + ToggleRow( + label = "Pause In-App Messages", + description = "Toggle in-app message display", + checked = isPaused, + onCheckedChange = onPausedChange + ) + } +} + +// === SEND IN-APP MESSAGE SECTION === +@Composable +fun SendInAppMessageSection( + onSendMessage: (InAppMessageType) -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Send In-App Message", showCard = false, onInfoClick = onInfoClick) { + InAppMessageType.values().forEach { type -> + Button( + onClick = { onSendMessage(type) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .height(44.dp), + colors = ButtonDefaults.buttonColors(containerColor = OneSignalRed), + shape = RoundedCornerShape(10.dp), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp + ) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = type.icon, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + type.title, + style = MaterialTheme.typography.labelLarge.copy( + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + letterSpacing = 0.8.sp + ) + ) + } + } + } + } +} + +// === ALIASES SECTION === +@Composable +fun AliasesSection( + aliases: List>, + onAddClick: () -> Unit, + onAddMultipleClick: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Aliases", onInfoClick = onInfoClick) { + PairList( + items = aliases, + emptyText = "No aliases added" + ) + } + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton(text = "ADD", onClick = onAddClick) + PrimaryButton(text = "ADD MULTIPLE", onClick = onAddMultipleClick) +} + +// === EMAILS SECTION === +@Composable +fun EmailsSection( + emails: List, + onAddClick: () -> Unit, + onRemove: (String) -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Emails", onInfoClick = onInfoClick) { + CollapsibleSingleList( + items = emails, + emptyText = "No emails added", + onDelete = onRemove + ) + } + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton(text = "ADD EMAIL", onClick = onAddClick) +} + +// === SMS SECTION === +@Composable +fun SmsSection( + smsNumbers: List, + onAddClick: () -> Unit, + onRemove: (String) -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "SMS", onInfoClick = onInfoClick) { + CollapsibleSingleList( + items = smsNumbers, + emptyText = "No SMS added", + onDelete = onRemove + ) + } + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton(text = "ADD SMS", onClick = onAddClick) +} + +// === TAGS SECTION === +@Composable +fun TagsSection( + tags: List>, + onAddClick: () -> Unit, + onAddMultipleClick: () -> Unit, + onRemove: (String) -> Unit, + onRemoveSelected: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Tags", onInfoClick = onInfoClick) { + PairList( + items = tags, + emptyText = "No tags added", + onDelete = onRemove + ) + } + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton(text = "ADD", onClick = onAddClick) + PrimaryButton(text = "ADD MULTIPLE", onClick = onAddMultipleClick) + + if (tags.isNotEmpty()) { + DestructiveButton(text = "REMOVE SELECTED", onClick = onRemoveSelected) + } +} + +// === OUTCOME SECTION === +@Composable +fun OutcomeSection( + onSendOutcome: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Outcome Events", showCard = false, onInfoClick = onInfoClick) { + PrimaryButton(text = "SEND OUTCOME", onClick = onSendOutcome) + } +} + +// === TRIGGERS SECTION === +@Composable +fun TriggersSection( + triggers: List>, + onAddClick: () -> Unit, + onAddMultipleClick: () -> Unit, + onRemove: (String) -> Unit, + onRemoveSelected: () -> Unit, + onClearAll: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Triggers", onInfoClick = onInfoClick) { + PairList( + items = triggers, + emptyText = "No triggers added", + onDelete = onRemove + ) + } + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton(text = "ADD", onClick = onAddClick) + PrimaryButton(text = "ADD MULTIPLE", onClick = onAddMultipleClick) + + if (triggers.isNotEmpty()) { + DestructiveButton(text = "REMOVE SELECTED", onClick = onRemoveSelected) + DestructiveButton(text = "CLEAR ALL", onClick = onClearAll) + } +} + +// === TRACK EVENT SECTION === +@Composable +fun TrackEventSection( + onTrackClick: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Track Event", showCard = false, onInfoClick = onInfoClick) { + PrimaryButton(text = "TRACK EVENT", onClick = onTrackClick) + } +} + +// === LOCATION SECTION === +@Composable +fun LocationSection( + locationShared: Boolean, + onLocationSharedChange: (Boolean) -> Unit, + onPromptLocation: () -> Unit, + onInfoClick: () -> Unit +) { + SectionCard(title = "Location", onInfoClick = onInfoClick) { + ToggleRow( + label = "Location Shared", + description = "Share device location with OneSignal", + checked = locationShared, + onCheckedChange = onLocationSharedChange + ) + } + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton(text = "PROMPT LOCATION", onClick = onPromptLocation) +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt new file mode 100644 index 0000000000..130d503eff --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt @@ -0,0 +1,69 @@ +package com.onesignal.sdktest.ui.secondary + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.onesignal.sdktest.ui.theme.LightBackground +import com.onesignal.sdktest.ui.theme.OneSignalRed +import com.onesignal.sdktest.ui.theme.OneSignalTheme + +class SecondaryActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + OneSignalTheme { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Secondary Activity", color = Color.White) }, + navigationIcon = { + IconButton(onClick = { finish() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = OneSignalRed + ) + ) + }, + containerColor = LightBackground + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + Text( + text = "Secondary Activity", + style = MaterialTheme.typography.headlineMedium + ) + } + } + } + } + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/theme/Theme.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/theme/Theme.kt new file mode 100644 index 0000000000..dca176608c --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/ui/theme/Theme.kt @@ -0,0 +1,98 @@ +package com.onesignal.sdktest.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +// OneSignal brand colors +val OneSignalRed = Color(0xFFE54B4D) +val OneSignalRedDark = Color(0xFFCE3E40) +val OneSignalGreen = Color(0xFF34A853) +val OneSignalGreenLight = Color(0xFFE6F4EA) +val DarkText = Color(0xFF1F1F1F) +val SecondaryText = Color(0xFF5F6368) +val LightBackground = Color(0xFFF8F9FA) +val CardBackground = Color.White +val DividerColor = Color(0xFFE8EAED) +val WarningBackground = Color(0xFFFFF8E1) +val SurfaceBorder = Color(0xFFDADCE0) + +private val LightColorScheme = lightColorScheme( + primary = OneSignalRed, + onPrimary = Color.White, + secondary = OneSignalGreen, + onSecondary = Color.White, + background = LightBackground, + surface = CardBackground, + onBackground = DarkText, + onSurface = DarkText, + surfaceVariant = Color(0xFFF1F3F4), + onSurfaceVariant = SecondaryText, + outline = SurfaceBorder, + error = OneSignalRed, + onError = Color.White +) + +private val AppTypography = Typography( + headlineSmall = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = (-0.2).sp, + color = DarkText + ), + titleMedium = TextStyle( + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.sp, + color = DarkText + ), + labelLarge = TextStyle( + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.4.sp, + color = SecondaryText + ), + bodyLarge = TextStyle( + fontSize = 15.sp, + fontWeight = FontWeight.Normal, + letterSpacing = 0.sp, + color = DarkText + ), + bodyMedium = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + letterSpacing = 0.sp, + color = DarkText + ), + bodySmall = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + letterSpacing = 0.sp, + color = SecondaryText + ) +) + +private val AppShapes = Shapes( + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(16.dp), + extraLarge = RoundedCornerShape(24.dp) +) + +@Composable +fun OneSignalTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = LightColorScheme, + typography = AppTypography, + shapes = AppShapes, + content = content + ) +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/LogManager.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/LogManager.kt new file mode 100644 index 0000000000..ce6a6f9d6e --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/LogManager.kt @@ -0,0 +1,111 @@ +package com.onesignal.sdktest.util + +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Pass-through log manager that both displays logs in the UI and forwards to Android's logcat. + * Use this instead of android.util.Log to get both logcat output and UI display. + */ +object LogManager { + + private const val TAG = "OneSignalDemo" + private const val MAX_LOGS = 100 + + private val _logs = mutableStateListOf() + val logs: List get() = _logs + + private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + private val mainHandler = Handler(Looper.getMainLooper()) + + /** + * Log with custom tag (used by SDK log listener) + */ + fun log(tag: String, message: String, level: LogLevel) { + // Forward to Android logcat (can happen on any thread) + when (level) { + LogLevel.DEBUG -> Log.d(tag, message) + LogLevel.INFO -> Log.i(tag, message) + LogLevel.WARN -> Log.w(tag, message) + LogLevel.ERROR -> Log.e(tag, message) + } + + // Create entry with current timestamp + val entry = LogEntry( + timestamp = timeFormat.format(Date()), + message = message, + level = level + ) + + // Add to UI log list on main thread (required for Compose state) + if (Looper.myLooper() == Looper.getMainLooper()) { + addLogEntry(entry) + } else { + mainHandler.post { addLogEntry(entry) } + } + } + + private fun addLogEntry(entry: LogEntry) { + _logs.add(0, entry) // Add to beginning (newest first) + + // Keep only the last MAX_LOGS entries + while (_logs.size > MAX_LOGS) { + _logs.removeAt(_logs.lastIndex) + } + } + + // Convenience methods with default tag + fun d(message: String) = log(TAG, message, LogLevel.DEBUG) + fun i(message: String) = log(TAG, message, LogLevel.INFO) + fun w(message: String) = log(TAG, message, LogLevel.WARN) + fun e(message: String) = log(TAG, message, LogLevel.ERROR) + + // Methods with custom tag (mimics android.util.Log API) + fun d(tag: String, message: String) = log(tag, message, LogLevel.DEBUG) + fun i(tag: String, message: String) = log(tag, message, LogLevel.INFO) + fun w(tag: String, message: String) = log(tag, message, LogLevel.WARN) + fun e(tag: String, message: String) = log(tag, message, LogLevel.ERROR) + + // Methods with throwable (mimics android.util.Log API) + fun e(tag: String, message: String, throwable: Throwable) { + Log.e(tag, message, throwable) + log(tag, "$message: ${throwable.message}", LogLevel.ERROR) + } + + fun w(tag: String, message: String, throwable: Throwable) { + Log.w(tag, message, throwable) + log(tag, "$message: ${throwable.message}", LogLevel.WARN) + } + + // Legacy methods for compatibility + fun info(message: String) = i(message) + fun debug(message: String) = d(message) + fun warn(message: String) = w(message) + fun error(message: String) = e(message) + + fun clear() { + if (Looper.myLooper() == Looper.getMainLooper()) { + _logs.clear() + } else { + mainHandler.post { _logs.clear() } + } + } +} + +data class LogEntry( + val timestamp: String, + val message: String, + val level: LogLevel +) + +enum class LogLevel { + DEBUG, + INFO, + WARN, + ERROR +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt new file mode 100644 index 0000000000..f3b93dfb00 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt @@ -0,0 +1,72 @@ +package com.onesignal.sdktest.util + +import android.content.Context +import android.content.SharedPreferences + +object SharedPreferenceUtil { + + private const val APP_SHARED_PREFS = "com.onesignal.sdktest" + private const val OS_APP_ID_SHARED_PREF = "OS_APP_ID_SHARED_PREF" + private const val PRIVACY_CONSENT_SHARED_PREF = "PRIVACY_CONSENT_SHARED_PREF" + private const val USER_EXTERNAL_USER_ID_SHARED_PREF = "USER_EXTERNAL_USER_ID_SHARED_PREF" + private const val LOCATION_SHARED_PREF = "LOCATION_SHARED_PREF" + private const val IN_APP_MESSAGING_PAUSED_PREF = "IN_APP_MESSAGING_PAUSED_PREF" + private const val CONSENT_REQUIRED_PREF = "CONSENT_REQUIRED_PREF" + + private fun getSharedPreference(context: Context): SharedPreferences { + return context.getSharedPreferences(APP_SHARED_PREFS, Context.MODE_PRIVATE) + } + + fun exists(context: Context, key: String): Boolean { + return getSharedPreference(context).contains(key) + } + + fun getOneSignalAppId(context: Context): String? { + val defaultAppId = "77e32082-ea27-42e3-a898-c72e141824ef" + return getSharedPreference(context).getString(OS_APP_ID_SHARED_PREF, defaultAppId) + } + + fun getUserPrivacyConsent(context: Context): Boolean { + return getSharedPreference(context).getBoolean(PRIVACY_CONSENT_SHARED_PREF, false) + } + + fun getCachedUserExternalUserId(context: Context): String { + return getSharedPreference(context).getString(USER_EXTERNAL_USER_ID_SHARED_PREF, "") ?: "" + } + + fun getCachedLocationSharedStatus(context: Context): Boolean { + return getSharedPreference(context).getBoolean(LOCATION_SHARED_PREF, false) + } + + fun getCachedInAppMessagingPausedStatus(context: Context): Boolean { + return getSharedPreference(context).getBoolean(IN_APP_MESSAGING_PAUSED_PREF, true) + } + + fun cacheOneSignalAppId(context: Context, appId: String) { + getSharedPreference(context).edit().putString(OS_APP_ID_SHARED_PREF, appId).apply() + } + + fun cacheUserPrivacyConsent(context: Context, privacyConsent: Boolean) { + getSharedPreference(context).edit().putBoolean(PRIVACY_CONSENT_SHARED_PREF, privacyConsent).apply() + } + + fun cacheUserExternalUserId(context: Context, userId: String) { + getSharedPreference(context).edit().putString(USER_EXTERNAL_USER_ID_SHARED_PREF, userId).apply() + } + + fun cacheLocationSharedStatus(context: Context, shared: Boolean) { + getSharedPreference(context).edit().putBoolean(LOCATION_SHARED_PREF, shared).apply() + } + + fun cacheInAppMessagingPausedStatus(context: Context, paused: Boolean) { + getSharedPreference(context).edit().putBoolean(IN_APP_MESSAGING_PAUSED_PREF, paused).apply() + } + + fun getCachedConsentRequired(context: Context): Boolean { + return getSharedPreference(context).getBoolean(CONSENT_REQUIRED_PREF, false) + } + + fun cacheConsentRequired(context: Context, required: Boolean) { + getSharedPreference(context).edit().putBoolean(CONSENT_REQUIRED_PREF, required).apply() + } +} diff --git a/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/TooltipHelper.kt b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/TooltipHelper.kt new file mode 100644 index 0000000000..0a8c65a3a4 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/java/com/onesignal/sdktest/util/TooltipHelper.kt @@ -0,0 +1,103 @@ +package com.onesignal.sdktest.util + +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URL + +/** + * Helper object for loading tooltip content from the sdk-shared repository. + * Tooltip content is fetched at runtime from a remote URL, ensuring all SDK demo apps + * share the same tooltip definitions. + */ +object TooltipHelper { + + private var tooltips: Map = emptyMap() + private var initialized = false + + private const val TOOLTIP_URL = + "https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/tooltip_content.json" + + /** + * Initialize the tooltip helper by fetching content from remote URL on a background thread. + * Call this once during app startup (e.g., in Application.onCreate()). + * On failure (no network, etc.), tooltips remain empty β€” they are non-critical. + */ + @Suppress("unused") + fun init(context: Context) { + if (initialized) return + + CoroutineScope(Dispatchers.IO).launch { + try { + val connection = URL(TOOLTIP_URL).openConnection() as HttpURLConnection + connection.connectTimeout = 10000 + connection.readTimeout = 10000 + connection.requestMethod = "GET" + + if (connection.responseCode == HttpURLConnection.HTTP_OK) { + val json = connection.inputStream.bufferedReader().use { it.readText() } + val jsonObject = JSONObject(json) + + val tooltipMap = mutableMapOf() + + jsonObject.keys().forEach { key -> + val tooltipJson = jsonObject.getJSONObject(key) + val title = tooltipJson.getString("title") + val description = tooltipJson.getString("description") + + val options = if (tooltipJson.has("options")) { + val optionsArray = tooltipJson.getJSONArray("options") + (0 until optionsArray.length()).map { i -> + val optionJson = optionsArray.getJSONObject(i) + TooltipOption( + name = optionJson.getString("name"), + description = optionJson.getString("description") + ) + } + } else { + null + } + + tooltipMap[key] = TooltipData(title, description, options) + } + + withContext(Dispatchers.Main) { + tooltips = tooltipMap + initialized = true + } + } + } catch (e: Exception) { + // Tooltips are non-critical; log and continue + android.util.Log.w("TooltipHelper", "Failed to fetch tooltip content: ${e.message}") + } + } + } + + /** + * Get tooltip data for a specific key. + */ + fun getTooltip(key: String): TooltipData? { + return tooltips[key] + } +} + +/** + * Data class representing tooltip content. + */ +data class TooltipData( + val title: String, + val description: String, + val options: List? = null +) + +/** + * Data class representing a tooltip option (for sections with multiple buttons). + */ +data class TooltipOption( + val name: String, + val description: String +) diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_octagon_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_octagon_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..d4673106b213b703d28616a951c2fb312cea34e4 GIT binary patch literal 763 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3MyH2$4jE?g!S;H9~A zO306>i=PzivsY=isGA`3c#At% z!(~2AfpZ5vGUa6$_!*Xk)=xcL%~~)w=$D?E(@N#^(-nDrOVTW+HkP?rojU4x=5kiB zFvIzs9+3_VaUaYJ>Si-kocft?&9&-wrh$v*(%#P*rth+Lx?kAu>)^&@Bs5`SLk~-e zih?quBPTnU^aLcZ~nLX?B@diD$WN+OTX+idnVby z+Hg6^TK>trFL$l{?0@nzd|#_Ro%@RDoWsAT<>s>p4$~9&CKJd(G);=UEeFCht+G zlom>7)x5FROLmMeVf3_;a0$35x_WR!j$E^K)sew=Yq{hT(B!U8FUqlZ^^ zh={v`g4^rpJNC7=Pl literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..91e6b537ebcdbda130c1865a64640bc3f6f94324 GIT binary patch literal 1178 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3apG7>2+*sQczhjTg>!#k?c*ijENqr6F8!_2y7|)$^XLs!J)Yx-2ol~C*^g!n$}#s z;D7AW@vE-6?`Brl?YaMZ-sT);YdI7^HQ_ob#>aq6?Lok1yuE)Xt?)Y z;9Q6B((fHy>4AS$ogMbC()Z7Pm-%mYJbO_MV=R;1%tv=VuF!s!*7mJu$NF{C?rEnx zh#R*VKMH!ex2Scq7*KbCt@JP7$hqu!41vcVRzKJ}&U#NJ3svwYzHkmdFAxJ>oaoC`0_9L%$_ZTl=3 z{JbH*;o5v>Bl(Sj*BhHPm$JuxJ6(9>-fJO0h2M?kyYDRtZocCBz)aRRF`${DjyWvY zOFB3I!dA!Ev4=UAZJw8VSD}=32j`v$t5(iG+;6-!@8*4erpNoM>0dju0e{WSZ)evs z1zm42_s+l9V4kxwU0UAov1yjb+Ale?*<7=)Xhgld>Bd`BC1u)mtZG|kvy(~IDg}iF zK=wC*SBwv|Cj7lF+9)q_{G))yl%QQ<&mXDrS_upKF+OY9T>3RA%qtRkh8p*$ZfX*2|l@(pOv;pW_hz`eq5+SJ%aEhJok9%sgy) zVibRfE7(4a@Ot@ahxjVFzPYBadJ5-q*+!Qh`L!x=@eTjhd3P@@h>u3s4 zg9gZk;<9wBq{QM>-O{2=hP2F_R4aXb{gT`Q{oKU#%;ap{>~QlSH)qdmy`ue$Ks^ke Lu6{1-oD!M|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;vyZT^vIq4!^x_=y%vaP_f!d)b)@%gg~EL?&8qj$)zvpG zRr{~$z`lcZ+5zJU40C5JXxZRvn8SKL59u8&VN=psV;J5B%&;@q z-jI{V_U8Aol15inpK0z3UJIAJZ#Zf6wN;YwzMMn+fy~JL(^_9fP5QWDTUgX;gVx@L zeM`(etK*L8)_O@vB;Q-@_u`DQTjtupi`=OTg!?5{&q`g;%&?%5TjNOlwhhb9hKFTK z1u$nhY;<&ItO{Grd8fjn=t=7K*`5h!@-z~HnAJ`nN#mDgzhPdva&!1r3t8VrZO_@| zKlzi-#7?=&;2-_%s1lbND{tdh{WDS$yRMY%^JQqCbnfzT_T)3!TR2-!@N>@5a6fdn zvWt@QQvOL7bJa}(1ule2Y;Ork1+ XTq6wjg#Pyf>S6G7^>bP0l+XkKV+Pw0 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_brightness_percent_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_brightness_percent_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..2203ae978f20a5f8b049b48af38f74596d0dd743 GIT binary patch literal 772 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;w^kx;TbJ9DX}(w->XcNZWs27VhS*^@-YI zN-i71Sq>gIh!b$U;=+=zk@ZkGB3o6g%WTr2=@--@sx|jG*l};(act(?v+s^|JZaBO zJNNJN*)ubZmH)akem>x|+OlRw=Dsa|rYF>_-NLo3Ay#@nuW@Vi#szyOIA(s@Q)P3$ zdPV*jlgbNiSJ}LzIXuND^{Ir)YBm)) z*CDy_AWPDw1F0bkdRUhwWbEs|@J%oC(&s&s9d0jJ!gzT9GQ%E&Wv>|)FqK?As&^}F z*|quX^PIcVFR~iHUa{}#Ewisz6XJvq{Ls3Ykf!t}XA)-`M@80whZtvZT79DW&uJA?oC)Paj^RS!;dpw z`7|VXxb}xNUY@pe(R-D0Kenw>4LAMX&EUHtwc%b?-?A6X>3eO&{C}xG2F9#viEBhj zN@7W>RdP`(kYX@0FtpS)Fw-?O4KXmVGO@HWFxLh$3=C|e()v*}ZtQajI@|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;yeTT^vIq4!@mdtIrZB(sqAyj?%=O7i*k0 zbJ;g6y~Hu~YPO13pr+|2*1*+MxIW5v+zoisrC}n~>asjQK+$AVf11kVxi@Cc-D#Zj zDEHapb330~zCXFQd`bP+MGabxvj5n;*B!oL6!6R2p>~qlCN?gP8OqXk86Pj?n;98< zDU@yNbd^x2Mw_>33|A(F^nX!JIN*Cq*ZKvQe1xaqGg*n(VjBE+ma}Y;U48OE6_-K3 zgQDIAh7S)UGIYgy__Ho6p3$A)&lSQjPnI!M=-`h9WikimST}xbNE1G^rNMK$!*R~W zZHu05;xOQzh-;jEKW_jwyo=IEp-jdbPY{I3=FJHjjRkzw1Er*1JgSZswf(A^HVa@DsgLYX?!pfs6hi{LvdNU zRZ?Pcs%~jfCPP|gPO6o@zJ5t=fqrgcdS-IAZiH!-M~+EVDEF4kYd}2=p00i_>zopr E0QQ3ku>b%7 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_checkbox_marked_circle_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_checkbox_marked_circle_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..47c5c85a99325677f6f7f45d03712629b4e53d3f GIT binary patch literal 1525 zcmZ{kdpOg39LIldBO+p5Tw2Lo=CYYf*}6@%jf}B~qZ(st2Voo49LGqB<5CMVM2~}9 zLY}l5tyYJ|8kI|rYt#{Ta;v99E;;8{|D8Y1`+2^f*Yo*&p7-+{X@BjI(mjkN&) z&?OQ){6P}FKP?DYPt4uJgHUJSd~g8JoTsxMrUCXUY=68v(AZ1)*M;Q9-O;|vjq`E)NFr)ngtVjH6P zqk|6AE~`-;YINNpx}Hid5oI<~{1!DtKA~T&`RWamU42a_A7IrX@VP?HZZRy@~BA+1}jdvsLc5cP#QMm&5TPe9i7zv+-wd(|ESpAHGgL?~MGl+-0UqULF3o!t3XvAz?N;czea2 z`v@3S>txPBPj@;Ay+J3L)Jy#WO)fgk>>--SVKZ{!4d+4^Hm|xevoz6_FueVMHkTd0 zF+PCsA*C*nuE=+)7t%&f;-R+(xP|fKEiLemMr{GT&nJ{Y2gN$*U%|1fSL;EKR}IZM zEhsDIsG=v(j*-)UGt&;&riv+E;FXfPO9Rgl)^2&>G(XVCqB~g6G5gu>v;uzpW#mgy ze;Z_fz|*t?nxO)x=M_3E|4_Mq+_;V)9l+#bmS8xk;3PH4_V}!nR*Y~M;+7);*QgEj4rClf7 z@7q+sVA3CUU7cL#L=JTEYLjX0L~eypf>hogG$@?7CNy&}V!z%MSK9*Xip*IuEGM+S zR0L*Gw0$g|9PiCvwF+7_5qo^@sDNH_o$H=O5|`ywE6bQOUahtsxs>-mM|F0qyV;q4N+Z{ApwKgdjOEGrdnFWi`Bs$1tW5H0Fg|?Uxr0}U zo#Zz(3SP_`^Eu*5dt&pG54#X4f;DDV`Q$upa&p& Lk~|vSscHWLpsku2 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_down_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_down_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..2e435ce0c1dcc661b20eef680c5aae5858110964 GIT binary patch literal 588 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3oabiQ=YWs*qLo<-=_UOQ>br0N$gbh3$@-ImXp53?RK8(F)41oo!?52 zpUaO9}F(VEb|_;iRHLkh62S@ zOI#yLQW8s2t&)pUffR$0fuW_Yp`oskS%{ILm7$T9iKVuIft7&)XExh86b-rgDVb@N zxHVLm?JELm&;Z#`T$XN?lvtdqTUwOKkd~Q~YNfBQUy@s(pPQJTnVhX_9vqlk9N^=0 S*y6YZ$fcgHelF{r5}E+nOwI}b literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_up_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_up_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..96e1891eae3223341f802cf95b41149607202b57 GIT binary patch literal 578 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3;=;WNKgV!uA0vu~{=$WSn7SU)Mm`___skJU{Jqg+0f zJ`y@`jL z%4;nrwjI;d3QCw$dH(NDw<>c#^-GGYJ?3{kwq1WpEy!Wc=Lr^(u3e()-5whsFX=M* zm9RMa{LaNQb>|jOFK@SazAkN>$>zJ=yO$_uetnSrPBm08%~vH?{!H}pcLpk#;x3sy zT9dcre&6Lz5z{{N)xBTj3-=s7Zh4Lg<^zU+1=hb+x^g1R)|djtR7+eVN>UO_QmvAU zQh^kMk%6J5uA!l>ky(h5p_QSLm8q$=fq|8Q0aId*3W|o@{FKbJO57TXZLR?0hd~2m zLvdNURZ?Pcs%~jfCPP|gPO6o@zJ5t=fqrgcdS-IAZnj0FsbQsOU~onHYLH7kUHx3v IIVCg!0FRo!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3}28WR?tSIc_0(|WhaYbSBNuJaG%TUbP{i*;6K@7fSiuUFKpn3DEx z@60=%hkhyu%-Fg7dCm88CsSWEBucbF&_xqHGsjDr59Btm#xS0H=2zv#*QkF$Z^Pp` zzhk2(EHV2awqx4vRgxE3&E^!_-e+0A;L*vSCqA8$yu`ZB`gxxk?>ZK}J?B52W{hCo zR@9EeEhFXMswK~Np7ppjxkNEE_9RPh%y!L}(QJmcp62PH;V&JI%I=cb`K@P( z`=oy+3eO@g8GEkv@LN48&Q#HZYt6@v5}WfL$@ZE)0dhVgwlfJEH%Xhl)Le>wc!{lM5 zkjm26g0iYvhd!&V@M~P`yXRy4$(W`k2aT#0{SeCZt_rTXHF4)!rL;}o*UV{Zo09cN z_0y`WE#i(BCS(YIe_|Xe9H-(KbW$$($;2g``m+`&%CgSdA$2GJ)6;-TcSFPM1J=EJ zJ6BiuGubh|E^S_}uq8A8@5b4E?+-LT`W?K$)^*J*r8%=-B+X+fSw(dUC*AKopRk=;8vgw>z?!t`FqZk ztSlB;Xu-NUkR>8)7Z2ZNnV)J?Ecr?&9gJ+deIQiq|Ni&C52Qa(ZGIf!9(>H0=db|; z34P{YB(eOEjU3ZMV3JlXag8WRNi0dVN-jzTQVd20hL*YpX1WHJA%+%K#^zQ=hS~-O zRt5%Vu105|Xvob^$xN%ntzmD+<10W78Xy~r%hIiq5{pxHON%lY(lT>Wt@QQvOL7bJ ja}(1ule2X*A`A`7vkP-p`n3B4^)Pt4`njxgN@xNA3`Ct< literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_gesture_tap_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_gesture_tap_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..358bbf760083fccc9385abff9e5f9a20a65eb513 GIT binary patch literal 798 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;!Rwx;TbJ9DX}(W44H+Nc;Z4GmV*xmI(Sd z_;GI(W^&_hUaO>$v9z^g>5XNbaf|oN@v3y&bp1 zO)pHnq5ZX|+&g!bx$(V!9lK*pFXX7KO5JnyLtV{SNE~6 z+wX;03s@KM#=H?MXZ*oxqt7(OyYT#$D=S~D*Zvh;c#22*v@{)bc z9iSo`oW6_U?37l`Kf13O*7{EgTDjBe)aI4d2W^AjuUdXZcec5{i}~Kr^(z-QGhMmZ zRTVu~LS#W-*wyM+2OSw-tyn2{Bj?=yTQ%$cetvVK$+U+l-HUPM^G4AJbE`b>#tCJm zKUBZ!`So|(ru`xQ$14O)xxzJ;Zef|jJWKMozz2_5jmVXM92mc{Sa9j&N?3jKZ(w@G zYQU=##yC-0W5Kk>#|IuaGH|UBD-hcetn~c4SJwo@coTO|**UjoCOa_BRZCnWN>UO_QmvAUQh^kMk%6J5u7R1Zp=pSL zft9I|m9eQdkYQjTs}fR#q9HdwB{QuOw}za`H|KyFG(a{Km!(@JB^IaZmKJ3)q-Ex$ rTIuWSm*f`c=O(6SCTHuKltlRWmHQS6G7^>bP0l+XkKT9-eE literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_human_greeting_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_human_greeting_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..a14b71f59f933da0866c23520aca5349d609452c GIT binary patch literal 751 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;t)fx;TbJ9DX}>ueWlbh{sK$ zSwEgkKfCk#{7*^-j42KLU%!~HV@Ytmv`Q;|S~t6=`J{C!eK)XR)kH zuDPezb-?JtWsa3G%q{nJuug0E_R7w#f8oRu&s!5pBL1G+JN<%m@T6z^bc0Wx|9H`zslfc_x}MI!uWMMxttoYeY#(Vo9o1a#1RfVlXl=wA3{)(={{= zF)*+)HL@}_)dn&Q3~qbJenruco1c=IR*74~DzT@YKn)ro8;Z-)t&$RpQ*}#=G8xh` tb5gDJ_4P|~3-ogn(=(H^bshZ;&BMb(G8iWEJp<}t@O1TaS?83{1OQ1SALsx8 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_image_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_image_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..8cb259f375d7a3aa6f934ad80be6f03e299b299c GIT binary patch literal 649 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;ykUT^vIq4!@nQ=+EpZ;C8=!g2E5QQVFL+ z{0&t?E0hxs1TcDp*h=a62KTe`i)sjYd}wL0I?`!&AR*(@mg>E4zrOQ2WLIv!+c>{G z?2VIxfwULbj7ZT4MzNxGKUrMcr5AryZoiy$PIuOkS^XD+ zI-QcYF4BqYQsH1-s36kv%s0@<{lMpV!CezdIIlG9;8`ek!MCHdA(+9_-(hdY!s-VM zH<+rFH|lLrUTObBaV4iow@?-PHr21HcNyEAH|8gVUs|0UH&^rAtX4ubJlkVxf!T{wTBK?T?l$|Dv{mPv`j_ZNm!_ zOQM6snf~|4PtrPHczf%V24L{2mbgZgq$HN4S|t~y0x1R~14Bz)12bJi(+~p#D^p`D zBNJ^P!@yvMwMiO^hTQy=%(P0}8eXRL+yZLQ0NGGnmTr}lSe&X`T9nC!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3NFHSo*v+{mAUkMoSXkYur?G?qC!)(Sp z`_umRhOGy>Y~F|!$o^pd!McN|hW*Q#H`BInQCrgESrr`Q9~6J}rMH#(OI}auJ+Gx& zW_WrYS3P@mf>4s*B|}f)d!=E5n`1BiWLwhm!gJQAb=IzDW-MXW?fR+ju4?<}t)fc$ z_9c<-mfvSg+;aK$@xMvaJb&G|eafarBXi~UIa4S3Rn6`wh`Kb($~0^r-=r;a7K}D8 zf9Z*Sw*4??=HIx~B2~vpVa&@<>V0c#ly59Fo~>zJxa19s=jn#SJpD2a>J81Ic9Xrh znZ7X!-xCc_;Hp^vW=YWU8B1ahJij;f+7ngd-zKIOkrS4*H_mUkdQbFPide;+Gj-ma zdc|FP&wn`07{R<}w$z*w7SCIJHJ3w3RZU% zKlA40q6LS7eJ-U;E|RJAx>OMua4P*m^u4JjCvLkkUIa>Le9+#oj5&s(Qx6_XF zEm*5lXZggGZU4LErfn7J_mmD!?VPc|_g>_7j}6!S0*)mwY`hU(k!?M_@4Dl630_{H zj!<3I?E)O@f{w7P{c>)XYZJTkRMqSMMGYOz_fF29D7~<8qSjO~q zotX_C!}n~?n|K;3H;6_M_V#n9x|H9g$?KDg6yC<^RLWO#+>90p(9pQQ|6OJH zC5H4fZ(;%iUfum8_dbiQr=dKEX)?>qo0<a{d4)R zGG5K?s=nS;zBh&Ydmn$iEOPp!)%WcT{gX`fdztt@<-4Es-8B1Dc8a}kVe6Z!XEiyE zdl!Sj>j6qq9HdwB{QuOw+1GuXH$S0G(a{Km!(@JB^IaZmKJ3)q-Ex$TIuWS nm*f`c=O(6SCTHt9n}!*tRi&l|e6BqL)WhKE>gTe~DWM4fcMwT# literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_map_marker_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_map_marker_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..a59232fbbcdf1d7360bdbea07ddf78ef64af4e5f GIT binary patch literal 793 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;v(Dx;TbJ9DaL!uUE38#IcX*EUs5Lq@)Y~ z9tsoK!@+5`&g{@5-W?U{TxNA5RlIc~i{>V+ZHm~HweW#ho9eoxi6`zWDa$y`v7BS6 zuJprU*5`9)exLR{3V-hEVMCQFIOtZ62ynIV_>qjya%QOpd#)MxliGuZY5%OAFX zkIvf~@SCKzzGT>z?2xsYy<}N$CS#v)!o6Y^ljWRI{0H`=tLmPS_%Msp9Oor1;#ae6S%M>`J??;vE{$A_Y|Gc-cj}ITSuLH z?nQ~uG1V9I+PwQJnH3l8SzPvbMXY6(t>OZA_uLzS(uG&NyrdR9_@NiWywj@qR-^av zJ)Hsp<^hE|bu2ut$M|n&%P(arQDwWS#y)TFQl>df>2fv)>I9B8J@e4tU-Ng*bH!EF zXZJ3dlfOp&zv|a(x3w01oEQ3rDf{5|sHm@rN$e+>mp1gTVtMlTOIPVE<6|PKy6k(F zr|x#EVzg@9%~Jk+n$_>ns1-9=7R;ZSprmpCr^c+kK89jf*hP{Px*X!4DRj^3W1N-C zq1JS(B*9FoYvY2jRp&R{n!}vS%N}_};Df-yKg@22!ugy!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3jlJf90*EaOeLm7kOS{jtZTnM-D0+Ms_S^zB*ql31xPe~;Ly$X>fwqjI{# z;+51FRwP2;<1V@Et2ew8YCP@|}jue3t3gimhtj=^;@Mf!tLclf+ z-XGE*8dh@zU(kq8%k*admX#lIFzC|eCG1aM2fPjQ_nf_R?cZyQmCwCrnUyxjl-bw( zoK)j7+2brqeIYz9b3`9W95L)j7C7#qdH3o)Mg|6+kG!w8$1S=N_j(gB@KsA(BT7;d zOH!?pi&B9UgOP!urLKXwu7PEUp}Ca-5SeKk7+4t?D5(d2N70a*pOTqYiCaT()k|-n z1`Uu6#bxPMNr}a&x}`;#3~8A;saE>>`X#vq`nieenaSC@u1R_6PNl`pM|p$S1NAU? My85}Sb4q9e0IrehFaQ7m literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_newspaper_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_newspaper_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..9d659ee7cb3445dd5bb60bd0f092e16c379f5e1e GIT binary patch literal 576 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;s!bT^vIq4!@mh+jq!7z|CIyh~x@}gOV$n zrnOD`eBn}&+&YHi4%${)d=8(tZ48?xYOVH!z3lH2jdK>0_Z%_qoO05H!{Bp&<4W<_ zZ0Az$MtDQe)m7D2q*%+Ps@6V*CdfBq|C*>x&ed7$;y|m!C>8>||a~RK+ zoYz#_!tze`)SbqquM@nM9GkRlk{Ivw_VccnE=m+<|FoM~e~iCwmv7bY*qflhRxNRj zC`m~yNwrEYN(E93Mh1qKx&~&thNd9~hE@hfRz}9!1_o9J26FR71W`2P=BH$)RpQo= ze`w8Gpau<)4aH^YR!ND)sk)^_nG9)}IjL6q`uZig1^T&(>6yvdx-Q|N6**~{p{cD) R!hw1iJYD@<);T3K0RV^E&2Rt! literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..0ab3e54948bc6d713e2f0548943e323b995e1898 GIT binary patch literal 764 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9wy$!fk$L9koEv$x0Bg+K;!p%x;TbJ9DX~^TiZ2Ii*9+u1oIt zZ#6fxdhOQw>bX~MxvzR*&n3aw_W0Qe_FIhGfYiYq2c$pnZE-z2eZzq*8Y}&R)-L(E zV|6g^oX=~y7pVCyFtIn4RlndUxZv-O1U8Me2RBqMHu<{g_^nIv;mc(!KCZaHeL+rb zd+>(9^-K6X*G`K0>?e8q?B>_C&+4a^XurI*r}$g%#(b4ntG?o}S5GD%5UyXIp73h> z`HS%e(WjPbC!EOdFJYQ#bZ8FKT}Nqi&NtH-)A%!ZE`Qj$I`Oj788NeUVhOA#+!#(W zWlwbETJY@0ucI8R$`h`=x9f{{S5SNz7vy|kvyDeqp^W0)1>yJ`C zCKEv>wSpXh1s^#a_9Ztn^j5A(Jsa#5xaxOsjqkglQw)m_C=|SScs*4(W7!Ed{xrML zr$1HCRP4}wG=CfCmOz6i+Zx08|EDF${y5Pcy`6K4p{GB?eO@jeMt8MuH^h8|DpJjtvfMj=(vpQ~VP$jU`sHeWb@Kh&O-#>B&ely#@%K-3&W((H^xOidhr!d;&t;uc GLK6VC`YDtE literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_octagon_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_octagon_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..e7e89204883bd2765b033470c0f9f49803d7a8b1 GIT binary patch literal 621 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@pz(U1E{-7?_ukIj%X`Q`z-_-X4})xw%8CVP zOe$`P#%77ES`$2kxvFM7$dFmc;?ePTv*pXUscO&P-|TxNf53ppS@(=h@-yMz#fi_H zvVYVF2_9ZPU4Uc0pOe9!JuXcjii9{1AD@0gz;2$4#}EFkm%LiO)u=SaalR7R!4UDQ zPfpM@tA3J{gQv^tg-nXP$HXdDhZ}Qg{^FnEkmsXdt~NpXD^v2yLksQK#Ru9my<$DG zOrU}5z&+dd3_2=O6QjQ_tDSJ}o_(X(nE@*Xx=^ns<#&a$VtKd}=SgsLU35hW>!C8<`) zMX5lF!N|bSQrEyz*T_7?z|hLT!phV@+rYrez+kCqeJP5D-29Zxv`X9>u4EiP3Dlqg zvZ1&v-6|=uI90c_D3c*AGbhzbUthl@w?IEPF+DRmTesLEFUhI2EV=*llR%&z22WQ% Jmvv4FO#uIh*JA(x literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..5e2414150342370b198cc2c39a2bf33b55693461 GIT binary patch literal 902 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_mZ~PZ!6KjC*fq?9LVmlyE=a>k@e+LsW;! z>|lnX1P2G#t=VzG-M2Wzx*|B0A|hOBN`CRbuun*we#l8FLs{*X2AAEc2g33vH%>4t z+`PLuy{!BlSMjOc-_OmtUsGQG-aH^U__Fzx%&!Gm{hK}=e%18L>F$(U%Y7O)uPaj9 z>L2(sur$-Rq5svlD{-z&cgk$H2UQGS!Qgu<-aoF*2c+ydVb3I zzP;(UZ{fZkjt^0H@2vjK_toHVWrF*WJ%$fr*2^b^uSkCNykq(NkDG7-Ya%?{q`h`e7h`YB>L&D)}UFfIFhSIxT7~Youewj7<#(IX<+vgXhFTCtv z|NWIk!KxEYFE;PA;<91i<`!$wa$(E88Kno*6Vf>z#-4eiB)UUJ`@U*)gZIYCi?tT` zF@1Ijt^cikV4uLD#d|t8itY$oSo5s(ZpSxc^9CzsnKc1+$@T>*`9a59vY0E}VwPX! zzsIoKi6j1&?}^n8IkR{CJ82SV&Y}IH zJ3h*vVCG>q5B9zS1b-fyyHzbNdYo;_4NP0AC9V-ADTyViR>?)FK#IZ0z|d0Hz*5)9 zJjB4z%D}?P$WYtBz{!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3TTbHdedUr9P>G zVNbJMQ`4cxFGUY9uV7V8R{Zya<(}3-m*w})>^b``&9#m#fm5bIgyR>-5*rUY&2Nn} zv zrI}`S^zqHeCLzAN*ae zt$knJmgNosE0U)tWsFr+J%o2)$}cpvBy)e_f;l9a@fRIB8oR3OD*WMF8iYhb2p zXc}T*U}a)qWoo7kWEdFO@N*=hXvob^$xN%nt-&{F!(E^T4Ui4RW$9K)iN&e9rA3(x vX_+~xR{Hw-CAkIqxrynS$=SM926@3jrKZ7?+M@3P^)Pt4`njxgN@xNAC26K3 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_brightness_percent_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_brightness_percent_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..816b2a842ebf9ef2b02729bd4a8fffcc2b471e27 GIT binary patch literal 646 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3=yt4 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_cart_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_cart_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..ee59c0cb96e64c65d65da8ec679dfec15b70110d GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3OIu6S&^>2e`nZ}a=}_x~!@E6y`r zf9nC$mQ`Ml67}^@mc-eJslK^+&THdlAA>VS643{?H>62kX>exz!TaLr-#4C&&09TZ zzdCS}p^kM|rjGjCfKX+@NJ*3Qm8;w*v+zEclKh$D27_;Ns!Ky8&kg3ji%;jU^}luB zw&uz?rpAtvp2Su8d;C8xR}z}`&m>;hfWb+DA?EhZE!~cby7rxo`6+s4=7HYrpBwkR zy_5f%Z}W54%& iO-#>B&eo0eGA)YAk1YJZNTma)hr!d;&t;ucLK6UJ%f49v literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_checkbox_marked_circle_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_checkbox_marked_circle_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..9fec6c23228dbbbab5ecbe0c04c05c5de175d61f GIT binary patch literal 994 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_q|Zo-U3d8Ta1KwDlKpWN5o@X~UJI(k1fj zLRi(5j83tCOwAYL9Tsifcwu)7%R2U^Bl8m$wK^pyy>?$uVC!i#ql8d#*&S8EB>tIdaC@a`;>ps zxva^@vJ(s+{*p5YR5{R={`2F8yT3X2uYWpq{-Lwpr`mEKq^$Xyw#J9wV)~OP?~?`5 zmmX+4nB}X)njWj^kL`bd(M?tN#<#lOg!zy5K0o{|TJxzj%j{!k7^d5v_%Hc++9$!y zX3iPs4sY0**7H+v`t&Pjc6{$TD_rqOoOiZGe=gIW^D9yeWtjH0olU%VljX(tPgMfb zEE)HFy75`C+Klniwf|{r=HzmOe`2`n$h1ZHR9b6+X1?r#`vRwg4{f!H;9I!H^N!b! z^f`-#Zy#;(J(MMMTW9g#c`lN+@`+n}?rmP9^VW09#m|D}xlA#q0uxU!vICkVp38Kl z;jYZoZMG-tnX+aw9%`Fj9ClADQZC`7mtD@D6Y{glIzPoONR(BW5}|(~U^Po+!&DRh z%jyR-S(l%zJEwEma)L>2&rSbdhNVFg$@@Ha1#jG=qPyf|v|~Y7r08nqm@237_JiO2OtQ8eV{r(~v8;?^KAYx+u{1`Uu6#bxPMNr}a&x}`;#3~8A;saE>> o`X#vq`nieenaSC@Vg4zeFVdQ&MBb@01|1N@&Et; literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_chevron_down_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_chevron_down_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..3dfb8c58a40a90d04bc22fa02e602e029c51a2a8 GIT binary patch literal 510 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 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_information_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_information_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..5d40a5a2af59d73b3598b710c34b97a99b65ceb2 GIT binary patch literal 940 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_q{FPZ!6KjC*fq+UH+(kZ7N8o@?cjnwhx5 zsa2qN_nr2^3ihGz{g?`gUp5Kj=l!MLqq zd)hbatSZmXljOW!y?9bH?Tf>upAwIrtJqGxBI{`58GdQklFgwf?@uccaIiVKZ%K4& z*LBt0DGKwRbS|mN_}RAPy6B1Tm8vTRH$7iH<^5;3OCcQ$p?`8OMd)kQt1qgV|5V5Q z^o04W_ZYA4xp&EuB`R&(`|LHTe+ssC>`~PaNMPGBze@hTn#&t)xol&^X5!0DF(^0!J;ZpIXm=5cy9|WrJ)+igyeeAT+ zVzT;ym50P1Xlx5%v;V~ML+Z9sv!Z!?!}pE4#y8FiCs=PwWpmEk#}EzVOB8v}K2yu` ztozQ&miG?QJ7%nT#@p-gzU6E{tk?wKc+F2+bTW1wP&K~oZB}8Ql6H-89_!2P201^T zC{_pBH&^E#jk_Yxz^xv4W%f%=^LclPf3#_=fEbM>TIt};%!o39l-ouL}@a&fD+3vb@$vV!d1+&gQE)PI@12Brmu zdWLoL7903a^X&qrK-Ci0h?11Vl2ohYqEsNoU}RuuscT@VYh)f`U}$AvVP$BlZD3$! zU{LHHr-7m&H$NpatrE8eCaGsrfEqMFHWZhoTO}nHr|Om#Wiq5?=A>Hb>+6@~7U<_D hre`K+>jq`InV0y6x*U)yQU&T^@O1TaS?83{1OSNIbSnS= literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_map_marker_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_map_marker_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..5c816ab84900af0f9ff09c7cc3cd71eda38e52b9 GIT binary patch literal 604 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3?!a+UUjXD9~2#7j4?xzBX_J z$G2@4!vp3kMLlEvBHLuwlt1CvF^653rwhFC@e>R@k#5|WdUN-iXG$H+!EesK`F{7z z+%NB$Pc&vf5Ukj))1a-8&t=c-wQ4bo-|PeFI(?Cwj$8|JJMi3)b(`~=HHQi{GY?mr zO)SZ_{F*g;l3tKn#ix58L~oS7%1wCmRYbGN{QW0|3&oG?KTHv=b^Vgc{qBmN0pFSx zZ?Al3(w1tEcrDt;?&*GRr|L?EcW-L+GPMiTtD5`{>09+5&S>;>Oi-P=f}@hT^hxtE9x@RNc~|Oop_~ toK!1)ef^T$0{z^?^vvXJ-7EudLuXUZinq&G+y&}k@O1TaS?83{1OVBF+wuSa literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_message_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_message_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..3970be757ba917f8112304d424123aba9c695832 GIT binary patch literal 537 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@Amf&&i(^Q}y|*(A`5YZZT;E^rzL0&@Yxm;d zTkRe9^sZ#B?YMd1s>Z(j8mGel-tRQy)g~7|D`l2Qj#IEy-KiY8V)=wSljmHGS**tY zfFog1-3hU9hVBjv)Ar_52YvaUGA8gO+?Bg#7Qn!Bbz8oMpy4kOhpnmNf*+sQ{P?

>E)pJGt9y6r<;S{Pxk{}Zxecc&a-}3Ywa+7yD7-aswJ)wB`Jv|saDBFsX&Us z$iUE2*T7uYz%s!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3*ZuHIdJ&AdHj#q!`&x$EnL5sTW4m+%$X{w+3x#yIs7|&YkiH|-oqW!{`qF|Ru+h_ zym`*C#j@Dzvg0P>9o|~XKiRT=8M2Fu|D1lWTJ}f literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..4f78bf8367a5c67cf99bcc79eb4b26eeebcd2a9a GIT binary patch literal 609 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 z≶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 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-nodpi/onesignal_square.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-nodpi/onesignal_square.png new file mode 100644 index 0000000000000000000000000000000000000000..f8fa700c4e537dd44f39365ce0960027cb668584 GIT binary patch literal 99328 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 VjuIhdJ_G~(Bt&F|D+KlZ{|{Y&l0g6f literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_alert_octagon_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_alert_octagon_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..6d1293c5cf86eb5a845a7199eb8cd818ce7ef7a3 GIT binary patch literal 994 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 a`FU7|m1kI9yxz43NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_q{7PZ!6KjC*fq+WLzGinN_~_3k{zewrK^afj(s#F-vR_3y=HZM#)I zPqTi0@Asbcb7ySJ! z#k}$A`o?^vPfjjsE-9Wab}T))ohMe`l8w6eG`^vD)(_ME+6TFMbyo$eH|jB;=l!UB zU?TsX<(#V-vpE?jPdh!o)Lv`pEu~L)4VEpP!lgU=L8mm$} z>R)Osv`x?xspI`=U6PsiIYw;m&yLppuX_@5t)L2H_I7{+axV>d9t`;&i9z@>PqKbo=kSn`R!uFc;~=+#y5UO_QmvAUQh^kMk%6J5u7R1Zp=pSLft87+m4Uf7kYQk88Wt@QQvOL7bJa}(1ule2Ym aeUdGF{XEQb-I76hg2B_(&t;ucLK6VUZ;D0$ literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_cart_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_cart_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..2bc1e4e09aa24dca72f9c299c6951bcd6e39d640 GIT binary patch literal 867 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 KbggkAC;toB?S?l1 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_down_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_down_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..37b13c70bef8b19223f3b702b7d7d75193367179 GIT binary patch literal 668 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3iY=RHwYeuivOe?TZ=349`xi|46!df7;-FK`=LILdnI81c zTl4r<`ImRn6C537xO?_f$__{=NEI(wD#gS~~Z$ z_}B6Gx(n1x_qqwxi(B_8)Nw}x32$L{f&J~L1dqJWJk0UveBxOkk(tW!(f!2j`(oV} zpTolh$oRKCeE#7tAv=0FErC4M64!{5l*E!$tK_0oAjM#0U}&jpXsBys7Gh**WoTq& zVySIlU}a#ynay?%MMG|WN@iLmZVeS?`-*@XG(a{Km!(@JB^IaZmKJ3)q-Ex$TIuWS nm*f`c=O(6SCTHvV`s8PpM+N2S6G7^>bP0l+XkK%Bb~! literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_up_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_up_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..872bb2673cbc9e9875d92438dcf448860c16d60f GIT binary patch literal 687 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St39y|Ltx-Wz@*}54Ae_ z|2Ri>mFRsSp}tS>T7lX=!D%K2K*GRipWrhSLm*L*Sfd=1wCso5jHG8j+$54y{yct} zzvB1W_uI3TAOH9JqE} zeW>Nfd~u7zrF|RPe>~OME&cHFCH;)GdwKrsPtJb3?e#8uZ`mcUcRjy$>-x1@kI$sv z->%Ybe1FS)@4C7nC2@p`p4oc|Qf zvpRIV56GUr05uYY`MIh1;|q^ag8WRNi0dVN-jzT zQVd20hL*a9hPp;(Ax4H)hDKJVrrHJuRt5%4i8(4L8glbfGSez?Ybdt40!$1H8Xy~r z%hIiq5{pxHON%lY(lT>Wt@QQvOL7bJa}(1ule2Ym)BHS6G7^>bP0 Hl+XkK2Q?1T literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_email_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_email_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..68f209e2daee26228a2594e05013a6802dda6044 GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3dHgIvbLLKezWuq)=H12YP8^CY0&rr(8=dd0lfE&# zH!vTtOW6M1(_%OO9R@w7^fPa~&UH^_p72FwgJ0;M*n8=F7sxRzZ}30+=KU0L71jXp z|LzY;-y7>3NqsPPia^l)ldoMq7VlxKnQFA5lhoH<{N2{OXUrPJ8}wI7o!?Z=P{VDLpLYNJ?u}DaJ%Y^>(u9-U4`eSTW00-PI~!`jN21>gyw$4-04B zeY44~_ujhqXLC-U;S0{2e2_$5-8}O4d#3ZHeLauw zslJ@C#9C8YUaR5QvL$Ptx?Y;KcV<`cygkX!yGkut-rv(o z@#TF+uKP0g=PCA`S8h0#>)HOhu08eEl^o+Q$JSVX;aM>8&XU)EO|#$3mldnKCRdtw zPgdWF;j_qsZneAFlXX>r_OxYRYBsrfiD|;G{7cPdH-(rpzFb^lZdx!|*RsJ)-1pYn zlX}h!L03Q6oz!^{=V^cC|F^U~c}wqwtNWJSV|ez@XkBu$(SDxOitYN$ckB3PT;%u> zIn#Gn?<+O#KgXJCg?DEdCtMQy&^mMO+;rth*BbIyyn7-SbDJmP34_%0#{M%puft74 z?K|suQ)wG#T%Fvg>ZzYL~2}+R%M3J#fSPTWm}2O#38sRW|eF_jl|I zstit^PAkj3wcwg$fIaumDc^63yhw@usQCBdQGW*U4)=&7DmM)8-;F8i-{R_mBD1 zs^{BPpS~0UW`5NY*NBpo#FA92*s0kWaEEZr(8u{c$?v?!AyEi)(8N?%{UB)336H!(dk eIa}AT+|b;`&^IV$yEmvvWbkzLb6Mw<&;$VYxINea literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_gesture_tap_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_gesture_tap_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..2bc4b93477fe1922984c495f7e048b1e500dec7d GIT binary patch literal 911 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_q`8PZ!6KjC*fq?CLFqxB-MLNet}|Ebi&#N0eOFzMN*tqwM=$fX&YucXK$Jm+0&nNzWep3 zW7WprclGbd|5SAUPWkyc=fD4(W3IF{>*}h|)nRM@J2)`UU=wQ)-7VJe`oO*`HG2=3 zGi-0r`}vFO4$HOW&;H8(5pI5SgQJIeaak|3`5VatkG*Cx9m^{n6 zIqSxjmtUH``;_EZz4bk-zs-VinfH-15ofMm`NOowXa;lKU&GmY-UkBT-)uR0X8AMm z1-S;&dw;2>f6uHuZx&f=EpmJpW76)hRpI-u?z}&3a{dY~HnFvLf|b;=e{bVrn-^8N z%;ij`wtSn>h3yS@511#2W>|f-Ke;lWX@2hOyHjV`H}6WDbDEV$Cp?Y2bb(ugCBMme zh6Bn87N*(rxFW^i=K5q_hYbHcXS;}l@{LQyx>nT*O%zrtQr*`0g6SM%yp!+D z7ugEOk0d@w_F`t1PjPT|k?S}SapGL(CblA>v)w0JI`(j!xUY4~;6&!~RqtJ&Ojy(F z!f}`PhU8IJ9>(cEK8kxqZf)oe`MY-Qw8iglzqU6ov7Is%n8;L1Tq8Hb>+6@~7U<_Dre`K+>w2Y?Raj(}n`%CO7XsA7;OXk;vd$@? F2>?+KaRmSX literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_human_greeting_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_human_greeting_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..4fa79f8a8b3fe8cda75393b9781d5e5e514c0edf GIT binary patch literal 873 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_mZAPZ!6KjC*fq7-ow&O0?bgH(cGZKyXW- z&KsW~7HdJ5fKU7^vrVFYu)h)AB6PIEvA&~Ms5eGY=%|(;kK+`LMXg0buFT8(kLEro znq$1#x;jqmS)*>=%)k4-@0>e(uC3-o4;7(K85x%=a~RSZR3B8==`e0@{HUwVka8gQ z!S=;D(lc1ywoP%6tD9FK7p}zkr^MtW*Oi^&3V+yowOH(|q+~eO7(Se7w(g}#n91jD z^UmuYzp=Q{RR7yb=|g3n;z_Lctd9AyU9$;R`oor<%o-OK@TZ{Vz(hwbAI9HGhgt(F zcD@tM@QV6>{6_4gWz1Ro`UN6-g$y=NThiY5aeCw2d9#WInGCO&F`TQOwp)AN>*JqQ zH=Z%7TEo>S_+akhi~0$df|Iwk9Plamv5sN7&Dnblzis0WEoOT7yg@dUE9F4f@|R2- zjw@gM;IrIp?%b@>hr5~P9NMp1!6@_PiX?L$WA}lmIvpl+?U|qFJ?m|_y`O82OvVI` zMxG2dk1dVX2ks@Nzf(8l(qaE65q+R^!G~!Jbi_0YJuWeOF1&r$e$u>^pA)L|UT$5o zKJ8ns2GhBp}IG;Co-Bdg)QH-hiTov{;My) zG_dT-WD4G=b1-;Czst-0QCy!~E^V1vyJB^NO6|mwb7!8t)>81aSCKW}{Pf-A)VO`^ zNAo$Mt`i$A$Yu9`o(za<9&s`P}KgmG}6o-eO=r7=Ws<$SM$s3q_R7+eV zN>UO_QmvAUQh^kMk%6J5u7R1Zp=pSLft9I|m8q#VkYQkO+dK9viiX_$l+3hB+!|Ji zJ@o`?&;Z#`T$XN?lvtdqTUwOKkd~Q~YNfBQUy@s(pPQJTnVhX#l@;ObTjk`*x$1B> PP!EHrtDnm{r-UW|$kJ5g literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..85a213f10d5b60d0de6b8237b5ef660fba9de07f GIT binary patch literal 731 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@pz(7&T^vI)?!BF{*4sHyqJ4gOl#7d6Shu5N zbCc`tqgfjw*vvlG8%X6YJ9tLR&GD$tjI@Xg3T&(S*7vbqoS!J|v%~t{?*3}AbHeX` zR@9&W*>JkiK|w$wO69DF)npOPY9-I-lb*#$o)wkTU=E&leIwVJSvQkolFu1_DBw86 z{G_qjuXmknM+4{mm$O~c%w~o;>ga_2gk}^ndw=Z?s#{=roj7g?LGDN2R6;zd&IQt z4x@23!?cEAg--%DOnxyBnHwDr*zeyxo2RO-ajqCJ!cmdKI;Vst08x4l A1poj5 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_information_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_information_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..895e3642ef45435d707c925f562ef2cd5f812e18 GIT binary patch literal 1716 zcmZ{kdo06Hu6w-Zosj>rmQ9t9nvdtSqd zO%f~Y7yx;W0LV%J*n~^6MF4Wh0KDM=Kve)>F8Q`E)EgGii5w;aCcHNM`U_#`Nm$WY zFj>FH=ffv`06^ui8T5$LPnYT+RGRwlKKD;|ywqvVM9Q!TMVO-IN}U3#{$N&KwLWEO zPB8wW$n~R1IsTz(>~mp>)B5>A+tNjDC|znWI4flEbQ=G%@sCBvQ6pu?a%|7ol5$gC z*Wc#Two3DwoIDZvy=^s%U|hzqt7cuGsQMv$TOszUu7dW`1J!HXduRu$|4(v`Cst6D*FWq1h+owXCN^q3=?P z@5GFp%kERn6b8=@abifXLk#~GU+%*mW8+`7GwbgJf2EZ-%~#xsSAD@IIa?u_q>+h_ zS9_k?OBfR-YU&NY^$hmR%}JNr7`Y(tS2NBwMIMxYAf^bx4wnAx@j)$j&=u4TM3hj#OvL{c57?`+FVXH@J5QS+00MG*1dc;-u}@!kCNCT%u%ba^#*=cH&D*= z+KWqV*d{+_ke6|Bh8D%30BR`>Wc#`EP}Vj+$;ir*+?~u0$CPoea0GaC-`NSmsYyxjc>{8VDl>kBfuxf#03l50)GPz{2VL zhdeAKk;Ny@PV`JZ>j);R^g6@<(82CDwNV{IPG2b;5WW!)F?w_ zxJvV+8gDA_jngdVi^cdKm+e+;x7ocEsAG{;S*ES!Cs>;pQzu+Id7 z!gFyUY5qiy?Y_UCsUN-XHs|*~=~BKd1Bat|LYuM;{;oy(SxSQY78LdNW%9m8+*Ann z%hp~ev7iy`rBY{v>6EU@OUI`IIxCa|8P{U;l9cWP#g}fpbQW`5y9%z|O;xXE1nXQ8 zHCM%Rwu)8hw0E^V*ovD8g4kHs@!#w?AJfM6eArrln+zWcONaAe>4E1n6J%KDY%KnkvD zGHn^Nr$PIe#5EG^-y*WYr|g88$Zz~ad(2;GY0c0BcGqFy8tp8IkSrtBhD?)aq8%F=BY?^rIZCP%8qR(2Y@ zT%I+5``_V5FwR*aWe&BxxoB{?8J@%r+5S=4$qCtfYEmX225=+0dbyCjT--b&$gWhf zC)Jg5fJ~;6$yv-er~fgC#K}T|^#2=>_=T@vgVU!QvQDK?6BDxdE;*S(AP}bWsU#99 lTO=Wg5>kXoCtZ$G0@9+QJcFaGySrcyVEYF#I*#&+{|!a$%f|o! literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_map_marker_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_map_marker_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..cedac382bea4a16d5a82fd36341f06b5a25e24e1 GIT binary patch literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_q`mPZ!6KjC*fq+GdA1inQGq)VUIS#OqH+ zlaiE9iJ-Qtup+nH#(*#WON?3tbdES#=bnwQq z_S}4K=k}dP%^R7`8&%(AGMVow3HzaZz=`3<{7PP)r%m?kYrZhvKc{67W%h^TLKnmR zbN&yUB0o4EnBMVWuK%NqtP1z~?0wY%#fN`50uk(EI zuyqquOD+U_z7g>GL_qNgF40dy{{uG8+r9MFoapx#mutPO?zek=Zs)32?I)ZqSIW9m zFP#yVX4;yy!cutUvuv)$ROhpAJxr%}DyCREFY;kCOU*KG z0S{}o4H6rsId7g(Q`PXXg=zX^?uyQzEFZY1l%8RV@lpu*b@E7z%a5Hw5BDDMd?d3r zhhe#w_y-|=We%GYOz&CC7*E$8sAu_>BK$!pxt60Rwe|gk8TZ)!OulnxF4OMD?~EVh z8{+K!y{E4Tl)3cHi`!!M>~if+&*lRudFS$&_$qF(*0tUFe@a3{>%i{)2lN@Mi$6!I zwl!|$`(c`3VIIE1Wp2{!58OK@%}(DfBz(Xnug{U~=k(9g4HIWR{gbUw2uyCOC9V-A zDTyViR>?)FK#IZ0z|d0Hz)aWBG{nHb%GB7(#7rB=FfcfITIU3chTQy=%(P0}8g@*Z zQUcVV0kWaEEZr(8u{c$?v?!AyEi)(8N?%{UB)336H!(dkIa}Am!{5Wv&@4T#e|9HO O4}+(xpUXO@geCwQl5ooa literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_message_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_message_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..609b11fc6a99f987c49d887985f711ac5ef6175f GIT binary patch literal 786 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St39nu7{3YorDnX%wq(I`(J#CazV}m>ty`Uyd$VyKgKllN zYFbbCZej={XsH|GNfhZq+03g>zCvf=;tP;XC`Os7FVU17r9i0?r!q)2kK$)boFyt I=akR{0PJWc0RR91 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_newspaper_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_newspaper_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..67536da6f4b497a6603da1d356cb23de1f47953f GIT binary patch literal 621 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@pz(U1E{-7?_ukGh^gA5D(^ju+=CWFDfw8iO zj!gf?^hWN0P}ztFJZCmEiBvVrf2wHPvs!$m-yDyB(_Y-{d}}}Lem9ef=OiDlNz#Y6 zDg1Q#bne65Np~j3NbakCUs21l!O!bc)05(j<)xPsQd(UvK?cBPU>22?C80&p>dE$RomvOQ)w?((!i;y4(6HSF#pRe&& zy*j>ZtM<8l%s{S)fN!z*tY6zNzOkObv`F%e%C*gpMcs{m^DSw}m}UInXu$K65Sd)I zPcj~i!bfK{GB=#Q-+KMuTao3L*;X@GuitLW5S_c>pINS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)pVryh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6Mi^+1ZVz$3C4NPB>>+sSM@1_q{BPZ!6KjC*fq?#~u+6ghtX$dgaZx=m`+ zI}RQ@bVw;9kSEkEn_<5$B4Lx$8^53x^$}6vaM1)%z~R_~|951gqJuGY7&GcBlEhm7F0yaiZS$cGoA1z8zS}@q#&S z@>=r)(>7IFOU^jWc>j{*+C6IOa|9VvRKv~#$MaE`(=#f*IrCbKI3#Pqo3(J*A!0 z^%=jxfvuZPHqKVbzTkg=^?^I*1N%mAt<{_soHbn>`}yXCHJ1ClVZP_qP|xnO`jEAn zTH|rXhx3_sHyyB6+s5S0_~1R$?Ixi3uSQnaAJIqbppSI^(U4 zQ_eAk8O3$Sd-!cK-JKr8XqWz_Qj)vQhB<|E#;XhYLJ=%x!M}2uX1&(Bdf=u=L->J} zGJpG-&j%-+IciX_SodX{?(~C9hHN>JYquZWC3pD1;%!AY?>euUyKPNWm4sB?j3@o4 zxw_Hb>+6@~7U<_Dre`K+>t;ndXJ=Y?d#j(& RE(Gdf@O1TaS?83{1OT(kZA$z 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 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_bell_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_bell_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..d3d04b4617323ba92a4b9542eea321a9b74283c0 GIT binary patch literal 979 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 J);T3K0RUKU{)PYm literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_checkbox_marked_circle_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_checkbox_marked_circle_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..389601b23aeaba6b5971ea4064dc28a74aea2671 GIT binary patch literal 3041 zcmZ{mdpOf?AIHD8IgKGi&RL2aGKYvpWU?jaLrNH9wV30uP_`VhM0^ob2?_m3L`zPs zP{|~UNC%A{$%JSQiD$p(zvsE0=f1Ab{krbYeZ8;Geck`O-wao02LxOh4gdfGhsC%H znDbAGi3s-TBiv>IV5e=JYyqI*s>DX1u;45dIR zI2j0Q%>?0vm}lxl_GJgZ`tq_Hh9(j(5t3EjzdK?BrVG7?WYTBpWew!B6Y|U=Z)j*A zj~bS8>dh|*NZDNv^MSRQu&h!h_u~=uuQ+ktW|g_vVolP`mRtA0sTZ3iS+;|!xPw8D zT15HSlNGgZzwD=EHGp*xZ!oL;J12a6kIC4!v7D#l2|BZ?xZ7G*z()3Zrs#Ml+YBe| z*rLNSg1ViM&TswM&6cJaHOw)(YS_{EJItHe34{s*E9^DaKuTK?kMFvf4hW##Y8jV=WJ{j0|+QJ=4m9 z4nsT$V}rYSFa_r5(~)$HVopUb9l>rMVNsMW(asV1RYgZIh(NFsMD*uTEvIG2XOb@H zD-Xc5NK*BTDX9TJlq}VKP@AA6V{Zd?0BET;1Mlw8E@Ad<&|2g6!ua`}xLZgS5`y9O zRXGOI{FyU+N#C@ZmQQXUfn5aJ--qP&@n}~$?Wri-bndT2kjgcYrH;J! z4-Gh9dOwH1)ErFcSvH}ooZm5Z$_6|FU0Fr3%4$)KkJ;vqQ!!7prf0LQ(Qn{`kh^-` zMWc^|I6MdTpzOJ<=bhCY(kad(g^ka#9f-fLP!o2M_Kw=7Aa95eo6*LTwPj1rAHs{H zmY=jSIcP`9p0J?Wa@F`({*S*+&mgbi2;rcBUTb=!pqGYUysNpJ#NTaYdrY zLs!lne>xek&zJ5l_~r;<|FXnzks8^wPVY>ROAjmiWkuu@2P9u5lnQGt%9p|a-D@#c z%HRHm3(j1}dUd@m_f%1k3Isn3t5zgDD)VOk#SnckVN+N%nu}!gQcGuX+?mwD+Xm8H zdM!dx!Rgcyn?}{+6|cM0$q3BXJ6VS{^#cx^PotW%Vc!1@>vRerzOMhU@g9rH?(zsOr9gWPr2{*CDB;$2v_r{5@?n;64rEx4)}RZLZ>T zF>3MU1y;j+LtTpP>9lXG`l0J@*%{6neR{}?;=*r-ZiXvJ2b0%3kPOR^!=>;7D~au3 zIB#jWyUSTle%qVhP|wNX4gGg3kbJLe;d4vN_A)|id>ixW>(7lE58mZ#cDGPV6x1ZD zv1#UsNhY(b3$Hf9X)7(3cU}_t{Dfa!br_Y+Rs>kG7`!nD&G+5CZO9D`Hkqf=q!LgY z!?xGX$MX#J?=COmy*$w1j%}YX}pyi8d_>Ppm5mcg_?5GXG}LpaFQ3=P0q?C(@GQ8)I$&s z%B=1Y4AZs~JM(b*=<@;B%2ioOSKE`PUZ6NZn!jo@j0QY3NB9C&{ zG;T?0mg+&gUp~qJsjrFdVnT>QiZ1?65<`bE9p=2Zb-w2ixT)M6U1y))wY0Ty=ViRk zdi2lm@dppG-U#(-wFeTd>`hRO2MgMfm@Gc3H8`Li)cPbplZ+#|OqTwLT|yhc+dJJT z`#0}m=N{=-W_Y^vzYPk_#e~$wTfo{5x`)i0H2NM^@QGPWZU5ZOfrT-AzLu6$PjQ7- z$=eof9wR~;p7~^kMA!3-WDqja>Rxu5O#TeS*#HiudotTH@zH)Zf^Z6KHN``j_fDyk_pN6?j#yvDAQYt8(Z`h@FWZRl z%Et@VgnT}KaCsNG$TMvu)fva^*$Lp@zGMk|{!$(v-fFA7JZ-u#kTgC4tAapsnl3NP zM0l++nCFK=(yd{p@Db@N;=jCPQ@A>)0Zw)c=iRw#qecRoe8=Qy^OSF0H&h)hLwnFS z>*BE>UX~ie0vdm2(VQ2Nf*}NYdxfjQbpkusWGOl}^U(mvRD*>BH)^bn&cHr?b3~7y zYbFzC=^E?$yP}+bm^23b)@S>Qg=&(-PDs{qjU8@|igWZ^PF$#`D$3lLktT~iPt!4c zDui!gs{QnH%~6_YJ7lVIK()LrJqA{!co;1c+4Gd+4@~7*6*3n(qwsX5mEvFQ(okISCzIs>tUvE=R$dH3I@_B@5(aXcbYx$YLcyvtD==WVWl0<-`dL`GZkoA+4roDzmt4BT8~r z1l4+(OTU~@`WdS%z6=CFKg*7lxDM@EVmtPJ{3Lhrl|*$~g&w_toj<~&615j%3mqb4 z+jC6q2A_`<>L%iAD%RG7`cfmJHScA9`ktL?qRn5?Kl;ALZ#4R)?05|OyzKl0!v}4~Lr~XvGLvWUuu%`6d+kTJV9p@2&OZ}k;!axz1;&IJ#72_f h2c9MoLJf>!qQh~~M;)@&c8Lo#0M6bSQ+F7D@n6#?avcBw literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_chevron_down_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_chevron_down_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..640389ab4a9bbe03b9625aefb130644d96dec3f7 GIT binary patch literal 793 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q4M;wBd$a>cv7|ftIx;Y9?C1WI$O_~$76-XI zF|0c$^AgBWNcITwWnidMV_;}#VPN_c&H|6fVj%4S#%?FG?SRJL@^oY3C&O&dk|!n-ORPF5r;s8Ml4j!8}*hpR=sr zN1fkvC@=i`?!Q^KrzcFRIq7`Z<%_?~!v1LHpA6gE0+VV^eLm&( z#m8py`?cvhx+jmH3D#RA`OzmY`g~N-q}MaqWAqj&Kbm=G%eMY?yb2xIB`)aSO<2zSiYoBVF{a%iD?_aH`^zp+cbtNJG&$d9FGWzB6OW#JH#}f@Xm*vCM z6oV>PziI+Tlxm4cv7|ftIx;Y9?C1WI$O_~$76-XI zF|0c$^AgBWNcITwWnidMV_;}#VPN_c&H|6fVj%4S#%?FG?SRI=^K@|xskrs_^4Xv{4kB%dCtROS@(FYM z9GtaisrU4qx8FQVS)6)Z!_>gU@hc#6a4t>3{d7xh0B)onU4f7%BA+XyHsxquu;0z z-1=BB@N`(0yNhm=rJUf_B|yO}$A=XzA)9?Z+PECi7kulptm56|=YK^m`aS7WuUyv= zwLQe<13Ir-&1In-O$O_k*_vEkAs?e7elVp`v`7W!d zOTY6u)@ACAD4Uadg1L9Y9(#W4%vf!7w0QfWbJ>EqKDr-Os$I8;Zaq5rjaXpj)?<_3 zhzDxUe6;iK0oib+s7URPD&IRYbk`o8eCKM4%X}~X9|!i&+`s?z@4907UGIPIkG&pS z9xHAvXZ-#48)uh0bAIyg)!bWqBsW~J(Bkp7{6q>xAIVKytD=XSgead~tyQDM=l$DVo8T^=@;vaVNLY)iatrix6Vo%3vvqySbIpPcE#3e1NrDn4gQu&X%Q~lo FCIA?KDr*1$ literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_email_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_email_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7ee73fcd73af236f89d179592eb363d5b0b1259c GIT binary patch literal 1788 zcmb_cX;hPE7XCsA!30uq1{eedLxMtp1h616Fp&@;r0k0ZK>`E_!y?KmN|OwWp=hOF zX`r%ANl>i4)DB*ti)zoGb}fuphYUDU(=qbOYP%sYV1 zjlJ`0h!HLmwP{)cpPf{M$w&H)-p4O}`A60-X= zx0EUMCW;ar@YJJ?KZV>1)ulG>x!&mOLVB91s5jHEUrc&c)0XhvttoWi@Tgp)uvyr;=aU^U4jaKP^)Du^*vUpb zX2zZi8{jXEM)=CJ4~iz{kYPp_jt_ZZQkw;Fk_>bns-&#R_D5PhzaP6KoBnk6mJL@) zwZ<+Ta@TfR@rNwBc~Zk=+H!GSqMT`)#c)^8I%i)g3||HZTYTf{R@yedW^7*d(Q2Q;^L$0AVz;c@tua^0iXI|jrbT?YPz!ou=vW=hPk`cRdvkVdG zmfP%aTBE`JD;O~vws%et^tcZ`!rnyp-o2SKPt>Kymaj+gtK`g=qqFi3!?khoqv*{o zF>+Q3Fn%X$rOqn)y}j3Ouzr)h??hM1Z{4Z;B~G-0Ei;{5gRP~gatom;bO7dsotz2r zD*kS;JsfxETTXF1&RUnnuS%bO0M2!S39Zjy;G<~yQ5H0&0Djdq7j3hisqi7FgIvP9-``z9;6d~8XKr9RikNmLWfZISm09@`)$r|A= zc+k5$rbxnzxQ3JJCscg>>t%K0NU6(}z4$f0|8(JGGY`9Z)czbhqnqSG` z$6k}K+XYE6i_O_31&(_*n=zLTDqF{?-?%ty6A3DM!p$+Ji#E|sMZbt5$YNA2y%=^tB@oW8D8OQg)DR3CL0T5voWD zle=PS)6Vi;Yv3G~ox|7?z_R*3P>fxZbM45SJI=qWcAR1H+){$nyX|UD2>)cW@e*#<4-#Q?4M0@>~Dj16G}*5!!(dCWDY)FgiACrMA}8ELG3UA`!&urZ~o z;}%>ZRIjSym#qYe7SD^dh%fbU`f$!d>_W2URIXOUcXVD9T(+&Yc*(8k4?k!U`yIrMf68ddeOEt+esp{R{bT_r zxgd?4dM*tDz?0qhc+Bl*U793gx8kUc#JBodiKdN_TG`#*x5Gn~xy^B)!r3YVuK z!J&5*@{2OaDarY1ZiVMEf%MGmG%}G$EXc_t<|Joire?XFI9`y<%8fYlvWU+26OWhjv literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_gesture_tap_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_gesture_tap_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..625409633fb406e98f006e0331857112f12169ea GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3JG6){r1&Em!t)V;)rOYqjT*&7yyJ~;LChTl@x7QGgZWmf}oSM@&Hq*1g$ha<5_ z$4k|;Yu3#8?(ceYcHXmoe|GQldByiX|4Ddo=FHCfw)fx9Ek1Ag{hqq2P^XL1L=P1R z#Um>AtL4CqCDT>jPB+oZ-12*Uyh`k}FDsX18qe<4Ro^m0@Ih_Dyn8wf!3Qedl(kPP zX4rAHeTI#Bqj>W6{h5}+cjX>_vixARL3GFZ-x_{XdnG+{>ve;Zc zURd2AUp%R8wL0VCor$G)lxO?YqpbBOSfB=)^*IC&+qu|&DMQO&c!q?Sgl%m-MTcd z^~}%Z2K`oL0}L@K<(}U(lSN@8_=2;9gX{p_a9jOLN6`{p|rY7G-t&RBE@}30o}h zcz12v?vx(}WpQ?_z2)XJB*nEjc0Ks>+Q02Bo74xX4H1)S?3;|l80D{T`o_3tuIDO^ z48a*4bKNgb;$t!gDrMXwwfpZvmEI?H@7*t)ICbvP{0IHDF^;XgM%~wzDK)*zW}A5J ztAgiyuU$%+VV6>kf2)N_FLwH0!!5)1uJ`Vex4XZfR*eHn-wuX8X^XFbVmGbe7j zeI_rA*Wiuz%eRyIu5lW?vGtf#XSUgWwcMLffen$D_NSGt?ccSKMR&%{SF6~oC6f;5 zJ*f69p7g9i_&{?b%a^3kzXGNV^BC5NpHbPqJNRAOq{mHy!RPoM^vk{dEnC4JVcpU6 zif^Otq-)<+Z1%jgWQpVhfx|m=Blr^34R$y2f6;z$)A6NaoSm=L=IiQ{=Jot-6jh6S zY_@KCyfeZr!S(T6?6b-rgDVb@NxHaTdzBvcf zpaHU>xGdc&DX}!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3wp|52 zTO2em3d)*h8T{R8k^bPt9P96Up6_`s_u9_h!r;u__n$xReqMWi?|aKhk1hHj=(5ED zKdzu;Uhc-V2Q(7|DkjJ^zx=faMGG_wHxs3iw~F zK7G99*HrOIpF5W*Pg*uj!p`?nV8VjFTc22L`7+jQN>~to>G`T}iM|uQxXPvn#RV!e zK0aOZDALk)5?|Pq$#0(u80gQN_vg?*P6_+HN&jM3Y5bI2w$yO(MD+}fgc|>wM-Di7 z${)#5d%F0`nI5Nk3+$PX>93ryJO0v%z^Z$G(?tP1>ou zY+`!HH{JbH7Z@`d+Ud>w-JfA~OsHs~zB}7hr2ou~F1{sFzouo0^nos+omCBI4|qOMI+^zG@0a(&2h^+zHTiX__!7Cl zY1B{7Iraa);!e}xGDBOQGg-UZ{XA}k1qr;A+#xvQN{M`__pfD5Djs4Vcn?fjR=v{u zg32k~Tb1cot{?DP(D(P{sVVoicr8C!=PB>D>S^DS_o{z&UIta&nfB$tnSZ~|b9$-H zTyl5Pw(Bncl6eSCODEgUI;;R zp7*J@%vXQ#t&S0x#~D;hTq8R*60J1ZvO#*-%`TZk3c+oT^(|l*y2mnUiXzudiQ{TcDqt hn4X!Ot!q(eSZEj)VEOyqL{p$122WQ%mvv4FO#q^;AZGvo literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..5aa91f34331cef031046d3d5a6fbe54fdd43dc77 GIT binary patch literal 975 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3lLi_^%f}_g}1h;J1bTO`1K_}x#kbq};qFKkHe>!E# zitntxzumXL&~JD7v$Hba?tZ_qo7qu-1%wP(PH&31B$yz%gKZ9TRH*rc6MP@UBADOJ zy1D%htEUyK4dc1+yKh@c8ZC`~PuXPQdEvqA&5oa+zPZGjcJ|Hg?H>Y?4yE$-$uw{u z=$+EKvPo?9yA=%Umg{)!J-V%NacKDzV_ReEXS!aZOIGdHWHK)P?Z5Zvzi6ik&KyM? zj(mzI6j>}=6xs!z2sFvy74#Ll!YuYeUU|CPWR|UG-v}0LKGSXHdO_B>nj;}?^?|HN z@w`JjZ*V{Obg#I?Q}=;rLE55c*(G}4s?O}tO{n`npXW?Y`=oD+jaY1%(;C?&AJ;L2 zpUug>(Vn)=^W^V~9wv73Dsv^C%$T-iqT;u(q};iM-K(Q6gnLf^_H^dNFNTWWHZHmH zccW_N=FX;D%0;nLuKaetQ|)=&_*(h3labbr-{;hPo>a%%d3&14waK2Bf`VB}RejZ; z#7GM5Hb|U1cNh0BotGs^ev`NCQ~y@RSDJRK|CHF5;7e0`D$D2RoLBXI8j}5~E@az+ zsfNY#rQ}vB|D#x$asT=%G-0VLkaAlrN z^}|Wi)C8}z%(v!R>}OwYGELyg1g#a}r3+ahH0BL zPCMN7{#6{t-l$i}cZE02<~dvEY`^LMJeFy*Z{q*zcX9re=q2`7ZcmZiB=>WLFN6R~4LRBqsjVMV;EJ?LWE=mPb3`PcqmbwOJx`w791_oB9 z##Tlq+CYYZ!3=AYG!zZF`6-!cmAEy$OzXJ?)Sv;fp|~vFDk-rzRkyS#lOZiLC)G+{ oU%w=`KtDGzJu^95*C?#e(lyn?&@=vh5>O9=r>mdKI;Vst04%qH^#A|> literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_information_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_information_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..815941700e227c99eb5916a0a37667bacc12dc05 GIT binary patch literal 2625 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 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_map_marker_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_map_marker_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..be7d044e3014257a2c5f37b0f406c98d85144287 GIT binary patch literal 1436 zcmZ{kX;hL~9L8TnG&NH)!yMN{6BWS)6SNFL;nj3po5)nukVxD>G&$v#rHz`3mK&NC zQ+aA!a;ZceXKFO8+>YhaXnD*DmnPeAVDdO`eVY&SoOAE*KKK5g^L)GKg92%~+Q!-d z0O*2rY6wL7*R@3h+QZ+r(;%p`$pK^lXe-tE5`%y|j2}Yt2AYRWmLOvhM-K@A0HF;4 zAZ$&U1TYF~v@NznG+0XIqtpok~+O%FK6^mRWdT z?J1O>S@kMpYRAgq4~q(OEmK;-oB6>wW8+Sz0&t>6DQH_SUr4#le=b~)qP-ZI?Pt6b zzGSyB*gtnp=KlCib%y?9O%lU?0KerEY+t`GlVHKzR;`wWxB#c+2D?bbM-Gq0s;p1j zE#)0y`;Nze9wC##`PHVPKHV~&Ro|tKeS%1{*lu($jx!e=SaM4=SJB}tsK3G!qD^rJ zo`eio&tC4HJGAJzo71gMBn7{);zd(MF;}w%i5Af*#mHU*_sdL&u|j$GUG;c6k2O_-otKY7m>gNlhqZS*gFF$==v>U6bY_6 z(%h;{+6`rq9hQwxkuK7i6IL!#Z$-eQUB=)JBYU|b$L+>$Rh)PQ1*_L|mjob19H98nNt6Im`om*`hBPZq= zw;S8~)04sFB*g=f8$4Mu8b_a!@8b#!w0u zC`G32S@Rf8YRSRD)~lz_R$Gj*$K&1Lry6E_;roj&!m|n#mnDL+vIk~l&u9~~Yy0Mk zV8wOQ6tmo~)(?FR`A&%VU_v(S*JAo>TM5MK!lw==P|*(()oZ5JE`0BpcP5J0znMPh ze8<14mlYvXBIG*6wjN{yUtd|_5!7{&6t!L%Y9#6_o$@;CY=@C`{;5y)u*O!}vdM@) z9N}-W+l=GgG)^6rhu&)_{jh%{0b>^jW&mFB^_|aX!#i4^#YK!2di(0<)I_RB+1q8A zQha0>1Q@;3eZ6=`efE5NpCsUz{pz#fE1Mc-`1Vkr$sl);0)-bUZZtm*Edy0U!NYsI z8P7ej^cf4z5P7&mQqr`7@f)uY?X-IB;3sqwnELpu@ZO`EO&@WTpsL^gG6|$F`_*SN zL|r#KU&OYHx~>mPx!?$aXpZ$0Vg!9nXwwtkK_21|8-oeUC24j!KWrMaxQSw|T$5?= zY^={7-QN4TY~t2x#(RRJ-?D%G4-uA2S}d>64OO2GE}xp&4pKztLf!;_Q)cv7|ftIx;Y9?C1WI$O_~$76-XI zF|0c$^AgBWNcITwWnidMV_;}#VPN_c&H|6fVj%4S#%?FG?HCxC4tlychE&{odt+~(jH^WJ!{8Y_bChp7 zxe5FTYhdUWNq^$ta^ilifaSIcy!*tAnva}%l6@kfE1_qL;?;C_-L(1Y|Gk3OhRppL=e16;0&g)a+q-xK1YL&_Fz3&y&tL^mo#U? zPpf=CE}93HyUk8Ww!08ICpWa z%>IYEH-#?zuD(BI^3R0Q1aF7;Z)?5%p549LsLQx_>vvttnbxbBqyM>_-u> n`X#vq`nieenaSC@KF$ScuD-b`g%`{&f!yio>gTe~DWM4fP3x;c literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_newspaper_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_newspaper_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..258794b941d70347b241caa31443964afa647f7e GIT binary patch literal 736 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3GGx zat)0;I|93RCc50&Izu`?>uxENYv$(9=9>+)&hSouc1$;Z{jXiG*|P4uIrZq~*|HhZ z=4V1@16`*Uy%EEOpT4_f@I&+GUt4K6|fOvG}BKDzP(n zYQ0`IMJl?4^ULz5OmB0ll55=+njP30d6*61V(R_>TuGg(u{3bY} zO27Ey_;G)&=xI)C=;_>BA*7cZPKlP9WZpOu*T4BNR?|xf2!j1m&OY7L)8)6Dj z6MHU&=BT|a_#yFW&Y139WCt`Q|Ei6yC4$wjF^iowXh&{EgHOxMsf#K6$X zz{twTSlhtB%D_NwzK9@-hTQy=%(P0}8uAaVSqs#l0kWaEEZr(8u{c$?v?!AyEi)(8 qN?%{UB)336H!(dkIa{~N(k!C1!rQNW%FNF|Jq(_%elF{r5}E*jsSi>B literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..cc6a0424d2721a522142c65f270283f66350aea7 GIT binary patch literal 1346 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 GE%7grm?Go= literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_bell_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_bell_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..ac926778577530a7a05dcfbe7f040f60d7d9da8c GIT binary patch literal 1269 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3RJs?*=?E?=VJISGOqnQfToFzPXzzrLRO zp!h*RN&RHj3V|O?JI>yXWIo3jz3BH`k@R!R_&2cskci0pxH4Vr^a1w+!WTcw z$Q)RFAoGEL`-AJntEcTTuDaLEP$9b`O5w-X7-m10^Y%tR-X3UecyRr*>IPk_{!0(f z2j4v?!)4Fx@0JqyM_H;M;?nj>ZQs&%sJsj{j@Gfv2xJWT{#xI&YR!`!yb;q%uAHgU zG)*vMJg{fUe3iF)rfJ^ORQ9H%?Vm2O=0vN+Ro4qmZSwn6-cJ0&yJ7EyXP>5 zL_1A0xx>9~*_{bnuAlk$Zuf@kk_9oBIpe>^#C_Iyqx!&izs{kG(~C3Sdk5_OqvKt5 z!+PSOH6B%W&is41dBt(b4~v$G?^8MJS+(!|A~~kG59gJ7?md5|_#pDX%%O^P@0;Y9 z_(V9Ax_<8CuHv{6uF#?J?R)G6=L3E~=@+$ma!kwi9A5BUKhtI%v(?+DGFG+kO=X|l zyKvTpi$jf-WinHfC*#2-4GBRG2AT>pLnqD)lHuhzsNfZEI<1uWjfPbmYh!?-e2%^%4U@OSD&G~aB2h3|N2bT zg&P}q>Yb&$Gn5l9{r2fx`K z#Sj{NIV#h4jc=gv-j+$B7bwKNk)c&!~6}_zWK&Md{ zn3YvaTq8Hb>+6@~7U<_Dre`K+>pGe_1%w9# V6gjt~UI*%7@O1TaS?83{1OSs^8JqwB literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_brightness_percent_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_brightness_percent_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..c3d6a0549b22fdcfb5d417b36af191a7390a96b7 GIT binary patch literal 1630 zcmaKse>~H99LK-LW{h&F2W4XNTWdp@X@-$)BIHN$K&okkH`D-{JdZ9_v`Wb4p-~$Z+@RtCHbL&S{!mj5=ipjv9tH6 zI{-jUDBhkS$KS}S`V%&UBA<1V{F?8OJ&m@Mb`PH#aWc|DxLC&fxUh{P+MQO^*b%i2 zPxBG!P?Og(NbMQSok4AETStt6U3R9owY0Kh!!(6<^bL zOGTBgj-_VBWIr{$hZWbjVjWFm(GUk^BMv^VuQeq~xjHqL4oZ_mmq-YsX4>couaPh* zO^34A9+Z0Ud#me+kpj%WVVLpigS)_R18N8=gXytJ=_@f|!Ai z?QZCq?38$93RCQJ_)&A?Tx+6KlNK&p*>uiV`9$v{VvM<^cd~jGln<16k&*dZxcXV; zRDM+Nyck{6Fh0`_K=jq&yL3#k_PnltNd`DFh&h^!x0zy6T+HQV^A`jpzU7W{sRsrt z1shv4*6q7VJ*2)So+C=6J$)Q2o9-%O7qG7Q@J0kpL@UU6FzE)bNml!XjT8QaQI?_hLTL-SfT06!#_I9AUFY4lO>y|< zZS19)>@(SK4K<5;iFA&Pm`7>~RV`EJvRIVnjkxlfk7!I6_PyH>^(2sTPFY^qAFyp) zJh(^jAj5Yg!8r8^ZD38yP@vLvMPXblRF#zFu4om6BNe%M{m+#TViQ8m*-%kZh-_DG zqE!}gA9zYzTV9@4DU1!$G{{Q@Zd#QI#j46wCMs%W}-OxC(; zt>qYTz97YdFm3*;NcAJvY5tyoLiuV@nmTa3srhn!Wyw63Z+vlF0n-k}RC9(3kmNZJbNW6okXQmC1>|@G)Jdg_R>O zJLMrYR}U#BD687;S_Geah8N1$wrK1aY0g?R%aTzYVU!ggTFU6oyFqcvVa|H=g z!uI>O7fgY*-{gX39;-riGK-WUbksm5|K7hrjIx@$?}dA5rkpPsPZl!Hr$xZN;7TK{ zj}^?f(&CC%=}$yEvEwH$+}7SP;gebO!no_eV~IKvE0bHCJru?}-O-0xj}J&wzjtFU zf!4y(x#Tb-v3c#YGdz?`5UR`0R>g68{u6My*JKI|I zIb_ikw%K%fwuw4@W$g%7!*?#}eAx`$@bl~OAs8ndeqJI)GO}0gJ@B97%0!KjBaiI* z<0<&c<(X;ciBL*26>q`vnPPSysvA}rSd6mKZE4XF`jDbQJwc4;D0=Df9$oz zkwsIz|9eseXY2Bx84u0n39JYTRfRxmu`H3Lcb;f!bDzEa>3WZ?IQEf_hn*Iv8;;dc1H)O;;V7+VT^^biCTvOWg9fZ)xQn z*9{UEqNtMr)Eqs@S4#w)Zmjehao01jd4@-U*GYZGEF*Lhx_M`yAaN6`ta ZL@!Urpy1Vxpia;OP<#Tso4jbJ{sQ1R%Rm4C literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_cart_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_cart_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..47b2f3b0dbbcbbd2fc6b167fee8b40b88fb4d741 GIT binary patch literal 1571 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 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_chevron_down_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_chevron_down_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..f26a8ffa666bae8227d42f43846ebaa3c7979ddd GIT binary patch literal 999 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zcalEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3u<>yq!;n>0brBN4m^nzUKv`P%ar zcdI=2JQp>;SunQmfAA!E&v)DH{MS#4QTaLT$z~2Qm6NCcC^kvw^wxja_;sd)<0SE& zek{>>68|4KzusKfB>PzW;Qj9z*MGPgq>i(T`rCKC;@|_S zjeTvrxn0GdzhcGeo(CO31>5Yb|4)2!QjotiRB(>Lzk{={T|dA($;R&C!gWebg2(JD z?*59pG3m)g4*t@Mf;|@h4z9j--9=ONWZIGTT_-rSdg?c1*5)qXb@>;A@{h=e*6*hU z?bA_gQW0M|W&P4A)u#LQef=8f<@ssBq1(CN_OpoI|Ks^-!`H7HU0JTbYyY^umt)<# z=8x;I-cmNG3zKgASe%+GC}DqnjVjaQ^yN$46aL)ES_u@Wj*enEcK+ZrA)tyaJAuM; za@GNb7tf3a3cq*;6b|-%4-~liRjTo$@yvTl26bxJy;+X=8?OZ#JS$tY@#E#*l|be$ z<4~aJ_AH!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ua371Hn({-St3$R-}zCR@vVh1Q0}MOv3wSt>n=cn z)5e8=+zu3_blvAz5&!H*Z$s5tk^O=J^JhLbXL=Q=QK!6M&mo|)ry)Z71r7F}dSuS@ zEI1QrX5G}|`7BR1PXOw8ncDOJ`Zx9V*_YnCF&iEKZ>ALIcBJ3u{)hPupP%;G|LhYC zn7;Fo|GspVD~iQ`^1s!5zr~@kD&)_}@4XF1UdQLpIm|I5T=%2;b3>+Qfzf|X?(AvU z12m?p>9Ztb)e!DndIb!!#pxXN!*RtMzRDWNP$)c_N@Bh8S9M_!wSl>U&5vBgG`F*jZ z;QgM28;dWo&p7a&XaDDYuXlZ~y!_a+Dnp7*-1`dHf6l_6M@NZRqS+zVv*=EkR|0|C8QIH2x5- zc2oE#W&>og^Yl3U(Y@LCVE2!5Z;+ttk;j+PSIjkMA}LjY6gTYp%UCKgJu9APW+0HO zTH+c}l9E`GYL#4+3Zxi}3=A!G4GndT%tDL|tqhHXsH|GNfhZq+03g>zCvf=;tP;XC`Os`Z)XNyQCCY U%&zSc0_tJ#boFyt=akR{0B7Hn+W-In literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_email_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_email_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8b286912e6f2d5400cd5b0975775799df2b62d GIT binary patch literal 2257 zcmc&#Ygp3B9{vM@S0XYc%}mW3mWt&SZz*^{%}eO!D&h^%%sZN*mSL9C(i)+grAC>t zWg^vXmWC2}bd#;rb<@&FakaF}vWwT_W&if=d^pd3JoC)VJ2Ss|-+AWpymKlzkc8CG z*8l(j=}RV3K$NW)H3V2w6e+48s3dv?cmcreGn&h>aIg=_qL91+aks%+uwz6gQvv`W z-y8tgCjejx9AZxbz+oH!yo&(<{CNPtWYpZ>?*;}CasDJCh+tYINREM^oQ@^!)D>~IqY$mFeq5SPn6;!4|pmf*g8(|^jL5&Bc!=uE&F;--E?m!;wiylqRu1|fhNZlUk5YTGJ1Q?v&wbn=h7{ymJ?QK*C~^^^oNOl{|V zcnw?-gk5Rqfd62>m*}Roti@^#Wjw1GfkX#jY2EG_a|jOS5&MhmAzGs@rxj)pgr)uN za&Q0~CCGe~qiD^#oD?W2#4n7AWJQaMO#a#-N-Iz-TfN>m^QE!voX=eZ&x$styGm=3-R-8biZKG1BvTZq4?#VZbGy|4H{`%bZ$f3E>i9{uc*4r_e6zyLOw)1 zLCp9llavwDirPxn>tv`YCfbULHaK}#NT_aV*r9{1i}3H3dqWE@+S z(lx!i0o0w+=U*Fz5z;V0>^55kFY@^=kT&Qjb0mD8OweIUp3LbUENv0Rl5LWTX(bAq zrum-tL2Oq&KKDXu*Yq#fL5Vy){+BJAnUZJ2o?JZSLp<3q1#NuKe-^yaEc7QiWo#=y zH?@Md6202YnAGu>W4ZLo8+}_B{XlL%VHRKU+99-XVkEIhsDqSKlP%{o9K2py zye~ePoNs9owX^L!KY3EpYxiW%@NnD!JptT=yy+`#hH{MUuOa6ig&rVYBv|ON))7E9eE2W*)4fUn*gfx_J>d$NJET<-3 zO4qBCe1~_`F0=E$yU*osVde_s8<%vmeFy3S$pSxEcXw}9=vtge{XVi)>xirHYhgW? z*b%D~Hja2yQtn62M4+G}Fai7r$H5mRg?8tVw|)&!QnA9Y^%hzy{6_jSa6eBU zYa87}YLtxYetWh_;dOh-j^pRX)Pn=h@f7)8({XA7sryOr6g-Ea0V87XUyO4j1;E&l z61Lu)cZ0lP{8(@TjJpef*+_)o<n+N@qjNwUW7@k4z&j;K}+{N z5kv7yhbHdC_)ISpmOVvX2n0a)1X$Y^yc=&^SI4mFSm8s|LnAs2Aa-~uO){FjWA7ZP zmi#VVfG8m_$#u#!>d=u=0Y# TM4fgLkOTPo1QNyGF~|P~J?`fV literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_gesture_tap_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_gesture_tap_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..bf9a4481434e46ad38762c29ac4d4e8b175cf7f6 GIT binary patch literal 1624 zcmZ{kX;hPE7RR5LYzPPf5hyWM+r)MRnl}az0ha*LL`*~kq((~+LkNVhgrzKzgmElX z5F3^X1c9QpIxMY#h>}dC91w~xN)-hpM6n36T0j;V?dyD-GiUC(_xaubea`>fZ#UbI zw{NY9y$JwdEr;#nk5cfVjPdCE{6exD3ap69WdiU^p6O~R4%Lv%f1ekqMK+A1VpAB~ zp9?_h7625d0a!stiZK8ZVE|qU0C2wmfNiY$)*(;y1AoGIpASkjmc4IhpjZ>j{!)gL z{h!*~8MO-l+(#TAX2912Q)=HVGK+ZTJ;S;qtJrOtn7ie~HV2n!q9xUU8EGZ_AU$oD zon<;S?po{oQ-pIX+sl=CAM4G&zV0E{hgF6B^yWHxcFI|LUiMhT>alsUUDS$Rn>W2f zEzNCz*+#bD=xyz14Tf^QMdE<|-f7{(pNjYfr_vkh|Y!K#~ z(zfPSzQ{@3u$*!aNfIz^U!PHuxg}L+H4A;_*9p=tl5UR$ouJq*dtSTH$9K4Ut3(_l zN~3j_TcvG!$#b*iJ|Ny*Sr+q$;_83@4Dn91R-;~%4a_ekEjZs zW&SDR+r|E9c>3(pYB{>b^CI*@2vGmGhVt-h9{QW^I3yk?^ z9cLY?9dhMu%otx!vnQn+hC@P+izn;I^EcXlRqX71?H#De;-tdMOALZ$AyeWdY#FLF zHi?MSzfxCb^|qI;*b8JT^@vW$?MjjW*htO(RyyBzoTQj2-nBtfsD3uaTf2#Lju)ce zqL^q&d~9N9RHtjp4%Z&WxV|9rU)D4wfjQ;OAUom;{!;qB1DsKeBXlk>LU%~;OA~Sw z<5(W)g)9!)#})(^^={NP>aoP`3-|DcB`Lltb1sIKKV@l)FA(THJs#;{?rD0Ah@Y3X+TR*}JGEzIRS8#Yw+>Q8 zTmSBqro7!^{Z%7Ws3m~y>;bB{ue+m2V^!C?2pEI3aT}rcZKipl1?xgTos%4~f>4#= z5sr7e5#Q9{UI%^VM0-vi4vL~gpMRnKq>|Ki3V+kA9ZL2cxc2;>Yh*_?yyw;MM*on7 zOq0_-797NS+XL@wcXFN_o7vn<+9dSYP||-^b+IU*-(GFCq?uGS!U8Ks+L2z2s$Ele zdCleBb&)~^rMO?pX^L7wjwTB77jfNoy1`0wSCZ@TyU}Y&O<)BloD5A6x`)LJQGjhQ zeTNh5>O^-5fMIu+ZSD*g2eg4-=N~r6l0@z&LS;gy w#CS0X7fXchG#V`-I+hk4DiVi9Il<99x47-n;B)!=^H2}quy{VTUV_ZO0IPt)?EnA( literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_human_greeting_white_24dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_human_greeting_white_24dp.png new file mode 100755 index 0000000000000000000000000000000000000000..a9ac8423ccc89de8993e98a6cc848be6062d44a5 GIT binary patch literal 1620 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 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_information_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_information_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..19be0de61505055062702929a96be09ce764c618 GIT binary patch literal 3520 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 literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_message_white_48dp.png b/Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_message_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..cc37ca735f4638860cfea2039a4cafbd166e3364 GIT binary patch literal 1304 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 N!2Ogbp$;FC`Y*zXC@25` literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_background.xml b/Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..0d025f9bf6 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_foreground.xml b/Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..1f6bb29060 --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/Examples/OneSignalDemoV2/app/src/main/res/mipmap-hdpi/ic_onesignal_launcher.png b/Examples/OneSignalDemoV2/app/src/main/res/mipmap-hdpi/ic_onesignal_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ea135ad29b578e063949f56574e5dbda85685479 GIT binary patch literal 5972 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 LNkvXXu0mjfH9M1X literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/app/src/main/res/mipmap-xxxhdpi/ic_onesignal_launcher.png b/Examples/OneSignalDemoV2/app/src/main/res/mipmap-xxxhdpi/ic_onesignal_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..68bd42f9a62d1784747a821f39b570afe89b30d7 GIT binary patch literal 20157 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 + + #E54B4D + #C13E40 + #E54B4D + + #ECECEC + #3A3A3A + + #3A3A3A + #7A7A7A + + #ECECEC + #E9444E + + #FFFFFF + + #1E88E5 + #47B84C + #FFB300 + #E53935 + diff --git a/Examples/OneSignalDemoV2/app/src/main/res/values/strings.xml b/Examples/OneSignalDemoV2/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..3b257250bc --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/res/values/strings.xml @@ -0,0 +1,132 @@ + + + OneSignal + 77e32082-ea27-42e3-a898-c72e141824ef + + + App + App-Id: + REVOKE CONSENT + LOGIN USER + SWITCH USER + LOGOUT USER + Logged in as: + + + Add your own App ID, then rebuild to fully test all functionality. + Get your keys at onesignal.com + + + %d more available + + + Privacy Consent + Privacy Consent: + Consent given for data collection + + + Aliases + No Aliases Added + ADD ALIAS + ADD ALIASES + REMOVE ALIASES + + + Push + Push-Id: + Enabled + PROMPT PUSH + + + Emails + No Emails Added + ADD EMAIL + New Email + + + SMSs + No SMSs Added + ADD SMS + New SMS + + + Tags + No Tags Added + ADD TAG + ADD TAGS + REMOVE TAGS + + + Outcome Events + SEND OUTCOME + Outcome Type + Outcome Name + Outcome Value + Select an Outcome Type… + Normal Outcome + Unique Outcome + Outcome with Value + SEND + + + In-App Messaging + Pause In-App Messages: + Toggle in-app messages + + + Triggers + No Triggers Added + ADD TRIGGER + ADD TRIGGERS + REMOVE TRIGGERS + CLEAR TRIGGERS + + + Track Event + TRACK EVENT + Event Name + Properties (optional) + {\"ABC\":123} + + + Location + Location Shared: + Location will be shared from device + PROMPT LOCATION + + + Send Push Notification + SIMPLE NOTIFICATION + NOTIFICATION WITH IMAGE + CUSTOM NOTIFICATION + Notification Title + Notification Body + + + Send In-App Message + + + NEXT ACTIVITY + Secondary Activity + + + External User Id + Key + Value + CANCEL + ADD + LOGIN + Remove + Icon + Add Alias + Add Aliases + Add Tag + Add Tags + Add Trigger + Add Triggers + + ADD ROW + Remove Aliases + Remove Tags + Remove Triggers + REMOVE + diff --git a/Examples/OneSignalDemoV2/app/src/main/res/values/styles.xml b/Examples/OneSignalDemoV2/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..bfda9fbacc --- /dev/null +++ b/Examples/OneSignalDemoV2/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/Examples/OneSignalDemoV2/build.gradle.kts b/Examples/OneSignalDemoV2/build.gradle.kts new file mode 100644 index 0000000000..3454c480fb --- /dev/null +++ b/Examples/OneSignalDemoV2/build.gradle.kts @@ -0,0 +1,30 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + mavenCentral() + gradlePluginPortal() + // Huawei maven + maven { url = uri("https://developer.huawei.com/repo/") } + } + dependencies { + classpath("com.android.tools.build:gradle:${Versions.androidGradlePlugin}") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}") + classpath("com.google.gms:google-services:${Versions.googleServices}") + classpath("com.huawei.agconnect:agcp:${Versions.huaweiAgcp}") + } +} + +allprojects { + repositories { + google() + mavenCentral() + // Huawei maven + maven { url = uri("https://developer.huawei.com/repo/") } + } +} + +tasks.register("clean", Delete::class) { + delete(rootProject.layout.buildDirectory) +} diff --git a/Examples/OneSignalDemoV2/buildSrc/build.gradle.kts b/Examples/OneSignalDemoV2/buildSrc/build.gradle.kts new file mode 100644 index 0000000000..61553d8308 --- /dev/null +++ b/Examples/OneSignalDemoV2/buildSrc/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + google() +} diff --git a/Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Dependencies.kt b/Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Dependencies.kt new file mode 100644 index 0000000000..3373508401 --- /dev/null +++ b/Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Dependencies.kt @@ -0,0 +1,52 @@ +object Dependencies { + // Kotlin + const val kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}" + const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" + + // AndroidX + const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}" + const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}" + const val multidex = "androidx.multidex:multidex:${Versions.multidex}" + + // Compose BOM + const val composeBom = "androidx.compose:compose-bom:${Versions.composeBom}" + + // Compose (versions managed by BOM) + const val composeUi = "androidx.compose.ui:ui" + const val composeUiGraphics = "androidx.compose.ui:ui-graphics" + const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview" + const val composeMaterial3 = "androidx.compose.material3:material3" + const val composeMaterialIcons = "androidx.compose.material:material-icons-extended" + const val composeUiTooling = "androidx.compose.ui:ui-tooling" + const val composeRuntime = "androidx.compose.runtime:runtime" + const val composeRuntimeLivedata = "androidx.compose.runtime:runtime-livedata" + + // Activity Compose + const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}" + + // Lifecycle Compose + const val lifecycleViewModelCompose = "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.lifecycleCompose}" + const val lifecycleRuntimeCompose = "androidx.lifecycle:lifecycle-runtime-compose:${Versions.lifecycleCompose}" + + // Lifecycle + const val lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}" + const val lifecycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}" + + // Google Play Services + const val playServicesLocation = "com.google.android.gms:play-services-location:${Versions.playServicesLocation}" + + // Huawei + const val huaweiPush = "com.huawei.hms:push:${Versions.huaweiPush}" + const val huaweiLocation = "com.huawei.hms:location:${Versions.huaweiLocation}" +} + +object Plugins { + const val androidApplication = "com.android.application" + const val kotlinAndroid = "kotlin-android" + const val googleServices = "com.google.gms.google-services" + const val huaweiAgconnect = "com.huawei.agconnect" +} + +object AppConfig { + const val applicationId = "com.onesignal.sdktest" +} diff --git a/Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Versions.kt b/Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 0000000000..3c78dab00c --- /dev/null +++ b/Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,46 @@ +object Versions { + // OneSignal SDK + const val oneSignalSdk = "5.6.1" + + // SDK + const val compileSdk = 34 + const val minSdk = 21 + const val targetSdk = 34 + + // App + const val versionCode = 1 + const val versionName = "1.0" + + // Kotlin + const val kotlin = "1.9.24" + const val coroutines = "1.7.3" + + // Compose + const val composeBom = "2024.02.00" + const val composeCompiler = "1.5.14" + const val activityCompose = "1.8.2" + const val lifecycleCompose = "2.7.0" + + // AndroidX + const val appcompat = "1.5.1" + const val coreKtx = "1.9.0" + const val multidex = "2.0.1" + + // Lifecycle + const val lifecycle = "2.7.0" + + // Material + const val material3 = "1.2.0" + + // Google Play Services + const val playServicesLocation = "21.0.0" + const val googleServices = "4.3.10" + + // Huawei + const val huaweiAgcp = "1.9.1.304" + const val huaweiPush = "6.3.0.304" + const val huaweiLocation = "4.0.0.300" + + // Gradle Plugins + const val androidGradlePlugin = "8.8.2" +} diff --git a/Examples/OneSignalDemoV2/build_app_prompt.md b/Examples/OneSignalDemoV2/build_app_prompt.md new file mode 100644 index 0000000000..00b9bce835 --- /dev/null +++ b/Examples/OneSignalDemoV2/build_app_prompt.md @@ -0,0 +1,945 @@ +# OneSignal Sample App V2 - Build Guide + +This document contains all the prompts and requirements needed to build the OneSignal Sample App V2 from scratch. Give these prompts to an AI assistant or follow them manually to recreate the app. + +--- + +## Phase 1: Initial Setup + +### Prompt 1.1 - Project Foundation + +``` +Build a sample Android app with: +- MVVM architecture with Jetpack Compose UI +- Kotlin Coroutines for background threading (Dispatchers.IO, Dispatchers.Main) +- Gradle Kotlin DSL with buildSrc for type-safe dependency management +- Support for Google FCM and Huawei HMS product flavors (matching existing OneSignalDemo setup) +- Package name: com.onesignal.sdktest (must match google-services.json and agconnect-services.json) +- All dialogs should have EMPTY input fields (for Appium testing - test framework enters values) +- Material3 theming with OneSignal brand colors +``` + +### Prompt 1.2 - OneSignal Code Organization + +``` +Centralize all OneSignal SDK calls in a single OneSignalRepository.kt class: + +User operations: +- loginUser(externalUserId: String) +- logoutUser() + +Alias operations: +- addAlias(label: String, id: String) +- addAliases(aliases: Map) // Batch add + +Email operations: +- addEmail(email: String) +- removeEmail(email: String) + +SMS operations: +- addSms(smsNumber: String) +- removeSms(smsNumber: String) + +Tag operations: +- addTag(key: String, value: String) +- addTags(tags: Map) // Batch add +- removeTag(key: String) +- removeTags(keys: Collection) // Batch remove +- getTags(): Map + +Trigger operations: +- addTrigger(key: String, value: String) +- addTriggers(triggers: Map) // Batch add +- removeTrigger(key: String) +- clearTriggers(keys: Collection) + +Outcome operations: +- sendOutcome(name: String) +- sendUniqueOutcome(name: String) +- sendOutcomeWithValue(name: String, value: Float) + +Track Event: +- trackEvent(name: String, properties: Map?) // Properties as parsed JSON map + +Push subscription: +- getPushSubscriptionId(): String? +- isPushEnabled(): Boolean +- setPushEnabled(enabled: Boolean) + +In-App Messages: +- setInAppMessagesPaused(paused: Boolean) +- isInAppMessagesPaused(): Boolean + +Location: +- setLocationShared(shared: Boolean) +- isLocationShared(): Boolean +- promptLocation() + +Privacy consent: +- setConsentRequired(required: Boolean) +- getConsentRequired(): Boolean +- setPrivacyConsent(granted: Boolean) +- getPrivacyConsent(): Boolean + +Notification sending (via REST API, delegated to OneSignalService): +- sendNotification(type: NotificationType): Boolean +- sendCustomNotification(title: String, body: String): Boolean +- fetchUser(onesignalId: String): UserData? +``` + +### Prompt 1.3 - OneSignalService (REST API Client) + +``` +Create OneSignalService.kt object for REST API calls: + +Properties: +- appId: String (set from MainApplication) + +Methods: +- setAppId(appId: String) +- getAppId(): String +- sendNotification(type: NotificationType): Boolean +- sendCustomNotification(title: String, body: String): Boolean +- fetchUser(onesignalId: String): UserData? + +sendNotification endpoint: +- POST https://onesignal.com/api/v1/notifications +- Accept header: "application/vnd.onesignal.v1+json" +- Uses include_subscription_ids (not include_player_ids) +- Includes big_picture for image notifications + +fetchUser endpoint: +- GET https://api.onesignal.com/apps/{app_id}/users/by/onesignal_id/{onesignal_id} +- NO Authorization header needed (public endpoint) +- Returns UserData with aliases, tags, emails, smsNumbers, externalId +``` + +### Prompt 1.4 - SDK Observers + +``` +In MainApplication.kt, set up OneSignal listeners: +- IInAppMessageLifecycleListener (onWillDisplay, onDidDisplay, onWillDismiss, onDidDismiss) +- IInAppMessageClickListener +- INotificationClickListener +- INotificationLifecycleListener (with preventDefault() for async display testing) +- IUserStateObserver (log when user state changes) +- After registering listeners, restore cached SDK states from SharedPreferences: + - OneSignal.InAppMessages.paused = cached paused status + - OneSignal.Location.isShared = cached location shared status + +In MainViewModel.kt, implement observers: +- IPushSubscriptionObserver - react to push subscription changes +- IPermissionObserver - react to notification permission changes +- IUserStateObserver - call fetchUserDataFromApi() when user changes (login/logout) +``` + +--- + +## Phase 2: UI Sections + +### Section Order (top to bottom) - FINAL + +1. **App Section** (App ID, Guidance Banner, Consent Toggle, Logged-in-as display, Login/Logout) +2. **Push Section** (Push ID, Enabled Toggle, Auto-prompts permission on load) +3. **Send Push Notification Section** (Simple, With Image, Custom buttons) +4. **In-App Messaging Section** (Pause toggle) +5. **Send In-App Message Section** (Top Banner, Bottom Banner, Center Modal, Full Screen - with icons) +6. **Aliases Section** (Add/Add Multiple, read-only list) +7. **Emails Section** (Collapsible list >5 items) +8. **SMS Section** (Collapsible list >5 items) +9. **Tags Section** (Add/Add Multiple/Remove Selected) +10. **Outcome Events Section** (Send Outcome dialog with type selection) +11. **Triggers Section** (Add/Add Multiple/Remove Selected/Clear All - IN MEMORY ONLY) +12. **Track Event Section** (Track Event with JSON validation) +13. **Location Section** (Location Shared toggle, Prompt Location button) +14. **Next Activity Button** + +### Prompt 2.1 - App Section + +``` +App Section layout: + +1. App ID display (readonly Text showing the OneSignal App ID) + +2. Sticky guidance banner below App ID: + - Text: "Add your own App ID, then rebuild to fully test all functionality." + - Link text: "Get your keys at onesignal.com" (clickable, opens browser) + - Light background color to stand out + +3. Consent card with up to two toggles: + a. "Consent Required" toggle (always visible): + - Label: "Consent Required" + - Description: "Require consent before SDK processes data" + - Sets OneSignal.consentRequired + b. "Privacy Consent" toggle (only visible when Consent Required is ON): + - Label: "Privacy Consent" + - Description: "Consent given for data collection" + - Sets OneSignal.consentGiven + - Separated from the above toggle by a horizontal divider + - NOT a blocking overlay - user can interact with app regardless of state + +4. "Logged in as" display (ABOVE the buttons, only visible when logged in): + - Prominent green Card background (#E8F5E9) + - "Logged in as:" label + - External User ID displayed large and centered (bold, green #2E7D32) + - Positioned ABOVE the Login/Switch User button + +5. LOGIN USER button: + - Shows "LOGIN USER" when no user is logged in + - Shows "SWITCH USER" when a user is logged in + - Opens dialog with empty "External User Id" field + +6. LOGOUT USER button +``` + +### Prompt 2.2 - Push Section + +``` +Push Section: +- Section title: "Push" with info icon for tooltip +- Push Subscription ID display (readonly) +- Enabled toggle switch (controls optIn/optOut) +- Notification permission is automatically requested when MainActivity loads +- PROMPT PUSH button: + - Only visible when notification permission is NOT granted (fallback if user denied) + - Requests notification permission when clicked + - Hidden once permission is granted +``` + +### Prompt 2.3 - Send Push Notification Section + +``` +Send Push Notification Section (placed right after Push Section): +- Section title: "Send Push Notification" with info icon for tooltip +- Three buttons: + 1. SIMPLE - sends basic notification with title/body + 2. WITH IMAGE - sends notification with big picture + (use https://media.onesignal.com/automated_push_templates/ratings_template.png) + 3. CUSTOM - opens dialog for custom title and body + +Tooltip should explain each button type. +``` + +### Prompt 2.4 - In-App Messaging Section + +``` +In-App Messaging Section (placed right after Send Push): +- Section title: "In-App Messaging" with info icon for tooltip +- Pause In-App Messages toggle switch: + - Label: "Pause In-App Messages" + - Description: "Toggle in-app message display" +``` + +### Prompt 2.5 - Send In-App Message Section + +``` +Send In-App Message Section (placed right after In-App Messaging): +- Section title: "Send In-App Message" with info icon for tooltip +- Four FULL-WIDTH buttons (not a grid): + 1. TOP BANNER - VerticalAlignTop icon, trigger: "iam_type" = "top_banner" + 2. BOTTOM BANNER - VerticalAlignBottom icon, trigger: "iam_type" = "bottom_banner" + 3. CENTER MODAL - CropSquare icon, trigger: "iam_type" = "center_modal" + 4. FULL SCREEN - Fullscreen icon, trigger: "iam_type" = "full_screen" +- Button styling: + - RED background color (#E9444E) + - WHITE text + - Type-specific icon on LEFT side only (no right side icon) + - Full width of the card + - Left-aligned text and icon content (not centered) + - UPPERCASE button text +- On click: adds trigger "iam_type" with the type's value and shows toast "Sent In-App Message: {type}" + +Tooltip should explain each IAM type. +``` + +### Prompt 2.6 - Aliases Section + +``` +Aliases Section (placed after Send In-App Message): +- Section title: "Aliases" with info icon for tooltip +- Compose list showing key-value pairs (read-only, no delete icons) +- Each item shows: Label | ID +- Filter out "external_id" and "onesignal_id" from display (these are special) +- "No Aliases Added" text when empty +- ADD button -> PairInputDialog with empty Label and ID fields (single add) +- ADD MULTIPLE button -> MultiPairInputDialog (dynamic rows, add/remove) +- No remove/delete functionality (aliases are add-only from the UI) +``` + +### Prompt 2.7 - Emails Section + +``` +Emails Section: +- Section title: "Emails" with info icon for tooltip +- Compose list showing email addresses +- Each item shows email with delete icon +- "No Emails Added" text when empty +- ADD EMAIL button -> dialog with empty email field +- Collapse behavior when >5 items: + - Show first 5 items + - Show "X more" text (clickable) + - Expand to show all when clicked +``` + +### Prompt 2.8 - SMS Section + +``` +SMS Section: +- Section title: "SMS" with info icon for tooltip +- Compose list showing phone numbers +- Each item shows phone number with delete icon +- "No SMS Added" text when empty +- ADD SMS button -> dialog with empty SMS field +- Collapse behavior when >5 items (same as Emails) +``` + +### Prompt 2.9 - Tags Section + +``` +Tags Section: +- Section title: "Tags" with info icon for tooltip +- Compose list showing key-value pairs +- Each item shows: Key | Value with delete icon +- "No Tags Added" text when empty +- ADD button -> PairInputDialog with empty Key and Value fields (single add) +- ADD MULTIPLE button -> MultiPairInputDialog (dynamic rows) +- REMOVE SELECTED button: + - Only visible when at least one tag exists + - Opens MultiSelectRemoveDialog with checkboxes +``` + +### Prompt 2.10 - Outcome Events Section + +``` +Outcome Events Section: +- Section title: "Outcome Events" with info icon for tooltip +- SEND OUTCOME button -> opens dialog with 3 radio options: + 1. Normal Outcome -> shows name input field + 2. Unique Outcome -> shows name input field + 3. Outcome with Value -> shows name and value (float) input fields +``` + +### Prompt 2.11 - Triggers Section (IN MEMORY ONLY) + +``` +Triggers Section: +- Section title: "Triggers" with info icon for tooltip +- Compose list showing key-value pairs +- Each item shows: Key | Value with delete icon +- "No Triggers Added" text when empty +- ADD button -> PairInputDialog with empty Key and Value fields (single add) +- ADD MULTIPLE button -> MultiPairInputDialog (dynamic rows) +- Two action buttons (only visible when triggers exist): + - REMOVE SELECTED -> MultiSelectRemoveDialog with checkboxes + - CLEAR ALL -> Removes all triggers at once + +IMPORTANT: Triggers are stored IN MEMORY ONLY during the app session. +- triggersList is a mutableListOf>() in MainViewModel +- Triggers are NOT persisted to SharedPreferences +- Triggers are cleared when the app is killed/restarted +- This is intentional - triggers are transient test data for IAM testing +``` + +### Prompt 2.12 - Track Event Section + +``` +Track Event Section: +- Section title: "Track Event" with info icon for tooltip +- TRACK EVENT button -> opens TrackEventDialog with: + - "Event Name" label + empty input field (required, shows error if empty on submit) + - "Properties (optional, JSON)" label + input field with placeholder hint {"key": "value"} + - If non-empty and not valid JSON, shows "Invalid JSON format" error on the field + - If valid JSON, parsed via JSONObject and converted to Map for the SDK call + - If empty, passes null + - TRACK button disabled until name is filled AND JSON is valid (or empty) +- Calls OneSignal.User.trackEvent(name, properties) +``` + +### Prompt 2.13 - Location Section + +``` +Location Section: +- Section title: "Location" with info icon for tooltip +- Location Shared toggle switch: + - Label: "Location Shared" + - Description: "Share device location with OneSignal" +- PROMPT LOCATION button +``` + +### Prompt 2.14 - Secondary Activity + +``` +Secondary Activity (launched by "Next Activity" button at bottom of main screen): +- Activity title: "Secondary Activity" +- Page content: centered text "Secondary Activity" using headlineMedium style +- Simple screen, no additional functionality needed +``` + +--- + +## Phase 3: View User API Integration + +### Prompt 3.1 - Data Loading Flow + +``` +Loading indicator overlay: +- Full-screen semi-transparent overlay with centered spinner +- isLoading LiveData in MainViewModel +- Show/hide based on isLoading state +- IMPORTANT: Add 100ms delay after populating data before dismissing loading indicator + - This ensures UI has time to render + - Use kotlinx.coroutines.delay(100) after setting all LiveData values + +On cold start: +- Check if OneSignal.User.onesignalId is not null +- If exists: show loading -> call fetchUserDataFromApi() -> populate UI -> delay 100ms -> hide loading +- If null: just show empty state (no loading indicator) + +On login (LOGIN USER / SWITCH USER): +- Show loading indicator immediately +- Call OneSignal.login(externalUserId) +- Clear old user data (aliases, emails, sms, triggers) +- Wait for onUserStateChange callback +- onUserStateChange calls fetchUserDataFromApi() +- fetchUserDataFromApi() populates UI, delays 100ms, then hides loading + +On logout: +- Show loading indicator +- Call OneSignal.logout() +- Clear local lists (aliases, emails, sms, triggers) +- Hide loading indicator + +On onUserStateChange callback: +- Call fetchUserDataFromApi() to sync with server state +- Update UI with new data (aliases, tags, emails, sms) + +Note: REST API key is NOT required for fetchUser endpoint. +``` + +### Prompt 3.2 - UserData Model + +``` +data class UserData( + val aliases: Map, // From identity object (filter out external_id, onesignal_id) + val tags: Map, // From properties.tags object + val emails: List, // From subscriptions where type="Email" -> token + val smsNumbers: List, // From subscriptions where type="SMS" -> token + val externalId: String? // From identity.external_id +) +``` + +--- + +## Phase 4: Info Tooltips + +### Prompt 4.1 - Tooltip Content (Remote) + +``` +Tooltip content is fetched at runtime from the sdk-shared repo. Do NOT bundle a local copy. + +URL: +https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/tooltip_content.json + +This file is maintained in the sdk-shared repo and shared across all platform demo apps. +``` + +### Prompt 4.2 - Tooltip Helper + +``` +Create TooltipHelper.kt: + +object TooltipHelper { + private var tooltips: Map = emptyMap() + private var initialized = false + + private const val TOOLTIP_URL = + "https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/tooltip_content.json" + + fun init(context: Context) { + if (initialized) return + + // IMPORTANT: Fetch on background thread to avoid blocking app startup + CoroutineScope(Dispatchers.IO).launch { + // Fetch tooltip_content.json from TOOLTIP_URL using HttpURLConnection + // Parse JSON into tooltips map + // On failure (no network, etc.), leave tooltips empty β€” tooltips are non-critical + + withContext(Dispatchers.Main) { + // Update tooltips map on main thread + initialized = true + } + } + } + + fun getTooltip(key: String): TooltipData? +} + +data class TooltipData( + val title: String, + val description: String, + val options: List? = null +) + +data class TooltipOption( + val name: String, + val description: String +) +``` + +### Prompt 4.3 - Tooltip UI Integration (Compose) + +``` +For each section, pass an onInfoClick callback to SectionCard: +- SectionCard has an optional info icon that calls onInfoClick when tapped +- In MainScreen, wire onInfoClick to show a TooltipDialog composable +- TooltipDialog displays title, description, and options (if present) + +Example in MainScreen.kt: +AliasesSection( + ..., + onInfoClick = { showTooltipDialog = "aliases" } +) + +showTooltipDialog?.let { key -> + val tooltip = TooltipHelper.getTooltip(key) + if (tooltip != null) { + TooltipDialog( + title = tooltip.title, + description = tooltip.description, + options = tooltip.options?.map { it.name to it.description }, + onDismiss = { showTooltipDialog = null } + ) + } +} +``` + +--- + +## Phase 5: Data Persistence & Initialization + +### What IS Persisted (SharedPreferences) + +``` +SharedPreferenceUtil.kt stores: +- OneSignal App ID +- Consent required status +- Privacy consent status +- External user ID (for login state restoration) +- Location shared status +- In-app messaging paused status +``` + +### Initialization Flow + +``` +On app startup, state is restored in two layers: + +1. MainApplication.kt restores SDK state from SharedPreferences cache BEFORE init: + - OneSignal.consentRequired = SharedPreferenceUtil.getCachedConsentRequired(context) + - OneSignal.consentGiven = SharedPreferenceUtil.getUserPrivacyConsent(context) + - OneSignal.initWithContext(this, appId) + Then AFTER init, restores remaining SDK state: + - OneSignal.InAppMessages.paused = SharedPreferenceUtil.getCachedInAppMessagingPausedStatus(context) + - OneSignal.Location.isShared = SharedPreferenceUtil.getCachedLocationSharedStatus(context) + This ensures consent settings are in place before the SDK initializes. + +2. MainViewModel.loadInitialState() reads UI state from the SDK (not SharedPreferences): + - _consentRequired from repository.getConsentRequired() (reads OneSignal.consentRequired) + - _privacyConsentGiven from repository.getPrivacyConsent() (reads OneSignal.consentGiven) + - _inAppMessagesPaused from repository.isInAppMessagesPaused() (reads OneSignal.InAppMessages.paused) + - _locationShared from repository.isLocationShared() (reads OneSignal.Location.isShared) + - _externalUserId from OneSignal.User.externalId (empty string means no user logged in) + - _appId from SharedPreferenceUtil (app-level config, no SDK getter) + +This two-layer approach ensures: +- The SDK is configured with the user's last preferences before anything else runs +- The ViewModel reads the SDK's actual state as the source of truth for the UI +- The UI always reflects what the SDK reports, not stale cache values +``` + +### What is NOT Persisted (In-Memory Only) + +``` +MainViewModel holds in memory: +- triggersList: MutableList> + - Triggers are session-only + - Cleared on app restart + - Used for testing IAM trigger conditions + +- aliasesList: + - Populated from REST API on each session start + - When user adds alias locally, added to list immediately (SDK syncs async) + - Fetched fresh via fetchUserDataFromApi() on login/app start + +- emailsList, smsNumbersList: + - Populated from REST API on each session + - Not cached locally + - Fetched fresh via fetchUserDataFromApi() + +- tagsList: + - Can be read from SDK via getTags() + - Also fetched from API for consistency +``` + +--- + +## Phase 6: Testing Values (Appium Compatibility) + +``` +All dialog input fields should be EMPTY by default. +The test automation framework (Appium) will enter these values: + +- Login Dialog: External User Id = "test" +- Add Alias Dialog: Key = "Test", Value = "Value" +- Add Multiple Aliases Dialog: Key = "Test", Value = "Value" (first row; supports multiple rows) +- Add Email Dialog: Email = "test@onesignal.com" +- Add SMS Dialog: SMS = "123-456-5678" +- Add Tag Dialog: Key = "Test", Value = "Value" +- Add Multiple Tags Dialog: Key = "Test", Value = "Value" (first row; supports multiple rows) +- Add Trigger Dialog: Key = "trigger_key", Value = "trigger_value" +- Add Multiple Triggers Dialog: Key = "trigger_key", Value = "trigger_value" (first row; supports multiple rows) +- Outcome Dialog: Name = "test_outcome", Value = "1.5" +- Track Event Dialog: Name = "test_event", Properties = "{\"key\": \"value\"}" +- Custom Notification Dialog: Title = "Test Title", Body = "Test Body" +``` + +--- + +## Phase 7: Important Implementation Details + +### Alias Management + +``` +Aliases are managed with a hybrid approach: + +1. On app start/login: Fetched from REST API via fetchUserDataFromApi() +2. When user adds alias locally: + - Call OneSignal.User.addAlias(label, id) - syncs to server async + - Immediately add to local aliasesList (don't wait for API) + - This ensures instant UI feedback while SDK syncs in background +3. On next app launch: Fresh data from API includes the synced alias +``` + +### Notification Permission + +``` +Notification permission is automatically requested when MainActivity loads: +- Call viewModel.promptPush() at end of onCreate() +- This ensures prompt appears after user sees the app UI +- PROMPT PUSH button remains as fallback if user initially denied +- Button hidden once permission is granted +``` + +--- + +## Phase 8: Jetpack Compose Architecture + +### Prompt 8.1 - Compose Setup + +``` +Enable Jetpack Compose in the project: + +build.gradle.kts (app): +- buildFeatures { compose = true } +- composeOptions { kotlinCompilerExtensionVersion = "1.5.10" } + +Dependencies (via BOM): +- composeBom = "2024.02.00" +- composeUi, composeUiGraphics, composeUiToolingPreview +- composeMaterial3 +- composeMaterialIconsExtended (for IAM type icons) +- composeRuntime, composeRuntimeLivedata +- activityCompose +- lifecycleViewModelCompose, lifecycleRuntimeCompose +``` + +### Prompt 8.2 - Reusable Components + +``` +Create reusable Compose components in ui/components/: + +SectionCard.kt: +- Card with title text and optional info icon +- Column content slot +- OnInfoClick callback for tooltips + +ToggleRow.kt: +- Label, optional description, Switch +- Horizontal layout with space between + +ActionButton.kt: +- PrimaryButton (filled, primary color background) +- DestructiveButton (outlined, red accent) +- Full-width buttons for consistent styling + +ListComponents.kt: +- PairItem (key-value with delete icon) +- SingleItem (single value with delete icon) +- EmptyState (centered "No items" text) +- CollapsibleSingleList (shows 5, expandable) +- PairList (simple list of pairs) + +LoadingOverlay.kt: +- Semi-transparent full-screen overlay +- Centered CircularProgressIndicator +- Shown via isLoading state + +Dialogs.kt: +- SingleInputDialog (one text field) +- PairInputDialog (key-value fields, single pair) +- MultiPairInputDialog (dynamic rows, add/remove, batch submit) +- MultiSelectRemoveDialog (checkboxes for batch remove) +- LoginDialog, OutcomeDialog, TrackEventDialog +- CustomNotificationDialog, TooltipDialog +``` + +### Prompt 8.3 - Reusable Multi-Pair Dialog (Compose) + +``` +Tags, Aliases, and Triggers all share a reusable MultiPairInputDialog composable +for adding multiple key-value pairs at once. + +Behavior: +- Dialog opens with one empty key-value row +- "Add Row" button below the rows adds another empty row +- Each row has a remove button (hidden when only one row exists) +- "Add All" button is disabled until ALL key and value fields in every row are filled +- Validation runs on every text change and after row add/remove +- On "Add All" press, all rows are collected and submitted as a batch +- Batch operations use SDK bulk APIs (addAliases, addTags, addTriggers) + +Used by: +- ADD MULTIPLE button (Aliases section) -> calls viewModel.addAliases(pairs) +- ADD MULTIPLE button (Tags section) -> calls viewModel.addTags(pairs) +- ADD MULTIPLE button (Triggers section) -> calls viewModel.addTriggers(pairs) +``` + +### Prompt 8.4 - Reusable Remove Multi Dialog (Compose) + +``` +Aliases, Tags, and Triggers share a reusable MultiSelectRemoveDialog composable +for selectively removing items from the current list. + +Behavior: +- Accepts the current list of items as List> +- Renders one Checkbox per item on the left with just the key as the label (not "key: value") +- User can check 0, 1, or more items +- "Remove (N)" button shows count of selected items, disabled when none selected +- On confirm, checked items' keys are collected as Collection and passed to the callback + +Used by: +- REMOVE SELECTED button (Tags section) -> calls viewModel.removeSelectedTags(keys) +- REMOVE SELECTED button (Triggers section) -> calls viewModel.removeSelectedTriggers(keys) +``` + +### Prompt 8.5 - Theme + +``` +Create OneSignal theme in ui/theme/Theme.kt: + +Colors: +- OneSignalRed = #E54B4D (primary) +- OneSignalGreen = #34A853 (success) +- OneSignalGreenLight = #E6F4EA (success background) +- LightBackground = #F8F9FA +- CardBackground = White +- DividerColor = #E8EAED +- WarningBackground = #FFF8E1 + +OneSignalTheme composable: +- MaterialTheme with LightColorScheme +- Custom Typography with SemiBold weights +- Custom Shapes with rounded corners (8/12/16/24dp) +- Primary = OneSignalRed +- Surface variants for cards +``` + +### Prompt 8.6 - Log View (Appium-Ready) + +``` +Add collapsible log view at top of screen for debugging and Appium testing. + +Files: +- util/LogManager.kt - Thread-safe pass-through logger +- ui/components/LogView.kt - Compose UI with test tags + +LogManager Features: +- Pass-through to Android logcat AND UI display +- Thread-safe (posts to main thread for Compose state) +- Captures SDK logs via OneSignal.Debug.addLogListener +- API: LogManager.d/i/w/e(tag, message) mimics android.util.Log + +LogView Features: +- Collapsible header (default expanded) +- 5-line height (~100dp) +- Color-coded by level (Debug=blue, Info=green, Warn=amber, Error=red) +- Clear button +- Auto-scroll to newest + +Appium Test Tags: +| Tag | Description | +|-----|-------------| +| log_view_container | Main container | +| log_view_header | Clickable expand/collapse | +| log_view_count | Shows "(N)" log count | +| log_view_clear_button | Clear all logs | +| log_view_list | Scrollable LazyColumn | +| log_view_empty | "No logs yet" state | +| log_entry_N | Each log row (N=index) | +| log_entry_N_timestamp | Timestamp text | +| log_entry_N_level | D/I/W/E indicator | +| log_entry_N_message | Log message content | + +SDK Log Integration (MainApplication): +OneSignal.Debug.addLogListener { event -> + LogManager.log("SDK", event.entry, level) +} + +Appium Example: +# Verify a log message exists +log_msg = driver.find_element(By.XPATH, "//*[@resource-id='log_entry_0_message']") +assert "Notification sent" in log_msg.text + +# Scroll logs +log_list = driver.find_element(By.XPATH, "//*[@resource-id='log_view_list']") +driver.execute_script("mobile: scroll", {"element": log_list, "direction": "down"}) +``` + +### Prompt 8.7 - Toast Messages + +``` +All user actions should display toast messages: + +- Login: "Logged in as: {userId}" +- Logout: "Logged out" +- Add alias: "Alias added: {label}" +- Add multiple aliases: "{count} alias(es) added" +- Similar patterns for tags, triggers, emails, SMS +- Notifications: "Notification sent: {type}" or "Failed to send notification" +- In-App Messages: "Sent In-App Message: {type}" +- Outcomes: "Outcome sent: {name}" +- Events: "Event tracked: {name}" +- Location: "Location sharing enabled/disabled" +- Push: "Push enabled/disabled" + +Implementation: +- MainViewModel has toastMessage: LiveData +- MainActivity observes and shows Android Toast +- LaunchedEffect triggers on toastMessage change +- All toast messages are also logged via LogManager.info() +``` + +--- + +## Key Files Structure + +``` +Examples/OneSignalDemoV2/ +β”œβ”€β”€ buildSrc/ +β”‚ └── src/main/kotlin/ +β”‚ β”œβ”€β”€ Versions.kt # Version constants (includes Compose versions) +β”‚ └── Dependencies.kt # Dependency strings (includes Compose deps) +β”œβ”€β”€ app/ +β”‚ β”œβ”€β”€ src/main/ +β”‚ β”‚ β”œβ”€β”€ java/com/onesignal/sdktest/ +β”‚ β”‚ β”‚ β”œβ”€β”€ application/ +β”‚ β”‚ β”‚ β”‚ └── MainApplication.kt # SDK init, log listener, observers +β”‚ β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ NotificationType.kt # With bigPicture URL +β”‚ β”‚ β”‚ β”‚ β”‚ └── InAppMessageType.kt # With Material icons +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ network/ +β”‚ β”‚ β”‚ β”‚ β”‚ └── OneSignalService.kt # REST API client +β”‚ β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ β”‚ └── OneSignalRepository.kt +β”‚ β”‚ β”‚ β”œβ”€β”€ ui/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ components/ # Reusable Compose components +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SectionCard.kt # Card with title and info icon +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ToggleRow.kt # Label + Switch +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ActionButton.kt # Primary/Destructive buttons +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ListComponents.kt # PairList, SingleList, EmptyState +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ LoadingOverlay.kt # Full-screen loading spinner +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ LogView.kt # Collapsible log viewer (Appium-ready) +β”‚ β”‚ β”‚ β”‚ β”‚ └── Dialogs.kt # All dialog composables +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ main/ +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MainActivity.kt # ComponentActivity with setContent +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ MainScreen.kt # Main Compose screen (includes LogView) +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Sections.kt # Individual section composables +β”‚ β”‚ β”‚ β”‚ β”‚ └── MainViewModel.kt # With batch operations +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ secondary/ +β”‚ β”‚ β”‚ β”‚ β”‚ └── SecondaryActivity.kt # Simple Compose screen +β”‚ β”‚ β”‚ β”‚ └── theme/ +β”‚ β”‚ β”‚ β”‚ └── Theme.kt # OneSignal Material3 theme +β”‚ β”‚ β”‚ └── util/ +β”‚ β”‚ β”‚ β”œβ”€β”€ SharedPreferenceUtil.kt +β”‚ β”‚ β”‚ β”œβ”€β”€ LogManager.kt # Thread-safe pass-through logger +β”‚ β”‚ β”‚ └── TooltipHelper.kt # Fetches tooltips from remote URL +β”‚ β”‚ └── res/ +β”‚ β”‚ └── values/ +β”‚ β”‚ β”œβ”€β”€ strings.xml +β”‚ β”‚ β”œβ”€β”€ colors.xml +β”‚ β”‚ └── styles.xml +β”‚ └── src/huawei/ +β”‚ └── java/com/onesignal/sdktest/notification/ +β”‚ └── HmsMessageServiceAppLevel.kt +β”œβ”€β”€ google-services.json +β”œβ”€β”€ agconnect-services.json +└── build_app_prompt.md (this file) +``` + +Note: + +- All UI is Jetpack Compose (no XML layouts) +- Tooltip content is fetched from remote URL (not bundled locally) +- LogView at top of screen displays SDK and app logs for debugging/Appium testing + +--- + +## Configuration + +### strings.xml Placeholders + +```xml + +YOUR_APP_ID_HERE +``` + +Note: REST API key is NOT required for the fetchUser endpoint. + +### Package Name + +The package name MUST be `com.onesignal.sdktest` to work with the existing: + +- `google-services.json` (Firebase configuration) +- `agconnect-services.json` (Huawei configuration) + +If you change the package name, you must also update these files with your own Firebase/Huawei project configuration. + +--- + +## Summary + +This app demonstrates all OneSignal Android SDK features: + +- User management (login/logout, aliases with batch add) +- Push notifications (subscription, sending with images, auto-permission prompt) +- Email and SMS subscriptions +- Tags for segmentation (batch add/remove support) +- Triggers for in-app message targeting (in-memory only, batch operations) +- Outcomes for conversion tracking +- Event tracking with JSON properties validation +- In-app messages (display testing with type-specific icons) +- Location sharing +- Privacy consent management + +The app is designed to be: + +1. **Testable** - Empty dialogs for Appium automation +2. **Comprehensive** - All SDK features demonstrated +3. **Clean** - MVVM architecture with Jetpack Compose UI +4. **Cross-platform ready** - Tooltip content in JSON for sharing across wrappers +5. **Session-based triggers** - Triggers stored in memory only, cleared on restart +6. **Responsive UI** - Loading indicator with delay to ensure UI populates before dismissing +7. **Performant** - Tooltip JSON loaded on background thread +8. **Modern UI** - Material3 theming with reusable Compose components +9. **Batch Operations** - Add multiple items at once, select and remove multiple items diff --git a/Examples/OneSignalDemoV2/gradle.properties b/Examples/OneSignalDemoV2/gradle.properties new file mode 100644 index 0000000000..a03b354896 --- /dev/null +++ b/Examples/OneSignalDemoV2/gradle.properties @@ -0,0 +1,21 @@ +# 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. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# 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 +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.jar b/Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.properties b/Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..18330fcba8 --- /dev/null +++ b/Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Examples/OneSignalDemoV2/gradlew b/Examples/OneSignalDemoV2/gradlew new file mode 100755 index 0000000000..c53aefaa5f --- /dev/null +++ b/Examples/OneSignalDemoV2/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Examples/OneSignalDemoV2/gradlew.bat b/Examples/OneSignalDemoV2/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/Examples/OneSignalDemoV2/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/Examples/OneSignalDemoV2/settings.gradle.kts b/Examples/OneSignalDemoV2/settings.gradle.kts new file mode 100644 index 0000000000..11dfb5ad57 --- /dev/null +++ b/Examples/OneSignalDemoV2/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "OneSignalDemoV2" +include(":app") From 91925cb1930cc875e9f83cf911187f8ef8f030ed Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 17 Feb 2026 14:54:13 -0800 Subject: [PATCH 17/19] chore: Removes outdated example app (#2548) --- .github/workflows/create-release-pr.yml | 2 +- .gitignore | 4 +- Examples/OneSignalDemo/.gitignore | 8 - Examples/OneSignalDemo/app/build.gradle | 122 -- .../app/src/huawei/AndroidManifest.xml | 20 - .../HmsMessageServiceAppLevel.java | 62 - .../app/src/main/AndroidManifest.xml | 91 -- .../app/src/main/assets/api_key.txt | 2 - .../app/src/main/assets/fonts/Sarala-Bold.ttf | Bin 224888 -> 0 bytes .../src/main/assets/fonts/Sarala-Regular.ttf | Bin 227208 -> 0 bytes .../sdktest/activity/MainActivity.java | 48 - .../sdktest/activity/SecondaryActivity.java | 16 - .../sdktest/activity/SplashActivity.java | 28 - .../EnumSelectionRecyclerViewAdapter.java | 94 -- .../InAppMessageRecyclerViewAdapter.java | 123 -- .../NotificationRecyclerViewAdapter.java | 120 -- .../adapter/PairRecyclerViewAdapter.java | 104 -- .../adapter/SingleRecyclerViewAdapter.java | 99 -- .../SubscriptionRecyclerViewAdapter.java | 105 -- .../sdktest/application/MainApplication.java | 155 -- .../sdktest/application/MainApplicationKT.kt | 157 -- .../callback/AddPairAlertDialogCallback.java | 10 - .../callback/EnumSelectionCallback.java | 7 - .../callback/PairItemActionCallback.java | 7 - .../SendOutcomeAlertDialogCallback.java | 10 - .../callback/SingleItemActionCallback.java | 7 - .../SubscriptionItemActionCallback.java | 9 - .../callback/UpdateAlertDialogCallback.java | 8 - .../com/onesignal/sdktest/constant/Tag.java | 5 - .../com/onesignal/sdktest/constant/Text.java | 32 - .../sdktest/model/ActivityViewModel.java | 54 - .../sdktest/model/MainActivityViewModel.java | 937 ------------ .../model/SplashActivityViewModel.java | 147 -- .../notification/NotificationData.java | 194 --- .../NotificationServiceExtension.java | 28 - .../OneSignalNotificationSender.java | 95 -- .../onesignal/sdktest/type/InAppMessage.java | 26 - .../onesignal/sdktest/type/Notification.java | 129 -- .../onesignal/sdktest/type/OutcomeEvent.java | 32 - .../com/onesignal/sdktest/type/ToastType.java | 28 - .../sdktest/ui/CustomAlertDialogBuilder.java | 195 --- .../sdktest/ui/FixAppBarLayoutBehavior.java | 45 - .../sdktest/ui/RecyclerViewBuilder.java | 34 - .../com/onesignal/sdktest/util/Animate.java | 18 - .../com/onesignal/sdktest/util/Dialog.java | 374 ----- .../java/com/onesignal/sdktest/util/Font.java | 28 - .../com/onesignal/sdktest/util/IntentTo.java | 30 - .../onesignal/sdktest/util/InterfaceUtil.java | 31 - .../onesignal/sdktest/util/ProfileUtil.java | 148 -- .../sdktest/util/SharedPreferenceUtil.java | 65 - .../com/onesignal/sdktest/util/Toaster.java | 57 - .../java/com/onesignal/sdktest/util/Util.java | 28 - .../app/src/main/res/anim/fade_in.xml | 5 - .../app/src/main/res/anim/fade_out.xml | 5 - .../ripple_selector_red_white.xml | 11 - .../ripple_selector_white_red.xml | 11 - .../app/src/main/res/drawable/divider.xml | 6 - .../app/src/main/res/drawable/no_divider.xml | 6 - .../drawable/ripple_selector_red_white.xml | 10 - .../drawable/ripple_selector_white_red.xml | 11 - .../main/res/layout/activity_secondary.xml | 8 - .../layout/add_pair_alert_dialog_layout.xml | 69 - ...um_selection_recycler_view_item_layout.xml | 25 - .../main/res/layout/main_activity_layout.xml | 1275 ----------------- ...app_messages_recycler_view_item_layout.xml | 76 - ...otifications_recycler_view_item_layout.xml | 76 - .../layout/pair_recycler_view_item_layout.xml | 53 - .../send_outcome_alert_dialog_layout.xml | 145 -- .../single_recycler_view_item_layout.xml | 35 - .../res/layout/splash_activity_layout.xml | 16 - ...subscription_recycler_view_item_layout.xml | 83 -- .../res/layout/title_body_notification.xml | 29 - .../res/layout/toaster_toast_card_layout.xml | 53 - .../res/layout/update_alert_dialog_layout.xml | 37 - .../app/src/main/res/values/colors.xml | 23 - .../app/src/main/res/values/strings.xml | 69 - .../app/src/main/res/values/styles.xml | 48 - Examples/OneSignalDemo/build.gradle | 40 - Examples/OneSignalDemo/gradle.properties | 20 - Examples/OneSignalDemo/settings.gradle | 1 - .../app/agconnect-services.json | 96 -- .../OneSignalDemoV2/app/google-services.json | 30 - .../OneSignalDemoV2/app/proguard-rules.pro | 21 - .../ic_alert_octagon_white_48dp.png | Bin 763 -> 0 bytes .../res/drawable-hdpi/ic_alert_white_48dp.png | Bin 1178 -> 0 bytes .../res/drawable-hdpi/ic_bell_white_24dp.png | Bin 635 -> 0 bytes .../ic_brightness_percent_white_24dp.png | Bin 772 -> 0 bytes .../res/drawable-hdpi/ic_cart_white_24dp.png | Bin 714 -> 0 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 1525 -> 0 bytes .../ic_chevron_down_white_48dp.png | Bin 588 -> 0 bytes .../ic_chevron_up_white_48dp.png | Bin 578 -> 0 bytes .../res/drawable-hdpi/ic_email_white_48dp.png | Bin 1017 -> 0 bytes .../ic_gesture_tap_white_24dp.png | Bin 798 -> 0 bytes .../ic_human_greeting_white_24dp.png | Bin 751 -> 0 bytes .../res/drawable-hdpi/ic_image_white_24dp.png | Bin 649 -> 0 bytes .../ic_information_white_48dp.png | Bin 1352 -> 0 bytes .../ic_map_marker_white_24dp.png | Bin 793 -> 0 bytes .../drawable-hdpi/ic_message_white_48dp.png | Bin 675 -> 0 bytes .../drawable-hdpi/ic_newspaper_white_24dp.png | Bin 576 -> 0 bytes .../res/drawable-hdpi/ic_star_white_24dp.png | Bin 764 -> 0 bytes .../ic_alert_octagon_white_48dp.png | Bin 621 -> 0 bytes .../res/drawable-mdpi/ic_alert_white_48dp.png | Bin 902 -> 0 bytes .../res/drawable-mdpi/ic_bell_white_24dp.png | Bin 508 -> 0 bytes .../ic_brightness_percent_white_24dp.png | Bin 646 -> 0 bytes .../res/drawable-mdpi/ic_cart_white_24dp.png | Bin 544 -> 0 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 994 -> 0 bytes .../ic_chevron_down_white_48dp.png | Bin 510 -> 0 bytes .../ic_chevron_up_white_48dp.png | Bin 500 -> 0 bytes .../res/drawable-mdpi/ic_email_white_48dp.png | Bin 757 -> 0 bytes .../ic_gesture_tap_white_24dp.png | Bin 578 -> 0 bytes .../ic_human_greeting_white_24dp.png | Bin 588 -> 0 bytes .../res/drawable-mdpi/ic_image_white_24dp.png | Bin 506 -> 0 bytes .../ic_information_white_48dp.png | Bin 940 -> 0 bytes .../ic_map_marker_white_24dp.png | Bin 604 -> 0 bytes .../drawable-mdpi/ic_message_white_48dp.png | Bin 537 -> 0 bytes .../drawable-mdpi/ic_newspaper_white_24dp.png | Bin 472 -> 0 bytes .../res/drawable-mdpi/ic_star_white_24dp.png | Bin 609 -> 0 bytes .../drawable-nodpi/onesignal_rectangle.png | Bin 18638 -> 0 bytes .../res/drawable-nodpi/onesignal_square.png | Bin 99328 -> 0 bytes .../ic_alert_octagon_white_48dp.png | Bin 994 -> 0 bytes .../drawable-xhdpi/ic_alert_white_48dp.png | Bin 1402 -> 0 bytes .../res/drawable-xhdpi/ic_bell_white_24dp.png | Bin 698 -> 0 bytes .../ic_brightness_percent_white_24dp.png | Bin 932 -> 0 bytes .../res/drawable-xhdpi/ic_cart_white_24dp.png | Bin 867 -> 0 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 1994 -> 0 bytes .../ic_chevron_down_white_48dp.png | Bin 668 -> 0 bytes .../ic_chevron_up_white_48dp.png | Bin 687 -> 0 bytes .../drawable-xhdpi/ic_email_white_48dp.png | Bin 1319 -> 0 bytes .../ic_gesture_tap_white_24dp.png | Bin 911 -> 0 bytes .../ic_human_greeting_white_24dp.png | Bin 873 -> 0 bytes .../drawable-xhdpi/ic_image_white_24dp.png | Bin 731 -> 0 bytes .../ic_information_white_48dp.png | Bin 1716 -> 0 bytes .../ic_map_marker_white_24dp.png | Bin 921 -> 0 bytes .../drawable-xhdpi/ic_message_white_48dp.png | Bin 786 -> 0 bytes .../ic_newspaper_white_24dp.png | Bin 621 -> 0 bytes .../res/drawable-xhdpi/ic_star_white_24dp.png | Bin 924 -> 0 bytes .../ic_alert_octagon_white_48dp.png | Bin 1504 -> 0 bytes .../drawable-xxhdpi/ic_alert_white_48dp.png | Bin 1860 -> 0 bytes .../drawable-xxhdpi/ic_bell_white_24dp.png | Bin 979 -> 0 bytes .../ic_brightness_percent_white_24dp.png | Bin 1213 -> 0 bytes .../drawable-xxhdpi/ic_cart_white_24dp.png | Bin 1218 -> 0 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 3041 -> 0 bytes .../ic_chevron_down_white_48dp.png | Bin 793 -> 0 bytes .../ic_chevron_up_white_48dp.png | Bin 813 -> 0 bytes .../drawable-xxhdpi/ic_email_white_48dp.png | Bin 1788 -> 0 bytes .../ic_gesture_tap_white_24dp.png | Bin 1319 -> 0 bytes .../ic_human_greeting_white_24dp.png | Bin 1248 -> 0 bytes .../drawable-xxhdpi/ic_image_white_24dp.png | Bin 975 -> 0 bytes .../ic_information_white_48dp.png | Bin 2625 -> 0 bytes .../ic_map_marker_white_24dp.png | Bin 1436 -> 0 bytes .../drawable-xxhdpi/ic_message_white_48dp.png | Bin 1024 -> 0 bytes .../ic_newspaper_white_24dp.png | Bin 736 -> 0 bytes .../drawable-xxhdpi/ic_star_white_24dp.png | Bin 1346 -> 0 bytes .../ic_alert_octagon_white_48dp.png | Bin 1848 -> 0 bytes .../drawable-xxxhdpi/ic_alert_white_48dp.png | Bin 2329 -> 0 bytes .../drawable-xxxhdpi/ic_bell_white_24dp.png | Bin 1269 -> 0 bytes .../ic_brightness_percent_white_24dp.png | Bin 1630 -> 0 bytes .../drawable-xxxhdpi/ic_cart_white_24dp.png | Bin 1571 -> 0 bytes .../ic_checkbox_marked_circle_white_48dp.png | Bin 4124 -> 0 bytes .../ic_chevron_down_white_48dp.png | Bin 999 -> 0 bytes .../ic_chevron_up_white_48dp.png | Bin 1032 -> 0 bytes .../drawable-xxxhdpi/ic_email_white_48dp.png | Bin 2257 -> 0 bytes .../ic_gesture_tap_white_24dp.png | Bin 1624 -> 0 bytes .../ic_human_greeting_white_24dp.png | Bin 1620 -> 0 bytes .../drawable-xxxhdpi/ic_image_white_24dp.png | Bin 1210 -> 0 bytes .../ic_information_white_48dp.png | Bin 3520 -> 0 bytes .../ic_map_marker_white_24dp.png | Bin 1809 -> 0 bytes .../ic_message_white_48dp.png | Bin 1304 -> 0 bytes .../ic_newspaper_white_24dp.png | Bin 921 -> 0 bytes .../drawable-xxxhdpi/ic_star_white_24dp.png | Bin 1795 -> 0 bytes .../res/drawable/ic_launcher_background.xml | 170 --- .../res/drawable/ic_launcher_foreground.xml | 34 - .../res/mipmap-hdpi/ic_onesignal_launcher.png | Bin 5972 -> 0 bytes .../res/mipmap-mdpi/ic_onesignal_launcher.png | Bin 3989 -> 0 bytes .../mipmap-xhdpi/ic_onesignal_launcher.png | Bin 8639 -> 0 bytes .../mipmap-xxhdpi/ic_onesignal_launcher.png | Bin 13885 -> 0 bytes .../mipmap-xxxhdpi/ic_onesignal_launcher.png | Bin 20157 -> 0 bytes .../OneSignalDemoV2/buildSrc/build.gradle.kts | 8 - .../buildSrc/src/main/kotlin/Dependencies.kt | 52 - .../buildSrc/src/main/kotlin/Versions.kt | 46 - .../gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - Examples/OneSignalDemoV2/gradlew | 234 --- Examples/OneSignalDemoV2/gradlew.bat | 89 -- OneSignalSDK/onesignal/spotless.gradle | 4 +- OneSignalSDK/settings.gradle | 2 +- .../build_app_prompt.md => examples/build.md | 10 +- .../demo}/app/agconnect-services.json | 0 .../demo}/app/build.gradle.kts | 74 +- .../demo}/app/google-services.json | 0 .../demo}/app/proguard-rules.pro | 0 .../demo}/app/src/huawei/AndroidManifest.xml | 0 .../notification/HmsMessageServiceAppLevel.kt | 0 .../demo}/app/src/main/AndroidManifest.xml | 0 .../sdktest/application/MainApplication.kt | 0 .../sdktest/data/model/InAppMessageType.kt | 0 .../sdktest/data/model/NotificationType.kt | 0 .../sdktest/data/network/OneSignalService.kt | 0 .../data/repository/OneSignalRepository.kt | 0 .../sdktest/ui/components/ActionButton.kt | 0 .../sdktest/ui/components/Dialogs.kt | 0 .../sdktest/ui/components/ListComponents.kt | 0 .../sdktest/ui/components/LoadingOverlay.kt | 0 .../sdktest/ui/components/LogView.kt | 0 .../sdktest/ui/components/SectionCard.kt | 0 .../sdktest/ui/components/ToggleRow.kt | 0 .../onesignal/sdktest/ui/main/MainActivity.kt | 0 .../onesignal/sdktest/ui/main/MainScreen.kt | 0 .../sdktest/ui/main/MainViewModel.kt | 0 .../com/onesignal/sdktest/ui/main/Sections.kt | 0 .../sdktest/ui/secondary/SecondaryActivity.kt | 0 .../com/onesignal/sdktest/ui/theme/Theme.kt | 0 .../com/onesignal/sdktest/util/LogManager.kt | 0 .../sdktest/util/SharedPreferenceUtil.kt | 0 .../onesignal/sdktest/util/TooltipHelper.kt | 0 .../ic_alert_octagon_white_48dp.png | Bin .../res/drawable-hdpi/ic_alert_white_48dp.png | Bin .../res/drawable-hdpi/ic_bell_white_24dp.png | Bin .../ic_brightness_percent_white_24dp.png | Bin .../res/drawable-hdpi/ic_cart_white_24dp.png | Bin .../ic_checkbox_marked_circle_white_48dp.png | Bin .../ic_chevron_down_white_48dp.png | Bin .../ic_chevron_up_white_48dp.png | Bin .../res/drawable-hdpi/ic_email_white_48dp.png | Bin .../ic_gesture_tap_white_24dp.png | Bin .../ic_human_greeting_white_24dp.png | Bin .../res/drawable-hdpi/ic_image_white_24dp.png | Bin .../ic_information_white_48dp.png | Bin .../ic_map_marker_white_24dp.png | Bin .../drawable-hdpi/ic_message_white_48dp.png | Bin .../drawable-hdpi/ic_newspaper_white_24dp.png | Bin .../res/drawable-hdpi/ic_star_white_24dp.png | Bin .../ic_alert_octagon_white_48dp.png | Bin .../res/drawable-mdpi/ic_alert_white_48dp.png | Bin .../res/drawable-mdpi/ic_bell_white_24dp.png | Bin .../ic_brightness_percent_white_24dp.png | Bin .../res/drawable-mdpi/ic_cart_white_24dp.png | Bin .../ic_checkbox_marked_circle_white_48dp.png | Bin .../ic_chevron_down_white_48dp.png | Bin .../ic_chevron_up_white_48dp.png | Bin .../res/drawable-mdpi/ic_email_white_48dp.png | Bin .../ic_gesture_tap_white_24dp.png | Bin .../ic_human_greeting_white_24dp.png | Bin .../res/drawable-mdpi/ic_image_white_24dp.png | Bin .../ic_information_white_48dp.png | Bin .../ic_map_marker_white_24dp.png | Bin .../drawable-mdpi/ic_message_white_48dp.png | Bin .../drawable-mdpi/ic_newspaper_white_24dp.png | Bin .../res/drawable-mdpi/ic_star_white_24dp.png | Bin .../drawable-nodpi/onesignal_rectangle.png | Bin .../res/drawable-nodpi/onesignal_square.png | Bin .../ic_alert_octagon_white_48dp.png | Bin .../drawable-xhdpi/ic_alert_white_48dp.png | Bin .../res/drawable-xhdpi/ic_bell_white_24dp.png | Bin .../ic_brightness_percent_white_24dp.png | Bin .../res/drawable-xhdpi/ic_cart_white_24dp.png | Bin .../ic_checkbox_marked_circle_white_48dp.png | Bin .../ic_chevron_down_white_48dp.png | Bin .../ic_chevron_up_white_48dp.png | Bin .../drawable-xhdpi/ic_email_white_48dp.png | Bin .../ic_gesture_tap_white_24dp.png | Bin .../ic_human_greeting_white_24dp.png | Bin .../drawable-xhdpi/ic_image_white_24dp.png | Bin .../ic_information_white_48dp.png | Bin .../ic_map_marker_white_24dp.png | Bin .../drawable-xhdpi/ic_message_white_48dp.png | Bin .../ic_newspaper_white_24dp.png | Bin .../res/drawable-xhdpi/ic_star_white_24dp.png | Bin .../ic_alert_octagon_white_48dp.png | Bin .../drawable-xxhdpi/ic_alert_white_48dp.png | Bin .../drawable-xxhdpi/ic_bell_white_24dp.png | Bin .../ic_brightness_percent_white_24dp.png | Bin .../drawable-xxhdpi/ic_cart_white_24dp.png | Bin .../ic_checkbox_marked_circle_white_48dp.png | Bin .../ic_chevron_down_white_48dp.png | Bin .../ic_chevron_up_white_48dp.png | Bin .../drawable-xxhdpi/ic_email_white_48dp.png | Bin .../ic_gesture_tap_white_24dp.png | Bin .../ic_human_greeting_white_24dp.png | Bin .../drawable-xxhdpi/ic_image_white_24dp.png | Bin .../ic_information_white_48dp.png | Bin .../ic_map_marker_white_24dp.png | Bin .../drawable-xxhdpi/ic_message_white_48dp.png | Bin .../ic_newspaper_white_24dp.png | Bin .../drawable-xxhdpi/ic_star_white_24dp.png | Bin .../ic_alert_octagon_white_48dp.png | Bin .../drawable-xxxhdpi/ic_alert_white_48dp.png | Bin .../drawable-xxxhdpi/ic_bell_white_24dp.png | Bin .../ic_brightness_percent_white_24dp.png | Bin .../drawable-xxxhdpi/ic_cart_white_24dp.png | Bin .../ic_checkbox_marked_circle_white_48dp.png | Bin .../ic_chevron_down_white_48dp.png | Bin .../ic_chevron_up_white_48dp.png | Bin .../drawable-xxxhdpi/ic_email_white_48dp.png | Bin .../ic_gesture_tap_white_24dp.png | Bin .../ic_human_greeting_white_24dp.png | Bin .../drawable-xxxhdpi/ic_image_white_24dp.png | Bin .../ic_information_white_48dp.png | Bin .../ic_map_marker_white_24dp.png | Bin .../ic_message_white_48dp.png | Bin .../ic_newspaper_white_24dp.png | Bin .../drawable-xxxhdpi/ic_star_white_24dp.png | Bin .../res/drawable/ic_launcher_background.xml | 0 .../res/drawable/ic_launcher_foreground.xml | 0 .../res/mipmap-hdpi/ic_onesignal_launcher.png | Bin .../res/mipmap-mdpi/ic_onesignal_launcher.png | Bin .../mipmap-xhdpi/ic_onesignal_launcher.png | Bin .../mipmap-xxhdpi/ic_onesignal_launcher.png | Bin .../mipmap-xxxhdpi/ic_onesignal_launcher.png | Bin .../demo}/app/src/main/res/values/colors.xml | 0 .../demo}/app/src/main/res/values/strings.xml | 2 +- .../demo}/app/src/main/res/values/styles.xml | 0 .../demo}/build.gradle.kts | 8 +- .../demo}/gradle.properties | 0 .../demo}/gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../OneSignalDemo => examples/demo}/gradlew | 0 .../demo}/gradlew.bat | 0 .../demo}/settings.gradle.kts | 0 319 files changed, 51 insertions(+), 7258 deletions(-) delete mode 100644 Examples/OneSignalDemo/.gitignore delete mode 100644 Examples/OneSignalDemo/app/build.gradle delete mode 100644 Examples/OneSignalDemo/app/src/huawei/AndroidManifest.xml delete mode 100644 Examples/OneSignalDemo/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.java delete mode 100644 Examples/OneSignalDemo/app/src/main/AndroidManifest.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/assets/api_key.txt delete mode 100644 Examples/OneSignalDemo/app/src/main/assets/fonts/Sarala-Bold.ttf delete mode 100644 Examples/OneSignalDemo/app/src/main/assets/fonts/Sarala-Regular.ttf delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/activity/MainActivity.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/activity/SecondaryActivity.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/activity/SplashActivity.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/EnumSelectionRecyclerViewAdapter.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/InAppMessageRecyclerViewAdapter.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/NotificationRecyclerViewAdapter.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/PairRecyclerViewAdapter.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SingleRecyclerViewAdapter.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SubscriptionRecyclerViewAdapter.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/AddPairAlertDialogCallback.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/EnumSelectionCallback.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/PairItemActionCallback.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SendOutcomeAlertDialogCallback.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SingleItemActionCallback.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SubscriptionItemActionCallback.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/UpdateAlertDialogCallback.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Tag.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Text.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/ActivityViewModel.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/SplashActivityViewModel.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationData.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationServiceExtension.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/OneSignalNotificationSender.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/InAppMessage.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/Notification.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/OutcomeEvent.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/ToastType.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/CustomAlertDialogBuilder.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/FixAppBarLayoutBehavior.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/RecyclerViewBuilder.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Animate.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Dialog.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Font.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/IntentTo.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/InterfaceUtil.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Toaster.java delete mode 100644 Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Util.java delete mode 100644 Examples/OneSignalDemo/app/src/main/res/anim/fade_in.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/anim/fade_out.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_red_white.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_white_red.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/drawable/divider.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/drawable/no_divider.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_red_white.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_white_red.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/activity_secondary.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/add_pair_alert_dialog_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/enum_selection_recycler_view_item_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/main_in_app_messages_recycler_view_item_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/main_notifications_recycler_view_item_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/pair_recycler_view_item_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/send_outcome_alert_dialog_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/single_recycler_view_item_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/splash_activity_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/subscription_recycler_view_item_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/title_body_notification.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/toaster_toast_card_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/layout/update_alert_dialog_layout.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/values/colors.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/values/strings.xml delete mode 100644 Examples/OneSignalDemo/app/src/main/res/values/styles.xml delete mode 100644 Examples/OneSignalDemo/build.gradle delete mode 100644 Examples/OneSignalDemo/gradle.properties delete mode 100644 Examples/OneSignalDemo/settings.gradle delete mode 100644 Examples/OneSignalDemoV2/app/agconnect-services.json delete mode 100644 Examples/OneSignalDemoV2/app/google-services.json delete mode 100644 Examples/OneSignalDemoV2/app/proguard-rules.pro delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_octagon_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_alert_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_bell_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_brightness_percent_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_cart_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_checkbox_marked_circle_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_down_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_chevron_up_white_48dp.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_email_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_gesture_tap_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_human_greeting_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_image_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_information_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_map_marker_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_message_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_newspaper_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_octagon_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_alert_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_bell_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_brightness_percent_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_cart_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_checkbox_marked_circle_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_chevron_down_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_chevron_up_white_48dp.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_email_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_gesture_tap_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_human_greeting_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_image_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_information_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_map_marker_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_message_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_newspaper_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-nodpi/onesignal_rectangle.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-nodpi/onesignal_square.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_alert_octagon_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_alert_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_bell_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_brightness_percent_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_cart_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_checkbox_marked_circle_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_down_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_chevron_up_white_48dp.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_email_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_gesture_tap_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_human_greeting_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_information_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_map_marker_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_message_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_newspaper_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xhdpi/ic_star_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_alert_octagon_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_alert_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_bell_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_brightness_percent_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_cart_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_checkbox_marked_circle_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_chevron_down_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_chevron_up_white_48dp.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_email_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_gesture_tap_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_human_greeting_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_information_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_map_marker_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_message_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_newspaper_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_alert_octagon_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_alert_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_bell_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_brightness_percent_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_cart_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_checkbox_marked_circle_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_chevron_down_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_chevron_up_white_48dp.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_email_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_gesture_tap_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_human_greeting_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_image_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_information_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_map_marker_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_message_white_48dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_newspaper_white_24dp.png delete mode 100755 Examples/OneSignalDemoV2/app/src/main/res/drawable-xxxhdpi/ic_star_white_24dp.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-hdpi/ic_onesignal_launcher.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-mdpi/ic_onesignal_launcher.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-xhdpi/ic_onesignal_launcher.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-xxhdpi/ic_onesignal_launcher.png delete mode 100644 Examples/OneSignalDemoV2/app/src/main/res/mipmap-xxxhdpi/ic_onesignal_launcher.png delete mode 100644 Examples/OneSignalDemoV2/buildSrc/build.gradle.kts delete mode 100644 Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Dependencies.kt delete mode 100644 Examples/OneSignalDemoV2/buildSrc/src/main/kotlin/Versions.kt delete mode 100644 Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.jar delete mode 100644 Examples/OneSignalDemoV2/gradle/wrapper/gradle-wrapper.properties delete mode 100755 Examples/OneSignalDemoV2/gradlew delete mode 100644 Examples/OneSignalDemoV2/gradlew.bat rename Examples/OneSignalDemoV2/build_app_prompt.md => examples/build.md (98%) rename {Examples/OneSignalDemo => examples/demo}/app/agconnect-services.json (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/build.gradle.kts (56%) rename {Examples/OneSignalDemo => examples/demo}/app/google-services.json (100%) rename {Examples/OneSignalDemo => examples/demo}/app/proguard-rules.pro (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/huawei/AndroidManifest.xml (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/AndroidManifest.xml (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/data/model/InAppMessageType.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/data/model/NotificationType.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/data/network/OneSignalService.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/data/repository/OneSignalRepository.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/components/ActionButton.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/components/Dialogs.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/components/ListComponents.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/components/LoadingOverlay.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/components/LogView.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/components/SectionCard.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/components/ToggleRow.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/main/MainActivity.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/main/MainScreen.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/main/MainViewModel.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/main/Sections.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/ui/theme/Theme.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/util/LogManager.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.kt (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/java/com/onesignal/sdktest/util/TooltipHelper.kt (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_alert_octagon_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_alert_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_bell_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_brightness_percent_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_cart_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_checkbox_marked_circle_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_chevron_down_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_chevron_up_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_email_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_gesture_tap_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_human_greeting_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_image_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_information_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_map_marker_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_message_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_newspaper_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-hdpi/ic_star_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_alert_octagon_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_alert_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_bell_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_brightness_percent_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_cart_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_checkbox_marked_circle_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_chevron_down_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_chevron_up_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_email_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_gesture_tap_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_human_greeting_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_image_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_information_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_map_marker_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_message_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_newspaper_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-mdpi/ic_star_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-nodpi/onesignal_rectangle.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-nodpi/onesignal_square.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_alert_octagon_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_alert_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_bell_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_brightness_percent_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_cart_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_checkbox_marked_circle_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_chevron_down_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_chevron_up_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_email_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_gesture_tap_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_human_greeting_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_information_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_map_marker_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_message_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_newspaper_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xhdpi/ic_star_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_alert_octagon_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_alert_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_bell_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_brightness_percent_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_cart_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_checkbox_marked_circle_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_chevron_down_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_chevron_up_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_email_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_gesture_tap_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_human_greeting_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_information_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_map_marker_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_message_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_newspaper_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxhdpi/ic_star_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_alert_octagon_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_alert_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_bell_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_brightness_percent_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_cart_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_checkbox_marked_circle_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_chevron_down_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_chevron_up_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_email_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_gesture_tap_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_human_greeting_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_image_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_information_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_map_marker_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_message_white_48dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_newspaper_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable-xxxhdpi/ic_star_white_24dp.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable/ic_launcher_background.xml (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/drawable/ic_launcher_foreground.xml (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/mipmap-hdpi/ic_onesignal_launcher.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/mipmap-mdpi/ic_onesignal_launcher.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/mipmap-xhdpi/ic_onesignal_launcher.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/mipmap-xxhdpi/ic_onesignal_launcher.png (100%) rename {Examples/OneSignalDemo => examples/demo}/app/src/main/res/mipmap-xxxhdpi/ic_onesignal_launcher.png (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/res/values/colors.xml (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/res/values/strings.xml (99%) rename {Examples/OneSignalDemoV2 => examples/demo}/app/src/main/res/values/styles.xml (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/build.gradle.kts (65%) rename {Examples/OneSignalDemoV2 => examples/demo}/gradle.properties (100%) rename {Examples/OneSignalDemo => examples/demo}/gradle/wrapper/gradle-wrapper.jar (100%) rename {Examples/OneSignalDemo => examples/demo}/gradle/wrapper/gradle-wrapper.properties (100%) rename {Examples/OneSignalDemo => examples/demo}/gradlew (100%) rename {Examples/OneSignalDemo => examples/demo}/gradlew.bat (100%) rename {Examples/OneSignalDemoV2 => examples/demo}/settings.gradle.kts (100%) diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 1720874a11..c2bfcdb54c 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -86,7 +86,7 @@ jobs: - name: Update SDK_VERSION in gradle.properties run: | sed -i "s/^SDK_VERSION=.*/SDK_VERSION=$VERSION/" OneSignalSDK/gradle.properties - sed -i "s/^SDK_VERSION=.*/SDK_VERSION=$VERSION/" Examples/OneSignalDemo/gradle.properties + sed -i "s/^SDK_VERSION=.*/SDK_VERSION=$VERSION/" examples/demo/gradle.properties - name: Commit and Push changes run: | diff --git a/.gitignore b/.gitignore index 0cd7cbc570..b2f46c5ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ build/ # Local properties local.properties OneSignalSDK/local.properties -Examples/OneSignalDemo/local.properties +examples/demo/local.properties # macOS .DS_Store @@ -18,7 +18,7 @@ Examples/OneSignalDemo/local.properties # Captures captures/ OneSignalSDK/captures/ -Examples/OneSignalDemo/captures/ +examples/demo/captures/ # GPG files *.gpg diff --git a/Examples/OneSignalDemo/.gitignore b/Examples/OneSignalDemo/.gitignore deleted file mode 100644 index 24bfabcdf9..0000000000 --- a/Examples/OneSignalDemo/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.gradle -local.properties -.DS_Store -/build -/captures -*.gpg -/**/.idea -*.iml \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/build.gradle b/Examples/OneSignalDemo/app/build.gradle deleted file mode 100644 index c994d6615d..0000000000 --- a/Examples/OneSignalDemo/app/build.gradle +++ /dev/null @@ -1,122 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' -} - -android { - namespace 'com.onesignal.sdktest' - compileSdkVersion 34 - defaultConfig { - minSdkVersion 21 - targetSdkVersion 34 - versionCode 1 - versionName "1.0" - multiDexEnabled true - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - flavorDimensions "default" - } - -// signingConfigs { -// huawei { -// storeFile file('SdkTest.jks') -// keyAlias 'SdkTest' -// keyPassword '' -// storePassword '' -// v1SigningEnabled true -// v2SigningEnabled true -// } -// } - - productFlavors { - gms { - dimension "default" - applicationId "com.onesignal.sdktest" - } - huawei { - dimension "default" - minSdkVersion 21 - applicationId "com.onesignal.sdktest" - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - debug { -// signingConfig null -// productFlavors.huawei.signingConfig signingConfigs.huawei - debuggable true - // Note: profileable is automatically enabled when debuggable=true - // Enable method tracing for detailed performance analysis - testCoverageEnabled false - } - // Profileable release build for performance testing - profileable { - initWith release - debuggable false - profileable true - minifyEnabled false - signingConfig signingConfigs.debug - matchingFallbacks = ['release'] - } - } - - kotlinOptions { - jvmTarget = '1.8' - } - - // Forced downgrade to Java 1.8 for compiling the application due to Android N error when building - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - packagingOptions { - exclude 'androidsupportmultidexversion.txt' - } - - task flavorSelection() { - def tasksList = gradle.startParameter.taskRequests.toString() - if (tasksList.contains('Gms')) { - apply plugin: 'com.google.gms.google-services' - googleServices { disableVersionCheck = true } - } else if (tasksList.contains('Huawei')) { - apply plugin: 'com.huawei.agconnect' - } - } -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' - implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'androidx.vectordrawable:vectordrawable:1.1.0' - - implementation 'com.google.android.material:material:1.7.0' - implementation 'com.google.android.gms:play-services-location:21.0.0' - - implementation 'com.github.bumptech.glide:glide:4.12.0' - - /** START - Google Play Builds **/ - gmsImplementation("com.onesignal:OneSignal:$SDK_VERSION") - /** END - Google Play Builds **/ - - /** START - Huawei Builds **/ - // Omit Google / Firebase libraries for Huawei builds. - huaweiImplementation("com.onesignal:OneSignal:$SDK_VERSION") { - exclude group: 'com.google.android.gms', module: 'play-services-gcm' - exclude group: 'com.google.android.gms', module: 'play-services-analytics' - exclude group: 'com.google.android.gms', module: 'play-services-location' - exclude group: 'com.google.firebase', module: 'firebase-messaging' - } - // Add HMS Push library - huaweiImplementation 'com.huawei.hms:push:6.3.0.304' - // Optionally add HMS location - huaweiImplementation 'com.huawei.hms:location:4.0.0.300' - /** END - Huawei Builds **/ -} diff --git a/Examples/OneSignalDemo/app/src/huawei/AndroidManifest.xml b/Examples/OneSignalDemo/app/src/huawei/AndroidManifest.xml deleted file mode 100644 index 6778b0ea3b..0000000000 --- a/Examples/OneSignalDemo/app/src/huawei/AndroidManifest.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.java b/Examples/OneSignalDemo/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.java deleted file mode 100644 index 1d49484664..0000000000 --- a/Examples/OneSignalDemo/app/src/huawei/java/com/onesignal/sdktest/notification/HmsMessageServiceAppLevel.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.onesignal.sdktest.notification; - -import android.os.Bundle; -import android.util.Log; - -import com.huawei.hms.push.HmsMessageService; -import com.huawei.hms.push.RemoteMessage; -import com.onesignal.notifications.bridges.OneSignalHmsEventBridge; -import com.onesignal.sdktest.constant.Tag; - -public class HmsMessageServiceAppLevel extends HmsMessageService { - - /** - * When an app calls the getToken method to apply for a token from the server, - * if the server does not return the token during current method calling, the server can return the token through this method later. - * This method callback must be completed in 10 seconds. Otherwise, you need to start a new Job for callback processing. - * - * @param token token - * @param bundle bundle - */ - @Override - public void onNewToken(String token, Bundle bundle) { - Log.d(Tag.LOG_TAG, "HmsMessageServiceAppLevel onNewToken refresh token:" + token + " bundle: " + bundle); - - // Forward event on to OneSignal SDK - OneSignalHmsEventBridge.INSTANCE.onNewToken(this, token, bundle); - } - - @Deprecated - @Override - public void onNewToken(String token) { - Log.d(Tag.LOG_TAG, "HmsMessageServiceAppLevel onNewToken refresh token:" + token); - - // Forward event on to OneSignal SDK - OneSignalHmsEventBridge.INSTANCE.onNewToken(this, token); - } - - /** - * This method is called in the following cases: - * 1. "Data messages" - App process is alive when received. - * 2. "Notification Message" - foreground_show = false and app is in focus - * This method callback must be completed in 10 seconds. Start a new Job if more time is needed. - * - * @param message RemoteMessage - */ - @Override - public void onMessageReceived(RemoteMessage message) { - Log.d(Tag.LOG_TAG, "HMS onMessageReceived: " + message); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.ttl:" + message.getTtl()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.data:" + message.getData()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.title: " + message.getNotification().getTitle()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.body: " + message.getNotification().getBody()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.icon: " + message.getNotification().getIcon()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.color: " + message.getNotification().getColor()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.channelId: " + message.getNotification().getChannelId()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.imageURL: " + message.getNotification().getImageUrl()); - Log.d(Tag.LOG_TAG, "HMS onMessageReceived.tag: " + message.getNotification().getTag()); - - // Forward event on to OneSignal SDK - OneSignalHmsEventBridge.INSTANCE.onMessageReceived(this, message); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/AndroidManifest.xml b/Examples/OneSignalDemo/app/src/main/AndroidManifest.xml deleted file mode 100644 index 98ac927a3a..0000000000 --- a/Examples/OneSignalDemo/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/assets/api_key.txt b/Examples/OneSignalDemo/app/src/main/assets/api_key.txt deleted file mode 100644 index 652febffec..0000000000 --- a/Examples/OneSignalDemo/app/src/main/assets/api_key.txt +++ /dev/null @@ -1,2 +0,0 @@ -Insert Amazon API Key here. -https://documentation.onesignal.com/docs/generate-an-amazon-api-key \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/assets/fonts/Sarala-Bold.ttf b/Examples/OneSignalDemo/app/src/main/assets/fonts/Sarala-Bold.ttf deleted file mode 100644 index f9c11ca7948e4acf163d5f16f6fce59b5a22dad2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224888 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 diff --git a/Examples/OneSignalDemo/app/src/main/assets/fonts/Sarala-Regular.ttf b/Examples/OneSignalDemo/app/src/main/assets/fonts/Sarala-Regular.ttf deleted file mode 100644 index df722b2ad3ce956ad427e97a48adb880a152fbcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 227208 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%0Dj4S4tC)?;4s`}(a+Rn;B_lpO>m{bmC64e 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 { - - private Font font; - private LayoutInflater layoutInflater; - - private Context context; - private Object[] enums; - private EnumSelectionCallback callback; - - - public EnumSelectionRecyclerViewAdapter(Context context, Object[] enums, EnumSelectionCallback callback) { - this.context = context; - this.enums = enums; - this.callback = callback; - - this.font = new Font(context); - this.layoutInflater = LayoutInflater.from(context); - } - - - @Override - public int getItemViewType(int position) { - return 0; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = layoutInflater.inflate(R.layout.enum_selection_recycler_view_item_layout, parent, false); - view.setHasTransientState(true); - return new EnumSelectionViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ((EnumSelectionViewHolder) holder).setData(position, enums[position].toString()); - } - - @Override - public int getItemCount() { - return enums.length; - } - - - public class EnumSelectionViewHolder extends RecyclerView.ViewHolder { - - private RelativeLayout enumRelativeLayout; - private TextView enumTextView; - - private String title; - - EnumSelectionViewHolder(View itemView) { - super(itemView); - - enumRelativeLayout = itemView.findViewById(R.id.enum_selection_recycler_view_item_relative_layout); - enumTextView = itemView.findViewById(R.id.enum_selection_recycler_view_item_text_view); - } - - private void setData(int position, String title) { - this.title = title; - populateInterfaceElements(position); - } - - private void populateInterfaceElements(int position) { - enumRelativeLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - callback.onSelection(title); - } - }); - - font.applyFont(enumTextView, font.saralaRegular); - enumTextView.setText(title); - } - - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/InAppMessageRecyclerViewAdapter.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/InAppMessageRecyclerViewAdapter.java deleted file mode 100644 index fe6884c1c2..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/InAppMessageRecyclerViewAdapter.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.onesignal.sdktest.adapter; - -import android.content.Context; -import android.graphics.Bitmap; -import android.os.Handler; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.target.BitmapImageViewTarget; -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.type.InAppMessage; -import com.onesignal.sdktest.type.ToastType; -import com.onesignal.sdktest.util.Animate; -import com.onesignal.sdktest.util.Toaster; - -public class InAppMessageRecyclerViewAdapter extends RecyclerView.Adapter { - - private Animate animate; - private LayoutInflater layoutInflater; - private Toaster toaster; - - private Context context; - private InAppMessage[] inAppMessages; - - - public InAppMessageRecyclerViewAdapter(Context context, InAppMessage[] inAppMessages) { - this.context = context; - this.inAppMessages = inAppMessages; - - this.animate = new Animate(); - this.layoutInflater = LayoutInflater.from(context); - this.toaster = new Toaster(context); - } - - - @Override - public int getItemViewType(int position) { - return 0; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = layoutInflater.inflate(R.layout.main_in_app_messages_recycler_view_item_layout, parent, false); - view.setHasTransientState(true); - return new InAppMessageViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ((InAppMessageViewHolder) holder).setData(position, inAppMessages[position]); - } - - @Override - public int getItemCount() { - return inAppMessages.length; - } - - - public class InAppMessageViewHolder extends RecyclerView.ViewHolder { - - private LinearLayout inAppMessageLinearLayout; - private ImageView inAppMessageImageView; - private ProgressBar inAppMessageProgressBar; - private TextView inAppMessageTextView; - - private InAppMessage inAppMessage; - - InAppMessageViewHolder(View itemView) { - super(itemView); - - inAppMessageLinearLayout = itemView.findViewById(R.id.in_app_message_recycler_view_item_linear_layout); - inAppMessageImageView = itemView.findViewById(R.id.in_app_message_recycler_view_item_image_view); - inAppMessageProgressBar = itemView.findViewById(R.id.in_app_message_recycler_view_item_progress_bar); - inAppMessageTextView = itemView.findViewById(R.id.in_app_message_recycler_view_item_text_view); - } - - private void setData(int position, InAppMessage inAppMessage) { - this.inAppMessage = inAppMessage; - populateInterfaceElements(position); - } - - private void populateInterfaceElements(int position) { - animate.toggleAnimationView(true, View.INVISIBLE, inAppMessageImageView, inAppMessageProgressBar); - - inAppMessageLinearLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - String message = "In-App Messaging Coming Soon..."; - toaster.makeCustomViewToast(message, ToastType.INFO); - } - }); - - inAppMessageTextView.setText(inAppMessage.getTitle()); - - Glide.with(context) - .asBitmap() - .load(inAppMessage.getIconUrl()) - .into(new BitmapImageViewTarget(inAppMessageImageView) { - @Override - protected void setResource(Bitmap resource) { - inAppMessageImageView.setImageBitmap(resource); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - animate.toggleAnimationView(false, View.INVISIBLE, inAppMessageImageView, inAppMessageProgressBar); - } - }, 300); - } - }); - } - - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/NotificationRecyclerViewAdapter.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/NotificationRecyclerViewAdapter.java deleted file mode 100644 index b4cb9050f3..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/NotificationRecyclerViewAdapter.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.onesignal.sdktest.adapter; - -import android.content.Context; -import android.graphics.Bitmap; -import android.os.Handler; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.target.BitmapImageViewTarget; -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.notification.OneSignalNotificationSender; -import com.onesignal.sdktest.type.Notification; -import com.onesignal.sdktest.util.Animate; - -public class NotificationRecyclerViewAdapter extends RecyclerView.Adapter { - - private Animate animate; - private LayoutInflater layoutInflater; - - private Context context; - private Notification[] notifications; - - - public NotificationRecyclerViewAdapter(Context context, Notification[] notifications) { - this.context = context; - this.notifications = notifications; - - this.animate = new Animate(); - this.layoutInflater = LayoutInflater.from(context); - } - - - @Override - public int getItemViewType(int position) { - return 0; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = layoutInflater.inflate(R.layout.main_notifications_recycler_view_item_layout, parent, false); - view.setHasTransientState(true); - return new NotificationViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ((NotificationViewHolder) holder).setData(position, notifications[position]); - } - - @Override - public int getItemCount() { - return notifications.length; - } - - - public class NotificationViewHolder extends RecyclerView.ViewHolder { - - private LinearLayout notificationLinearLayout; - private ImageView notificationImageView; - private ProgressBar notificationProgressBar; - private TextView notificationTextView; - - private Notification notification; - - NotificationViewHolder(View itemView) { - super(itemView); - - notificationLinearLayout = itemView.findViewById(R.id.notification_recycler_view_item_linear_layout); - notificationImageView = itemView.findViewById(R.id.notification_recycler_view_item_image_view); - notificationProgressBar = itemView.findViewById(R.id.notification_recycler_view_item_progress_bar); - notificationTextView = itemView.findViewById(R.id.notification_recycler_view_item_text_view); - } - - private void setData(int position, Notification notification) { - this.notification = notification; - populateInterfaceElements(position); - } - - private void populateInterfaceElements(int position) { - animate.toggleAnimationView(true, View.INVISIBLE, notificationImageView, notificationProgressBar); - - notificationLinearLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - OneSignalNotificationSender.sendDeviceNotification(notification); - } - }); - - notificationTextView.setText(notification.getGroup()); - - Glide.with(context) - .asBitmap() - .load(notification.getIconUrl()) - .into(new BitmapImageViewTarget(notificationImageView) { - @Override - protected void setResource(Bitmap resource) { - notificationImageView.setImageBitmap(resource); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - animate.toggleAnimationView(false, View.INVISIBLE, notificationImageView, notificationProgressBar); - } - }, 300); - } - }); - } - - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/PairRecyclerViewAdapter.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/PairRecyclerViewAdapter.java deleted file mode 100644 index 6315ba5b0d..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/PairRecyclerViewAdapter.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.onesignal.sdktest.adapter; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.callback.PairItemActionCallback; -import com.onesignal.sdktest.util.Util; - -import java.util.ArrayList; -import java.util.Map; - -public class PairRecyclerViewAdapter extends RecyclerView.Adapter { - - private LayoutInflater layoutInflater; - - private Context context; - - private ArrayList tags; - private PairItemActionCallback callback; - - public PairRecyclerViewAdapter(Context context, ArrayList tags, PairItemActionCallback callback) { - this.context = context; - - this.tags = tags; - this.callback = callback; - - layoutInflater = LayoutInflater.from(context); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { - View view = layoutInflater.inflate(R.layout.pair_recycler_view_item_layout, parent, false); - view.setHasTransientState(true); - return new PairViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ((PairViewHolder) holder).setData(position, tags.get(position)); - } - - @Override - public int getItemCount() { - return tags.size(); - } - - public class PairViewHolder extends RecyclerView.ViewHolder { - - private RelativeLayout pairRelativeLayout; - private TextView pairKeyTextView; - private TextView pairValueTextView; - - private Map.Entry pair; - - PairViewHolder(View itemView) { - super(itemView); - - pairRelativeLayout = itemView.findViewById(R.id.pair_recycler_view_item_relative_layout); - pairKeyTextView = itemView.findViewById(R.id.pair_recycler_view_item_key_text_view); - pairValueTextView = itemView.findViewById(R.id.pair_recycler_view_item_value_text_view); - } - - private void setData(int position, Map.Entry pair) { - this.pair = pair; - populateInterfaceElements(position); - } - - private void populateInterfaceElements(final int position) { - - pairKeyTextView.setText(pair.getKey().toString()); - - String value = pair.getValue().toString(); - - if (Util.isBoolean(value)) - value += " (bool)"; - else if (Util.isInteger(value)) - value += " (int)"; - else if (Util.isFloat(value)) - value += " (float)"; - else - value += " (str)"; - pairValueTextView.setText(value); - - pairRelativeLayout.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - callback.onLongClick(pair.getKey().toString()); - return false; - } - }); - - } - - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SingleRecyclerViewAdapter.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SingleRecyclerViewAdapter.java deleted file mode 100644 index 7cae936707..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SingleRecyclerViewAdapter.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.onesignal.sdktest.adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.callback.SingleItemActionCallback; -import com.onesignal.sdktest.util.Util; - -import java.util.ArrayList; - -public class SingleRecyclerViewAdapter extends RecyclerView.Adapter { - - private LayoutInflater layoutInflater; - - private Context context; - - private ArrayList values; - private SingleItemActionCallback callback; - - public SingleRecyclerViewAdapter(Context context, ArrayList values, SingleItemActionCallback callback) { - this.context = context; - - this.values = values; - this.callback = callback; - - layoutInflater = LayoutInflater.from(context); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { - View view = layoutInflater.inflate(R.layout.single_recycler_view_item_layout, parent, false); - view.setHasTransientState(true); - return new SingleViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ((SingleViewHolder) holder).setData(position, values.get(position)); - } - - @Override - public int getItemCount() { - return values.size(); - } - - public class SingleViewHolder extends RecyclerView.ViewHolder { - - private RelativeLayout singleRelativeLayout; - private TextView singleTextView; - - private Object item; - - SingleViewHolder(View itemView) { - super(itemView); - - singleRelativeLayout = itemView.findViewById(R.id.single_recycler_view_item_relative_layout); - singleTextView = itemView.findViewById(R.id.single_recycler_view_item_text_view); - } - - private void setData(int position, Object item) { - this.item = item; - populateInterfaceElements(position); - } - - private void populateInterfaceElements(final int position) { - String value = item.toString(); - - if (Util.isBoolean(value)) - value += " (bool)"; - else if (Util.isInteger(value)) - value += " (int)"; - else if (Util.isFloat(value)) - value += " (float)"; - else - value += " (str)"; - singleTextView.setText(value); - - singleRelativeLayout.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - callback.onLongClick(item.toString()); - return false; - } - }); - - } - - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SubscriptionRecyclerViewAdapter.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SubscriptionRecyclerViewAdapter.java deleted file mode 100644 index c7a49f2645..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/adapter/SubscriptionRecyclerViewAdapter.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.onesignal.sdktest.adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.callback.SingleItemActionCallback; -import com.onesignal.sdktest.callback.SubscriptionItemActionCallback; -import com.onesignal.user.subscriptions.IEmailSubscription; -import com.onesignal.user.subscriptions.ISmsSubscription; -import com.onesignal.user.subscriptions.ISubscription; - -import java.util.ArrayList; - -public class SubscriptionRecyclerViewAdapter extends RecyclerView.Adapter { - - private LayoutInflater layoutInflater; - - private Context context; - - private ArrayList subscriptions; - private SubscriptionItemActionCallback callback; - - public SubscriptionRecyclerViewAdapter(Context context, ArrayList subscriptions, SubscriptionItemActionCallback callback) { - this.context = context; - - this.subscriptions = subscriptions; - this.callback = callback; - - layoutInflater = LayoutInflater.from(context); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { - View view = layoutInflater.inflate(R.layout.subscription_recycler_view_item_layout, parent, false); - view.setHasTransientState(true); - return new SubscriptionViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ((SubscriptionViewHolder) holder).setData(position, subscriptions.get(position)); - } - - @Override - public int getItemCount() { - return subscriptions.size(); - } - - public class SubscriptionViewHolder extends RecyclerView.ViewHolder { - - private LinearLayout singleLinearLayout; - private TextView idTextView; - private TextView addressTitleTextView; - private TextView addressTextView; - - private ISubscription item; - - SubscriptionViewHolder(View itemView) { - super(itemView); - - singleLinearLayout = itemView.findViewById(R.id.subscription_recycler_view_item_linear_layout); - idTextView = itemView.findViewById(R.id.subscription_recycler_view_item_id_text_view); - addressTitleTextView = itemView.findViewById(R.id.subscription_recycler_view_item_address_title_text_view); - addressTextView = itemView.findViewById(R.id.subscription_recycler_view_item_address_text_view); - } - - private void setData(int position, ISubscription item) { - this.item = item; - populateInterfaceElements(position); - } - - private void populateInterfaceElements(final int position) { - idTextView.setText(item.getId()); - - if(item instanceof IEmailSubscription) { - addressTitleTextView.setText(R.string.email_colon); - addressTextView.setText(((IEmailSubscription) item).getEmail()); - } - else if(item instanceof ISmsSubscription) { - addressTitleTextView.setText(R.string.sms_colon); - addressTextView.setText(((ISmsSubscription) item).getNumber()); - } - - singleLinearLayout.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - callback.onLongClick(item); - return false; - } - }); - - } - - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java deleted file mode 100644 index df299d9e57..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.onesignal.sdktest.application; - -import android.annotation.SuppressLint; -import android.os.StrictMode; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.multidex.MultiDexApplication; - -import com.onesignal.Continue; -import com.onesignal.OneSignal; -import com.onesignal.inAppMessages.IInAppMessageClickListener; -import com.onesignal.inAppMessages.IInAppMessageClickEvent; -import com.onesignal.inAppMessages.IInAppMessageDidDismissEvent; -import com.onesignal.inAppMessages.IInAppMessageDidDisplayEvent; -import com.onesignal.inAppMessages.IInAppMessageLifecycleListener; -import com.onesignal.debug.LogLevel; -import com.onesignal.inAppMessages.IInAppMessageWillDismissEvent; -import com.onesignal.inAppMessages.IInAppMessageWillDisplayEvent; -import com.onesignal.notifications.IDisplayableNotification; -import com.onesignal.notifications.INotificationLifecycleListener; -import com.onesignal.notifications.INotificationWillDisplayEvent; -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.constant.Tag; -import com.onesignal.sdktest.constant.Text; -import com.onesignal.sdktest.notification.OneSignalNotificationSender; -import com.onesignal.sdktest.util.SharedPreferenceUtil; -import com.onesignal.user.state.IUserStateObserver; -import com.onesignal.user.state.UserChangedState; -import com.onesignal.user.state.UserState; -import org.json.JSONObject; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * This Java implementation is not used any more. Use {@link MainApplicationKT} instead. - * The Kotlin version provides better async handling and modern coroutines support. - * - */ -public class MainApplication extends MultiDexApplication { - private static final int SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION = 2000; - - public MainApplication() { - // run strict mode to surface any potential issues easier - StrictMode.enableDefaults(); - Log.w(Tag.LOG_TAG, "MainApplication (Java) is deprecated. Please use MainApplicationKT (Kotlin) instead."); - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - super.onCreate(); - Log.w(Tag.LOG_TAG, "DEPRECATED: Using MainApplication (Java). Please migrate to MainApplicationKT (Kotlin) for better async support."); - - OneSignal.getDebug().setLogLevel(LogLevel.DEBUG); - - // OneSignal Initialization - String appId = SharedPreferenceUtil.getOneSignalAppId(this); - // If cached app id is null use the default, otherwise use cached. - if (appId == null) { - appId = getString(R.string.onesignal_app_id); - SharedPreferenceUtil.cacheOneSignalAppId(this, appId); - } - - OneSignalNotificationSender.setAppId(appId); - - OneSignal.initWithContext(this, appId); - - // Ensure calling requestPermission in a thread right after initWithContext does not crash - // This will reproduce result similar to Kotlin CouroutineScope.launch{}, which may potentially crash the app - ExecutorService executor = Executors.newSingleThreadExecutor(); - @SuppressLint({"NewApi", "LocalSuppress"}) CompletableFuture future = CompletableFuture.runAsync(() -> { - OneSignal.getNotifications().requestPermission(true, Continue.none()); - }, executor); - future.join(); // Waits for the task to complete - executor.shutdown(); - - OneSignal.getInAppMessages().addLifecycleListener(new IInAppMessageLifecycleListener() { - @Override - public void onWillDisplay(@NonNull IInAppMessageWillDisplayEvent event) { - Log.v(Tag.LOG_TAG, "onWillDisplayInAppMessage"); - } - - @Override - public void onDidDisplay(@NonNull IInAppMessageDidDisplayEvent event) { - Log.v(Tag.LOG_TAG, "onDidDisplayInAppMessage"); - } - - @Override - public void onWillDismiss(@NonNull IInAppMessageWillDismissEvent event) { - Log.v(Tag.LOG_TAG, "onWillDismissInAppMessage"); - } - - @Override - public void onDidDismiss(@NonNull IInAppMessageDidDismissEvent event) { - Log.v(Tag.LOG_TAG, "onDidDismissInAppMessage"); - } - }); - - OneSignal.getInAppMessages().addClickListener(new IInAppMessageClickListener() { - @Override - public void onClick(@Nullable IInAppMessageClickEvent event) { - Log.v(Tag.LOG_TAG, "INotificationClickListener.inAppMessageClicked"); - } - }); - - OneSignal.getNotifications().addClickListener(event -> - { - Log.v(Tag.LOG_TAG, "INotificationClickListener.onClick fired" + - " with event: " + event); - }); - - OneSignal.getNotifications().addForegroundLifecycleListener(new INotificationLifecycleListener() { - @Override - public void onWillDisplay(@NonNull INotificationWillDisplayEvent event) { - Log.v(Tag.LOG_TAG, "INotificationLifecycleListener.onWillDisplay fired" + - " with event: " + event); - - IDisplayableNotification notification = event.getNotification(); - JSONObject data = notification.getAdditionalData(); - - //Prevent OneSignal from displaying the notification immediately on return. Spin - //up a new thread to mimic some asynchronous behavior, when the async behavior (which - //takes 2 seconds) completes, then the notification can be displayed. - event.preventDefault(); - Runnable r = () -> { - try { - Thread.sleep(SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION); - } catch (InterruptedException ignored) { - } - - notification.display(); - }; - - Thread t = new Thread(r); - t.start(); - } - }); - - OneSignal.getUser().addObserver(new IUserStateObserver() { - @Override - public void onUserStateChange(@NonNull UserChangedState state) { - UserState currentUserState = state.getCurrent(); - Log.v(Tag.LOG_TAG, "onUserStateChange fired " + currentUserState.toJSONObject()); - } - }); - - OneSignal.getInAppMessages().setPaused(true); - OneSignal.getLocation().setShared(false); - - Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT); - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt deleted file mode 100644 index c9c61be6a5..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.onesignal.sdktest.application - -/** - * Modern Kotlin implementation of MainApplication. - * - * This replaces the deprecated MainApplication.java with: - * - Better async handling using Kotlin Coroutines - * - Modern OneSignal API usage - * - Cleaner code structure - * - Proper ANR prevention - * - * @see MainApplication.java (deprecated Java version) - */ -import android.annotation.SuppressLint -import android.os.StrictMode -import android.util.Log -import androidx.annotation.NonNull -import androidx.multidex.MultiDexApplication -import com.onesignal.OneSignal -import com.onesignal.debug.LogLevel -import com.onesignal.inAppMessages.IInAppMessageClickEvent -import com.onesignal.inAppMessages.IInAppMessageClickListener -import com.onesignal.inAppMessages.IInAppMessageDidDismissEvent -import com.onesignal.inAppMessages.IInAppMessageDidDisplayEvent -import com.onesignal.inAppMessages.IInAppMessageLifecycleListener -import com.onesignal.inAppMessages.IInAppMessageWillDismissEvent -import com.onesignal.inAppMessages.IInAppMessageWillDisplayEvent -import com.onesignal.notifications.IDisplayableNotification -import com.onesignal.notifications.INotificationClickEvent -import com.onesignal.notifications.INotificationClickListener -import com.onesignal.notifications.INotificationLifecycleListener -import com.onesignal.notifications.INotificationWillDisplayEvent -import com.onesignal.sdktest.R -import com.onesignal.sdktest.constant.Tag -import com.onesignal.sdktest.constant.Text -import com.onesignal.sdktest.notification.OneSignalNotificationSender -import com.onesignal.sdktest.util.SharedPreferenceUtil -import com.onesignal.user.state.IUserStateObserver -import com.onesignal.user.state.UserChangedState -import com.onesignal.user.state.UserState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch - -class MainApplicationKT : MultiDexApplication() { - - private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - - init { - // run strict mode to surface any potential issues easier - StrictMode.enableDefaults() - } - - @SuppressLint("NewApi") - override fun onCreate() { - super.onCreate() - OneSignal.Debug.logLevel = LogLevel.DEBUG - - // OneSignal Initialization - var appId = SharedPreferenceUtil.getOneSignalAppId(this) - // If cached app id is null use the default, otherwise use cached. - if (appId == null) { - appId = getString(R.string.onesignal_app_id) - SharedPreferenceUtil.cacheOneSignalAppId(this, appId) - } - - OneSignalNotificationSender.setAppId(appId) - - // Initialize OneSignal asynchronously on background thread to avoid ANR - applicationScope.launch { - OneSignal.initWithContextSuspend(this@MainApplicationKT, appId) - Log.d(Tag.LOG_TAG, "OneSignal async init completed") - - // Set up all OneSignal listeners after successful async initialization - setupOneSignalListeners() - - // Request permission - this will internally switch to Main thread for UI operations - // Even though the MainActivity comes on top of this, we can still request permission by tapping the prompt push button. - OneSignal.Notifications.requestPermission(true) - - Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT) - } - } - - private fun setupOneSignalListeners() { - OneSignal.InAppMessages.addLifecycleListener(object : IInAppMessageLifecycleListener { - override fun onWillDisplay(@NonNull event: IInAppMessageWillDisplayEvent) { - Log.v(Tag.LOG_TAG, "onWillDisplayInAppMessage") - } - - override fun onDidDisplay(@NonNull event: IInAppMessageDidDisplayEvent) { - Log.v(Tag.LOG_TAG, "onDidDisplayInAppMessage") - } - - override fun onWillDismiss(@NonNull event: IInAppMessageWillDismissEvent) { - Log.v(Tag.LOG_TAG, "onWillDismissInAppMessage") - } - - override fun onDidDismiss(@NonNull event: IInAppMessageDidDismissEvent) { - Log.v(Tag.LOG_TAG, "onDidDismissInAppMessage") - } - }) - - OneSignal.InAppMessages.addClickListener(object : IInAppMessageClickListener { - override fun onClick(event: IInAppMessageClickEvent) { - Log.v(Tag.LOG_TAG, "INotificationClickListener.inAppMessageClicked") - } - }) - - OneSignal.Notifications.addClickListener(object : INotificationClickListener { - override fun onClick(event: INotificationClickEvent) { - Log.v(Tag.LOG_TAG, "INotificationClickListener.onClick fired" + - " with event: " + event) - } - }) - - OneSignal.Notifications.addForegroundLifecycleListener(object : INotificationLifecycleListener { - override fun onWillDisplay(@NonNull event: INotificationWillDisplayEvent) { - Log.v(Tag.LOG_TAG, "INotificationLifecycleListener.onWillDisplay fired" + - " with event: " + event) - - val notification: IDisplayableNotification = event.notification - - //Prevent OneSignal from displaying the notification immediately on return. Spin - //up a new thread to mimic some asynchronous behavior, when the async behavior (which - //takes 2 seconds) completes, then the notification can be displayed. - event.preventDefault() - val r = Runnable { - try { - Thread.sleep(SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION.toLong()) - } catch (ignored: InterruptedException) { - } - - notification.display() - } - - val t = Thread(r) - t.start() - } - }) - - OneSignal.User.addObserver(object : IUserStateObserver { - override fun onUserStateChange(@NonNull state: UserChangedState) { - val currentUserState: UserState = state.current - Log.v(Tag.LOG_TAG, "onUserStateChange fired " + currentUserState.toJSONObject()) - } - }) - - OneSignal.InAppMessages.paused = true - OneSignal.Location.isShared = false - } - - companion object { - private const val SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION = 2000 - } -} \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/AddPairAlertDialogCallback.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/AddPairAlertDialogCallback.java deleted file mode 100644 index cb836c7bcc..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/AddPairAlertDialogCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.onesignal.sdktest.callback; - -import android.util.Pair; - -public interface AddPairAlertDialogCallback { - - void onSuccess(Pair pair); - void onFailure(); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/EnumSelectionCallback.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/EnumSelectionCallback.java deleted file mode 100644 index c65dc236d1..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/EnumSelectionCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.onesignal.sdktest.callback; - -public interface EnumSelectionCallback { - - void onSelection(String title); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/PairItemActionCallback.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/PairItemActionCallback.java deleted file mode 100644 index 57a9aacb51..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/PairItemActionCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.onesignal.sdktest.callback; - -public interface PairItemActionCallback { - - void onLongClick(String key); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SendOutcomeAlertDialogCallback.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SendOutcomeAlertDialogCallback.java deleted file mode 100644 index e66ef198e7..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SendOutcomeAlertDialogCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.onesignal.sdktest.callback; - -import com.onesignal.sdktest.type.OutcomeEvent; - -public interface SendOutcomeAlertDialogCallback { - - boolean onSuccess(OutcomeEvent outcomeEvent, String name, String value); - void onFailure(); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SingleItemActionCallback.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SingleItemActionCallback.java deleted file mode 100644 index ea38662efc..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SingleItemActionCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.onesignal.sdktest.callback; - -public interface SingleItemActionCallback { - - void onLongClick(String value); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SubscriptionItemActionCallback.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SubscriptionItemActionCallback.java deleted file mode 100644 index 579faaa25b..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/SubscriptionItemActionCallback.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.onesignal.sdktest.callback; - -import com.onesignal.user.subscriptions.ISubscription; - -public interface SubscriptionItemActionCallback { - - void onLongClick(ISubscription value); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/UpdateAlertDialogCallback.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/UpdateAlertDialogCallback.java deleted file mode 100644 index e48c58618c..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/callback/UpdateAlertDialogCallback.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.onesignal.sdktest.callback; - -public interface UpdateAlertDialogCallback { - - void onSuccess(String update); - void onFailure(); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Tag.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Tag.java deleted file mode 100644 index 927df53e4f..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Tag.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.onesignal.sdktest.constant; - -public class Tag { - public static final String LOG_TAG = "sdktest"; -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Text.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Text.java deleted file mode 100644 index b3e7344f7d..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/constant/Text.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.onesignal.sdktest.constant; - -public class Text { - - public static final String EMPTY = ""; - - public static final String ONESIGNAL_SDK_INIT = "OneSignal SDK initialized"; - public static final String PRIVACY_CONSENT_REQUIRED_SET = "Privacy consent required set"; - - public static final String APP_ID_IS_REQUIRED = "App id is required"; - public static final String INVALID_APP_ID = "Invalid app id"; - - public static final String ALIAS_LABEL_IS_REQUIRED = "Alias label is required"; - - public static final String EMAIL_IS_REQUIRED = "Email is required"; - public static final String INVALID_EMAIL = "Invalid email"; - - public static final String SMS_IS_REQUIRED = "SMS number is required"; - - public static final String EXTERNAL_USER_ID_IS_REQUIRED = "External user id is required"; - - public static final String KEY_IS_REQUIRED = "Key is required"; - - public static final String ERROR = "Error"; - - public static final String BUTTON_ADD = "ADD"; - public static final String BUTTON_SEND = "SEND"; - public static final String BUTTON_UPDATE = "UPDATE"; - public static final String BUTTON_LOGIN = "LOGIN"; - - public static final String BUTTON_CANCEL = "CANCEL"; -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/ActivityViewModel.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/ActivityViewModel.java deleted file mode 100644 index 8aeb6fcbd3..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/ActivityViewModel.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.onesignal.sdktest.model; - -import android.app.Activity; -import android.content.Context; -import androidx.appcompat.app.AppCompatActivity; - -import com.onesignal.notifications.IPermissionObserver; - -/** - * This is the interface created with a few generic methods for setting a ViewModel - * as the responsible guardian of an Activity - */ -public interface ActivityViewModel extends IPermissionObserver { - - /** - * Casts Context of the given Activity to an Activity object - * @return - Activity used to get to specific methods to the Activity - */ - Activity getActivity(); - - /** - * Casts Context of the given Activity to an AppCompatActivity object - * @return - AppCompatActivity used to get to specific methods to the Activity - */ - AppCompatActivity getAppCompatActivity(); - - /** - * Context is passed in and used to define all of the ui elements across the activity - * and initialize any other objects that may be used through out the activity - * @param context - Context context of the given Activity being setup - * @return - ViewModel implementing the ActivityViewModel - */ - ActivityViewModel onActivityCreated(Context context); - - /** - * This method is for calling any setup methods and applying any fonts - * Called at the end of onCreate strung after onActivityCreated is called - * @return - ViewModel implementing the ActivityViewModel - */ - ActivityViewModel setupInterfaceElements(); - - /** - * Some Activities use a Toolbar and this is a generic method for setting it, design it, and controlling it - * If it is not used, it will be left empty - */ - void setupToolbar(); - - /** - * Methods for handling network connected and disconnected states - */ - void networkConnected(); - void networkDisconnected(); - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java deleted file mode 100644 index c88c630dc6..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/MainActivityViewModel.java +++ /dev/null @@ -1,937 +0,0 @@ -package com.onesignal.sdktest.model; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import com.google.android.material.appbar.AppBarLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.widget.NestedScrollView; -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.appcompat.widget.Toolbar; -import android.content.Intent; -import android.os.Build; -import android.util.Pair; -import android.view.View; -import android.view.ViewTreeObserver; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.Switch; -import android.widget.TextView; -import com.onesignal.Continue; -import com.onesignal.OneSignal; -import com.onesignal.sdktest.adapter.SubscriptionRecyclerViewAdapter; -import com.onesignal.user.subscriptions.IPushSubscription; -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.activity.SecondaryActivity; -import com.onesignal.sdktest.adapter.InAppMessageRecyclerViewAdapter; -import com.onesignal.sdktest.adapter.NotificationRecyclerViewAdapter; -import com.onesignal.sdktest.adapter.PairRecyclerViewAdapter; -import com.onesignal.sdktest.callback.AddPairAlertDialogCallback; -import com.onesignal.sdktest.callback.PairItemActionCallback; -import com.onesignal.sdktest.callback.SendOutcomeAlertDialogCallback; -import com.onesignal.sdktest.callback.UpdateAlertDialogCallback; -import com.onesignal.sdktest.constant.Text; -import com.onesignal.sdktest.type.InAppMessage; -import com.onesignal.sdktest.type.Notification; -import com.onesignal.sdktest.type.OutcomeEvent; -import com.onesignal.sdktest.type.ToastType; -import com.onesignal.sdktest.ui.RecyclerViewBuilder; -import com.onesignal.sdktest.util.Animate; -import com.onesignal.sdktest.util.Dialog; -import com.onesignal.sdktest.util.Font; -import com.onesignal.sdktest.util.IntentTo; -import com.onesignal.sdktest.util.SharedPreferenceUtil; -import com.onesignal.sdktest.util.ProfileUtil; -import com.onesignal.sdktest.util.Toaster; -import com.onesignal.user.subscriptions.ISubscription; -import com.onesignal.user.subscriptions.IPushSubscriptionObserver; -import com.onesignal.user.subscriptions.PushSubscriptionChangedState; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@RequiresApi(api = Build.VERSION_CODES.N) -public class MainActivityViewModel implements ActivityViewModel, IPushSubscriptionObserver { - - private Animate animate; - private Dialog dialog; - private Font font; - private IntentTo intentTo; - private RecyclerViewBuilder recyclerViewBuilder; - private Toaster toaster; - - private AppBarLayout appBarLayout; - private Toolbar toolbar; - private LinearLayout privacyConsentLinearLayout; - private NestedScrollView nestedScrollView; - - // Privacy Consent - private TextView privacyConsentTitleTextView; - private TextView privacyConsentDescriptionTextView; - private Button privacyConsentAllowButton; - - // App - private TextView appTitleTextView; - private RelativeLayout appIdRelativeLayout; - private TextView appIdTitleTextView; - private TextView appIdTextView; - private Button loginUserButton; - private Button logoutUserButton; - - // Alias - private TextView aliasTitleTextView; - private RecyclerView aliasesRecyclerView; - private PairRecyclerViewAdapter aliasesRecyclerViewAdapter; - private TextView noAliasesTextView; - private Button addAliasButton; - - // Email - private TextView emailHeaderTextView; - private TextView noEmailsTextView; - private Button addEmailButton; - - // SMS - private TextView smsHeaderTextView; - private TextView noSmssTextView; - private Button addSMSButton; - - // Tags - private TextView tagsTitleTextView; - private TextView noTagsTextView; - private RecyclerView tagsRecyclerView; - private PairRecyclerViewAdapter tagPairRecyclerViewAdapter; - - private RecyclerView emailsRecyclerView; - private SubscriptionRecyclerViewAdapter emailsRecyclerViewAdapter; - private RecyclerView smssRecyclerView; - private SubscriptionRecyclerViewAdapter smssRecyclerViewAdapter; - - private Button addTagButton; - - // Notification Demo - private TextView pushNotificationTitleTextView; - private TextView sendPushNotificationTitleTextView; - private RecyclerView pushNotificationRecyclerView; - private NotificationRecyclerViewAdapter pushNotificationRecyclerViewAdapter; - - // Outcomes - private TextView outcomeTitleTextView; - private Button sendOutcomeButton; - - // Triggers - private TextView triggersTitleTextView; - private TextView noTriggersTextView; - private RecyclerView triggersRecyclerView; - private PairRecyclerViewAdapter triggerPairRecyclerViewAdapter; - private Button addTriggerButton; - - // In App Messaging Demo - private TextView inAppMessagingTitleTextView; - private TextView sendInAppMessagingTitleTextView; - private RecyclerView inAppMessagingRecyclerView; - private InAppMessageRecyclerViewAdapter inAppMessagingRecyclerViewAdapter; - - // Location - private TextView locationTitleTextView; - private RelativeLayout locationSharedRelativeLayout; - private TextView locationSharedTextView; - private TextView locationSharedDescriptionTextView; - private Switch locationSharedSwitch; - private Button promptLocationButton; - - // Push - private TextView pushSubscriptionIdTitleTextView; - private TextView pushSubscriptionIdTextView; - private RelativeLayout pushSubscriptionEnabledRelativeLayout; - private TextView pushSubscriptionEnabledTitleTextView; - private Switch pushSubscriptionEnabledSwitch; - private LinearLayout promptPushBottonLayout; - private Button promptPushButton; - private RelativeLayout pauseInAppMessagesRelativeLayout; - private TextView pauseInAppMessagesTextView; - private TextView pauseInAppMessagesDescriptionTextView; - private Switch pauseInAppMessagesSwitch; - private Button revokeConsentButton; - - private boolean shouldScrollTop = false; - - private Context context; - - private HashMap aliasSet; - private ArrayList aliasArrayList; - private ArrayList emailArrayList; - private ArrayList smsArrayList; - - private HashMap tagSet; - private ArrayList tagArrayList; - - private HashMap triggerSet; - private ArrayList triggerArrayList; - - @Override - public Activity getActivity() { - return (Activity) context; - } - - @Override - public AppCompatActivity getAppCompatActivity() { - return (AppCompatActivity) context; - } - - @Override - public ActivityViewModel onActivityCreated(Context context) { - this.context = context; - - animate = new Animate(); - dialog = new Dialog(context); - font = new Font(context); - intentTo = new IntentTo(context); - recyclerViewBuilder = new RecyclerViewBuilder(context); - toaster = new Toaster(context); - - appBarLayout = getActivity().findViewById(R.id.main_activity_app_bar_layout); - toolbar = getActivity().findViewById(R.id.main_activity_toolbar); - privacyConsentLinearLayout = getActivity().findViewById(R.id.main_activity_privacy_consent_linear_layout); - nestedScrollView = getActivity().findViewById(R.id.main_activity_nested_scroll_view); - - privacyConsentTitleTextView = getActivity().findViewById(R.id.main_activity_privacy_consent_title_text_view); - privacyConsentDescriptionTextView = getActivity().findViewById(R.id.main_activity_privacy_consent_description_text_view); - privacyConsentAllowButton = getActivity().findViewById(R.id.main_activity_privacy_consent_allow_button); - - appTitleTextView = getActivity().findViewById(R.id.main_activity_account_title_text_view); - appIdRelativeLayout = getActivity().findViewById(R.id.main_activity_account_details_app_id_relative_layout); - appIdTitleTextView = getActivity().findViewById(R.id.main_activity_account_details_app_id_title_text_view); - appIdTextView = getActivity().findViewById(R.id.main_activity_account_details_app_id_text_view); - revokeConsentButton = getActivity().findViewById(R.id.main_activity_app_revoke_consent_button); - loginUserButton = getActivity().findViewById(R.id.main_activity_login_user_button); - logoutUserButton = getActivity().findViewById(R.id.main_activity_logout_user_button); - - aliasTitleTextView = getActivity().findViewById(R.id.main_activity_aliases_title_text_view); - noAliasesTextView = getActivity().findViewById(R.id.main_activity_aliases_no_aliases_text_view); - addAliasButton = getActivity().findViewById(R.id.main_activity_add_alias_button); - aliasesRecyclerView = getActivity().findViewById(R.id.main_activity_aliases_recycler_view); - - emailHeaderTextView = getActivity().findViewById(R.id.main_activity_email_title_text_view); - noEmailsTextView = getActivity().findViewById(R.id.main_activity_emails_no_emails_text_view); - addEmailButton = getActivity().findViewById(R.id.main_activity_add_email_button); - emailsRecyclerView = getActivity().findViewById(R.id.main_activity_emails_recycler_view); - - smsHeaderTextView = getActivity().findViewById(R.id.main_activity_sms_title_text_view); - noSmssTextView = getActivity().findViewById(R.id.main_activity_smss_no_smss_text_view); - addSMSButton = getActivity().findViewById(R.id.main_activity_add_sms_button); - smssRecyclerView = getActivity().findViewById(R.id.main_activity_smss_recycler_view); - - tagsTitleTextView = getActivity().findViewById(R.id.main_activity_tags_title_text_view); - noTagsTextView = getActivity().findViewById(R.id.main_activity_tags_no_tags_text_view); - tagsRecyclerView = getActivity().findViewById(R.id.main_activity_tags_recycler_view); - addTagButton = getActivity().findViewById(R.id.main_activity_add_tags_button); - - pushNotificationTitleTextView = getActivity().findViewById(R.id.main_activity_push_notification_title_text_view); - sendPushNotificationTitleTextView= getActivity().findViewById(R.id.main_activity_send_push_notification_title_text_view); - pushNotificationRecyclerView = getActivity().findViewById(R.id.main_activity_push_notification_recycler_view); - - outcomeTitleTextView = getActivity().findViewById(R.id.main_activity_outcomes_title_text_view); - sendOutcomeButton = getActivity().findViewById(R.id.main_activity_outcomes_send_outcome_button); - - triggersTitleTextView = getActivity().findViewById(R.id.main_activity_in_app_messages_triggers_title_text_view); - noTriggersTextView = getActivity().findViewById(R.id.main_activity_in_app_messages_triggers_no_triggers_text_view); - triggersRecyclerView = getActivity().findViewById(R.id.main_activity_in_app_messages_triggers_recycler_view); - addTriggerButton = getActivity().findViewById(R.id.main_activity_add_triggers_button); - - inAppMessagingTitleTextView = getActivity().findViewById(R.id.main_activity_in_app_messaging_title_text_view); - sendInAppMessagingTitleTextView = getActivity().findViewById(R.id.main_activity_send_in_app_messaging_title_text_view); - inAppMessagingRecyclerView = getActivity().findViewById(R.id.main_activity_in_app_messaging_recycler_view); - - locationTitleTextView = getActivity().findViewById(R.id.main_activity_location_title_text_view); - locationSharedRelativeLayout = getActivity().findViewById(R.id.main_activity_location_shared_relative_layout); - locationSharedTextView = getActivity().findViewById(R.id.main_activity_location_shared_text_view); - locationSharedDescriptionTextView = getActivity().findViewById(R.id.main_activity_location_shared_info_text_view); - locationSharedSwitch = getActivity().findViewById(R.id.main_activity_location_shared_switch); - promptLocationButton = getActivity().findViewById(R.id.main_activity_location_prompt_location_button); - - pushSubscriptionEnabledRelativeLayout = getActivity().findViewById(R.id.main_activity_push_subscription_relative_layout); - pushSubscriptionEnabledTitleTextView = getActivity().findViewById(R.id.main_activity_push_subscription_info_text_view); - pushSubscriptionIdTitleTextView = getActivity().findViewById(R.id.main_activity_push_subscription_id_title_text_view); - pushSubscriptionIdTextView = getActivity().findViewById(R.id.main_activity_push_subscription_id_text_view); - pushSubscriptionEnabledSwitch = getActivity().findViewById(R.id.main_activity_push_subscription_switch); - - promptPushBottonLayout = getActivity().findViewById(R.id.main_activity_push_prompt_layout); - promptPushButton = getActivity().findViewById(R.id.main_activity_push_prompt_push_button); - - pauseInAppMessagesRelativeLayout = getActivity().findViewById(R.id.main_activity_iam_pause_in_app_messages_relative_layout); - pauseInAppMessagesTextView = getActivity().findViewById(R.id.main_activity_iam_pause_in_app_messages_text_view); - pauseInAppMessagesDescriptionTextView = getActivity().findViewById(R.id.main_activity_iam_pause_in_app_messages_info_text_view); - pauseInAppMessagesSwitch = getActivity().findViewById(R.id.main_activity_iam_pause_in_app_messages_switch); - - Button navigateNextActivity = getActivity().findViewById(R.id.main_activity_navigate_button); - navigateNextActivity.setOnClickListener(v -> { - getActivity().startActivity(new Intent(getActivity(), SecondaryActivity.class)); - }); - - aliasSet = new HashMap<>(); - aliasArrayList = new ArrayList<>(); - - emailArrayList = new ArrayList<>(); - smsArrayList = new ArrayList<>(); - - tagSet = new HashMap<>(); - tagArrayList = new ArrayList<>(); - - triggerSet = new HashMap<>(); - triggerArrayList = new ArrayList<>(); - - OneSignal.getUser().getPushSubscription().addObserver(this); - return this; - } - - @Override - public ActivityViewModel setupInterfaceElements() { - font.applyFont(appTitleTextView, font.saralaBold); - font.applyFont(privacyConsentTitleTextView, font.saralaBold); - font.applyFont(privacyConsentDescriptionTextView, font.saralaRegular); - font.applyFont(privacyConsentAllowButton, font.saralaBold); - font.applyFont(appIdTitleTextView, font.saralaBold); - font.applyFont(appIdTextView, font.saralaRegular); - font.applyFont(loginUserButton, font.saralaBold); - font.applyFont(logoutUserButton, font.saralaBold); - font.applyFont(aliasTitleTextView, font.saralaBold); - font.applyFont(noAliasesTextView, font.saralaBold); - font.applyFont(emailHeaderTextView, font.saralaBold); - font.applyFont(noEmailsTextView, font.saralaBold); - font.applyFont(smsHeaderTextView, font.saralaBold); - font.applyFont(noSmssTextView, font.saralaBold); - font.applyFont(tagsTitleTextView, font.saralaBold); - font.applyFont(noTagsTextView, font.saralaBold); - font.applyFont(addTagButton, font.saralaBold); - font.applyFont(pushNotificationTitleTextView, font.saralaBold); - font.applyFont(sendPushNotificationTitleTextView, font.saralaBold); - font.applyFont(outcomeTitleTextView, font.saralaBold); - font.applyFont(sendOutcomeButton, font.saralaBold); - font.applyFont(triggersTitleTextView, font.saralaBold); - font.applyFont(noTriggersTextView, font.saralaBold); - font.applyFont(addTriggerButton, font.saralaBold); - font.applyFont(inAppMessagingTitleTextView, font.saralaBold); - font.applyFont(sendInAppMessagingTitleTextView, font.saralaBold); - font.applyFont(locationTitleTextView, font.saralaBold); - font.applyFont(locationSharedTextView, font.saralaBold); - font.applyFont(locationSharedDescriptionTextView, font.saralaRegular); - font.applyFont(promptLocationButton, font.saralaBold); - font.applyFont(promptPushButton, font.saralaBold); - font.applyFont(pushSubscriptionEnabledTitleTextView, font.saralaBold); - font.applyFont(pushSubscriptionIdTitleTextView, font.saralaBold); - font.applyFont(pauseInAppMessagesTextView, font.saralaBold); - font.applyFont(pauseInAppMessagesDescriptionTextView, font.saralaRegular); - font.applyFont(revokeConsentButton, font.saralaBold); - - boolean hasConsent = SharedPreferenceUtil.getUserPrivacyConsent(context); - setupConsentLayout(hasConsent); - - if (hasConsent) - postPrivacyConsentSetup(); - - return this; - } - - @Override - public void setupToolbar() { - toolbar.setTitle(Text.EMPTY); - getAppCompatActivity().setSupportActionBar(toolbar); - } - - @Override - public void networkConnected() { - - } - - @Override - public void networkDisconnected() { - - } - - @Override - public void onNotificationPermissionChange(@Nullable boolean permission) { - refreshSubscriptionState(); - } - - private void setupConsentLayout(boolean hasConsent) { - int consentVisibility = hasConsent ? View.GONE : View.VISIBLE; - int scrollVisibility = hasConsent ? View.VISIBLE : View.GONE; - privacyConsentLinearLayout.setVisibility(consentVisibility); - nestedScrollView.setVisibility(scrollVisibility); - appBarLayout.setExpanded(true); - - privacyConsentAllowButton.setOnClickListener(v -> { - togglePrivacyConsent(true); - postPrivacyConsentSetup(); - }); - } - - private void postPrivacyConsentSetup() { - setupLayout(); - refreshState(); - } - - public void setupLayout() { - setupScrollView(); - setupAppLayout(); - setupUserLayout(); - setupLocationLayout(); - setupPushNotificationLayout(); - setupInAppMessagingLayout(); - } - - private void setupScrollView() { - nestedScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() { - @Override - public void onScrollChanged() { - int scrollY = nestedScrollView.getScrollY(); - shouldScrollTop = scrollY != 0; - } - }); - } - - private void setupAppLayout() { - revokeConsentButton.setOnClickListener(v -> togglePrivacyConsent(false)); - - loginUserButton.setOnClickListener(v -> { - dialog.createUpdateAlertDialog("", Dialog.DialogAction.LOGIN, ProfileUtil.FieldType.EXTERNAL_USER_ID, new UpdateAlertDialogCallback() { - @Override - public void onSuccess(String update) { - if (update != null && !update.isEmpty()) { - OneSignal.login(update); - refreshState(); - } - } - - @Override - public void onFailure() { - - } - }); - }); - - logoutUserButton.setOnClickListener(v -> { - OneSignal.logout(); - refreshState(); - }); - } - - private void setupUserLayout() { - setupAliasLayout(); - setupEmailLayout(); - setupSMSLayout(); - setupTagsLayout(); - setupOutcomeLayout(); - setupTriggersLayout(); - } - - private void setupAliasLayout() { - setupAliasesRecyclerView(); - addAliasButton.setOnClickListener(v -> dialog.createAddPairAlertDialog("Add Alias", ProfileUtil.FieldType.ALIAS, new AddPairAlertDialogCallback() { - @Override - public void onSuccess(Pair pair) { - if (pair.second == null || pair.second.toString().isEmpty()) { - OneSignal.getUser().removeAlias(pair.first); - aliasSet.remove(pair.first); - toaster.makeCustomViewToast("Deleted alias " + pair.first, ToastType.SUCCESS); - } else { - OneSignal.getUser().addAlias(pair.first, pair.second.toString()); - aliasSet.put(pair.first, pair.second); - toaster.makeCustomViewToast("Added alias " + pair.first, ToastType.SUCCESS); - } - - refreshAliasRecyclerView(); - } - - @Override - public void onFailure() { - refreshAliasRecyclerView(); - } - })); - } - - private void setupAliasesRecyclerView() { - recyclerViewBuilder.setupRecyclerView(aliasesRecyclerView, 20, false, true); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); - aliasesRecyclerView.setLayoutManager(linearLayoutManager); - aliasesRecyclerViewAdapter = new PairRecyclerViewAdapter(context, aliasArrayList, new PairItemActionCallback() { - @Override - public void onLongClick(String key) { - OneSignal.getUser().removeAlias(key); - aliasSet.remove(key); - refreshAliasRecyclerView(); - toaster.makeCustomViewToast("Deleted alias " + key, ToastType.SUCCESS); - } - }); - aliasesRecyclerView.setAdapter(aliasesRecyclerViewAdapter); - } - - private void refreshAliasRecyclerView() { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - aliasArrayList.clear(); - aliasArrayList.addAll(aliasSet.entrySet()); - - if (aliasArrayList.size() > 0) { - animate.toggleAnimationView(false, View.GONE, aliasesRecyclerView, noAliasesTextView); - } else { - animate.toggleAnimationView(true, View.GONE, aliasesRecyclerView, noAliasesTextView); - } - - aliasesRecyclerViewAdapter.notifyDataSetChanged(); - } - }); - } - - @Override - public void onPushSubscriptionChange(@NonNull PushSubscriptionChangedState state) { - refreshSubscriptionState(); - } - - private class DummySubscription implements ISubscription { - - private String _id; - public DummySubscription(String id) { - _id = id; - } - - @NonNull - @Override - public String getId() { - return _id; - } - } - - private void setupEmailLayout() { - setupEmailRecyclerView(); - - MainActivityViewModel self = this; - addEmailButton.setOnClickListener(v -> dialog.createUpdateAlertDialog("", Dialog.DialogAction.ADD, ProfileUtil.FieldType.EMAIL, new UpdateAlertDialogCallback() { - @Override - public void onSuccess(String value) { - if (value != null && !value.isEmpty()) { - OneSignal.getUser().addEmail(value); - emailArrayList.add(new DummySubscription(value)); - toaster.makeCustomViewToast("Added email " + value, ToastType.SUCCESS); - } - - refreshEmailRecyclerView(); - } - - @Override - public void onFailure() { - refreshEmailRecyclerView(); - } - })); - } - - private void setupSMSLayout() { - setupSMSRecyclerView(); - - MainActivityViewModel self = this; - addSMSButton.setOnClickListener(v -> dialog.createUpdateAlertDialog("", Dialog.DialogAction.ADD, ProfileUtil.FieldType.SMS, new UpdateAlertDialogCallback() { - @Override - public void onSuccess(String value) { - if (value != null && !value.isEmpty()) { - OneSignal.getUser().addSms(value); - smsArrayList.add(new DummySubscription(value)); - toaster.makeCustomViewToast("Added SMS " + value, ToastType.SUCCESS); - } - - refreshSMSRecyclerView(); - } - - @Override - public void onFailure() { - refreshSMSRecyclerView(); - } - })); - } - - private void setupTagsLayout() { - setupTagRecyclerView(); - - addTagButton.setOnClickListener(v -> dialog.createAddPairAlertDialog("Add Tag", ProfileUtil.FieldType.TAG, new AddPairAlertDialogCallback() { - @Override - public void onSuccess(Pair pair) { - if (pair.second == null || pair.second.toString().isEmpty()) { - OneSignal.getUser().removeTag(pair.first); - tagSet.remove(pair.first); - toaster.makeCustomViewToast("Deleted tag " + pair.first, ToastType.SUCCESS); - } else { - OneSignal.getUser().addTag(pair.first, pair.second.toString()); - tagSet.put(pair.first, pair.second); - toaster.makeCustomViewToast("Added tag " + pair.first, ToastType.SUCCESS); - } - - refreshTagRecyclerView(); - } - - @Override - public void onFailure() { - refreshTagRecyclerView(); - } - })); - } - - private void setupTagRecyclerView() { - recyclerViewBuilder.setupRecyclerView(tagsRecyclerView, 20, false, true); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); - tagsRecyclerView.setLayoutManager(linearLayoutManager); - tagPairRecyclerViewAdapter = new PairRecyclerViewAdapter(context, tagArrayList, new PairItemActionCallback() { - @Override - public void onLongClick(String key) { - OneSignal.getUser().removeTag(key); - tagSet.remove(key); - refreshTagRecyclerView(); - toaster.makeCustomViewToast("Deleted tag " + key, ToastType.SUCCESS); - } - }); - tagsRecyclerView.setAdapter(tagPairRecyclerViewAdapter); - } - - private void refreshTagRecyclerView() { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - tagArrayList.clear(); - tagArrayList.addAll(tagSet.entrySet()); - - if (tagArrayList.size() > 0) { - animate.toggleAnimationView(false, View.GONE, tagsRecyclerView, noTagsTextView); - } else { - animate.toggleAnimationView(true, View.GONE, tagsRecyclerView, noTagsTextView); - } - - tagPairRecyclerViewAdapter.notifyDataSetChanged(); - } - }); - } - - private void setupEmailRecyclerView() { - recyclerViewBuilder.setupRecyclerView(emailsRecyclerView, 20, false, true); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); - emailsRecyclerView.setLayoutManager(linearLayoutManager); - emailsRecyclerViewAdapter = new SubscriptionRecyclerViewAdapter(context, emailArrayList, value -> { - String email = ((DummySubscription)value).getId(); - OneSignal.getUser().removeEmail(email); - emailArrayList.remove(value); - refreshEmailRecyclerView(); - toaster.makeCustomViewToast("Deleted email " + email, ToastType.SUCCESS); - }); - emailsRecyclerView.setAdapter(emailsRecyclerViewAdapter); - } - - private void refreshEmailRecyclerView() { - getActivity().runOnUiThread(() -> { - if (emailArrayList.size() > 0) { - animate.toggleAnimationView(false, View.GONE, emailsRecyclerView, noEmailsTextView); - } else { - animate.toggleAnimationView(true, View.GONE, emailsRecyclerView, noEmailsTextView); - } - - emailsRecyclerViewAdapter.notifyDataSetChanged(); - }); - } - - private void setupSMSRecyclerView() { - recyclerViewBuilder.setupRecyclerView(smssRecyclerView, 20, false, true); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); - smssRecyclerView.setLayoutManager(linearLayoutManager); - smssRecyclerViewAdapter = new SubscriptionRecyclerViewAdapter(context, smsArrayList, value -> { - String number = ((DummySubscription)value).getId(); - OneSignal.getUser().removeSms(number); - smsArrayList.remove(value); - refreshSMSRecyclerView(); - toaster.makeCustomViewToast("Deleted SMS " + number, ToastType.SUCCESS); - }); - smssRecyclerView.setAdapter(smssRecyclerViewAdapter); - } - - private void refreshSMSRecyclerView() { - getActivity().runOnUiThread(() -> { - if (smsArrayList.size() > 0) { - animate.toggleAnimationView(false, View.GONE, smssRecyclerView, noSmssTextView); - } else { - animate.toggleAnimationView(true, View.GONE, smssRecyclerView, noSmssTextView); - } - - smssRecyclerViewAdapter.notifyDataSetChanged(); - }); - } - - private void setupOutcomeLayout() { - sendOutcomeButton.setOnClickListener(v -> dialog.createSendOutcomeAlertDialog("Select an Outcome Type...", new SendOutcomeAlertDialogCallback() { - @Override - public boolean onSuccess(OutcomeEvent outcomeEvent, String name, String value) { - switch (outcomeEvent) { - case OUTCOME: - OneSignal.getSession().addOutcome(name); - break; - case UNIQUE_OUTCOME: - OneSignal.getSession().addUniqueOutcome(name); - break; - case OUTCOME_WITH_VALUE: - if (value.isEmpty()) { - toaster.makeCustomViewToast("Please enter an outcome value!", ToastType.ERROR); - return false; - } - - OneSignal.getSession().addOutcomeWithValue(name, Float.parseFloat(value)); - break; - } - - return true; - } - - @Override - public void onFailure() { - - } - })); - } - - private void setupTriggersLayout() { - animate.toggleAnimationView(true, View.GONE, triggersRecyclerView, noTriggersTextView); - - setupTriggerRecyclerView(); - addTriggerButton.setOnClickListener(v -> dialog.createAddPairAlertDialog("Add Trigger", ProfileUtil.FieldType.TRIGGER, new AddPairAlertDialogCallback() { - @Override - public void onSuccess(Pair pair) { - if (pair.second == null || pair.second.toString().isEmpty()) { - OneSignal.getInAppMessages().removeTrigger(pair.first); - triggerSet.remove(pair.first); - toaster.makeCustomViewToast("Deleted trigger " + pair.first, ToastType.SUCCESS); - } else { - OneSignal.getInAppMessages().addTrigger(pair.first, pair.second.toString()); - triggerSet.put(pair.first, pair.second); - toaster.makeCustomViewToast("Added trigger " + pair.first, ToastType.SUCCESS); - } - - refreshTriggerRecyclerView(); - } - - @Override - public void onFailure() { - refreshTriggerRecyclerView(); - } - })); - } - - private void setupTriggerRecyclerView() { - recyclerViewBuilder.setupRecyclerView(triggersRecyclerView, 20, false, true); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); - triggersRecyclerView.setLayoutManager(linearLayoutManager); - triggerPairRecyclerViewAdapter = new PairRecyclerViewAdapter(context, triggerArrayList, key -> { - OneSignal.getInAppMessages().removeTrigger(key); - triggerSet.remove(key); - - refreshTriggerRecyclerView(); - - toaster.makeCustomViewToast("Deleted trigger " + key, ToastType.SUCCESS); - }); - triggersRecyclerView.setAdapter(triggerPairRecyclerViewAdapter); - } - - private void refreshTriggerRecyclerView() { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - triggerArrayList.clear(); - triggerArrayList.addAll(triggerSet.entrySet()); - - if (triggerArrayList.size() > 0) { - animate.toggleAnimationView(false, View.GONE, triggersRecyclerView, noTriggersTextView); - } else { - animate.toggleAnimationView(true, View.GONE, triggersRecyclerView, noTriggersTextView); - } - - triggerPairRecyclerViewAdapter.notifyDataSetChanged(); - } - }); - } - - private void setupLocationLayout() { - setupLocationSharedSwitch(); - setupPromptLocationButton(); - } - - private void setupLocationSharedSwitch() { - locationSharedRelativeLayout.setOnClickListener(v -> { - boolean isLocationShared = !locationSharedSwitch.isChecked(); - locationSharedSwitch.setChecked(isLocationShared); - SharedPreferenceUtil.cacheLocationSharedStatus(context, isLocationShared); - }); - - boolean isLocationShared = SharedPreferenceUtil.getCachedLocationSharedStatus(context); - locationSharedSwitch.setChecked(isLocationShared); - locationSharedSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferenceUtil.cacheLocationSharedStatus(context, isChecked); - OneSignal.getLocation().setShared(isChecked); - }); - } - - private void setupPromptLocationButton() { - promptLocationButton.setOnClickListener(v -> { - OneSignal.getLocation().requestPermission(Continue.none()); - }); - } - - private void setupPushNotificationLayout() { - setupSubscriptionSwitch(); - setupPromptPushButton(); - setupSendNotificationsLayout(); - } - - private void setupSubscriptionSwitch() { - refreshSubscriptionState(); - // Add a listener to toggle the push notification enablement for the push subscription. - pushSubscriptionEnabledSwitch.setOnClickListener(v -> { - IPushSubscription subscription = OneSignal.getUser().getPushSubscription(); - if(pushSubscriptionEnabledSwitch.isChecked()) { - subscription.optIn(); - } - else { - subscription.optOut(); - } - }); - } - - private void setupPromptPushButton() { - promptPushButton.setOnClickListener(v -> { - ExecutorService executor = Executors.newSingleThreadExecutor(); - @SuppressLint({"NewApi", "LocalSuppress"}) CompletableFuture future = CompletableFuture.runAsync(() -> { - OneSignal.getNotifications().requestPermission(true, Continue.none()); - }, executor); - future.join(); // Waits for the task to complete - executor.shutdown(); - }); - } - - private void refreshSubscriptionState() { - boolean isPermissionEnabled = OneSignal.getNotifications().getPermission(); - IPushSubscription pushSubscription = OneSignal.getUser().getPushSubscription(); - - pushSubscriptionIdTextView.setText(pushSubscription.getId()); - promptPushBottonLayout.setVisibility(isPermissionEnabled ? View.GONE : View.VISIBLE); - pushSubscriptionEnabledRelativeLayout.setEnabled(isPermissionEnabled); - pushSubscriptionEnabledSwitch.setEnabled(isPermissionEnabled); - pushSubscriptionEnabledSwitch.setChecked(pushSubscription.getOptedIn()); - } - - private void setupSendNotificationsLayout() { - recyclerViewBuilder.setupRecyclerView(pushNotificationRecyclerView, 16, false, true); - GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 2); - pushNotificationRecyclerView.setLayoutManager(gridLayoutManager); - - pushNotificationRecyclerViewAdapter = new NotificationRecyclerViewAdapter(context, Notification.values()); - pushNotificationRecyclerView.setAdapter(pushNotificationRecyclerViewAdapter); - } - - private void setupInAppMessagingLayout() { - setupPauseInAppMessagesSwitch(); - setupSendIAMsLayout(); - } - - private void setupPauseInAppMessagesSwitch() { - pauseInAppMessagesRelativeLayout.setOnClickListener(v -> { - boolean isInAppMessagesPaused = pauseInAppMessagesSwitch.isChecked(); - pauseInAppMessagesSwitch.setChecked(!isInAppMessagesPaused); - }); - - pauseInAppMessagesSwitch.setChecked(SharedPreferenceUtil.getCachedInAppMessagingPausedStatus(context)); - pauseInAppMessagesSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - OneSignal.getInAppMessages().setPaused(isChecked); - SharedPreferenceUtil.cacheInAppMessagingPausedStatus(context, isChecked); - }); - } - - private void setupSendIAMsLayout() { - recyclerViewBuilder.setupRecyclerView(inAppMessagingRecyclerView, 4, false, true); - GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 2); - inAppMessagingRecyclerView.setLayoutManager(gridLayoutManager); - - inAppMessagingRecyclerViewAdapter = new InAppMessageRecyclerViewAdapter(context, InAppMessage.values()); - inAppMessagingRecyclerView.setAdapter(inAppMessagingRecyclerViewAdapter); - } - - public boolean scrollToTopIfAvailable() { - if (shouldScrollTop) { - if (nestedScrollView != null) { - nestedScrollView.smoothScrollTo(0, 0); - appBarLayout.setExpanded(true); - } - } - return shouldScrollTop; - } - - private String getOneSignalAppId() { - return SharedPreferenceUtil.getOneSignalAppId(context); - } - - private void togglePrivacyConsent(boolean hasConsent) { - OneSignal.setConsentGiven(hasConsent); - SharedPreferenceUtil.cacheUserPrivacyConsent(context, hasConsent); - - shouldScrollTop = hasConsent; - - int consentVisibility = hasConsent ? View.GONE : View.VISIBLE; - int scrollVisibility = hasConsent ? View.VISIBLE : View.GONE; - privacyConsentLinearLayout.setVisibility(consentVisibility); - nestedScrollView.setVisibility(scrollVisibility); - - appBarLayout.setExpanded(true); - } - - - - private void refreshState() { - // appId - appIdTextView.setText(getOneSignalAppId()); - - // aliases -// aliasSet.clear(); -// for (Map.Entry aliasEntry :OneSignal.getUser().getAliases().entrySet()) { -// aliasSet.put(aliasEntry.getKey(), aliasEntry.getValue()); -// } -// refreshAliasRecyclerView(); - - // email subscriptions -// emailArrayList.clear(); -// List emailSubs = OneSignal.getUser().getSubscriptions().getEmails(); -// for (IEmailSubscription emailSub: emailSubs) { -// emailArrayList.add(emailSub); -// } -// refreshEmailRecyclerView(); - - // sms subscriptions -// smsArrayList.clear(); -// List smsSubs = OneSignal.getUser().getSubscriptions().getSmss(); -// for (ISmsSubscription smsSub: smsSubs) { -// smsArrayList.add(smsSub); -// } -// refreshSMSRecyclerView(); - - // tags -// tagSet.clear(); -// for (Map.Entry tagEntry :OneSignal.getUser().getTags().entrySet()) { -// tagSet.put(tagEntry.getKey(), tagEntry.getValue()); -// } -// refreshTagRecyclerView(); - - // triggers - triggerSet.clear(); - // triggers are not persisted, they are always "starting from scratch" - refreshTriggerRecyclerView(); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/SplashActivityViewModel.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/SplashActivityViewModel.java deleted file mode 100644 index c3116b9058..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/model/SplashActivityViewModel.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.onesignal.sdktest.model; - -import android.app.Activity; -import android.content.Context; -import android.os.Handler; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; - -import com.onesignal.OneSignal; -import com.onesignal.sdktest.constant.Tag; -import com.onesignal.sdktest.constant.Text; -import com.onesignal.sdktest.util.IntentTo; -import com.onesignal.sdktest.util.SharedPreferenceUtil; - -public class SplashActivityViewModel implements ActivityViewModel { - - private IntentTo intentTo; - - private Context context; - - private boolean[] tasks = {false, false, false}; - - @Override - public Activity getActivity() { - return (Activity) context; - } - - @Override - public AppCompatActivity getAppCompatActivity() { - return (AppCompatActivity) context; - } - - @Override - public ActivityViewModel onActivityCreated(Context context) { - this.context = context; - intentTo = new IntentTo(context); - setupOneSignalSDK(); - return this; - } - - @Override - public ActivityViewModel setupInterfaceElements() { - return this; - } - - @Override - public void setupToolbar() { - - } - - @Override - public void networkConnected() { - - } - - @Override - public void networkDisconnected() { - - } - - private void setupOneSignalSDK() { - boolean privacyConsent = true; - - OneSignal.setConsentRequired(privacyConsent); - - boolean isLocationShared = SharedPreferenceUtil.getCachedLocationSharedStatus(context); - OneSignal.getLocation().setShared(isLocationShared); - - boolean isInAppMessagingPaused = SharedPreferenceUtil.getCachedInAppMessagingPausedStatus(context); - OneSignal.getInAppMessages().setPaused(isInAppMessagingPaused); - - Log.d(Tag.LOG_TAG, Text.PRIVACY_CONSENT_REQUIRED_SET + ": " + privacyConsent); - -// boolean isEmailCached = attemptSignIn(new EmailUpdateCallback() { -// @Override -// public void onSuccess() { -// tasks[0] = true; -// attemptEnterApplication(); -// } -// -// @Override -// public void onFailure() { -// tasks[0] = true; -// attemptEnterApplication(); -// } -// }); -// if (!isEmailCached) { - tasks[0] = true; - attemptEnterApplication(); -// } - - new Thread() { - public void run() { - boolean isExternalUserIdCached = attemptExternalUserId(); - tasks[1] = true; - - attemptEnterApplication(); - } - }.start(); - - new Thread() { - public void run() { - boolean hasConsent = SharedPreferenceUtil.getUserPrivacyConsent(context); - // TODO() -// OneSignal.provideUserConsent(hasConsent); - tasks[2] = true; - - attemptEnterApplication(); - } - }.start(); - } - -// public boolean attemptSignIn(EmailUpdateCallback callback) { -// boolean isEmailCached = SharedPreferenceUtil.exists(context, SharedPreferenceUtil.USER_EMAIL_SHARED_PREF); -// if (isEmailCached) { -// String email = SharedPreferenceUtil.getCachedUserEmail(context); -// currentUser.setEmail(email, callback); -// } -// return isEmailCached; -// return false; -// } - - public boolean attemptExternalUserId() { - boolean isExternalUserIdCached = SharedPreferenceUtil.exists(context, SharedPreferenceUtil.USER_EXTERNAL_USER_ID_SHARED_PREF); - if (isExternalUserIdCached) { - String externalUserId = SharedPreferenceUtil.getCachedUserExternalUserId(context); -// currentUser.setExternalUserId(context, externalUserId); - } - return isExternalUserIdCached; - } - - private void attemptEnterApplication() { - for (boolean task : tasks) { - if (!task) - return; - } - - ((Activity) context).runOnUiThread(() -> new Handler().postDelayed(() -> intentTo.mainActivity(), 1000)); - } - - @Override - public void onNotificationPermissionChange(@Nullable boolean permission) { - - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationData.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationData.java deleted file mode 100644 index 411b912f22..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationData.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.onesignal.sdktest.notification; - -public class NotificationData { - - public static final String[][] GENERAL_DATA = { - new String[]{ - "Liked post", - "Michael DiCioccio liked your post!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fbell_red.png?alt=media&token=c80c4e76-1fd7-4912-93f4-f1aee1d98b20", - "" - }, - new String[]{ - "Birthdays", - "Say happy birthday to Rodrigo and 5 others!", - "https://images.vexels.com/media/users/3/147226/isolated/preview/068af50eededd7a739aac52d8e509ab5-three-candles-birthday-cake-icon-by-vexels.png", - "" - }, - new String[]{ - "New Post", - "Neil just posted for the first time in while, check it out!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fbell_red.png?alt=media&token=c80c4e76-1fd7-4912-93f4-f1aee1d98b20", - "" - } - }; - - public static final String[][] GREETING_DATA = { - new String[]{ - "", - "Welcome to Nike!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fhuman-greeting-red.png?alt=media&token=cb9f3418-db61-443c-955a-57e664d30271", - "" - }, - new String[]{ - "", - "Welcome to Adidas!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fhuman-greeting-red.png?alt=media&token=cb9f3418-db61-443c-955a-57e664d30271", - "" - }, - new String[]{ - "", - "Welcome to Sandra’s cooking blog!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fhuman-greeting-red.png?alt=media&token=cb9f3418-db61-443c-955a-57e664d30271", - "" - } - }; - - public static final String[][] PROMOTION_DATA = { - new String[]{ - "", - "Get 20% off site-wide!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fbrightness-percent-red.png?alt=media&token=9e43c45e-8bcc-413e-8a42-612020c406ba", - "" - }, - new String[]{ - "", - "Half-off all shoes today only!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fbrightness-percent-red.png?alt=media&token=9e43c45e-8bcc-413e-8a42-612020c406ba", - "" - }, - new String[]{ - "", - "3 hour flash sale!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fbrightness-percent-red.png?alt=media&token=9e43c45e-8bcc-413e-8a42-612020c406ba", - "" - } - }; - - public static final String[][] BREAKING_NEWS_DATA = { - new String[]{ - "The rap game wont be the same", - "Nipsey Hussle shot dead in his own hometown!", - "https://pbs.twimg.com/profile_images/719602655337656321/kQUzR2Es_400x400.jpg", - "https://lab.fm/wp-content/uploads/2019/04/nipsey-hussle-cipriani-diamond-ball-2018-nyc-credit-jstone-shutterstock@1800x1013.jpg" - }, - new String[]{ - "CNN being bought by Fox?", - "Fox has shown an increasing interest in purchasing Fox and because of some other deals this year it could actually happen!", - "https://www.thewrap.com/sites/default/wp-content/uploads/files/2013/Jul/08/101771/gallupinside.png", - "https://i.ytimg.com/vi/C8YBKBuX43Q/maxresdefault.jpg" - }, - new String[]{ - "Teslas next venture!", - "Tesla releasing fully autonomous driving service!", - "https://i.etsystatic.com/13567406/r/il/6657a5/1083941709/il_794xN.1083941709_k3vi.jpg", - "https://electrek.co/wp-content/uploads/sites/3/2018/01/screen-shot-2018-01-04-at-12-59-25-pm.jpg?quality=82&strip=all&w=1600" - } - }; - - public static final String[][] ABANDONED_CART_DATA = { - new String[]{ - "", - "You have some shoes left in your cart!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fcart-red.png?alt=media&token=3e9ca206-540c-4275-8f21-1840e9cba930", - "" - }, - new String[]{ - "", - "Still want to buy the dress you saw?", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fcart-red.png?alt=media&token=3e9ca206-540c-4275-8f21-1840e9cba930", - "" - }, - new String[]{ - "", - "20% off the shoes you saw today.", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fcart-red.png?alt=media&token=3e9ca206-540c-4275-8f21-1840e9cba930", - "" - } - }; - - public static final String[][] NEW_POST_DATA = { - new String[]{ - "", - "I just published a new blog post!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fimage-red.png?alt=media&token=3f44fd3d-27a5-4d05-9544-423edf2f6284", - "" - }, - new String[]{ - "", - "Come check out my new blog post on aliens!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fimage-red.png?alt=media&token=3f44fd3d-27a5-4d05-9544-423edf2f6284", - "" - }, - new String[]{ - "", - "10 places you have to see before you die.", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fimage-red.png?alt=media&token=3f44fd3d-27a5-4d05-9544-423edf2f6284", - "" - } - }; - - public static final String[][] RE_ENGAGEMENT_DATA = { - new String[]{ - "", - "Your friend George just joined Facebook", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fgesture-tap-red.png?alt=media&token=8ea7f6db-18e4-4fdd-aabf-ac97f04522fd", - "" - }, - new String[]{ - "", - "Can you beat level 23?", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fgesture-tap-red.png?alt=media&token=8ea7f6db-18e4-4fdd-aabf-ac97f04522fd", - "" - }, - new String[]{ - "", - "Check out our Fall collection!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fgesture-tap-red.png?alt=media&token=8ea7f6db-18e4-4fdd-aabf-ac97f04522fd", - "" - } - }; - - public static final String[][] RATING_DATA = { - new String[]{ - "", - "How was your food/experience at Chipotle?", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fstar-red.png?alt=media&token=e18e99ce-96ad-4ee5-b0b9-40c7f90613d1", - "" - }, - new String[]{ - "", - "Rate your experience with Amazon.", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fstar-red.png?alt=media&token=e18e99ce-96ad-4ee5-b0b9-40c7f90613d1", - "" - }, - new String[]{ - "", - "Let your Lyft driver know how the ride was.", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fstar-red.png?alt=media&token=e18e99ce-96ad-4ee5-b0b9-40c7f90613d1", - "" - } - }; - - public static final String[][] LOCATION_DATA = { - new String[]{ - "", - "Your friend Neil is sharing his location with you.", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fmap-marker-red.png?alt=media&token=42631ed8-053b-4c28-a193-4d144001a474", - "" - }, - new String[]{ - "", - "An armed robbery occured at 34th and Market Street today.", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fmap-marker-red.png?alt=media&token=42631ed8-053b-4c28-a193-4d144001a474", - "" - }, - new String[]{ - "", - "Logic is performing at the Wells Fargo Center this weekend!", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fmap-marker-red.png?alt=media&token=42631ed8-053b-4c28-a193-4d144001a474", - "" - } - }; - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationServiceExtension.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationServiceExtension.java deleted file mode 100644 index 6f1c603da5..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/NotificationServiceExtension.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.onesignal.sdktest.notification; - -import android.util.Log; - -import com.onesignal.notifications.IActionButton; -import com.onesignal.notifications.IDisplayableMutableNotification; -import com.onesignal.notifications.INotificationReceivedEvent; -import com.onesignal.notifications.INotificationServiceExtension; -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.constant.Tag; - -public class NotificationServiceExtension implements INotificationServiceExtension { - - @Override - public void onNotificationReceived(INotificationReceivedEvent event) { - Log.v(Tag.LOG_TAG, "IRemoteNotificationReceivedHandler fired" + " with INotificationReceivedEvent: " + event.toString()); - - IDisplayableMutableNotification notification = event.getNotification(); - - if (notification.getActionButtons() != null) { - for (IActionButton button : notification.getActionButtons()) { - Log.v(Tag.LOG_TAG, "ActionButton: " + button.toString()); - } - } - - notification.setExtender(builder -> builder.setColor(event.getContext().getResources().getColor(R.color.colorPrimary))); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/OneSignalNotificationSender.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/OneSignalNotificationSender.java deleted file mode 100644 index 96c870c13a..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/notification/OneSignalNotificationSender.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.onesignal.sdktest.notification; - -import android.os.Build; -import android.util.Log; - -import com.onesignal.OneSignal; -import com.onesignal.user.subscriptions.IPushSubscription; -import com.onesignal.sdktest.constant.Tag; -import com.onesignal.sdktest.type.Notification; - -import org.json.JSONObject; - -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; - -public class OneSignalNotificationSender { - - public static void setAppId(String appId) { - _appId = appId; - } - private static String _appId = ""; - - /** - * Send a notification to this device through the OneSignal Platform. Note this form of the - * API should not be used in a production setting as it is not safe. Rather, the device should - * make an API call to it's own backend, which should handle the API call to OneSignal (where it - * can safely provide the application API Key). - * - * @param notification The notification that is to be sent. - */ - public static void sendDeviceNotification(final Notification notification) { - new Thread(() -> { - IPushSubscription subscription = OneSignal.getUser().getPushSubscription(); - - if (!subscription.getOptedIn()) - return; - - int pos = notification.getTemplatePos(); - try { - JSONObject notificationContent = new JSONObject("{'app_id': '" + _appId + "', 'include_player_ids': ['" + subscription.getId() + "']," + - "'headings': {'en': '" + notification.getTitle(pos) + "'}," + - "'contents': {'en': '" + notification.getMessage(pos) + "'}," + - "'small_icon': '" + notification.getSmallIconRes() + "'," + - "'large_icon': '" + notification.getLargeIconUrl(pos) + "'," + - "'big_picture': '" + notification.getBigPictureUrl(pos) + "'," + - "'android_group': '" + notification.getGroup() + "'," + - "'buttons': " + notification.getButtons() + "," + - "'android_led_color': 'FFE9444E'," + - "'android_accent_color': 'FFE9444E'," + - "'android_sound': 'nil'}"); - - HttpURLConnection con = (HttpURLConnection) new URL("https://onesignal.com/api/v1/notifications").openConnection(); - - con.setUseCaches(false); - con.setConnectTimeout(30000); - con.setReadTimeout(30000); - con.setRequestProperty("Accept", "application/vnd.onesignal.v1+json"); - con.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); - con.setRequestMethod("POST"); - con.setDoOutput(true); - con.setDoInput(true); - - byte[] outputBytes = notificationContent.toString().getBytes(StandardCharsets.UTF_8); - con.setFixedLengthStreamingMode(outputBytes.length); - con.getOutputStream().write(outputBytes); - - int httpResponse = con.getResponseCode(); - - if(httpResponse == HttpURLConnection.HTTP_ACCEPTED || httpResponse == HttpURLConnection.HTTP_OK) { - InputStream inputStream = con.getInputStream(); - Scanner scanner = new Scanner(inputStream, "UTF-8"); - String responseStr = ""; - if (scanner.useDelimiter("\\A").hasNext()) - responseStr = scanner.next(); - scanner.close(); - Log.d(Tag.LOG_TAG, "Success sending notification: " + responseStr); - } - else { - InputStream inputStream = con.getErrorStream(); - Scanner scanner = new Scanner(inputStream, "UTF-8"); - String responseStr = ""; - if (scanner.useDelimiter("\\A").hasNext()) - responseStr = scanner.next(); - scanner.close(); - Log.d(Tag.LOG_TAG, "Failure sending notification: " + responseStr); - } - } catch (Exception e) { - e.printStackTrace(); - } - }).start(); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/InAppMessage.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/InAppMessage.java deleted file mode 100644 index eca0e9c3b2..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/InAppMessage.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.onesignal.sdktest.type; - -public enum InAppMessage { - - TOP_BANNER("Top Banner", "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/IN_APP_MESSAGE_ICON%2Ftop_banner_icon.png?alt=media&token=88fe1f2f-01c0-44fb-bb88-c1a4bf00969d"), - BOTTOM_BANNER("Bottom Banner", "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/IN_APP_MESSAGE_ICON%2Fbottom_banner_icon.png?alt=media&token=a8faee09-137d-4049-b41b-0dc3c66b0d8e"), - CENTER_MODAL("Center Modal", "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/IN_APP_MESSAGE_ICON%2Fcenter_modal_icon.png?alt=media&token=c0998b1e-5bab-404a-bfaa-e432ec298bdf"), - FULL_SCREEN("Full Screen", "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/IN_APP_MESSAGE_ICON%2Ffull_screen_icon.png?alt=media&token=db3bb9ea-c61c-4df7-9f1f-6673316a4395"); - - private final String title; - private final String icon; - - InAppMessage(String title, String iconUrl) { - this.title = title; - this.icon = iconUrl; - } - - public String getTitle() { - return title; - } - - public String getIconUrl() { - return icon; - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/Notification.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/Notification.java deleted file mode 100644 index 4c1ddb4093..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/Notification.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.onesignal.sdktest.type; - -import com.onesignal.sdktest.notification.NotificationData; - -public enum Notification { - - GENERAL("General", - NotificationData.GENERAL_DATA, - "ic_bell_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fbell.png?alt=media&token=73c2bdd9-355f-42bb-80d7-aead737a9dbc", - "[]", - true, - 0), - - GREETING("Greetings", - NotificationData.GREETING_DATA, - "ic_human_greeting_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fhuman-greeting.png?alt=media&token=178bd69d-634e-40b3-ac32-b56c88e6cd6a", - "[]", - true, - 0), - - PROMOTIONS("Promotions", - NotificationData.PROMOTION_DATA, - "ic_brightness_percent_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fbrightness-percent.png?alt=media&token=6a8b4348-ad51-4e3a-97d0-4deb46b1576e", - "[]", - true, - 0), - - BREAKING_NEWS("Breaking News", - NotificationData.BREAKING_NEWS_DATA, - "ic_newspaper_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fnewspaper.png?alt=media&token=053e419b-14f1-4f0d-a439-bb5b46d1b917", - "[{'id': 'id1', 'text': 'view', 'icon': ''}, {'id': 'id2', 'text': 'save', 'icon': ''}, {'id': 'id3', 'text': 'share', 'icon': ''}]", - true, - 0), - - ABANDONED_CART("Abandoned Cart", - NotificationData.ABANDONED_CART_DATA, - "ic_cart_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fcart.png?alt=media&token=cf7f4d13-6aa2-4824-9b2f-42e5f33f545b", - "[]", - true, - 0), - - NEW_POST("New Post", - NotificationData.NEW_POST_DATA, - "ic_image_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fimage.png?alt=media&token=6fb66f31-23de-4c76-a2ff-da40d46ebf15", - "[]", - true, - 0), - - RE_ENGAGEMENT("Re-Engagement", - NotificationData.RE_ENGAGEMENT_DATA, - "ic_gesture_tap_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fgesture-tap.png?alt=media&token=045ddcb9-f4e5-457e-8577-baa0e264e227", - "[]", - true, - 0), - - RATING("Rating", - NotificationData.RATING_DATA, - "ic_star_white_24dp", - "https://firebasestorage.googleapis.com/v0/b/onesignaltest-e7802.appspot.com/o/NOTIFICATION_ICON%2Fstar.png?alt=media&token=da0987e5-a635-488f-9fba-24a1ee5d704a", - "[]", - true, - 0); - - private final String title; - private final String[][] data; - private final String smallIconRes; - private final String iconUrl; - private final String buttons; - private final boolean shouldShow; - private int pos; - - Notification(String title, String[][] data, String smallIconRes, String iconUrl, String buttons, boolean shouldShow, int pos) { - this.title = title; - this.data = data; - this.smallIconRes = smallIconRes; - this.iconUrl = iconUrl; - this.buttons = buttons; - this.shouldShow = shouldShow; - this.pos = pos; - } - - public String getGroup() { - return title; - } - - public String getTitle(int pos) { - return data[pos][0]; - } - - public String getMessage(int pos) { - return data[pos][1]; - } - - public String getSmallIconRes() { - return smallIconRes; - } - - public String getIconUrl() { - return iconUrl; - } - - public String getLargeIconUrl(int pos) { - return data[pos][2]; - } - - public String getBigPictureUrl(int pos) { - return data[pos][3]; - } - - public String getButtons() { - return buttons; - } - - public boolean shouldShow() { - return shouldShow; - } - - public int getTemplatePos() { - if (pos > 2) pos = 0; - return pos++; - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/OutcomeEvent.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/OutcomeEvent.java deleted file mode 100644 index d1cc277411..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/OutcomeEvent.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.onesignal.sdktest.type; - -public enum OutcomeEvent { - - OUTCOME("Normal Outcome"), - UNIQUE_OUTCOME("Unique Outcome"), - OUTCOME_WITH_VALUE("Outcome with Value"); - - String title; - - OutcomeEvent(String title) { - this.title = title; - } - - public static OutcomeEvent enumFromTitleString(String title) { - for (OutcomeEvent outcomeEvent : OutcomeEvent.values()) { - if (title.equals(outcomeEvent.getTitle())) - return outcomeEvent; - } - - return null; - } - - public String getTitle() { - return title; - } - - @Override - public String toString() { - return getTitle(); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/ToastType.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/ToastType.java deleted file mode 100644 index 3da88ad419..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/type/ToastType.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.onesignal.sdktest.type; - -import com.onesignal.sdktest.R; - -public enum ToastType { - - INFO(R.drawable.ic_information_white_48dp, R.color.colorToastInfo), - SUCCESS(R.drawable.ic_checkbox_marked_circle_white_48dp, R.color.colorToastSuccess), - WARNING(R.drawable.ic_alert_white_48dp, R.color.colorToastWarning), - ERROR(R.drawable.ic_alert_octagon_white_48dp, R.color.colorToastError); - - private int icon; - private int color; - - ToastType(int icon, int color) { - this.icon = icon; - this.color = color; - } - - public int getIcon() { - return icon; - } - - public int getColor() { - return color; - } - -} \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/CustomAlertDialogBuilder.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/CustomAlertDialogBuilder.java deleted file mode 100644 index 3350c70356..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/CustomAlertDialogBuilder.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.onesignal.sdktest.ui; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.view.KeyEvent; -import android.view.View; -import android.widget.Button; - -import com.onesignal.sdktest.R; - -public class CustomAlertDialogBuilder extends AlertDialog.Builder { - - /* - * Click listener - */ - private DialogInterface.OnClickListener mPositiveButtonListener = null; - private DialogInterface.OnClickListener mNegativeButtonListener = null; - private DialogInterface.OnClickListener mNeutralButtonListener = null; - private DialogInterface.OnDismissListener mOnDismissListener = null; - - /* - * Buttons text - */ - private CharSequence mPositiveButtonText = null; - private CharSequence mNegativeButtonText = null; - private CharSequence mNeutralButtonText = null; - - - /* - * Buttons - */ - private Button positiveButton; - private Button negativeButton; - - private View view; - private Boolean mCancelOnTouchOutside = null; - private boolean isCancelable = true; - - public CustomAlertDialogBuilder(Context context, View view) { - super(context); - this.view = view; - } - - public CustomAlertDialogBuilder setOnDismissListener(DialogInterface.OnDismissListener listener) { - mOnDismissListener = listener; - return this; - } - - @Override - public CustomAlertDialogBuilder setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener) { - mNegativeButtonListener = listener; - mNegativeButtonText = text; - return this; - } - - @Override - public CustomAlertDialogBuilder setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) { - mNeutralButtonListener = listener; - mNeutralButtonText = text; - return this; - } - - @Override - public CustomAlertDialogBuilder setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener) { - mPositiveButtonListener = listener; - mPositiveButtonText = text; - return this; - } - - @Override - public CustomAlertDialogBuilder setNegativeButton(int textId, DialogInterface.OnClickListener listener) { - setNegativeButton(getContext().getString(textId), listener); - return this; - } - - @Override - public CustomAlertDialogBuilder setNeutralButton(int textId, DialogInterface.OnClickListener listener) { - setNeutralButton(getContext().getString(textId), listener); - return this; - } - - @Override - public CustomAlertDialogBuilder setPositiveButton(int textId, DialogInterface.OnClickListener listener) { - setPositiveButton(getContext().getString(textId), listener); - return this; - } - - public CustomAlertDialogBuilder setCanceledOnTouchOutside(boolean cancelOnTouchOutside) { - mCancelOnTouchOutside = cancelOnTouchOutside; - return this; - } - - @Override - public AlertDialog create() { - throw new UnsupportedOperationException("CustomAlertDialogBuilder.create(): use show() instead.."); - } - - public Button getPositiveButtonElement() { - return positiveButton; - } - - public Button getNegativeButtonElement() { - return negativeButton; - } - - public void setIsCancelable(boolean isCancelable) { - this.isCancelable = isCancelable; - } - - @Override - public AlertDialog show() { - AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getContext(), R.style.WhiteThemeAlertDialog); - alertDialogBuilder.setView(view); - alertDialogBuilder.setOnKeyListener(new DialogInterface.OnKeyListener() { - @Override - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - return !isCancelable; - } - return false; - } - }); - final AlertDialog alertDialog = alertDialogBuilder.create(); - - DialogInterface.OnClickListener emptyOnClickListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { } - }; - - // Enable buttons (needed for Android 1.6) - otherwise later getButton() returns null - if (mPositiveButtonText != null) { - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, mPositiveButtonText, emptyOnClickListener); - } - - if (mNegativeButtonText != null) { - alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, mNegativeButtonText, emptyOnClickListener); - } - - if (mNeutralButtonText != null) { - alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, mNeutralButtonText, emptyOnClickListener); - } - - // Set OnDismissListener if available - if (mOnDismissListener != null) { - alertDialog.setOnDismissListener(mOnDismissListener); - } - - if (mCancelOnTouchOutside != null) { - alertDialog.setCanceledOnTouchOutside(mCancelOnTouchOutside); - } - - alertDialog.show(); - - // Set the OnClickListener directly on the Button object, avoiding the auto-dismiss feature - // IMPORTANT: this must be after alert.show(), otherwise the button doesn't exist.. - // If the listener are null don't do anything so that they will still dismiss the dialog when clicked - if (mPositiveButtonListener != null) { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - mPositiveButtonListener.onClick(alertDialog, AlertDialog.BUTTON_POSITIVE); - } - }); - } - - if (mNegativeButtonListener != null) { - alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - mNegativeButtonListener.onClick(alertDialog, AlertDialog.BUTTON_NEGATIVE); - } - }); - } - - if (mNeutralButtonListener != null) { - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - mNeutralButtonListener.onClick(alertDialog, AlertDialog.BUTTON_NEUTRAL); - } - }); - } - - positiveButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); - negativeButton = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE); - - return alertDialog; - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/FixAppBarLayoutBehavior.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/FixAppBarLayoutBehavior.java deleted file mode 100644 index cc3a0e7ffe..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/FixAppBarLayoutBehavior.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.onesignal.sdktest.ui; - -import android.content.Context; -import com.google.android.material.appbar.AppBarLayout; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.ViewCompat; -import android.util.AttributeSet; -import android.view.View; - - -public class FixAppBarLayoutBehavior extends AppBarLayout.Behavior { - - public FixAppBarLayoutBehavior() { - super(); - } - - public FixAppBarLayoutBehavior(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, - int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { - super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, - dxUnconsumed, dyUnconsumed, type); - stopNestedScrollIfNeeded(dyUnconsumed, child, target, type); - } - - @Override - public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, - View target, int dx, int dy, int[] consumed, int type) { - super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); - stopNestedScrollIfNeeded(dy, child, target, type); - } - - private void stopNestedScrollIfNeeded(int dy, AppBarLayout child, View target, int type) { - if (type == ViewCompat.TYPE_NON_TOUCH) { - final int currOffset = getTopAndBottomOffset(); - if ((dy < 0 && currOffset == 0) - || (dy > 0 && currOffset == -child.getTotalScrollRange())) { - ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH); - } - } - } -} \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/RecyclerViewBuilder.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/RecyclerViewBuilder.java deleted file mode 100644 index 236f940386..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/ui/RecyclerViewBuilder.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.onesignal.sdktest.ui; - -import android.content.Context; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.RecyclerView; - -import com.onesignal.sdktest.R; - -public class RecyclerViewBuilder { - - private Context context; - - - public RecyclerViewBuilder(Context context) { - this.context = context; - } - - public void setupRecyclerView(RecyclerView recyclerView, int viewCache, boolean hasDivider, boolean isVertical) { - DefaultItemAnimator defaultItemAnimator = new DefaultItemAnimator(); - - int orientation = isVertical ? DividerItemDecoration.VERTICAL : DividerItemDecoration.HORIZONTAL; - DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, orientation); - - int divider = hasDivider ? R.drawable.divider : R.drawable.no_divider; - dividerItemDecoration.setDrawable(context.getResources().getDrawable(divider)); - - recyclerView.setItemAnimator(defaultItemAnimator); - recyclerView.addItemDecoration(dividerItemDecoration); - recyclerView.setHasFixedSize(false); - recyclerView.setItemViewCacheSize(viewCache); - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Animate.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Animate.java deleted file mode 100644 index a2d994f4ee..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Animate.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.onesignal.sdktest.util; - -import android.view.View; - -public class Animate { - - public Animate() { - } - - public void toggleAnimationView(boolean showAnimation, int visibility, View view, View anim) { - int viewVis = showAnimation ? visibility : View.VISIBLE; - int animVis = showAnimation ? View.VISIBLE : visibility; - - view.setVisibility(viewVis); - anim.setVisibility(animVis); - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Dialog.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Dialog.java deleted file mode 100644 index 797d06372e..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Dialog.java +++ /dev/null @@ -1,374 +0,0 @@ -package com.onesignal.sdktest.util; - -import android.content.Context; -import android.content.DialogInterface; -import android.graphics.drawable.Drawable; -import com.google.android.material.textfield.TextInputLayout; -import androidx.cardview.widget.CardView; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.adapter.EnumSelectionRecyclerViewAdapter; -import com.onesignal.sdktest.callback.AddPairAlertDialogCallback; -import com.onesignal.sdktest.callback.EnumSelectionCallback; -import com.onesignal.sdktest.callback.SendOutcomeAlertDialogCallback; -import com.onesignal.sdktest.callback.UpdateAlertDialogCallback; -import com.onesignal.sdktest.constant.Text; -import com.onesignal.sdktest.type.OutcomeEvent; -import com.onesignal.sdktest.type.ToastType; -import com.onesignal.sdktest.ui.CustomAlertDialogBuilder; -import com.onesignal.sdktest.ui.RecyclerViewBuilder; - -public class Dialog { - - private Font font; - private LayoutInflater layoutInflater; - private RecyclerViewBuilder recyclerViewBuilder; - private Toaster toaster; - - private Context context; - - public Dialog(Context context) { - this.context = context; - - font = new Font(context); - layoutInflater = LayoutInflater.from(context); - recyclerViewBuilder = new RecyclerViewBuilder(context); - toaster = new Toaster(context); - } - - public enum DialogAction { - LOGIN, - ADD, - UPDATE - } - - /** - * Create an AlertDialog for when the user updates a single value field - * Click OK to verify and update the field being updated - */ - public void createUpdateAlertDialog(final String content, final DialogAction action, final ProfileUtil.FieldType field, final UpdateAlertDialogCallback callback) { - View updateAlertDialogView = layoutInflater.inflate(R.layout.update_alert_dialog_layout, null, false); - - final TextInputLayout updateAlertDialogTextInputLayout = updateAlertDialogView.findViewById(R.id.update_alert_dialog_text_input_layout); - final EditText updateAlertDialogEditText = updateAlertDialogView.findViewById(R.id.update_alert_dialog_edit_text); - final ProgressBar updateAlertDialogProgressBar = updateAlertDialogView.findViewById(R.id.update_alert_dialog_progress_bar); - - String hintTitle = action == DialogAction.LOGIN ? field.getTitle() : "New " + field.getTitle(); - updateAlertDialogTextInputLayout.setHint(hintTitle); - updateAlertDialogEditText.setText(content); - - font.applyFont(updateAlertDialogTextInputLayout, font.saralaBold); - font.applyFont(updateAlertDialogEditText, font.saralaBold); - - final CustomAlertDialogBuilder updateAlertDialog = new CustomAlertDialogBuilder(context, updateAlertDialogView); - updateAlertDialog.setView(updateAlertDialogView); - updateAlertDialog.setIsCancelable(true); - updateAlertDialog.setCanceledOnTouchOutside(false); - - String buttonText = Text.BUTTON_UPDATE; - - switch (action) { - case ADD: - buttonText = Text.BUTTON_ADD; - break; - case UPDATE: - buttonText = Text.BUTTON_UPDATE; - break; - case LOGIN: - buttonText = Text.BUTTON_LOGIN; - break; - } - updateAlertDialog.setPositiveButton(buttonText, new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, int which) { - toggleUpdateAlertDialogAttributes(true); - - final String newContent = updateAlertDialogEditText.getText().toString().trim(); - - if (newContent.equals(content)) { - InterfaceUtil.hideKeyboardFrom(context, updateAlertDialogEditText); - - toggleUpdateAlertDialogAttributes(false); - dialog.dismiss(); - } else if (ProfileUtil.isContentValid(field, updateAlertDialogTextInputLayout)) { - InterfaceUtil.hideKeyboardFrom(context, updateAlertDialogEditText); - toggleUpdateAlertDialogAttributes(false); - dialog.dismiss(); - callback.onSuccess(newContent); - } else { - toggleUpdateAlertDialogAttributes(false); - } - } - - private void toggleUpdateAlertDialogAttributes(boolean disableAttributes) { - int progressVisibility = disableAttributes ? View.VISIBLE : View.GONE; - updateAlertDialogProgressBar.setVisibility(progressVisibility); - - int buttonVisibility = disableAttributes ? View.GONE : View.VISIBLE; - updateAlertDialog.getPositiveButtonElement().setVisibility(buttonVisibility); - updateAlertDialog.getNegativeButtonElement().setVisibility(buttonVisibility); - - updateAlertDialog.getPositiveButtonElement().setEnabled(!disableAttributes); - updateAlertDialog.getNegativeButtonElement().setEnabled(!disableAttributes); - updateAlertDialog.setIsCancelable(!disableAttributes); - } - }).setNegativeButton(Text.BUTTON_CANCEL, null); - updateAlertDialog.show(); - updateAlertDialogEditText.requestFocus(); - } - - /** - * Create an AlertDialog for when the user updates a single value field - * Click OK to verify and update the field being updated - */ - public void createAddPairAlertDialog(String content, final ProfileUtil.FieldType field, final AddPairAlertDialogCallback callback) { - final View addPairAlertDialogView = layoutInflater.inflate(R.layout.add_pair_alert_dialog_layout, null, false); - - final TextView addPairAlertDialogTitleTextView = addPairAlertDialogView.findViewById(R.id.add_pair_alert_dialog_title_text_view); - final TextInputLayout addPairAlertDialogKeyTextInputLayout = addPairAlertDialogView.findViewById(R.id.add_pair_alert_dialog_key_text_input_layout); - final EditText addPairAlertDialogKeyEditText = addPairAlertDialogView.findViewById(R.id.add_pair_alert_dialog_key_edit_text); - final TextInputLayout addPairAlertDialogValueTextInputLayout = addPairAlertDialogView.findViewById(R.id.add_pair_alert_dialog_value_text_input_layout); - final EditText addPairAlertDialogValueEditText = addPairAlertDialogView.findViewById(R.id.add_pair_alert_dialog_value_edit_text); - final ProgressBar addPairAlertDialogProgressBar = addPairAlertDialogView.findViewById(R.id.add_pair_alert_dialog_progress_bar); - - addPairAlertDialogKeyTextInputLayout.setHint("Key"); - addPairAlertDialogValueTextInputLayout.setHint("Value"); - addPairAlertDialogTitleTextView.setText(content); - - font.applyFont(addPairAlertDialogTitleTextView, font.saralaBold); - font.applyFont(addPairAlertDialogKeyTextInputLayout, font.saralaBold); - font.applyFont(addPairAlertDialogKeyEditText, font.saralaBold); - font.applyFont(addPairAlertDialogValueTextInputLayout, font.saralaBold); - font.applyFont(addPairAlertDialogValueEditText, font.saralaBold); - - final CustomAlertDialogBuilder addPairAlertDialog = new CustomAlertDialogBuilder(context, addPairAlertDialogView); - addPairAlertDialog.setView(addPairAlertDialogView); - addPairAlertDialog.setIsCancelable(true); - addPairAlertDialog.setCanceledOnTouchOutside(false); - addPairAlertDialog.setPositiveButton(Text.BUTTON_ADD, new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, int which) { - toggleUpdateAlertDialogAttributes(true); - - final String pairKey = addPairAlertDialogKeyEditText.getText().toString().trim(); - final String pairStringValue = addPairAlertDialogValueEditText.getText().toString().trim(); - - Object pairValue = pairStringValue; - if (Util.isBoolean(pairStringValue)) { - pairValue = Boolean.parseBoolean(pairStringValue.toLowerCase()); - } else if (Util.isInteger(pairStringValue)) { - pairValue = Long.parseLong(pairStringValue); - } else if (Util.isFloat(pairStringValue)) { - pairValue = Double.parseDouble(pairStringValue); - } - - if (ProfileUtil.isContentValid(field, addPairAlertDialogKeyTextInputLayout)) { - InterfaceUtil.hideKeyboardFrom(context, addPairAlertDialogView); - dialog.dismiss(); - callback.onSuccess(new Pair<>(pairKey, pairValue)); - } else { - toggleUpdateAlertDialogAttributes(false); - } - } - - private void toggleUpdateAlertDialogAttributes(boolean disableAttributes) { - int progressVisibility = disableAttributes ? View.VISIBLE : View.GONE; - addPairAlertDialogProgressBar.setVisibility(progressVisibility); - - int buttonVisibility = disableAttributes ? View.GONE : View.VISIBLE; - addPairAlertDialog.getPositiveButtonElement().setVisibility(buttonVisibility); - addPairAlertDialog.getNegativeButtonElement().setVisibility(buttonVisibility); - - addPairAlertDialog.getPositiveButtonElement().setEnabled(!disableAttributes); - addPairAlertDialog.getNegativeButtonElement().setEnabled(!disableAttributes); - addPairAlertDialog.setIsCancelable(!disableAttributes); - } - - }).setNegativeButton(Text.BUTTON_CANCEL, null); - addPairAlertDialog.show(); - addPairAlertDialogKeyEditText.requestFocus(); - } - - public void createSendOutcomeAlertDialog(final String content, final SendOutcomeAlertDialogCallback callback) { - final View sendOutcomeAlertDialogView = layoutInflater.inflate(R.layout.send_outcome_alert_dialog_layout, null, false); - - final CardView sendOutcomeDialogTitleCardView = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_selection_card_view); - final RelativeLayout sendOutcomeDialogTitleRelativeLayout = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_selection_relative_layout); - final TextView sendOutcomeDialogTitleTextView = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_selection_text_view); - final ImageView sendOutcomeDialogTitleArrowImageView = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_selection_arrow_image_view); - final RecyclerView sendOutcomeDialogSelectionRecyclerView = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_selection_recycler_view); - final LinearLayout sendOutcomeDialogSelectionContentLinearLayout = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_content_linear_layout); - final TextInputLayout sendOutcomeDialogNameTextInputLayout = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_name_text_input_layout); - final EditText sendOutcomeDialogNameEditText = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_name_edit_text); - final TextInputLayout sendOutcomeDialogValueTextInputLayout = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_value_text_input_layout); - final EditText sendOutcomeDialogValueEditText = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_value_edit_text); - final ProgressBar sendOutcomeDialogProgressBar = sendOutcomeAlertDialogView.findViewById(R.id.send_outcome_alert_dialog_progress_bar); - - sendOutcomeDialogNameTextInputLayout.setHint("Name"); - sendOutcomeDialogValueTextInputLayout.setHint("Value"); - sendOutcomeDialogTitleTextView.setText(content); - - font.applyFont(sendOutcomeDialogTitleTextView, font.saralaBold); - font.applyFont(sendOutcomeDialogNameTextInputLayout, font.saralaBold); - font.applyFont(sendOutcomeDialogValueTextInputLayout, font.saralaBold); - - sendOutcomeDialogTitleCardView.setCardElevation(8f); - - recyclerViewBuilder.setupRecyclerView(sendOutcomeDialogSelectionRecyclerView, 3, false, true); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context); - sendOutcomeDialogSelectionRecyclerView.setLayoutManager(linearLayoutManager); - EnumSelectionRecyclerViewAdapter enumSelectionRecyclerViewAdapter = new EnumSelectionRecyclerViewAdapter(context, OutcomeEvent.values(), new EnumSelectionCallback() { - @Override - public void onSelection(String title) { - int nameVisibility = View.GONE; - int valueVisibility = View.GONE; - - OutcomeEvent outcomeEvent = OutcomeEvent.enumFromTitleString(title); - if (outcomeEvent == null) { - Drawable arrow = context.getResources().getDrawable(R.drawable.ic_chevron_down_white_48dp); - sendOutcomeDialogTitleArrowImageView.setImageDrawable(arrow); - sendOutcomeDialogTitleCardView.setCardElevation(0f); - sendOutcomeDialogSelectionRecyclerView.setVisibility(View.GONE); - sendOutcomeDialogSelectionContentLinearLayout.setVisibility(View.GONE); - - sendOutcomeDialogNameEditText.setVisibility(nameVisibility); - sendOutcomeDialogValueTextInputLayout.setVisibility(valueVisibility); - return; - } - - switch(outcomeEvent) { - case OUTCOME: - case UNIQUE_OUTCOME: - nameVisibility = View.VISIBLE; - break; - case OUTCOME_WITH_VALUE: - nameVisibility = View.VISIBLE; - valueVisibility = View.VISIBLE; - break; - } - - sendOutcomeDialogTitleTextView.setText(outcomeEvent.getTitle()); - - Drawable arrow = context.getResources().getDrawable(R.drawable.ic_chevron_down_white_48dp); - sendOutcomeDialogTitleArrowImageView.setImageDrawable(arrow); - sendOutcomeDialogTitleCardView.setCardElevation(0f); - sendOutcomeDialogSelectionRecyclerView.setVisibility(View.GONE); - - sendOutcomeDialogSelectionContentLinearLayout.setVisibility(View.VISIBLE); - sendOutcomeDialogNameTextInputLayout.setVisibility(nameVisibility); - sendOutcomeDialogNameEditText.setVisibility(nameVisibility); - sendOutcomeDialogValueTextInputLayout.setVisibility(valueVisibility); - sendOutcomeDialogValueEditText.setVisibility(valueVisibility); - } - }); - sendOutcomeDialogSelectionRecyclerView.setAdapter(enumSelectionRecyclerViewAdapter); - - sendOutcomeDialogTitleRelativeLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean showMenu = sendOutcomeDialogSelectionRecyclerView.getVisibility() == View.GONE; - Drawable arrow = context.getResources().getDrawable(showMenu ? R.drawable.ic_chevron_up_white_48dp : R.drawable.ic_chevron_down_white_48dp); - int menuVisibility = showMenu ? View.VISIBLE : View.GONE; - int contentVisibility = showMenu ? View.GONE : View.VISIBLE; - float shadow = showMenu ? 8f : 0f; - - sendOutcomeDialogTitleArrowImageView.setImageDrawable(arrow); - sendOutcomeDialogTitleCardView.setCardElevation(shadow); - sendOutcomeDialogSelectionRecyclerView.setVisibility(menuVisibility); - sendOutcomeDialogSelectionContentLinearLayout.setVisibility(contentVisibility); - - - int nameVisibility = View.GONE; - int valueVisibility = View.GONE; - - String selectedTitle = sendOutcomeDialogTitleTextView.getText().toString(); - OutcomeEvent outcomeEvent = OutcomeEvent.enumFromTitleString(selectedTitle); - - if (outcomeEvent == null) { - sendOutcomeDialogSelectionContentLinearLayout.setVisibility(View.GONE); - return; - } - - if (!showMenu) { - switch (outcomeEvent) { - case OUTCOME: - case UNIQUE_OUTCOME: - nameVisibility = View.VISIBLE; - break; - case OUTCOME_WITH_VALUE: - nameVisibility = View.VISIBLE; - valueVisibility = View.VISIBLE; - break; - } - } - - sendOutcomeDialogSelectionContentLinearLayout.setVisibility(nameVisibility); - sendOutcomeDialogNameEditText.setVisibility(nameVisibility); - sendOutcomeDialogValueTextInputLayout.setVisibility(valueVisibility); - sendOutcomeDialogValueEditText.setVisibility(valueVisibility); - } - }); - - final CustomAlertDialogBuilder sendOutcomeAlertDialog = new CustomAlertDialogBuilder(context, sendOutcomeAlertDialogView); - sendOutcomeAlertDialog.setView(sendOutcomeAlertDialogView); - sendOutcomeAlertDialog.setIsCancelable(true); - sendOutcomeAlertDialog.setCanceledOnTouchOutside(false); - sendOutcomeAlertDialog.setPositiveButton(Text.BUTTON_SEND, new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, int which) { - toggleUpdateAlertDialogAttributes(true); - - String selectedTitle = sendOutcomeDialogTitleTextView.getText().toString(); - OutcomeEvent outcomeEvent = OutcomeEvent.enumFromTitleString(selectedTitle); - - if (outcomeEvent == null) { - toaster.makeCustomViewToast("Please select an outcome type!", ToastType.ERROR); - toggleUpdateAlertDialogAttributes(false); - return; - } - - String name = sendOutcomeDialogNameEditText.getText().toString().trim(); - String value = sendOutcomeDialogValueEditText.getText().toString().trim(); - - if (name.isEmpty()) { - toaster.makeCustomViewToast("Please enter an outcome name!", ToastType.ERROR); - toggleUpdateAlertDialogAttributes(false); - return; - } - - if(callback.onSuccess(outcomeEvent, name, value)) { - toggleUpdateAlertDialogAttributes(false); - dialog.dismiss(); - InterfaceUtil.hideKeyboardFrom(context, sendOutcomeAlertDialogView); - } - } - - private void toggleUpdateAlertDialogAttributes(boolean disableAttributes) { - int progressVisibility = disableAttributes ? View.VISIBLE : View.GONE; - sendOutcomeDialogProgressBar.setVisibility(progressVisibility); - - int buttonVisibility = disableAttributes ? View.GONE : View.VISIBLE; - sendOutcomeAlertDialog.getPositiveButtonElement().setVisibility(buttonVisibility); - sendOutcomeAlertDialog.getNegativeButtonElement().setVisibility(buttonVisibility); - - sendOutcomeAlertDialog.getPositiveButtonElement().setEnabled(!disableAttributes); - sendOutcomeAlertDialog.getNegativeButtonElement().setEnabled(!disableAttributes); - sendOutcomeAlertDialog.setIsCancelable(!disableAttributes); - } - - }).setNegativeButton(Text.BUTTON_CANCEL, null); - sendOutcomeAlertDialog.show(); - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Font.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Font.java deleted file mode 100644 index aff8304a73..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Font.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.onesignal.sdktest.util; - -import android.content.Context; -import android.graphics.Typeface; -import com.google.android.material.textfield.TextInputLayout; -import android.view.View; -import android.widget.TextView; - -public class Font { - - public Typeface saralaRegular; - public Typeface saralaBold; - - - public Font(Context context) { - saralaRegular = Typeface.createFromAsset(context.getAssets(), "fonts/Sarala-Regular.ttf"); - saralaBold = Typeface.createFromAsset(context.getAssets(), "fonts/Sarala-Bold.ttf"); - } - - public void applyFont(View view, Typeface typeface) { - if (view instanceof TextView) { - ((TextView) view).setTypeface(typeface); - } else if (view instanceof TextInputLayout) { - ((TextInputLayout) view).setTypeface(typeface); - } - } - -} \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/IntentTo.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/IntentTo.java deleted file mode 100644 index d99f53b339..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/IntentTo.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.onesignal.sdktest.util; - -import android.app.Activity; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Build; - -import androidx.annotation.RequiresApi; - -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.activity.MainActivity; - -public class IntentTo { - - private Context context; - - public IntentTo(Context context) { - this.context = context; - } - - @RequiresApi(api = Build.VERSION_CODES.N) - public void mainActivity() { - Intent mainActivityIntent = new Intent(context, MainActivity.class); - ComponentName componentName = mainActivityIntent.getComponent(); - mainActivityIntent = Intent.makeRestartActivityTask(componentName); - context.startActivity(mainActivityIntent); - ((Activity) context).overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/InterfaceUtil.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/InterfaceUtil.java deleted file mode 100644 index 48bf7b918f..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/InterfaceUtil.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.onesignal.sdktest.util; - -import android.app.Activity; -import android.content.Context; -import android.view.View; -import android.view.inputmethod.InputMethodManager; - -public class InterfaceUtil { - - public static float getScreenDensity(Context context) { - return context.getResources().getDisplayMetrics().density; - } - - public static int getScreenWidth(Context context) { - return ((Activity) context).getWindowManager().getDefaultDisplay().getWidth(); - } - - public static int getScreenHeight(Context context) { - return ((Activity) context).getWindowManager().getDefaultDisplay().getHeight(); - } - - public static void showKeyboardFrom(Context context) { - InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); - } - - public static void hideKeyboardFrom(Context context, View view) { - InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java deleted file mode 100644 index 9a026c5f47..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/ProfileUtil.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.onesignal.sdktest.util; - -import com.google.android.material.textfield.TextInputLayout; -import android.util.Patterns; - -import com.onesignal.sdktest.constant.Text; - -public class ProfileUtil { - - public enum FieldType { - - APP_ID("App Id"), - ALIAS("Alias"), - EMAIL("Email"), - SMS("SMS"), - EXTERNAL_USER_ID("External User Id"), - - TAG("Tags"), - TRIGGER("Triggers"); - - private final String title; - - FieldType(String title) { - this.title = title; - } - - public String getTitle() { - return title; - } - } - - private static boolean isAppIdValid(TextInputLayout appIdTextInputLayout) { - appIdTextInputLayout.setErrorEnabled(false); - if (appIdTextInputLayout.getEditText() != null) { - String appId = appIdTextInputLayout.getEditText().getText().toString().trim(); - if (appId.isEmpty()) { - appIdTextInputLayout.setError(Text.APP_ID_IS_REQUIRED); - return false; - } - if (appId.length() != 36) { - appIdTextInputLayout.setError(Text.INVALID_APP_ID); - return false; - } - } else { - appIdTextInputLayout.setError(Text.ERROR); - return false; - } - return true; - } - - private static boolean isAliasValid(TextInputLayout appIdTextInputLayout) { - appIdTextInputLayout.setErrorEnabled(false); - if (appIdTextInputLayout.getEditText() != null) { - String aliasLabel = appIdTextInputLayout.getEditText().getText().toString().trim(); - if (aliasLabel.isEmpty()) { - appIdTextInputLayout.setError(Text.ALIAS_LABEL_IS_REQUIRED); - return false; - } - } else { - appIdTextInputLayout.setError(Text.ERROR); - return false; - } - return true; - } - - public static boolean isEmailValid(TextInputLayout emailTextInputLayout) { - emailTextInputLayout.setErrorEnabled(false); - if (emailTextInputLayout.getEditText() != null) { - String email = emailTextInputLayout.getEditText().getText().toString().trim(); - if (email.isEmpty()) { - emailTextInputLayout.setError(Text.EMAIL_IS_REQUIRED); - return false; - } - if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) { - emailTextInputLayout.setError(Text.INVALID_EMAIL); - return false; - } - } else { - emailTextInputLayout.setError(Text.ERROR); - return false; - } - return true; - } - - public static boolean isSMSValid(TextInputLayout smsTextInputLayout) { - smsTextInputLayout.setErrorEnabled(false); - if (smsTextInputLayout.getEditText() != null) { - String smsNumber = smsTextInputLayout.getEditText().getText().toString().trim(); - if (smsNumber.isEmpty()) { - smsTextInputLayout.setError(Text.SMS_IS_REQUIRED); - return false; - } - } else { - smsTextInputLayout.setError(Text.ERROR); - return false; - } - return true; - } - - private static boolean isExternalUserIdValid(TextInputLayout externalUserIdTextInputLayout) { - externalUserIdTextInputLayout.setErrorEnabled(false); - if (externalUserIdTextInputLayout.getEditText() != null) { - String externalUserId = externalUserIdTextInputLayout.getEditText().getText().toString().trim(); - if (externalUserId.isEmpty()) { - externalUserIdTextInputLayout.setError(Text.EXTERNAL_USER_ID_IS_REQUIRED); - return false; - } - } else { - externalUserIdTextInputLayout.setError(Text.ERROR); - return false; - } - return true; - } - - private static boolean isKeyValid(TextInputLayout keyTextInputLayout) { - keyTextInputLayout.setErrorEnabled(false); - if (keyTextInputLayout.getEditText() != null) { - String key = keyTextInputLayout.getEditText().getText().toString().trim(); - if (key.isEmpty()) { - keyTextInputLayout.setError(Text.KEY_IS_REQUIRED); - return false; - } - } else { - keyTextInputLayout.setError(Text.ERROR); - return false; - } - return true; - } - - static boolean isContentValid(FieldType field, TextInputLayout alertDialogTextInputLayout) { - switch (field) { - case APP_ID: - return isAppIdValid(alertDialogTextInputLayout); - case ALIAS: - return isAliasValid(alertDialogTextInputLayout); - case EMAIL: - return isEmailValid(alertDialogTextInputLayout); - case SMS: - return isSMSValid(alertDialogTextInputLayout); - case EXTERNAL_USER_ID: - return isExternalUserIdValid(alertDialogTextInputLayout); - case TAG: - case TRIGGER: - return isKeyValid(alertDialogTextInputLayout); - } - return false; - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.java deleted file mode 100644 index 5cb286d7bf..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/SharedPreferenceUtil.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.onesignal.sdktest.util; - -import android.content.Context; -import android.content.SharedPreferences; - -import com.onesignal.sdktest.constant.Text; - -public class SharedPreferenceUtil { - - private static final String APP_SHARED_PREFS = "com.onesignal.sdktest"; - - public static final String OS_APP_ID_SHARED_PREF = "OS_APP_ID_SHARED_PREF"; - private static final String PRIVACY_CONSENT_SHARED_PREF = "PRIVACY_CONSENT_SHARED_PREF"; - public static final String USER_EXTERNAL_USER_ID_SHARED_PREF = "USER_EXTERNAL_USER_ID_SHARED_PREF"; - private static final String LOCATION_SHARED_PREF = "LOCATION_SHARED_PREF"; - private static final String IN_APP_MESSAGING_PAUSED_PREF = "IN_APP_MESSAGING_PAUSED_PREF"; - - private static SharedPreferences getSharedPreference(Context context) { - return context.getSharedPreferences(APP_SHARED_PREFS, Context.MODE_PRIVATE); - } - - public static boolean exists(Context context, String key) { - return getSharedPreference(context).contains(key); - } - - public static String getOneSignalAppId(Context context) { - return getSharedPreference(context).getString(OS_APP_ID_SHARED_PREF, "77e32082-ea27-42e3-a898-c72e141824ef"); - } - - public static boolean getUserPrivacyConsent(Context context) { - return getSharedPreference(context).getBoolean(PRIVACY_CONSENT_SHARED_PREF, false); - } - - public static String getCachedUserExternalUserId(Context context) { - return getSharedPreference(context).getString(USER_EXTERNAL_USER_ID_SHARED_PREF, Text.EMPTY); - } - - public static boolean getCachedLocationSharedStatus(Context context) { - return getSharedPreference(context).getBoolean(LOCATION_SHARED_PREF, false); - } - - public static boolean getCachedInAppMessagingPausedStatus(Context context) { - return getSharedPreference(context).getBoolean(IN_APP_MESSAGING_PAUSED_PREF, true); - } - - public static void cacheOneSignalAppId(Context context, String appId) { - getSharedPreference(context).edit().putString(OS_APP_ID_SHARED_PREF, appId).apply(); - } - - public static void cacheUserPrivacyConsent(Context context, boolean privacyConsent) { - getSharedPreference(context).edit().putBoolean(PRIVACY_CONSENT_SHARED_PREF, privacyConsent).apply(); - } - - public static void cacheUserExternalUserId(Context context, String userId) { - getSharedPreference(context).edit().putString(USER_EXTERNAL_USER_ID_SHARED_PREF, userId).apply(); - } - - public static void cacheLocationSharedStatus(Context context, boolean subscribed) { - getSharedPreference(context).edit().putBoolean(LOCATION_SHARED_PREF, subscribed).apply(); - } - - public static void cacheInAppMessagingPausedStatus(Context context, boolean paused) { - getSharedPreference(context).edit().putBoolean(IN_APP_MESSAGING_PAUSED_PREF, paused).apply(); - } -} diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Toaster.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Toaster.java deleted file mode 100644 index d41aafd1be..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Toaster.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.onesignal.sdktest.util; - -import android.app.Activity; -import android.content.Context; -import android.graphics.drawable.Drawable; -import androidx.cardview.widget.CardView; -import android.view.Gravity; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.onesignal.sdktest.R; -import com.onesignal.sdktest.constant.Text; -import com.onesignal.sdktest.type.ToastType; - -public class Toaster { - - private Context context; - private Font font; - - public Toaster(Context context) { - this.context = context; - - font = new Font(context); - } - - public void makeToast(String bread) { - Toast.makeText(context, bread, Toast.LENGTH_SHORT).show(); - } - - public void makeLongToast(String bread) { - Toast.makeText(context, bread, Toast.LENGTH_LONG).show(); - } - - public void makeCustomViewToast(String bread, ToastType toastType) { - View toastView = ((Activity) context).getLayoutInflater().inflate(R.layout.toaster_toast_card_layout, null, false); - CardView toastCardView = toastView.findViewById(R.id.toaster_toast_card_view); - ImageView toastIcon = toastView.findViewById(R.id.toaster_toast_image_view); - TextView toastTextView = toastView.findViewById(R.id.toaster_toast_text_view); - - int color = context.getResources().getColor(toastType.getColor()); - toastCardView.setCardBackgroundColor(color); - - toastTextView.setTypeface(font.saralaBold); - toastTextView.setText(bread); - - Drawable icon = context.getResources().getDrawable(toastType.getIcon()); - toastIcon.setImageDrawable(icon); - - Toast toast = Toast.makeText(context, Text.EMPTY, Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, (int) (InterfaceUtil.getScreenHeight(context) * 0.25f)); - toast.setView(toastView); - toast.show(); - } - -} \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Util.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Util.java deleted file mode 100644 index 737c927a78..0000000000 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/util/Util.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.onesignal.sdktest.util; - -public class Util { - - public static boolean isBoolean(String string) { - string = string.toLowerCase(); - return (string.equals("true") || string.equals("false")); - } - - public static boolean isInteger(String string) { - try { - Long.parseLong(string); - return true; - } catch (NumberFormatException e) { - return false; - } - } - - public static boolean isFloat(String string) { - try { - Double.parseDouble(string); - return true; - } catch (NumberFormatException e) { - return false; - } - } - -} diff --git a/Examples/OneSignalDemo/app/src/main/res/anim/fade_in.xml b/Examples/OneSignalDemo/app/src/main/res/anim/fade_in.xml deleted file mode 100644 index 508851c6d5..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/anim/fade_in.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/anim/fade_out.xml b/Examples/OneSignalDemo/app/src/main/res/anim/fade_out.xml deleted file mode 100644 index 3be6cf46b7..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/anim/fade_out.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_red_white.xml b/Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_red_white.xml deleted file mode 100644 index 9ee1d9ebe6..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_red_white.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_white_red.xml b/Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_white_red.xml deleted file mode 100644 index 80114e3322..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/drawable-v21/ripple_selector_white_red.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/Examples/OneSignalDemo/app/src/main/res/drawable/divider.xml b/Examples/OneSignalDemo/app/src/main/res/drawable/divider.xml deleted file mode 100644 index 453e94ec1c..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/drawable/divider.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/drawable/no_divider.xml b/Examples/OneSignalDemo/app/src/main/res/drawable/no_divider.xml deleted file mode 100644 index d4943f5968..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/drawable/no_divider.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_red_white.xml b/Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_red_white.xml deleted file mode 100644 index f3a53ae07f..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_red_white.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_white_red.xml b/Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_white_red.xml deleted file mode 100644 index b042e412eb..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/drawable/ripple_selector_white_red.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/layout/activity_secondary.xml b/Examples/OneSignalDemo/app/src/main/res/layout/activity_secondary.xml deleted file mode 100644 index 267817f25b..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/layout/activity_secondary.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/layout/add_pair_alert_dialog_layout.xml b/Examples/OneSignalDemo/app/src/main/res/layout/add_pair_alert_dialog_layout.xml deleted file mode 100644 index dd54164959..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/layout/add_pair_alert_dialog_layout.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/layout/enum_selection_recycler_view_item_layout.xml b/Examples/OneSignalDemo/app/src/main/res/layout/enum_selection_recycler_view_item_layout.xml deleted file mode 100644 index 6dbbc6d3a8..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/layout/enum_selection_recycler_view_item_layout.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml b/Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml deleted file mode 100644 index ee4ce93cb5..0000000000 --- a/Examples/OneSignalDemo/app/src/main/res/layout/main_activity_layout.xml +++ /dev/null @@ -1,1275 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -