diff --git a/modules/ads/build.gradle b/modules/ads/build.gradle new file mode 100644 index 00000000..f3c75c8e --- /dev/null +++ b/modules/ads/build.gradle @@ -0,0 +1,6 @@ +dependencies { + implementation project(":util") +} + +ext.moduleName = 'com.gluonhq.attach.ads' +ext.description = 'Common API to access ad features' \ No newline at end of file diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/Ad.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/Ad.java new file mode 100644 index 00000000..efbd0bce --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/Ad.java @@ -0,0 +1,51 @@ +package com.gluonhq.attach.ads; + +/** + * The base class for all ad types. + * + * @param the type of service the ad uses + */ +public abstract class Ad { + + /** + * The unique id of the ad. + */ + protected final long id; + + /** + * The service the ad uses. + */ + protected final T service; + + /** + * Constructs an ad. + * + * @param id the unique id of the ad + * @param service the service the ad uses + */ + public Ad(long id, T service) { + this.id = id; + this.service = service; + } + + /** + * Disposes the ad. + */ + public void dispose() { + service.dispose(this); + } + + /** + * Returns the unique id of the ad. + * + * @return the unique id of the ad + */ + public long getId() { + return id; + } + + public interface Service { + + void dispose(Ad ad); + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/AdListener.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdListener.java new file mode 100644 index 00000000..8f98b643 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdListener.java @@ -0,0 +1,88 @@ +package com.gluonhq.attach.ads; + +/** + * A listener for receiving notifications during the lifecycle of an ad. + */ +public abstract class AdListener implements Callback { + + /** + * Called when a click is recorded for an ad. + */ + public void onAdClicked() { + // empty + } + + /** + * Called when the user is about to return to the application after clicking on an ad. + */ + public void onAdClosed() { + // empty + } + + /** + * Called when an ad request failed. + */ + public void onAdFailedToLoad() { + // empty + } + + /** + * Called when an impression is recorded for an ad. + */ + public void onAdImpression() { + // empty + } + + /** + * Called when an ad is received. + */ + public void onAdLoaded() { + // empty + } + + /** + * Called when an ad opens an overlay that covers the screen. + */ + public void onAdOpened() { + // empty + } + + /** + * Called when a swipe gesture on an ad is recorded as a click. + */ + public void onAdSwipeGestureClicked() { + // empty + } + + static class Adapter implements CallbackAdapter { + + @Override + public void invoke(Ad ad, Callback callback, String method, String[] params) { + AdListener c = (AdListener) callback; + + switch (method) { + case "onAdClicked": + c.onAdClicked(); + break; + case "onAdClosed": + c.onAdClosed(); + break; + case "onAdFailedToLoad": + c.onAdFailedToLoad(); + break; + case "onAdImpression": + c.onAdImpression(); + break; + case "onAdLoaded": + c.onAdLoaded(); + break; + case "onAdOpened": + c.onAdOpened(); + break; + case "onAdSwipeGestureClicked": + c.onAdSwipeGestureClicked(); + break; + } + } + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/AdLoadCallback.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdLoadCallback.java new file mode 100644 index 00000000..3815d86d --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdLoadCallback.java @@ -0,0 +1,43 @@ +package com.gluonhq.attach.ads; + +/** + * Callback to be invoked when an ad finishes loading. + * + * @param the type of ad the callback uses + */ +public abstract class AdLoadCallback implements Callback { + + /** + * Called when an ad fails to load. + */ + public void onAdFailedToLoad() { + // empty + } + + /** + * Called when an ad successfully loads. + * + * @param ad the loaded ad + */ + public void onAdLoaded(T ad) { + // empty + } + + static class Adapter implements CallbackAdapter { + + @Override + @SuppressWarnings("unchecked") + public void invoke(Ad ad, Callback callback, String method, String[] params) { + AdLoadCallback c = (AdLoadCallback) callback; + + switch (method) { + case "onAdFailedToLoad": + c.onAdFailedToLoad(); + break; + case "onAdLoaded": + c.onAdLoaded((T) ad); + break; + } + } + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/AdRegistry.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdRegistry.java new file mode 100644 index 00000000..a81f9dcb --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdRegistry.java @@ -0,0 +1,68 @@ +package com.gluonhq.attach.ads; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AdRegistry { + + private long id; + + private final Map handlers; + + private final Map> ads; + + private final Map> callbacks; + + public AdRegistry() { + handlers = new HashMap<>(); + ads = new HashMap<>(); + callbacks = new HashMap<>(); + + handlers.put(AdListener.class.getSimpleName(), new AdListener.Adapter()); + handlers.put(InterstitialAdLoadCallback.class.getSimpleName(), new AdLoadCallback.Adapter()); + handlers.put(RewardedAdLoadCallback.class.getSimpleName(), new AdLoadCallback.Adapter()); + handlers.put(OnUserEarnedRewardListener.class.getSimpleName(), new OnUserEarnedRewardListener.Adapter()); + handlers.put(FullScreenContentCallback.class.getSimpleName(), new FullScreenContentCallback.Adapter()); + } + + public void addAd(Ad ad) { + ads.put(ad.getId(), ad); + callbacks.put(ad.getId(), new HashMap<>()); + } + + public void removeAd(long id) { + ads.remove(id); + callbacks.remove(id); + } + + @SuppressWarnings("unchecked") + public > T getAd(long id) { + return (T) ads.get(id); + } + + public void setCallback(long id, Class callbackClass, T callback) { + Map values = callbacks.get(id); + String name = callbackClass.getSimpleName(); + + values.remove(name); + + if (callback != null) { + values.put(name, callback); + } + } + + public void invokeCallback(long id, String callback, String method, String[] params) { + CallbackAdapter handler = handlers.get(callback); + Callback value = callbacks.get(id).get(callback); + + if (value != null) { + handler.invoke(ads.get(id), value, method, params); + } + } + + public long nextId() { + return id++; + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/AdRequest.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdRequest.java new file mode 100644 index 00000000..82a83212 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdRequest.java @@ -0,0 +1,41 @@ +package com.gluonhq.attach.ads; + +/** + * An AdRequest contains targeting information used to fetch an ad. Ad requests are created using AdRequest.Builder. + */ +public class AdRequest { + + /** + * Constructs an AdRequest. + */ + private AdRequest() { + // empty + } + + /** + * Builds an AdRequest. + */ + public static class Builder { + + /** + * The AdRequest for this builder. + */ + private final AdRequest request; + + /** + * Constructs a Builder. + */ + public Builder() { + request = new AdRequest(); + } + + /** + * Constructs an AdRequest with the specified attributes. + * + * @return the constructed ad request + */ + public AdRequest build() { + return request; + } + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/AdsService.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdsService.java new file mode 100644 index 00000000..4c706d97 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/AdsService.java @@ -0,0 +1,80 @@ +package com.gluonhq.attach.ads; + +import com.gluonhq.attach.util.Services; + +import java.util.Optional; + +/** + * The ads service provides the ability to show ads with the Google Mobile Ads SDK on Android and iOS. + * + *

Example

+ *
+ * {@code AdsService.create().ifPresent(service -> {
+ *      service.initialize(() -> {
+ *          BannerAd ad = service.newBannerAd();
+ *
+ *          ad.setAdUnitId(BannerAd.TEST_AD_UNIT_ID);
+ *          ad.setAdLayout(BannerAd.Layout.BOTTOM);
+ *          ad.setAdSize(BannerAd.Size.BANNER);
+ *          ad.load(new AdRequest.Builder().build());
+ *          ad.show();
+ *      });
+ *  });}
+ * + *

Android Configuration: none

+ *

iOS Configuration: none

+ * + * @since 4.0.24 + */ +public interface AdsService { + + /** + * Creates an AdsService. + * + * @return the created ad service + */ + static Optional create() { + return Services.get(AdsService.class); + } + + /** + * Initializes the Google Mobile Ads SDK. Call this method as early as possible after the app launches to reduce + * latency on the session's first ad request. If this method is not called, the first ad request automatically + * initializes the Google Mobile Ads SDK. + * + * @param listener a callback to be invoked upon initialization completion + */ + void initialize(OnInitializationCompleteListener listener); + + /** + * Constructs a new BannerAd. + * + * @return the constructed banner ad + */ + BannerAd newBannerAd(); + + /** + * Loads an InterstitialAd. + * + * @param adUnitId the ad unit ID + * @param adRequest an ad request with targeting information + * @param callback a callback to be invoked when an interstitial ad finishes loading + */ + void loadInterstitialAd(String adUnitId, AdRequest adRequest, InterstitialAdLoadCallback callback); + + /** + * Loads a RewardedAd. + * + * @param adUnitId the ad unit ID + * @param adRequest an ad request with targeting information + * @param callback a callback to be invoked when a rewarded ad finishes loading + */ + void loadRewardedAd(String adUnitId, AdRequest adRequest, RewardedAdLoadCallback callback); + + /** + * Sets the global RequestConfiguration that will be used for every AdRequest during the app's session. + * + * @param requestConfiguration the request configuration + */ + void setRequestConfiguration(RequestConfiguration requestConfiguration); +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/BannerAd.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/BannerAd.java new file mode 100644 index 00000000..d0a82b8a --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/BannerAd.java @@ -0,0 +1,158 @@ +package com.gluonhq.attach.ads; + +/** + * An ad used to display banner ads. The ad size and ad unit ID must be set prior to calling loadAd. + */ +public class BannerAd extends Ad { + + /** + * The ad unit ID used for testing. + */ + public static String TEST_AD_UNIT_ID = "ca-app-pub-3940256099942544/6300978111"; + + /** + * {@inheritDoc} + */ + public BannerAd(long id, Service service) { + super(id, service); + } + + /** + * Loads an ad. + * + * @param adRequest the ad request + */ + public void load(AdRequest adRequest) { + service.load(this, adRequest); + } + + /** + * Shows the ad. + */ + public void show() { + service.show(this); + } + + /** + * Hides the ad. + */ + public void hide() { + service.hide(this); + } + + /** + * Sets the layout of the ad. + * + * @param layout the layout + */ + public void setAdLayout(Layout layout) { + service.setAdLayout(this, layout); + } + + /** + * Sets the size of the ad. + * + * @param size the size of the ad + */ + public void setAdSize(Size size) { + service.setAdSize(this, size); + } + + /** + * Sets the ad unit ID. + * + * @param adUnitId the ad unit ID + */ + public void setAdUnitId(String adUnitId) { + service.setAdUnitId(this, adUnitId); + } + + /** + * Sets an AdListener for this ad view. + * + * @param listener the listener + */ + public void setAdListener(AdListener listener) { + service.setAdListener(this, listener); + } + + /** + * The layout of a banner ad. + */ + public enum Layout { + + /** + * Positions the banner ad at the top center. + */ + TOP, + + /** + * Positions the banner ad at the bottom center. + */ + BOTTOM, + } + + /** + * The size of a banner ad. + */ + public enum Size { + + /** + * Mobile Marketing Association (MMA) banner ad size (320x50 density-independent pixels). + */ + BANNER, + + /** + * Interactive Advertising Bureau (IAB) full banner ad size (468x60 density-independent pixels). + */ + FULL_BANNER, + + /** + * Large banner ad size (320x100 density-independent pixels). + */ + LARGE_BANNER, + + /** + * Interactive Advertising Bureau (IAB) leaderboard ad size (728x90 density-independent pixels). + */ + LEADERBOARD, + + /** + * Interactive Advertising Bureau (IAB) medium rectangle ad size (300x250 density-independent pixels). + */ + MEDIUM_RECTANGLE, + + /** + * IAB wide skyscraper ad size (160x600 density-independent pixels). + */ + WIDE_SKYSCRAPER, + + /** + * A dynamically sized banner that matches its parent's width and expands/contracts its height to match the ad's + * content after loading completes. + */ + FLUID, + + /** + * An invalid AdSize that will cause the ad request to fail immediately. + */ + INVALID, + } + + public interface Service extends Ad.Service { + + void load(BannerAd ad, AdRequest adRequest); + + void show(BannerAd ad); + + void hide(BannerAd ad); + + void setAdLayout(BannerAd ad, Layout layout); + + void setAdSize(BannerAd ad, Size size); + + void setAdUnitId(BannerAd ad, String adUnitId); + + void setAdListener(BannerAd ad, AdListener listener); + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/Callback.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/Callback.java new file mode 100644 index 00000000..ec6924eb --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/Callback.java @@ -0,0 +1,8 @@ +package com.gluonhq.attach.ads; + +/** + * Base class for ad callbacks and listeners. + */ +public interface Callback { + // empty +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/CallbackAdapter.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/CallbackAdapter.java new file mode 100644 index 00000000..00aae942 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/CallbackAdapter.java @@ -0,0 +1,6 @@ +package com.gluonhq.attach.ads; + +public interface CallbackAdapter { + + void invoke(Ad ad, Callback callback, String method, String[] params); +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/FullScreenContentCallback.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/FullScreenContentCallback.java new file mode 100644 index 00000000..e21a021e --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/FullScreenContentCallback.java @@ -0,0 +1,69 @@ +package com.gluonhq.attach.ads; + +/** + * Callback to be invoked when ads show and dismiss full screen content, such as a fullscreen ad experience or an in-app + * browser. + */ +public abstract class FullScreenContentCallback implements Callback { + + /** + * Called when a click is recorded for an ad. + */ + public void onAdClicked() { + // empty + } + + /** + * Called when the ad dismissed full screen content. + */ + public void onAdDismissedFullScreenContent() { + // empty + } + + /** + * Called when the ad failed to show full screen content. + */ + public void onAdFailedToShowFullScreenContent() { + // empty + } + + /** + * Called when an impression is recorded for an ad. + */ + public void onAdImpression() { + // empty + } + + /** + * Called when the ad showed the full screen content. + */ + public void onAdShowedFullScreenContent() { + // empty + } + + static class Adapter implements CallbackAdapter { + + @Override + public void invoke(Ad ad, Callback callback, String method, String[] params) { + FullScreenContentCallback c = (FullScreenContentCallback) callback; + + switch (method) { + case "onAdClicked": + c.onAdClicked(); + break; + case "onAdDismissedFullScreenContent": + c.onAdDismissedFullScreenContent(); + break; + case "onAdFailedToShowFullScreenContent": + c.onAdFailedToShowFullScreenContent(); + break; + case "onAdImpression": + c.onAdImpression(); + break; + case "onAdShowedFullScreenContent": + c.onAdShowedFullScreenContent(); + break; + } + } + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/InterstitialAd.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/InterstitialAd.java new file mode 100644 index 00000000..7d75bbc0 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/InterstitialAd.java @@ -0,0 +1,48 @@ +package com.gluonhq.attach.ads; + +/** + * A full page ad experience at natural transition points such as a page change, an app launch, or a game level load. + * Interstitials use a close button that removes the ad from the user's experience. + */ +public class InterstitialAd extends Ad { + + /** + * The ad unit ID used for testing. + */ + public static String TEST_AD_UNIT_ID = "ca-app-pub-3940256099942544/1033173712"; + + /** + * The ad unit ID used for video testing. + */ + public static String VIDEO_TEST_AD_UNIT_ID = "ca-app-pub-3940256099942544/8691691433"; + + /** + * {@inheritDoc} + */ + public InterstitialAd(long id, Service service) { + super(id, service); + } + + /** + * Shows the ad. + */ + public void show() { + service.show(this); + } + + /** + * Registers a callback to be invoked when ads show and dismiss full screen content. + * + * @param callback the callback + */ + public void setFullScreenContentCallback(FullScreenContentCallback callback) { + service.setFullScreenContentCallback(this, callback); + } + + public interface Service extends Ad.Service { + + void show(InterstitialAd ad); + + void setFullScreenContentCallback(InterstitialAd ad, FullScreenContentCallback callback); + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/InterstitialAdLoadCallback.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/InterstitialAdLoadCallback.java new file mode 100644 index 00000000..ed44d8ae --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/InterstitialAdLoadCallback.java @@ -0,0 +1,8 @@ +package com.gluonhq.attach.ads; + +/** + * {@inheritDoc} + */ +public abstract class InterstitialAdLoadCallback extends AdLoadCallback { + // empty +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/OnInitializationCompleteListener.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/OnInitializationCompleteListener.java new file mode 100644 index 00000000..540a56f1 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/OnInitializationCompleteListener.java @@ -0,0 +1,12 @@ +package com.gluonhq.attach.ads; + +/** + * Listener that returns the status of SDK initialization upon initialization completion. + */ +public interface OnInitializationCompleteListener { + + /** + * Called when the SDK initialization is complete. + */ + void onInitializationComplete(); +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/OnUserEarnedRewardListener.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/OnUserEarnedRewardListener.java new file mode 100644 index 00000000..5fe25a07 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/OnUserEarnedRewardListener.java @@ -0,0 +1,27 @@ +package com.gluonhq.attach.ads; + +/** + * Interface definition for a callback to be invoked when the user earned a reward. + */ +public interface OnUserEarnedRewardListener extends Callback { + + /** + * Called when the user earned a reward. The app is responsible for crediting the user with the reward. + * + * @param type the type of the reward + * @param amount the amount of the reward + */ + void onUserEarnedReward(String type, int amount); + + class Adapter implements CallbackAdapter { + + @Override + public void invoke(Ad ad, Callback callback, String method, String[] params) { + OnUserEarnedRewardListener c = (OnUserEarnedRewardListener) callback; + + if (method.equals("onUserEarnedReward")) { + c.onUserEarnedReward(params[0], Integer.parseInt(params[1])); + } + } + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/RequestConfiguration.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/RequestConfiguration.java new file mode 100644 index 00000000..6f42d850 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/RequestConfiguration.java @@ -0,0 +1,211 @@ +package com.gluonhq.attach.ads; + +import java.util.ArrayList; +import java.util.List; + +/** + * Global configuration that will be used for every AdRequest. + */ +public class RequestConfiguration { + + /** + * Provides no indication whether ad requests should be treated as child-directed for purposes of the Children’s + * Online Privacy Protection Act (COPPA). + */ + public static final int TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED = -1; + + /** + * Indicates that ad requests should not be treated as child-directed for purposes of the Children’s Online Privacy + * Protection Act (COPPA). + */ + public static final int TAG_FOR_CHILD_DIRECTED_TREATMENT_FALSE = 0; + + /** + * Indicates that ad requests should be treated as child-directed for purposes of the Children’s Online Privacy + * Protection Act (COPPA). + */ + public static final int TAG_FOR_CHILD_DIRECTED_TREATMENT_TRUE = 1; + + /** + * Indicates that the publisher has not specified whether the ad request should receive treatment for users in the + * European Economic Area (EEA) under the age of consent. + */ + public static final int TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED = -1; + + /** + * Indicates the publisher specified that the ad request should not receive treatment for users in the European + * Economic Area (EEA) under the age of consent. + */ + public static final int TAG_FOR_UNDER_AGE_OF_CONSENT_FALSE = 0; + + /** + * Indicates the publisher specified that the ad request should receive treatment for users in the European Economic + * Area (EEA) under the age of consent. + */ + public static final int TAG_FOR_UNDER_AGE_OF_CONSENT_TRUE = 1; + + /** + * No specified content rating. + */ + public static final String MAX_AD_CONTENT_RATING_UNSPECIFIED = ""; + + /** + * Content suitable for general audiences, including families. + */ + public static final String MAX_AD_CONTENT_RATING_G = "G"; + + /** + * Content suitable for most audiences with parental guidance. + */ + public static final String MAX_AD_CONTENT_RATING_PG = "PG"; + + /** + * Content suitable for teen and older audiences. + */ + public static final String MAX_AD_CONTENT_RATING_T = "T"; + + /** + * Content suitable only for mature audiences. + */ + public static final String MAX_AD_CONTENT_RATING_MA = "MA"; + + /** + * The tag for child directed treatment. + */ + private int tagForChildDirectedTreatment; + + /** + * The tag for underage of consent. + */ + private int tagForUnderAgeOfConsent; + + /** + * The max ad content rating. + */ + private String maxAdContentRating; + + /** + * The test device ID's. + */ + private List testDeviceIds; + + /** + * Constructs a RequestConfiguration. + */ + private RequestConfiguration() { + tagForChildDirectedTreatment = TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED; + tagForUnderAgeOfConsent = TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED; + maxAdContentRating = MAX_AD_CONTENT_RATING_UNSPECIFIED; + testDeviceIds = new ArrayList<>(); + } + + /** + * Returns the tag for child directed treatment. + * + * @return the tag for child directed treatment + */ + public int getTagForChildDirectedTreatment() { + return tagForChildDirectedTreatment; + } + + /** + * Returns the tag for underage of consent. + * + * @return the tag for underage of consent + */ + public int getTagForUnderAgeOfConsent() { + return tagForUnderAgeOfConsent; + } + + /** + * Returns the max ad content rating. + * + * @return the max ad content rating + */ + public String getMaxAdContentRating() { + return maxAdContentRating; + } + + /** + * Returns the test device ID's. + * + * @return the test device ID's + */ + public List getTestDeviceIds() { + return testDeviceIds; + } + + /** + * Builder for RequestConfiguration. + */ + public static class Builder { + + /** + * The request configuration. + */ + private final RequestConfiguration config; + + /** + * Constructs a Builder. + */ + public Builder() { + config = new RequestConfiguration(); + } + + /** + * Builds the RequestConfiguration. + * + * @return the request configuration + */ + public RequestConfiguration build() { + return config; + } + + /** + * This method allows you to specify whether you would like your app to be treated as child-directed for + * purposes of the Children’s Online Privacy Protection Act (COPPA) - + * See here. + * + * @param tag the tag + * @return the builder + */ + public Builder setTagForChildDirectedTreatment(int tag) { + config.tagForChildDirectedTreatment = tag; + return this; + } + + /** + * This method allows you to mark your app to receive treatment for users in the European Economic Area (EEA) + * under the age of consent. + * + * @param tag the tag + * @return the builder + */ + public Builder setTagForUnderAgeOfConsent(int tag) { + config.tagForUnderAgeOfConsent = tag; + return this; + } + + /** + * Sets a maximum ad content rating. + * + * @param rating the rating + * @return the builder + */ + public Builder setMaxAdContentRating(String rating) { + config.maxAdContentRating = rating; + return this; + } + + /** + * Sets a list of test device IDs corresponding to test devices which will always request test ads. + * + * @param ids the ids + * @return the builder + */ + public Builder setTestDeviceIds(List ids) { + config.testDeviceIds = ids; + return this; + } + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/RewardedAd.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/RewardedAd.java new file mode 100644 index 00000000..0f5be40a --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/RewardedAd.java @@ -0,0 +1,44 @@ +package com.gluonhq.attach.ads; + +/** + * This class is used to request and display a rewarded ad. + */ +public class RewardedAd extends Ad { + + /** + * The ad unit ID used for testing. + */ + public static String TEST_AD_UNIT_ID = "ca-app-pub-3940256099942544/5224354917"; + + /** + * {@inheritDoc} + */ + public RewardedAd(long id, Service service) { + super(id, service); + } + + /** + * Shows the ad. + * + * @param listener the listener + */ + public void show(OnUserEarnedRewardListener listener) { + service.show(this, listener); + } + + /** + * Registers a callback to be invoked when ads show and dismiss full screen content. + * + * @param callback the callback + */ + public void setFullScreenContentCallback(FullScreenContentCallback callback) { + service.setFullScreenContentCallback(this, callback); + } + + public interface Service extends Ad.Service { + + void show(RewardedAd ad, OnUserEarnedRewardListener listener); + + void setFullScreenContentCallback(RewardedAd ad, FullScreenContentCallback callback); + } +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/RewardedAdLoadCallback.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/RewardedAdLoadCallback.java new file mode 100644 index 00000000..14abe8b7 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/RewardedAdLoadCallback.java @@ -0,0 +1,8 @@ +package com.gluonhq.attach.ads; + +/** + * {@inheritDoc} + */ +public abstract class RewardedAdLoadCallback extends AdLoadCallback { + // empty +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/AndroidAdsService.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/AndroidAdsService.java new file mode 100644 index 00000000..ba60c89f --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/AndroidAdsService.java @@ -0,0 +1,50 @@ +package com.gluonhq.attach.ads.impl; + +public class AndroidAdsService extends DefaultAdsService { + + static { + System.loadLibrary("ads"); + } + + @Override + protected native void nativeInitialize(); + + @Override + protected native void nativeSetRequestConfiguration(int tagForChildDirectedTreatment, int tagForUnderAgeOfConsent, String maxAdContentRating, String[] testDeviceIds); + + @Override + protected native void nativeRemoveAd(long id); + + @Override + protected native void nativeBannerAdNew(long id); + + @Override + protected native void nativeBannerAdLoad(long id); + + @Override + protected native void nativeBannerAdShow(long id); + + @Override + protected native void nativeBannerAdHide(long id); + + @Override + protected native void nativeBannerAdSetAdLayout(long id, String layout); + + @Override + protected native void nativeBannerAdSetAdSize(long id, String size); + + @Override + protected native void nativeBannerAdSetAdUnitId(long id, String adUnitId); + + @Override + protected native void nativeInterstitialAdLoad(long id, String adUnitId); + + @Override + protected native void nativeInterstitialAdShow(long id); + + @Override + protected native void nativeRewardedAdLoad(long id, String adUnitId); + + @Override + protected native void nativeRewardedAdShow(long id); +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/DefaultAdsService.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/DefaultAdsService.java new file mode 100644 index 00000000..165b05d7 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/DefaultAdsService.java @@ -0,0 +1,180 @@ +package com.gluonhq.attach.ads.impl; + +import com.gluonhq.attach.ads.Ad; +import com.gluonhq.attach.ads.AdListener; +import com.gluonhq.attach.ads.AdRegistry; +import com.gluonhq.attach.ads.AdRequest; +import com.gluonhq.attach.ads.AdsService; +import com.gluonhq.attach.ads.BannerAd; +import com.gluonhq.attach.ads.FullScreenContentCallback; +import com.gluonhq.attach.ads.InterstitialAd; +import com.gluonhq.attach.ads.InterstitialAdLoadCallback; +import com.gluonhq.attach.ads.OnInitializationCompleteListener; +import com.gluonhq.attach.ads.OnUserEarnedRewardListener; +import com.gluonhq.attach.ads.RequestConfiguration; +import com.gluonhq.attach.ads.RewardedAd; +import com.gluonhq.attach.ads.RewardedAdLoadCallback; + +public abstract class DefaultAdsService implements AdsService, BannerAd.Service, InterstitialAd.Service, RewardedAd.Service { + + private static DefaultAdsService instance; + + private final AdRegistry registry; + + private OnInitializationCompleteListener listener; + + public DefaultAdsService() { + if (instance != null) { + throw new IllegalStateException("Ads service is already initialized"); + } + + instance = this; + registry = new AdRegistry(); + listener = null; + } + + @Override + public void initialize(OnInitializationCompleteListener listener) { + this.listener = listener; + nativeInitialize(); + } + + @Override + public void dispose(Ad ad) { + nativeRemoveAd(ad.getId()); + registry.removeAd(ad.getId()); + } + + @Override + public BannerAd newBannerAd() { + BannerAd ad = new BannerAd(registry.nextId(), this); + + registry.addAd(ad); + nativeBannerAdNew(ad.getId()); + + return ad; + } + + @Override + public void loadInterstitialAd(String adUnitId, AdRequest adRequest, InterstitialAdLoadCallback callback) { + InterstitialAd ad = new InterstitialAd(registry.nextId(), this); + + registry.addAd(ad); + registry.setCallback(ad.getId(), InterstitialAdLoadCallback.class, callback); + + nativeInterstitialAdLoad(ad.getId(), adUnitId); + } + + @Override + public void loadRewardedAd(String adUnitId, AdRequest adRequest, RewardedAdLoadCallback callback) { + RewardedAd ad = new RewardedAd(registry.nextId(), this); + + registry.addAd(ad); + registry.setCallback(ad.getId(), RewardedAdLoadCallback.class, callback); + + nativeRewardedAdLoad(ad.getId(), adUnitId); + } + + @Override + public void setRequestConfiguration(RequestConfiguration requestConfiguration) { + nativeSetRequestConfiguration( + requestConfiguration.getTagForChildDirectedTreatment(), + requestConfiguration.getTagForUnderAgeOfConsent(), + requestConfiguration.getMaxAdContentRating(), + requestConfiguration.getTestDeviceIds().toArray(String[]::new)); + } + + @Override + public void load(BannerAd ad, AdRequest adRequest) { + nativeBannerAdLoad(ad.getId()); + } + + @Override + public void show(BannerAd ad) { + nativeBannerAdShow(ad.getId()); + } + + @Override + public void hide(BannerAd ad) { + nativeBannerAdHide(ad.getId()); + } + + @Override + public void setAdLayout(BannerAd ad, BannerAd.Layout layout) { + nativeBannerAdSetAdLayout(ad.getId(), layout.toString()); + } + + @Override + public void setAdSize(BannerAd ad, BannerAd.Size size) { + nativeBannerAdSetAdSize(ad.getId(), size.toString()); + } + + @Override + public void setAdUnitId(BannerAd ad, String adUnitId) { + nativeBannerAdSetAdUnitId(ad.getId(), adUnitId); + } + + @Override + public void setAdListener(BannerAd ad, AdListener listener) { + registry.setCallback(ad.getId(), AdListener.class, listener); + } + + @Override + public void show(InterstitialAd ad) { + nativeInterstitialAdShow(ad.getId()); + } + + @Override + public void setFullScreenContentCallback(InterstitialAd ad, FullScreenContentCallback callback) { + registry.setCallback(ad.getId(), FullScreenContentCallback.class, callback); + } + + @Override + public void show(RewardedAd ad, OnUserEarnedRewardListener listener) { + registry.setCallback(ad.getId(), OnUserEarnedRewardListener.class, listener); + nativeRewardedAdShow(ad.getId()); + } + + @Override + public void setFullScreenContentCallback(RewardedAd ad, FullScreenContentCallback callback) { + registry.setCallback(ad.getId(), FullScreenContentCallback.class, callback); + } + + private static void invokeCallback(long id, String callback, String method, String[] params) { + if (id == -1) { + if (instance.listener != null) { + instance.listener.onInitializationComplete(); + } + } else { + instance.registry.invokeCallback(id, callback, method, params); + } + } + + protected abstract void nativeInitialize(); + + protected abstract void nativeSetRequestConfiguration(int tagForChildDirectedTreatment, int tagForUnderAgeOfConsent, String maxAdContentRating, String[] testDeviceIds); + + protected abstract void nativeRemoveAd(long id); + + protected abstract void nativeBannerAdNew(long id); + + protected abstract void nativeBannerAdLoad(long id); + + protected abstract void nativeBannerAdShow(long id); + + protected abstract void nativeBannerAdHide(long id); + + protected abstract void nativeBannerAdSetAdLayout(long id, String layout); + + protected abstract void nativeBannerAdSetAdSize(long id, String size); + + protected abstract void nativeBannerAdSetAdUnitId(long id, String adUnitId); + + protected abstract void nativeInterstitialAdLoad(long id, String adUnitId); + + protected abstract void nativeInterstitialAdShow(long id); + + protected abstract void nativeRewardedAdLoad(long id, String adUnitId); + + protected abstract void nativeRewardedAdShow(long id); +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/DummyAdsService.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/DummyAdsService.java new file mode 100644 index 00000000..0465a152 --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/DummyAdsService.java @@ -0,0 +1,7 @@ +package com.gluonhq.attach.ads.impl; + +import com.gluonhq.attach.ads.AdsService; + +public abstract class DummyAdsService implements AdsService { + // empty +} diff --git a/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/IOSAdsService.java b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/IOSAdsService.java new file mode 100644 index 00000000..66bbaecb --- /dev/null +++ b/modules/ads/src/main/java/com/gluonhq/attach/ads/impl/IOSAdsService.java @@ -0,0 +1,53 @@ +package com.gluonhq.attach.ads.impl; + +public class IOSAdsService extends DefaultAdsService { + + static { + System.loadLibrary("Ads"); + // initAds(); + } + + private static native void initAds(); + + @Override + protected native void nativeInitialize(); + + @Override + protected native void nativeSetRequestConfiguration(int tagForChildDirectedTreatment, int tagForUnderAgeOfConsent, String maxAdContentRating, String[] testDeviceIds); + + @Override + protected native void nativeRemoveAd(long id); + + @Override + protected native void nativeBannerAdNew(long id); + + @Override + protected native void nativeBannerAdLoad(long id); + + @Override + protected native void nativeBannerAdShow(long id); + + @Override + protected native void nativeBannerAdHide(long id); + + @Override + protected native void nativeBannerAdSetAdLayout(long id, String layout); + + @Override + protected native void nativeBannerAdSetAdSize(long id, String size); + + @Override + protected native void nativeBannerAdSetAdUnitId(long id, String adUnitId); + + @Override + protected native void nativeInterstitialAdLoad(long id, String adUnitId); + + @Override + protected native void nativeInterstitialAdShow(long id); + + @Override + protected native void nativeRewardedAdLoad(long id, String adUnitId); + + @Override + protected native void nativeRewardedAdShow(long id); +} diff --git a/modules/ads/src/main/java/module-info.java b/modules/ads/src/main/java/module-info.java new file mode 100644 index 00000000..89a9ca94 --- /dev/null +++ b/modules/ads/src/main/java/module-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Gluon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +module com.gluonhq.attach.ads { + + requires com.gluonhq.attach.util; + + exports com.gluonhq.attach.ads; + exports com.gluonhq.attach.ads.impl to com.gluonhq.attach.util; +} \ No newline at end of file diff --git a/modules/ads/src/main/native/android/c/ads.c b/modules/ads/src/main/native/android/c/ads.c new file mode 100644 index 00000000..9c2843ab --- /dev/null +++ b/modules/ads/src/main/native/android/c/ads.c @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2025 Gluon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#include "util.h" + +static jclass jGraalAdsClass; +static jmethodID jGraalInvokeCallbackMethod; + +static jclass jAdsServiceClass; +static jobject jDalvikAdsService; +static jmethodID jAdsServiceInitialize; +static jmethodID jAdsServiceSetRequestConfiguration; +static jmethodID jAdsServiceRemoveAd; +static jmethodID jAdsServiceBannerAdNew; +static jmethodID jAdsServiceBannerAdLoad; +static jmethodID jAdsServiceBannerAdShow; +static jmethodID jAdsServiceBannerAdHide; +static jmethodID jAdsServiceBannerAdSetAdLayout; +static jmethodID jAdsServiceBannerAdSetAdSize; +static jmethodID jAdsServiceBannerAdSetAdUnitId; +static jmethodID jAdsServiceInterstitialAdLoad; +static jmethodID jAdsServiceInterstitialAdShow; +static jmethodID jAdsServiceRewardedAdLoad; +static jmethodID jAdsServiceRewardedAdShow; + +void initializeGraalHandles(JNIEnv *graalEnv) { + jGraalAdsClass = (*graalEnv)->NewGlobalRef(graalEnv, (*graalEnv)->FindClass(graalEnv, "com/gluonhq/attach/ads/impl/DefaultAdsService")); + jGraalInvokeCallbackMethod = (*graalEnv)->GetStaticMethodID(graalEnv, jGraalAdsClass, "invokeCallback", "(JLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;)V"); + + ATTACH_LOG_INFO("initializeGraalHandles %ld %ld", (long) jGraalAdsClass, (long) jGraalInvokeCallbackMethod); +} + +void initializeAdsDalvikHandles() { + jAdsServiceClass = GET_REGISTER_DALVIK_CLASS(jAdsServiceClass, "com/gluonhq/helloandroid/DalvikAdsService"); + ATTACH_DALVIK(); + jmethodID jAdsServiceInitMethod = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "", "(Landroid/app/Activity;)V"); + + jAdsServiceInitialize = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "initialize", "()V"); + jAdsServiceSetRequestConfiguration = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "setRequestConfiguration", "(IILjava/lang/String;[Ljava/lang/String;)V"); + jAdsServiceRemoveAd = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "removeAd", "(J)V"); + jAdsServiceBannerAdNew = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "bannerAdNew", "(J)V"); + jAdsServiceBannerAdLoad = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "bannerAdLoad", "(J)V"); + jAdsServiceBannerAdShow =(*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "bannerAdShow", "(J)V"); + jAdsServiceBannerAdHide =(*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "bannerAdHide", "(J)V"); + jAdsServiceBannerAdSetAdLayout = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "bannerAdSetAdLayout", "(JLjava/lang/String;)V"); + jAdsServiceBannerAdSetAdSize = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "bannerAdSetAdSize", "(JLjava/lang/String;)V"); + jAdsServiceBannerAdSetAdUnitId = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "bannerAdSetAdUnitId", "(JLjava/lang/String;)V"); + jAdsServiceInterstitialAdLoad = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "interstitialAdLoad", "(JLjava/lang/String;)V"); + jAdsServiceInterstitialAdShow = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "interstitialAdShow", "(J)V"); + jAdsServiceRewardedAdLoad = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "rewardedAdLoad", "(JLjava/lang/String;)V"); + jAdsServiceRewardedAdShow = (*dalvikEnv)->GetMethodID(dalvikEnv, jAdsServiceClass, "rewardedAdShow", "(J)V"); + + jobject jActivity = substrateGetActivity(); + jobject jObj = (*dalvikEnv)->NewObject(dalvikEnv, jAdsServiceClass, jAdsServiceInitMethod, jActivity); + jDalvikAdsService = (*dalvikEnv)->NewGlobalRef(dalvikEnv, jObj); + + DETACH_DALVIK(); +} + +JNIEXPORT jint JNICALL +JNI_OnLoad_ads(JavaVM *vm, void *reserved) +{ + JNIEnv* graalEnv; + ATTACH_LOG_INFO("JNI_OnLoad_ads called"); +#ifdef JNI_VERSION_1_8 + if ((*vm)->GetEnv(vm, (void **)&graalEnv, JNI_VERSION_1_8) != JNI_OK) { + ATTACH_LOG_WARNING("Error initializing native Ads from OnLoad"); + return JNI_FALSE; + } + ATTACH_LOG_FINE("[Ads Service] Initializing native Ads from OnLoad"); + initializeGraalHandles(graalEnv); + initializeAdsDalvikHandles(); + return JNI_VERSION_1_8; +#else + #error Error: Java 8+ SDK is required to compile Attach +#endif +} + +// from Java to Android + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeInitialize +(JNIEnv *env, jclass jClass) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceInitialize); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeSetRequestConfiguration +(JNIEnv *env, jclass jClass, jint jtagForChildDirectedTreatment, jint jtagForUnderAgeOfConsent, jstring jmaxAdContentRating, jobjectArray jtestDeviceIds) +{ + const char *maxAdContentRatingChars = (*env)->GetStringUTFChars(env, jmaxAdContentRating, NULL); + int count = (*env)->GetArrayLength(env, jtestDeviceIds); + + ATTACH_DALVIK(); + jstring maxAdContentRating = (*dalvikEnv)->NewStringUTF(dalvikEnv, maxAdContentRatingChars); + jobjectArray result = (jobjectArray) (*dalvikEnv)->NewObjectArray(dalvikEnv, count, + (*dalvikEnv)->FindClass(dalvikEnv, "java/lang/String"), NULL); + + for (int i = 0; i < count; i++) { + jstring id = (jstring) ((*env)->GetObjectArrayElement(env, jtestDeviceIds, i)); + const char *idString = (*env)->GetStringUTFChars(env, id, NULL); + (*dalvikEnv)->SetObjectArrayElement(dalvikEnv, result, i, + (*dalvikEnv)->NewStringUTF(dalvikEnv, idString)); + (*env)->ReleaseStringUTFChars(env, id, idString); + } + + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceSetRequestConfiguration, jtagForChildDirectedTreatment, jtagForUnderAgeOfConsent, maxAdContentRating, result); + (*dalvikEnv)->DeleteLocalRef(dalvikEnv, result); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeRemoveAd +(JNIEnv *env, jclass jClass, jlong jid) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceRemoveAd, jid); + DETACH_DALVIK(); +} + +// banner ad + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeBannerAdNew +(JNIEnv *env, jclass jClass, jlong jid) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceBannerAdNew, jid); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeBannerAdLoad +(JNIEnv *env, jclass jClass, jlong jid) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceBannerAdLoad, jid); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeBannerAdShow +(JNIEnv *env, jclass jClass, jlong jid) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceBannerAdShow, jid); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeBannerAdHide +(JNIEnv *env, jclass jClass, jlong jid) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceBannerAdHide, jid); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeBannerAdSetAdLayout +(JNIEnv *env, jclass jClass, jlong jid, jstring jlayout) +{ + const char *layoutChars = (*env)->GetStringUTFChars(env, jlayout, NULL); + + ATTACH_DALVIK(); + jstring layout = (*dalvikEnv)->NewStringUTF(dalvikEnv, layoutChars); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceBannerAdSetAdLayout, jid, layout); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeBannerAdSetAdSize +(JNIEnv *env, jclass jClass, jlong jid, jstring jsize) +{ + const char *sizeChars = (*env)->GetStringUTFChars(env, jsize, NULL); + + ATTACH_DALVIK(); + jstring size = (*dalvikEnv)->NewStringUTF(dalvikEnv, sizeChars); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceBannerAdSetAdSize, jid, size); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeBannerAdSetAdUnitId +(JNIEnv *env, jclass jClass, jlong jid, jstring jadUnitId) +{ + const char *adUnitIdChars = (*env)->GetStringUTFChars(env, jadUnitId, NULL); + + ATTACH_DALVIK(); + jstring adUnitId = (*dalvikEnv)->NewStringUTF(dalvikEnv, adUnitIdChars); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceBannerAdSetAdUnitId, jid, adUnitId); + DETACH_DALVIK(); +} + +// interstitial ad + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeInterstitialAdLoad +(JNIEnv *env, jclass jClass, jlong jid, jstring jadUnitId) +{ + const char *adUnitIdChars = (*env)->GetStringUTFChars(env, jadUnitId, NULL); + + ATTACH_DALVIK(); + jstring adUnitId = (*dalvikEnv)->NewStringUTF(dalvikEnv, adUnitIdChars); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceInterstitialAdLoad, jid, adUnitId); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeInterstitialAdShow +(JNIEnv *env, jclass jClass, jlong jid) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceInterstitialAdShow, jid); + DETACH_DALVIK(); +} + +// rewarded ad + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeRewardedAdLoad +(JNIEnv *env, jclass jClass, jlong jid, jstring jadUnitId) +{ + const char *adUnitIdChars = (*env)->GetStringUTFChars(env, jadUnitId, NULL); + + ATTACH_DALVIK(); + jstring adUnitId = (*dalvikEnv)->NewStringUTF(dalvikEnv, adUnitIdChars); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceRewardedAdLoad, jid, adUnitId); + DETACH_DALVIK(); +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_AndroidAdsService_nativeRewardedAdShow +(JNIEnv *env, jclass jClass, jlong jid) +{ + ATTACH_DALVIK(); + (*dalvikEnv)->CallVoidMethod(dalvikEnv, jDalvikAdsService, jAdsServiceRewardedAdShow, jid); + DETACH_DALVIK(); +} + +// from Dalvik to native + +JNIEXPORT void JNICALL Java_com_gluonhq_helloandroid_DalvikAdsService_nativeInvokeCallback +(JNIEnv *env, jobject service, jlong id, jstring callbackClass, jstring callbackMethod, jobjectArray params) +{ + const char *callbackClassChars = (*env)->GetStringUTFChars(env, callbackClass, NULL); + const char *callbackMethodChars = (*env)->GetStringUTFChars(env, callbackMethod, NULL); + int count = (*env)->GetArrayLength(env, params); + + ATTACH_GRAAL(); + + jobjectArray result = (jobjectArray) (*graalEnv)->NewObjectArray(graalEnv, count, + (*graalEnv)->FindClass(graalEnv, "java/lang/String"), NULL); + + for (int i = 0; i < count; i++) { + jstring param = (jstring) ((*env)->GetObjectArrayElement(env, params, i)); + const char *paramString = (*env)->GetStringUTFChars(env, param, NULL); + (*graalEnv)->SetObjectArrayElement(graalEnv, result, i, + (*graalEnv)->NewStringUTF(graalEnv, paramString)); + (*env)->ReleaseStringUTFChars(env, param, paramString); + } + + jstring jcallbackClass = (*graalEnv)->NewStringUTF(graalEnv, callbackClassChars); + jstring jcallbackMethod = (*graalEnv)->NewStringUTF(graalEnv, callbackMethodChars); + (*graalEnv)->CallStaticVoidMethod(graalEnv, jGraalAdsClass, jGraalInvokeCallbackMethod, id, jcallbackClass, jcallbackMethod, result); + DETACH_GRAAL(); + + (*graalEnv)->DeleteLocalRef(graalEnv, result); + (*env)->ReleaseStringUTFChars(env, callbackMethod, callbackMethodChars); + (*env)->ReleaseStringUTFChars(env, callbackClass, callbackClassChars); +} \ No newline at end of file diff --git a/modules/ads/src/main/native/android/dalvik/AdRegistry.java b/modules/ads/src/main/native/android/dalvik/AdRegistry.java new file mode 100644 index 00000000..ab6bccc1 --- /dev/null +++ b/modules/ads/src/main/native/android/dalvik/AdRegistry.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Gluon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.gluonhq.helloandroid; + +import java.util.HashMap; +import java.util.Map; + +public class AdRegistry { + + private final Map ads; + + public AdRegistry() { + ads = new HashMap<>(); + } + + public void add(long id, Object ad) { + ads.put(id, ad); + } + + public Object remove(long id) { + return ads.remove(id); + } + + @SuppressWarnings("unchecked") + public T get(long id) { + return (T) ads.get(id); + } +} diff --git a/modules/ads/src/main/native/android/dalvik/DalvikAdsService.java b/modules/ads/src/main/native/android/dalvik/DalvikAdsService.java new file mode 100644 index 00000000..7b2c2e91 --- /dev/null +++ b/modules/ads/src/main/native/android/dalvik/DalvikAdsService.java @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2025 Gluon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.gluonhq.helloandroid; + +import android.app.Activity; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.google.android.gms.ads.AdError; +import com.google.android.gms.ads.AdListener; +import com.google.android.gms.ads.AdRequest; +import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.AdView; +import com.google.android.gms.ads.FullScreenContentCallback; +import com.google.android.gms.ads.LoadAdError; +import com.google.android.gms.ads.MobileAds; +import com.google.android.gms.ads.OnUserEarnedRewardListener; +import com.google.android.gms.ads.interstitial.InterstitialAd; +import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback; +import com.google.android.gms.ads.rewarded.RewardItem; +import com.google.android.gms.ads.rewarded.RewardedAd; +import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback; + +import java.security.InvalidParameterException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class DalvikAdsService { + + private static final String TAG = Util.TAG; + + private static final boolean debug = Util.isDebug(); + + private final Activity activity; + + private final ViewGroup viewGroup; + + private final AdRegistry registry; + + private final Map bannerAdLayouts; + + public DalvikAdsService(Activity activity) { + this.activity = activity; + this.viewGroup = (ViewGroup) activity.getWindow().getDecorView(); + this.registry = new AdRegistry(); + this.bannerAdLayouts = new HashMap<>(); + } + + private void initialize() { + Log.v(TAG, "Initializing Google Mobile Ads..."); + + MobileAds.initialize(activity, initializationStatus -> { + Log.v(TAG, "Initialization of Google Mobile Ads completed"); + invokeCallback(-1, "", ""); + }); + } + + private void setRequestConfiguration(int tagForChildDirectedTreatment, int tagForUnderAgeOfConsent, String maxAdContentRating, String[] testDeviceIds) { + if (debug) { + Log.d(TAG, "setRequestConfiguration(" + tagForChildDirectedTreatment + ", " + tagForUnderAgeOfConsent + ", " + maxAdContentRating + ", " + testDeviceIds + ")"); + } + + MobileAds.setRequestConfiguration(MobileAds.getRequestConfiguration().toBuilder() + .setTagForChildDirectedTreatment(tagForChildDirectedTreatment) + .setTagForUnderAgeOfConsent(tagForUnderAgeOfConsent) + .setMaxAdContentRating(maxAdContentRating) + .setTestDeviceIds(Arrays.asList(testDeviceIds)) + .build()); + } + + private void removeAd(long id) { + if (debug) { + Log.d(TAG, "removeAd(" + id + ")"); + } + + Object ad = registry.remove(id); + + if (ad instanceof AdView) { + AdView adView = (AdView) ad; + View parentView = (View) adView.getParent(); + + activity.runOnUiThread(() -> { + if (parentView instanceof ViewGroup) { + ViewGroup parentViewGroup = (ViewGroup) parentView; + parentViewGroup.removeView(adView); + } + + adView.destroy(); + }); + } + } + + private void bannerAdNew(long id) { + if (debug) { + Log.d(TAG, "bannerAdNew(" + id + ")"); + } + + AdView adView = new AdView(activity); + FrameLayout layout = new FrameLayout(activity); + + layout.addView(adView, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL)); + + registry.add(id, adView); + bannerAdLayouts.put(id, layout); + + // register the ad listener + activity.runOnUiThread(() -> adView.setAdListener(new AdListener() { + @Override + public void onAdClicked() { + invokeCallback(id, "AdListener", "onAdClicked"); + } + + @Override + public void onAdClosed() { + invokeCallback(id, "AdListener", "onAdClosed"); + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + invokeCallback(id, "AdListener", "onAdFailedToLoad"); + } + + @Override + public void onAdImpression() { + invokeCallback(id, "AdListener", "onAdImpression"); + } + + @Override + public void onAdLoaded() { + invokeCallback(id, "AdListener", "onAdLoaded"); + } + + @Override + public void onAdOpened() { + invokeCallback(id, "AdListener", "onAdOpened"); + } + + @Override + public void onAdSwipeGestureClicked() { + invokeCallback(id, "AdListener", "onAdSwipeGestureClicked"); + } + })); + } + + private void bannerAdShow(long id) { + if (debug) { + Log.d(TAG, "bannerAdShow(" + id + ")"); + } + + activity.runOnUiThread(() -> { + FrameLayout layout = bannerAdLayouts.get(id); + if (layout.getParent() == null) { + viewGroup.addView(layout); + } + }); + } + + private void bannerAdHide(long id) { + if (debug) { + Log.d(TAG, "bannerAdHide(" + id + ")"); + } + + activity.runOnUiThread(() -> { + FrameLayout layout = bannerAdLayouts.get(id); + if (layout.getParent() != null) { + viewGroup.removeView(layout); + } + }); + } + + private void bannerAdLoad(long id) { + if (debug) { + Log.d(TAG, "bannerAdLoad(" + id + ")"); + } + + activity.runOnUiThread(() -> + registry.get(id).loadAd(new AdRequest.Builder().build())); + } + + private void bannerAdSetAdLayout(long id, String layout) { + if (debug) { + Log.d(TAG, "bannerAdSetAdLayout(" + id + ", " + layout + ")"); + } + + int gravity; + + switch (layout) { + case "TOP": gravity = Gravity.TOP; break; + case "BOTTOM": gravity = Gravity.BOTTOM; break; + default: + throw new InvalidParameterException("Layout '" + layout + "' is invalid!"); + } + + activity.runOnUiThread(() -> + bannerAdLayouts.get(id).updateViewLayout(registry.get(id), new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + gravity | Gravity.CENTER_HORIZONTAL))); + } + + private void bannerAdSetAdSize(long id, String size) { + if (debug) { + Log.d(TAG, "bannerAdSetAdSize(" + id + ", " + size + ")"); + } + + AdSize adSize; + + switch (size) { + case "BANNER": adSize = AdSize.BANNER; break; + case "FLUID": adSize = AdSize.FLUID; break; + case "FULL_BANNER": adSize = AdSize.FULL_BANNER; break; + case "INVALID": adSize = AdSize.INVALID; break; + case "LARGE_BANNER": adSize = AdSize.LARGE_BANNER; break; + case "LEADERBOARD": adSize = AdSize.LEADERBOARD; break; + case "MEDIUM_RECTANGLE": adSize = AdSize.MEDIUM_RECTANGLE; break; + case "WIDE_SKYSCRAPER": adSize = AdSize.WIDE_SKYSCRAPER; break; + default: + throw new InvalidParameterException("AdSize '" + size + "' is invalid!"); + } + + activity.runOnUiThread(() -> + registry.get(id).setAdSize(adSize)); + } + + private void bannerAdSetAdUnitId(long id, String adUnitId) { + if (debug) { + Log.d(TAG, "bannerAdSetAdUnitId(" + id + ", " + adUnitId + ")"); + } + + activity.runOnUiThread(() -> + registry.get(id).setAdUnitId(adUnitId)); + } + + private void interstitialAdLoad(long id, String adUnitId) { + if (debug) { + Log.d(TAG, "interstitialAdLoad(" + id + ", " + adUnitId + ")"); + } + + activity.runOnUiThread(() -> InterstitialAd.load(activity, adUnitId, new AdRequest.Builder().build(), new InterstitialAdLoadCallback() { + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + invokeCallback(id, "InterstitialAdLoadCallback", "onAdFailedToLoad"); + } + + @Override + public void onAdLoaded(@NonNull InterstitialAd interstitialAd) { + registry.add(id, interstitialAd); + + activity.runOnUiThread(() -> interstitialAd.setFullScreenContentCallback(new FullScreenContentCallback() { + @Override + public void onAdClicked() { + invokeCallback(id, "FullScreenContentCallback", "onAdClicked"); + } + + @Override + public void onAdDismissedFullScreenContent() { + invokeCallback(id, "FullScreenContentCallback", "onAdDismissedFullScreenContent"); + } + + @Override + public void onAdFailedToShowFullScreenContent(@NonNull AdError adError) { + invokeCallback(id, "FullScreenContentCallback", "onAdFailedToShowFullScreenContent"); + } + + @Override + public void onAdImpression() { + invokeCallback(id, "FullScreenContentCallback", "onAdImpression"); + } + + @Override + public void onAdShowedFullScreenContent() { + invokeCallback(id, "FullScreenContentCallback", "onAdShowedFullScreenContent"); + } + })); + + invokeCallback(id, "InterstitialAdLoadCallback", "onAdLoaded"); + } + })); + } + + private void interstitialAdShow(long id) { + if (debug) { + Log.d(TAG, "interstitialAdShow(" + id + ")"); + } + + activity.runOnUiThread(() -> + registry.get(id).show(activity)); + } + + private void rewardedAdLoad(long id, String adUnitId) { + if (debug) { + Log.d(TAG, "rewardedAdLoad(" + id + ", " + adUnitId + ")"); + } + + activity.runOnUiThread(() -> RewardedAd.load(activity, adUnitId, new AdRequest.Builder().build(), new RewardedAdLoadCallback() { + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + invokeCallback(id, "RewardedAdLoadCallback", "onAdFailedToLoad"); + } + + @Override + public void onAdLoaded(@NonNull RewardedAd rewardedAd) { + registry.add(id, rewardedAd); + + activity.runOnUiThread(() -> rewardedAd.setFullScreenContentCallback(new FullScreenContentCallback() { + @Override + public void onAdClicked() { + invokeCallback(id, "FullScreenContentCallback", "onAdClicked"); + } + + @Override + public void onAdDismissedFullScreenContent() { + invokeCallback(id, "FullScreenContentCallback", "onAdDismissedFullScreenContent"); + } + + @Override + public void onAdFailedToShowFullScreenContent(@NonNull AdError adError) { + invokeCallback(id, "FullScreenContentCallback", "onAdFailedToShowFullScreenContent"); + } + + @Override + public void onAdImpression() { + invokeCallback(id, "FullScreenContentCallback", "onAdImpression"); + } + + @Override + public void onAdShowedFullScreenContent() { + invokeCallback(id, "FullScreenContentCallback", "onAdShowedFullScreenContent"); + } + })); + + invokeCallback(id, "RewardedAdLoadCallback", "onAdLoaded"); + } + })); + } + + private void rewardedAdShow(long id) { + if (debug) { + Log.d(TAG, "rewardedAdShow(" + id + ")"); + } + + activity.runOnUiThread(() -> registry.get(id).show(activity, new OnUserEarnedRewardListener() { + @Override + public void onUserEarnedReward(@NonNull RewardItem rewardItem) { + invokeCallback(id, "OnUserEarnedRewardListener", "onUserEarnedReward", new String[] { rewardItem.getType(), String.valueOf(rewardItem.getAmount()) }); + } + })); + } + + private void invokeCallback(long id, String callback, String method) { + invokeCallback(id, callback, method, new String[0]); + } + + private void invokeCallback(long id, String callback, String method, String[] params) { + if (debug) { + Log.d(TAG, "invokeCallback(" + id + ", " + callback + ", " + method + ", " + params + ")"); + } + + nativeInvokeCallback(id, callback, method, params); + } + + private native void nativeInvokeCallback(long id, String callback, String method, String[] params); +} diff --git a/modules/ads/src/main/native/ios/Ads.h b/modules/ads/src/main/native/ios/Ads.h new file mode 100644 index 00000000..298f7204 --- /dev/null +++ b/modules/ads/src/main/native/ios/Ads.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, Gluon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import +#import +// #import + +#include "jni.h" +#include "AttachMacros.h" + +@interface AdsService : NSObject + + - (void) initialize; + - (void) setRequestConfiguration:(int)tagForChildDirectedTreatment tagForUnderAgeOfConsent:(int)tagForUnderAgeOfConsent maxAdContentRating:(NSString*)rating testDeviceIds:(NSArray*)testDevices; + + // banner + - (void) bannerAdNew:(long)adId; + - (void) bannerAdShow:(long)adId; + - (void) bannerAdHide:(long)adId; + - (void) bannerAdLoad:(long)adId; + - (void) bannerAdSetAdLayout:(long)adId layout:(NSString*)layout; + - (void) bannerAdSetAdSize:(long)adId size:(NSString*)size; + - (void) bannerAdSetAdUnitId:(long)adId adUnitId:(NSString*)unitId; + + // interstitial + - (void) interstitialAdLoad:(long)adId adUnitId:(NSString*)unitId; + - (void) interstitialAdShow:(long)adId; + + // rewarded + - (void) rewardedAdLoad:(long)adId adUnitId:(NSString*)unitId; + - (void) rewardedAdShow:(long)adId; + +@end \ No newline at end of file diff --git a/modules/ads/src/main/native/ios/Ads.m b/modules/ads/src/main/native/ios/Ads.m new file mode 100644 index 00000000..5a7f4e95 --- /dev/null +++ b/modules/ads/src/main/native/ios/Ads.m @@ -0,0 +1,295 @@ +#import "Ads.h" + +JNIEnv *env; + +JNIEXPORT int JNICALL +JNI_OnLoad_Ads(JavaVM *vm, void *reserved) +{ +#ifdef JNI_VERSION_1_8 + //min. returned JNI_VERSION required by JDK8 for builtin libraries + if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK) { + return JNI_VERSION_1_4; + } + return JNI_VERSION_1_8; +#else + return JNI_VERSION_1_4; +#endif +} + +static bool adsInitialized = false; + +AdsService *adsService; // singleton instance of the native AdsService +NSMutableDictionary *adRegistry; +NSMutableDictionary *bannerContainers; + +@implementation AdsService + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_initAds +(JNIEnv *env, jclass jClass) +{ + // Note: there is no need for callbacks from native to Java + if (!adsInitialized) { + adsInitialized = true; + adsService = [[AdsService alloc] init]; + adRegistry = [NSMutableDictionary dictionary]; + bannerContainers = [NSMutableDictionary dictionary]; + } +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeInitialize +(JNIEnv *env, jclass jClass) +{ + [adsService initialize]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeSetRequestConfiguration +(JNIEnv *env, jclass jClass, int jtagForChildDirectedTreatment, int jtagForUnderAgeOfConsent, jstring jmaxAdContentRating, jobjectArray jtestDeviceIds) +{ + const char *maxAdContentRatingChars = (*env)->GetStringUTFChars(env, jmaxAdContentRating, NULL); + NSString *maxAdContentRating = [NSString stringWithCharacters:(UniChar *)maxAdContentRatingChars length:(*env)->GetStringLength(env, jmaxAdContentRating)]; + (*env)->ReleaseStringChars(env, jmaxAdContentRating, maxAdContentRatingChars); + + int count = (*env)->GetArrayLength(env, jtestDeviceIds); + NSMutableArray *testDeviceIds = [NSMutableArray arrayWithCapacity:count]; + + for (jsize i = 0; i < count; i++) { + jstring jtestDeviceId = (jstring)(*env)->GetObjectArrayElement(env, jtestDeviceIds, i); + const jchar *testDeviceIdString = (*env)->GetStringChars(env, jtestDeviceId, NULL); + NSString *testDeviceId = [NSString stringWithCharacters:(UniChar *)testDeviceIdString length:(*env)->GetStringLength(env, jtestDeviceId)]; + (*env)->ReleaseStringChars(env, jtestDeviceId, testDeviceIdString); + + [testDeviceIds addObject:testDeviceId]; + } + + [adsService setRequestConfiguration:jtagForChildDirectedTreatment tagForUnderAgeOfConsent:jtagForUnderAgeOfConsent maxAdContentRating:jmaxAdContentRating testDeviceIds:testDeviceIds]; +} + +// banner + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeBannerAdNew +(JNIEnv *env, jclass jClass, long adId) +{ + [adsService bannerAdNew:adId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeBannerAdLoad +(JNIEnv *env, jclass jClass, long adId) +{ + [adsService bannerAdLoad:adId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeBannerAdShow +(JNIEnv *env, jclass jClass, long adId) +{ + [adsService bannerAdShow:adId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeBannerAdHide +(JNIEnv *env, jclass jClass, long adId) +{ + [adsService bannerAdHide:adId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeBannerAdSetAdLayout +(JNIEnv *env, jclass jClass, long adId, jstring jlayout) +{ + const char *layoutChars = (*env)->GetStringUTFChars(env, jlayout, NULL); + NSString *layout = [NSString stringWithCharacters:(UniChar *)layoutChars length:(*env)->GetStringLength(env, jlayout)]; + (*env)->ReleaseStringChars(env, jlayout, layoutChars); + + [adsService bannerAdSetAdLayout:adId layout:layout]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeBannerAdSetAdSize +(JNIEnv *env, jclass jClass, long adId, jstring jsize) +{ + const char *sizeChars = (*env)->GetStringUTFChars(env, jsize, NULL); + NSString *size = [NSString stringWithCharacters:(UniChar *)sizeChars length:(*env)->GetStringLength(env, jsize)]; + (*env)->ReleaseStringChars(env, jsize, sizeChars); + + [adsService bannerAdSetAdSize:adId size:size]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeBannerAdSetAdUnitId +(JNIEnv *env, jclass jClass, long adId, jstring jadUnitId) +{ + const char *adUnitIdChars = (*env)->GetStringUTFChars(env, jadUnitId, NULL); + NSString *adUnitId = [NSString stringWithCharacters:(UniChar *)adUnitIdChars length:(*env)->GetStringLength(env, jadUnitId)]; + (*env)->ReleaseStringChars(env, jadUnitId, adUnitIdChars); + + [adsService bannerAdSetAdUnitId:adId adUnitId:adUnitId]; +} + +// interstitial + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeInterstitialAdLoad +(JNIEnv *env, jclass jClass, long adId, jstring jadUnitId) +{ + const char *adUnitIdChars = (*env)->GetStringUTFChars(env, jadUnitId, NULL); + NSString *adUnitId = [NSString stringWithCharacters:(UniChar *)adUnitIdChars length:(*env)->GetStringLength(env, jadUnitId)]; + (*env)->ReleaseStringChars(env, jadUnitId, adUnitIdChars); + + [adsService interstitialAdLoad:adId adUnitId:adUnitId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeInterstitialAdShow +(JNIEnv *env, jclass jClass, long adId) +{ + [adsService interstitialAdShow:adId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeInterstitialAdSetFullScreenContentCallback +(JNIEnv *env, jclass jClass, long adId) +{ + [adsService interstitialAdSetFullScreenContentCallback:adId]; +} + +// rewarded + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeRewardedAdLoad +(JNIEnv *env, jclass jClass, long adId, jstring jadUnitId) +{ + const char *adUnitIdChars = (*env)->GetStringUTFChars(env, jadUnitId, NULL); + NSString *adUnitId = [NSString stringWithCharacters:(UniChar *)adUnitIdChars length:(*env)->GetStringLength(env, jadUnitId)]; + (*env)->ReleaseStringChars(env, jadUnitId, adUnitIdChars); + + [adsService rewardedAdLoad:adId adUnitId:adUnitId]; +} + +JNIEXPORT void JNICALL Java_com_gluonhq_attach_ads_impl_IOSAdsService_nativeRewardedAdShow +(JNIEnv *env, jclass jClass, long adId) +{ + [adsService rewardedAdShow:adId]; +} + +// from native to Java + +JNIEXPORT void JNICALL Java_com_gluonhq_helloandroid_DalvikAdsService_nativeInvokeCallback +(JNIEnv *env, jobject service, long adId, jstring callbackClass, jstring callbackMethod, jobjectArray params) +{ +// const char *callbackClassChars = (*env)->GetStringUTFChars(env, callbackClass, NULL); +// const char *callbackMethodChars = (*env)->GetStringUTFChars(env, callbackMethod, NULL); +// int count = (*env)->GetArrayLength(env, params); +// +// ATTACH_GRAAL(); +// +// jobjectArray result = (jobjectArray) (*graalEnv)->NewObjectArray(graalEnv, count, +// (*graalEnv)->FindClass(graalEnv, "java/lang/String"), NULL); +// +// for (int i = 0; i < count; i++) { +// jstring param = (jstring) ((*env)->GetObjectArrayElement(env, params, i)); +// const char *paramString = (*env)->GetStringUTFChars(env, param, NULL); +// (*graalEnv)->SetObjectArrayElement(graalEnv, result, i, +// (*graalEnv)->NewStringUTF(graalEnv, paramString)); +// (*env)->ReleaseStringUTFChars(env, param, paramString); +// } +// +// jstring jcallbackClass = (*graalEnv)->NewStringUTF(graalEnv, callbackClassChars); +// jstring jcallbackMethod = (*graalEnv)->NewStringUTF(graalEnv, callbackMethodChars); +// (*graalEnv)->CallStaticVoidMethod(graalEnv, jGraalAdsClass, jGraalInvokeCallbackMethod, adId, jcallbackClass, jcallbackMethod, result); +// DETACH_GRAAL(); +// +// (*graalEnv)->DeleteLocalRef(graalEnv, result); +// (*env)->ReleaseStringUTFChars(env, callbackMethod, callbackMethodChars); +// (*env)->ReleaseStringUTFChars(env, callbackClass, callbackClassChars); +} + +- (void) initialize { +// [[GADMobileAds sharedInstance] startWithCompletionHandler:^(GADInitializationStatus * _Nonnull status) { +// [self invokeCallback:-1 callback:@"" method:@"" params:@[]]; +// }]; +} + +- (void) setRequestConfiguration:(int)tagForChildDirectedTreatment tagForUnderAgeOfConsent:(int)tagForUnderAgeOfConsent maxAdContentRating:(NSString*)rating testDeviceIds:(NSArray*)testDevices { +// GADRequestConfiguration *config = GADMobileAds.sharedInstance.requestConfiguration; +// config.tagForChildDirectedTreatment = tagForChildDirectedTreatment; +// config.tagForUnderAgeOfConsent = tagForUnderAgeOfConsent; +// config.maxAdContentRating = rating; +// config.testDeviceIdentifiers = testDevices; +} + +- (void) bannerAdNew:(long)adId { +// GADBannerView *banner = [[GADBannerView alloc] initWithAdSize:kGADAdSizeBanner]; +// +// UIView *container = [[UIView alloc] init]; +// [container addSubview:banner]; +// +// banner.rootViewController = UIApplication.sharedApplication.keyWindow.rootViewController; +// +// self.adRegistry[@(adId)] = banner; +// self.bannerContainers[@(adId)] = container; +} + +- (void) bannerAdShow:(long)adId { +// UIView *container = self.bannerContainers[@(adId)]; +// UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController; +// [root.view addSubview:container]; +// +// CGRect frame = container.frame; +// frame.origin.y = root.view.frame.size.height - 50; +// frame.origin.x = (root.view.frame.size.width - 320) / 2; +// container.frame = frame; +} + +- (void) bannerAdHide:(long)adId { +// UIView *container = self.bannerContainers[@(adId)]; +// [container removeFromSuperview]; +} + +- (void) bannerAdLoad:(long)adId { +// GADBannerView *banner = self.adRegistry[@(adId)]; +// GADRequest *request = [GADRequest request]; +// +// [banner loadRequest:request]; +} + +- (void) bannerAdSetAdUnitId:(long)adId adUnitId:(NSString*)unitId { +// GADBannerView *banner = self.adRegistry[@(adId)]; +// banner.adUnitID = unitId; +} + +- (void) interstitialAdLoad:(long)adId adUnitId:(NSString*)unitId { +// [GADInterstitialAd loadWithAdUnitID:unitId request:[GADRequest request] completionHandler:^(GADInterstitialAd *ad, NSError *error) { +// if (error) { +// [self invokeCallback:adId callback:@"InterstitialAd" method:@"onAdFailedToLoad" params:@[]]; +// } else { +// self.adRegistry[@(adId)] = ad; +// [self invokeCallback:adId callback:@"InterstitialAd" method:@"onAdLoaded" params:@[]]; +// } +// }]; +} + +- (void) interstitialAdShow:(long)adId { +// GADInterstitialAd *ad = self.adRegistry[@(adId)]; +// UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController; +// +// [ad presentFromRootViewController:root]; +} + +- (void) rewardedAdLoad:(long)adId adUnitId:(NSString*)unitId { +// [GADRewardedAd loadWithAdUnitID:unitId request:[GADRequest request] completionHandler:^(GADRewardedAd *ad, NSError *error) { +// if (error) { +// [self invokeCallback:adId callback:@"RewardedAd" method:@"onAdFailedToLoad" params:@[]]; +// } else { +// self.adRegistry[@(adId)] = ad; +// [self invokeCallback:adId callback:@"RewardedAd" method:@"onAdLoaded" params:@[]]; +// } +// }]; +} + +- (void) rewardedAdShow:(long)adId { +// GADRewardedAd *ad = self.adRegistry[@(adId)]; +// UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController; +// +// [ad presentFromRootViewController:root userDidEarnRewardHandler:^{ +// GADAdReward *reward = ad.adReward; +// [self invokeCallback:adId callback:@"Rewarded" method:@"onUserEarnedReward" params:@[reward.type, [NSString stringWithFormat:@"%ld", (long)reward.amount]]]; +// }]; +} + +- (void) invokeCallback:(long)adId callback:(NSString*)callback method:(NSString*)method params:(NSArray*)params { + // This calls your JNI bridge generated by Gluon Attach + // Same concept as nativeInvokeCallback on IOS +} + +@end \ No newline at end of file diff --git a/modules/ads/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json b/modules/ads/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json new file mode 100644 index 00000000..c31acd80 --- /dev/null +++ b/modules/ads/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json @@ -0,0 +1,6 @@ +[ + { + "name" : "com.gluonhq.attach.ads.impl.DefaultAdsService", + "methods":[{"name":"invokeCallback","parameterTypes":["long", "java.lang.String", "java.lang.String", "java.lang.String[]"] }] + } +] \ No newline at end of file diff --git a/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json b/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json new file mode 100644 index 00000000..5c25fba9 --- /dev/null +++ b/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-aarch64-android.json @@ -0,0 +1,6 @@ +[ + { + "name" : "com.gluonhq.attach.ads.impl.AndroidAdsService", + "methods":[{"name":"","parameterTypes":[] }] + } +] \ No newline at end of file diff --git a/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json b/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json new file mode 100644 index 00000000..1aa17dfb --- /dev/null +++ b/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-arm64-ios.json @@ -0,0 +1,6 @@ +[ + { + "name": "com.gluonhq.attach.ads.impl.IOSAdsService", + "methods": [{"name": "", "parameterTypes": []}] + } +] \ No newline at end of file diff --git a/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-x86_64-ios.json b/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-x86_64-ios.json new file mode 100644 index 00000000..1aa17dfb --- /dev/null +++ b/modules/ads/src/main/resources/META-INF/substrate/config/reflectionconfig-x86_64-ios.json @@ -0,0 +1,6 @@ +[ + { + "name": "com.gluonhq.attach.ads.impl.IOSAdsService", + "methods": [{"name": "", "parameterTypes": []}] + } +] \ No newline at end of file diff --git a/modules/ads/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml b/modules/ads/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml new file mode 100644 index 00000000..9398d5e7 --- /dev/null +++ b/modules/ads/src/main/resources/META-INF/substrate/dalvik/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/ads/src/main/resources/META-INF/substrate/dalvik/android-dependencies.txt b/modules/ads/src/main/resources/META-INF/substrate/dalvik/android-dependencies.txt new file mode 100644 index 00000000..43713e9a --- /dev/null +++ b/modules/ads/src/main/resources/META-INF/substrate/dalvik/android-dependencies.txt @@ -0,0 +1,2 @@ +implementation 'com.android.support:appcompat-v7:28.0.0' +implementation 'com.google.android.gms:play-services-ads:24.4.0' \ No newline at end of file diff --git a/modules/ads/src/main/resources/META-INF/substrate/dalvik/build.gradle b/modules/ads/src/main/resources/META-INF/substrate/dalvik/build.gradle new file mode 100644 index 00000000..5d880709 --- /dev/null +++ b/modules/ads/src/main/resources/META-INF/substrate/dalvik/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + + namespace 'com.gluonhq.helloandroid' + + compileSdkVersion 34 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 34 + } + +} + +repositories { + google() +} + +dependencies { + compileOnly fileTree(dir: '../libs', include: '*.jar') + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.google.android.gms:play-services-ads:24.4.0' +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9bc2af14..cfd91450 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ pluginManagement { rootProject.name = 'attach' include 'accelerometer' +include 'ads' include 'audio' include 'audio-recording' include 'augmented-reality' @@ -43,6 +44,7 @@ include 'video' include 'util' project(':accelerometer').projectDir = file('modules/accelerometer') +project(':ads').projectDir = file('modules/ads') project(':audio').projectDir = file('modules/audio') project(':audio-recording').projectDir = file('modules/audio-recording') project(':augmented-reality').projectDir = file('modules/augmented-reality')