@capgo/capacitor-admob 8.1.20 → 8.2.0
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/CapgoCapacitorAdmob.podspec +1 -0
- package/Package.swift +4 -2
- package/README.md +348 -10
- package/android/build.gradle +3 -2
- package/android/src/main/kotlin/admob/plus/capacitor/AdMobPlusPlugin.kt +121 -0
- package/android/src/main/kotlin/admob/plus/capacitor/ConsentHelper.kt +140 -0
- package/android/src/main/kotlin/admob/plus/capacitor/Generated.kt +7 -0
- package/android/src/main/kotlin/admob/plus/capacitor/feed/FeedAdManager.kt +512 -0
- package/android/src/main/kotlin/admob/plus/core/Ad.kt +2 -4
- package/android/src/main/kotlin/admob/plus/core/Helper.kt +18 -1
- package/dist/docs.json +543 -5
- package/dist/esm/definitions.d.ts +284 -1
- package/dist/esm/definitions.js +55 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/events.d.ts +127 -0
- package/dist/esm/events.js +50 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/feed-ad.d.ts +32 -0
- package/dist/esm/feed-ad.js +133 -0
- package/dist/esm/feed-ad.js.map +1 -0
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +4 -32
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/plugin-instance.d.ts +3 -0
- package/dist/esm/plugin-instance.js +35 -0
- package/dist/esm/plugin-instance.js.map +1 -0
- package/dist/esm/web.d.ts +11 -1
- package/dist/esm/web.js +40 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +313 -31
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +313 -31
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/AdmobPlusPlugin/AdmobPlusPlugin.swift +131 -1
- package/ios/Sources/AdmobPlusPlugin/BannerAd.swift +6 -9
- package/ios/Sources/AdmobPlusPlugin/ConsentExecutor.swift +100 -0
- package/ios/Sources/AdmobPlusPlugin/FeedAdManager.swift +435 -0
- package/ios/Sources/AdmobPlusPlugin/Generated.swift +7 -0
- package/ios/Sources/AdmobPlusPlugin/Helper.swift +29 -0
- package/ios/Sources/AdmobPlusPlugin/InterstitialAd.swift +7 -13
- package/ios/Sources/AdmobPlusPlugin/RewardedAd.swift +13 -20
- package/ios/Sources/AdmobPlusPlugin/RewardedInterstitialAd.swift +13 -20
- package/ios/Tests/AdmobPlusPluginTests/AdmobPlusPluginTests.swift +3 -6
- package/package.json +2 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package admob.plus.capacitor
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import com.getcapacitor.JSArray
|
|
5
|
+
import com.getcapacitor.JSObject
|
|
6
|
+
import com.getcapacitor.PluginCall
|
|
7
|
+
import com.google.android.ump.ConsentDebugSettings
|
|
8
|
+
import com.google.android.ump.ConsentInformation
|
|
9
|
+
import com.google.android.ump.ConsentRequestParameters
|
|
10
|
+
import com.google.android.ump.UserMessagingPlatform
|
|
11
|
+
|
|
12
|
+
class ConsentHelper(private val plugin: AdMobPlusPlugin) {
|
|
13
|
+
private var consentInformation: ConsentInformation? = null
|
|
14
|
+
|
|
15
|
+
fun requestConsentInfo(call: PluginCall) {
|
|
16
|
+
try {
|
|
17
|
+
val consentInfo = ensureConsentInfo()
|
|
18
|
+
val activity = plugin.activity
|
|
19
|
+
if (activity == null) {
|
|
20
|
+
call.reject("Trying to request consent info but the Activity is null")
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
val debugSettingsBuilder = ConsentDebugSettings.Builder(plugin.context)
|
|
25
|
+
val data = call.data
|
|
26
|
+
|
|
27
|
+
if (data.has("testDeviceIdentifiers")) {
|
|
28
|
+
val devices = call.getArray("testDeviceIdentifiers") ?: JSArray()
|
|
29
|
+
for (i in 0 until devices.length()) {
|
|
30
|
+
debugSettingsBuilder.addTestDeviceHashedId(devices.getString(i))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (data.has("debugGeography")) {
|
|
35
|
+
debugSettingsBuilder.setDebugGeography(data.getInt("debugGeography"))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
val paramsBuilder = ConsentRequestParameters.Builder()
|
|
39
|
+
.setConsentDebugSettings(debugSettingsBuilder.build())
|
|
40
|
+
|
|
41
|
+
if (data.has("tagForUnderAgeOfConsent")) {
|
|
42
|
+
paramsBuilder.setTagForUnderAgeOfConsent(data.getBoolean("tagForUnderAgeOfConsent"))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
consentInfo.requestConsentInfoUpdate(
|
|
46
|
+
activity,
|
|
47
|
+
paramsBuilder.build(),
|
|
48
|
+
{
|
|
49
|
+
call.resolve(buildConsentInfo(consentInfo))
|
|
50
|
+
},
|
|
51
|
+
{ formError ->
|
|
52
|
+
call.reject(formError.message)
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
} catch (ex: Exception) {
|
|
56
|
+
call.reject(ex.localizedMessage, ex)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun showPrivacyOptionsForm(call: PluginCall) {
|
|
61
|
+
try {
|
|
62
|
+
val activity = plugin.activity
|
|
63
|
+
if (activity == null) {
|
|
64
|
+
call.reject("Trying to show the privacy options form but the Activity is null")
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ensureConsentInfo()
|
|
69
|
+
activity.runOnUiThread {
|
|
70
|
+
UserMessagingPlatform.showPrivacyOptionsForm(activity) { formError ->
|
|
71
|
+
if (formError != null) {
|
|
72
|
+
call.reject("Error when show privacy form", formError.message)
|
|
73
|
+
} else {
|
|
74
|
+
call.resolve()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (ex: Exception) {
|
|
79
|
+
call.reject(ex.localizedMessage, ex)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fun showConsentForm(call: PluginCall) {
|
|
84
|
+
try {
|
|
85
|
+
val activity = plugin.activity
|
|
86
|
+
if (activity == null) {
|
|
87
|
+
call.reject("Trying to show the consent form but the Activity is null")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
val consentInfo = ensureConsentInfo()
|
|
92
|
+
activity.runOnUiThread {
|
|
93
|
+
UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity) { formError ->
|
|
94
|
+
if (formError != null) {
|
|
95
|
+
call.reject("Error when show consent form", formError.message)
|
|
96
|
+
return@loadAndShowConsentFormIfRequired
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
call.resolve(buildConsentInfo(consentInfo, includeFormAvailability = false))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (ex: Exception) {
|
|
103
|
+
call.reject(ex.localizedMessage, ex)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun ensureConsentInfo(): ConsentInformation {
|
|
108
|
+
if (consentInformation == null) {
|
|
109
|
+
consentInformation = UserMessagingPlatform.getConsentInformation(plugin.context)
|
|
110
|
+
}
|
|
111
|
+
return consentInformation!!
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun buildConsentInfo(
|
|
115
|
+
consentInfo: ConsentInformation,
|
|
116
|
+
includeFormAvailability: Boolean = true,
|
|
117
|
+
): JSObject {
|
|
118
|
+
val result = JSObject()
|
|
119
|
+
result.put("status", getConsentStatusString(consentInfo.consentStatus))
|
|
120
|
+
if (includeFormAvailability) {
|
|
121
|
+
result.put("isConsentFormAvailable", consentInfo.isConsentFormAvailable)
|
|
122
|
+
}
|
|
123
|
+
result.put("canRequestAds", consentInfo.canRequestAds())
|
|
124
|
+
result.put(
|
|
125
|
+
"privacyOptionsRequirementStatus",
|
|
126
|
+
consentInfo.privacyOptionsRequirementStatus.name,
|
|
127
|
+
)
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun getConsentStatusString(consentStatus: Int): String {
|
|
132
|
+
return when (consentStatus) {
|
|
133
|
+
ConsentInformation.ConsentStatus.REQUIRED -> "REQUIRED"
|
|
134
|
+
ConsentInformation.ConsentStatus.NOT_REQUIRED -> "NOT_REQUIRED"
|
|
135
|
+
ConsentInformation.ConsentStatus.OBTAINED -> "OBTAINED"
|
|
136
|
+
ConsentInformation.ConsentStatus.UNKNOWN -> "UNKNOWN"
|
|
137
|
+
else -> "UNKNOWN"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -38,5 +38,12 @@ class Generated {
|
|
|
38
38
|
const val REWARDED_REWARD = "rewarded.reward"
|
|
39
39
|
const val REWARDED_SHOW = "rewarded.show"
|
|
40
40
|
const val REWARDED_SHOW_FAIL = "rewarded.showfail"
|
|
41
|
+
const val FEED_CLICK = "feed.click"
|
|
42
|
+
const val FEED_CLOSE = "feed.close"
|
|
43
|
+
const val FEED_IMPRESSION = "feed.impression"
|
|
44
|
+
const val FEED_LOAD = "feed.load"
|
|
45
|
+
const val FEED_LOAD_FAIL = "feed.loadfail"
|
|
46
|
+
const val FEED_OPEN = "feed.open"
|
|
47
|
+
const val FEED_REFRESH = "feed.refresh"
|
|
41
48
|
}
|
|
42
49
|
}
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
package admob.plus.capacitor.feed
|
|
2
|
+
|
|
3
|
+
import admob.plus.capacitor.AdMobPlusPlugin
|
|
4
|
+
import admob.plus.capacitor.Generated
|
|
5
|
+
import android.content.pm.ApplicationInfo
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.graphics.Canvas
|
|
8
|
+
import android.graphics.Color
|
|
9
|
+
import android.util.Log
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.util.TypedValue
|
|
13
|
+
import android.view.Gravity
|
|
14
|
+
import android.view.View
|
|
15
|
+
import android.view.ViewGroup
|
|
16
|
+
import android.webkit.WebView
|
|
17
|
+
import java.io.File
|
|
18
|
+
import java.io.FileOutputStream
|
|
19
|
+
import android.widget.Button
|
|
20
|
+
import android.widget.FrameLayout
|
|
21
|
+
import android.widget.LinearLayout
|
|
22
|
+
import android.widget.TextView
|
|
23
|
+
import com.getcapacitor.JSObject
|
|
24
|
+
import com.google.android.libraries.ads.mobile.sdk.banner.AdSize
|
|
25
|
+
import com.google.android.libraries.ads.mobile.sdk.banner.AdView
|
|
26
|
+
import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd
|
|
27
|
+
import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdEventCallback
|
|
28
|
+
import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback
|
|
29
|
+
import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError
|
|
30
|
+
import com.google.android.libraries.ads.mobile.sdk.nativead.MediaView
|
|
31
|
+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd
|
|
32
|
+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoader
|
|
33
|
+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback
|
|
34
|
+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest
|
|
35
|
+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView
|
|
36
|
+
import kotlin.math.max
|
|
37
|
+
|
|
38
|
+
private const val MIN_AUTO_REFRESH_MS = 30_000L
|
|
39
|
+
private const val FEED_SCREENSHOT_LOG_TAG = "CapgoAdmobFeed"
|
|
40
|
+
private const val MAX_NATIVE_NO_FILL_RETRIES = 0
|
|
41
|
+
private const val NATIVE_NO_FILL_RETRY_DELAY_MS = 2_000L
|
|
42
|
+
|
|
43
|
+
class FeedAdManager(private val plugin: AdMobPlusPlugin) {
|
|
44
|
+
private val entries = mutableMapOf<Int, FeedAdEntry>()
|
|
45
|
+
private var nextId = 1
|
|
46
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
47
|
+
|
|
48
|
+
fun create(
|
|
49
|
+
format: String,
|
|
50
|
+
adUnitId: String,
|
|
51
|
+
positionKey: String?,
|
|
52
|
+
autoRefreshMs: Int?,
|
|
53
|
+
nativeStyle: Map<String, String>,
|
|
54
|
+
): Int {
|
|
55
|
+
val id = nextId++
|
|
56
|
+
val entry = FeedAdEntry(
|
|
57
|
+
id = id,
|
|
58
|
+
format = if (format == "banner") FeedAdFormat.BANNER else FeedAdFormat.NATIVE,
|
|
59
|
+
adUnitId = adUnitId,
|
|
60
|
+
positionKey = positionKey,
|
|
61
|
+
nativeStyle = nativeStyle,
|
|
62
|
+
plugin = plugin,
|
|
63
|
+
mainHandler = mainHandler,
|
|
64
|
+
)
|
|
65
|
+
autoRefreshMs?.let { entry.setAutoRefresh(it) }
|
|
66
|
+
entries[id] = entry
|
|
67
|
+
return id
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun destroy(id: Int) {
|
|
71
|
+
entries.remove(id)?.destroy()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fun load(id: Int, onComplete: (String?) -> Unit) {
|
|
75
|
+
val entry = entries[id]
|
|
76
|
+
if (entry == null) {
|
|
77
|
+
onComplete("Feed ad not found")
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
entry.load(onComplete)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fun isLoaded(id: Int): Boolean = entries[id]?.isLoaded == true
|
|
84
|
+
|
|
85
|
+
fun updateBounds(
|
|
86
|
+
id: Int,
|
|
87
|
+
x: Double,
|
|
88
|
+
y: Double,
|
|
89
|
+
width: Double,
|
|
90
|
+
height: Double,
|
|
91
|
+
visible: Boolean,
|
|
92
|
+
) {
|
|
93
|
+
entries[id]?.updateBounds(x, y, width, height, visible)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fun setAutoRefresh(id: Int, autoRefreshMs: Int?) {
|
|
97
|
+
entries[id]?.setAutoRefresh(autoRefreshMs)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private enum class FeedAdFormat {
|
|
102
|
+
NATIVE,
|
|
103
|
+
BANNER,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private class FeedAdEntry(
|
|
107
|
+
val id: Int,
|
|
108
|
+
val format: FeedAdFormat,
|
|
109
|
+
val adUnitId: String,
|
|
110
|
+
val positionKey: String?,
|
|
111
|
+
val nativeStyle: Map<String, String>,
|
|
112
|
+
private val plugin: AdMobPlusPlugin,
|
|
113
|
+
private val mainHandler: Handler,
|
|
114
|
+
) {
|
|
115
|
+
private var adHost: View? = null
|
|
116
|
+
private var nativeAd: NativeAd? = null
|
|
117
|
+
private var bannerAdView: AdView? = null
|
|
118
|
+
private var loaded = false
|
|
119
|
+
private var refreshRunnable: Runnable? = null
|
|
120
|
+
private var loadCallback: ((String?) -> Unit)? = null
|
|
121
|
+
private var nativeNoFillRetries = 0
|
|
122
|
+
private var nativeNoFillRetryRunnable: Runnable? = null
|
|
123
|
+
private var overlayVisibleLogged = false
|
|
124
|
+
private var ciBannerSnapshotWritten = false
|
|
125
|
+
private val ciBannerSnapshotToken = Any()
|
|
126
|
+
|
|
127
|
+
val isLoaded: Boolean
|
|
128
|
+
get() = loaded
|
|
129
|
+
|
|
130
|
+
fun destroy() {
|
|
131
|
+
mainHandler.removeCallbacksAndMessages(ciBannerSnapshotToken)
|
|
132
|
+
refreshRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
133
|
+
refreshRunnable = null
|
|
134
|
+
nativeNoFillRetryRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
135
|
+
nativeNoFillRetryRunnable = null
|
|
136
|
+
nativeAd?.destroy()
|
|
137
|
+
nativeAd = null
|
|
138
|
+
adHost?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
|
139
|
+
adHost = null
|
|
140
|
+
bannerAdView?.destroy()
|
|
141
|
+
bannerAdView = null
|
|
142
|
+
loaded = false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fun setAutoRefresh(autoRefreshMs: Int?) {
|
|
146
|
+
refreshRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
147
|
+
refreshRunnable = null
|
|
148
|
+
if (autoRefreshMs == null || autoRefreshMs < MIN_AUTO_REFRESH_MS) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
val runnable = object : Runnable {
|
|
152
|
+
override fun run() {
|
|
153
|
+
emitFeed(Generated.Events.FEED_REFRESH, null)
|
|
154
|
+
load { }
|
|
155
|
+
mainHandler.postDelayed(this, autoRefreshMs.toLong())
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
refreshRunnable = runnable
|
|
159
|
+
mainHandler.postDelayed(runnable, autoRefreshMs.toLong())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fun load(onComplete: (String?) -> Unit) {
|
|
163
|
+
loadCallback = onComplete
|
|
164
|
+
mainHandler.post {
|
|
165
|
+
when (format) {
|
|
166
|
+
FeedAdFormat.NATIVE -> loadNative()
|
|
167
|
+
FeedAdFormat.BANNER -> loadBanner()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun updateBounds(x: Double, y: Double, width: Double, height: Double, visible: Boolean) {
|
|
173
|
+
mainHandler.post {
|
|
174
|
+
val webView = plugin.bridge.webView
|
|
175
|
+
val host = adHost ?: return@post
|
|
176
|
+
val parent = webView.parent as? ViewGroup ?: return@post
|
|
177
|
+
if (host.parent == null) {
|
|
178
|
+
parent.addView(host)
|
|
179
|
+
}
|
|
180
|
+
val density = webView.resources.displayMetrics.density
|
|
181
|
+
val left = (x * density).toInt()
|
|
182
|
+
val top = (y * density).toInt() + webView.top
|
|
183
|
+
val w = max(1, (width * density).toInt())
|
|
184
|
+
val h = max(1, (height * density).toInt())
|
|
185
|
+
val params = ViewGroup.MarginLayoutParams(w, h).apply {
|
|
186
|
+
leftMargin = left
|
|
187
|
+
topMargin = top
|
|
188
|
+
}
|
|
189
|
+
host.layoutParams = params
|
|
190
|
+
host.visibility = if (visible && loaded) View.VISIBLE else View.INVISIBLE
|
|
191
|
+
host.bringToFront()
|
|
192
|
+
if (visible && loaded && !overlayVisibleLogged && w > 40 && h > 20) {
|
|
193
|
+
overlayVisibleLogged = true
|
|
194
|
+
val formatLabel = if (format == FeedAdFormat.BANNER) "banner" else "native"
|
|
195
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "overlay_visible id=$id format=$formatLabel")
|
|
196
|
+
if (format == FeedAdFormat.BANNER) {
|
|
197
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_slot_ready id=$id")
|
|
198
|
+
writeCiBannerOverlaySnapshotNow(host, w, h)
|
|
199
|
+
}
|
|
200
|
+
} else if (
|
|
201
|
+
visible &&
|
|
202
|
+
loaded &&
|
|
203
|
+
format == FeedAdFormat.BANNER &&
|
|
204
|
+
!ciBannerSnapshotWritten &&
|
|
205
|
+
w > 40 &&
|
|
206
|
+
h > 20
|
|
207
|
+
) {
|
|
208
|
+
writeCiBannerOverlaySnapshotNow(host, w, h)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fun scheduleCiBannerOverlaySnapshotAfterLoad() {
|
|
214
|
+
if (ciBannerSnapshotWritten || format != FeedAdFormat.BANNER) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_snapshot_schedule id=$id")
|
|
218
|
+
adHost?.let { host ->
|
|
219
|
+
val lp = host.layoutParams
|
|
220
|
+
val w = host.width.takeIf { it > 20 } ?: lp?.width ?: 0
|
|
221
|
+
val h = host.height.takeIf { it > 10 } ?: lp?.height ?: 0
|
|
222
|
+
if (w > 20 && h > 10) {
|
|
223
|
+
mainHandler.postDelayed(
|
|
224
|
+
{
|
|
225
|
+
if (!ciBannerSnapshotWritten) {
|
|
226
|
+
writeCiBannerOverlaySnapshotNow(host, w, h)
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
ciBannerSnapshotToken,
|
|
230
|
+
0L,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
val delaysMs = longArrayOf(150L, 400L, 900L, 1_600L, 2_400L)
|
|
235
|
+
for (delayMs in delaysMs) {
|
|
236
|
+
mainHandler.postDelayed(
|
|
237
|
+
{
|
|
238
|
+
if (!ciBannerSnapshotWritten) {
|
|
239
|
+
adHost?.let { host ->
|
|
240
|
+
val lp = host.layoutParams
|
|
241
|
+
val w = host.width.takeIf { it > 20 } ?: lp?.width ?: 0
|
|
242
|
+
val h = host.height.takeIf { it > 10 } ?: lp?.height ?: 0
|
|
243
|
+
writeCiBannerOverlaySnapshotNow(host, w, h)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
ciBannerSnapshotToken,
|
|
248
|
+
delayMs,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun writeCiBannerOverlaySnapshotNow(host: View, layoutW: Int, layoutH: Int) {
|
|
254
|
+
var width = if (host.width >= 20) host.width else layoutW
|
|
255
|
+
var height = if (host.height >= 10) host.height else layoutH
|
|
256
|
+
if (width < 20 || height < 10) {
|
|
257
|
+
Log.i(
|
|
258
|
+
FEED_SCREENSHOT_LOG_TAG,
|
|
259
|
+
"ci_banner_snapshot_failed message=host_not_laid_out w=${host.width} h=${host.height} lw=$layoutW lh=$layoutH",
|
|
260
|
+
)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if (host.width < 20 || host.height < 10) {
|
|
264
|
+
host.measure(
|
|
265
|
+
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
266
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
|
|
267
|
+
)
|
|
268
|
+
host.layout(0, 0, width, height)
|
|
269
|
+
}
|
|
270
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_snapshot_attempt id=$id")
|
|
271
|
+
var bitmap: Bitmap? = null
|
|
272
|
+
try {
|
|
273
|
+
width = host.width.coerceAtLeast(width).coerceAtLeast(1)
|
|
274
|
+
height = host.height.coerceAtLeast(height).coerceAtLeast(1)
|
|
275
|
+
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
276
|
+
val canvas = Canvas(bitmap)
|
|
277
|
+
canvas.drawColor(Color.WHITE)
|
|
278
|
+
host.draw(canvas)
|
|
279
|
+
val file = File(plugin.context.cacheDir, "ci_feed_banner.png")
|
|
280
|
+
FileOutputStream(file).use { out ->
|
|
281
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
|
282
|
+
}
|
|
283
|
+
ciBannerSnapshotWritten = true
|
|
284
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_snapshot_written path=${file.absolutePath}")
|
|
285
|
+
} catch (e: Exception) {
|
|
286
|
+
Log.i(
|
|
287
|
+
FEED_SCREENSHOT_LOG_TAG,
|
|
288
|
+
"ci_banner_snapshot_failed message=${e.message}",
|
|
289
|
+
)
|
|
290
|
+
} finally {
|
|
291
|
+
bitmap?.recycle()
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private fun finishLoad(errorMessage: String?) {
|
|
296
|
+
val cb = loadCallback
|
|
297
|
+
loadCallback = null
|
|
298
|
+
cb?.invoke(errorMessage)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private fun loadNative() {
|
|
302
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load_start id=$id format=native")
|
|
303
|
+
val request = NativeAdRequest.Builder(adUnitId, listOf(NativeAd.NativeAdType.NATIVE)).build()
|
|
304
|
+
NativeAdLoader.load(
|
|
305
|
+
request,
|
|
306
|
+
object : NativeAdLoaderCallback {
|
|
307
|
+
override fun onNativeAdLoaded(ad: NativeAd) {
|
|
308
|
+
mainHandler.post {
|
|
309
|
+
displayNativeAd(ad)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override fun onAdFailedToLoad(adError: LoadAdError) {
|
|
314
|
+
Log.i(
|
|
315
|
+
FEED_SCREENSHOT_LOG_TAG,
|
|
316
|
+
"feed_load_fail id=$id format=native code=${adError.code.value} message=${adError.message}",
|
|
317
|
+
)
|
|
318
|
+
if (shouldRetryNativeNoFill(adError)) {
|
|
319
|
+
nativeNoFillRetries += 1
|
|
320
|
+
nativeNoFillRetryRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
321
|
+
val retry = Runnable { loadNative() }
|
|
322
|
+
nativeNoFillRetryRunnable = retry
|
|
323
|
+
mainHandler.postDelayed(retry, NATIVE_NO_FILL_RETRY_DELAY_MS)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
emitFeedFail(adError)
|
|
327
|
+
loaded = false
|
|
328
|
+
finishLoad(adError.message)
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private fun displayNativeAd(ad: NativeAd) {
|
|
335
|
+
val oldHost = adHost
|
|
336
|
+
val oldParent = oldHost?.parent as? ViewGroup
|
|
337
|
+
oldParent?.removeView(oldHost)
|
|
338
|
+
nativeAd?.destroy()
|
|
339
|
+
nativeAd = ad
|
|
340
|
+
val nativeAdView = buildNativeAdView(ad)
|
|
341
|
+
adHost = nativeAdView
|
|
342
|
+
oldHost?.layoutParams?.let { nativeAdView.layoutParams = it }
|
|
343
|
+
oldParent?.addView(nativeAdView)
|
|
344
|
+
loaded = true
|
|
345
|
+
nativeAdView.visibility = View.VISIBLE
|
|
346
|
+
|
|
347
|
+
val assets = JSObject()
|
|
348
|
+
assets.put("headline", ad.headline ?: "")
|
|
349
|
+
assets.put("body", ad.body ?: "")
|
|
350
|
+
assets.put("callToAction", ad.callToAction ?: "")
|
|
351
|
+
assets.put("advertiser", ad.advertiser ?: "")
|
|
352
|
+
assets.put("price", ad.price ?: "")
|
|
353
|
+
assets.put("store", ad.store ?: "")
|
|
354
|
+
ad.starRating?.let { assets.put("starRating", it) }
|
|
355
|
+
ad.mediaContent?.aspectRatio?.let { assets.put("mediaAspectRatio", it) }
|
|
356
|
+
nativeNoFillRetries = 0
|
|
357
|
+
emitFeed(Generated.Events.FEED_LOAD, assets)
|
|
358
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load id=$id format=native")
|
|
359
|
+
finishLoad(null)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private fun shouldRetryNativeNoFill(adError: LoadAdError): Boolean {
|
|
363
|
+
val isDebuggable =
|
|
364
|
+
(plugin.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
365
|
+
return isDebuggable &&
|
|
366
|
+
adError.message.equals("No fill.", ignoreCase = true) &&
|
|
367
|
+
nativeNoFillRetries < MAX_NATIVE_NO_FILL_RETRIES
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private fun buildNativeAdView(ad: NativeAd): NativeAdView {
|
|
371
|
+
val context = plugin.context
|
|
372
|
+
val nativeAdView = NativeAdView(context)
|
|
373
|
+
nativeAdView.setBackgroundColor(parseColor(nativeStyle["backgroundColor"]) ?: Color.parseColor("#F2F2F2"))
|
|
374
|
+
|
|
375
|
+
val column = LinearLayout(context)
|
|
376
|
+
column.orientation = LinearLayout.VERTICAL
|
|
377
|
+
column.setPadding(24, 24, 24, 24)
|
|
378
|
+
|
|
379
|
+
val attribution = TextView(context)
|
|
380
|
+
attribution.text = "Ad"
|
|
381
|
+
attribution.gravity = Gravity.END
|
|
382
|
+
attribution.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
|
|
383
|
+
column.addView(attribution)
|
|
384
|
+
|
|
385
|
+
val mediaView = MediaView(context)
|
|
386
|
+
mediaView.layoutParams = LinearLayout.LayoutParams(
|
|
387
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
388
|
+
(180 * context.resources.displayMetrics.density).toInt(),
|
|
389
|
+
)
|
|
390
|
+
column.addView(mediaView)
|
|
391
|
+
|
|
392
|
+
val headline = TextView(context)
|
|
393
|
+
headline.text = ad.headline
|
|
394
|
+
headline.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
|
395
|
+
headline.setTextColor(parseColor(nativeStyle["headlineTextColor"]) ?: Color.BLACK)
|
|
396
|
+
nativeAdView.headlineView = headline
|
|
397
|
+
column.addView(headline)
|
|
398
|
+
|
|
399
|
+
if (!ad.body.isNullOrBlank()) {
|
|
400
|
+
val body = TextView(context)
|
|
401
|
+
body.text = ad.body
|
|
402
|
+
body.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
|
|
403
|
+
body.setTextColor(parseColor(nativeStyle["bodyTextColor"]) ?: Color.DKGRAY)
|
|
404
|
+
nativeAdView.bodyView = body
|
|
405
|
+
column.addView(body)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
val cta = Button(context)
|
|
409
|
+
cta.text = ad.callToAction
|
|
410
|
+
cta.isClickable = false
|
|
411
|
+
cta.isEnabled = false
|
|
412
|
+
cta.setBackgroundColor(parseColor(nativeStyle["ctaBackgroundColor"]) ?: Color.parseColor("#2563EB"))
|
|
413
|
+
cta.setTextColor(parseColor(nativeStyle["ctaTextColor"]) ?: Color.WHITE)
|
|
414
|
+
nativeAdView.callToActionView = cta
|
|
415
|
+
column.addView(cta)
|
|
416
|
+
|
|
417
|
+
nativeAdView.addView(
|
|
418
|
+
column,
|
|
419
|
+
ViewGroup.LayoutParams(
|
|
420
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
421
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
nativeAdView.registerNativeAd(ad, mediaView)
|
|
425
|
+
return nativeAdView
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private fun loadBanner() {
|
|
429
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load_start id=$id format=banner")
|
|
430
|
+
val activity = plugin.activity
|
|
431
|
+
val webView = plugin.bridge.webView
|
|
432
|
+
if (bannerAdView == null) {
|
|
433
|
+
bannerAdView = AdView(activity)
|
|
434
|
+
adHost = bannerAdView
|
|
435
|
+
}
|
|
436
|
+
loaded = false
|
|
437
|
+
val adWidth = (webView.width / webView.resources.displayMetrics.density).toInt().coerceAtLeast(320)
|
|
438
|
+
val adSize = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(activity, adWidth)
|
|
439
|
+
bannerAdView!!.loadAd(
|
|
440
|
+
com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest.Builder(adUnitId, adSize).build(),
|
|
441
|
+
object : AdLoadCallback<BannerAd> {
|
|
442
|
+
override fun onAdLoaded(bannerAd: BannerAd) {
|
|
443
|
+
loaded = true
|
|
444
|
+
bannerAd.adEventCallback = object : BannerAdEventCallback {
|
|
445
|
+
override fun onAdClicked() {
|
|
446
|
+
emitFeed(Generated.Events.FEED_CLICK, null)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
override fun onAdDismissedFullScreenContent() {
|
|
450
|
+
emitFeed(Generated.Events.FEED_CLOSE, null)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
override fun onAdImpression() {
|
|
454
|
+
emitFeed(Generated.Events.FEED_IMPRESSION, null)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
override fun onAdShowedFullScreenContent() {
|
|
458
|
+
emitFeed(Generated.Events.FEED_OPEN, null)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
val assets = JSObject()
|
|
462
|
+
assets.put("width", adSize.width)
|
|
463
|
+
assets.put("height", adSize.height)
|
|
464
|
+
emitFeed(Generated.Events.FEED_LOAD, assets)
|
|
465
|
+
Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load id=$id format=banner")
|
|
466
|
+
scheduleCiBannerOverlaySnapshotAfterLoad()
|
|
467
|
+
finishLoad(null)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
override fun onAdFailedToLoad(loadAdError: LoadAdError) {
|
|
471
|
+
Log.i(
|
|
472
|
+
FEED_SCREENSHOT_LOG_TAG,
|
|
473
|
+
"feed_load_fail id=$id format=banner code=${loadAdError.code.value} message=${loadAdError.message}",
|
|
474
|
+
)
|
|
475
|
+
emitFeedFail(loadAdError)
|
|
476
|
+
loaded = false
|
|
477
|
+
finishLoad(loadAdError.message)
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private fun emitFeed(event: String, assets: JSObject?) {
|
|
484
|
+
val payload = JSObject()
|
|
485
|
+
payload.put("feedAdId", id)
|
|
486
|
+
positionKey?.let { payload.put("positionKey", it) }
|
|
487
|
+
payload.put("format", if (format == FeedAdFormat.BANNER) "banner" else "native")
|
|
488
|
+
assets?.let { payload.put("assets", it) }
|
|
489
|
+
plugin.emit(event, payload)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private fun emitFeedFail(error: LoadAdError) {
|
|
493
|
+
val payload = JSObject()
|
|
494
|
+
payload.put("feedAdId", id)
|
|
495
|
+
positionKey?.let { payload.put("positionKey", it) }
|
|
496
|
+
payload.put("format", if (format == FeedAdFormat.BANNER) "banner" else "native")
|
|
497
|
+
payload.put("code", error.code.value)
|
|
498
|
+
payload.put("message", error.message)
|
|
499
|
+
plugin.emit(Generated.Events.FEED_LOAD_FAIL, payload)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private fun parseColor(hex: String?): Int? {
|
|
503
|
+
if (hex.isNullOrBlank() || !hex.startsWith("#") || hex.length != 7) {
|
|
504
|
+
return null
|
|
505
|
+
}
|
|
506
|
+
return try {
|
|
507
|
+
Color.parseColor(hex)
|
|
508
|
+
} catch (_: IllegalArgumentException) {
|
|
509
|
+
null
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
@@ -6,16 +6,14 @@ import android.view.ViewGroup
|
|
|
6
6
|
import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError
|
|
7
7
|
import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError
|
|
8
8
|
import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardItem
|
|
9
|
-
import java.util.Objects
|
|
10
|
-
|
|
11
9
|
abstract class Ad(val id: Int, val adUnitId: String) {
|
|
12
10
|
init {
|
|
13
11
|
Helper.ads.put(id, this)
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
constructor(ctx: Context) : this(
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
requireNotNull(ctx.optId()),
|
|
16
|
+
requireNotNull(ctx.optAdUnitID()),
|
|
19
17
|
)
|
|
20
18
|
|
|
21
19
|
open fun destroy() {
|