@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,67 @@
|
|
|
1
|
+
package io.advenue.plugin
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The capability contracts each plugin artifact implements.
|
|
7
|
+
*
|
|
8
|
+
* The base declares them and never depends on Play Services or Firebase, so an
|
|
9
|
+
* app on Amazon, Huawei or F-Droid uses the base unchanged. A plugin brings its
|
|
10
|
+
* own vendor dependency and talks to it with full compile-time typing — the
|
|
11
|
+
* reflection is one lookup of the plugin's own class, not a wrapper around every
|
|
12
|
+
* vendor call.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Play Install Referrer. */
|
|
16
|
+
public interface InstallReferrerSource {
|
|
17
|
+
/**
|
|
18
|
+
* Blocking; the caller runs it off the main thread with its own deadline.
|
|
19
|
+
* Returns null when the referrer is unavailable, which is best-effort by
|
|
20
|
+
* design — an install with no referrer is organic, not a failure.
|
|
21
|
+
*/
|
|
22
|
+
public fun read(context: Context, timeoutMs: Long): ReferrerResult?
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public data class ReferrerResult(
|
|
26
|
+
val referrer: String,
|
|
27
|
+
/** Device clock: `referrerClickTimestampSeconds`. */
|
|
28
|
+
val clickTimestamp: Long,
|
|
29
|
+
/** Device clock: `installBeginTimestampSeconds`. */
|
|
30
|
+
val installTimestamp: Long,
|
|
31
|
+
/** Play's server clock; 0 when the install is unverified. */
|
|
32
|
+
val clickServerTimestamp: Long,
|
|
33
|
+
val installServerTimestamp: Long,
|
|
34
|
+
val googlePlayInstant: Boolean,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/** GAID and App Set ID. */
|
|
38
|
+
public interface AdvertisingIdSource {
|
|
39
|
+
/** Blocking; the caller runs it off the main thread. */
|
|
40
|
+
public fun read(context: Context): AdvertisingId?
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public data class AdvertisingId(
|
|
44
|
+
val gaid: String?,
|
|
45
|
+
val limitAdTracking: Boolean,
|
|
46
|
+
/**
|
|
47
|
+
* Developer-scoped, non-advertising. First-party fraud and dedup only —
|
|
48
|
+
* Google forbids using it for ads or measurement.
|
|
49
|
+
*/
|
|
50
|
+
val appSetId: String?,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/** Play Integrity Standard API. */
|
|
54
|
+
public interface AttestationSource {
|
|
55
|
+
/**
|
|
56
|
+
* [requestHash] is `sha256Hex(deviceId)`. The server re-derives the same
|
|
57
|
+
* string and compares it byte for byte, so no app id is hashed in: the token
|
|
58
|
+
* is decoded against this app's Play package name, which binds the app
|
|
59
|
+
* already.
|
|
60
|
+
*/
|
|
61
|
+
public fun token(context: Context, requestHash: String): String?
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Firebase Analytics App Instance ID. */
|
|
65
|
+
public interface AppInstanceIdSource {
|
|
66
|
+
public fun read(context: Context, timeoutMs: Long): String?
|
|
67
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package io.advenue.plugin
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import java.util.concurrent.CountDownLatch
|
|
5
|
+
import java.util.concurrent.TimeUnit
|
|
6
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The Firebase App Instance ID source for React Native, by reflection.
|
|
10
|
+
*
|
|
11
|
+
* The class name and package are not incidental — they are the contract. The
|
|
12
|
+
* Android SDK resolves its enrichment plugins with `Class.forName` against the
|
|
13
|
+
* fixed names in `PluginRegistry`, so this file exists at exactly the name that
|
|
14
|
+
* lookup asks for. Rename it and the SDK stops finding it, silently.
|
|
15
|
+
*
|
|
16
|
+
* The monorepo's `plugin-firebase` artifact does the same job with ordinary
|
|
17
|
+
* typed code, and that is the better implementation — but it declares a compile
|
|
18
|
+
* dependency on `firebase-analytics`, and adding that here would force Firebase
|
|
19
|
+
* on every React Native app that installs this package. So this one is checked
|
|
20
|
+
* in rather than vendored, and reaches Firebase only if the host app already
|
|
21
|
+
* ships it.
|
|
22
|
+
*
|
|
23
|
+
* Every failure is best-effort null: Firebase absent, not initialised,
|
|
24
|
+
* analytics collection disabled, or simply slower than the deadline. The
|
|
25
|
+
* install goes out without the id rather than waiting for it.
|
|
26
|
+
*/
|
|
27
|
+
public class FirebaseAppInstanceIdSource : AppInstanceIdSource {
|
|
28
|
+
override fun read(context: Context, timeoutMs: Long): String? {
|
|
29
|
+
return try {
|
|
30
|
+
val ctx = context.applicationContext
|
|
31
|
+
val analyticsClass = Class.forName("com.google.firebase.analytics.FirebaseAnalytics")
|
|
32
|
+
val analytics =
|
|
33
|
+
analyticsClass.getMethod("getInstance", Context::class.java).invoke(null, ctx)
|
|
34
|
+
val task = analyticsClass.getMethod("getAppInstanceId").invoke(analytics)
|
|
35
|
+
|
|
36
|
+
val taskClass = Class.forName("com.google.android.gms.tasks.Task")
|
|
37
|
+
val listenerClass = Class.forName("com.google.android.gms.tasks.OnCompleteListener")
|
|
38
|
+
val result = AtomicReference<String?>(null)
|
|
39
|
+
val done = CountDownLatch(1)
|
|
40
|
+
|
|
41
|
+
val listener =
|
|
42
|
+
java.lang.reflect.Proxy.newProxyInstance(
|
|
43
|
+
listenerClass.classLoader,
|
|
44
|
+
arrayOf(listenerClass),
|
|
45
|
+
) { _, method, args ->
|
|
46
|
+
if (method.name == "onComplete") {
|
|
47
|
+
try {
|
|
48
|
+
val completed = args?.firstOrNull()
|
|
49
|
+
val ok =
|
|
50
|
+
completed != null && taskClass.getMethod("isSuccessful").invoke(completed) == true
|
|
51
|
+
if (ok) {
|
|
52
|
+
result.set(taskClass.getMethod("getResult").invoke(completed) as? String)
|
|
53
|
+
}
|
|
54
|
+
} catch (_: Throwable) {
|
|
55
|
+
// Best effort; the latch still counts down below.
|
|
56
|
+
} finally {
|
|
57
|
+
done.countDown()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
taskClass.getMethod("addOnCompleteListener", listenerClass).invoke(task, listener)
|
|
64
|
+
// The caller already runs this on the enrichment pool behind its own
|
|
65
|
+
// deadline; blocking here is what turns Firebase's callback into the
|
|
66
|
+
// synchronous answer the contract asks for.
|
|
67
|
+
done.await(timeoutMs, TimeUnit.MILLISECONDS)
|
|
68
|
+
result.get()?.takeIf { it.isNotEmpty() }
|
|
69
|
+
} catch (_: Throwable) {
|
|
70
|
+
null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package io.advenue.plugin
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
|
5
|
+
import com.google.android.gms.appset.AppSet
|
|
6
|
+
import com.google.android.gms.tasks.Tasks
|
|
7
|
+
import java.util.concurrent.TimeUnit
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GAID and App Set ID.
|
|
11
|
+
*
|
|
12
|
+
* Adding this artifact merges `com.google.android.gms.permission.AD_ID` into the
|
|
13
|
+
* consumer's manifest. That is required for GAID access when targeting API 33+,
|
|
14
|
+
* **and** it changes the app's Play Console Data Safety declaration, and apps in
|
|
15
|
+
* the kids category must remove it. The integration docs carry the
|
|
16
|
+
* `tools:node="remove"` recipe — the consequence is the consumer's to accept,
|
|
17
|
+
* not ours to hide.
|
|
18
|
+
*/
|
|
19
|
+
public class PlayAdvertisingIdSource : AdvertisingIdSource {
|
|
20
|
+
override fun read(context: Context): AdvertisingId? {
|
|
21
|
+
val app = context.applicationContext
|
|
22
|
+
|
|
23
|
+
// Blocking by contract; the caller runs this off the main thread, where
|
|
24
|
+
// Play Services would throw.
|
|
25
|
+
val info = runCatching { AdvertisingIdClient.getAdvertisingIdInfo(app) }.getOrNull()
|
|
26
|
+
|
|
27
|
+
val raw = info?.id
|
|
28
|
+
// The all-zero GAID is what Play returns when tracking is limited. It looks
|
|
29
|
+
// like an identifier and is not one, so it must never travel.
|
|
30
|
+
val gaid = if (raw.isNullOrEmpty() || raw == ZERO_GAID) null else raw
|
|
31
|
+
|
|
32
|
+
val appSetId =
|
|
33
|
+
runCatching { Tasks.await(AppSet.getClient(app).appSetIdInfo, 2, TimeUnit.SECONDS).id }
|
|
34
|
+
.getOrNull()
|
|
35
|
+
|
|
36
|
+
if (gaid == null && appSetId == null && info == null) return null
|
|
37
|
+
return AdvertisingId(
|
|
38
|
+
gaid = gaid,
|
|
39
|
+
limitAdTracking = info?.isLimitAdTrackingEnabled ?: false,
|
|
40
|
+
appSetId = appSetId,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private companion object {
|
|
45
|
+
private const val ZERO_GAID = "00000000-0000-0000-0000-000000000000"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package io.advenue.plugin
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import com.android.installreferrer.api.InstallReferrerClient
|
|
7
|
+
import com.android.installreferrer.api.InstallReferrerStateListener
|
|
8
|
+
import java.util.concurrent.CountDownLatch
|
|
9
|
+
import java.util.concurrent.TimeUnit
|
|
10
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Google Play Install Referrer.
|
|
15
|
+
*
|
|
16
|
+
* This is the deterministic attribution on Android: an install sent without it
|
|
17
|
+
* is an organic install. Which is why the caller waits for it, unlike iOS where
|
|
18
|
+
* waiting for ATT is wrong.
|
|
19
|
+
*
|
|
20
|
+
* The Play callback can fire setup-finished, disconnect, or never respond at all
|
|
21
|
+
* on an offline device. It must settle **exactly once** on every path and the
|
|
22
|
+
* service binding must be released on every path — a disconnect or a hung
|
|
23
|
+
* connection otherwise leaks the binding and hangs the install forever. The
|
|
24
|
+
* React Native module got this right with an AtomicBoolean; this keeps it.
|
|
25
|
+
*/
|
|
26
|
+
public class PlayInstallReferrerSource : InstallReferrerSource {
|
|
27
|
+
override fun read(context: Context, timeoutMs: Long): ReferrerResult? {
|
|
28
|
+
val app = context.applicationContext
|
|
29
|
+
val client = InstallReferrerClient.newBuilder(app).build()
|
|
30
|
+
val settled = AtomicBoolean(false)
|
|
31
|
+
val result = AtomicReference<ReferrerResult?>(null)
|
|
32
|
+
val done = CountDownLatch(1)
|
|
33
|
+
val handler = Handler(Looper.getMainLooper())
|
|
34
|
+
|
|
35
|
+
fun finish(value: ReferrerResult?) {
|
|
36
|
+
if (!settled.compareAndSet(false, true)) return
|
|
37
|
+
result.set(value)
|
|
38
|
+
runCatching { client.endConnection() }
|
|
39
|
+
done.countDown()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Watchdog: the service may never call back. The referrer is best-effort
|
|
43
|
+
// enrichment, so the caller must always be released.
|
|
44
|
+
handler.postDelayed({ finish(null) }, timeoutMs)
|
|
45
|
+
|
|
46
|
+
runCatching {
|
|
47
|
+
client.startConnection(
|
|
48
|
+
object : InstallReferrerStateListener {
|
|
49
|
+
override fun onInstallReferrerSetupFinished(responseCode: Int) {
|
|
50
|
+
if (responseCode != InstallReferrerClient.InstallReferrerResponse.OK) {
|
|
51
|
+
finish(null)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
val value =
|
|
55
|
+
runCatching {
|
|
56
|
+
val details = client.installReferrer
|
|
57
|
+
ReferrerResult(
|
|
58
|
+
referrer = details.installReferrer,
|
|
59
|
+
clickTimestamp = details.referrerClickTimestampSeconds,
|
|
60
|
+
installTimestamp = details.installBeginTimestampSeconds,
|
|
61
|
+
// Play returns 0 here when the install is unverified; the
|
|
62
|
+
// caller forwards these as server-trusted only when non-zero.
|
|
63
|
+
clickServerTimestamp = details.referrerClickTimestampServerSeconds,
|
|
64
|
+
installServerTimestamp = details.installBeginTimestampServerSeconds,
|
|
65
|
+
googlePlayInstant = details.googlePlayInstantParam,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
.getOrNull()
|
|
69
|
+
finish(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun onInstallReferrerServiceDisconnected() {
|
|
73
|
+
// Dropped before setup finished, e.g. under memory pressure.
|
|
74
|
+
finish(null)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
.onFailure { finish(null) }
|
|
80
|
+
|
|
81
|
+
done.await(timeoutMs + 500, TimeUnit.MILLISECONDS)
|
|
82
|
+
handler.removeCallbacksAndMessages(null)
|
|
83
|
+
return result.get()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package io.advenue.plugin
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import com.google.android.play.core.integrity.IntegrityManagerFactory
|
|
6
|
+
import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
|
|
7
|
+
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
|
|
8
|
+
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
|
|
9
|
+
import com.google.android.gms.tasks.Tasks
|
|
10
|
+
import java.util.concurrent.TimeUnit
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Play Integrity Standard API — device attestation.
|
|
14
|
+
*
|
|
15
|
+
* The token provider is warmed once, because preparing it is the slow half and
|
|
16
|
+
* doing it per request would put seconds on the install path.
|
|
17
|
+
*
|
|
18
|
+
* The Google Cloud project number comes from the app's manifest:
|
|
19
|
+
* ```xml
|
|
20
|
+
* <meta-data android:name="io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER"
|
|
21
|
+
* android:value="123456789012" />
|
|
22
|
+
* ```
|
|
23
|
+
* Read as a long **and** as a string, because Android stores a large
|
|
24
|
+
* `android:value` as a String — reading only one of the two makes the feature
|
|
25
|
+
* silently unavailable for most projects.
|
|
26
|
+
*/
|
|
27
|
+
public class PlayIntegritySource : AttestationSource {
|
|
28
|
+
@Volatile private var provider: StandardIntegrityTokenProvider? = null
|
|
29
|
+
@Volatile private var warmed = false
|
|
30
|
+
|
|
31
|
+
override fun token(context: Context, requestHash: String): String? {
|
|
32
|
+
val app = context.applicationContext
|
|
33
|
+
warm(app)
|
|
34
|
+
val provider = this.provider ?: return null
|
|
35
|
+
|
|
36
|
+
return runCatching {
|
|
37
|
+
Tasks.await(
|
|
38
|
+
provider.request(
|
|
39
|
+
StandardIntegrityTokenRequest.builder().setRequestHash(requestHash).build(),
|
|
40
|
+
),
|
|
41
|
+
10,
|
|
42
|
+
TimeUnit.SECONDS,
|
|
43
|
+
)
|
|
44
|
+
.token()
|
|
45
|
+
}
|
|
46
|
+
.getOrNull()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun warm(context: Context) {
|
|
50
|
+
if (warmed) return
|
|
51
|
+
warmed = true
|
|
52
|
+
val projectNumber = cloudProjectNumber(context)
|
|
53
|
+
// Zero means the app never configured it — a different situation from Play
|
|
54
|
+
// Integrity being unavailable, and the two need different answers from
|
|
55
|
+
// support. Neither is an error here: attestation is best-effort and the
|
|
56
|
+
// install proceeds without it.
|
|
57
|
+
if (projectNumber == 0L) return
|
|
58
|
+
|
|
59
|
+
runCatching {
|
|
60
|
+
val manager = IntegrityManagerFactory.createStandard(context)
|
|
61
|
+
provider =
|
|
62
|
+
Tasks.await(
|
|
63
|
+
manager.prepareIntegrityToken(
|
|
64
|
+
PrepareIntegrityTokenRequest.builder().setCloudProjectNumber(projectNumber).build(),
|
|
65
|
+
),
|
|
66
|
+
15,
|
|
67
|
+
TimeUnit.SECONDS,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun cloudProjectNumber(context: Context): Long =
|
|
73
|
+
runCatching {
|
|
74
|
+
val info =
|
|
75
|
+
context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
|
|
76
|
+
val meta = info.metaData ?: return 0L
|
|
77
|
+
meta.getLong(META_KEY, 0L).takeIf { it != 0L }
|
|
78
|
+
?: meta.getString(META_KEY)?.toLongOrNull()
|
|
79
|
+
?: 0L
|
|
80
|
+
}
|
|
81
|
+
.getOrDefault(0L)
|
|
82
|
+
|
|
83
|
+
private companion object {
|
|
84
|
+
private const val META_KEY = "io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package io.advenue.plugin
|
|
2
|
+
|
|
3
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a plugin's entry class by fixed name, once, at initialisation.
|
|
7
|
+
*
|
|
8
|
+
* The consumer adds a Gradle line and writes nothing else, which is how Adjust's
|
|
9
|
+
* ten plugins work — `sdk-core/src/main/java/com/adjust/sdk/Reflection.java`
|
|
10
|
+
* resolves them the same way.
|
|
11
|
+
*
|
|
12
|
+
* **This is not the rejected "compileOnly plus reflection" shape.** There,
|
|
13
|
+
* reflection wrapped every vendor API call and every one of them was untyped.
|
|
14
|
+
* Here there is one lookup of *our own* class, at init, and the resolved class
|
|
15
|
+
* then talks to its dependency with ordinary compile-time typing.
|
|
16
|
+
*
|
|
17
|
+
* **ServiceLoader was considered and rejected.** It is the JVM standard and R8
|
|
18
|
+
* understands it, but its registration lives in a `META-INF/services` resource,
|
|
19
|
+
* which jar merging and aggressive shrinkers drop silently. A silently absent
|
|
20
|
+
* referrer plugin is an attribution outage that looks like organic traffic — the
|
|
21
|
+
* worst failure shape available. A missing class is detectable, and one
|
|
22
|
+
* `consumerProguardFiles` rule keeps it.
|
|
23
|
+
*/
|
|
24
|
+
public object PluginRegistry {
|
|
25
|
+
public const val PLAY_REFERRER: String = "io.advenue.plugin.PlayInstallReferrerSource"
|
|
26
|
+
public const val PLAY_IDENTITY: String = "io.advenue.plugin.PlayAdvertisingIdSource"
|
|
27
|
+
public const val PLAY_INTEGRITY: String = "io.advenue.plugin.PlayIntegritySource"
|
|
28
|
+
public const val FIREBASE: String = "io.advenue.plugin.FirebaseAppInstanceIdSource"
|
|
29
|
+
|
|
30
|
+
private val cache = ConcurrentHashMap<String, Any>()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The plugin at [className], or null when the artifact is not on the
|
|
34
|
+
* classpath — which is the ordinary case, not an error.
|
|
35
|
+
*
|
|
36
|
+
* A class that exists but does not implement the contract is a packaging
|
|
37
|
+
* mistake, and it degrades to "no plugin" rather than taking the host app
|
|
38
|
+
* down: an SDK that throws during `initialize` crashes an app that merely
|
|
39
|
+
* mis-declared a dependency.
|
|
40
|
+
*/
|
|
41
|
+
@Suppress("UNCHECKED_CAST")
|
|
42
|
+
public inline fun <reified T : Any> load(className: String): T? = loadAs(className, T::class.java)
|
|
43
|
+
|
|
44
|
+
@PublishedApi
|
|
45
|
+
internal fun <T : Any> loadAs(className: String, type: Class<T>): T? {
|
|
46
|
+
cache[className]?.let { return if (type.isInstance(it)) type.cast(it) else null }
|
|
47
|
+
return try {
|
|
48
|
+
val loaded = Class.forName(className).getDeclaredConstructor().newInstance()
|
|
49
|
+
if (!type.isInstance(loaded)) return null
|
|
50
|
+
cache[className] = loaded
|
|
51
|
+
type.cast(loaded)
|
|
52
|
+
} catch (_: Throwable) {
|
|
53
|
+
null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Test surface: forget what was resolved, so a test can vary the classpath. */
|
|
58
|
+
internal fun reset() {
|
|
59
|
+
cache.clear()
|
|
60
|
+
}
|
|
61
|
+
}
|