Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add HTTP header OneSignal-Install-Id #2072

Merged
merged 4 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import com.onesignal.core.internal.config.impl.ConfigModelStoreListener
import com.onesignal.core.internal.database.IDatabaseProvider
import com.onesignal.core.internal.database.impl.DatabaseProvider
import com.onesignal.core.internal.device.IDeviceService
import com.onesignal.core.internal.device.IInstallIdService
import com.onesignal.core.internal.device.impl.DeviceService
import com.onesignal.core.internal.device.impl.InstallIdService
import com.onesignal.core.internal.http.IHttpClient
import com.onesignal.core.internal.http.impl.HttpClient
import com.onesignal.core.internal.http.impl.HttpConnectionFactory
Expand Down Expand Up @@ -53,6 +55,7 @@ internal class CoreModule : IModule {
builder.register<Time>().provides<ITime>()
builder.register<DatabaseProvider>().provides<IDatabaseProvider>()
builder.register<StartupService>().provides<StartupService>()
builder.register<InstallIdService>().provides<IInstallIdService>()

// Params (Config)
builder.register<ConfigModelStore>().provides<ConfigModelStore>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.onesignal.core.internal.device

import java.util.UUID

interface IInstallIdService {
/**
* WARNING: This may do disk I/O on the first call, so never call this from
* the main thread.
*/
suspend fun getId(): UUID
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.onesignal.core.internal.device.impl

import com.onesignal.core.internal.device.IInstallIdService
import com.onesignal.core.internal.preferences.IPreferencesService
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
import com.onesignal.core.internal.preferences.PreferenceStores
import java.util.UUID

/**
* Manages a persistent UUIDv4, generated once when app is first opened.
* Value is for a HTTP header, OneSignal-Install-Id, added on all calls made
* to OneSignal's backend. This allows the OneSignal's backend know where
* traffic is coming from, no matter if the SubscriptionId or OneSignalId
* changes or isn't available yet.
*/
internal class InstallIdService(
private val _prefs: IPreferencesService,
) : IInstallIdService {
private val currentId: UUID by lazy {
val idFromPrefs = _prefs.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID)
if (idFromPrefs != null) {
UUID.fromString(idFromPrefs)
} else {
val newId = UUID.randomUUID()
_prefs.saveString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID, newId.toString())
newId
}
}

/**
* WARNING: This may do disk I/O on the first call, so never call this from
* the main thread. Disk I/O is done inside of "currentId by lazy".
*/
override suspend fun getId() = currentId
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.onesignal.common.JSONUtils
import com.onesignal.common.OneSignalUtils
import com.onesignal.common.OneSignalWrapper
import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.core.internal.device.IInstallIdService
import com.onesignal.core.internal.http.HttpResponse
import com.onesignal.core.internal.http.IHttpClient
import com.onesignal.core.internal.preferences.IPreferencesService
Expand All @@ -23,6 +24,7 @@ import kotlinx.coroutines.withTimeout
import org.json.JSONObject
import java.net.ConnectException
import java.net.HttpURLConnection
import java.net.URL
import java.net.UnknownHostException
import java.util.Scanner
import javax.net.ssl.HttpsURLConnection
Expand All @@ -32,6 +34,7 @@ internal class HttpClient(
private val _prefs: IPreferencesService,
private val _configModelStore: ConfigModelStore,
private val _time: ITime,
private val _installIdService: IInstallIdService,
) : IHttpClient {
/**
* Delay making network requests until we reach this time.
Expand Down Expand Up @@ -149,6 +152,8 @@ internal class HttpClient(
con.setRequestProperty("OneSignal-Subscription-Id", subscriptionId)
}

con.setRequestProperty("OneSignal-Install-Id", _installIdService.getId().toString())

if (jsonBody != null) {
con.doInput = true
}
Expand All @@ -159,16 +164,14 @@ internal class HttpClient(
con.doOutput = true
}

logHTTPSent(con.requestMethod, con.url, jsonBody, con.requestProperties)

if (jsonBody != null) {
val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody)
Logging.debug("HttpClient: ${method ?: "GET"} $url - $strJsonBody")

val sendBytes = strJsonBody.toByteArray(charset("UTF-8"))
con.setFixedLengthStreamingMode(sendBytes.size)
val outputStream = con.outputStream
outputStream.write(sendBytes)
} else {
Logging.debug("HttpClient: ${method ?: "GET"} $url")
}

if (cacheKey != null) {
Expand All @@ -194,7 +197,7 @@ internal class HttpClient(
PreferenceStores.ONESIGNAL,
PreferenceOneSignalKeys.PREFS_OS_HTTP_CACHE_PREFIX + cacheKey,
)
Logging.debug("HttpClient: ${method ?: "GET"} $url - Using Cached response due to 304: " + cachedResponse)
Logging.debug("HttpClient: Got Response = ${method ?: "GET"} ${con.url} - Using Cached response due to 304: " + cachedResponse)

// TODO: SHOULD RETURN OK INSTEAD OF NOT_MODIFIED TO MAKE TRANSPARENT?
retVal = HttpResponse(httpResponse, cachedResponse, retryAfterSeconds = retryAfter)
Expand All @@ -204,12 +207,12 @@ internal class HttpClient(
val scanner = Scanner(inputStream, "UTF-8")
val json = if (scanner.useDelimiter("\\A").hasNext()) scanner.next() else ""
scanner.close()
Logging.debug("HttpClient: ${method ?: "GET"} $url - STATUS: $httpResponse JSON: " + json)
Logging.debug("HttpClient: Got Response = ${method ?: "GET"} ${con.url} - STATUS: $httpResponse - Body: " + json)

if (cacheKey != null) {
val eTag = con.getHeaderField("etag")
if (eTag != null) {
Logging.debug("HttpClient: Response has etag of $eTag so caching the response.")
Logging.debug("HttpClient: Got Response = Response has etag of $eTag so caching the response.")

_prefs.saveString(
PreferenceStores.ONESIGNAL,
Expand All @@ -227,7 +230,7 @@ internal class HttpClient(
retVal = HttpResponse(httpResponse, json, retryAfterSeconds = retryAfter)
}
else -> {
Logging.debug("HttpClient: ${method ?: "GET"} $url - FAILED STATUS: $httpResponse")
Logging.debug("HttpClient: Got Response = ${method ?: "GET"} ${con.url} - FAILED STATUS: $httpResponse")

var inputStream = con.errorStream
if (inputStream == null) {
Expand All @@ -240,9 +243,9 @@ internal class HttpClient(
jsonResponse =
if (scanner.useDelimiter("\\A").hasNext()) scanner.next() else ""
scanner.close()
Logging.warn("HttpClient: $method RECEIVED JSON: $jsonResponse")
Logging.warn("HttpClient: Got Response = $method - STATUS: $httpResponse - Body: $jsonResponse")
} else {
Logging.warn("HttpClient: $method HTTP Code: $httpResponse No response body!")
Logging.warn("HttpClient: Got Response = $method - STATUS: $httpResponse - No response body!")
}

retVal = HttpResponse(httpResponse, jsonResponse, retryAfterSeconds = retryAfter)
Expand Down Expand Up @@ -285,6 +288,18 @@ internal class HttpClient(
}
}

private fun logHTTPSent(
method: String?,
url: URL,
jsonBody: JSONObject?,
headers: Map<String, List<String>>,
) {
val headersStr = headers.entries.joinToString()
val methodStr = method ?: "GET"
val bodyStr = if (jsonBody != null) JSONUtils.toUnescapedEUIDString(jsonBody) else null
Logging.debug("HttpClient: Request Sent = $methodStr $url - Body: $bodyStr - Headers: $headersStr")
}

companion object {
private const val OS_API_VERSION = "1"
private const val OS_ACCEPT_HEADER = "application/vnd.onesignal.v$OS_API_VERSION+json"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ object PreferenceOneSignalKeys {
*/
const val PREFS_OS_ETAG_PREFIX = "PREFS_OS_ETAG_PREFIX_"

/**
* (String) A install id, a UUIDv4 generated once when app is first opened.
* Value is for a HTTP header, OneSignal-Install-Id, added on all calls
* made to OneSignal's backend.
*/
const val PREFS_OS_INSTALL_ID = "PREFS_OS_INSTALL_ID"

/**
* (String) A prefix key for retrieving the response for a given HTTP GET cache key. The cache
* key should be appended to this prefix.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.onesignal.core.internal.device

import com.onesignal.core.internal.device.impl.InstallIdService
import com.onesignal.mocks.MockPreferencesService
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class InstallIdServiceTests : FunSpec({
test("2 calls result in the same value") {
// Given
val service = InstallIdService(MockPreferencesService())

// When
val value1 = service.getId()
val value2 = service.getId()

// Then
value1 shouldBe value2
}

// Real world scenario we are testing is if we cold restart the app we get
// the same value
test("reads from shared prefs") {
// Given
val sharedPrefs = MockPreferencesService()

// When
val service1 = InstallIdService(sharedPrefs)
val value1 = service1.getId()
val service2 = InstallIdService(sharedPrefs)
val value2 = service2.getId()

// Then
value1 shouldBe value2
}
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.onesignal.core.internal.http

import com.onesignal.common.OneSignalUtils
import com.onesignal.core.internal.device.impl.InstallIdService
import com.onesignal.core.internal.http.impl.HttpClient
import com.onesignal.core.internal.time.impl.Time
import com.onesignal.debug.LogLevel
Expand All @@ -20,8 +21,9 @@ class Mocks {
internal val mockConfigModel = MockHelper.configModelStore()
internal val response = MockHttpConnectionFactory.MockResponse()
internal val factory = MockHttpConnectionFactory(response)
internal val installIdService = InstallIdService(MockPreferencesService())
internal val httpClient by lazy {
HttpClient(factory, MockPreferencesService(), mockConfigModel, Time())
HttpClient(factory, MockPreferencesService(), mockConfigModel, Time(), installIdService)
}
}

Expand Down Expand Up @@ -50,7 +52,7 @@ class HttpClientTests : FunSpec({
response.throwable should beInstanceOf<TimeoutCancellationException>()
}

test("SDKHeader is included in all requests") {
test("SDK Headers are included in all requests") {
// Given
val mocks = Mocks()
val httpClient = mocks.httpClient
Expand All @@ -65,6 +67,7 @@ class HttpClientTests : FunSpec({
// Then
for (connection in mocks.factory.connections) {
connection.getRequestProperty("SDK-Version") shouldBe "onesignal/android/${OneSignalUtils.SDK_VERSION}"
connection.getRequestProperty("OneSignal-Install-Id") shouldBe mocks.installIdService.getId().toString()
}
}

Expand Down
Loading