diff --git a/packages/interactive_media_ads/CHANGELOG.md b/packages/interactive_media_ads/CHANGELOG.md index 00f06496437..5ff40f5153b 100644 --- a/packages/interactive_media_ads/CHANGELOG.md +++ b/packages/interactive_media_ads/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.2.0 + +* Adds support for pausing and resuming Ad playback. See `AdsManager.pause` and `AdsManager.resume`. +* Adds support to skip an Ad. See `AdsManager.skip` and `AdsManager.discardAdBreak`. +* **Breaking Change** To keep platform consistency, Android no longer continues playing an Ad + whenever it returns from an Ad click. Call `AdsManager.resume` to resume Ad playback. + ## 0.1.2+6 * Fixes bug where the ad would play when the app returned to foreground during content playback. diff --git a/packages/interactive_media_ads/CONTRIBUTING.md b/packages/interactive_media_ads/CONTRIBUTING.md index 7ea6e025646..e16bd92cbe5 100644 --- a/packages/interactive_media_ads/CONTRIBUTING.md +++ b/packages/interactive_media_ads/CONTRIBUTING.md @@ -112,8 +112,8 @@ platform classes that are returned by this. #### SDK Wrappers The platform implementations use Dart wrappers of their native SDKs. The SDKs are wrapped using -using the `pigeon` package. However, the code that handles generating the wrappers are still in the -process of review, so this plugin must use a git dependency in the pubspec. +using the `pigeon` package. However, the code that handles generating the wrappers for iOS is still +in the process of review, so this plugin must use a git dependency in the pubspec. The wrappers for the SDK of each platform can be updated and modified by changing the pigeon files: @@ -135,16 +135,12 @@ To update a wrapper for a platform, follow the steps: * Android: Run `flutter build apk --debug` in `example/`. * iOS: Run `flutter build ios --simulator` in `example/` -##### 2. Add the correct `pigeon` package to `dev_dependencies` in the `pubspec.yaml` and run `pub upgrade` +##### 2. Ensure the correct `pigeon` package is added to `dev_dependencies` in the `pubspec.yaml` and run `pub upgrade` Android: ```yaml -pigeon: - git: - url: git@github.com:bparrishMines/packages.git - ref: pigeon_kotlin_split - path: packages/pigeon +pigeon: ^22.2.0 ``` iOS: diff --git a/packages/interactive_media_ads/README.md b/packages/interactive_media_ads/README.md index 01324117f22..70922f1001f 100644 --- a/packages/interactive_media_ads/README.md +++ b/packages/interactive_media_ads/README.md @@ -79,7 +79,8 @@ class AdExampleWidget extends StatefulWidget { State createState() => _AdExampleWidgetState(); } -class _AdExampleWidgetState extends State { +class _AdExampleWidgetState extends State + with WidgetsBindingObserver { // IMA sample tag for a single skippable inline video ad. See more IMA sample // tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags static const String _adTagUrl = @@ -91,6 +92,7 @@ class _AdExampleWidgetState extends State { // AdsManager exposes methods to control ad playback and listen to ad events. AdsManager? _adsManager; + // ··· // Whether the widget should be displaying the content video. The content // player is hidden while Ads are playing. bool _shouldShowContentVideo = true; @@ -124,6 +126,7 @@ late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer( @override void initState() { super.initState(); + // ··· _contentVideoController = VideoPlayerController.networkUrl( Uri.parse( 'https://storage.googleapis.com/gvabox/media/samples/stock.mp4', @@ -132,8 +135,8 @@ void initState() { ..addListener(() { if (_contentVideoController.value.isCompleted) { _adsLoader.contentComplete(); - setState(() {}); } + setState(() {}); }) ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. @@ -257,7 +260,7 @@ Future _pauseContent() { ### 7. Dispose Resources -Dispose the content player and the destroy the [AdsManager][6]. +Dispose the content player and destroy the [AdsManager][6]. ```dart @@ -266,6 +269,7 @@ void dispose() { super.dispose(); _contentVideoController.dispose(); _adsManager?.destroy(); + // ··· } ``` diff --git a/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt b/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt index d2203267c47..bde68a301c9 100644 --- a/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt +++ b/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt @@ -21,7 +21,7 @@ class AdsRequestProxyApi(override val pigeonRegistrar: ProxyApiRegistrar) : * * This must match the version in pubspec.yaml. */ - const val pluginVersion = "0.1.2+6" + const val pluginVersion = "0.2.0" } override fun setAdTagUrl(pigeon_instance: AdsRequest, adTagUrl: String) { diff --git a/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/InteractiveMediaAdsLibrary.g.kt b/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/InteractiveMediaAdsLibrary.g.kt index 3ca62b9f75e..ea53d0b11d3 100644 --- a/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/InteractiveMediaAdsLibrary.g.kt +++ b/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/InteractiveMediaAdsLibrary.g.kt @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.1.0), do not edit directly. +// Autogenerated from Pigeon (v22.2.0), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass", "SyntheticAccessor") diff --git a/packages/interactive_media_ads/example/lib/main.dart b/packages/interactive_media_ads/example/lib/main.dart index 13d6efff9c3..43e507b5e96 100644 --- a/packages/interactive_media_ads/example/lib/main.dart +++ b/packages/interactive_media_ads/example/lib/main.dart @@ -32,7 +32,8 @@ class AdExampleWidget extends StatefulWidget { State createState() => _AdExampleWidgetState(); } -class _AdExampleWidgetState extends State { +class _AdExampleWidgetState extends State + with WidgetsBindingObserver { // IMA sample tag for a single skippable inline video ad. See more IMA sample // tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags static const String _adTagUrl = @@ -44,6 +45,11 @@ class _AdExampleWidgetState extends State { // AdsManager exposes methods to control ad playback and listen to ad events. AdsManager? _adsManager; + // #enddocregion example_widget + // Last state received in `didChangeAppLifecycleState`. + AppLifecycleState _lastLifecycleState = AppLifecycleState.resumed; + + // #docregion example_widget // Whether the widget should be displaying the content video. The content // player is hidden while Ads are playing. bool _shouldShowContentVideo = true; @@ -64,6 +70,11 @@ class _AdExampleWidgetState extends State { @override void initState() { super.initState(); + // #enddocregion ad_and_content_players + // Adds this instance as an observer for `AppLifecycleState` changes. + WidgetsBinding.instance.addObserver(this); + + // #docregion ad_and_content_players _contentVideoController = VideoPlayerController.networkUrl( Uri.parse( 'https://storage.googleapis.com/gvabox/media/samples/stock.mp4', @@ -72,8 +83,8 @@ class _AdExampleWidgetState extends State { ..addListener(() { if (_contentVideoController.value.isCompleted) { _adsLoader.contentComplete(); - setState(() {}); } + setState(() {}); }) ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. @@ -82,6 +93,29 @@ class _AdExampleWidgetState extends State { } // #enddocregion ad_and_content_players + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + if (!_shouldShowContentVideo) { + _adsManager?.resume(); + } + case AppLifecycleState.inactive: + // Pausing the Ad video player on Android can only be done in this state + // because it corresponds to `Activity.onPause`. This state is also + // triggered before resume, so this will only pause the Ad if the app is + // in the process of being sent to the background. + if (!_shouldShowContentVideo && + _lastLifecycleState == AppLifecycleState.resumed) { + _adsManager?.pause(); + } + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + case AppLifecycleState.detached: + } + _lastLifecycleState = state; + } + // #docregion request_ads Future _requestAds(AdDisplayContainer container) { _adsLoader = AdsLoader( @@ -146,6 +180,9 @@ class _AdExampleWidgetState extends State { super.dispose(); _contentVideoController.dispose(); _adsManager?.destroy(); + // #enddocregion dispose + WidgetsBinding.instance.removeObserver(this); + // #docregion dispose } // #enddocregion dispose diff --git a/packages/interactive_media_ads/ios/interactive_media_ads/Sources/interactive_media_ads/AdsRequestProxyAPIDelegate.swift b/packages/interactive_media_ads/ios/interactive_media_ads/Sources/interactive_media_ads/AdsRequestProxyAPIDelegate.swift index e22cdf10a14..a929dae41f5 100644 --- a/packages/interactive_media_ads/ios/interactive_media_ads/Sources/interactive_media_ads/AdsRequestProxyAPIDelegate.swift +++ b/packages/interactive_media_ads/ios/interactive_media_ads/Sources/interactive_media_ads/AdsRequestProxyAPIDelegate.swift @@ -13,7 +13,7 @@ class AdsRequestProxyAPIDelegate: PigeonApiDelegateIMAAdsRequest { /// The current version of the `interactive_media_ads` plugin. /// /// This must match the version in pubspec.yaml. - static let pluginVersion = "0.1.2+6" + static let pluginVersion = "0.2.0" func pigeonDefaultConstructor( pigeonApi: PigeonApiIMAAdsRequest, adTagUrl: String, adDisplayContainer: IMAAdDisplayContainer, diff --git a/packages/interactive_media_ads/lib/src/ads_loader.dart b/packages/interactive_media_ads/lib/src/ads_loader.dart index 4ab3a97c147..4c8c6c29d7a 100644 --- a/packages/interactive_media_ads/lib/src/ads_loader.dart +++ b/packages/interactive_media_ads/lib/src/ads_loader.dart @@ -158,6 +158,30 @@ class AdsManager { return platform.setAdsManagerDelegate(delegate.platform); } + /// Pauses the current ad. + Future pause() { + return platform.pause(); + } + + /// Resumes the current ad. + Future resume() { + return platform.resume(); + } + + /// Skips the current ad. + /// + /// This only skips ads if IMA does not render the 'Skip ad' button. + Future skip() { + return platform.skip(); + } + + /// Discards current ad break and resumes content. + /// + /// If there is no current ad then the next ad break is discarded. + Future discardAdBreak() { + return platform.discardAdBreak(); + } + /// Stops the ad and all tracking, then releases all assets that were loaded /// to play the ad. Future destroy() { diff --git a/packages/interactive_media_ads/lib/src/android/android_ad_display_container.dart b/packages/interactive_media_ads/lib/src/android/android_ad_display_container.dart index 0d057bfa12a..5ede6632c54 100644 --- a/packages/interactive_media_ads/lib/src/android/android_ad_display_container.dart +++ b/packages/interactive_media_ads/lib/src/android/android_ad_display_container.dart @@ -120,6 +120,11 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer { int? _adDuration; + // Whether MediaPlayer.start() should be called whenever the VideoView + // `onPrepared` callback is triggered. `onPrepared` is triggered whenever the + // app is resumed after being inactive. + bool _startPlayerWhenVideoIsPrepared = true; + late final AndroidAdDisplayContainerCreationParams _androidParams = params is AndroidAdDisplayContainerCreationParams ? params as AndroidAdDisplayContainerCreationParams @@ -217,10 +222,12 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer { if (container._savedAdPosition > 0) { await player.seekTo(container._savedAdPosition); } - } - await player.start(); - container?._startAdProgressTracking(); + if (container._startPlayerWhenVideoIsPrepared) { + await player.start(); + container._startAdProgressTracking(); + } + } }, onError: (_, __, ___, ____) { final AndroidAdDisplayContainer? container = weakThis.target; @@ -256,6 +263,9 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer { pauseAd: (_, __) async { final AndroidAdDisplayContainer? container = weakThis.target; if (container != null) { + // Setting this to false ensures the ad doesn't start playing if an + // app is returned to the foreground. + container._startPlayerWhenVideoIsPrepared = false; await container._mediaPlayer!.pause(); container._savedAdPosition = await container._videoView.getCurrentPosition(); @@ -263,17 +273,23 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer { } }, playAd: (_, ima.AdMediaInfo adMediaInfo) { - weakThis.target?._videoView.setVideoUri(adMediaInfo.url); + final AndroidAdDisplayContainer? container = weakThis.target; + if (container != null) { + container._startPlayerWhenVideoIsPrepared = true; + container._videoView.setVideoUri(adMediaInfo.url); + } }, release: (_) {}, stopAd: (_, __) { final AndroidAdDisplayContainer? container = weakThis.target; if (container != null) { + // Clear and reset all state. container._stopAdProgressTracking(); container._videoView.setVideoUri(null); container._clearMediaPlayer(); container._loadedAdMediaInfo = null; container._adDuration = null; + container._startPlayerWhenVideoIsPrepared = true; } }, ); diff --git a/packages/interactive_media_ads/lib/src/android/android_ads_manager.dart b/packages/interactive_media_ads/lib/src/android/android_ads_manager.dart index 0c0943c46f7..4ec075838b9 100644 --- a/packages/interactive_media_ads/lib/src/android/android_ads_manager.dart +++ b/packages/interactive_media_ads/lib/src/android/android_ads_manager.dart @@ -49,6 +49,26 @@ class AndroidAdsManager extends PlatformAdsManager { return _manager.start(); } + @override + Future discardAdBreak() { + return _manager.discardAdBreak(); + } + + @override + Future pause() { + return _manager.pause(); + } + + @override + Future resume() { + return _manager.resume(); + } + + @override + Future skip() { + return _manager.skip(); + } + // This value is created in a static method because the callback methods for // any wrapped classes must not reference the encapsulating object. This is to // prevent a circular reference that prevents garbage collection. diff --git a/packages/interactive_media_ads/lib/src/android/interactive_media_ads.g.dart b/packages/interactive_media_ads/lib/src/android/interactive_media_ads.g.dart index 271ced40aad..eb6f9516673 100644 --- a/packages/interactive_media_ads/lib/src/android/interactive_media_ads.g.dart +++ b/packages/interactive_media_ads/lib/src/android/interactive_media_ads.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.1.0), do not edit directly. +// Autogenerated from Pigeon (v22.2.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers diff --git a/packages/interactive_media_ads/lib/src/ios/ios_ads_manager.dart b/packages/interactive_media_ads/lib/src/ios/ios_ads_manager.dart index 595ccf57306..0c51bf926f1 100644 --- a/packages/interactive_media_ads/lib/src/ios/ios_ads_manager.dart +++ b/packages/interactive_media_ads/lib/src/ios/ios_ads_manager.dart @@ -48,4 +48,24 @@ class IOSAdsManager extends PlatformAdsManager { Future start(AdsManagerStartParams params) { return _manager.start(); } + + @override + Future discardAdBreak() { + return _manager.discardAdBreak(); + } + + @override + Future pause() { + return _manager.pause(); + } + + @override + Future resume() { + return _manager.resume(); + } + + @override + Future skip() { + return _manager.skip(); + } } diff --git a/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_manager.dart b/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_manager.dart index 85488aaa251..0f3d7a7ea46 100644 --- a/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_manager.dart +++ b/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_manager.dart @@ -28,6 +28,22 @@ abstract class PlatformAdsManager { /// /// The [AdsManagerDelegate] to notify with events during ad playback. Future setAdsManagerDelegate(PlatformAdsManagerDelegate delegate); + /// Pauses the current ad. + Future pause(); + + /// Resumes the current ad. + Future resume(); + + /// Skips the current ad. + /// + /// This only skips ads if IMA does not render the 'Skip ad' button. + Future skip(); + + /// Discards current ad break and resumes content. + /// + /// If there is no current ad then the next ad break is discarded. + Future discardAdBreak(); + /// Stops the ad and all tracking, then releases all assets that were loaded /// to play the ad. Future destroy(); diff --git a/packages/interactive_media_ads/pigeons/interactive_media_ads_android.dart b/packages/interactive_media_ads/pigeons/interactive_media_ads_android.dart index be495a8cefc..d2dff23dddc 100644 --- a/packages/interactive_media_ads/pigeons/interactive_media_ads_android.dart +++ b/packages/interactive_media_ads/pigeons/interactive_media_ads_android.dart @@ -6,7 +6,7 @@ // https://github.com/flutter/packages/pull/6371 lands. This file uses the // Kotlin ProxyApi feature from pigeon. // ignore_for_file: avoid_unused_constructor_parameters -/* + import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( @@ -738,4 +738,3 @@ abstract class AdEventListener { /// Respond to an occurrence of an AdEvent. late final void Function(AdEvent event) onAdEvent; } -*/ diff --git a/packages/interactive_media_ads/pubspec.yaml b/packages/interactive_media_ads/pubspec.yaml index 749ff8ddaa9..e1f1634d5cb 100644 --- a/packages/interactive_media_ads/pubspec.yaml +++ b/packages/interactive_media_ads/pubspec.yaml @@ -2,7 +2,7 @@ name: interactive_media_ads description: A Flutter plugin for using the Interactive Media Ads SDKs on Android and iOS. repository: https://github.com/flutter/packages/tree/main/packages/interactive_media_ads issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+interactive_media_ads%22 -version: 0.1.2+6 # This must match the version in +version: 0.2.0 # This must match the version in # `android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt` and # `ios/interactive_media_ads/Sources/interactive_media_ads/AdsRequestProxyAPIDelegate.swift` @@ -31,6 +31,7 @@ dev_dependencies: flutter_test: sdk: flutter mockito: 5.4.4 + pigeon: ^22.2.0 topics: - ads diff --git a/packages/interactive_media_ads/test/ads_manager_test.dart b/packages/interactive_media_ads/test/ads_manager_test.dart index cb5f42ccb3f..eaa5994b626 100644 --- a/packages/interactive_media_ads/test/ads_manager_test.dart +++ b/packages/interactive_media_ads/test/ads_manager_test.dart @@ -41,6 +41,42 @@ void main() { )); }); + test('discardAdBreak', () async { + final TestAdsManager platformManager = TestAdsManager( + onDiscardAdBreak: expectAsync0(() async {}), + ); + + final AdsManager manager = createAdsManager(platformManager); + await manager.discardAdBreak(); + }); + + test('pause', () async { + final TestAdsManager platformManager = TestAdsManager( + onPause: expectAsync0(() async {}), + ); + + final AdsManager manager = createAdsManager(platformManager); + await manager.pause(); + }); + + test('resume', () async { + final TestAdsManager platformManager = TestAdsManager( + onResume: expectAsync0(() async {}), + ); + + final AdsManager manager = createAdsManager(platformManager); + await manager.resume(); + }); + + test('skip', () async { + final TestAdsManager platformManager = TestAdsManager( + onSkip: expectAsync0(() async {}), + ); + + final AdsManager manager = createAdsManager(platformManager); + await manager.skip(); + }); + test('destroy', () async { final TestAdsManager platformManager = TestAdsManager( onDestroy: expectAsync0(() async {}), diff --git a/packages/interactive_media_ads/test/android/ad_display_container_test.dart b/packages/interactive_media_ads/test/android/ad_display_container_test.dart index 4071d041ac3..4aecc405eb9 100644 --- a/packages/interactive_media_ads/test/android/ad_display_container_test.dart +++ b/packages/interactive_media_ads/test/android/ad_display_container_test.dart @@ -464,6 +464,115 @@ void main() { verify(mockMediaPlayer.pause()); }); + test('ad does not play automatically after calling pause', () async { + late final void Function( + ima.VideoAdPlayer, + ima.AdMediaInfo, + ima.AdPodInfo, + ) loadAdCallback; + + late final Future Function( + ima.VideoView, + ima.MediaPlayer, + ) onPreparedCallback; + + late final Future Function( + ima.VideoAdPlayer, + ima.AdMediaInfo, + ) pauseAdCallback; + + late final void Function( + ima.VideoAdPlayer, + ima.AdMediaInfo, + ) playAdCallback; + + final InteractiveMediaAdsProxy imaProxy = InteractiveMediaAdsProxy( + newFrameLayout: () => MockFrameLayout(), + newVideoView: ({ + dynamic onError, + void Function( + ima.VideoView, + ima.MediaPlayer, + )? onPrepared, + dynamic onCompletion, + }) { + // VideoView.onPrepared returns void, but the implementation uses an + // async callback method. + onPreparedCallback = onPrepared! as Future Function( + ima.VideoView, + ima.MediaPlayer, + ); + final MockVideoView mockVideoView = MockVideoView(); + when(mockVideoView.getCurrentPosition()).thenAnswer((_) async => 10); + return mockVideoView; + }, + createAdDisplayContainerImaSdkFactory: (_, __) async { + return MockAdDisplayContainer(); + }, + newVideoAdPlayer: ({ + required dynamic addCallback, + required void Function( + ima.VideoAdPlayer, + ima.AdMediaInfo, + ima.AdPodInfo, + ) loadAd, + required dynamic pauseAd, + required void Function( + ima.VideoAdPlayer, + ima.AdMediaInfo, + ) playAd, + required dynamic release, + required dynamic removeCallback, + required dynamic stopAd, + }) { + loadAdCallback = loadAd; + // VideoAdPlayer.pauseAd returns void, but the implementation uses an + // async callback method. + pauseAdCallback = pauseAd as Future Function( + ima.VideoAdPlayer, + ima.AdMediaInfo, + ); + playAdCallback = playAd; + return MockVideoAdPlayer(); + }, + newVideoProgressUpdate: ({ + required int currentTimeMs, + required int durationMs, + }) { + return MockVideoProgressUpdate(); + }, + ); + + AndroidAdDisplayContainer( + AndroidAdDisplayContainerCreationParams( + onContainerAdded: (_) {}, + imaProxy: imaProxy, + ), + ); + + final ima.AdMediaInfo mockAdMediaInfo = MockAdMediaInfo(); + when(mockAdMediaInfo.url).thenReturn('url'); + loadAdCallback(MockVideoAdPlayer(), mockAdMediaInfo, MockAdPodInfo()); + + final MockMediaPlayer mockMediaPlayer = MockMediaPlayer(); + when(mockMediaPlayer.getDuration()).thenAnswer((_) async => 100); + + await onPreparedCallback(MockVideoView(), mockMediaPlayer); + + // Pausing the ad prevents Ad from starting again automatically when it is + // prepared. + await pauseAdCallback(MockVideoAdPlayer(), mockAdMediaInfo); + reset(mockMediaPlayer); + await onPreparedCallback(MockVideoView(), mockMediaPlayer); + verifyNever(mockMediaPlayer.start()); + + // The playAd callback allows the Ad to start automatically once it is + // prepared. + playAdCallback(MockVideoAdPlayer(), mockAdMediaInfo); + await onPreparedCallback(MockVideoView(), mockMediaPlayer); + verify(mockMediaPlayer.start()); + }); + test('play ad', () async { late final void Function( ima.VideoAdPlayer, diff --git a/packages/interactive_media_ads/test/android/ads_loader_test.mocks.dart b/packages/interactive_media_ads/test/android/ads_loader_test.mocks.dart index bdcfc1f4d54..a56865191d6 100644 --- a/packages/interactive_media_ads/test/android/ads_loader_test.mocks.dart +++ b/packages/interactive_media_ads/test/android/ads_loader_test.mocks.dart @@ -569,6 +569,36 @@ class MockAdsManager extends _i1.Mock implements _i2.AdsManager { returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); + @override + _i6.Future> getAdCuePoints() => (super.noSuchMethod( + Invocation.method( + #getAdCuePoints, + [], + ), + returnValue: _i6.Future>.value([]), + returnValueForMissingStub: _i6.Future>.value([]), + ) as _i6.Future>); + + @override + _i6.Future resume() => (super.noSuchMethod( + Invocation.method( + #resume, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future skip() => (super.noSuchMethod( + Invocation.method( + #skip, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override _i2.AdsManager pigeon_copy() => (super.noSuchMethod( Invocation.method( diff --git a/packages/interactive_media_ads/test/android/ads_manager_test.dart b/packages/interactive_media_ads/test/android/ads_manager_test.dart index 3d9a9a5ffa8..bfcbaeec676 100644 --- a/packages/interactive_media_ads/test/android/ads_manager_test.dart +++ b/packages/interactive_media_ads/test/android/ads_manager_test.dart @@ -48,6 +48,38 @@ void main() { verify(mockAdsManager.start()); }); + test('discardAdBreak', () { + final MockAdsManager mockAdsManager = MockAdsManager(); + final AndroidAdsManager adsManager = AndroidAdsManager(mockAdsManager); + adsManager.discardAdBreak(); + + verify(mockAdsManager.discardAdBreak()); + }); + + test('pause', () { + final MockAdsManager mockAdsManager = MockAdsManager(); + final AndroidAdsManager adsManager = AndroidAdsManager(mockAdsManager); + adsManager.pause(); + + verify(mockAdsManager.pause()); + }); + + test('skip', () { + final MockAdsManager mockAdsManager = MockAdsManager(); + final AndroidAdsManager adsManager = AndroidAdsManager(mockAdsManager); + adsManager.skip(); + + verify(mockAdsManager.skip()); + }); + + test('resume', () { + final MockAdsManager mockAdsManager = MockAdsManager(); + final AndroidAdsManager adsManager = AndroidAdsManager(mockAdsManager); + adsManager.resume(); + + verify(mockAdsManager.resume()); + }); + test('onAdEvent', () async { final MockAdsManager mockAdsManager = MockAdsManager(); diff --git a/packages/interactive_media_ads/test/android/ads_manager_test.mocks.dart b/packages/interactive_media_ads/test/android/ads_manager_test.mocks.dart index 57069cf78e1..5a70ecf2c71 100644 --- a/packages/interactive_media_ads/test/android/ads_manager_test.mocks.dart +++ b/packages/interactive_media_ads/test/android/ads_manager_test.mocks.dart @@ -435,6 +435,36 @@ class MockAdsManager extends _i1.Mock implements _i2.AdsManager { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override + _i4.Future> getAdCuePoints() => (super.noSuchMethod( + Invocation.method( + #getAdCuePoints, + [], + ), + returnValue: _i4.Future>.value([]), + returnValueForMissingStub: _i4.Future>.value([]), + ) as _i4.Future>); + + @override + _i4.Future resume() => (super.noSuchMethod( + Invocation.method( + #resume, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future skip() => (super.noSuchMethod( + Invocation.method( + #skip, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override _i2.AdsManager pigeon_copy() => (super.noSuchMethod( Invocation.method( diff --git a/packages/interactive_media_ads/test/ios/ads_manager_test.dart b/packages/interactive_media_ads/test/ios/ads_manager_test.dart index 8c9c6d3a35d..06d1e1dab51 100644 --- a/packages/interactive_media_ads/test/ios/ads_manager_test.dart +++ b/packages/interactive_media_ads/test/ios/ads_manager_test.dart @@ -41,6 +41,38 @@ void main() { verify(mockAdsManager.start()); }); + test('discardAdBreak', () { + final MockIMAAdsManager mockAdsManager = MockIMAAdsManager(); + final IOSAdsManager adsManager = IOSAdsManager(mockAdsManager); + adsManager.discardAdBreak(); + + verify(mockAdsManager.discardAdBreak()); + }); + + test('pause', () { + final MockIMAAdsManager mockAdsManager = MockIMAAdsManager(); + final IOSAdsManager adsManager = IOSAdsManager(mockAdsManager); + adsManager.pause(); + + verify(mockAdsManager.pause()); + }); + + test('skip', () { + final MockIMAAdsManager mockAdsManager = MockIMAAdsManager(); + final IOSAdsManager adsManager = IOSAdsManager(mockAdsManager); + adsManager.skip(); + + verify(mockAdsManager.skip()); + }); + + test('resume', () { + final MockIMAAdsManager mockAdsManager = MockIMAAdsManager(); + final IOSAdsManager adsManager = IOSAdsManager(mockAdsManager); + adsManager.resume(); + + verify(mockAdsManager.resume()); + }); + test('setAdsManagerDelegate', () { final MockIMAAdsManager mockAdsManager = MockIMAAdsManager(); final IOSAdsManager adsManager = IOSAdsManager(mockAdsManager); diff --git a/packages/interactive_media_ads/test/test_stubs.dart b/packages/interactive_media_ads/test/test_stubs.dart index fd59fb3073f..628c46d547b 100644 --- a/packages/interactive_media_ads/test/test_stubs.dart +++ b/packages/interactive_media_ads/test/test_stubs.dart @@ -92,6 +92,10 @@ class TestAdsManager extends PlatformAdsManager { this.onSetAdsManagerDelegate, this.onStart, this.onDestroy, + this.onDiscardAdBreak, + this.onPause, + this.onResume, + this.onSkip, }); Future Function(AdsManagerInitParams params)? onInit; @@ -101,6 +105,14 @@ class TestAdsManager extends PlatformAdsManager { Future Function(AdsManagerStartParams params)? onStart; + Future Function()? onDiscardAdBreak; + + Future Function()? onPause; + + Future Function()? onResume; + + Future Function()? onSkip; + Future Function()? onDestroy; @override @@ -124,4 +136,24 @@ class TestAdsManager extends PlatformAdsManager { Future destroy() async { return onDestroy?.call(); } + + @override + Future discardAdBreak() async { + return onDiscardAdBreak?.call(); + } + + @override + Future pause() async { + return onPause?.call(); + } + + @override + Future resume() async { + return onResume?.call(); + } + + @override + Future skip() async { + return onSkip?.call(); + } }