Skip to content

Commit a8bafc0

Browse files
abdulraqeeb33AR Abdul Azeezcursoragent
authored
feat: [SDK-5146] send host FID-readiness signals on API request headers (#2729)
Co-authored-by: AR Abdul Azeez <abdul@onesignal.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 53d8b08 commit a8bafc0

10 files changed

Lines changed: 471 additions & 1 deletion

File tree

OneSignalSDK/detekt/detekt-baseline-core.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
<ID>ConstructorParameterNaming:DatabaseCursor.kt$DatabaseCursor$private val _cursor: Cursor</ID>
2626
<ID>ConstructorParameterNaming:DatabaseProvider.kt$DatabaseProvider$private val _application: IApplicationService</ID>
2727
<ID>ConstructorParameterNaming:DeviceService.kt$DeviceService$private val _applicationService: IApplicationService</ID>
28+
<ID>ConstructorParameterNaming:FidEnv.kt$FidEnvService$private val _applicationService: IApplicationService</ID>
29+
<ID>ConstructorParameterNaming:FidEnv.kt$FidEnvService$private val _configModelStore: ConfigModelStore</ID>
2830
<ID>ConstructorParameterNaming:HttpClient.kt$HttpClient$private val _configModelStore: ConfigModelStore</ID>
2931
<ID>ConstructorParameterNaming:HttpClient.kt$HttpClient$private val _connectionFactory: IHttpConnectionFactory</ID>
32+
<ID>ConstructorParameterNaming:HttpClient.kt$HttpClient$private val _fidEnv: IFidEnv</ID>
3033
<ID>ConstructorParameterNaming:HttpClient.kt$HttpClient$private val _installIdService: IInstallIdService</ID>
3134
<ID>ConstructorParameterNaming:HttpClient.kt$HttpClient$private val _prefs: IPreferencesService</ID>
3235
<ID>ConstructorParameterNaming:HttpClient.kt$HttpClient$private val _time: ITime</ID>
@@ -187,6 +190,7 @@
187190
<ID>InstanceOfCheckForException:HttpClient.kt$HttpClient$t is UnknownHostException</ID>
188191
<ID>LongMethod:ApplicationService.kt$ApplicationService$override suspend fun waitUntilSystemConditionsAvailable(): Boolean</ID>
189192
<ID>LongMethod:ConfigModelStoreListener.kt$ConfigModelStoreListener$private fun fetchParams()</ID>
193+
<ID>LongMethod:CoreModule.kt$CoreModule$override fun register(builder: ServiceBuilder)</ID>
190194
<ID>LongMethod:FeatureFlagsBackendService.kt$FeatureFlagsBackendService$override suspend fun fetchRemoteFeatureFlags(appId: String): RemoteFeatureFlagsFetchOutcome</ID>
191195
<ID>LongMethod:HttpClient.kt$HttpClient$private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse</ID>
192196
<ID>LongMethod:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List&lt;Operation>): ExecutionResponse</ID>

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
@@ -17,8 +17,10 @@ import com.onesignal.core.internal.config.impl.IdentityVerificationService
1717
import com.onesignal.core.internal.database.IDatabaseProvider
1818
import com.onesignal.core.internal.database.impl.DatabaseProvider
1919
import com.onesignal.core.internal.device.IDeviceService
20+
import com.onesignal.core.internal.device.IFidEnv
2021
import com.onesignal.core.internal.device.IInstallIdService
2122
import com.onesignal.core.internal.device.impl.DeviceService
23+
import com.onesignal.core.internal.device.impl.FidEnvService
2224
import com.onesignal.core.internal.device.impl.InstallIdService
2325
import com.onesignal.core.internal.features.FeatureManager
2426
import com.onesignal.core.internal.features.IFeatureManager
@@ -62,6 +64,7 @@ internal class CoreModule : IModule {
6264
builder.register { ApplicationService.getInstanceOrNull() ?: ApplicationService() }
6365
.provides<IApplicationService>()
6466
builder.register<DeviceService>().provides<IDeviceService>()
67+
builder.register<FidEnvService>().provides<IFidEnv>()
6568
builder.register<Time>().provides<ITime>()
6669
builder.register<DatabaseProvider>().provides<IDatabaseProvider>()
6770
builder.register<InstallIdService>().provides<IInstallIdService>()

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ class ConfigModel : Model() {
228228
setOptStringProperty(::googleProjectNumber.name, value)
229229
}
230230

231+
/**
232+
* App ID [googleProjectNumber] was fetched for.
233+
*/
234+
var dashboardSenderAppId: String?
235+
get() = getOptStringProperty(::dashboardSenderAppId.name)
236+
set(value) {
237+
setOptStringProperty(::dashboardSenderAppId.name, value)
238+
}
239+
231240
/**
232241
* Whether the current application is an enterprise-level
233242
*/

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ internal class ConfigModelStoreListener(
8080
config.appId = appId
8181
config.notificationChannels = params.notificationChannels
8282
config.googleProjectNumber = params.googleProjectNumber
83+
config.dashboardSenderAppId = appId
8384
config.fcmParams.projectId = params.fcmParams.projectId
8485
config.fcmParams.appId = params.fcmParams.appId
8586
config.fcmParams.apiKey = params.fcmParams.apiKey
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.onesignal.core.internal.device
2+
3+
/**
4+
* Host-app FID-readiness signals for the OneSignal-Fid-Env request header.
5+
* google-services.json is not packaged; gs=1 means the plugin wrote google_app_id.
6+
*/
7+
internal interface IFidEnv {
8+
fun headerValue(): String
9+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package com.onesignal.core.internal.device.impl
2+
3+
import android.content.Context
4+
import android.content.pm.PackageManager
5+
import android.os.Build
6+
import com.onesignal.common.AndroidUtils
7+
import com.onesignal.core.internal.application.IApplicationService
8+
import com.onesignal.core.internal.config.ConfigModelStore
9+
import com.onesignal.core.internal.device.IFidEnv
10+
import com.onesignal.debug.internal.logging.Logging
11+
import java.util.Properties
12+
import java.util.concurrent.atomic.AtomicBoolean
13+
import java.util.zip.ZipFile
14+
15+
internal const val HTTP_FID_ENV_HEADER_KEY = "OneSignal-Fid-Env"
16+
17+
internal data class FidEnvSnapshot(
18+
val googleServices: Boolean,
19+
val agpVersion: String?,
20+
val fidFlag: Boolean,
21+
val defaultFirebaseApp: Boolean,
22+
val firebaseInitProvider: Boolean,
23+
val minSdk: Int?,
24+
val targetSdk: Int?,
25+
val senderMatch: Boolean?,
26+
) {
27+
fun toHeaderValue(): String =
28+
listOf(
29+
"gs=${googleServices.toBit()}",
30+
"agp=${agpVersion.sanitized()}",
31+
"flag=${fidFlag.toBit()}",
32+
"def=${defaultFirebaseApp.toBit()}",
33+
"prov=${firebaseInitProvider.toBit()}",
34+
"min=${minSdk?.toString() ?: "-"}",
35+
"tgt=${targetSdk?.toString() ?: "-"}",
36+
"snd=${senderMatch.toBitOrDash()}",
37+
).joinToString(";")
38+
}
39+
40+
internal fun sanitizeToken(value: String): String =
41+
value.filter { it in TOKEN_CHARS }.take(MAX_TOKEN_CHARS).ifEmpty { "-" }
42+
43+
@Suppress("TooGenericExceptionCaught")
44+
internal fun parseAgpVersion(propertiesText: String): String? {
45+
return try {
46+
val props = Properties()
47+
props.load(propertiesText.reader())
48+
props.getProperty("androidGradlePluginVersion")?.takeIf { it.isNotBlank() }
49+
} catch (_: Exception) {
50+
null
51+
}
52+
}
53+
54+
internal fun senderMatch(
55+
resourceSender: String?,
56+
dashboardSender: String?,
57+
): Boolean? =
58+
when {
59+
dashboardSender.isNullOrBlank() -> null
60+
resourceSender.isNullOrBlank() -> false
61+
else -> resourceSender == dashboardSender
62+
}
63+
64+
internal fun dashboardSenderForCensus(
65+
hydrated: Boolean,
66+
appId: String,
67+
senderAppId: String?,
68+
sender: String?,
69+
): String? =
70+
if (hydrated && senderAppId == appId) sender else null
71+
72+
internal class AndroidFidEnvReader(
73+
private val context: Context,
74+
) {
75+
private val gcmSenderId: String? by lazy {
76+
AndroidUtils.getResourceString(context, GCM_SENDER_ID, null)
77+
}
78+
79+
private val staticProbe: FidEnvSnapshot by lazy { collectStatic() }
80+
81+
fun collect(dashboardSenderId: String?): FidEnvSnapshot =
82+
staticProbe.copy(
83+
defaultFirebaseApp = hasDefaultFirebaseApp(),
84+
senderMatch = senderMatch(gcmSenderId, dashboardSenderId),
85+
)
86+
87+
private fun collectStatic(): FidEnvSnapshot {
88+
val googleAppId = AndroidUtils.getResourceString(context, GOOGLE_APP_ID, null)
89+
return FidEnvSnapshot(
90+
googleServices = !googleAppId.isNullOrBlank(),
91+
agpVersion = readApkEntry(AGP_METADATA_PATH)?.let { parseAgpVersion(it) },
92+
fidFlag = AndroidUtils.getManifestMetaBoolean(context, FID_FLAG),
93+
defaultFirebaseApp = false,
94+
firebaseInitProvider = hasFirebaseInitProvider(),
95+
minSdk = minSdk(),
96+
targetSdk = context.applicationInfo.targetSdkVersion,
97+
senderMatch = null,
98+
)
99+
}
100+
101+
@Suppress("TooGenericExceptionCaught")
102+
private fun readApkEntry(path: String): String? {
103+
try {
104+
context.classLoader.getResourceAsStream(path)?.use { return it.bufferedReader().readText() }
105+
} catch (_: Exception) {
106+
}
107+
return readFromApkZip(path)
108+
}
109+
110+
@Suppress("TooGenericExceptionCaught")
111+
private fun readFromApkZip(path: String): String? {
112+
val sourceDir = context.applicationInfo.sourceDir ?: return null
113+
return try {
114+
readZipEntry(sourceDir, path)
115+
} catch (_: Exception) {
116+
null
117+
}
118+
}
119+
120+
private fun readZipEntry(
121+
sourceDir: String,
122+
path: String,
123+
): String? {
124+
ZipFile(sourceDir).use { zip ->
125+
val entry = zip.getEntry(path) ?: return null
126+
return zip.getInputStream(entry).bufferedReader().use { it.readText() }
127+
}
128+
}
129+
130+
@Suppress("TooGenericExceptionCaught", "UNCHECKED_CAST")
131+
private fun hasDefaultFirebaseApp(): Boolean {
132+
return try {
133+
val clazz = Class.forName(FIREBASE_APP)
134+
val apps =
135+
clazz.getMethod("getApps", Context::class.java).invoke(null, context) as? List<*>
136+
?: return false
137+
val getName = clazz.getMethod("getName")
138+
apps.any { getName.invoke(it) == DEFAULT_APP_NAME }
139+
} catch (_: Throwable) {
140+
false
141+
}
142+
}
143+
144+
@Suppress("TooGenericExceptionCaught", "DEPRECATION")
145+
private fun hasFirebaseInitProvider(): Boolean {
146+
return try {
147+
val pkg =
148+
context.packageManager.getPackageInfo(
149+
context.packageName,
150+
PackageManager.GET_PROVIDERS,
151+
)
152+
pkg.providers?.any { it.name == FIREBASE_INIT_PROVIDER } == true
153+
} catch (_: Throwable) {
154+
false
155+
}
156+
}
157+
158+
private fun minSdk(): Int? {
159+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
160+
context.applicationInfo.minSdkVersion
161+
} else {
162+
null
163+
}
164+
}
165+
166+
companion object {
167+
private const val GOOGLE_APP_ID = "google_app_id"
168+
private const val GCM_SENDER_ID = "gcm_defaultSenderId"
169+
private const val FID_FLAG = "firebase_messaging_installation_id_enabled"
170+
private const val FIREBASE_INIT_PROVIDER = "com.google.firebase.provider.FirebaseInitProvider"
171+
private const val AGP_METADATA_PATH = "META-INF/com/android/build/gradle/app-metadata.properties"
172+
private const val FIREBASE_APP = "com.google.firebase.FirebaseApp"
173+
private const val DEFAULT_APP_NAME = "[DEFAULT]"
174+
}
175+
}
176+
177+
internal class FidEnvService(
178+
private val _applicationService: IApplicationService,
179+
private val _configModelStore: ConfigModelStore,
180+
) : IFidEnv {
181+
private val reader by lazy { AndroidFidEnvReader(_applicationService.appContext) }
182+
private val logged = AtomicBoolean(false)
183+
184+
@Suppress("TooGenericExceptionCaught")
185+
override fun headerValue(): String {
186+
return try {
187+
val model = _configModelStore.model
188+
val dashboardSender =
189+
dashboardSenderForCensus(
190+
hydrated = model.isInitializedWithRemote,
191+
appId = model.appId,
192+
senderAppId = model.dashboardSenderAppId,
193+
sender = model.googleProjectNumber,
194+
)
195+
val value = reader.collect(dashboardSender).toHeaderValue()
196+
if (logged.compareAndSet(false, true)) {
197+
Logging.debug("HttpClient: $HTTP_FID_ENV_HEADER_KEY $value")
198+
}
199+
value
200+
} catch (t: Throwable) {
201+
Logging.debug("HttpClient: $HTTP_FID_ENV_HEADER_KEY probe failed", t)
202+
""
203+
}
204+
}
205+
}
206+
207+
private const val MAX_TOKEN_CHARS = 32
208+
private const val TOKEN_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._+-"
209+
210+
private fun Boolean.toBit(): String = if (this) "1" else "0"
211+
212+
private fun Boolean?.toBitOrDash(): String = this?.toBit() ?: "-"
213+
214+
private fun String?.sanitized(): String = this?.let { sanitizeToken(it) } ?: "-"

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import com.onesignal.common.OneSignalUtils
77
import com.onesignal.common.OneSignalWrapper
88
import com.onesignal.common.threading.launchOnIO
99
import com.onesignal.core.internal.config.ConfigModelStore
10+
import com.onesignal.core.internal.device.IFidEnv
1011
import com.onesignal.core.internal.device.IInstallIdService
12+
import com.onesignal.core.internal.device.impl.HTTP_FID_ENV_HEADER_KEY
1113
import com.onesignal.core.internal.http.HttpResponse
1214
import com.onesignal.core.internal.http.IHttpClient
1315
import com.onesignal.core.internal.preferences.IPreferencesService
@@ -35,6 +37,7 @@ internal class HttpClient(
3537
private val _configModelStore: ConfigModelStore,
3638
private val _time: ITime,
3739
private val _installIdService: IInstallIdService,
40+
private val _fidEnv: IFidEnv,
3841
) : IHttpClient {
3942
/**
4043
* Delay making network requests until we reach this time.
@@ -149,6 +152,14 @@ internal class HttpClient(
149152

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

155+
val fidEnv = _fidEnv.headerValue()
156+
if (fidEnv.isNotEmpty()) {
157+
try {
158+
con.setRequestProperty(HTTP_FID_ENV_HEADER_KEY, fidEnv)
159+
} catch (_: IllegalArgumentException) {
160+
}
161+
}
162+
152163
if (jsonBody != null) {
153164
con.doInput = true
154165
}

0 commit comments

Comments
 (0)