Skip to content

AdMob Rewarded Ads on iOS #752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admob/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ set(ios_SRCS
src/ios/FADBannerView.mm
src/ios/FADInterstitialDelegate.mm
src/ios/FADRequest.mm
src/ios/FADRewardedAdDelegate.mm
src/ios/ad_result_ios.mm
src/ios/adapter_response_info_ios.mm
src/ios/admob_ios.mm
Expand Down
8 changes: 0 additions & 8 deletions admob/integration_test/src/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,6 @@ TEST_F(FirebaseAdMobTest, TestInterstitialAdLoad) {

TEST_F(FirebaseAdMobTest, TestRewardedAdLoad) {
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

// Note: while showing an ad requires user interaction (below),
// we test that we can simply load an ad first.
Expand Down Expand Up @@ -726,7 +725,6 @@ TEST_F(FirebaseAdMobTest, TestInterstitialAdLoadAndShow) {
TEST_F(FirebaseAdMobTest, TestRewardedAdLoadAndShow) {
TEST_REQUIRES_USER_INTERACTION;
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

firebase::admob::RewardedAd* rewarded = new firebase::admob::RewardedAd();

Expand Down Expand Up @@ -1272,7 +1270,6 @@ TEST_F(FirebaseAdMobTest, TestInterstitialAdErrorBadExtrasClassName) {

TEST_F(FirebaseAdMobTest, TestRewardedAdErrorNotInitialized) {
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

firebase::admob::RewardedAd* rewarded_ad = new firebase::admob::RewardedAd();

Expand All @@ -1287,7 +1284,6 @@ TEST_F(FirebaseAdMobTest, TestRewardedAdErrorNotInitialized) {

TEST_F(FirebaseAdMobTest, TesRewardedAdErrorAlreadyInitialized) {
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

{
firebase::admob::RewardedAd* rewarded = new firebase::admob::RewardedAd();
Expand Down Expand Up @@ -1327,7 +1323,6 @@ TEST_F(FirebaseAdMobTest, TesRewardedAdErrorAlreadyInitialized) {

TEST_F(FirebaseAdMobTest, TestRewardedAdErrorLoadInProgress) {
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

firebase::admob::RewardedAd* rewarded = new firebase::admob::RewardedAd();
WaitForCompletion(rewarded->Initialize(app_framework::GetWindowContext()),
Expand Down Expand Up @@ -1362,7 +1357,6 @@ TEST_F(FirebaseAdMobTest, TestRewardedAdErrorLoadInProgress) {

TEST_F(FirebaseAdMobTest, TestRewardedAdErrorBadAdUnitId) {
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

firebase::admob::RewardedAd* rewarded = new firebase::admob::RewardedAd();
WaitForCompletion(rewarded->Initialize(app_framework::GetWindowContext()),
Expand All @@ -1389,7 +1383,6 @@ TEST_F(FirebaseAdMobTest, TestRewardedAdErrorBadAdUnitId) {

TEST_F(FirebaseAdMobTest, TestRewardedAdErrorBadExtrasClassName) {
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

firebase::admob::RewardedAd* rewarded = new firebase::admob::RewardedAd();
WaitForCompletion(rewarded->Initialize(app_framework::GetWindowContext()),
Expand Down Expand Up @@ -1446,7 +1439,6 @@ TEST_F(FirebaseAdMobTest, TestInterstitialAdStress) {
TEST_F(FirebaseAdMobTest, TestRewardedAdStress) {
TEST_REQUIRES_USER_INTERACTION;
SKIP_TEST_ON_DESKTOP;
SKIP_TEST_ON_IOS;

for (int i = 0; i < 10; ++i) {
firebase::admob::RewardedAd* rewarded = new firebase::admob::RewardedAd();
Expand Down
44 changes: 44 additions & 0 deletions admob/src/ios/FADRewardedAdDelegate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// An Objective-C++ wrapper class that conforms to the
// GADRewardedAdDelegate protocol. When the delegate for receiving state
// change messages from a GADRewardedAd is notified, this wrapper class
// forwards the notification to the RewardedAdInternalIOS object to handle
// the state changes for an rewarded ad.

#import <Foundation/Foundation.h>
#import <GoogleMobileAds/GoogleMobileAds.h>

namespace firebase {
namespace admob {
namespace internal {
class RewardedAdInternalIOS;
} // namespace internal
} // namespace admob
} // namespace firebase

NS_ASSUME_NONNULL_BEGIN

@interface FADRewardedAdDelegate : NSObject<GADFullScreenContentDelegate>

/// Returns a FADInterstitialDelegate object with InterstitialAdInternalIOS.
- (FADRewardedAdDelegate *)initWithInternalRewardedAd:
(firebase::admob::internal::RewardedAdInternalIOS *)rewardedAd;

@end

NS_ASSUME_NONNULL_END
74 changes: 74 additions & 0 deletions admob/src/ios/FADRewardedAdDelegate.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import "admob/src/ios/FADRewardedAdDelegate.h"

#include "admob/src/ios/ad_result_ios.h"
#include "admob/src/ios/rewarded_ad_internal_ios.h"

@interface FADRewardedAdDelegate () {
/// The RewardedAdInternalIOS object.
firebase::admob::internal::RewardedAdInternalIOS *_rewardedAd;
}
@end

@implementation FADRewardedAdDelegate : NSObject

#pragma mark - Initialization

- (instancetype)initWithInternalRewardedAd:
(firebase::admob::internal::RewardedAdInternalIOS *)rewardedAd {
self = [super init];
if (self) {
_rewardedAd = rewardedAd;
}

return self;
}

#pragma mark - GADFullScreenContentDelegate

// Capture AdMob iOS Full screen events and forward them to our C++
// translation layer.
- (void)adDidRecordImpression:(nonnull id<GADFullScreenPresentingAd>)ad {
_rewardedAd->NotifyListenerOfAdImpression();
}

- (void)adDidRecordClick:(nonnull id<GADFullScreenPresentingAd>)ad {
_rewardedAd->NotifyListenerOfAdClickedFullScreenContent();
}

- (void)ad:(nonnull id<GADFullScreenPresentingAd>)ad
didFailToPresentFullScreenContentWithError:(nonnull NSError *)error {
firebase::admob::AdResultInternal ad_result_internal;
ad_result_internal.is_wrapper_error = false;
ad_result_internal.is_successful = false;
ad_result_internal.ios_error = error;
// Invoke AdMobInternal, a friend of AdResult, to have it access its
// protected constructor with the AdError data.
const firebase::admob::AdResult& ad_result = firebase::admob::AdMobInternal::CreateAdResult(ad_result_internal);
_rewardedAd->NotifyListenerOfAdFailedToShowFullScreenContent(ad_result);
}

- (void)adDidPresentFullScreenContent:(nonnull id<GADFullScreenPresentingAd>)ad {
_rewardedAd->NotifyListenerOfAdShowedFullScreenContent();
}

- (void)adDidDismissFullScreenContent:(nonnull id<GADFullScreenPresentingAd>)ad {
_rewardedAd->NotifyListenerOfAdDismissedFullScreenContent();
}

@end
13 changes: 12 additions & 1 deletion admob/src/ios/rewarded_ad_internal_ios.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
#ifndef FIREBASE_ADMOB_SRC_IOS_REWARDED_AD_INTERNAL_IOS_H_
#define FIREBASE_ADMOB_SRC_IOS_REWARDED_AD_INTERNAL_IOS_H_

#ifdef __OBJC__
#import "admob/src/ios/FADRewardedAdDelegate.h"
#endif // __OBJC__

extern "C" {
#include <objc/objc.h>
} // extern "C"
Expand All @@ -39,8 +43,15 @@ class RewardedAdInternalIOS : public RewardedAdInternal {
Future<void> Show(UserEarnedRewardListener* listener) override;
bool is_initialized() const override { return initialized_; }

#ifdef __OBJC__
void RewardedAdDidReceiveAd(GADRewardedAd* ad);
void RewardedAdDidFailToReceiveAdWithError(NSError *gad_error);
void RewardedAdWillPresentScreen();
void RewardedAdDidDismissScreen();
#endif // __OBJC__

private:
/// Prevents duplicate invocations of initailize on the Interstitial Ad.
/// Prevents duplicate invocations of initailize on the Rewarded Ad.
bool initialized_;

/// Contains information to asynchronously complete the LoadAd Future.
Expand Down
139 changes: 132 additions & 7 deletions admob/src/ios/rewarded_ad_internal_ios.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,148 @@
ad_load_callback_data_(nil), rewarded_ad_(nil),
parent_view_(nil), rewarded_ad_delegate_(nil) {}

RewardedAdInternalIOS::~RewardedAdInternalIOS() { }
RewardedAdInternalIOS::~RewardedAdInternalIOS() {
firebase::MutexLock lock(mutex_);
// Clean up any resources created in RewardedAdInternalIOS.
Mutex mutex(Mutex::kModeNonRecursive);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the model we use for other things that block until the destruction is complete? Or do we use a Semaphore?

__block Mutex *mutex_in_block = &mutex;
mutex.Acquire();
void (^destroyBlock)() = ^{
((GADRewardedAd*)rewarded_ad_).fullScreenContentDelegate = nil;
rewarded_ad_delegate_ = nil;
rewarded_ad_ = nil;
if(ad_load_callback_data_ != nil) {
delete ad_load_callback_data_;
ad_load_callback_data_ = nil;
}
mutex_in_block->Release();
};
util::DispatchAsyncSafeMainQueue(destroyBlock);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a pattern we've done before, I wonder if we should actually make a DispatchAsyncSafeMainQueueBlocking, and also add tests for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to change this in the next PR which includes some other stability fixes.

mutex.Acquire();
mutex.Release();
}

Future<void> RewardedAdInternalIOS::Initialize(AdParent parent) {
firebase::MutexLock lock(mutex_);
return CreateAndCompleteFuture(
kRewardedAdFnInitialize, kAdMobErrorNone, nullptr, &future_data_);
const SafeFutureHandle<void> future_handle =
future_data_.future_impl.SafeAlloc<void>(kRewardedAdFnInitialize);

if(initialized_) {
CompleteFuture(kAdMobErrorAlreadyInitialized,
kAdAlreadyInitializedErrorMessage, future_handle, &future_data_);
} else {
initialized_ = true;
parent_view_ = (UIView *)parent;
CompleteFuture(kAdMobErrorNone, nullptr, future_handle, &future_data_);
}
return MakeFuture(&future_data_.future_impl, future_handle);
}

Future<AdResult> RewardedAdInternalIOS::LoadAd(
const char* ad_unit_id, const AdRequest& request) {
return CreateAndCompleteFutureWithResult(
kRewardedAdFnLoadAd, kAdMobErrorNone, nullptr, &future_data_, AdResult());
firebase::MutexLock lock(mutex_);
FutureCallbackData<AdResult>* callback_data =
CreateAdResultFutureCallbackData(kRewardedAdFnLoadAd,
&future_data_);
SafeFutureHandle<AdResult> future_handle = callback_data->future_handle;

if (ad_load_callback_data_ != nil) {
CompleteLoadAdInternalResult(callback_data, kAdMobErrorLoadInProgress,
kAdLoadInProgressErrorMessage);
return MakeFuture(&future_data_.future_impl, future_handle);
}

// Persist a pointer to the callback data so that we may use it after the iOS
// SDK returns the AdResult.
ad_load_callback_data_ = callback_data;

rewarded_ad_delegate_ =
[[FADRewardedAdDelegate alloc] initWithInternalRewardedAd:this];

dispatch_async(dispatch_get_main_queue(), ^{
// Create a GADRequest from an admob::AdRequest.
AdMobError error_code = kAdMobErrorNone;
std::string error_message;
GADRequest *ad_request =
GADRequestFromCppAdRequest(request, &error_code, &error_message);
if (ad_request == nullptr) {
if (error_code == kAdMobErrorNone) {
error_code = kAdMobErrorInternalError;
error_message = kAdCouldNotParseAdRequestErrorMessage;
}
CompleteLoadAdInternalResult(ad_load_callback_data_, error_code,
error_message.c_str());
ad_load_callback_data_ = nil;
} else {
// Make the rewarded ad request.
[GADRewardedAd loadWithAdUnitID:@(ad_unit_id)
request:ad_request
completionHandler:^(GADRewardedAd *ad, NSError *error) // NO LINT
{
if (error) {
RewardedAdDidFailToReceiveAdWithError(error);
} else {
RewardedAdDidReceiveAd(ad);
}
}];
}
});

return MakeFuture(&future_data_.future_impl, future_handle);
}

Future<void> RewardedAdInternalIOS::Show(UserEarnedRewardListener* listener) {
return CreateAndCompleteFuture(
kRewardedAdFnShow, kAdMobErrorNone, nullptr, &future_data_);
firebase::MutexLock lock(mutex_);
const firebase::SafeFutureHandle<void> handle =
future_data_.future_impl.SafeAlloc<void>(kRewardedAdFnShow);
user_earned_reward_listener_ = listener;

dispatch_async(dispatch_get_main_queue(), ^{
AdMobError error_code = kAdMobErrorLoadInProgress;
const char* error_message = kAdLoadInProgressErrorMessage;
if (rewarded_ad_ == nil) {
error_code = kAdMobErrorUninitialized;
error_message = kAdUninitializedErrorMessage;
} else {
[rewarded_ad_
presentFromRootViewController:[parent_view_ window].rootViewController
userDidEarnRewardHandler:^{
GADAdReward *reward = ((GADRewardedAd*)rewarded_ad_).adReward;
NotifyListenerOfUserEarnedReward(
util::NSStringToString(reward.type),
reward.amount.integerValue);
}];
error_code = kAdMobErrorNone;
error_message = nullptr;
}
CompleteFuture(error_code, error_message, handle, &future_data_);
});
return MakeFuture(&future_data_.future_impl, handle);
}

void RewardedAdInternalIOS::RewardedAdDidReceiveAd(GADRewardedAd* ad) {
firebase::MutexLock lock(mutex_);
rewarded_ad_ = ad;
ad.fullScreenContentDelegate = rewarded_ad_delegate_;
ad.paidEventHandler = ^void(GADAdValue *_Nonnull adValue) {
NotifyListenerOfPaidEvent(
firebase::admob::ConvertGADAdValueToCppAdValue(adValue));
};

if (ad_load_callback_data_ != nil) {
CompleteLoadAdInternalResult(ad_load_callback_data_, kAdMobErrorNone,
/*error_message=*/"");
ad_load_callback_data_ = nil;
}
}

void RewardedAdInternalIOS::RewardedAdDidFailToReceiveAdWithError(NSError *gad_error) {
firebase::MutexLock lock(mutex_);
FIREBASE_ASSERT(gad_error);
if (ad_load_callback_data_ != nil) {
CompleteAdResultIOS(ad_load_callback_data_, gad_error);
ad_load_callback_data_ = nil;
}
}

} // namespace internal
Expand Down