@ququplay/admob 8.1.0 → 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/README.md CHANGED
@@ -1327,14 +1327,15 @@ https://developers.google.com/android/reference/com/google/android/gms/ads/AdErr
1327
1327
 
1328
1328
  #### NativeAdOptions
1329
1329
 
1330
- | Prop | Type | Description | Default | Since |
1331
- | ------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
1332
- | **`style`** | <code><a href="#nativeadstyle">NativeAdStyle</a></code> | Colours of the native ad strip, as `#RRGGBB` (or `#AARRGGBB`). | | 8.1.0 |
1333
- | **`adId`** | <code>string</code> | The ad unit ID that you want to request | | 1.1.2 |
1334
- | **`isTesting`** | <code>boolean</code> | You can use test mode of ad. | <code>false</code> | 1.1.2 |
1335
- | **`margin`** | <code>number</code> | Margin Banner. Default is 0px; If position is BOTTOM_CENTER, margin is be margin-bottom. If position is TOP_CENTER, margin is be margin-top. | <code>0</code> | 1.1.2 |
1336
- | **`npa`** | <code>boolean</code> | The default behavior of the Google Mobile Ads SDK is to serve personalized ads. Set this to true to request Non-Personalized Ads | <code>false</code> | 1.2.0 |
1337
- | **`immersiveMode`** | <code>boolean</code> | Sets a flag that controls if this interstitial or reward object will be displayed in immersive mode. Call this method before show. During show, if this flag is on and immersive mode is supported, SYSTEM_UI_FLAG_IMMERSIVE_STICKY &SYSTEM_UI_FLAG_HIDE_NAVIGATION will be turned on for interstitial or reward ad. | | 7.0.3 |
1330
+ | Prop | Type | Description | Default | Since |
1331
+ | --------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
1332
+ | **`style`** | <code><a href="#nativeadstyle">NativeAdStyle</a></code> | Colours of the native ad strip, as `#RRGGBB` (or `#AARRGGBB`). | | 8.1.0 |
1333
+ | **`refreshInterval`** | <code>number</code> | Seconds the ad stays on screen before a new one replaces it. Only the time it is shown counts: hidden, or with the app in the background, it waits. The old ad stays up until the new one loads. 0, the default, keeps it until it expires after an hour. | | 8.2.0 |
1334
+ | **`adId`** | <code>string</code> | The ad unit ID that you want to request | | 1.1.2 |
1335
+ | **`isTesting`** | <code>boolean</code> | You can use test mode of ad. | <code>false</code> | 1.1.2 |
1336
+ | **`margin`** | <code>number</code> | Margin Banner. Default is 0px; If position is BOTTOM_CENTER, margin is be margin-bottom. If position is TOP_CENTER, margin is be margin-top. | <code>0</code> | 1.1.2 |
1337
+ | **`npa`** | <code>boolean</code> | The default behavior of the Google Mobile Ads SDK is to serve personalized ads. Set this to true to request Non-Personalized Ads | <code>false</code> | 1.2.0 |
1338
+ | **`immersiveMode`** | <code>boolean</code> | Sets a flag that controls if this interstitial or reward object will be displayed in immersive mode. Call this method before show. During show, if this flag is on and immersive mode is supported, SYSTEM_UI_FLAG_IMMERSIVE_STICKY &SYSTEM_UI_FLAG_HIDE_NAVIGATION will be turned on for interstitial or reward ad. | | 7.0.3 |
1338
1339
 
1339
1340
 
1340
1341
  #### NativeAdStyle
@@ -168,6 +168,18 @@ public class AdMob extends Plugin {
168
168
  bannerExecutor.removeBanner(call);
169
169
  }
170
170
 
171
+ @Override
172
+ protected void handleOnPause() {
173
+ super.handleOnPause();
174
+ nativeAdExecutor.onPause();
175
+ }
176
+
177
+ @Override
178
+ protected void handleOnResume() {
179
+ super.handleOnResume();
180
+ nativeAdExecutor.onResume();
181
+ }
182
+
171
183
  // Show a native ad across the top of the screen, loading one when there is none or it expired
172
184
  @PluginMethod
173
185
  public void showNativeAd(final PluginCall call) {
@@ -39,6 +39,8 @@ import com.google.android.gms.common.util.BiConsumer;
39
39
  /**
40
40
  * A native ad drawn as a strip across the top of the screen, over the WebView, like a top banner.
41
41
  * Hiding keeps the loaded ad, so showing it again brings back the same ad without a new request.
42
+ * With a refresh interval, a new ad replaces it once it has been on screen that long in all, hidden time and time in
43
+ * the background not counted; the old one stays up until the new one comes.
42
44
  */
43
45
  public class NativeAdExecutor extends Executor {
44
46
 
@@ -55,6 +57,17 @@ public class NativeAdExecutor extends Executor {
55
57
  private long mLoadedAt;
56
58
  private boolean mLoading;
57
59
  private boolean mWantsVisible;
60
+ private boolean mPaused;
61
+ // What the next refresh asks for: the last show's ad unit and request.
62
+ private String mAdId;
63
+ private AdRequest mAdRequest;
64
+ private long mRefreshMs;
65
+ // How long the current ad has been on screen: the time before it was last hidden, and since when it shows.
66
+ private long mShownMs;
67
+ private long mShownSince = -1;
68
+ private final Runnable mRefresh = () -> {
69
+ if (mAdId != null) load(mAdId, mAdRequest);
70
+ };
58
71
  // The status bar can change height while the ad is up (rotation, display size), so its margin follows it.
59
72
  private final ViewTreeObserver.OnGlobalLayoutListener insetListener = () -> {
60
73
  if (mAdView != null) applyTopInset();
@@ -76,6 +89,7 @@ public class NativeAdExecutor extends Executor {
76
89
  public void showNativeAd(final PluginCall call) {
77
90
  final AdOptions adOptions = AdOptions.getFactory().createGenericOptions(call, NATIVE_TESTER_ID);
78
91
  final JSObject style = call.getObject("style", new JSObject());
92
+ final long refreshMs = Math.max(0, call.getInt("refreshInterval", 0)) * 1000L;
79
93
 
80
94
  activitySupplier
81
95
  .get()
@@ -89,8 +103,13 @@ public class NativeAdExecutor extends Executor {
89
103
  final AdRequest adRequest = RequestHelper.createRequest(adOptions);
90
104
  final String adId = AdViewIdHelper.getFinalAdId(adOptions, adRequest, logTag, contextSupplier.get());
91
105
 
106
+ mAdId = adId;
107
+ mAdRequest = adRequest;
108
+ mRefreshMs = refreshMs;
109
+
92
110
  if (mNativeAd != null && adId.equals(mLoadedAdId) && !isExpired()) {
93
111
  setVisible(true);
112
+ startCounting();
94
113
  } else {
95
114
  load(adId, adRequest);
96
115
  }
@@ -107,6 +126,7 @@ public class NativeAdExecutor extends Executor {
107
126
  .get()
108
127
  .runOnUiThread(() -> {
109
128
  mWantsVisible = false;
129
+ stopCounting();
110
130
  if (mAdView != null) {
111
131
  setVisible(false);
112
132
  }
@@ -119,6 +139,7 @@ public class NativeAdExecutor extends Executor {
119
139
  .get()
120
140
  .runOnUiThread(() -> {
121
141
  mWantsVisible = false;
142
+ stopCounting();
122
143
  destroyNativeAd();
123
144
 
124
145
  if (mAdView != null) {
@@ -151,15 +172,18 @@ public class NativeAdExecutor extends Executor {
151
172
  return;
152
173
  }
153
174
 
175
+ stopCounting();
154
176
  destroyNativeAd();
155
177
  mNativeAd = nativeAd;
156
178
  mLoadedAdId = adId;
157
179
  mLoadedAt = SystemClock.elapsedRealtime();
180
+ mShownMs = 0;
158
181
  populate(nativeAd);
159
182
  notifyListeners(NativeAdPluginEvents.Loaded.getWebEventName(), emptyObject);
160
183
 
161
184
  if (mWantsVisible) {
162
185
  setVisible(true);
186
+ startCounting();
163
187
  }
164
188
  })
165
189
  .withAdListener(
@@ -167,14 +191,18 @@ public class NativeAdExecutor extends Executor {
167
191
  @Override
168
192
  public void onAdFailedToLoad(@NonNull LoadAdError adError) {
169
193
  mLoading = false;
194
+ stopCounting();
170
195
 
171
- // An expired ad may not be shown again; a fresh one keeps showing.
196
+ // An expired ad may not be shown again; a fresh one keeps showing, and the next refresh is
197
+ // tried after another interval on screen.
172
198
  if (mNativeAd != null && isExpired()) {
173
199
  destroyNativeAd();
174
200
  }
175
201
  if (mNativeAd == null && mAdView != null) {
176
202
  setVisible(false);
177
203
  }
204
+ mShownMs = 0;
205
+ startCounting();
178
206
 
179
207
  notifyListeners(NativeAdPluginEvents.FailedToLoad.getWebEventName(), new AdMobPluginError(adError));
180
208
  }
@@ -195,6 +223,33 @@ public class NativeAdExecutor extends Executor {
195
223
  adLoader.loadAd(adRequest);
196
224
  }
197
225
 
226
+ // The app went to the background, or came back: the ad is not on screen meanwhile.
227
+ public void onPause() {
228
+ mPaused = true;
229
+ stopCounting();
230
+ }
231
+
232
+ public void onResume() {
233
+ mPaused = false;
234
+ startCounting();
235
+ }
236
+
237
+ // Counts the time the ad is on screen and asks for a new one when it has been there for the refresh interval.
238
+ private void startCounting() {
239
+ if (mShownSince >= 0 || mRefreshMs <= 0 || mNativeAd == null || mAdView == null || !mWantsVisible || mPaused) return;
240
+
241
+ mShownSince = SystemClock.elapsedRealtime();
242
+ mAdView.postDelayed(mRefresh, Math.max(0, mRefreshMs - mShownMs));
243
+ }
244
+
245
+ private void stopCounting() {
246
+ if (mAdView != null) mAdView.removeCallbacks(mRefresh);
247
+ if (mShownSince < 0) return;
248
+
249
+ mShownMs += SystemClock.elapsedRealtime() - mShownSince;
250
+ mShownSince = -1;
251
+ }
252
+
198
253
  private void ensureAdView() {
199
254
  if (mAdView != null) return;
200
255
 
package/dist/docs.json CHANGED
@@ -2053,6 +2053,18 @@
2053
2053
  ],
2054
2054
  "type": "NativeAdStyle"
2055
2055
  },
2056
+ {
2057
+ "name": "refreshInterval",
2058
+ "tags": [
2059
+ {
2060
+ "text": "8.2.0",
2061
+ "name": "since"
2062
+ }
2063
+ ],
2064
+ "docs": "Seconds the ad stays on screen before a new one replaces it. Only the time it is shown counts: hidden, or with\nthe app in the background, it waits. The old ad stays up until the new one loads. 0, the default, keeps it until\nit expires after an hour.",
2065
+ "complexTypes": [],
2066
+ "type": "number | undefined"
2067
+ },
2056
2068
  {
2057
2069
  "name": "adId",
2058
2070
  "tags": [
@@ -6,6 +6,14 @@ export interface NativeAdOptions extends AdOptions {
6
6
  * @since 8.1.0
7
7
  */
8
8
  style?: NativeAdStyle;
9
+ /**
10
+ * Seconds the ad stays on screen before a new one replaces it. Only the time it is shown counts: hidden, or with
11
+ * the app in the background, it waits. The old ad stays up until the new one loads. 0, the default, keeps it until
12
+ * it expires after an hour.
13
+ *
14
+ * @since 8.2.0
15
+ */
16
+ refreshInterval?: number;
9
17
  }
10
18
  export interface NativeAdStyle {
11
19
  /**
@@ -5,6 +5,8 @@ import GoogleMobileAds
5
5
  /**
6
6
  * A native ad drawn as a strip across the top of the screen, below the status bar, over the WebView like a top banner.
7
7
  * Hiding keeps the loaded ad, so showing it again brings back the same ad without a new request.
8
+ * With a refresh interval, a new ad replaces it once it has been on screen that long in all, hidden time and time in
9
+ * the background not counted; the old one stays up until the new one comes.
8
10
  */
9
11
  class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
10
12
  weak var plugin: AdMobPlugin?
@@ -18,6 +20,16 @@ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
18
20
  private var loadedAdUnitID: String?
19
21
  private var loadedAt = Date.distantPast
20
22
  private var wantsVisible = false
23
+ private var inBackground = false
24
+ // What the next refresh asks for: the last show's ad unit and request.
25
+ private var adUnitID: String?
26
+ private var request: Request?
27
+ private var refreshInterval: TimeInterval = 0
28
+ // How long the current ad has been on screen: the time before it was last hidden, and since when it shows.
29
+ private var shownTime: TimeInterval = 0
30
+ private var shownSince: Date?
31
+ private var refreshTimer: Timer?
32
+ private var observers: [NSObjectProtocol] = []
21
33
 
22
34
  private let iconView = UIImageView()
23
35
  private let badgeLabel = PaddedLabel()
@@ -35,10 +47,15 @@ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
35
47
 
36
48
  ensureAdView(in: rootViewController.view)
37
49
  applyStyle(call.getObject("style") ?? [:])
50
+ observeApp()
38
51
  wantsVisible = true
52
+ self.adUnitID = adUnitID
53
+ self.request = request
54
+ refreshInterval = TimeInterval(max(0, call.getInt("refreshInterval") ?? 0))
39
55
 
40
56
  if nativeAd != nil && loadedAdUnitID == adUnitID && !isExpired() {
41
57
  setVisible(true)
58
+ startCounting()
42
59
  } else {
43
60
  load(adUnitID, request, rootViewController)
44
61
  }
@@ -48,6 +65,7 @@ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
48
65
 
49
66
  func hideNativeAd(_ call: CAPPluginCall) {
50
67
  wantsVisible = false
68
+ stopCounting()
51
69
  if adView != nil {
52
70
  setVisible(false)
53
71
  }
@@ -56,6 +74,7 @@ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
56
74
 
57
75
  func removeNativeAd(_ call: CAPPluginCall) {
58
76
  wantsVisible = false
77
+ stopCounting()
59
78
  nativeAd = nil
60
79
  loadedAdUnitID = nil
61
80
 
@@ -87,23 +106,28 @@ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
87
106
  // Removed while it loaded.
88
107
  guard let adView = adView else { return }
89
108
 
109
+ stopCounting()
90
110
  nativeAd.delegate = self
91
111
  self.nativeAd = nativeAd
92
112
  loadedAdUnitID = adLoader.adUnitID
93
113
  loadedAt = Date()
114
+ shownTime = 0
94
115
  populate(adView, nativeAd)
95
116
  plugin?.notifyListeners(NativeAdPluginEvents.Loaded.rawValue, data: [:])
96
117
 
97
118
  if wantsVisible {
98
119
  setVisible(true)
120
+ startCounting()
99
121
  }
100
122
  }
101
123
 
102
124
  func adLoader(_ adLoader: AdLoader, didFailToReceiveAdWithError error: Error) {
103
125
  self.adLoader = nil
104
126
  NSLog("AdMob: native ad failed to load: \(error.localizedDescription)")
127
+ stopCounting()
105
128
 
106
- // An expired ad may not be shown again; a fresh one keeps showing.
129
+ // An expired ad may not be shown again; a fresh one keeps showing, and the next refresh is tried after another
130
+ // interval on screen.
107
131
  if nativeAd != nil && isExpired() {
108
132
  nativeAd = nil
109
133
  loadedAdUnitID = nil
@@ -111,6 +135,8 @@ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
111
135
  if nativeAd == nil && adView != nil {
112
136
  setVisible(false)
113
137
  }
138
+ shownTime = 0
139
+ startCounting()
114
140
 
115
141
  plugin?.notifyListeners(NativeAdPluginEvents.FailedToLoad.rawValue, data: [
116
142
  "code": (error as NSError).code,
@@ -126,6 +152,45 @@ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
126
152
  plugin?.notifyListeners(NativeAdPluginEvents.AdImpression.rawValue, data: [:])
127
153
  }
128
154
 
155
+ // Counts the time the ad is on screen and asks for a new one when it has been there for the refresh interval.
156
+ private func startCounting() {
157
+ if shownSince != nil || refreshInterval <= 0 || nativeAd == nil || !wantsVisible || inBackground { return }
158
+
159
+ shownSince = Date()
160
+ refreshTimer = Timer.scheduledTimer(withTimeInterval: max(0, refreshInterval - shownTime), repeats: false) { [weak self] _ in
161
+ guard let self = self, let adUnitID = self.adUnitID, let request = self.request,
162
+ let rootViewController = self.plugin?.getRootVC() else { return }
163
+
164
+ self.load(adUnitID, request, rootViewController)
165
+ }
166
+ }
167
+
168
+ private func stopCounting() {
169
+ refreshTimer?.invalidate()
170
+ refreshTimer = nil
171
+ guard let shownSince = shownSince else { return }
172
+
173
+ shownTime += Date().timeIntervalSince(shownSince)
174
+ self.shownSince = nil
175
+ }
176
+
177
+ // The app went to the background, or came back: the ad is not on screen meanwhile.
178
+ private func observeApp() {
179
+ if !observers.isEmpty { return }
180
+
181
+ let center = NotificationCenter.default
182
+ observers = [
183
+ center.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in
184
+ self?.inBackground = true
185
+ self?.stopCounting()
186
+ },
187
+ center.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
188
+ self?.inBackground = false
189
+ self?.startCounting()
190
+ }
191
+ ]
192
+ }
193
+
129
194
  // Icon, "Ad" badge, headline and body, the call to action and AdChoices in the top right corner; a hairline along
130
195
  // the bottom separates it from the screen below. The wider trailing padding keeps the button clear of AdChoices.
131
196
  private func ensureAdView(in parent: UIView) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ququplay/admob",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
4
4
  "description": "A native plugin for AdMob",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",