Skip to content

Commit 18a388c

Browse files
authored
Merge pull request #2072 from OneSignal/feat/add-install-id-http-header
[Feat] Add HTTP header OneSignal-Install-Id
2 parents 702d13c + 37487e0 commit 18a388c

File tree

7 files changed

+122
-12
lines changed

7 files changed

+122
-12
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import com.onesignal.core.internal.config.impl.ConfigModelStoreListener
1313
import com.onesignal.core.internal.database.IDatabaseProvider
1414
import com.onesignal.core.internal.database.impl.DatabaseProvider
1515
import com.onesignal.core.internal.device.IDeviceService
16+
import com.onesignal.core.internal.device.IInstallIdService
1617
import com.onesignal.core.internal.device.impl.DeviceService
18+
import com.onesignal.core.internal.device.impl.InstallIdService
1719
import com.onesignal.core.internal.http.IHttpClient
1820
import com.onesignal.core.internal.http.impl.HttpClient
1921
import com.onesignal.core.internal.http.impl.HttpConnectionFactory
@@ -53,6 +55,7 @@ internal class CoreModule : IModule {
5355
builder.register<Time>().provides<ITime>()
5456
builder.register<DatabaseProvider>().provides<IDatabaseProvider>()
5557
builder.register<StartupService>().provides<StartupService>()
58+
builder.register<InstallIdService>().provides<IInstallIdService>()
5659

5760
// Params (Config)
5861
builder.register<ConfigModelStore>().provides<ConfigModelStore>()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.onesignal.core.internal.device
2+
3+
import java.util.UUID
4+
5+
interface IInstallIdService {
6+
/**
7+
* WARNING: This may do disk I/O on the first call, so never call this from
8+
* the main thread.
9+
*/
10+
suspend fun getId(): UUID
11+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.onesignal.core.internal.device.impl
2+
3+
import com.onesignal.core.internal.device.IInstallIdService
4+
import com.onesignal.core.internal.preferences.IPreferencesService
5+
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
6+
import com.onesignal.core.internal.preferences.PreferenceStores
7+
import java.util.UUID
8+
9+
/**
10+
* Manages a persistent UUIDv4, generated once when app is first opened.
11+
* Value is for a HTTP header, OneSignal-Install-Id, added on all calls made
12+
* to OneSignal's backend. This allows the OneSignal's backend know where
13+
* traffic is coming from, no matter if the SubscriptionId or OneSignalId
14+
* changes or isn't available yet.
15+
*/
16+
internal class InstallIdService(
17+
private val _prefs: IPreferencesService,
18+
) : IInstallIdService {
19+
private val currentId: UUID by lazy {
20+
val idFromPrefs = _prefs.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID)
21+
if (idFromPrefs != null) {
22+
UUID.fromString(idFromPrefs)
23+
} else {
24+
val newId = UUID.randomUUID()
25+
_prefs.saveString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID, newId.toString())
26+
newId
27+
}
28+
}
29+
30+
/**
31+
* WARNING: This may do disk I/O on the first call, so never call this from
32+
* the main thread. Disk I/O is done inside of "currentId by lazy".
33+
*/
34+
override suspend fun getId() = currentId
35+
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.onesignal.common.JSONUtils
66
import com.onesignal.common.OneSignalUtils
77
import com.onesignal.common.OneSignalWrapper
88
import com.onesignal.core.internal.config.ConfigModelStore
9+
import com.onesignal.core.internal.device.IInstallIdService
910
import com.onesignal.core.internal.http.HttpResponse
1011
import com.onesignal.core.internal.http.IHttpClient
1112
import com.onesignal.core.internal.preferences.IPreferencesService
@@ -23,6 +24,7 @@ import kotlinx.coroutines.withTimeout
2324
import org.json.JSONObject
2425
import java.net.ConnectException
2526
import java.net.HttpURLConnection
27+
import java.net.URL
2628
import java.net.UnknownHostException
2729
import java.util.Scanner
2830
import javax.net.ssl.HttpsURLConnection
@@ -32,6 +34,7 @@ internal class HttpClient(
3234
private val _prefs: IPreferencesService,
3335
private val _configModelStore: ConfigModelStore,
3436
private val _time: ITime,
37+
private val _installIdService: IInstallIdService,
3538
) : IHttpClient {
3639
/**
3740
* Delay making network requests until we reach this time.
@@ -149,6 +152,8 @@ internal class HttpClient(
149152
con.setRequestProperty("OneSignal-Subscription-Id", subscriptionId)
150153
}
151154

155+
con.setRequestProperty("OneSignal-Install-Id", _installIdService.getId().toString())
156+
152157
if (jsonBody != null) {
153158
con.doInput = true
154159
}
@@ -159,16 +164,14 @@ internal class HttpClient(
159164
con.doOutput = true
160165
}
161166

167+
logHTTPSent(con.requestMethod, con.url, jsonBody, con.requestProperties)
168+
162169
if (jsonBody != null) {
163170
val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody)
164-
Logging.debug("HttpClient: ${method ?: "GET"} $url - $strJsonBody")
165-
166171
val sendBytes = strJsonBody.toByteArray(charset("UTF-8"))
167172
con.setFixedLengthStreamingMode(sendBytes.size)
168173
val outputStream = con.outputStream
169174
outputStream.write(sendBytes)
170-
} else {
171-
Logging.debug("HttpClient: ${method ?: "GET"} $url")
172175
}
173176

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

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

209212
if (cacheKey != null) {
210213
val eTag = con.getHeaderField("etag")
211214
if (eTag != null) {
212-
Logging.debug("HttpClient: Response has etag of $eTag so caching the response.")
215+
Logging.debug("HttpClient: Got Response = Response has etag of $eTag so caching the response.")
213216

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

232235
var inputStream = con.errorStream
233236
if (inputStream == null) {
@@ -240,9 +243,9 @@ internal class HttpClient(
240243
jsonResponse =
241244
if (scanner.useDelimiter("\\A").hasNext()) scanner.next() else ""
242245
scanner.close()
243-
Logging.warn("HttpClient: $method RECEIVED JSON: $jsonResponse")
246+
Logging.warn("HttpClient: Got Response = $method - STATUS: $httpResponse - Body: $jsonResponse")
244247
} else {
245-
Logging.warn("HttpClient: $method HTTP Code: $httpResponse No response body!")
248+
Logging.warn("HttpClient: Got Response = $method - STATUS: $httpResponse - No response body!")
246249
}
247250

248251
retVal = HttpResponse(httpResponse, jsonResponse, retryAfterSeconds = retryAfter)
@@ -285,6 +288,18 @@ internal class HttpClient(
285288
}
286289
}
287290

291+
private fun logHTTPSent(
292+
method: String?,
293+
url: URL,
294+
jsonBody: JSONObject?,
295+
headers: Map<String, List<String>>,
296+
) {
297+
val headersStr = headers.entries.joinToString()
298+
val methodStr = method ?: "GET"
299+
val bodyStr = if (jsonBody != null) JSONUtils.toUnescapedEUIDString(jsonBody) else null
300+
Logging.debug("HttpClient: Request Sent = $methodStr $url - Body: $bodyStr - Headers: $headersStr")
301+
}
302+
288303
companion object {
289304
private const val OS_API_VERSION = "1"
290305
private const val OS_ACCEPT_HEADER = "application/vnd.onesignal.v$OS_API_VERSION+json"

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/preferences/IPreferencesService.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ object PreferenceOneSignalKeys {
215215
*/
216216
const val PREFS_OS_ETAG_PREFIX = "PREFS_OS_ETAG_PREFIX_"
217217

218+
/**
219+
* (String) A install id, a UUIDv4 generated once when app is first opened.
220+
* Value is for a HTTP header, OneSignal-Install-Id, added on all calls
221+
* made to OneSignal's backend.
222+
*/
223+
const val PREFS_OS_INSTALL_ID = "PREFS_OS_INSTALL_ID"
224+
218225
/**
219226
* (String) A prefix key for retrieving the response for a given HTTP GET cache key. The cache
220227
* key should be appended to this prefix.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.onesignal.core.internal.device
2+
3+
import com.onesignal.core.internal.device.impl.InstallIdService
4+
import com.onesignal.mocks.MockPreferencesService
5+
import io.kotest.core.spec.style.FunSpec
6+
import io.kotest.matchers.shouldBe
7+
8+
class InstallIdServiceTests : FunSpec({
9+
test("2 calls result in the same value") {
10+
// Given
11+
val service = InstallIdService(MockPreferencesService())
12+
13+
// When
14+
val value1 = service.getId()
15+
val value2 = service.getId()
16+
17+
// Then
18+
value1 shouldBe value2
19+
}
20+
21+
// Real world scenario we are testing is if we cold restart the app we get
22+
// the same value
23+
test("reads from shared prefs") {
24+
// Given
25+
val sharedPrefs = MockPreferencesService()
26+
27+
// When
28+
val service1 = InstallIdService(sharedPrefs)
29+
val value1 = service1.getId()
30+
val service2 = InstallIdService(sharedPrefs)
31+
val value2 = service2.getId()
32+
33+
// Then
34+
value1 shouldBe value2
35+
}
36+
})

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.onesignal.core.internal.http
22

33
import com.onesignal.common.OneSignalUtils
4+
import com.onesignal.core.internal.device.impl.InstallIdService
45
import com.onesignal.core.internal.http.impl.HttpClient
56
import com.onesignal.core.internal.time.impl.Time
67
import com.onesignal.debug.LogLevel
@@ -20,8 +21,9 @@ class Mocks {
2021
internal val mockConfigModel = MockHelper.configModelStore()
2122
internal val response = MockHttpConnectionFactory.MockResponse()
2223
internal val factory = MockHttpConnectionFactory(response)
24+
internal val installIdService = InstallIdService(MockPreferencesService())
2325
internal val httpClient by lazy {
24-
HttpClient(factory, MockPreferencesService(), mockConfigModel, Time())
26+
HttpClient(factory, MockPreferencesService(), mockConfigModel, Time(), installIdService)
2527
}
2628
}
2729

@@ -50,7 +52,7 @@ class HttpClientTests : FunSpec({
5052
response.throwable should beInstanceOf<TimeoutCancellationException>()
5153
}
5254

53-
test("SDKHeader is included in all requests") {
55+
test("SDK Headers are included in all requests") {
5456
// Given
5557
val mocks = Mocks()
5658
val httpClient = mocks.httpClient
@@ -65,6 +67,7 @@ class HttpClientTests : FunSpec({
6567
// Then
6668
for (connection in mocks.factory.connections) {
6769
connection.getRequestProperty("SDK-Version") shouldBe "onesignal/android/${OneSignalUtils.SDK_VERSION}"
70+
connection.getRequestProperty("OneSignal-Install-Id") shouldBe mocks.installIdService.getId().toString()
6871
}
6972
}
7073

0 commit comments

Comments
 (0)