@advenue/react-native 0.9.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +117 -368
- package/android/src/main/kotlin/io/advenue/Advenue.kt +574 -0
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +141 -0
- package/android/src/main/kotlin/io/advenue/core/Backoff.kt +28 -0
- package/android/src/main/kotlin/io/advenue/core/ClientEvent.kt +136 -0
- package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +193 -0
- package/android/src/main/kotlin/io/advenue/core/Consent.kt +52 -0
- package/android/src/main/kotlin/io/advenue/core/Contracts.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/Conversion.kt +69 -0
- package/android/src/main/kotlin/io/advenue/core/Engine.kt +436 -0
- package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +89 -0
- package/android/src/main/kotlin/io/advenue/core/HmacSigner.kt +31 -0
- package/android/src/main/kotlin/io/advenue/core/InstallReferrer.kt +146 -0
- package/android/src/main/kotlin/io/advenue/core/Json.kt +272 -0
- package/android/src/main/kotlin/io/advenue/core/Limits.kt +50 -0
- package/android/src/main/kotlin/io/advenue/core/MetaReferrer.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/PiiScrub.kt +77 -0
- package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +189 -0
- package/android/src/main/kotlin/io/advenue/core/SystemServices.kt +58 -0
- package/android/src/main/kotlin/io/advenue/core/Tcf.kt +48 -0
- package/android/src/main/kotlin/io/advenue/core/Time.kt +56 -0
- package/android/src/main/kotlin/io/advenue/platform/Collectors.kt +163 -0
- package/android/src/main/kotlin/io/advenue/platform/CompositeStore.kt +33 -0
- package/android/src/main/kotlin/io/advenue/platform/ConversionFetcher.kt +68 -0
- package/android/src/main/kotlin/io/advenue/platform/ForegroundTracker.kt +90 -0
- package/android/src/main/kotlin/io/advenue/platform/HttpUrlTransport.kt +101 -0
- package/android/src/main/kotlin/io/advenue/platform/Identity.kt +79 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallEnrichment.kt +197 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallScopedStore.kt +96 -0
- package/android/src/main/kotlin/io/advenue/platform/LifecycleBridge.kt +72 -0
- package/android/src/main/kotlin/io/advenue/platform/PreferencesStore.kt +32 -0
- package/android/src/main/kotlin/io/advenue/plugin/Contracts.kt +67 -0
- package/android/src/main/kotlin/io/advenue/plugin/FirebaseAppInstanceIdSource.kt +73 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayAdvertisingIdSource.kt +47 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayInstallReferrerSource.kt +85 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayIntegritySource.kt +86 -0
- package/android/src/main/kotlin/io/advenue/plugin/PluginRegistry.kt +61 -0
- package/dist/index.cjs +188 -675
- package/dist/index.d.cts +203 -336
- package/dist/index.d.ts +203 -336
- package/dist/index.js +187 -681
- package/ios/AdvenueIosModule.swift +143 -446
- package/ios/vendor/Advenue/Advenue.swift +632 -0
- package/ios/vendor/Advenue/AdvenueConfig.swift +89 -0
- package/ios/vendor/AdvenueCore/AdvenueValue.swift +103 -0
- package/ios/vendor/AdvenueCore/Attestation.swift +52 -0
- package/ios/vendor/AdvenueCore/Backoff.swift +28 -0
- package/ios/vendor/AdvenueCore/ClientEvent.swift +154 -0
- package/ios/vendor/AdvenueCore/Consent.swift +40 -0
- package/ios/vendor/AdvenueCore/Contracts.swift +59 -0
- package/ios/vendor/AdvenueCore/Conversion.swift +74 -0
- package/ios/vendor/AdvenueCore/ConversionValue.swift +217 -0
- package/ios/vendor/AdvenueCore/Engine.swift +607 -0
- package/ios/vendor/AdvenueCore/EventQueue.swift +89 -0
- package/ios/vendor/AdvenueCore/Limits.swift +41 -0
- package/ios/vendor/AdvenueCore/PIIScrub.swift +102 -0
- package/ios/vendor/AdvenueCore/SessionTracker.swift +139 -0
- package/ios/vendor/AdvenueCore/SkanConfig.swift +76 -0
- package/ios/vendor/AdvenueCore/SkanReporter.swift +25 -0
- package/ios/vendor/AdvenueCore/SkanState.swift +258 -0
- package/ios/vendor/AdvenueCore/Tcf.swift +48 -0
- package/ios/vendor/AdvenueCore/Transport.swift +14 -0
- package/ios/vendor/AdvenueFirebase/FirebaseAppInstanceId.swift +33 -0
- package/ios/vendor/AdvenuePlatform/AdvertisingIdentity.swift +87 -0
- package/ios/vendor/AdvenuePlatform/CacheFileStore.swift +90 -0
- package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +42 -0
- package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +63 -0
- package/ios/vendor/AdvenuePlatform/CryptoKitSigner.swift +19 -0
- package/ios/vendor/AdvenuePlatform/DeviceCheckAttestation.swift +91 -0
- package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +77 -0
- package/ios/vendor/AdvenuePlatform/ForegroundTracker.swift +55 -0
- package/ios/vendor/AdvenuePlatform/HttpTransport.swift +96 -0
- package/ios/vendor/AdvenuePlatform/Identity.swift +52 -0
- package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +155 -0
- package/ios/vendor/AdvenuePlatform/KeychainStore.swift +95 -0
- package/ios/vendor/AdvenuePlatform/SearchAdsToken.swift +76 -0
- package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +70 -0
- package/ios/vendor/AdvenuePlatform/StoreKitSkanReporter.swift +140 -0
- package/ios/vendor/AdvenuePlatform/SystemServices.swift +55 -0
- package/ios/vendor/AdvenuePlatform/TcfReader.swift +18 -0
- package/ios/vendor/AdvenuePlatform/UserDefaultsStore.swift +26 -0
- package/package.json +13 -15
- package/scripts/check-dist.mjs +17 -0
- package/scripts/check-vendored-swift.mjs +148 -0
- package/scripts/vendor-natives.mjs +149 -0
- package/scripts/vendor-natives.test.mjs +112 -0
- package/src/deep-links.ts +19 -1
- package/src/index.ts +273 -830
- package/src/native-types.ts +73 -181
- package/src/native.ts +0 -23
- package/src/types.ts +86 -0
- package/src/aem.ts +0 -33
- package/src/mmkv-storage.ts +0 -21
- package/src/native-storage.ts +0 -63
- package/src/secure-store.ts +0 -26
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
package io.advenue
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.content.BroadcastReceiver
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.IntentFilter
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import io.advenue.core.AdvenueEngine
|
|
10
|
+
import io.advenue.core.Command
|
|
11
|
+
import io.advenue.core.CommandPipe
|
|
12
|
+
import io.advenue.core.Consent
|
|
13
|
+
import io.advenue.core.DeepLink
|
|
14
|
+
import io.advenue.core.resolveDeferredDeepLink
|
|
15
|
+
import io.advenue.core.EngineConfig
|
|
16
|
+
import io.advenue.core.EventTransport
|
|
17
|
+
import io.advenue.core.IngestError
|
|
18
|
+
import io.advenue.core.SystemClock
|
|
19
|
+
import io.advenue.core.SystemUuids
|
|
20
|
+
import io.advenue.core.TimerScheduler
|
|
21
|
+
import io.advenue.platform.HttpConversionFetcher
|
|
22
|
+
import io.advenue.platform.HttpUrlTransport
|
|
23
|
+
import io.advenue.platform.IdentityResolution
|
|
24
|
+
import io.advenue.platform.EnrichmentSources
|
|
25
|
+
import io.advenue.platform.InstallScopedStore
|
|
26
|
+
import io.advenue.platform.LifecycleBridge
|
|
27
|
+
import io.advenue.platform.CompositeStore
|
|
28
|
+
import io.advenue.platform.PreferencesStore
|
|
29
|
+
import io.advenue.core.extractAemCampaignIds
|
|
30
|
+
import io.advenue.core.tcfToConsent
|
|
31
|
+
import io.advenue.platform.collectDeviceInfo
|
|
32
|
+
import io.advenue.platform.readTcf
|
|
33
|
+
import io.advenue.platform.readAppVersion
|
|
34
|
+
import io.advenue.platform.collectEnrichment
|
|
35
|
+
import io.advenue.platform.sha256Hex
|
|
36
|
+
import io.advenue.platform.resolveIdentity
|
|
37
|
+
import io.advenue.platform.systemUnlockProbe
|
|
38
|
+
import java.util.Timer
|
|
39
|
+
import java.util.TimerTask
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The public SDK. A static facade matching the React Native and iOS SDKs' names,
|
|
43
|
+
* so a developer moving between them reads the same API, delegating to state
|
|
44
|
+
* that is replaceable and inspectable rather than to a hidden singleton.
|
|
45
|
+
*/
|
|
46
|
+
public object Advenue {
|
|
47
|
+
private val state = FacadeState()
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Starts the SDK. Safe to call from `Application.onCreate`, and safe to call
|
|
51
|
+
* twice: the previous instance is shut down first. Without that, a second call
|
|
52
|
+
* would leave two consumer threads draining one queue, so events would be
|
|
53
|
+
* processed twice or lost.
|
|
54
|
+
*
|
|
55
|
+
* Takes the `Application` explicitly. ContentProvider auto-initialisation —
|
|
56
|
+
* the Firebase pattern — is rejected: it charges every cold start and hides
|
|
57
|
+
* initialisation order.
|
|
58
|
+
*/
|
|
59
|
+
@JvmStatic
|
|
60
|
+
public fun initialize(application: Application, config: AdvenueConfig) {
|
|
61
|
+
state.start(application, config)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Records an event. Synchronous, non-blocking and ordered. */
|
|
65
|
+
@JvmStatic
|
|
66
|
+
@JvmOverloads
|
|
67
|
+
public fun track(name: String, properties: Map<String, Any?>? = null) {
|
|
68
|
+
state.submit(Command.Track(name, properties, "custom"))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@JvmStatic
|
|
72
|
+
public fun setUserId(id: String?) {
|
|
73
|
+
state.submit(Command.SetUserId(id))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Granular ad-platform consent (Google DMA), forwarded by server-side
|
|
78
|
+
* postbacks as gdpr_applies / ad_user_data / ad_personalization / ad_storage.
|
|
79
|
+
*
|
|
80
|
+
* Leave a field null when the user has not been asked: "not stated" is not
|
|
81
|
+
* "denied", and inventing false on their behalf records a refusal that never
|
|
82
|
+
* happened.
|
|
83
|
+
*/
|
|
84
|
+
@JvmStatic
|
|
85
|
+
@JvmOverloads
|
|
86
|
+
public fun setConsentData(
|
|
87
|
+
isUserSubjectToGDPR: Boolean,
|
|
88
|
+
hasConsentForDataUsage: Boolean? = null,
|
|
89
|
+
hasConsentForAdsPersonalization: Boolean? = null,
|
|
90
|
+
hasConsentForAdStorage: Boolean? = null,
|
|
91
|
+
) {
|
|
92
|
+
val consent =
|
|
93
|
+
Consent(
|
|
94
|
+
isUserSubjectToGDPR,
|
|
95
|
+
hasConsentForDataUsage,
|
|
96
|
+
hasConsentForAdsPersonalization,
|
|
97
|
+
hasConsentForAdStorage,
|
|
98
|
+
)
|
|
99
|
+
state.consentDataMirror = consent
|
|
100
|
+
state.submit(Command.SetConsentData(consent))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Registers the device's push token for uninstall measurement.
|
|
105
|
+
*
|
|
106
|
+
* Advenue never asks for the notification permission and never displays
|
|
107
|
+
* anything. Pass the token your push library already gives you, on every
|
|
108
|
+
* launch: the OS can rotate it, and a stale token is what makes uninstall
|
|
109
|
+
* measurement report churn that did not happen. Pass null to stop.
|
|
110
|
+
*/
|
|
111
|
+
@JvmStatic
|
|
112
|
+
@JvmOverloads
|
|
113
|
+
public fun setPushToken(token: String?, provider: String? = null) {
|
|
114
|
+
state.submit(Command.SetPushToken(token, provider))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The app the ingestion service resolved this API key to, or null until a
|
|
119
|
+
* batch has been accepted.
|
|
120
|
+
*
|
|
121
|
+
* The API key is the SDK's entire app identity, so pasting the wrong one is
|
|
122
|
+
* silent: events are still accepted, just recorded against another app, and
|
|
123
|
+
* every screen the integrator checks is the one they believe they configured.
|
|
124
|
+
* This is the answer to "which app am I actually writing to", read from the
|
|
125
|
+
* device rather than inferred from the dashboard.
|
|
126
|
+
*/
|
|
127
|
+
@JvmStatic public fun resolvedAppId(): String? = state.resolvedAppId
|
|
128
|
+
|
|
129
|
+
@JvmStatic
|
|
130
|
+
public fun setConsent(granted: Boolean) {
|
|
131
|
+
state.consentMirror = granted
|
|
132
|
+
state.submit(Command.SetConsent(granted))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether tracking consent is currently granted. Persisted, so this is the
|
|
137
|
+
* answer after a restart too — a caller that defaulted to false on every cold
|
|
138
|
+
* start would silently discard a preference the user gave.
|
|
139
|
+
*/
|
|
140
|
+
@JvmStatic public fun trackingConsent(): Boolean = state.consentMirror
|
|
141
|
+
|
|
142
|
+
/** The granular DMA consent last set, or null if none has been. */
|
|
143
|
+
@JvmStatic public fun consentData(): Consent? = state.consentDataMirror
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The Firebase App Instance ID, when the app resolves it itself rather than
|
|
147
|
+
* through the `advenue-firebase` plugin. Does not disturb the advertising
|
|
148
|
+
* identity.
|
|
149
|
+
*/
|
|
150
|
+
@JvmStatic
|
|
151
|
+
public fun setAppInstanceId(id: String?) {
|
|
152
|
+
state.submit(Command.SetAppInstanceId(id))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Sends what is buffered. A no-op when the queue is empty or a backoff is open. */
|
|
156
|
+
@JvmStatic
|
|
157
|
+
public fun flush() {
|
|
158
|
+
state.submit(Command.Flush)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Forward from `onCreate` and `onNewIntent`. Callable **before** `initialize`:
|
|
163
|
+
* a cold start from a link runs the launcher activity first, and the links
|
|
164
|
+
* that carry attribution are precisely the ones that would be lost.
|
|
165
|
+
*
|
|
166
|
+
* `Intent.EXTRA_REFERRER` is deliberately not read here — it is not the Play
|
|
167
|
+
* install referrer, and conflating them would make a warm-start deep link look
|
|
168
|
+
* like one.
|
|
169
|
+
*/
|
|
170
|
+
@JvmStatic
|
|
171
|
+
public fun processDeepLink(intent: Intent?) {
|
|
172
|
+
intent?.data?.toString()?.let { state.deepLink(it) }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Erasure. Spans both stores — see [FacadeState.forgetMe]. */
|
|
176
|
+
@JvmStatic
|
|
177
|
+
public fun forgetMe() {
|
|
178
|
+
state.forgetMe()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The device identifier, or **null** while identity is deferred.
|
|
183
|
+
*
|
|
184
|
+
* sdk-core's getter is non-null; this one is not, and that is a real API
|
|
185
|
+
* difference rather than an oversight. Identity can be deferred while
|
|
186
|
+
* credential-encrypted storage is unreadable (Direct Boot), and blocking until
|
|
187
|
+
* it resolves could block indefinitely. Returning an invented id is the
|
|
188
|
+
* phantom-device bug the design exists to prevent.
|
|
189
|
+
*/
|
|
190
|
+
@JvmStatic public fun deviceId(): String? = state.currentDeviceId
|
|
191
|
+
|
|
192
|
+
@JvmStatic public fun pendingCount(): Int = state.pendingCount()
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolves the deferred deep link for this install, or null for an organic
|
|
196
|
+
* one — which is most of them. Safe to call once on first launch; the app
|
|
197
|
+
* routes on the result.
|
|
198
|
+
*
|
|
199
|
+
* Blocking, and deliberately so: it polls with backoff, and hiding that
|
|
200
|
+
* behind a callback would invite calling it on the main thread. Run it on
|
|
201
|
+
* your own background thread.
|
|
202
|
+
*/
|
|
203
|
+
@JvmStatic public fun resolveDeferredDeepLink(): DeepLink? = state.resolveDeferredDeepLink()
|
|
204
|
+
|
|
205
|
+
@JvmStatic
|
|
206
|
+
public fun shutdown() {
|
|
207
|
+
state.stop()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Holds what a static facade cannot: the live engine, the command pipe, the
|
|
213
|
+
* pre-init deep-link buffer and the resolved identity.
|
|
214
|
+
*/
|
|
215
|
+
internal class FacadeState {
|
|
216
|
+
private val lock = Any()
|
|
217
|
+
private var pipe: CommandPipe? = null
|
|
218
|
+
private var engine: AdvenueEngine? = null
|
|
219
|
+
private var scoped: InstallScopedStore? = null
|
|
220
|
+
private var prefs: PreferencesStore? = null
|
|
221
|
+
private var bridge: LifecycleBridge? = null
|
|
222
|
+
private var flushTimer: Timer? = null
|
|
223
|
+
private var unlockReceiver: BroadcastReceiver? = null
|
|
224
|
+
private var app: Application? = null
|
|
225
|
+
private var pendingDeepLinks = mutableListOf<String>()
|
|
226
|
+
private val seenAemUrlHashes = mutableSetOf<String>()
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Read caches for the two consent values. The engine owns persistence; these
|
|
230
|
+
* exist so a synchronous getter can answer without a round trip through the
|
|
231
|
+
* pipe, and so a caller reading back its own `setConsent` never sees the
|
|
232
|
+
* value it just replaced.
|
|
233
|
+
*/
|
|
234
|
+
@Volatile internal var consentMirror: Boolean = false
|
|
235
|
+
|
|
236
|
+
@Volatile internal var consentDataMirror: Consent? = null
|
|
237
|
+
|
|
238
|
+
@Volatile private var resolvedDeviceId: String? = null
|
|
239
|
+
|
|
240
|
+
/** Set by the transport after a batch is accepted; diagnostics only. */
|
|
241
|
+
@Volatile internal var resolvedAppId: String? = null
|
|
242
|
+
private var startedEndpoint: String? = null
|
|
243
|
+
private var startedApiKey: String? = null
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* `transport` is a parameter, not a hidden construction, because an unwired
|
|
247
|
+
* transport is otherwise invisible: every component of the send path can be
|
|
248
|
+
* green while nothing joins them. That is the defect the iOS SDK shipped with,
|
|
249
|
+
* and `FacadeSeamTest` injects a recorder here to make sure this one cannot.
|
|
250
|
+
*/
|
|
251
|
+
fun start(
|
|
252
|
+
application: Application,
|
|
253
|
+
config: AdvenueConfig,
|
|
254
|
+
transport: EventTransport? = null,
|
|
255
|
+
sources: EnrichmentSources? = null,
|
|
256
|
+
) {
|
|
257
|
+
// Replace-and-shut-down, never add.
|
|
258
|
+
stop()
|
|
259
|
+
|
|
260
|
+
// The SDK resolves the app version itself (readAppVersion); a config value
|
|
261
|
+
// is ignored — and said so, rather than looking like it took effect.
|
|
262
|
+
if (config.appVersion != null) {
|
|
263
|
+
config.onError(
|
|
264
|
+
"config.ignored:appVersion",
|
|
265
|
+
IllegalArgumentException("appVersion is resolved by the SDK and has no effect"),
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
val scoped = InstallScopedStore(application)
|
|
269
|
+
val prefs = PreferencesStore(application)
|
|
270
|
+
val uuid = SystemUuids()
|
|
271
|
+
|
|
272
|
+
val identity = resolveIdentity(scoped, systemUnlockProbe(application), uuid)
|
|
273
|
+
if (identity !is IdentityResolution.Resolved) {
|
|
274
|
+
// Deferred: credential-encrypted storage is unreadable. Nothing is minted
|
|
275
|
+
// and nothing starts, so no event carries an invented identifier. The
|
|
276
|
+
// receiver below is the exit — without it the deferral would trade a
|
|
277
|
+
// phantom id for permanent silence.
|
|
278
|
+
config.onError("identity.deferred", IngestError(0))
|
|
279
|
+
registerUnlockRetry(application, config, transport, sources)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
val eventTransport =
|
|
284
|
+
transport
|
|
285
|
+
?: HttpUrlTransport(
|
|
286
|
+
endpoint = config.endpoint,
|
|
287
|
+
apiKey = config.apiKey,
|
|
288
|
+
signingSecret = config.signingSecret,
|
|
289
|
+
clock = SystemClock(),
|
|
290
|
+
// Diagnostics only: it runs after the batch is already accepted, so
|
|
291
|
+
// nothing it does can turn a successful ingest into a failure.
|
|
292
|
+
onAccepted = { appId -> resolvedAppId = appId },
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
val scheduler = TimerScheduler()
|
|
296
|
+
val engine =
|
|
297
|
+
AdvenueEngine(
|
|
298
|
+
config =
|
|
299
|
+
EngineConfig(
|
|
300
|
+
apiKey = config.apiKey,
|
|
301
|
+
platform = "android",
|
|
302
|
+
deviceId = identity.deviceId,
|
|
303
|
+
installationId = identity.installationId,
|
|
304
|
+
appVersion = readAppVersion(application),
|
|
305
|
+
osVersion = Build.VERSION.RELEASE,
|
|
306
|
+
sdkVersion = (config.sdkVersion ?: AdvenueVersion.CURRENT).take(32),
|
|
307
|
+
requireConsent = config.requireConsent,
|
|
308
|
+
sessionWindowMs = config.sessionWindowMs,
|
|
309
|
+
batchSize = config.batchSize,
|
|
310
|
+
piiScrubEnabled = config.piiScrubEnabled,
|
|
311
|
+
),
|
|
312
|
+
store = CompositeStore(prefs, scoped),
|
|
313
|
+
clock = SystemClock(),
|
|
314
|
+
scheduler = scheduler,
|
|
315
|
+
uuid = uuid,
|
|
316
|
+
transport = eventTransport,
|
|
317
|
+
onError = config.onError,
|
|
318
|
+
)
|
|
319
|
+
val pipe = CommandPipe(engine, config.onError)
|
|
320
|
+
// The debounce timer may only WAKE; the work itself touches the event list,
|
|
321
|
+
// which belongs to the engine's thread.
|
|
322
|
+
scheduler.dispatcher = { work -> pipe.submit(Command.RunScheduled(work)) }
|
|
323
|
+
|
|
324
|
+
val buffered: List<String>
|
|
325
|
+
synchronized(lock) {
|
|
326
|
+
this.app = application
|
|
327
|
+
this.scoped = scoped
|
|
328
|
+
this.prefs = prefs
|
|
329
|
+
this.engine = engine
|
|
330
|
+
// Seeded from the same persisted values the engine loads, so consent
|
|
331
|
+
// granted in a previous run is still granted after a cold start.
|
|
332
|
+
this.consentMirror = engine.getConsent()
|
|
333
|
+
this.consentDataMirror = engine.getConsentData()
|
|
334
|
+
this.pipe = pipe
|
|
335
|
+
this.resolvedDeviceId = identity.deviceId
|
|
336
|
+
this.startedEndpoint = config.endpoint
|
|
337
|
+
this.startedApiKey = config.apiKey
|
|
338
|
+
buffered = pendingDeepLinks.toList()
|
|
339
|
+
pendingDeepLinks = mutableListOf()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Replayed in arrival order, before the first session, so a deferred deep
|
|
343
|
+
// link is attributed to the launch it belongs to.
|
|
344
|
+
buffered.forEach { sendDeepLink(it) }
|
|
345
|
+
|
|
346
|
+
val bridge =
|
|
347
|
+
LifecycleBridge(
|
|
348
|
+
application,
|
|
349
|
+
onForeground = { pipe.submit(Command.Foreground) },
|
|
350
|
+
onBackground = {
|
|
351
|
+
pipe.submit(Command.Background)
|
|
352
|
+
// A batch stranded when the app leaves the foreground may not be sent
|
|
353
|
+
// for hours.
|
|
354
|
+
pipe.submit(Command.Flush)
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
bridge.start()
|
|
358
|
+
// Application.onCreate runs before the first activity starts, so the
|
|
359
|
+
// foreground signal arrives from the bridge; a cold start where the SDK is
|
|
360
|
+
// initialised from an already-running activity still needs this.
|
|
361
|
+
pipe.submit(Command.Foreground)
|
|
362
|
+
|
|
363
|
+
// Enrichment then install, off the caller's thread. initialize() returns
|
|
364
|
+
// synchronously — an SDK that blocks Application.onCreate for five seconds
|
|
365
|
+
// is one nobody ships.
|
|
366
|
+
val installSources = sources ?: EnrichmentSources.discovered()
|
|
367
|
+
Thread(
|
|
368
|
+
{
|
|
369
|
+
val enrichment =
|
|
370
|
+
collectEnrichment(
|
|
371
|
+
application,
|
|
372
|
+
identity.deviceId,
|
|
373
|
+
installSources,
|
|
374
|
+
fbAppId = config.fbAppId,
|
|
375
|
+
)
|
|
376
|
+
pipe.submit(
|
|
377
|
+
Command.SetIdentity(
|
|
378
|
+
gaid = enrichment.gaid,
|
|
379
|
+
androidId = null,
|
|
380
|
+
appInstanceId = enrichment.appInstanceId,
|
|
381
|
+
// B2: bayrak artık düşürülmüyor — olaya işlenir.
|
|
382
|
+
limitAdTracking = enrichment.limitAdTracking,
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
// Device metadata rides the install, where Meta CAPI reads it.
|
|
386
|
+
// Merged under the referrer properties: an attribution field wins
|
|
387
|
+
// over a descriptive one if they ever collide.
|
|
388
|
+
val installProperties =
|
|
389
|
+
collectDeviceInfo(application) +
|
|
390
|
+
(enrichment.referrerProperties ?: emptyMap()) +
|
|
391
|
+
(enrichment.metaProperties ?: emptyMap())
|
|
392
|
+
// The CMP writes TCF to the platform's default preferences, and
|
|
393
|
+
// reading it is the difference between shipping a real consent
|
|
394
|
+
// signal and shipping none. Submitted before the install so the
|
|
395
|
+
// first event carries it.
|
|
396
|
+
readTcf(application)?.let { tcf ->
|
|
397
|
+
tcfToConsent(
|
|
398
|
+
tcf["gdprApplies"] as? Int,
|
|
399
|
+
tcf["purposeConsents"] as? String,
|
|
400
|
+
)
|
|
401
|
+
?.let { pipe.submit(Command.SetConsentData(it)) }
|
|
402
|
+
}
|
|
403
|
+
pipe.submit(Command.TrackInstall(installProperties))
|
|
404
|
+
pipe.submit(Command.Flush)
|
|
405
|
+
},
|
|
406
|
+
"advenue-install",
|
|
407
|
+
)
|
|
408
|
+
.apply { isDaemon = true }
|
|
409
|
+
.start()
|
|
410
|
+
|
|
411
|
+
val timer =
|
|
412
|
+
if (config.flushIntervalMs > 0) {
|
|
413
|
+
Timer("advenue-flush", true).also {
|
|
414
|
+
it.scheduleAtFixedRate(
|
|
415
|
+
object : TimerTask() {
|
|
416
|
+
override fun run() {
|
|
417
|
+
pipe.submit(Command.Flush)
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
config.flushIntervalMs,
|
|
421
|
+
config.flushIntervalMs,
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
synchronized(lock) {
|
|
429
|
+
this.bridge = bridge
|
|
430
|
+
this.flushTimer = timer
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The exit from a deferred identity. Registered only when identity could not
|
|
436
|
+
* be resolved; unregistered on the first successful start.
|
|
437
|
+
*/
|
|
438
|
+
private fun registerUnlockRetry(
|
|
439
|
+
application: Application,
|
|
440
|
+
config: AdvenueConfig,
|
|
441
|
+
transport: EventTransport?,
|
|
442
|
+
sources: EnrichmentSources?,
|
|
443
|
+
) {
|
|
444
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
|
445
|
+
val receiver =
|
|
446
|
+
object : BroadcastReceiver() {
|
|
447
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
448
|
+
runCatching { application.unregisterReceiver(this) }
|
|
449
|
+
synchronized(lock) { unlockReceiver = null }
|
|
450
|
+
start(application, config, transport, sources)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
runCatching {
|
|
454
|
+
application.registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_UNLOCKED))
|
|
455
|
+
synchronized(lock) { unlockReceiver = receiver }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
fun submit(command: Command) {
|
|
460
|
+
synchronized(lock) { pipe }?.submit(command)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
fun deepLink(url: String) {
|
|
464
|
+
val started = synchronized(lock) {
|
|
465
|
+
val running = pipe != null
|
|
466
|
+
if (!running) pendingDeepLinks.add(url)
|
|
467
|
+
running
|
|
468
|
+
}
|
|
469
|
+
if (started) sendDeepLink(url)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private fun sendDeepLink(url: String) {
|
|
473
|
+
// B3: ham query gönderilmez — token/e-posta query'de taşınır. AEM
|
|
474
|
+
// ayrıştırması ve sourceUrlHash TAM url'den yapılır (aşağıda), yalnızca
|
|
475
|
+
// gönderilen `url` özelliği scheme+host+path'e indirgenir.
|
|
476
|
+
submit(Command.Track("deep_link", mapOf("url" to stripUrlQuery(url)), "custom"))
|
|
477
|
+
|
|
478
|
+
// Meta AEM: a link from Meta carries al_applink_data with an opaque,
|
|
479
|
+
// Meta-encrypted campaign_ids blob. Emitted at most once per URL — the
|
|
480
|
+
// same link re-opened is not a second measurement, and the server dedups
|
|
481
|
+
// on this hash too.
|
|
482
|
+
val applink = queryValue(url, "al_applink_data") ?: return
|
|
483
|
+
val campaignIds = extractAemCampaignIds(applink) ?: return
|
|
484
|
+
val hash = sha256Hex(url)
|
|
485
|
+
val fresh = synchronized(lock) { seenAemUrlHashes.add(hash) }
|
|
486
|
+
if (!fresh) return
|
|
487
|
+
submit(
|
|
488
|
+
Command.Track(
|
|
489
|
+
"adv_meta_aem",
|
|
490
|
+
mapOf("campaignIds" to campaignIds, "sourceUrlHash" to hash),
|
|
491
|
+
"custom",
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Decoded value of one query parameter, or null. */
|
|
497
|
+
private fun queryValue(url: String, name: String): String? =
|
|
498
|
+
runCatching { android.net.Uri.parse(url).getQueryParameter(name) }.getOrNull()
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
/** Test surface: how many links are waiting for `initialize`. */
|
|
502
|
+
val bufferedDeepLinkCount: Int
|
|
503
|
+
get() = synchronized(lock) { pendingDeepLinks.size }
|
|
504
|
+
|
|
505
|
+
val currentDeviceId: String?
|
|
506
|
+
get() = resolvedDeviceId
|
|
507
|
+
|
|
508
|
+
fun pendingCount(): Int = synchronized(lock) { engine }?.pendingEventIds()?.size ?: 0
|
|
509
|
+
|
|
510
|
+
/** Polls the conversion lookup. Null for an organic install. */
|
|
511
|
+
fun resolveDeferredDeepLink(): DeepLink? {
|
|
512
|
+
val (endpoint, apiKey, deviceId) =
|
|
513
|
+
synchronized(lock) {
|
|
514
|
+
Triple(startedEndpoint, startedApiKey, resolvedDeviceId)
|
|
515
|
+
}
|
|
516
|
+
if (endpoint == null || apiKey == null || deviceId == null) return null
|
|
517
|
+
return resolveDeferredDeepLink(
|
|
518
|
+
HttpConversionFetcher(endpoint = endpoint, apiKey = apiKey, deviceId = deviceId))
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Erasure spans BOTH stores. The engine clears what lives in
|
|
523
|
+
* SharedPreferences; the install-scoped identifiers live in
|
|
524
|
+
* `noBackupFilesDir` and are wiped here. Calling only the engine's `forgetMe`
|
|
525
|
+
* is the layering trap the spec recorded — an erasure that leaves the
|
|
526
|
+
* identifiers behind, and looks finished.
|
|
527
|
+
*/
|
|
528
|
+
fun forgetMe() {
|
|
529
|
+
// Both stores are wiped by the engine, on the pipe's thread, so the wipe is
|
|
530
|
+
// ordered behind any command already queued — a wipe performed here would
|
|
531
|
+
// race a pending trackInstall and the flag would be written back after it.
|
|
532
|
+
submit(Command.ForgetMe)
|
|
533
|
+
synchronized(lock) { resolvedDeviceId = null }
|
|
534
|
+
// Erasure clears the read caches too: a getter still answering "granted"
|
|
535
|
+
// after forgetMe would report a consent the device no longer holds.
|
|
536
|
+
consentMirror = false
|
|
537
|
+
consentDataMirror = null
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
fun stop() {
|
|
541
|
+
val (pipe, bridge, timer, receiver, application) =
|
|
542
|
+
synchronized(lock) {
|
|
543
|
+
val snapshot =
|
|
544
|
+
Quintuple(this.pipe, this.bridge, this.flushTimer, this.unlockReceiver, this.app)
|
|
545
|
+
this.pipe = null
|
|
546
|
+
this.engine = null
|
|
547
|
+
this.bridge = null
|
|
548
|
+
this.flushTimer = null
|
|
549
|
+
this.unlockReceiver = null
|
|
550
|
+
snapshot
|
|
551
|
+
}
|
|
552
|
+
timer?.cancel()
|
|
553
|
+
bridge?.stop()
|
|
554
|
+
receiver?.let { r -> application?.let { runCatching { it.unregisterReceiver(r) } } }
|
|
555
|
+
pipe?.shutdown()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private data class Quintuple(
|
|
559
|
+
val pipe: CommandPipe?,
|
|
560
|
+
val bridge: LifecycleBridge?,
|
|
561
|
+
val timer: Timer?,
|
|
562
|
+
val receiver: BroadcastReceiver?,
|
|
563
|
+
val app: Application?,
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* B3: deep-link URL'sinden query+fragment atılır (token/e-posta taşınır).
|
|
569
|
+
* Saf dize işlemi — Android çalıştırıcısı gerekmez, JVM testinde de çalışır.
|
|
570
|
+
*/
|
|
571
|
+
internal fun stripUrlQuery(url: String): String {
|
|
572
|
+
val cut = url.indexOfAny(charArrayOf('?', '#'))
|
|
573
|
+
return if (cut >= 0) url.substring(0, cut) else url
|
|
574
|
+
}
|