|
| 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) } ?: "-" |
0 commit comments