diff --git a/packages/interactive_media_ads/CHANGELOG.md b/packages/interactive_media_ads/CHANGELOG.md index 211bf9ea278e..317d9079954b 100644 --- a/packages/interactive_media_ads/CHANGELOG.md +++ b/packages/interactive_media_ads/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.2 + +* Adds support for mid-roll ads. See `AdsRequest.contentProgressProvider`. + ## 0.2.1 * Adds internal wrapper for Android native `ContentProgressProvider`. diff --git a/packages/interactive_media_ads/README.md b/packages/interactive_media_ads/README.md index 70922f1001f1..74441a71def5 100644 --- a/packages/interactive_media_ads/README.md +++ b/packages/interactive_media_ads/README.md @@ -81,10 +81,10 @@ class AdExampleWidget extends StatefulWidget { class _AdExampleWidgetState extends State with WidgetsBindingObserver { - // IMA sample tag for a single skippable inline video ad. See more IMA sample + // IMA sample tag for a pre-, mid-, and post-roll, single 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 = - 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator='; + 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpost&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator='; // The AdsLoader instance exposes the request ads method. late final AdsLoader _adsLoader; @@ -99,6 +99,15 @@ class _AdExampleWidgetState extends State // Controls the content video player. late final VideoPlayerController _contentVideoController; + + // Periodically updates the SDK of the current playback progress of the + // content video. + Timer? _contentProgressTimer; + + // Provides the SDK with the current playback progress of the content video. + // This is required to support mid-roll ads. + final ContentProgressProvider _contentProgressProvider = + ContentProgressProvider(); // ··· @override Widget build(BuildContext context) { @@ -240,20 +249,43 @@ Future _requestAds(AdDisplayContainer container) { }, ); - return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl)); + return _adsLoader.requestAds(AdsRequest( + adTagUrl: _adTagUrl, + contentProgressProvider: _contentProgressProvider, + )); } -Future _resumeContent() { +Future _resumeContent() async { setState(() { _shouldShowContentVideo = true; }); - return _contentVideoController.play(); + + if (_adsManager != null) { + _contentProgressTimer = Timer.periodic( + const Duration(milliseconds: 200), + (Timer timer) async { + if (_contentVideoController.value.isInitialized) { + final Duration? progress = await _contentVideoController.position; + if (progress != null) { + await _contentProgressProvider.setProgress( + progress: progress, + duration: _contentVideoController.value.duration, + ); + } + } + }, + ); + } + + await _contentVideoController.play(); } Future _pauseContent() { setState(() { _shouldShowContentVideo = false; }); + _contentProgressTimer?.cancel(); + _contentProgressTimer = null; return _contentVideoController.pause(); } ``` @@ -267,6 +299,7 @@ Dispose the content player and destroy the [AdsManager][6]. @override void dispose() { super.dispose(); + _contentProgressTimer?.cancel(); _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 4f07af1d8142..fda8eb07e9bd 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.2.1" + const val pluginVersion = "0.2.2" } override fun setAdTagUrl(pigeon_instance: AdsRequest, adTagUrl: String) { diff --git a/packages/interactive_media_ads/example/lib/main.dart b/packages/interactive_media_ads/example/lib/main.dart index 43e507b5e96e..9bafdda819e9 100644 --- a/packages/interactive_media_ads/example/lib/main.dart +++ b/packages/interactive_media_ads/example/lib/main.dart @@ -34,10 +34,10 @@ class AdExampleWidget extends StatefulWidget { class _AdExampleWidgetState extends State with WidgetsBindingObserver { - // IMA sample tag for a single skippable inline video ad. See more IMA sample + // IMA sample tag for a pre-, mid-, and post-roll, single 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 = - 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator='; + 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpost&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator='; // The AdsLoader instance exposes the request ads method. late final AdsLoader _adsLoader; @@ -56,6 +56,15 @@ class _AdExampleWidgetState extends State // Controls the content video player. late final VideoPlayerController _contentVideoController; + + // Periodically updates the SDK of the current playback progress of the + // content video. + Timer? _contentProgressTimer; + + // Provides the SDK with the current playback progress of the content video. + // This is required to support mid-roll ads. + final ContentProgressProvider _contentProgressProvider = + ContentProgressProvider(); // #enddocregion example_widget // #docregion ad_and_content_players @@ -156,20 +165,43 @@ class _AdExampleWidgetState extends State }, ); - return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl)); + return _adsLoader.requestAds(AdsRequest( + adTagUrl: _adTagUrl, + contentProgressProvider: _contentProgressProvider, + )); } - Future _resumeContent() { + Future _resumeContent() async { setState(() { _shouldShowContentVideo = true; }); - return _contentVideoController.play(); + + if (_adsManager != null) { + _contentProgressTimer = Timer.periodic( + const Duration(milliseconds: 200), + (Timer timer) async { + if (_contentVideoController.value.isInitialized) { + final Duration? progress = await _contentVideoController.position; + if (progress != null) { + await _contentProgressProvider.setProgress( + progress: progress, + duration: _contentVideoController.value.duration, + ); + } + } + }, + ); + } + + await _contentVideoController.play(); } Future _pauseContent() { setState(() { _shouldShowContentVideo = false; }); + _contentProgressTimer?.cancel(); + _contentProgressTimer = null; return _contentVideoController.pause(); } // #enddocregion request_ads @@ -178,6 +210,7 @@ class _AdExampleWidgetState extends State @override void dispose() { super.dispose(); + _contentProgressTimer?.cancel(); _contentVideoController.dispose(); _adsManager?.destroy(); // #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 4ecd85c58c2f..051f7540aa8f 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.2.1" + static let pluginVersion = "0.2.2" func pigeonDefaultConstructor( pigeonApi: PigeonApiIMAAdsRequest, adTagUrl: String, adDisplayContainer: IMAAdDisplayContainer, diff --git a/packages/interactive_media_ads/lib/interactive_media_ads.dart b/packages/interactive_media_ads/lib/interactive_media_ads.dart index 954a27c9049a..06c180248410 100644 --- a/packages/interactive_media_ads/lib/interactive_media_ads.dart +++ b/packages/interactive_media_ads/lib/interactive_media_ads.dart @@ -5,8 +5,10 @@ export 'src/ad_display_container.dart'; export 'src/ads_loader.dart'; export 'src/ads_manager_delegate.dart'; +export 'src/ads_request.dart'; export 'src/android/android_interactive_media_ads.dart' show AndroidInteractiveMediaAds; +export 'src/content_progress_provider.dart'; export 'src/ios/ios_interactive_media_ads.dart' show IOSInteractiveMediaAds; export 'src/platform_interface/platform_interface.dart' show @@ -16,5 +18,4 @@ export 'src/platform_interface/platform_interface.dart' AdErrorType, AdEvent, AdEventType, - AdsLoadErrorData, - AdsRequest; + AdsLoadErrorData; diff --git a/packages/interactive_media_ads/lib/src/ads_loader.dart b/packages/interactive_media_ads/lib/src/ads_loader.dart index 4c8c6c29d7a4..9863d4c01293 100644 --- a/packages/interactive_media_ads/lib/src/ads_loader.dart +++ b/packages/interactive_media_ads/lib/src/ads_loader.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'ad_display_container.dart'; import 'ads_manager_delegate.dart'; +import 'ads_request.dart'; import 'platform_interface/platform_interface.dart'; /// Allows publishers to request ads from ad servers or a dynamic ad insertion @@ -97,7 +98,7 @@ class AdsLoader { /// Ads cannot be requested until the `AdDisplayContainer` has been added to /// the native View hierarchy. See [AdDisplayContainer.onContainerAdded]. Future requestAds(AdsRequest request) { - return platform.requestAds(request); + return platform.requestAds(request.platform); } } diff --git a/packages/interactive_media_ads/lib/src/ads_request.dart b/packages/interactive_media_ads/lib/src/ads_request.dart new file mode 100644 index 000000000000..ecc8e2f2ccc5 --- /dev/null +++ b/packages/interactive_media_ads/lib/src/ads_request.dart @@ -0,0 +1,37 @@ +// 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. + +import 'content_progress_provider.dart'; +import 'platform_interface/platform_interface.dart'; + +/// An object containing the data used to request ads from the server. +class AdsRequest { + /// Creates an [AdsRequest]. + AdsRequest({ + required String adTagUrl, + ContentProgressProvider? contentProgressProvider, + }) : this.fromPlatform( + PlatformAdsRequest( + adTagUrl: adTagUrl, + contentProgressProvider: contentProgressProvider?.platform, + ), + ); + + /// Constructs an [AdsRequest] from a specific platform implementation. + AdsRequest.fromPlatform(this.platform); + + /// Implementation of [PlatformAdsRequest] for the current platform. + final PlatformAdsRequest platform; + + /// The URL from which ads will be requested. + String get adTagUrl => platform.adTagUrl; + + /// A [ContentProgressProvider] instance to allow scheduling of ad breaks + /// based on content progress (cue points). + ContentProgressProvider? get contentProgressProvider => platform + .contentProgressProvider != + null + ? ContentProgressProvider.fromPlatform(platform.contentProgressProvider!) + : null; +} diff --git a/packages/interactive_media_ads/lib/src/android/android_ads_loader.dart b/packages/interactive_media_ads/lib/src/android/android_ads_loader.dart index 58fe19c0bdc8..2ccbd4b62e04 100644 --- a/packages/interactive_media_ads/lib/src/android/android_ads_loader.dart +++ b/packages/interactive_media_ads/lib/src/android/android_ads_loader.dart @@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart'; import '../platform_interface/platform_interface.dart'; import 'android_ad_display_container.dart'; import 'android_ads_manager.dart'; +import 'android_content_progress_provider.dart'; import 'enum_converter_utils.dart'; import 'interactive_media_ads.g.dart' as ima; import 'interactive_media_ads_proxy.dart'; @@ -79,13 +80,18 @@ base class AndroidAdsLoader extends PlatformAdsLoader { } @override - Future requestAds(AdsRequest request) async { + Future requestAds(PlatformAdsRequest request) async { final ima.AdsLoader adsLoader = await _adsLoaderFuture; final ima.AdsRequest androidRequest = await _sdkFactory.createAdsRequest(); await Future.wait(>[ androidRequest.setAdTagUrl(request.adTagUrl), + if (request.contentProgressProvider != null) + androidRequest.setContentProgressProvider( + (request.contentProgressProvider! as AndroidContentProgressProvider) + .progressProvider, + ), adsLoader.requestAds(androidRequest), ]); } diff --git a/packages/interactive_media_ads/lib/src/android/android_content_progress_provider.dart b/packages/interactive_media_ads/lib/src/android/android_content_progress_provider.dart new file mode 100644 index 000000000000..e3f4d57c2955 --- /dev/null +++ b/packages/interactive_media_ads/lib/src/android/android_content_progress_provider.dart @@ -0,0 +1,70 @@ +// 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. + +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../platform_interface/platform_content_progress_provider.dart'; +import 'interactive_media_ads.g.dart' as ima; +import 'interactive_media_ads_proxy.dart'; + +/// Android implementation of [PlatformContentProgressProviderCreationParams]. +final class AndroidContentProgressProviderCreationParams + extends PlatformContentProgressProviderCreationParams { + /// Constructs a [AndroidContentProgressProviderCreationParams]. + const AndroidContentProgressProviderCreationParams({ + @visibleForTesting InteractiveMediaAdsProxy? proxy, + }) : _proxy = proxy ?? const InteractiveMediaAdsProxy(), + super(); + + /// Creates a [AndroidContentProgressProviderCreationParams] from an instance of + /// [PlatformContentProgressProviderCreationParams]. + factory AndroidContentProgressProviderCreationParams.fromPlatformContentProgressProviderCreationParams( + // Placeholder to prevent requiring a breaking change if params are added to + // PlatformContentProgressProviderCreationParams. + // ignore: avoid_unused_constructor_parameters + PlatformContentProgressProviderCreationParams params, { + @visibleForTesting InteractiveMediaAdsProxy? proxy, + }) { + return AndroidContentProgressProviderCreationParams(proxy: proxy); + } + + final InteractiveMediaAdsProxy _proxy; +} + +/// Android implementation of [PlatformContentProgressProvider]. +base class AndroidContentProgressProvider + extends PlatformContentProgressProvider { + /// Constructs an [AndroidContentProgressProvider]. + AndroidContentProgressProvider(super.params) : super.implementation(); + + /// The native Android ContentProgressProvider. + /// + /// This allows the SDK to track progress of the content video. + @internal + late final ima.ContentProgressProvider progressProvider = + _androidParams._proxy.newContentProgressProvider(); + + late final AndroidContentProgressProviderCreationParams _androidParams = + params is AndroidContentProgressProviderCreationParams + ? params as AndroidContentProgressProviderCreationParams + : AndroidContentProgressProviderCreationParams + .fromPlatformContentProgressProviderCreationParams( + params, + ); + + @override + Future setProgress({ + required Duration progress, + required Duration duration, + }) async { + return progressProvider.setContentProgress( + _androidParams._proxy.newVideoProgressUpdate( + currentTimeMs: progress.inMilliseconds, + durationMs: duration.inMilliseconds, + ), + ); + } +} diff --git a/packages/interactive_media_ads/lib/src/android/android_interactive_media_ads.dart b/packages/interactive_media_ads/lib/src/android/android_interactive_media_ads.dart index 2406520d5531..0f05362a6919 100644 --- a/packages/interactive_media_ads/lib/src/android/android_interactive_media_ads.dart +++ b/packages/interactive_media_ads/lib/src/android/android_interactive_media_ads.dart @@ -6,9 +6,11 @@ import '../platform_interface/interactive_media_ads_platform.dart'; import '../platform_interface/platform_ad_display_container.dart'; import '../platform_interface/platform_ads_loader.dart'; import '../platform_interface/platform_ads_manager_delegate.dart'; +import '../platform_interface/platform_content_progress_provider.dart'; import 'android_ad_display_container.dart'; import 'android_ads_loader.dart'; import 'android_ads_manager_delegate.dart'; +import 'android_content_progress_provider.dart'; /// Android implementation of [InteractiveMediaAdsPlatform]. final class AndroidInteractiveMediaAds extends InteractiveMediaAdsPlatform { @@ -37,4 +39,11 @@ final class AndroidInteractiveMediaAds extends InteractiveMediaAdsPlatform { ) { return AndroidAdsManagerDelegate(params); } + + @override + PlatformContentProgressProvider createPlatformContentProgressProvider( + PlatformContentProgressProviderCreationParams params, + ) { + return AndroidContentProgressProvider(params); + } } diff --git a/packages/interactive_media_ads/lib/src/android/interactive_media_ads_proxy.dart b/packages/interactive_media_ads/lib/src/android/interactive_media_ads_proxy.dart index eff593541369..7aa120654e84 100644 --- a/packages/interactive_media_ads/lib/src/android/interactive_media_ads_proxy.dart +++ b/packages/interactive_media_ads/lib/src/android/interactive_media_ads_proxy.dart @@ -16,6 +16,7 @@ import 'interactive_media_ads.g.dart'; class InteractiveMediaAdsProxy { /// Constructs an [InteractiveMediaAdsProxy]. const InteractiveMediaAdsProxy({ + this.newContentProgressProvider = ContentProgressProvider.new, this.newVideoProgressUpdate = VideoProgressUpdate.new, this.newFrameLayout = FrameLayout.new, this.newVideoView = VideoView.new, @@ -30,6 +31,9 @@ class InteractiveMediaAdsProxy { _videoTimeNotReadyVideoProgressUpdate, }); + /// Constructs [ContentProgressProvider]. + final ContentProgressProvider Function() newContentProgressProvider; + /// Constructs [VideoProgressUpdate]. final VideoProgressUpdate Function({ required int currentTimeMs, diff --git a/packages/interactive_media_ads/lib/src/content_progress_provider.dart b/packages/interactive_media_ads/lib/src/content_progress_provider.dart new file mode 100644 index 000000000000..0b0f0e6df8b5 --- /dev/null +++ b/packages/interactive_media_ads/lib/src/content_progress_provider.dart @@ -0,0 +1,92 @@ +// 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. + +import 'platform_interface/platform_interface.dart'; + +/// Allow the SDK to track progress of the content video. +/// +/// Provides updates required to enable triggering ads at configured cue points. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro interactive_media_ads.ContentProgressProvider.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final ContentProgressProvider provider = ContentProgressProvider(); +/// +/// if (InteractiveMediaAdsPlatform.instance is IOSInteractiveMediaAdsPlatform) { +/// final IOSContentProgressProvider iosProvider = +/// provider.platform as IOSContentProgressProvider; +/// } else if (InteractiveMediaAdsPlatform.instance is AndroidInteractiveMediaAdsPlatform) { +/// final AndroidContentProgressProvider androidProvider = +/// provider.platform as AndroidContentProgressProvider; +/// } +/// ``` +class ContentProgressProvider { + /// Constructs an [ContentProgressProvider]. + /// + /// See [ContentProgressProvider.fromPlatformCreationParams] for setting + /// parameters for a specific platform. + ContentProgressProvider() + : this.fromPlatformCreationParams( + const PlatformContentProgressProviderCreationParams(), + ); + + /// Constructs an [ContentProgressProvider] from creation params for a + /// specific platform. + /// + /// {@template interactive_media_ads.ContentProgressProvider.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// PlatformContentProgressProviderCreationParams params = + /// const PlatformContentProgressProviderCreationParams(); + /// + /// if (InteractiveMediaAdsPlatform.instance is IOSInteractiveMediaAdsPlatform) { + /// params = IOSContentProgressProviderCreationParams + /// .fromPlatformContentProgressProviderCreationParams( + /// params, + /// ); + /// } else if (InteractiveMediaAdsPlatform.instance is AndroidInteractiveMediaAdsPlatform) { + /// params = AndroidContentProgressProviderCreationParams + /// .fromPlatformContentProgressProviderCreationParams( + /// params, + /// ); + /// } + /// + /// final ContentProgressProvider provider = ContentProgressProvider.fromPlatformCreationParams( + /// params, + /// ); + /// ``` + /// {@endtemplate} + ContentProgressProvider.fromPlatformCreationParams( + PlatformContentProgressProviderCreationParams params, + ) : this.fromPlatform(PlatformContentProgressProvider(params)); + + /// Constructs a [ContentProgressProvider] from a specific platform + /// implementation. + ContentProgressProvider.fromPlatform(this.platform); + + /// Implementation of [PlatformContentProgressProvider] for the current + /// platform. + final PlatformContentProgressProvider platform; + + /// Sends an update on the progress of the content video. + /// + /// When using a `Timer` to periodically send updates through this method, an + /// interval of 200ms is recommended. + Future setProgress({ + required Duration progress, + required Duration duration, + }) { + return platform.setProgress(progress: progress, duration: duration); + } +} diff --git a/packages/interactive_media_ads/lib/src/ios/interactive_media_ads_proxy.dart b/packages/interactive_media_ads/lib/src/ios/interactive_media_ads_proxy.dart index 29600d0b617c..97d1c5166d78 100644 --- a/packages/interactive_media_ads/lib/src/ios/interactive_media_ads_proxy.dart +++ b/packages/interactive_media_ads/lib/src/ios/interactive_media_ads_proxy.dart @@ -15,6 +15,7 @@ class InteractiveMediaAdsProxy { const InteractiveMediaAdsProxy({ this.newIMAAdDisplayContainer = IMAAdDisplayContainer.new, this.newUIViewController = UIViewController.new, + this.newIMAContentPlayhead = IMAContentPlayhead.new, this.newIMAAdsLoader = IMAAdsLoader.new, this.newIMAAdsRequest = IMAAdsRequest.new, this.newIMAAdsLoaderDelegate = IMAAdsLoaderDelegate.new, @@ -33,6 +34,9 @@ class InteractiveMediaAdsProxy { void Function(UIViewController, bool)? viewDidAppear, }) newUIViewController; + /// Constructs [IMAContentPlayhead]. + final IMAContentPlayhead Function() newIMAContentPlayhead; + /// Constructs [IMAAdsLoader]. final IMAAdsLoader Function({IMASettings? settings}) newIMAAdsLoader; diff --git a/packages/interactive_media_ads/lib/src/ios/ios_ads_loader.dart b/packages/interactive_media_ads/lib/src/ios/ios_ads_loader.dart index d0f68646570d..606cfb6c835e 100644 --- a/packages/interactive_media_ads/lib/src/ios/ios_ads_loader.dart +++ b/packages/interactive_media_ads/lib/src/ios/ios_ads_loader.dart @@ -12,6 +12,7 @@ import 'interactive_media_ads.g.dart'; import 'interactive_media_ads_proxy.dart'; import 'ios_ad_display_container.dart'; import 'ios_ads_manager.dart'; +import 'ios_content_progress_provider.dart'; /// Implementation of [PlatformAdsLoaderCreationParams] for iOS. final class IOSAdsLoaderCreationParams extends PlatformAdsLoaderCreationParams { @@ -72,11 +73,15 @@ base class IOSAdsLoader extends PlatformAdsLoader { } @override - Future requestAds(AdsRequest request) async { + Future requestAds(PlatformAdsRequest request) async { return _adsLoader.requestAds(_iosParams._proxy.newIMAAdsRequest( adTagUrl: request.adTagUrl, adDisplayContainer: (_iosParams.container as IOSAdDisplayContainer).adDisplayContainer!, + contentPlayhead: request.contentProgressProvider != null + ? (request.contentProgressProvider! as IOSContentProgressProvider) + .contentPlayhead + : null, )); } diff --git a/packages/interactive_media_ads/lib/src/ios/ios_content_progress_provider.dart b/packages/interactive_media_ads/lib/src/ios/ios_content_progress_provider.dart new file mode 100644 index 000000000000..9b75466ae548 --- /dev/null +++ b/packages/interactive_media_ads/lib/src/ios/ios_content_progress_provider.dart @@ -0,0 +1,64 @@ +// 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. + +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../platform_interface/platform_content_progress_provider.dart'; +import 'interactive_media_ads.g.dart' as ima; +import 'interactive_media_ads_proxy.dart'; + +/// Implementation of [PlatformContentProgressProviderCreationParams] for iOS. +final class IOSContentProgressProviderCreationParams + extends PlatformContentProgressProviderCreationParams { + /// Constructs an [IOSContentProgressProviderCreationParams]. + const IOSContentProgressProviderCreationParams({ + @visibleForTesting InteractiveMediaAdsProxy? proxy, + }) : _proxy = proxy ?? const InteractiveMediaAdsProxy(), + super(); + + /// Creates a [IOSContentProgressProviderCreationParams] from an instance of + /// [PlatformContentProgressProviderCreationParams]. + factory IOSContentProgressProviderCreationParams.fromPlatformContentProgressProviderCreationParams( + // Placeholder to prevent requiring a breaking change if params are added to + // PlatformContentProgressProviderCreationParams. + // ignore: avoid_unused_constructor_parameters + PlatformContentProgressProviderCreationParams params, { + @visibleForTesting InteractiveMediaAdsProxy? proxy, + }) { + return IOSContentProgressProviderCreationParams(proxy: proxy); + } + + final InteractiveMediaAdsProxy _proxy; +} + +/// Implementation of [PlatformContentProgressProvider] for iOS. +base class IOSContentProgressProvider extends PlatformContentProgressProvider { + /// Constructs an [IOSContentProgressProvider]. + IOSContentProgressProvider(super.params) : super.implementation(); + + /// The native iOS IMAContentPlayhead. + /// + /// This allows the SDK to track progress of the content video. + @internal + late final ima.IMAContentPlayhead contentPlayhead = + _iosParams._proxy.newIMAContentPlayhead(); + + late final IOSContentProgressProviderCreationParams _iosParams = + params is IOSContentProgressProviderCreationParams + ? params as IOSContentProgressProviderCreationParams + : IOSContentProgressProviderCreationParams + .fromPlatformContentProgressProviderCreationParams( + params, + ); + + @override + Future setProgress({ + required Duration progress, + required Duration duration, + }) async { + return contentPlayhead.setCurrentTime(progress.inSeconds.toDouble()); + } +} diff --git a/packages/interactive_media_ads/lib/src/ios/ios_interactive_media_ads.dart b/packages/interactive_media_ads/lib/src/ios/ios_interactive_media_ads.dart index 013f66451eb0..ee55ecce0c98 100644 --- a/packages/interactive_media_ads/lib/src/ios/ios_interactive_media_ads.dart +++ b/packages/interactive_media_ads/lib/src/ios/ios_interactive_media_ads.dart @@ -6,9 +6,11 @@ import '../platform_interface/interactive_media_ads_platform.dart'; import '../platform_interface/platform_ad_display_container.dart'; import '../platform_interface/platform_ads_loader.dart'; import '../platform_interface/platform_ads_manager_delegate.dart'; +import '../platform_interface/platform_content_progress_provider.dart'; import 'ios_ad_display_container.dart'; import 'ios_ads_loader.dart'; import 'ios_ads_manager_delegate.dart'; +import 'ios_content_progress_provider.dart'; /// Implementation of [InteractiveMediaAdsPlatform] for iOS. final class IOSInteractiveMediaAds extends InteractiveMediaAdsPlatform { @@ -35,4 +37,11 @@ final class IOSInteractiveMediaAds extends InteractiveMediaAdsPlatform { ) { return IOSAdsManagerDelegate(params); } + + @override + IOSContentProgressProvider createPlatformContentProgressProvider( + PlatformContentProgressProviderCreationParams params, + ) { + return IOSContentProgressProvider(params); + } } diff --git a/packages/interactive_media_ads/lib/src/platform_interface/ads_request.dart b/packages/interactive_media_ads/lib/src/platform_interface/ads_request.dart deleted file mode 100644 index 72e4dfc69b63..000000000000 --- a/packages/interactive_media_ads/lib/src/platform_interface/ads_request.dart +++ /dev/null @@ -1,12 +0,0 @@ -// 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. - -/// An object containing the data used to request ads from the server. -class AdsRequest { - /// Creates an [AdsRequest]. - AdsRequest({required this.adTagUrl}); - - /// The URL from which ads will be requested. - final String adTagUrl; -} diff --git a/packages/interactive_media_ads/lib/src/platform_interface/interactive_media_ads_platform.dart b/packages/interactive_media_ads/lib/src/platform_interface/interactive_media_ads_platform.dart index f1dac2db7d1d..fdb8593bdcb4 100644 --- a/packages/interactive_media_ads/lib/src/platform_interface/interactive_media_ads_platform.dart +++ b/packages/interactive_media_ads/lib/src/platform_interface/interactive_media_ads_platform.dart @@ -5,6 +5,7 @@ import 'platform_ad_display_container.dart'; import 'platform_ads_loader.dart'; import 'platform_ads_manager_delegate.dart'; +import 'platform_content_progress_provider.dart'; /// Interface for a platform implementation of the Interactive Media Ads SDKs. abstract base class InteractiveMediaAdsPlatform { @@ -29,4 +30,9 @@ abstract base class InteractiveMediaAdsPlatform { PlatformAdDisplayContainer createPlatformAdDisplayContainer( PlatformAdDisplayContainerCreationParams params, ); + + /// Creates a new [PlatformContentProgressProvider]. + PlatformContentProgressProvider createPlatformContentProgressProvider( + PlatformContentProgressProviderCreationParams params, + ); } diff --git a/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_loader.dart b/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_loader.dart index 7dc6a57a1d22..51ed2a64e686 100644 --- a/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_loader.dart +++ b/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_loader.dart @@ -5,10 +5,10 @@ import 'package:flutter/foundation.dart'; import 'ad_error.dart'; -import 'ads_request.dart'; import 'interactive_media_ads_platform.dart'; import 'platform_ad_display_container.dart'; import 'platform_ads_manager.dart'; +import 'platform_ads_request.dart'; /// Object specifying creation parameters for creating a [PlatformAdsLoader]. /// @@ -94,7 +94,7 @@ abstract base class PlatformAdsLoader { Future contentComplete(); /// Requests ads from a server. - Future requestAds(AdsRequest request); + Future requestAds(PlatformAdsRequest request); } /// Data when ads are successfully loaded from the ad server through an diff --git a/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_request.dart b/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_request.dart new file mode 100644 index 000000000000..b9ef51b0ec8c --- /dev/null +++ b/packages/interactive_media_ads/lib/src/platform_interface/platform_ads_request.dart @@ -0,0 +1,21 @@ +// 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. + +import 'platform_content_progress_provider.dart'; + +/// An object containing the data used to request ads from the server. +class PlatformAdsRequest { + /// Creates an [PlatformAdsRequest]. + PlatformAdsRequest({ + required this.adTagUrl, + this.contentProgressProvider, + }); + + /// The URL from which ads will be requested. + final String adTagUrl; + + /// A [PlatformContentProgressProvider] instance to allow scheduling of ad + /// breaks based on content progress (cue points). + final PlatformContentProgressProvider? contentProgressProvider; +} diff --git a/packages/interactive_media_ads/lib/src/platform_interface/platform_content_progress_provider.dart b/packages/interactive_media_ads/lib/src/platform_interface/platform_content_progress_provider.dart new file mode 100644 index 000000000000..fb5c54a563ea --- /dev/null +++ b/packages/interactive_media_ads/lib/src/platform_interface/platform_content_progress_provider.dart @@ -0,0 +1,86 @@ +// 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. + +import 'package:flutter/foundation.dart'; + +import 'interactive_media_ads_platform.dart'; + +/// Object specifying creation parameters for creating a +/// [PlatformContentProgressProvider]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// This example demonstrates how to extend the +/// [PlatformContentProgressProviderCreationParams] to provide additional +/// platform specific parameters. +/// +/// When extending [PlatformContentProgressProviderCreationParams] additional +/// parameters should always accept `null` or have a default value to prevent +/// breaking changes. +/// +/// ```dart +/// class AndroidPlatformContentProgressProviderCreationParams +/// extends PlatformContentProgressProviderCreationParams { +/// AndroidPlatformContentProgressProviderCreationParams._( +/// PlatformContentProgressProviderCreationParams params, { +/// this.uri, +/// }) : super(); +/// +/// factory AndroidPlatformContentProgressProviderCreationParams.fromPlatformContentProgressProviderCreationParams( +/// PlatformContentProgressProviderCreationParams params, { +/// Uri? uri, +/// }) { +/// return AndroidPlatformContentProgressProviderCreationParams._(params, uri: uri); +/// } +/// +/// final Uri? uri; +/// } +/// ``` +@immutable +base class PlatformContentProgressProviderCreationParams { + /// Used by the platform implementation to create a new + /// [PlatformContentProgressProvider]. + const PlatformContentProgressProviderCreationParams(); +} + +/// Interface to allow the SDK to track progress of the content video. +/// +/// Provides updates required to enable triggering ads at configured cue points. +abstract class PlatformContentProgressProvider { + /// Creates a new [PlatformAdsManagerDelegate] + factory PlatformContentProgressProvider( + PlatformContentProgressProviderCreationParams params, + ) { + assert( + InteractiveMediaAdsPlatform.instance != null, + 'A platform implementation for `interactive_media_ads` has not been set. ' + 'Please ensure that an implementation of `InteractiveMediaAdsPlatform` ' + 'has been set to `InteractiveMediaAdsPlatform.instance` before use. For ' + 'unit testing, `InteractiveMediaAdsPlatform.instance` can be set with ' + 'your own test implementation.', + ); + final PlatformContentProgressProvider implementation = + InteractiveMediaAdsPlatform.instance! + .createPlatformContentProgressProvider(params); + return implementation; + } + + /// Used by the platform implementation to create a new + /// [PlatformContentProgressProvider]. + /// + /// Should only be used by platform implementations because they can't extend + /// a class that only contains a factory constructor. + @protected + PlatformContentProgressProvider.implementation(this.params); + + /// The parameters used to initialize the [PlatformContentProgressProvider]. + final PlatformContentProgressProviderCreationParams params; + + /// Sends an update on the progress of the content video. + Future setProgress({ + required Duration progress, + required Duration duration, + }); +} diff --git a/packages/interactive_media_ads/lib/src/platform_interface/platform_interface.dart b/packages/interactive_media_ads/lib/src/platform_interface/platform_interface.dart index ad8896480443..fad06fe592f5 100644 --- a/packages/interactive_media_ads/lib/src/platform_interface/platform_interface.dart +++ b/packages/interactive_media_ads/lib/src/platform_interface/platform_interface.dart @@ -4,9 +4,10 @@ export 'ad_error.dart'; export 'ad_event.dart'; -export 'ads_request.dart'; export 'interactive_media_ads_platform.dart'; export 'platform_ad_display_container.dart'; export 'platform_ads_loader.dart'; export 'platform_ads_manager.dart'; export 'platform_ads_manager_delegate.dart'; +export 'platform_ads_request.dart'; +export 'platform_content_progress_provider.dart'; 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 1ca297d58469..4ac4afdf7e04 100644 --- a/packages/interactive_media_ads/pigeons/interactive_media_ads_android.dart +++ b/packages/interactive_media_ads/pigeons/interactive_media_ads_android.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(bparrishMines): Uncomment this file once -// 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'; diff --git a/packages/interactive_media_ads/pubspec.yaml b/packages/interactive_media_ads/pubspec.yaml index 925eae18beb6..e43e2c33f7f9 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.2.1 # This must match the version in +version: 0.2.2 # 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` diff --git a/packages/interactive_media_ads/test/ad_display_container_test.dart b/packages/interactive_media_ads/test/ad_display_container_test.dart index a82a16e8c4c2..c237424a806c 100644 --- a/packages/interactive_media_ads/test/ad_display_container_test.dart +++ b/packages/interactive_media_ads/test/ad_display_container_test.dart @@ -45,6 +45,8 @@ void main() { PlatformAdsManagerDelegateCreationParams params, ) { throw UnimplementedError(); + }, onCreatePlatformContentProgressProvider: (_) { + throw UnimplementedError(); }); final AdDisplayContainer adDisplayContainer = AdDisplayContainer( diff --git a/packages/interactive_media_ads/test/ads_loader_test.dart b/packages/interactive_media_ads/test/ads_loader_test.dart index 424c86ccc1d5..c571830fc4f3 100644 --- a/packages/interactive_media_ads/test/ads_loader_test.dart +++ b/packages/interactive_media_ads/test/ads_loader_test.dart @@ -18,7 +18,7 @@ void main() { onAdsLoadError: (AdsLoadErrorData data) {}, ), onContentComplete: expectAsync0(() async {}), - onRequestAds: (AdsRequest request) async {}, + onRequestAds: (PlatformAdsRequest request) async {}, ); final AdsLoader loader = AdsLoader.fromPlatform(adsLoader); @@ -26,18 +26,27 @@ void main() { }); test('requestAds', () async { + final PlatformAdsRequest platformRequest = PlatformAdsRequest( + adTagUrl: 'adTagUrl', + contentProgressProvider: TestContentProgressProvider( + const PlatformContentProgressProviderCreationParams(), + ), + ); + final TestPlatformAdsLoader adsLoader = TestPlatformAdsLoader( PlatformAdsLoaderCreationParams( container: createTestAdDisplayContainer(), onAdsLoaded: (PlatformOnAdsLoadedData data) {}, onAdsLoadError: (AdsLoadErrorData data) {}, ), - onRequestAds: expectAsync1((AdsRequest request) async {}), + onRequestAds: expectAsync1((PlatformAdsRequest request) async { + expect(request, platformRequest); + }), onContentComplete: () async {}, ); final AdsLoader loader = AdsLoader.fromPlatform(adsLoader); - await loader.requestAds(AdsRequest(adTagUrl: '')); + await loader.requestAds(AdsRequest.fromPlatform(platformRequest)); }); } diff --git a/packages/interactive_media_ads/test/ads_manager_delegate_test.dart b/packages/interactive_media_ads/test/ads_manager_delegate_test.dart index ba6801dc41f3..a0d519864c43 100644 --- a/packages/interactive_media_ads/test/ads_manager_delegate_test.dart +++ b/packages/interactive_media_ads/test/ads_manager_delegate_test.dart @@ -22,6 +22,8 @@ void main() { (PlatformAdDisplayContainerCreationParams params) { throw UnimplementedError(); }, + onCreatePlatformContentProgressProvider: (_) => + throw UnimplementedError(), ); void onAdEvent(AdEvent event) {} diff --git a/packages/interactive_media_ads/test/ads_manager_test.dart b/packages/interactive_media_ads/test/ads_manager_test.dart index eaa5994b6265..f27f37aa82c1 100644 --- a/packages/interactive_media_ads/test/ads_manager_test.dart +++ b/packages/interactive_media_ads/test/ads_manager_test.dart @@ -92,7 +92,7 @@ AdsManager createAdsManager(PlatformAdsManager platformManager) { onCreatePlatformAdsLoader: (PlatformAdsLoaderCreationParams params) { return TestPlatformAdsLoader(params, onContentComplete: () async {}, - onRequestAds: (AdsRequest request) async {}); + onRequestAds: (PlatformAdsRequest request) async {}); }, onCreatePlatformAdsManagerDelegate: (PlatformAdsManagerDelegateCreationParams params) { @@ -102,6 +102,7 @@ AdsManager createAdsManager(PlatformAdsManager platformManager) { (PlatformAdDisplayContainerCreationParams params) { throw UnimplementedError(); }, + onCreatePlatformContentProgressProvider: (_) => throw UnimplementedError(), ); late final AdsManager manager; diff --git a/packages/interactive_media_ads/test/android/ads_loader_test.dart b/packages/interactive_media_ads/test/android/ads_loader_test.dart index 3446c4e899ad..ae25fdde99f9 100644 --- a/packages/interactive_media_ads/test/android/ads_loader_test.dart +++ b/packages/interactive_media_ads/test/android/ads_loader_test.dart @@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:interactive_media_ads/src/android/android_ad_display_container.dart'; import 'package:interactive_media_ads/src/android/android_ads_loader.dart'; +import 'package:interactive_media_ads/src/android/android_content_progress_provider.dart'; import 'package:interactive_media_ads/src/android/interactive_media_ads.g.dart' as ima; import 'package:interactive_media_ads/src/android/interactive_media_ads_proxy.dart'; @@ -94,6 +95,8 @@ void main() { final InteractiveMediaAdsProxy proxy = InteractiveMediaAdsProxy( instanceImaSdkFactory: () => mockSdkFactory, + newContentProgressProvider: () => + ima.ContentProgressProvider.pigeon_detached(), ); final AndroidAdsLoader adsLoader = AndroidAdsLoader( @@ -105,10 +108,22 @@ void main() { ), ); - await adsLoader.requestAds(AdsRequest(adTagUrl: 'url')); + final AndroidContentProgressProvider progressProvider = + AndroidContentProgressProvider( + AndroidContentProgressProviderCreationParams(proxy: proxy), + ); + await adsLoader.requestAds( + PlatformAdsRequest( + adTagUrl: 'url', + contentProgressProvider: progressProvider, + ), + ); verifyInOrder(>[ mockAdsRequest.setAdTagUrl('url'), + mockAdsRequest.setContentProgressProvider( + progressProvider.progressProvider, + ), mockAdsLoader.requestAds(mockAdsRequest), ]); }); diff --git a/packages/interactive_media_ads/test/android/content_progress_provider_test.dart b/packages/interactive_media_ads/test/android/content_progress_provider_test.dart new file mode 100644 index 000000000000..bf7885bb77c6 --- /dev/null +++ b/packages/interactive_media_ads/test/android/content_progress_provider_test.dart @@ -0,0 +1,49 @@ +// 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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:interactive_media_ads/src/android/android_content_progress_provider.dart'; +import 'package:interactive_media_ads/src/android/interactive_media_ads.g.dart' + as ima; +import 'package:interactive_media_ads/src/android/interactive_media_ads_proxy.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'content_progress_provider_test.mocks.dart'; + +@GenerateNiceMocks(>[ + MockSpec(), +]) +void main() { + group('AndroidContentProgressProvider', () { + test('setProgress', () async { + final MockContentProgressProvider mockContentProgressProvider = + MockContentProgressProvider(); + + final AndroidContentProgressProvider provider = + AndroidContentProgressProvider( + AndroidContentProgressProviderCreationParams( + proxy: InteractiveMediaAdsProxy( + newContentProgressProvider: () => mockContentProgressProvider, + newVideoProgressUpdate: ({ + required int currentTimeMs, + required int durationMs, + }) { + expect(currentTimeMs, 1000); + expect(durationMs, 10000); + return ima.VideoProgressUpdate.pigeon_detached(); + }, + ), + ), + ); + + await provider.setProgress( + progress: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + ); + + verify(mockContentProgressProvider.setContentProgress(any)); + }); + }); +} diff --git a/packages/interactive_media_ads/test/android/content_progress_provider_test.mocks.dart b/packages/interactive_media_ads/test/android/content_progress_provider_test.mocks.dart new file mode 100644 index 000000000000..6d65d6475030 --- /dev/null +++ b/packages/interactive_media_ads/test/android/content_progress_provider_test.mocks.dart @@ -0,0 +1,97 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in interactive_media_ads/test/android/content_progress_provider_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:interactive_media_ads/src/android/interactive_media_ads.g.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakePigeonInstanceManager_0 extends _i1.SmartFake + implements _i2.PigeonInstanceManager { + _FakePigeonInstanceManager_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeContentProgressProvider_1 extends _i1.SmartFake + implements _i2.ContentProgressProvider { + _FakeContentProgressProvider_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [ContentProgressProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockContentProgressProvider extends _i1.Mock + implements _i2.ContentProgressProvider { + @override + _i2.PigeonInstanceManager get pigeon_instanceManager => (super.noSuchMethod( + Invocation.getter(#pigeon_instanceManager), + returnValue: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + returnValueForMissingStub: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + ) as _i2.PigeonInstanceManager); + + @override + _i3.Future setContentProgress(_i2.VideoProgressUpdate? update) => + (super.noSuchMethod( + Invocation.method( + #setContentProgress, + [update], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i2.ContentProgressProvider pigeon_copy() => (super.noSuchMethod( + Invocation.method( + #pigeon_copy, + [], + ), + returnValue: _FakeContentProgressProvider_1( + this, + Invocation.method( + #pigeon_copy, + [], + ), + ), + returnValueForMissingStub: _FakeContentProgressProvider_1( + this, + Invocation.method( + #pigeon_copy, + [], + ), + ), + ) as _i2.ContentProgressProvider); +} diff --git a/packages/interactive_media_ads/test/content_progress_provider_test.dart b/packages/interactive_media_ads/test/content_progress_provider_test.dart new file mode 100644 index 000000000000..180d9ce0d015 --- /dev/null +++ b/packages/interactive_media_ads/test/content_progress_provider_test.dart @@ -0,0 +1,41 @@ +// 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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:interactive_media_ads/src/content_progress_provider.dart'; +import 'package:interactive_media_ads/src/platform_interface/platform_content_progress_provider.dart'; + +import 'test_stubs.dart'; + +void main() { + test('setProgress', () async { + late final Duration callbackProgress; + late final Duration callbackDuration; + + final TestContentProgressProvider platformProvider = + TestContentProgressProvider( + const PlatformContentProgressProviderCreationParams(), + onSetProgress: ({ + required Duration progress, + required Duration duration, + }) async { + callbackProgress = progress; + callbackDuration = duration; + }, + ); + + final ContentProgressProvider provider = + ContentProgressProvider.fromPlatform( + platformProvider, + ); + + await provider.setProgress( + progress: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + ); + + expect(callbackProgress, const Duration(seconds: 1)); + expect(callbackDuration, const Duration(seconds: 10)); + }); +} diff --git a/packages/interactive_media_ads/test/ios/ads_loader_test.dart b/packages/interactive_media_ads/test/ios/ads_loader_test.dart index 04ea706d2128..633e96e9803f 100644 --- a/packages/interactive_media_ads/test/ios/ads_loader_test.dart +++ b/packages/interactive_media_ads/test/ios/ads_loader_test.dart @@ -9,6 +9,7 @@ import 'package:interactive_media_ads/src/ios/interactive_media_ads.g.dart' import 'package:interactive_media_ads/src/ios/interactive_media_ads_proxy.dart'; import 'package:interactive_media_ads/src/ios/ios_ad_display_container.dart'; import 'package:interactive_media_ads/src/ios/ios_ads_loader.dart'; +import 'package:interactive_media_ads/src/ios/ios_content_progress_provider.dart'; import 'package:interactive_media_ads/src/platform_interface/platform_interface.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -70,6 +71,8 @@ void main() { const String adTag = 'myAdTag'; final MockIMAAdsLoader mockLoader = MockIMAAdsLoader(); + final ima.IMAContentPlayhead contentPlayheadInstance = + ima.IMAContentPlayhead(); final InteractiveMediaAdsProxy imaProxy = InteractiveMediaAdsProxy( newIMAAdsLoader: ({ima.IMASettings? settings}) => mockLoader, newIMAAdsRequest: ({ @@ -79,8 +82,10 @@ void main() { }) { expect(adTagUrl, adTag); expect(adDisplayContainer, container.adDisplayContainer); + expect(contentPlayhead, contentPlayheadInstance); return MockIMAAdsRequest(); }, + newIMAContentPlayhead: () => contentPlayheadInstance, ); final IOSAdsLoader loader = IOSAdsLoader( @@ -92,7 +97,14 @@ void main() { ), ); - await loader.requestAds(AdsRequest(adTagUrl: adTag)); + final IOSContentProgressProvider provider = IOSContentProgressProvider( + IOSContentProgressProviderCreationParams(proxy: imaProxy), + ); + + await loader.requestAds(PlatformAdsRequest( + adTagUrl: adTag, + contentProgressProvider: provider, + )); verify(mockLoader.requestAds(any)); }); diff --git a/packages/interactive_media_ads/test/ios/content_progress_provider_test.dart b/packages/interactive_media_ads/test/ios/content_progress_provider_test.dart new file mode 100644 index 000000000000..80975174d3aa --- /dev/null +++ b/packages/interactive_media_ads/test/ios/content_progress_provider_test.dart @@ -0,0 +1,39 @@ +// 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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:interactive_media_ads/src/ios/interactive_media_ads.g.dart'; +import 'package:interactive_media_ads/src/ios/interactive_media_ads_proxy.dart'; +import 'package:interactive_media_ads/src/ios/ios_content_progress_provider.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'content_progress_provider_test.mocks.dart'; + +@GenerateNiceMocks(>[ + MockSpec(), +]) +void main() { + group('IOSContentProgressProvider', () { + test('setProgress', () async { + final MockIMAContentPlayhead mockContentPlayhead = + MockIMAContentPlayhead(); + + final IOSContentProgressProvider provider = IOSContentProgressProvider( + IOSContentProgressProviderCreationParams( + proxy: InteractiveMediaAdsProxy( + newIMAContentPlayhead: () => mockContentPlayhead, + ), + ), + ); + + await provider.setProgress( + progress: const Duration(seconds: 1), + duration: const Duration(seconds: 10), + ); + + verify(mockContentPlayhead.setCurrentTime(1.0)); + }); + }); +} diff --git a/packages/interactive_media_ads/test/ios/content_progress_provider_test.mocks.dart b/packages/interactive_media_ads/test/ios/content_progress_provider_test.mocks.dart new file mode 100644 index 000000000000..a82fb3fbade8 --- /dev/null +++ b/packages/interactive_media_ads/test/ios/content_progress_provider_test.mocks.dart @@ -0,0 +1,96 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in interactive_media_ads/test/ios/content_progress_provider_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:interactive_media_ads/src/ios/interactive_media_ads.g.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakePigeonInstanceManager_0 extends _i1.SmartFake + implements _i2.PigeonInstanceManager { + _FakePigeonInstanceManager_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIMAContentPlayhead_1 extends _i1.SmartFake + implements _i2.IMAContentPlayhead { + _FakeIMAContentPlayhead_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [IMAContentPlayhead]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockIMAContentPlayhead extends _i1.Mock + implements _i2.IMAContentPlayhead { + @override + _i2.PigeonInstanceManager get pigeon_instanceManager => (super.noSuchMethod( + Invocation.getter(#pigeon_instanceManager), + returnValue: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + returnValueForMissingStub: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + ) as _i2.PigeonInstanceManager); + + @override + _i3.Future setCurrentTime(double? timeInterval) => (super.noSuchMethod( + Invocation.method( + #setCurrentTime, + [timeInterval], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i2.IMAContentPlayhead pigeon_copy() => (super.noSuchMethod( + Invocation.method( + #pigeon_copy, + [], + ), + returnValue: _FakeIMAContentPlayhead_1( + this, + Invocation.method( + #pigeon_copy, + [], + ), + ), + returnValueForMissingStub: _FakeIMAContentPlayhead_1( + this, + Invocation.method( + #pigeon_copy, + [], + ), + ), + ) as _i2.IMAContentPlayhead); +} diff --git a/packages/interactive_media_ads/test/test_stubs.dart b/packages/interactive_media_ads/test/test_stubs.dart index 628c46d547b2..328f58f46995 100644 --- a/packages/interactive_media_ads/test/test_stubs.dart +++ b/packages/interactive_media_ads/test/test_stubs.dart @@ -11,6 +11,7 @@ final class TestInteractiveMediaAdsPlatform required this.onCreatePlatformAdsLoader, required this.onCreatePlatformAdsManagerDelegate, required this.onCreatePlatformAdDisplayContainer, + required this.onCreatePlatformContentProgressProvider, }); PlatformAdsLoader Function(PlatformAdsLoaderCreationParams params) @@ -24,6 +25,10 @@ final class TestInteractiveMediaAdsPlatform PlatformAdDisplayContainerCreationParams params, ) onCreatePlatformAdDisplayContainer; + PlatformContentProgressProvider Function( + PlatformContentProgressProviderCreationParams params, + ) onCreatePlatformContentProgressProvider; + @override PlatformAdsLoader createPlatformAdsLoader( PlatformAdsLoaderCreationParams params, @@ -44,6 +49,13 @@ final class TestInteractiveMediaAdsPlatform ) { return onCreatePlatformAdDisplayContainer(params); } + + @override + PlatformContentProgressProvider createPlatformContentProgressProvider( + PlatformContentProgressProviderCreationParams params, + ) { + return onCreatePlatformContentProgressProvider(params); + } } final class TestPlatformAdDisplayContainer extends PlatformAdDisplayContainer { @@ -69,7 +81,7 @@ final class TestPlatformAdsLoader extends PlatformAdsLoader { Future Function() onContentComplete; - Future Function(AdsRequest request) onRequestAds; + Future Function(PlatformAdsRequest request) onRequestAds; @override Future contentComplete() async { @@ -77,7 +89,7 @@ final class TestPlatformAdsLoader extends PlatformAdsLoader { } @override - Future requestAds(AdsRequest request) async { + Future requestAds(PlatformAdsRequest request) async { return onRequestAds(request); } } @@ -157,3 +169,23 @@ class TestAdsManager extends PlatformAdsManager { return onSkip?.call(); } } + +class TestContentProgressProvider extends PlatformContentProgressProvider { + TestContentProgressProvider( + super.params, { + this.onSetProgress, + }) : super.implementation(); + + Future Function({ + required Duration progress, + required Duration duration, + })? onSetProgress; + + @override + Future setProgress({ + required Duration progress, + required Duration duration, + }) async { + return onSetProgress?.call(progress: progress, duration: duration); + } +}