diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 774e04e24..e630c0264 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -29,7 +29,7 @@ jobs: - "samples/admob/rewarded_example" - "samples/admob/rewarded_interstitial_example" steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" @@ -60,11 +60,11 @@ jobs: - "samples/admob/rewarded_example" - "samples/admob/rewarded_interstitial_example" steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.3.0.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_26.2.app/Contents/Developer' - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" @@ -94,7 +94,7 @@ jobs: - "samples/admob/rewarded_example" - "samples/admob/rewarded_interstitial_example" steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/cancel.yaml b/.github/workflows/cancel.yaml index c0f41e7af..b41ac8c8f 100644 --- a/.github/workflows/cancel.yaml +++ b/.github/workflows/cancel.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 3 steps: - - uses: styfle/cancel-workflow-action@0.12.1 + - uses: styfle/cancel-workflow-action@0.13.1 with: workflow_id: 5958127 access_token: ${{ github.token }} diff --git a/.github/workflows/gma_mediation_applovin.yaml b/.github/workflows/gma_mediation_applovin.yaml index 8d648f831..ff1e55c1c 100644 --- a/.github/workflows/gma_mediation_applovin.yaml +++ b/.github/workflows/gma_mediation_applovin.yaml @@ -31,10 +31,10 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -56,11 +56,11 @@ jobs: - uses: swift-actions/setup-swift@v2 with: swift-version: "5.7.2" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.2.0.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_26.2.app/Contents/Developer' - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" @@ -73,8 +73,8 @@ jobs: flutter pub get flutter precache --ios pod install --repo-update - xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test - - uses: actions/upload-artifact@v4 + xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test + - uses: actions/upload-artifact@v7 if: failure() with: name: iOSTestResults @@ -84,7 +84,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_chartboost.yaml b/.github/workflows/gma_mediation_chartboost.yaml index 4589ecf6b..b8f93d1dc 100644 --- a/.github/workflows/gma_mediation_chartboost.yaml +++ b/.github/workflows/gma_mediation_chartboost.yaml @@ -31,7 +31,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_dtexchange.yaml b/.github/workflows/gma_mediation_dtexchange.yaml index 8a2dbe17e..756bcb263 100644 --- a/.github/workflows/gma_mediation_dtexchange.yaml +++ b/.github/workflows/gma_mediation_dtexchange.yaml @@ -31,10 +31,10 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -56,11 +56,11 @@ jobs: - uses: swift-actions/setup-swift@v2 with: swift-version: "6.0.0" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.2.0.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_26.2.app/Contents/Developer' - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" @@ -73,8 +73,8 @@ jobs: flutter pub get flutter precache --ios pod install - xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test - - uses: actions/upload-artifact@v4 + xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test + - uses: actions/upload-artifact@v7 if: failure() with: name: iOSTestResults @@ -84,7 +84,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_inmobi.yaml b/.github/workflows/gma_mediation_inmobi.yaml index 944721f33..73163d762 100644 --- a/.github/workflows/gma_mediation_inmobi.yaml +++ b/.github/workflows/gma_mediation_inmobi.yaml @@ -31,7 +31,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_ironsource.yaml b/.github/workflows/gma_mediation_ironsource.yaml index 5807d8cb8..d4032b306 100644 --- a/.github/workflows/gma_mediation_ironsource.yaml +++ b/.github/workflows/gma_mediation_ironsource.yaml @@ -31,10 +31,10 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -55,11 +55,11 @@ jobs: - uses: swift-actions/setup-swift@v2 with: swift-version: "6.0.0" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.2.0.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_26.2.app/Contents/Developer' - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" @@ -72,8 +72,8 @@ jobs: flutter pub get flutter precache --ios pod install - xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test - - uses: actions/upload-artifact@v4 + xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test + - uses: actions/upload-artifact@v7 if: failure() with: name: iOSTestResults @@ -83,7 +83,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_liftoffmonetize.yaml b/.github/workflows/gma_mediation_liftoffmonetize.yaml index 6a20ad0b1..43e9ed086 100644 --- a/.github/workflows/gma_mediation_liftoffmonetize.yaml +++ b/.github/workflows/gma_mediation_liftoffmonetize.yaml @@ -31,10 +31,10 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -56,11 +56,11 @@ jobs: - uses: swift-actions/setup-swift@v2 with: swift-version: "5.7.2" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.2.0.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_26.2.app/Contents/Developer' - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" @@ -73,8 +73,8 @@ jobs: flutter pub get flutter precache --ios pod install - xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test - - uses: actions/upload-artifact@v4 + xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test + - uses: actions/upload-artifact@v7 if: failure() with: name: iOSTestResults @@ -85,7 +85,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_meta.yaml b/.github/workflows/gma_mediation_meta.yaml index ff7134bd3..95eb6933e 100644 --- a/.github/workflows/gma_mediation_meta.yaml +++ b/.github/workflows/gma_mediation_meta.yaml @@ -31,7 +31,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_mintegral.yaml b/.github/workflows/gma_mediation_mintegral.yaml index bd9ff3e1d..332bb5b25 100644 --- a/.github/workflows/gma_mediation_mintegral.yaml +++ b/.github/workflows/gma_mediation_mintegral.yaml @@ -31,7 +31,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_pangle.yaml b/.github/workflows/gma_mediation_pangle.yaml index d4b573ef8..cb2d4ce9e 100644 --- a/.github/workflows/gma_mediation_pangle.yaml +++ b/.github/workflows/gma_mediation_pangle.yaml @@ -31,7 +31,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/gma_mediation_unity.yaml b/.github/workflows/gma_mediation_unity.yaml index e0fc3fffd..2e33056f6 100644 --- a/.github/workflows/gma_mediation_unity.yaml +++ b/.github/workflows/gma_mediation_unity.yaml @@ -31,10 +31,10 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -56,11 +56,11 @@ jobs: - uses: swift-actions/setup-swift@v2 with: swift-version: "6.0.0" - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.2.0.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_26.2.app/Contents/Developer' - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" @@ -73,8 +73,8 @@ jobs: flutter pub get flutter precache --ios pod install - xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test - - uses: actions/upload-artifact@v4 + xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test + - uses: actions/upload-artifact@v7 if: failure() with: name: iOSTestResults @@ -85,7 +85,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.github/workflows/google_mobile_ads.yaml b/.github/workflows/google_mobile_ads.yaml index 9024b03cb..720747440 100644 --- a/.github/workflows/google_mobile_ads.yaml +++ b/.github/workflows/google_mobile_ads.yaml @@ -34,10 +34,10 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -56,11 +56,14 @@ jobs: runs-on: macos-latest timeout-minutes: 40 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.0.0" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.3.0.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_26.2.app/Contents/Developer' - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" @@ -74,8 +77,8 @@ jobs: flutter clean flutter pub get pod install - xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' test - - uses: actions/upload-artifact@v4 + xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test + - uses: actions/upload-artifact@v7 if: failure() with: name: iOSTestResults @@ -85,7 +88,7 @@ jobs: if: github.event_name == 'pull_request' timeout-minutes: 30 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - name: "Install Flutter" diff --git a/.gitignore b/.gitignore index 3cb9a0984..ac23f68c9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,6 @@ Pods/ **/Flutter/flutter_assets/ ServiceDefinitions.json xcuserdata/ -xcshareddata/ local.properties keystore.properties diff --git a/CODEOWNERS b/CODEOWNERS index ad5d0447e..80db18083 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,5 +4,5 @@ # These names are just suggestions. It is fine to have your changes # reviewed by someone else. -packages/google_mobile_ads/* @bparrishMines @jjliu15 @luo86 @srichakradhar @LTPhantom -.github/ @bparrishMines @jjliu15 @luo86 @srichakradhar @LTPhantom +packages/google_mobile_ads/* @berile @luo86 @LTPhantom @thanvirm +.github/ @berile @luo86 @LTPhantom @thanvirm diff --git a/packages/google_mobile_ads/CHANGELOG.md b/packages/google_mobile_ads/CHANGELOG.md index 699008229..e3c5b1910 100644 --- a/packages/google_mobile_ads/CHANGELOG.md +++ b/packages/google_mobile_ads/CHANGELOG.md @@ -1,3 +1,41 @@ +## 9.0.0 +* Resolves pending start/stop method channel futures in AppStateNotifier Android. [PR 1438](https://github.com/googleads/googleads-mobile-flutter/pull/1438) +* Updates GMA Android SDK dependencies: + * [Google Mobile Ads](https://developers.google.com/admob/android/quick-start) SDK version `25.3.0`. + * [Google Mobile Ads Next-Gen](https://developers.google.com/admob/android/next-gen/rel-notes) SDK version `1.1.0`. +* Updates GMA [iOS](https://developers.google.com/admob/ios/rel-notes) dependency to `13.3.0`. +* Uses latest UMP SDK: + * [Android](https://developers.google.com/admob/android/privacy/release-notes) UMP SDK version 4.0.0. + * [iOS](https://developers.google.com/admob/ios/privacy/download#release_notes) UMP SDK version 3.1.0. + +## 8.0.0 +* Updates minimum Flutter SDK to 3.38.1 +* Updates Dart SDK low bound to 3.10.0. +* Adds Swift Package Manager Support for the plugin. [PR 1395](https://github.com/googleads/googleads-mobile-flutter/pull/1395) +* Adds `isCollapsible` API. [FR 1294](https://github.com/googleads/googleads-mobile-flutter/issues/1294) +* Renamed method name to avoid possible name collision. [FR 1394](https://github.com/googleads/googleads-mobile-flutter/issues/1394) +* Migrated to use UISceneDelegate protocol. [Issue 1391](https://github.com/googleads/googleads-mobile-flutter/issues/1391) +* New to Anchored adaptive banner ads: + * The following APIs have been deprecated for their replacement: + * The `getCurrentOrientationAnchoredAdaptiveBannerAdSize` function is deprecated. Instead, use the `getLargeAnchoredAdaptiveBannerAdSize` function. + * The `getAnchoredAdaptiveBannerAdSize` function is deprecated. Instead, use the `getLargeAnchoredAdaptiveBannerAdSizeWithOrientation` function. +* Updates GMA [Android](https://developers.google.com/admob/android/rel-notes) dependency to 25.1.0 +* Updates GMA [iOS](https://developers.google.com/admob/ios/rel-notes) dependency to 13.2.0 +* Uses latest UMP SDK: + * [Android](https://developers.google.com/admob/android/privacy/release-notes) UMP SDK version 4.0.0. + * [iOS](https://developers.google.com/admob/ios/privacy/download#release_notes) UMP SDK version 3.1.0. + +## 7.0.0 +* Added character limits expected for Native Ad Templates. Issues [1243](https://github.com/googleads/googleads-mobile-flutter/issues/1243) and [1332](https://github.com/googleads/googleads-mobile-flutter/issues/1332) +* Fixed padding for Native Ads small template. [Issue 1357](https://github.com/googleads/googleads-mobile-flutter/issues/1357) +* Updated to use Gradle plugin 9.2.1 [Issue 1361](https://github.com/googleads/googleads-mobile-flutter/issues/1361) +* Updates dependencies. [Issue 1366](https://github.com/googleads/googleads-mobile-flutter/issues/1366) +* Updates GMA [Android](https://developers.google.com/admob/android/rel-notes) dependency to 24.9.0 +* Updates GMA [iOS](https://developers.google.com/admob/ios/rel-notes) dependency to 12.14.0 +* Uses latest UMP SDK: + * [Android](https://developers.google.com/admob/android/privacy/release-notes) UMP SDK version 4.0.0. + * [iOS](https://developers.google.com/admob/ios/privacy/download#release_notes) UMP SDK version 3.1.0. + ## 6.0.0 * Updates minimum Flutter SDK to 3.27.0 * Updates Dart SDK low bound to 3.6.0. diff --git a/packages/google_mobile_ads/android/build.gradle b/packages/google_mobile_ads/android/build.gradle index 948e368f3..b2be9dcac 100644 --- a/packages/google_mobile_ads/android/build.gradle +++ b/packages/google_mobile_ads/android/build.gradle @@ -1,14 +1,17 @@ group 'io.flutter.plugins.googlemobileads' -version '6.0.0' +version '9.0.0' buildscript { + ext { + agp_version = '8.13.1' + } repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.3.2' + classpath "com.android.tools.build:gradle:$agp_version" } } @@ -22,29 +25,60 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdk 35 + def dartEnvironmentVariables = [] + if (project.hasProperty('dart-defines')) { + dartEnvironmentVariables = project.property('dart-defines') + .split(',') + .collectEntries { entry -> + def pair = new String(entry.decodeBase64(), 'UTF-8').split('=') + [(pair.first()): pair.last()] + } + } + + def useNextGenSdk = dartEnvironmentVariables.USE_NEXT_GEN_SDK ? dartEnvironmentVariables.USE_NEXT_GEN_SDK : false + + compileSdk 36 if (project.android.hasProperty('namespace')) { namespace 'io.flutter.plugins.googlemobileads' } defaultConfig { - minSdk 23 + minSdk 24 } lintOptions { disable 'InvalidPackage' } + sourceSets.named("main") { + java.srcDirs("src/main/java") + if (useNextGenSdk) { + java.srcDirs("src/adsNextGenSdk/java") + } else { + java.srcDirs("src/playServices/java") + } + + res.srcDirs("src/main/res") + if (useNextGenSdk) { + res.srcDirs("src/adsNextGenSdk/res") + } else { + res.srcDirs("src/playServices/res") + } + } dependencies { - api 'com.google.android.gms:play-services-ads:24.1.0' - implementation 'com.google.android.ump:user-messaging-platform:3.2.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.lifecycle:lifecycle-process:2.6.2' - implementation 'com.google.errorprone:error_prone_annotations:2.16' + if (useNextGenSdk) { + api 'com.google.android.libraries.ads.mobile.sdk:ads-mobile-sdk:1.1.1' + } else { + api 'com.google.android.gms:play-services-ads:25.3.0' + } + implementation 'com.google.android.ump:user-messaging-platform:4.0.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation 'androidx.lifecycle:lifecycle-process:2.10.0' + implementation 'com.google.errorprone:error_prone_annotations:2.48.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:hamcrest:3.0' - testImplementation 'org.mockito:mockito-core:5.15.2' - testImplementation 'org.robolectric:robolectric:4.14.1' - testImplementation 'androidx.test:core:1.5.0' + testImplementation 'org.mockito:mockito-core:5.23.0' + testImplementation 'org.robolectric:robolectric:4.16.1' + testImplementation 'androidx.test:core:1.7.0' } testOptions { unitTests { @@ -54,15 +88,11 @@ android { } afterEvaluate { - def containsEmbeddingDependencies = false - for (def configuration : configurations.all) { - for (def dependency : configuration.dependencies) { - if (dependency.group == 'io.flutter' && + def containsEmbeddingDependencies = configurations.any { configuration -> + configuration.dependencies.any { dependency -> + dependency.group == 'io.flutter' && dependency.name.startsWith('flutter_embedding') && - dependency.isTransitive()) { - containsEmbeddingDependencies = true - break - } + (dependency instanceof ModuleDependency && dependency.isTransitive()) } } if (!containsEmbeddingDependencies) { diff --git a/packages/google_mobile_ads/android/gradle/gradle-daemon-jvm.properties b/packages/google_mobile_ads/android/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000..6c1139ec0 --- /dev/null +++ b/packages/google_mobile_ads/android/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,12 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect +toolchainVersion=21 diff --git a/packages/google_mobile_ads/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_mobile_ads/android/gradle/wrapper/gradle-wrapper.properties index 9c034ab11..c5856afbe 100644 --- a/packages/google_mobile_ads/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_mobile_ads/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat May 13 14:37:03 CEST 2023 +#Thu Dec 04 10:45:11 PST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/com/google/android/ads/nativetemplates/TemplateView.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/com/google/android/ads/nativetemplates/TemplateView.java new file mode 100644 index 000000000..1319a2d57 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/com/google/android/ads/nativetemplates/TemplateView.java @@ -0,0 +1,295 @@ +// Copyright 2026 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. + +package com.google.android.ads.nativetemplates; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.RatingBar; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import com.google.android.libraries.ads.mobile.sdk.common.Image; +import com.google.android.libraries.ads.mobile.sdk.nativead.MediaView; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView; +import io.flutter.plugins.googlemobileads.R; + +/** Base class for a template view. */ +public final class TemplateView extends FrameLayout { + + private int templateType; + private NativeTemplateStyle styles; + private NativeAd nativeAd; + private NativeAdView nativeAdView; + + private TextView primaryView; + private TextView secondaryView; + private RatingBar ratingBar; + private TextView tertiaryView; + private ImageView iconView; + private MediaView mediaView; + private Button callToActionView; + private ConstraintLayout background; + + private static final String MEDIUM_TEMPLATE = "medium_template"; + private static final String SMALL_TEMPLATE = "small_template"; + + public TemplateView(Context context) { + super(context); + } + + public TemplateView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initView(context, attrs); + } + + public TemplateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context, attrs); + } + + public TemplateView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initView(context, attrs); + } + + public void setStyles(NativeTemplateStyle styles) { + this.styles = styles; + this.applyStyles(); + } + + public NativeAdView getNativeAdView() { + return nativeAdView; + } + + private void applyStyles() { + + Drawable mainBackground = styles.getMainBackgroundColor(); + if (mainBackground != null) { + background.setBackground(mainBackground); + if (primaryView != null) { + primaryView.setBackground(mainBackground); + } + if (secondaryView != null) { + secondaryView.setBackground(mainBackground); + } + if (tertiaryView != null) { + tertiaryView.setBackground(mainBackground); + } + } + + Typeface primary = styles.getPrimaryTextTypeface(); + if (primary != null && primaryView != null) { + primaryView.setTypeface(primary); + } + + Typeface secondary = styles.getSecondaryTextTypeface(); + if (secondary != null && secondaryView != null) { + secondaryView.setTypeface(secondary); + } + + Typeface tertiary = styles.getTertiaryTextTypeface(); + if (tertiary != null && tertiaryView != null) { + tertiaryView.setTypeface(tertiary); + } + + Typeface ctaTypeface = styles.getCallToActionTextTypeface(); + if (ctaTypeface != null && callToActionView != null) { + callToActionView.setTypeface(ctaTypeface); + } + + if (styles.getPrimaryTextTypefaceColor() != null && primaryView != null) { + primaryView.setTextColor(styles.getPrimaryTextTypefaceColor()); + } + + if (styles.getSecondaryTextTypefaceColor() != null && secondaryView != null) { + secondaryView.setTextColor(styles.getSecondaryTextTypefaceColor()); + } + + if (styles.getTertiaryTextTypefaceColor() != null && tertiaryView != null) { + tertiaryView.setTextColor(styles.getTertiaryTextTypefaceColor()); + } + + if (styles.getCallToActionTypefaceColor() != null && callToActionView != null) { + callToActionView.setTextColor(styles.getCallToActionTypefaceColor()); + } + + float ctaTextSize = styles.getCallToActionTextSize(); + if (ctaTextSize > 0 && callToActionView != null) { + callToActionView.setTextSize(ctaTextSize); + } + + float primaryTextSize = styles.getPrimaryTextSize(); + if (primaryTextSize > 0 && primaryView != null) { + primaryView.setTextSize(primaryTextSize); + } + + float secondaryTextSize = styles.getSecondaryTextSize(); + if (secondaryTextSize > 0 && secondaryView != null) { + secondaryView.setTextSize(secondaryTextSize); + } + + float tertiaryTextSize = styles.getTertiaryTextSize(); + if (tertiaryTextSize > 0 && tertiaryView != null) { + tertiaryView.setTextSize(tertiaryTextSize); + } + + Drawable ctaBackground = styles.getCallToActionBackgroundColor(); + if (ctaBackground != null && callToActionView != null) { + callToActionView.setBackground(ctaBackground); + } + + Drawable primaryBackground = styles.getPrimaryTextBackgroundColor(); + if (primaryBackground != null && primaryView != null) { + primaryView.setBackground(primaryBackground); + } + + Drawable secondaryBackground = styles.getSecondaryTextBackgroundColor(); + if (secondaryBackground != null && secondaryView != null) { + secondaryView.setBackground(secondaryBackground); + } + + Drawable tertiaryBackground = styles.getTertiaryTextBackgroundColor(); + if (tertiaryBackground != null && tertiaryView != null) { + tertiaryView.setBackground(tertiaryBackground); + } + + invalidate(); + requestLayout(); + } + + private boolean adHasOnlyStore(NativeAd nativeAd) { + String store = nativeAd.getStore(); + String advertiser = nativeAd.getAdvertiser(); + return !TextUtils.isEmpty(store) && TextUtils.isEmpty(advertiser); + } + + public void setNativeAd(NativeAd nativeAd) { + this.nativeAd = nativeAd; + + String store = nativeAd.getStore(); + String advertiser = nativeAd.getAdvertiser(); + String headline = nativeAd.getHeadline(); + String body = nativeAd.getBody(); + String cta = nativeAd.getCallToAction(); + Double starRating = nativeAd.getStarRating(); + Image icon = nativeAd.getIcon(); + + String secondaryText; + + nativeAdView.setCallToActionView(callToActionView); + nativeAdView.setHeadlineView(primaryView); + secondaryView.setVisibility(VISIBLE); + if (adHasOnlyStore(nativeAd)) { + nativeAdView.setStoreView(secondaryView); + secondaryText = store; + } else if (!TextUtils.isEmpty(advertiser)) { + nativeAdView.setAdvertiserView(secondaryView); + secondaryText = advertiser; + } else { + secondaryText = ""; + } + + primaryView.setText(headline); + callToActionView.setText(cta); + + // Set the secondary view to be the star rating if available. + if (starRating != null && starRating > 0) { + secondaryView.setVisibility(GONE); + ratingBar.setVisibility(VISIBLE); + ratingBar.setRating(starRating.floatValue()); + + nativeAdView.setStarRatingView(ratingBar); + } else { + secondaryView.setText(secondaryText); + secondaryView.setVisibility(VISIBLE); + ratingBar.setVisibility(GONE); + } + + if (icon != null) { + iconView.setVisibility(VISIBLE); + iconView.setImageDrawable(icon.getDrawable()); + } else { + iconView.setVisibility(GONE); + } + + if (tertiaryView != null) { + tertiaryView.setText(body); + nativeAdView.setBodyView(tertiaryView); + } + + nativeAdView.registerNativeAd(nativeAd, mediaView); + } + + /** + * To prevent memory leaks, make sure to destroy your ad when you don't need it anymore. This + * method does not destroy the template view. + * https://developers.google.com/admob/android/native-unified#destroy_ad + */ + public void destroyNativeAd() { + nativeAd.destroy(); + } + + public String getTemplateTypeName() { + if (templateType == R.layout.gnt_medium_template_view) { + return MEDIUM_TEMPLATE; + } else if (templateType == R.layout.gnt_small_template_view) { + return SMALL_TEMPLATE; + } + return ""; + } + + private void initView(Context context, AttributeSet attributeSet) { + + TypedArray attributes = + context.getTheme().obtainStyledAttributes(attributeSet, R.styleable.TemplateView, 0, 0); + + try { + templateType = + attributes.getResourceId( + R.styleable.TemplateView_gnt_template_type, R.layout.gnt_medium_template_view); + } finally { + attributes.recycle(); + } + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(templateType, this); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + nativeAdView = (NativeAdView) findViewById(R.id.native_ad_view); + primaryView = (TextView) findViewById(R.id.primary); + secondaryView = (TextView) findViewById(R.id.secondary); + tertiaryView = (TextView) findViewById(R.id.body); + + ratingBar = (RatingBar) findViewById(R.id.rating_bar); + ratingBar.setEnabled(false); + + callToActionView = (Button) findViewById(R.id.cta); + iconView = (ImageView) findViewById(R.id.icon); + mediaView = (MediaView) findViewById(R.id.media_view); + background = (ConstraintLayout) findViewById(R.id.background); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java new file mode 100644 index 000000000..1a3c91834 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java @@ -0,0 +1,272 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.app.Activity; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.AdError; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.PrecisionType; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.googlemobileads.FlutterAd.FlutterAdError; +import io.flutter.plugins.googlemobileads.FlutterAd.FlutterResponseInfo; +import java.util.HashMap; +import java.util.Map; + +/** + * Maintains reference to ad instances for the {@link + * io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin}. + * + *

When an Ad is loaded from Dart, an equivalent ad object is created and maintained here to + * provide access until the ad is disposed. + */ +class AdInstanceManager { + @Nullable private Activity activity; + + @NonNull private final Map ads; + @NonNull private final MethodChannel channel; + + static final String NEXT_GEN_DOMAIN = "com.google.android.libraries.ads.mobile.sdk"; + + /** + * Initializes the ad instance manager. We only need a method channel to start loading ads, but an + * activity must be present in order to attach any ads to the view hierarchy. + */ + AdInstanceManager(@NonNull MethodChannel channel) { + this.channel = channel; + this.ads = new HashMap<>(); + } + + void setActivity(@Nullable Activity activity) { + this.activity = activity; + } + + @Nullable + Activity getActivity() { + return activity; + } + + @Nullable + FlutterAd adForId(int id) { + return ads.get(id); + } + + @Nullable + Integer adIdFor(@NonNull FlutterAd ad) { + for (Integer adId : ads.keySet()) { + if (ads.get(adId) == ad) { + return adId; + } + } + return null; + } + + void trackAd(@NonNull FlutterAd ad, int adId) { + if (ads.get(adId) != null) { + throw new IllegalArgumentException( + String.format("Ad for following adId already exists: %d", adId)); + } + ads.put(adId, ad); + } + + void disposeAd(int adId) { + if (!ads.containsKey(adId)) { + return; + } + FlutterAd ad = ads.get(adId); + if (ad != null) { + ad.dispose(); + } + ads.remove(adId); + } + + void disposeAllAds() { + for (Map.Entry entry : ads.entrySet()) { + if (entry.getValue() != null) { + entry.getValue().dispose(); + } + } + ads.clear(); + } + + void onAdLoaded(int adId, @Nullable ResponseInfo responseInfo) { + Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAdLoaded"); + FlutterResponseInfo flutterResponseInfo = + (responseInfo == null) ? null : new FlutterResponseInfo(responseInfo); + arguments.put("responseInfo", flutterResponseInfo); + invokeOnAdEvent(arguments); + } + + void onAdFailedToLoad(int adId, @NonNull FlutterAd.FlutterLoadAdError error) { + Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAdFailedToLoad"); + arguments.put("loadAdError", error); + invokeOnAdEvent(arguments); + } + + void onAppEvent(int adId, @NonNull String name, @NonNull String data) { + Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAppEvent"); + arguments.put("name", name); + arguments.put("data", data); + invokeOnAdEvent(arguments); + } + + void onAdImpression(int id) { + Map arguments = new HashMap<>(); + arguments.put("adId", id); + arguments.put("eventName", "onAdImpression"); + invokeOnAdEvent(arguments); + } + + void onAdClicked(int id) { + Map arguments = new HashMap<>(); + arguments.put("adId", id); + arguments.put("eventName", "onAdClicked"); + invokeOnAdEvent(arguments); + } + + void onAdOpened(int adId) { + Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAdOpened"); + invokeOnAdEvent(arguments); + } + + void onAdClosed(int adId) { + Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAdClosed"); + invokeOnAdEvent(arguments); + } + + void onRewardedAdUserEarnedReward(int adId, @NonNull FlutterRewardedAd.FlutterRewardItem reward) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onRewardedAdUserEarnedReward"); + arguments.put("rewardItem", reward); + invokeOnAdEvent(arguments); + } + + void onRewardedInterstitialAdUserEarnedReward( + int adId, @NonNull FlutterRewardedAd.FlutterRewardItem reward) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onRewardedInterstitialAdUserEarnedReward"); + arguments.put("rewardItem", reward); + invokeOnAdEvent(arguments); + } + + void onPaidEvent(@NonNull FlutterAd ad, @NonNull FlutterAdValue adValue) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adIdFor(ad)); + arguments.put("eventName", "onPaidEvent"); + arguments.put("valueMicros", adValue.valueMicros); + arguments.put("precision", adValue.precisionType); + arguments.put("currencyCode", adValue.currencyCode); + invokeOnAdEvent(arguments); + } + + static FlutterAdValue comAdValueToFlutterAdValue(@NonNull AdValue adValue) { + int precisionTypeInt = 0; + switch (adValue.getPrecisionType()) { + case ESTIMATED: + precisionTypeInt = 1; + break; + case PUBLISHER_PROVIDED: + precisionTypeInt = 2; + break; + case PRECISE: + precisionTypeInt = 3; + break; + case UNKNOWN: + default: + precisionTypeInt = 0; + break; + } + return new FlutterAdValue( + precisionTypeInt, + adValue.getCurrencyCode(), + adValue.getValueMicros()); + } + + void onFailedToShowFullScreenContent(int adId, @NonNull AdError error) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onFailedToShowFullScreenContent"); + arguments.put("error", new FlutterAdError(error)); + invokeOnAdEvent(arguments); + } + + void onAdShowedFullScreenContent(int adId) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAdShowedFullScreenContent"); + invokeOnAdEvent(arguments); + } + + void onAdDismissedFullScreenContent(int adId) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAdDismissedFullScreenContent"); + invokeOnAdEvent(arguments); + } + + void onAdMetadataChanged(int adId) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onAdMetadataChanged"); + invokeOnAdEvent(arguments); + } + + void onFluidAdHeightChanged(int adId, int height) { + final Map arguments = new HashMap<>(); + arguments.put("adId", adId); + arguments.put("eventName", "onFluidAdHeightChanged"); + arguments.put("height", height); + invokeOnAdEvent(arguments); + } + + boolean showAdWithId(int id) { + final FlutterAd.FlutterOverlayAd ad = (FlutterAd.FlutterOverlayAd) adForId(id); + + if (ad == null) { + return false; + } + + ad.show(); + return true; + } + + /** Invoke the method channel using the UI thread. Otherwise the message gets silently dropped. */ + private void invokeOnAdEvent(final Map arguments) { + new Handler(Looper.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + channel.invokeMethod("onAdEvent", arguments); + } + }); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/BannerAdCreator.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/BannerAdCreator.java new file mode 100644 index 000000000..4e1918318 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/BannerAdCreator.java @@ -0,0 +1,34 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.content.Context; +import androidx.annotation.NonNull; +import com.google.android.libraries.ads.mobile.sdk.banner.AdView; + +/** Creates AdView and AdManagerAdViews for banner ads. */ +public class BannerAdCreator { + + @NonNull private final Context context; + + public BannerAdCreator(@NonNull Context context) { + this.context = context; + } + + /** Create a new {@link AdView}. */ + public AdView createAdView() { + return new AdView(context); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FluidAdManagerBannerAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FluidAdManagerBannerAd.java new file mode 100644 index 000000000..6de58d098 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FluidAdManagerBannerAd.java @@ -0,0 +1,124 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.util.Log; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import android.widget.ScrollView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import io.flutter.plugin.platform.PlatformView; + +/** A subclass of {@link FlutterAdManagerBannerAd} specifically for fluid ad size. */ +final class FluidAdManagerBannerAd extends FlutterBannerAd { + + private static final String TAG = "FluidAdManagerBannerAd"; + + @Nullable private ViewGroup containerView; + + private int height = -1; + + FluidAdManagerBannerAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdManagerAdRequest request, // Kept for compatibility + @NonNull BannerAdCreator bannerAdCreator) { + super( + adId, + manager, + adUnitId, + request, + new FlutterAdSize(AdSize.FLUID), + bannerAdCreator); + } + + @Override + public void onAdLoaded() { + if (adView != null && bannerAd != null) { + adView.addOnLayoutChangeListener( + new OnLayoutChangeListener() { + @Override + public void onLayoutChange( + View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + // Forward the new height to its container. + int newHeight = v.getMeasuredHeight(); + if (newHeight != height) { + manager.onFluidAdHeightChanged(adId, newHeight); + } + height = newHeight; + } + }); + manager.onAdLoaded(adId, bannerAd.getResponseInfo()); + } + } + + @Nullable + @Override + public PlatformView getPlatformView() { + if (adView == null) { + return null; + } + if (containerView != null) { + return new FlutterPlatformView(containerView); + } + // Place the ad view inside a scroll view. This allows the height of the ad view to overflow + // its container so we can calculate the height and send it back to flutter. + ScrollView scrollView = createContainerView(); + if (scrollView == null) { + return null; + } + scrollView.setClipChildren(false); + scrollView.setVerticalScrollBarEnabled(false); + scrollView.setHorizontalScrollBarEnabled(false); + containerView = scrollView; + containerView.addView(adView); + return new FlutterPlatformView(adView); + } + + @Nullable + @VisibleForTesting + ScrollView createContainerView() { + if (manager.getActivity() == null) { + Log.e(TAG, "Tried to create container view before plugin is attached to an activity."); + return null; + } + return new ScrollView(manager.getActivity()); + } + + @Override + void dispose() { + if (adView != null) { + adView.destroy(); + adView = null; + } + if (containerView != null) { + containerView.removeAllViews(); + containerView = null; + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAd.java new file mode 100644 index 000000000..a6fd9b86b --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAd.java @@ -0,0 +1,397 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.AdError; +import com.google.android.libraries.ads.mobile.sdk.common.AdSourceResponseInfo; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.MediationAdError; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import io.flutter.plugin.platform.PlatformView; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +abstract class FlutterAd { + + protected final int adId; + + FlutterAd(int adId) { + this.adId = adId; + } + + /** A {@link FlutterAd} that is overlaid on top of a running application. */ + abstract static class FlutterOverlayAd extends FlutterAd { + abstract void show(); + + abstract void setImmersiveMode(boolean immersiveModeEnabled); + + FlutterOverlayAd(int adId) { + super(adId); + } + } + + /** A wrapper around {@link ResponseInfo}. */ + static class FlutterResponseInfo { + + @Nullable private final String responseId; + @Nullable private final String mediationAdapterClassName; + @NonNull private final List adapterResponses; + @Nullable private final FlutterAdapterResponseInfo loadedAdapterResponseInfo; + @NonNull private final Map responseExtras; + + FlutterResponseInfo(@NonNull ResponseInfo responseInfo) { + this.responseId = responseInfo.getResponseId(); + this.mediationAdapterClassName = responseInfo.getAdapterClassName(); + final List adapterResponseInfos = new ArrayList<>(); + for (AdSourceResponseInfo adapterInfo : responseInfo.getAdSourceResponses()) { + adapterResponseInfos.add(new FlutterAdapterResponseInfo(adapterInfo)); + } + this.adapterResponses = adapterResponseInfos; + if (responseInfo.getLoadedAdSourceResponseInfo() != null) { + this.loadedAdapterResponseInfo = + new FlutterAdapterResponseInfo(responseInfo.getLoadedAdSourceResponseInfo()); + } else { + this.loadedAdapterResponseInfo = null; + } + Map extras = new HashMap<>(); + if (responseInfo.getResponseExtras() != null) { + for (String key : responseInfo.getResponseExtras().keySet()) { + String value = responseInfo.getResponseExtras().getString(key); + extras.put(key, value); + } + } + + this.responseExtras = extras; + } + + FlutterResponseInfo( + @Nullable String responseId, + @Nullable String mediationAdapterClassName, + @NonNull List adapterResponseInfos, + @Nullable FlutterAdapterResponseInfo loadedAdapterResponseInfo, + @NonNull Map responseExtras) { + this.responseId = responseId; + this.mediationAdapterClassName = mediationAdapterClassName; + this.adapterResponses = adapterResponseInfos; + this.loadedAdapterResponseInfo = loadedAdapterResponseInfo; + this.responseExtras = responseExtras; + } + + @Nullable + String getResponseId() { + return responseId; + } + + @Nullable + String getMediationAdapterClassName() { + return mediationAdapterClassName; + } + + @NonNull + List getAdapterResponses() { + return adapterResponses; + } + + @Nullable + FlutterAdapterResponseInfo getLoadedAdapterResponseInfo() { + return loadedAdapterResponseInfo; + } + + @NonNull + Map getResponseExtras() { + return responseExtras; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof FlutterResponseInfo)) { + return false; + } + + FlutterResponseInfo that = (FlutterResponseInfo) obj; + return Objects.equals(responseId, that.responseId) + && Objects.equals(mediationAdapterClassName, that.mediationAdapterClassName) + && Objects.equals(adapterResponses, that.adapterResponses) + && Objects.equals(loadedAdapterResponseInfo, that.loadedAdapterResponseInfo); + } + + @Override + public int hashCode() { + return Objects.hash( + responseId, mediationAdapterClassName, adapterResponses, loadedAdapterResponseInfo); + } + } + + /** A wrapper for {@link AdapterResponseInfo}. */ + static class FlutterAdapterResponseInfo { + + @NonNull private final String adapterClassName; + private final long latencyMillis; + @NonNull private final String description; + @NonNull private final Map adUnitMapping; + @Nullable private FlutterAdError error; + @NonNull private final String adSourceName; + @NonNull private final String adSourceId; + @NonNull private final String adSourceInstanceName; + @NonNull private final String adSourceInstanceId; + + FlutterAdapterResponseInfo(@NonNull AdSourceResponseInfo responseInfo) { + this.adapterClassName = responseInfo.getAdapterClassName(); + this.latencyMillis = responseInfo.getLatencyMillis(); + this.description = responseInfo.toString(); + if (responseInfo.getCredentials() != null) { + this.adUnitMapping = new HashMap<>(); + for (String key : responseInfo.getCredentials().keySet()) { + adUnitMapping.put(key, responseInfo.getCredentials().getString(key)); + } + } else { + adUnitMapping = new HashMap<>(); + } + if (responseInfo.getAdError() != null) { + this.error = new FlutterAdError(responseInfo.getAdError()); + } + adSourceName = responseInfo.getName(); + adSourceId = responseInfo.getId(); + adSourceInstanceName = responseInfo.getInstanceName(); + adSourceInstanceId = responseInfo.getInstanceId(); + } + + FlutterAdapterResponseInfo( + @NonNull String adapterClassName, + long latencyMillis, + @NonNull String description, + @NonNull Map adUnitMapping, + @Nullable FlutterAdError error, + @NonNull String adSourceName, + @NonNull String adSourceId, + @NonNull String adSourceInstanceName, + @NonNull String adSourceInstanceId) { + this.adapterClassName = adapterClassName; + this.latencyMillis = latencyMillis; + this.description = description; + this.adUnitMapping = adUnitMapping; + this.error = error; + this.adSourceName = adSourceName; + this.adSourceId = adSourceId; + this.adSourceInstanceName = adSourceInstanceName; + this.adSourceInstanceId = adSourceInstanceId; + } + + @NonNull + public String getAdapterClassName() { + return adapterClassName; + } + + public long getLatencyMillis() { + return latencyMillis; + } + + @NonNull + public String getDescription() { + return description; + } + + @NonNull + public Map getAdUnitMapping() { + return adUnitMapping; + } + + @Nullable + public FlutterAdError getError() { + return error; + } + + @NonNull + public String getAdSourceName() { + return adSourceName; + } + + @NonNull + public String getAdSourceId() { + return adSourceId; + } + + @NonNull + public String getAdSourceInstanceName() { + return adSourceInstanceName; + } + + @NonNull + public String getAdSourceInstanceId() { + return adSourceInstanceId; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof FlutterAdapterResponseInfo)) { + return false; + } + + final FlutterAdapterResponseInfo that = (FlutterAdapterResponseInfo) obj; + return Objects.equals(adapterClassName, that.adapterClassName) + && latencyMillis == that.latencyMillis + && Objects.equals(description, that.description) + && Objects.equals(error, that.error) + && Objects.equals(adUnitMapping, that.adUnitMapping) + && Objects.equals(adSourceName, that.adSourceName) + && Objects.equals(adSourceId, that.adSourceId) + && Objects.equals(adSourceInstanceName, that.adSourceInstanceName) + && Objects.equals(adSourceInstanceId, that.adSourceInstanceId); + } + + @Override + public int hashCode() { + return Objects.hash( + adapterClassName, + latencyMillis, + description, + error, + adSourceName, + adSourceId, + adSourceInstanceName, + adSourceInstanceId); + } + } + + /** Wrapper for {@link AdError}. */ + static class FlutterAdError { + final int code; + @NonNull final String domain; + @NonNull final String message; + + FlutterAdError(@NonNull AdError error) { + code = error.getCode(); + domain = error.getDomain(); + message = error.getMessage(); + } + + FlutterAdError(@NonNull MediationAdError error) { + code = error.component1(); + domain = error.getDomain(); + message = error.getMessage(); + } + + FlutterAdError(int code, @NonNull String domain, @NonNull String message) { + this.code = code; + this.domain = domain; + this.message = message; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (!(object instanceof FlutterAdError)) { + return false; + } + + final FlutterAdError that = (FlutterAdError) object; + + if (code != that.code) { + return false; + } else if (!domain.equals(that.domain)) { + return false; + } + return message.equals(that.message); + } + + @Override + public int hashCode() { + return Objects.hash(code, domain, message); + } + } + + /** Wrapper for {@link LoadAdError}. */ + static class FlutterLoadAdError { + final int code; + @NonNull final String domain; + @NonNull final String message; + @Nullable FlutterResponseInfo responseInfo; + + FlutterLoadAdError(@NonNull LoadAdError error) { + code = error.getCode().getValue(); + message = error.getMessage(); + + if (error.getResponseInfo() != null) { + responseInfo = new FlutterResponseInfo(error.getResponseInfo()); + domain = responseInfo.getMediationAdapterClassName(); + } else { + domain = "com.google.android.libraries.ads.mobile.sdk"; + } + } + + FlutterLoadAdError( + int code, + @NonNull String domain, + @NonNull String message, + @Nullable FlutterResponseInfo responseInfo) { + this.code = code; + this.domain = domain; + this.message = message; + this.responseInfo = responseInfo; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } else if (!(object instanceof FlutterLoadAdError)) { + return false; + } + + final FlutterLoadAdError that = (FlutterLoadAdError) object; + + if (code != that.code) { + return false; + } else if (!domain.equals(that.domain)) { + return false; + } else if (!Objects.equals(responseInfo, that.responseInfo)) { + return false; + } + return message.equals(that.message); + } + + @Override + public int hashCode() { + return Objects.hash(code, domain, message, responseInfo); + } + } + + abstract void load(); + + /** + * Gets the PlatformView for the ad. Default behavior is to return null. Should be overridden by + * ads with platform views, such as banner and native ads. + */ + @Nullable + PlatformView getPlatformView() { + return null; + }; + + /** + * Invoked when dispose() is called on the corresponding Flutter ad object. This perform any + * necessary cleanup. + */ + abstract void dispose(); +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdInspector.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdInspector.java new file mode 100644 index 000000000..f3099e1fc --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdInspector.java @@ -0,0 +1,37 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.content.Context; +import io.flutter.plugin.common.MethodChannel.Result; + +public final class FlutterAdInspector { + public static void openAdInspector( + Context context, FlutterMobileAdsWrapper flutterMobileAds, Result result) { + flutterMobileAds.openAdInspector( + context, + adInspectorError -> { + if (adInspectorError != null) { + String errorCode = Integer.toString(adInspectorError.getCode().getValue()); + result.error( + errorCode, + adInspectorError.getMessage(), + "com.google.android.libraries.ads.mobile.sdk"); + } else { + result.success(null); + } + }); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java new file mode 100644 index 000000000..9379fdf40 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -0,0 +1,152 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdEventCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdEventCallback; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; +import java.lang.ref.WeakReference; + +/** Callback type to notify when an ad successfully loads. */ +interface FlutterAdLoadedListener { + void onAdLoaded(); +} + +abstract class FlutterAdListener implements AdLoadCallback { + protected final int adId; + @NonNull protected final AdInstanceManager manager; + + FlutterAdListener(int adId, @NonNull AdInstanceManager manager) { + this.adId = adId; + this.manager = manager; + } + + @Override + public void onAdFailedToLoad(LoadAdError loadAdError) { + manager.onAdFailedToLoad(adId, new FlutterAd.FlutterLoadAdError(loadAdError)); + } +} + +/** + * Ad listener for banner ads. Does not override onAdClicked(), since that is only for native ads. + */ +class FlutterBannerAdListener extends FlutterAdListener implements BannerAdEventCallback { + + @NonNull final WeakReference adLoadedListenerWeakReference; + + FlutterBannerAdListener( + int adId, @NonNull AdInstanceManager manager, FlutterAdLoadedListener adLoadedListener) { + super(adId, manager); + adLoadedListenerWeakReference = new WeakReference<>(adLoadedListener); + } + + @Override + public void onAdLoaded(@NonNull BannerAd ad) { + if (adLoadedListenerWeakReference.get() != null) { + adLoadedListenerWeakReference.get().onAdLoaded(); + } + } + + @Override + public void onAdImpression() { + manager.onAdImpression(adId); + } + + @Override + public void onAdClicked() { + manager.onAdClicked(adId); + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + FlutterAd ad = manager.adForId(adId); + if (ad != null) { + manager.onPaidEvent(ad, AdInstanceManager.comAdValueToFlutterAdValue(adValue)); + } + } +} + +/** Listener for native ads. */ +class FlutterNativeAdListener implements NativeAdEventCallback { + private final int adId; + @NonNull private final AdInstanceManager manager; + + FlutterNativeAdListener(int adId, AdInstanceManager manager) { + this.adId = adId; + this.manager = manager; + } + + @Override + public void onAdShowedFullScreenContent() { + manager.onAdOpened(adId); + } + + @Override + public void onAdDismissedFullScreenContent() { + manager.onAdClosed(adId); + } + + @Override + public void onAdFailedToShowFullScreenContent(FullScreenContentError fullScreenContentError) {} + + @Override + public void onAdImpression() { + manager.onAdImpression(adId); + } + + @Override + public void onAdClicked() { + manager.onAdClicked(adId); + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + FlutterAd ad = manager.adForId(adId); + if (ad != null) { + manager.onPaidEvent(ad, AdInstanceManager.comAdValueToFlutterAdValue(adValue)); + } + } +} + +/** {@link NativeAdLoaderCallback} for native ads. */ +class FlutterNativeAdLoadedListener implements NativeAdLoaderCallback { + + private final WeakReference nativeAdWeakReference; + + FlutterNativeAdLoadedListener(FlutterNativeAd flutterNativeAd) { + nativeAdWeakReference = new WeakReference<>(flutterNativeAd); + } + + @Override + public void onNativeAdLoaded(@NonNull NativeAd nativeAd) { + if (nativeAdWeakReference.get() != null) { + nativeAdWeakReference.get().onNativeAdLoaded(nativeAd); + } + } + + @Override + public void onAdFailedToLoad(LoadAdError loadAdError) { + if (nativeAdWeakReference.get() != null) { + nativeAdWeakReference.get().onNativeAdFailed(loadAdError); + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java new file mode 100644 index 000000000..7be103afe --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -0,0 +1,122 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.android.gms.ads.nativead.NativeAdOptions; +import com.google.android.libraries.ads.mobile.sdk.appopen.AppOpenAd; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.VideoOptions; +import com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd.NativeAdType; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoader; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest; +import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardedAd; +import com.google.android.libraries.ads.mobile.sdk.rewardedinterstitial.RewardedInterstitialAd; +import java.util.List; + +/** + * A wrapper around load methods in GMA. This exists mainly to make the Android code more testable. + */ +public class FlutterAdLoader { + + @NonNull private final Context context; + + public FlutterAdLoader(@NonNull Context context) { + this.context = context; + } + + /** Load an app open ad. */ + public void loadAppOpen( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + AppOpenAd.load(adRequest, loadCallback); + } + + /** Load an ad manager app open ad. */ + public void loadAdManagerAppOpen( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + loadAppOpen(adRequest, loadCallback); + } + + /** Load an interstitial ad. */ + public void loadInterstitial( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + InterstitialAd.load(adRequest, loadCallback); + } + + /** Load an ad manager interstitial ad. */ + public void loadAdManagerInterstitial( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + loadInterstitial(adRequest, loadCallback); + } + + /** Load a rewarded ad. */ + public void loadRewarded( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + RewardedAd.load(adRequest, loadCallback); + } + + /** Load a rewarded interstitial ad. */ + public void loadRewardedInterstitial( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + RewardedInterstitialAd.load(adRequest, loadCallback); + } + + /** Load an ad manager rewarded ad. */ + public void loadAdManagerRewarded( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + loadRewarded(adRequest, loadCallback); + } + + /** Load an ad manager rewarded interstitial ad. */ + public void loadAdManagerRewardedInterstitial( + @NonNull AdRequest adRequest, @NonNull AdLoadCallback loadCallback) { + loadRewardedInterstitial(adRequest, loadCallback); + } + + /** Load a native ad. */ + public void loadNativeAd( + @NonNull String adUnitId, + @NonNull NativeAdLoaderCallback nativeAdLoaderCallback, + @NonNull NativeAdOptions nativeAdOptions) { + VideoOptions videoOptions = + nativeAdOptions.getVideoOptions() == null + ? new VideoOptions.Builder().build() + : new VideoOptions.Builder() + .setClickToExpandRequested( + nativeAdOptions.getVideoOptions().getClickToExpandRequested()) + .setCustomControlsRequested( + nativeAdOptions.getVideoOptions().getCustomControlsRequested()) + .setStartMuted(nativeAdOptions.getVideoOptions().getStartMuted()) + .build(); + NativeAdLoader.load( + new NativeAdRequest.Builder(adUnitId, List.of(NativeAdType.NATIVE)) + .setVideoOptions(videoOptions) + .build(), + nativeAdLoaderCallback); + } + + /** Load an ad manager native ad. */ + public void loadAdManagerNativeAd( + @NonNull String adUnitId, + @NonNull NativeAdLoaderCallback nativeAdLoaderCallback, + @NonNull NativeAdOptions nativeAdOptions) { + loadNativeAd(adUnitId, nativeAdLoaderCallback, nativeAdOptions); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java new file mode 100644 index 000000000..da53ddd1a --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java @@ -0,0 +1,185 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Instantiates and serializes {@link AdRequest} for + * the Google Mobile Ads Plugin. + */ +class FlutterAdManagerAdRequest extends FlutterAdRequest { + + @Nullable private final Map customTargeting; + @Nullable private final Map> customTargetingLists; + @Nullable private final String publisherProvidedId; + + static class Builder extends FlutterAdRequest.Builder { + + @Nullable private Map customTargeting; + @Nullable private Map> customTargetingLists; + @Nullable private String publisherProvidedId; + + @CanIgnoreReturnValue + public Builder setCustomTargeting(@Nullable Map customTargeting) { + this.customTargeting = customTargeting; + return this; + } + + @CanIgnoreReturnValue + public Builder setCustomTargetingLists( + @Nullable Map> customTargetingLists) { + this.customTargetingLists = customTargetingLists; + return this; + } + + @CanIgnoreReturnValue + public Builder setPublisherProvidedId(@Nullable String publisherProvidedId) { + this.publisherProvidedId = publisherProvidedId; + return this; + } + + @Override + FlutterAdManagerAdRequest build() { + return new FlutterAdManagerAdRequest( + getKeywords(), + getContentUrl(), + customTargeting, + customTargetingLists, + getNonPersonalizedAds(), + getNeighboringContentUrls(), + getHttpTimeoutMillis(), + publisherProvidedId, + getMediationExtrasIdentifier(), + getMediationNetworkExtrasProvider(), + getAdMobExtras(), + getRequestAgent(), + getMediationExtras()); + } + } + + @SuppressWarnings("deprecation") // Keeping for compatibility + private FlutterAdManagerAdRequest( + @Nullable List keywords, + @Nullable String contentUrl, + @Nullable Map customTargeting, + @Nullable Map> customTargetingLists, + @Nullable Boolean nonPersonalizedAds, + @Nullable List neighboringContentUrls, + @Nullable Integer httpTimeoutMillis, + @Nullable String publisherProvidedId, + @Nullable String mediationExtrasIdentifier, + @Nullable MediationNetworkExtrasProvider mediationNetworkExtrasProvider, + @Nullable Map adMobExtras, + @NonNull String requestAgent, + @Nullable List mediationExtras) { + super( + keywords, + contentUrl, + nonPersonalizedAds, + neighboringContentUrls, + httpTimeoutMillis, + mediationExtrasIdentifier, + mediationNetworkExtrasProvider, + adMobExtras, + requestAgent, + mediationExtras); + this.customTargeting = customTargeting; + this.customTargetingLists = customTargetingLists; + this.publisherProvidedId = publisherProvidedId; + } + + AdRequest asAdManagerAdRequest(String adUnitId) { + final AdRequest.Builder builder = new AdRequest.Builder(adUnitId); + updateAdRequestBuilder(builder, adUnitId); + + if (customTargeting != null) { + for (final Map.Entry entry : customTargeting.entrySet()) { + builder.putCustomTargeting(entry.getKey(), entry.getValue()); + } + } + if (customTargetingLists != null) { + for (final Map.Entry> entry : customTargetingLists.entrySet()) { + builder.putCustomTargeting(entry.getKey(), entry.getValue()); + } + } + if (publisherProvidedId != null) { + builder.setPublisherProvidedId(publisherProvidedId); + } + return builder.build(); + } + + @Override + protected BannerAdRequest.Builder toBannerAdRequestBuilder(String adUnitId, + List allSizes) { + BannerAdRequest.Builder builder = super.toBannerAdRequestBuilder(adUnitId, allSizes); + if (customTargeting != null) { + for (final Map.Entry entry : customTargeting.entrySet()) { + builder.putCustomTargeting(entry.getKey(), entry.getValue()); + } + } + if (customTargetingLists != null) { + for (final Map.Entry> entry : customTargetingLists.entrySet()) { + builder.putCustomTargeting(entry.getKey(), entry.getValue()); + } + } + if (publisherProvidedId != null) { + builder.setPublisherProvidedId(publisherProvidedId); + } + return builder; + } + + @Nullable + protected Map getCustomTargeting() { + return customTargeting; + } + + @Nullable + protected Map> getCustomTargetingLists() { + return customTargetingLists; + } + + @Nullable + protected String getPublisherProvidedId() { + return publisherProvidedId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof FlutterAdManagerAdRequest)) { + return false; + } + + FlutterAdManagerAdRequest request = (FlutterAdManagerAdRequest) o; + return super.equals(o) + && Objects.equals(customTargeting, request.customTargeting) + && Objects.equals(customTargetingLists, request.customTargetingLists); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), customTargeting, customTargetingLists); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java new file mode 100644 index 000000000..b35fdb86e --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java @@ -0,0 +1,40 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import java.util.List; + +/** + * Wrapper around {@link com.google.android.libraries.ads.mobile.sdk.banner.AdView} for the Google Mobile + * Ads Plugin. + */ +class FlutterAdManagerBannerAd extends FlutterBannerAd { + /** + * Constructs a `FlutterAdManagerBannerAd`. + * + *

Call `load()` to instantiate the `AdView` and load the `AdRequest`. `getView()` will return + * null only until `load` is called. + */ + public FlutterAdManagerBannerAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull List sizes, + FlutterAdManagerAdRequest flutterAdRequest, + @NonNull BannerAdCreator bannerAdCreator) { + super(adId, manager, adUnitId, flutterAdRequest, sizes, bannerAdCreator); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerInterstitialAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerInterstitialAd.java new file mode 100644 index 000000000..75c6396db --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerInterstitialAd.java @@ -0,0 +1,45 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; + +/** + * Wrapper around {@link com.google.android.gms.ads.admanager.AdManagerInterstitialAd} for the + * Google Mobile Ads Plugin. + */ +class FlutterAdManagerInterstitialAd extends FlutterInterstitialAd { + private static final String TAG = "FltGAMInterstitialAd"; + + + /** + * Constructs a `FlutterAdManagerInterstitialAd`. + * + *

Call `load()` to instantiate the `AdView` and load the `AdRequest`. `getView()` will return + * null only until `load` is called. + */ + public FlutterAdManagerInterstitialAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdManagerAdRequest request, + @NonNull FlutterAdLoader flutterAdLoader) { + super(adId, manager, adUnitId, request, flutterAdLoader); + } + + void onAppEvent(@NonNull String name, @NonNull String data) { + manager.onAppEvent(adId, name, data); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java new file mode 100644 index 000000000..95ccf9b56 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java @@ -0,0 +1,409 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.os.Bundle; +import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.mediation.MediationExtrasReceiver; +import com.google.android.gms.ads.mediation.admob.AdMobAdapter; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +class FlutterAdRequest { + @Nullable private final List keywords; + @Nullable private final String contentUrl; + @Nullable private final Boolean nonPersonalizedAds; + @Nullable private final List neighboringContentUrls; + @Nullable private final Integer httpTimeoutMillis; + @Nullable private final String mediationExtrasIdentifier; + + @SuppressWarnings("deprecation") // Keeping for compatibility + @Nullable + private final MediationNetworkExtrasProvider mediationNetworkExtrasProvider; + + @Nullable private final Map adMobExtras; + @NonNull private final String requestAgent; + @Nullable private final List mediationExtras; + + protected static class Builder { + @Nullable private List keywords; + @Nullable private String contentUrl; + @Nullable private Boolean nonPersonalizedAds; + @Nullable private List neighboringContentUrls; + @Nullable private Integer httpTimeoutMillis; + @Nullable private String mediationExtrasIdentifier; + + @SuppressWarnings("deprecation") // Keeping for compatibility + @Nullable + private MediationNetworkExtrasProvider mediationNetworkExtrasProvider; + + @Nullable private Map adMobExtras; + @NonNull private String requestAgent; + + @Nullable private List mediationExtras; + + @CanIgnoreReturnValue + Builder setRequestAgent(String requestAgent) { + this.requestAgent = requestAgent; + return this; + } + + @CanIgnoreReturnValue + Builder setKeywords(@Nullable List keywords) { + this.keywords = keywords; + return this; + } + + @CanIgnoreReturnValue + Builder setContentUrl(@Nullable String contentUrl) { + this.contentUrl = contentUrl; + return this; + } + + @CanIgnoreReturnValue + Builder setNonPersonalizedAds(@Nullable Boolean nonPersonalizedAds) { + this.nonPersonalizedAds = nonPersonalizedAds; + return this; + } + + @CanIgnoreReturnValue + Builder setNeighboringContentUrls(@Nullable List neighboringContentUrls) { + this.neighboringContentUrls = neighboringContentUrls; + return this; + } + + @CanIgnoreReturnValue + Builder setHttpTimeoutMillis(@Nullable Integer httpTimeoutMillis) { + this.httpTimeoutMillis = httpTimeoutMillis; + return this; + } + + @CanIgnoreReturnValue + Builder setMediationNetworkExtrasIdentifier(@Nullable String mediationExtrasIdentifier) { + this.mediationExtrasIdentifier = mediationExtrasIdentifier; + return this; + } + + @CanIgnoreReturnValue + @SuppressWarnings("deprecation") // Keeping for compatibility + Builder setMediationNetworkExtrasProvider( + @Nullable MediationNetworkExtrasProvider mediationNetworkExtrasProvider) { + this.mediationNetworkExtrasProvider = mediationNetworkExtrasProvider; + return this; + } + + @CanIgnoreReturnValue + Builder setAdMobExtras(@Nullable Map adMobExtras) { + this.adMobExtras = adMobExtras; + return this; + } + + @CanIgnoreReturnValue + Builder setMediationExtras(@Nullable List mediationExtras) { + this.mediationExtras = mediationExtras; + return this; + } + + @Nullable + protected List getKeywords() { + return keywords; + } + + @Nullable + protected String getContentUrl() { + return contentUrl; + } + + @Nullable + protected Boolean getNonPersonalizedAds() { + return nonPersonalizedAds; + } + + @Nullable + protected List getNeighboringContentUrls() { + return neighboringContentUrls; + } + + @Nullable + protected Integer getHttpTimeoutMillis() { + return httpTimeoutMillis; + } + + @Nullable + protected String getMediationExtrasIdentifier() { + return mediationExtrasIdentifier; + } + + @SuppressWarnings("deprecation") // Keeping for compatibility + @Nullable + protected MediationNetworkExtrasProvider getMediationNetworkExtrasProvider() { + return mediationNetworkExtrasProvider; + } + + @Nullable + protected Map getAdMobExtras() { + return adMobExtras; + } + + @NonNull + protected String getRequestAgent() { + return requestAgent; + } + + @Nullable + protected List getMediationExtras() { + return mediationExtras; + } + + FlutterAdRequest build() { + return new FlutterAdRequest( + keywords, + contentUrl, + nonPersonalizedAds, + neighboringContentUrls, + httpTimeoutMillis, + mediationExtrasIdentifier, + mediationNetworkExtrasProvider, + adMobExtras, + requestAgent, + mediationExtras); + } + } + + @SuppressWarnings("deprecation") // Keeping for compatibility + protected FlutterAdRequest( + @Nullable List keywords, + @Nullable String contentUrl, + @Nullable Boolean nonPersonalizedAds, + @Nullable List neighboringContentUrls, + @Nullable Integer httpTimeoutMillis, + @Nullable String mediationExtrasIdentifier, + @Nullable MediationNetworkExtrasProvider mediationNetworkExtrasProvider, + @Nullable Map adMobExtras, + String requestAgent, + @Nullable List mediationExtras) { + this.keywords = keywords; + this.contentUrl = contentUrl; + this.nonPersonalizedAds = nonPersonalizedAds; + this.neighboringContentUrls = neighboringContentUrls; + this.httpTimeoutMillis = httpTimeoutMillis; + this.mediationExtrasIdentifier = mediationExtrasIdentifier; + this.mediationNetworkExtrasProvider = mediationNetworkExtrasProvider; + this.adMobExtras = adMobExtras; + this.requestAgent = requestAgent; + this.mediationExtras = mediationExtras; + } + + /** Adds network extras to the ad request builder, if any. */ + private void addNetworkExtras( + AdRequest.Builder builder, String adUnitId) { + Map, Bundle> networkExtras = new HashMap<>(); + if (mediationExtras != null) { + for (FlutterMediationExtras flutterExtras : mediationExtras) { + Pair, Bundle> pair = + flutterExtras.getMediationExtras(); + networkExtras.put(pair.first, pair.second); + } + } else if (mediationNetworkExtrasProvider != null) { + Map, Bundle> providedExtras = + mediationNetworkExtrasProvider.getMediationExtras(adUnitId, mediationExtrasIdentifier); + networkExtras.putAll(providedExtras); + } + + if (adMobExtras != null && !adMobExtras.isEmpty()) { + Bundle adMobBundle = new Bundle(); + for (Map.Entry extra : adMobExtras.entrySet()) { + adMobBundle.putString(extra.getKey(), extra.getValue()); + } + networkExtras.put(AdMobAdapter.class, adMobBundle); + } + + if (nonPersonalizedAds != null && nonPersonalizedAds) { + Bundle adMobBundle = networkExtras.get(AdMobAdapter.class); + if (adMobBundle == null) { + adMobBundle = new Bundle(); + } + adMobBundle.putString("npa", "1"); + networkExtras.put(AdMobAdapter.class, adMobBundle); + } + + for (Entry, Bundle> entry : networkExtras.entrySet()) { + builder.putAdSourceExtrasBundle(entry.getKey(), entry.getValue()); + } + } + + /** Updates the {@link AdRequest.Builder} with the properties in this {@link FlutterAdRequest}. */ + protected AdRequest.Builder updateAdRequestBuilder(AdRequest.Builder builder, String adUnitId) { + if (keywords != null) { + for (final String keyword : keywords) { + builder.addKeyword(keyword); + } + } + if (contentUrl != null) { + builder.setContentUrl(contentUrl); + } + addNetworkExtras(builder, adUnitId); + if (neighboringContentUrls != null) { + builder.setNeighboringContentUrls(new HashSet(neighboringContentUrls)); + } + builder.setRequestAgent(requestAgent); + return builder; + } + + protected BannerAdRequest.Builder toBannerAdRequestBuilder(String adUnitId, List allSizes) { + BannerAdRequest.Builder builder = new BannerAdRequest.Builder(adUnitId, allSizes); + if (keywords != null) { + for (final String keyword : keywords) { + builder.addKeyword(keyword); + } + } + if (contentUrl != null) { + builder.setContentUrl(contentUrl); + } + + Map, Bundle> networkExtras = new HashMap<>(); + if (mediationExtras != null) { + for (FlutterMediationExtras flutterExtras : mediationExtras) { + Pair, Bundle> pair = + flutterExtras.getMediationExtras(); + networkExtras.put(pair.first, pair.second); + } + } else if (mediationNetworkExtrasProvider != null) { + Map, Bundle> providedExtras = + mediationNetworkExtrasProvider.getMediationExtras(adUnitId, mediationExtrasIdentifier); + networkExtras.putAll(providedExtras); + } + + if (adMobExtras != null && !adMobExtras.isEmpty()) { + Bundle adMobBundle = new Bundle(); + for (Map.Entry extra : adMobExtras.entrySet()) { + adMobBundle.putString(extra.getKey(), extra.getValue()); + } + networkExtras.put(AdMobAdapter.class, adMobBundle); + } + + if (nonPersonalizedAds != null && nonPersonalizedAds) { + Bundle adMobBundle = networkExtras.get(AdMobAdapter.class); + if (adMobBundle == null) { + adMobBundle = new Bundle(); + } + adMobBundle.putString("npa", "1"); + networkExtras.put(AdMobAdapter.class, adMobBundle); + } + + for (Entry, Bundle> entry : networkExtras.entrySet()) { + builder.putAdSourceExtrasBundle(entry.getKey(), entry.getValue()); + } + + if (neighboringContentUrls != null) { + builder.setNeighboringContentUrls(new HashSet(neighboringContentUrls)); + } + builder.setRequestAgent(requestAgent); + return builder; + } + + AdRequest asAdRequest(String adUnitId) { + return ((AdRequest.Builder) updateAdRequestBuilder(new AdRequest.Builder(adUnitId), adUnitId)) + .build(); + } + + @Nullable + protected List getKeywords() { + return keywords; + } + + @Nullable + protected String getContentUrl() { + return contentUrl; + } + + @Nullable + protected Boolean getNonPersonalizedAds() { + return nonPersonalizedAds; + } + + @Nullable + protected List getNeighboringContentUrls() { + return neighboringContentUrls; + } + + @Nullable + protected Integer getHttpTimeoutMillis() { + return httpTimeoutMillis; + } + + @Nullable + protected String getMediationExtrasIdentifier() { + return mediationExtrasIdentifier; + } + + @Nullable + protected Map getAdMobExtras() { + return adMobExtras; + } + + @NonNull + protected String getRequestAgent() { + return requestAgent; + } + + @Nullable + protected List getMediationExtras() { + return mediationExtras; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof FlutterAdRequest)) { + return false; + } + + FlutterAdRequest request = (FlutterAdRequest) o; + return Objects.equals(keywords, request.keywords) + && Objects.equals(contentUrl, request.contentUrl) + && Objects.equals(nonPersonalizedAds, request.nonPersonalizedAds) + && Objects.equals(neighboringContentUrls, request.neighboringContentUrls) + && Objects.equals(httpTimeoutMillis, request.httpTimeoutMillis) + && Objects.equals(mediationExtrasIdentifier, request.mediationExtrasIdentifier) + && Objects.equals(mediationNetworkExtrasProvider, request.mediationNetworkExtrasProvider) + && Objects.equals(adMobExtras, request.adMobExtras) + && Objects.equals(mediationExtras, request.mediationExtras); + } + + @Override + public int hashCode() { + return Objects.hash( + keywords, + contentUrl, + nonPersonalizedAds, + neighboringContentUrls, + httpTimeoutMillis, + mediationExtrasIdentifier, + mediationNetworkExtrasProvider, + mediationExtras); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java new file mode 100644 index 000000000..536d7e92a --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java @@ -0,0 +1,200 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; + +class FlutterAdSize { + @NonNull final AdSize size; + final int width; + final int height; + + /** Wrapper around static methods for {@link com.google.android.gms.ads.AdSize}. */ + static class AdSizeFactory { + + @SuppressWarnings("deprecation") + AdSize getPortraitAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getPortraitAnchoredAdaptiveBannerAdSize(context, width); + } + + @SuppressWarnings("deprecation") + AdSize getLandscapeAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLandscapeAnchoredAdaptiveBannerAdSize(context, width); + } + + @SuppressWarnings("deprecation") + AdSize getCurrentOrientationAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, width); + } + + AdSize getLargePortraitAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLargePortraitAnchoredAdaptiveBannerAdSize(context, width); + } + + AdSize getLargeLandscapeAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLargeLandscapeAnchoredAdaptiveBannerAdSize(context, width); + } + + AdSize getLargeAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLargeAnchoredAdaptiveBannerAdSize(context, width); + } + + AdSize getCurrentOrientationInlineAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getCurrentOrientationInlineAdaptiveBannerAdSize(context, width); + } + + AdSize getLandscapeInlineAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLandscapeInlineAdaptiveBannerAdSize(context, width); + } + + AdSize getPortraitInlineAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getPortraitInlineAdaptiveBannerAdSize(context, width); + } + + AdSize getInlineAdaptiveBannerAdSize(int width, int maxHeight) { + return AdSize.getInlineAdaptiveBannerAdSize(width, maxHeight); + } + } + + static class AnchoredAdaptiveBannerAdSize extends FlutterAdSize { + final String orientation; + + @NonNull + private static AdSize getAdSize( + @NonNull Context context, + @NonNull AdSizeFactory factory, + @Nullable String orientation, + int width, + boolean isLarge) { + if (orientation == null) { + if (isLarge) { + return factory.getLargeAnchoredAdaptiveBannerAdSize(context, width); + } + return factory.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, width); + } else if (orientation.equals("portrait")) { + if (isLarge) { + return factory.getLargePortraitAnchoredAdaptiveBannerAdSize(context, width); + } + return factory.getPortraitAnchoredAdaptiveBannerAdSize(context, width); + } else if (orientation.equals("landscape")) { + if (isLarge) { + return factory.getLargeLandscapeAnchoredAdaptiveBannerAdSize(context, width); + } + return factory.getLandscapeAnchoredAdaptiveBannerAdSize(context, width); + } else { + throw new IllegalArgumentException("Unexpected value for orientation: " + orientation); + } + } + + AnchoredAdaptiveBannerAdSize( + @NonNull Context context, + @NonNull AdSizeFactory factory, + @Nullable String orientation, + int width, + boolean isLarge) { + super(getAdSize(context, factory, orientation, width, isLarge)); + this.orientation = orientation; + } + } + + static class SmartBannerAdSize extends FlutterAdSize { + + @SuppressWarnings("deprecation") // Smart banner is already deprecated in Dart. + SmartBannerAdSize() { + super(AdSize.FLUID); + } + } + + static class FluidAdSize extends FlutterAdSize { + + FluidAdSize() { + super(AdSize.FLUID); + } + } + + static class InlineAdaptiveBannerAdSize extends FlutterAdSize { + + @Nullable final Integer orientation; + @Nullable final Integer maxHeight; + + private static AdSize getAdSize( + @NonNull AdSizeFactory adSizeFactory, + @NonNull Context context, + int width, + @Nullable Integer orientation, + @Nullable Integer maxHeight) { + if (orientation != null) { + return orientation == 0 + ? adSizeFactory.getPortraitInlineAdaptiveBannerAdSize(context, width) + : adSizeFactory.getLandscapeInlineAdaptiveBannerAdSize(context, width); + } else if (maxHeight != null) { + return adSizeFactory.getInlineAdaptiveBannerAdSize(width, maxHeight); + } else { + return adSizeFactory.getCurrentOrientationInlineAdaptiveBannerAdSize(context, width); + } + } + + InlineAdaptiveBannerAdSize( + @NonNull AdSizeFactory adSizeFactory, + @NonNull Context context, + int width, + @Nullable Integer orientation, + @Nullable Integer maxHeight) { + super(getAdSize(adSizeFactory, context, width, orientation, maxHeight)); + this.orientation = orientation; + this.maxHeight = maxHeight; + } + } + + FlutterAdSize(int width, int height) { + this(new AdSize(width, height)); + } + + FlutterAdSize(@NonNull AdSize size) { + this.size = size; + this.width = size.getWidth(); + this.height = size.getHeight(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof FlutterAdSize)) { + return false; + } + + final FlutterAdSize that = (FlutterAdSize) o; + + if (width != that.width) { + return false; + } + return height == that.height; + } + + @Override + public int hashCode() { + int result = width; + result = 31 * result + height; + return result; + } + + public AdSize getAdSize() { + return size; + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdapterStatus.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdapterStatus.java new file mode 100644 index 000000000..edc906106 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdapterStatus.java @@ -0,0 +1,92 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import com.google.android.libraries.ads.mobile.sdk.initialization.AdapterStatus; + +/** + * Wrapper around {@link com.google.android.gms.ads.initialization.AdapterStatus} for the Flutter + * Google Mobile Ads Plugin. + */ +class FlutterAdapterStatus { + @NonNull final AdapterInitializationState state; + @NonNull final String description; + @NonNull final Number latency; + + /** + * Represents {@link com.google.android.gms.ads.initialization.AdapterStatus.State} for the + * Flutter Google Mobile Ads Plugin. + */ + enum AdapterInitializationState { + NOT_READY, + READY + } + + FlutterAdapterStatus( + @NonNull AdapterInitializationState state, + @NonNull String description, + @NonNull Number latency) { + this.state = state; + this.description = description; + this.latency = latency; + } + + FlutterAdapterStatus(@NonNull AdapterStatus status) { + switch (status.getInitializationState()) { + case NOT_STARTED: + case FAILED: + case INITIALIZING: + case TIMED_OUT: + this.state = AdapterInitializationState.NOT_READY; + break; + case COMPLETE: + this.state = AdapterInitializationState.READY; + break; + default: + final String message = + String.format("Unable to handle state: %s", status.getInitializationState()); + throw new IllegalArgumentException(message); + } + + this.description = status.getDescription(); + this.latency = status.getLatency(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof FlutterAdapterStatus)) { + return false; + } + + final FlutterAdapterStatus that = (FlutterAdapterStatus) o; + if (state != that.state) { + return false; + } else if (!description.equals(that.description)) { + return false; + } + return latency.equals(that.latency); + } + + @Override + public int hashCode() { + int result = state.hashCode(); + result = 31 * result + description.hashCode(); + result = 31 * result + latency.hashCode(); + return result; + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAppOpenAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAppOpenAd.java new file mode 100644 index 000000000..59d83ee31 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAppOpenAd.java @@ -0,0 +1,190 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.appopen.AppOpenAd; +import com.google.android.libraries.ads.mobile.sdk.appopen.AppOpenAdEventCallback; +import com.google.android.gms.ads.AdError; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import io.flutter.util.Preconditions; +import java.lang.ref.WeakReference; + +/** A wrapper for {@link com.google.android.gms.ads.appopen.AppOpenAd}. */ +class FlutterAppOpenAd extends FlutterAd.FlutterOverlayAd { + + private static final String TAG = "FlutterAppOpenAd"; + + @NonNull private final AdInstanceManager manager; + @NonNull private final String adUnitId; + @Nullable private final FlutterAdRequest request; + @Nullable private final FlutterAdManagerAdRequest adManagerAdRequest; + @Nullable private AppOpenAd ad; + @NonNull private final FlutterAdLoader flutterAdLoader; + + FlutterAppOpenAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @Nullable FlutterAdRequest request, + @Nullable FlutterAdManagerAdRequest adManagerAdRequest, + @NonNull FlutterAdLoader flutterAdLoader) { + super(adId); + Preconditions.checkState( + request != null || adManagerAdRequest != null, + "One of request and adManagerAdRequest must be non-null."); + this.manager = manager; + this.adUnitId = adUnitId; + this.request = request; + this.adManagerAdRequest = adManagerAdRequest; + this.flutterAdLoader = flutterAdLoader; + } + + @Override + void load() { + if (request != null) { + flutterAdLoader.loadAppOpen( + request.asAdRequest(adUnitId), new DelegatingAppOpenAdLoadCallback(this)); + } else if (adManagerAdRequest != null) { + flutterAdLoader.loadAdManagerAppOpen( + adManagerAdRequest.asAdManagerAdRequest(adUnitId), + new DelegatingAppOpenAdLoadCallback(this)); + } + } + + private void onAdLoaded(@NonNull AppOpenAd ad) { + this.ad = ad; + manager.onAdLoaded(adId, ad.getResponseInfo()); + } + + private void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + manager.onAdFailedToLoad(adId, new FlutterLoadAdError(loadAdError)); + } + + @Override + void show() { + if (ad == null) { + Log.w(TAG, "Tried to show app open ad before it was loaded"); + return; + } + if (manager.getActivity() == null) { + Log.e(TAG, "Tried to show app open ad before activity was bound to the plugin."); + return; + } + ad.setAdEventCallback(new DelegatingAppOpenAdLoadCallback(this)); + ad.show(manager.getActivity()); + } + + @Override + void setImmersiveMode(boolean immersiveModeEnabled) { + if (ad == null) { + Log.w(TAG, "Tried to set immersive mode on app open ad before it was loaded"); + return; + } + ad.setImmersiveMode(immersiveModeEnabled); + } + + @Override + void dispose() { + ad = null; + } + + /** An AppOpenAdLoadCallback that just forwards events to a delegate. */ + private static final class DelegatingAppOpenAdLoadCallback + implements AdLoadCallback, AppOpenAdEventCallback { + + private final WeakReference delegate; + + DelegatingAppOpenAdLoadCallback(FlutterAppOpenAd delegate) { + this.delegate = new WeakReference<>(delegate); + } + + @Override + public void onAdLoaded(@NonNull AppOpenAd appOpenAd) { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.onAdLoaded(appOpenAd); + } + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.onAdFailedToLoad(loadAdError); + } + } + + @Override + public void onAdFailedToShowFullScreenContent(@NonNull FullScreenContentError adError) { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.manager.onFailedToShowFullScreenContent( + ad.adId, + new AdError( + adError.getCode().getValue(), + adError.getMessage(), + AdInstanceManager.NEXT_GEN_DOMAIN)); + } + } + + @Override + public void onAdShowedFullScreenContent() { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdShowedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdDismissedFullScreenContent() { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdDismissedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdImpression() { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdImpression(ad.adId); + } + } + + @Override + public void onAdClicked() { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdClicked(ad.adId); + } + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + FlutterAppOpenAd ad = delegate.get(); + if (ad != null) { + ad.manager.onPaidEvent( + ad, + AdInstanceManager.comAdValueToFlutterAdValue(adValue)); + } + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java new file mode 100644 index 000000000..9f9fc992d --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java @@ -0,0 +1,165 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.app.Activity; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.AdView; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.util.Preconditions; +import java.util.ArrayList; +import java.util.List; + +/** + * A wrapper for {@link AdView}. + */ +class FlutterBannerAd extends FlutterAd implements FlutterAdLoadedListener { + + private static final String TAG = "FlutterBannerAd"; + + @NonNull + protected final AdInstanceManager manager; + @NonNull + protected final String adUnitId; + @NonNull + private final List sizes; + @NonNull private final FlutterAdRequest request; + @NonNull + protected final BannerAdCreator bannerAdCreator; + @Nullable + protected BannerAd bannerAd; + @Nullable + protected AdView adView; + + /** + * Constructs the FlutterBannerAd. + */ + public FlutterBannerAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + FlutterAdRequest flutterAdRequest, // Kept for compatibility + @NonNull FlutterAdSize size, + @NonNull BannerAdCreator bannerAdCreator) { + this(adId, manager, adUnitId, flutterAdRequest, List.of(size), bannerAdCreator); + } + + public FlutterBannerAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdRequest request, + @NonNull List sizes, + @NonNull BannerAdCreator bannerAdCreator) { + super(adId); + Preconditions.checkNotNull(manager); + Preconditions.checkNotNull(adUnitId); + Preconditions.checkNotNull(sizes); + this.manager = manager; + this.adUnitId = adUnitId; + this.request = Preconditions.checkNotNull(request); + this.sizes = sizes; + this.bannerAdCreator = bannerAdCreator; + } + + @Override + public void onAdLoaded() { + if (bannerAd != null) { + manager.onAdLoaded(adId, bannerAd.getResponseInfo()); + } + } + + @Override + void load() { + adView = bannerAdCreator.createAdView(); + final List allSizes = new ArrayList(); + for (int i = 0; i < sizes.size(); i++) { + FlutterAdSize flutterAdSize = sizes.get(i); + if (flutterAdSize.getAdSize().isFluid()) { + allSizes.add(AdSize.FLUID); + continue; + } + AdSize adSize = + new AdSize(flutterAdSize.getAdSize().getWidth(), flutterAdSize.getAdSize().getHeight()); + allSizes.add(adSize); + } + BannerAdRequest adRequest = request.toBannerAdRequestBuilder(adUnitId, allSizes).build(); + adView.loadAd( + adRequest, + new AdLoadCallback() { + @Override + public void onAdLoaded(@NonNull BannerAd ad) { + Activity activity = manager.getActivity(); + if (activity == null) { + Log.e(TAG, "Tried to load banner ad before plugin is attached to an activity."); + return; + } + bannerAd = ad; + bannerAd.setAdEventCallback( + new FlutterBannerAdListener(adId, manager, FlutterBannerAd.this)); + adView.registerBannerAd(bannerAd, activity); + FlutterBannerAd.this.onAdLoaded(); + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError adError) { + bannerAd = null; + } + }); + } + + @Nullable + @Override + public PlatformView getPlatformView() { + if (adView == null) { + return null; + } + return new FlutterPlatformView(adView); + } + + @Override + void dispose() { + if (bannerAd != null) { + bannerAd = null; + } + if (adView != null) { + adView.destroy(); + } + } + + @Nullable + FlutterAdSize getAdSize() { + if (adView == null || adView.getBannerAd().getAdSize() == null) { + return null; + } + return new FlutterAdSize( + adView.getBannerAd().getAdSize().getWidth(), adView.getBannerAd().getAdSize().getHeight()); + } + + boolean isCollapsible() { + if (bannerAd == null) { + Log.e(TAG, "Banner ad is not loaded or has been disposed."); + return false; + } + return bannerAd.isCollapsible(); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterFullScreenContentCallback.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterFullScreenContentCallback.java new file mode 100644 index 000000000..0822fe27b --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterFullScreenContentCallback.java @@ -0,0 +1,75 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import com.google.android.gms.ads.AdError; +import com.google.android.libraries.ads.mobile.sdk.common.AdEventCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError; + +/** + * Flutter implementation of {@link FullScreenContentCallback}. Forwards events to + * AdInstanceManager. + */ +class FlutterFullScreenContentCallback implements AdEventCallback { + + @NonNull protected final AdInstanceManager manager; + + @NonNull protected final int adId; + + public FlutterFullScreenContentCallback(@NonNull AdInstanceManager manager, int adId) { + this.manager = manager; + this.adId = adId; + } + + @Override + public void onAdFailedToShowFullScreenContent(@NonNull FullScreenContentError adError) { + manager.onFailedToShowFullScreenContent( + adId, + new AdError( + adError.getCode().getValue(), + adError.getMessage(), + "com.google.android.libraries.ads.mobile.sdk")); + } + + @Override + public void onAdShowedFullScreenContent() { + manager.onAdShowedFullScreenContent(adId); + } + + @Override + public void onAdDismissedFullScreenContent() { + manager.onAdDismissedFullScreenContent(adId); + } + + @Override + public void onAdImpression() { + manager.onAdImpression(adId); + } + + @Override + public void onAdClicked() { + manager.onAdClicked(adId); + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + FlutterAd ad = manager.adForId(adId); + if (ad != null) { + manager.onPaidEvent(ad, AdInstanceManager.comAdValueToFlutterAdValue(adValue)); + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInitializationListener.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInitializationListener.java new file mode 100644 index 000000000..d757a0334 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInitializationListener.java @@ -0,0 +1,54 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import com.google.android.libraries.ads.mobile.sdk.initialization.InitializationStatus; +import com.google.android.libraries.ads.mobile.sdk.initialization.OnAdapterInitializationCompleteListener; +import io.flutter.plugin.common.MethodChannel.Result; +import java.lang.reflect.Method; + +/** + * An {@link OnAdapterInitializationCompleteListener} that invokes result.success() at most once. + */ +public final class FlutterInitializationListener + implements OnAdapterInitializationCompleteListener { + + private final Result result; + private boolean isInitializationCompleted; + + FlutterInitializationListener(@NonNull final Result result) { + this.result = result; + isInitializationCompleted = false; + } + + @Override + public void onAdapterInitializationComplete(@NonNull InitializationStatus initializationStatus) { + // Make sure not to invoke this more than once, since Dart will throw an exception if success + // is invoked more than once. See b/193418432. + if (isInitializationCompleted) { + return; + } + try { + Class clazz = Class.forName("com.google.android.gms.ads.MobileAds"); + Method method = clazz.getDeclaredMethod("setPlugin", String.class); + method.setAccessible(true); + method.invoke(null, Constants.REQUEST_AGENT_PREFIX_VERSIONED); + } catch (Exception ignored) { + } + result.success(new FlutterInitializationStatus(initializationStatus)); + isInitializationCompleted = true; + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInitializationStatus.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInitializationStatus.java new file mode 100644 index 000000000..03576bb2a --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInitializationStatus.java @@ -0,0 +1,44 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import com.google.android.libraries.ads.mobile.sdk.initialization.AdapterStatus; +import com.google.android.libraries.ads.mobile.sdk.initialization.InitializationStatus; +import java.util.HashMap; +import java.util.Map; + +/** + * Wrapper around {@link com.google.android.gms.ads.initialization.InitializationStatus} for the + * Flutter Google Mobile Ads Plugin. + */ +class FlutterInitializationStatus { + @NonNull final Map adapterStatuses; + + FlutterInitializationStatus(@NonNull Map adapterStatuses) { + this.adapterStatuses = adapterStatuses; + } + + FlutterInitializationStatus(@NonNull InitializationStatus initializationStatus) { + final HashMap newStatusMap = new HashMap<>(); + final Map adapterStatusMap = initializationStatus.getAdapterStatusMap(); + + for (Map.Entry status : adapterStatusMap.entrySet()) { + newStatusMap.put(status.getKey(), new FlutterAdapterStatus(status.getValue())); + } + + this.adapterStatuses = newStatusMap; + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInterstitialAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInterstitialAd.java new file mode 100644 index 000000000..dbd528596 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterInterstitialAd.java @@ -0,0 +1,188 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.AdError; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAd; +import com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAdEventCallback; +import java.lang.ref.WeakReference; + +class FlutterInterstitialAd extends FlutterAd.FlutterOverlayAd { + private static final String TAG = "FlutterInterstitialAd"; + + @NonNull protected final AdInstanceManager manager; + @NonNull private final String adUnitId; + @NonNull private final FlutterAdRequest request; + @Nullable private InterstitialAd ad; + @NonNull private final FlutterAdLoader flutterAdLoader; + + public FlutterInterstitialAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdRequest request, + @NonNull FlutterAdLoader flutterAdLoader) { + super(adId); + this.manager = manager; + this.adUnitId = adUnitId; + this.request = request; + this.flutterAdLoader = flutterAdLoader; + } + + @Override + void load() { + if (manager != null && adUnitId != null && request != null) { + flutterAdLoader.loadInterstitial( + request.asAdRequest(adUnitId), new DelegatingInterstitialAdLoadCallback(this)); + } + } + + void onAdLoaded(InterstitialAd ad) { + this.ad = ad; + manager.onAdLoaded(adId, ad.getResponseInfo()); + } + + void onAdFailedToLoad(LoadAdError loadAdError) { + manager.onAdFailedToLoad(adId, new FlutterAd.FlutterLoadAdError(loadAdError)); + } + + @Override + void dispose() { + ad = null; + } + + @Override + public void show() { + if (ad == null) { + Log.e(TAG, "Error showing interstitial - the interstitial ad wasn't loaded yet."); + return; + } + if (manager.getActivity() == null) { + Log.e(TAG, "Tried to show interstitial before activity was bound to the plugin."); + return; + } + ad.setAdEventCallback(new DelegatingInterstitialAdLoadCallback(this)); + ad.show(manager.getActivity()); + } + + @Override + public void setImmersiveMode(boolean immersiveModeEnabled) { + if (ad == null) { + Log.e( + TAG, + "Error setting immersive mode in interstitial ad - the interstitial ad wasn't loaded" + + " yet."); + return; + } + ad.setImmersiveMode(immersiveModeEnabled); + } + + /** An AdLoadCallback that just forwards events to a delegate. */ + private static final class DelegatingInterstitialAdLoadCallback + implements AdLoadCallback, InterstitialAdEventCallback { + + private final WeakReference delegate; + + DelegatingInterstitialAdLoadCallback(FlutterInterstitialAd delegate) { + this.delegate = new WeakReference<>(delegate); + } + + @Override + public void onAdLoaded(@NonNull InterstitialAd interstitialAd) { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.onAdLoaded(interstitialAd); + } + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.onAdFailedToLoad(loadAdError); + } + } + + @Override + public void onAdFailedToShowFullScreenContent(@NonNull FullScreenContentError adError) { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onFailedToShowFullScreenContent( + ad.adId, + new AdError( + adError.getCode().getValue(), + adError.getMessage(), + AdInstanceManager.NEXT_GEN_DOMAIN)); + } + } + + @Override + public void onAdShowedFullScreenContent() { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdShowedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdDismissedFullScreenContent() { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdDismissedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdImpression() { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdImpression(ad.adId); + } + } + + @Override + public void onAdClicked() { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdClicked(ad.adId); + } + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onPaidEvent( + ad, + AdInstanceManager.comAdValueToFlutterAdValue(adValue)); + } + } + + @Override + public void onAppEvent(@NonNull String name, @NonNull String data) { + FlutterInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAppEvent(ad.adId, name, data); + } + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java new file mode 100644 index 000000000..fa07a00a5 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java @@ -0,0 +1,136 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.MobileAds; +import com.google.android.libraries.ads.mobile.sdk.common.OnAdInspectorClosedListener; +import com.google.android.libraries.ads.mobile.sdk.common.RequestConfiguration; +import com.google.android.libraries.ads.mobile.sdk.initialization.InitializationConfig; +import com.google.android.libraries.ads.mobile.sdk.initialization.OnAdapterInitializationCompleteListener; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi; + +/** A wrapper around static methods in {@link com.google.android.gms.ads.MobileAds}. */ +public class FlutterMobileAdsWrapper { + + private static final String TAG = "FlutterMobileAdsWrapper"; + private static final String APPLICATION_ID_KEY = "com.google.android.gms.ads.APPLICATION_ID"; + private static boolean disableMediationAdapterInitialization = false; + + public FlutterMobileAdsWrapper() {} + + /** Initializes the sdk. */ + public void initialize( + @NonNull Context context, @NonNull OnAdapterInitializationCompleteListener listener) { + final String appId = getApplicationMetaData(context, APPLICATION_ID_KEY); + + if (appId == null) { + Log.e(TAG, "Application ID is null. Cannot initialize the Google Mobile Ads SDK."); + return; + } + + InitializationConfig.Builder configBuilder = new InitializationConfig.Builder(appId); + if(disableMediationAdapterInitialization) { + configBuilder.disableMediationAdapterInitialization(); + } + InitializationConfig config = configBuilder.build(); + new Thread( + new Runnable() { + @Override + public void run() { + MobileAds.initialize(context, config, listener); + } + }) + .start(); + } + + /** Wrapper for setAppMuted. */ + public void setAppMuted(boolean muted) { + MobileAds.setUserMutedApp(muted); + } + + /** Wrapper for setAppVolume. */ + public void setAppVolume(double volume) { + MobileAds.setUserControlledAppVolume((float) volume); + } + + /** Wrapper for disableMediationInitialization. */ + public void disableMediationInitialization(@NonNull Context context) { + disableMediationAdapterInitialization = true; + } + + /** Wrapper for getVersionString. */ + public String getVersionString() { + return MobileAds.getVersion().toString(); + } + + /** Wrapper for getRequestConfiguration. */ + public RequestConfiguration getRequestConfiguration() { + return MobileAds.getRequestConfiguration(); + } + + /** Wrapper for openDebugMenu. */ + public void openDebugMenu(Context context, String adUnitId) { + if (!(context instanceof Activity)) { + Log.w(TAG, "Cannot openDebugMenu before GMA SDK is attached to a Flutter Activity"); + return; + } + Activity activity = (Activity) context; + MobileAds.openDebugMenu(activity, adUnitId); + } + + /** Open the ad inspector. */ + public void openAdInspector(Context context, OnAdInspectorClosedListener listener) { + MobileAds.openAdInspector(listener); + } + + /** Register the webView for monetization. */ + public void registerWebView(int webViewId, FlutterEngine flutterEngine) { + WebView webView = WebViewFlutterAndroidExternalApi.getWebView(flutterEngine, webViewId); + if (webView == null) { + Log.w(TAG, "MobileAds.registerWebView unable to find webView with id: " + webViewId); + } else { + MobileAds.registerWebView(webView); + } + } + + @Nullable + private String getApplicationMetaData(Context context, String key) { + try { + ApplicationInfo appInfo = + context + .getPackageManager() + .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = appInfo.metaData; + if (bundle != null && bundle.containsKey(key)) { + return bundle.getString(key); + } else { + Log.e(TAG, "Application ID not found in manifest!"); + } + } catch (Exception e) { + Log.e(TAG, "Error reading application ID from manifest: " + e.getMessage()); + } + return null; + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java new file mode 100644 index 000000000..fc02185c9 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java @@ -0,0 +1,271 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.ads.nativetemplates.TemplateView; +import com.google.android.gms.ads.nativead.NativeAdOptions; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugins.googlemobileads.nativetemplates.FlutterNativeTemplateStyle; +import java.util.Map; + +/** A wrapper for {@link NativeAd}. */ +class FlutterNativeAd extends FlutterAd { + private static final String TAG = "FlutterNativeAd"; + + @NonNull private final AdInstanceManager manager; + @NonNull private final String adUnitId; + @Nullable private final NativeAdFactory adFactory; + @NonNull private final FlutterAdLoader flutterAdLoader; + @Nullable private FlutterAdRequest request; + @Nullable private FlutterAdManagerAdRequest adManagerRequest; + @Nullable private Map customOptions; + @Nullable private NativeAdView nativeAdView; + @Nullable private final FlutterNativeAdOptions nativeAdOptions; + @Nullable private final FlutterNativeTemplateStyle nativeTemplateStyle; + @Nullable private TemplateView templateView; + @NonNull private final Context context; + + static class Builder { + @Nullable private AdInstanceManager manager; + @Nullable private String adUnitId; + @Nullable private NativeAdFactory adFactory; + @Nullable private FlutterAdRequest request; + @Nullable private FlutterAdManagerAdRequest adManagerRequest; + @Nullable private Map customOptions; + @Nullable private Integer id; + @Nullable private FlutterNativeAdOptions nativeAdOptions; + @Nullable private FlutterAdLoader flutterAdLoader; + @Nullable private FlutterNativeTemplateStyle nativeTemplateStyle; + @NonNull private final Context context; + + Builder(Context context) { + this.context = context; + } + + @CanIgnoreReturnValue + public Builder setAdFactory(@NonNull NativeAdFactory adFactory) { + this.adFactory = adFactory; + return this; + } + + @CanIgnoreReturnValue + public Builder setId(int id) { + this.id = id; + return this; + } + + @CanIgnoreReturnValue + public Builder setCustomOptions(@Nullable Map customOptions) { + this.customOptions = customOptions; + return this; + } + + @CanIgnoreReturnValue + public Builder setManager(@NonNull AdInstanceManager manager) { + this.manager = manager; + return this; + } + + @CanIgnoreReturnValue + public Builder setAdUnitId(@NonNull String adUnitId) { + this.adUnitId = adUnitId; + return this; + } + + @CanIgnoreReturnValue + public Builder setRequest(@NonNull FlutterAdRequest request) { + this.request = request; + return this; + } + + @CanIgnoreReturnValue + public Builder setAdManagerRequest(@NonNull FlutterAdManagerAdRequest request) { + this.adManagerRequest = request; + return this; + } + + @CanIgnoreReturnValue + public Builder setNativeAdOptions(@Nullable FlutterNativeAdOptions nativeAdOptions) { + this.nativeAdOptions = nativeAdOptions; + return this; + } + + @CanIgnoreReturnValue + public Builder setFlutterAdLoader(@NonNull FlutterAdLoader flutterAdLoader) { + this.flutterAdLoader = flutterAdLoader; + return this; + } + + @CanIgnoreReturnValue + public Builder setNativeTemplateStyle( + @Nullable FlutterNativeTemplateStyle nativeTemplateStyle) { + this.nativeTemplateStyle = nativeTemplateStyle; + return this; + } + + FlutterNativeAd build() { + if (manager == null) { + throw new IllegalStateException("AdInstanceManager cannot be null."); + } else if (adUnitId == null) { + throw new IllegalStateException("AdUnitId cannot be null."); + } else if (adFactory == null && nativeTemplateStyle == null) { + throw new IllegalStateException("NativeAdFactory and nativeTemplateStyle cannot be null."); + } else if (request == null && adManagerRequest == null) { + throw new IllegalStateException("adRequest or addManagerRequest must be non-null."); + } + + final FlutterNativeAd nativeAd; + if (request == null) { + nativeAd = + new FlutterNativeAd( + context, + id, + manager, + adUnitId, + adFactory, + adManagerRequest, + flutterAdLoader, + customOptions, + nativeAdOptions, + nativeTemplateStyle); + } else { + nativeAd = + new FlutterNativeAd( + context, + id, + manager, + adUnitId, + adFactory, + request, + flutterAdLoader, + customOptions, + nativeAdOptions, + nativeTemplateStyle); + } + return nativeAd; + } + } + + protected FlutterNativeAd( + @NonNull Context context, + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull NativeAdFactory adFactory, + @NonNull FlutterAdRequest request, + @NonNull FlutterAdLoader flutterAdLoader, + @Nullable Map customOptions, + @Nullable FlutterNativeAdOptions nativeAdOptions, + @Nullable FlutterNativeTemplateStyle nativeTemplateStyle) { + super(adId); + this.context = context; + this.manager = manager; + this.adUnitId = adUnitId; + this.adFactory = adFactory; + this.request = request; + this.flutterAdLoader = flutterAdLoader; + this.customOptions = customOptions; + this.nativeAdOptions = nativeAdOptions; + this.nativeTemplateStyle = nativeTemplateStyle; + } + + protected FlutterNativeAd( + @NonNull Context context, + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull NativeAdFactory adFactory, + @NonNull FlutterAdManagerAdRequest adManagerRequest, + @NonNull FlutterAdLoader flutterAdLoader, + @Nullable Map customOptions, + @Nullable FlutterNativeAdOptions nativeAdOptions, + @Nullable FlutterNativeTemplateStyle nativeTemplateStyle) { + super(adId); + this.context = context; + this.manager = manager; + this.adUnitId = adUnitId; + this.adFactory = adFactory; + this.adManagerRequest = adManagerRequest; + this.flutterAdLoader = flutterAdLoader; + this.customOptions = customOptions; + this.nativeAdOptions = nativeAdOptions; + this.nativeTemplateStyle = nativeTemplateStyle; + } + + @Override + void load() { + final NativeAdLoaderCallback loadedListener = new FlutterNativeAdLoadedListener(this); + // Note we delegate loading the ad to FlutterAdLoader mainly for testing purposes. + // As of 20.0.0 of GMA, mockito is unable to mock AdLoader. + final NativeAdOptions options = + this.nativeAdOptions == null + ? new NativeAdOptions.Builder().build() + : nativeAdOptions.asNativeAdOptions(); + if (request != null) { + flutterAdLoader.loadNativeAd(adUnitId, loadedListener, options); + } else if (adManagerRequest != null) { + flutterAdLoader.loadAdManagerNativeAd(adUnitId, loadedListener, options); + } else { + Log.e(TAG, "A null or invalid ad request was provided."); + } + } + + @Override + @Nullable + public PlatformView getPlatformView() { + if (nativeAdView != null) { + return new FlutterPlatformView(nativeAdView); + } else if (templateView != null) { + return new FlutterPlatformView(templateView); + } + return null; + } + + void onNativeAdLoaded(@NonNull NativeAd nativeAd) { + if (nativeTemplateStyle != null) { + templateView = nativeTemplateStyle.asTemplateView(context); + templateView.setNativeAd(nativeAd); + } else { + nativeAdView = adFactory.createNativeAd(nativeAd, customOptions); + } + nativeAd.setAdEventCallback(new FlutterNativeAdListener(adId, manager)); + manager.onAdLoaded(adId, nativeAd.getResponseInfo()); + } + + void onNativeAdFailed(LoadAdError loadAdError) { + manager.onAdFailedToLoad(adId, new FlutterAd.FlutterLoadAdError(loadAdError)); + } + + @Override + void dispose() { + if (nativeAdView != null) { + nativeAdView.destroy(); + nativeAdView = null; + } + if (templateView != null) { + templateView.destroyNativeAd(); + templateView = null; + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRequestConfiguration.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRequestConfiguration.java new file mode 100644 index 000000000..54b379686 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRequestConfiguration.java @@ -0,0 +1,205 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import static com.google.android.gms.ads.RequestConfiguration.TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED; +import static com.google.android.gms.ads.RequestConfiguration.TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.MobileAds; +import com.google.android.libraries.ads.mobile.sdk.common.RequestConfiguration; +import com.google.android.libraries.ads.mobile.sdk.common.RequestConfiguration.MaxAdContentRating; +import com.google.android.libraries.ads.mobile.sdk.common.RequestConfiguration.TagForChildDirectedTreatment; +import com.google.android.libraries.ads.mobile.sdk.common.RequestConfiguration.TagForUnderAgeOfConsent; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.flutter.plugin.common.MethodCall; +import java.util.List; +import java.util.Objects; + +class FlutterRequestConfiguration { + + @Nullable private final String maxAdContentRating; + private final Integer tagForChildDirectedTreatment; + private final Integer tagForUnderAgeOfConsent; + @Nullable private final List testDeviceIds; + + public static void updateRequestConfiguration(@NonNull MethodCall call) { + RequestConfiguration currentRequestConfiguration = MobileAds.getRequestConfiguration(); + RequestConfiguration.Builder builder = + new RequestConfiguration.Builder() + .setMaxAdContentRating(currentRequestConfiguration.getMaxAdContentRating()) + .setTagForChildDirectedTreatment( + currentRequestConfiguration.getTagForChildDirectedTreatment()) + .setTagForUnderAgeOfConsent(currentRequestConfiguration.getTagForUnderAgeOfConsent()) + .setTestDeviceIds(currentRequestConfiguration.getTestDeviceIds()); + + String maxAdContentRating = call.argument("maxAdContentRating"); + Integer tagForChildDirectedTreatment = call.argument("tagForChildDirectedTreatment"); + Integer tagForUnderAgeOfConsent = call.argument("tagForUnderAgeOfConsent"); + List testDeviceIds = call.argument("testDeviceIds"); + if (maxAdContentRating != null) { + if (maxAdContentRating == MaxAdContentRating.MAX_AD_CONTENT_RATING_G.getValue()) { + builder.setMaxAdContentRating(MaxAdContentRating.MAX_AD_CONTENT_RATING_G); + } else if (maxAdContentRating == MaxAdContentRating.MAX_AD_CONTENT_RATING_PG.getValue()) { + builder.setMaxAdContentRating(MaxAdContentRating.MAX_AD_CONTENT_RATING_PG); + } else if (maxAdContentRating == MaxAdContentRating.MAX_AD_CONTENT_RATING_T.getValue()) { + builder.setMaxAdContentRating(MaxAdContentRating.MAX_AD_CONTENT_RATING_T); + } else if (maxAdContentRating == MaxAdContentRating.MAX_AD_CONTENT_RATING_MA.getValue()) { + builder.setMaxAdContentRating(MaxAdContentRating.MAX_AD_CONTENT_RATING_MA); + } else { + builder.setMaxAdContentRating(MaxAdContentRating.MAX_AD_CONTENT_RATING_UNSPECIFIED); + } + } + if (tagForChildDirectedTreatment != null) { + if (tagForChildDirectedTreatment + == TagForChildDirectedTreatment.TAG_FOR_CHILD_DIRECTED_TREATMENT_TRUE.getValue()) { + builder.setTagForChildDirectedTreatment( + TagForChildDirectedTreatment.TAG_FOR_CHILD_DIRECTED_TREATMENT_TRUE); + } else if (tagForChildDirectedTreatment + == TagForChildDirectedTreatment.TAG_FOR_CHILD_DIRECTED_TREATMENT_FALSE.getValue()) { + builder.setTagForChildDirectedTreatment( + TagForChildDirectedTreatment.TAG_FOR_CHILD_DIRECTED_TREATMENT_FALSE); + } else { + builder.setTagForChildDirectedTreatment( + TagForChildDirectedTreatment.TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED); + } + } + if (tagForUnderAgeOfConsent != null) { + if (tagForUnderAgeOfConsent + == TagForUnderAgeOfConsent.TAG_FOR_UNDER_AGE_OF_CONSENT_TRUE.getValue()) { + builder.setTagForUnderAgeOfConsent( + TagForUnderAgeOfConsent.TAG_FOR_UNDER_AGE_OF_CONSENT_TRUE); + } else if (tagForUnderAgeOfConsent + == TagForUnderAgeOfConsent.TAG_FOR_UNDER_AGE_OF_CONSENT_FALSE.getValue()) { + builder.setTagForUnderAgeOfConsent( + TagForUnderAgeOfConsent.TAG_FOR_UNDER_AGE_OF_CONSENT_FALSE); + } else { + builder.setTagForUnderAgeOfConsent( + TagForUnderAgeOfConsent.TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED); + } + } + if (testDeviceIds != null) { + builder.setTestDeviceIds(testDeviceIds); + } + RequestConfiguration rc = builder.build(); + MobileAds.setRequestConfiguration(rc); + } + + protected static class Builder { + @Nullable private String maxAdContentRating; + private Integer tagForChildDirectedTreatment = TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED; + private Integer tagForUnderAgeOfConsent = TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED; + @Nullable private List testDeviceIds; + + @CanIgnoreReturnValue + Builder setMaxAdContentRating(String maxAdContentRating) { + this.maxAdContentRating = maxAdContentRating; + return this; + } + + @CanIgnoreReturnValue + Builder setTagForChildDirectedTreatment(Integer tagForChildDirectedTreatment) { + this.tagForChildDirectedTreatment = tagForChildDirectedTreatment; + return this; + } + + @CanIgnoreReturnValue + Builder setTagForUnderAgeOfConsent(Integer tagForUnderAgeOfConsent) { + this.tagForUnderAgeOfConsent = tagForUnderAgeOfConsent; + return this; + } + + @CanIgnoreReturnValue + Builder setTestDeviceIds(List testDeviceIds) { + this.testDeviceIds = testDeviceIds; + return this; + } + + @Nullable + protected String getMaxAdContentRating() { + return maxAdContentRating; + } + + protected Integer getTagForChildDirectedTreatment() { + return tagForChildDirectedTreatment; + } + + protected Integer getTagForUnderAgeOfConsent() { + return tagForUnderAgeOfConsent; + } + + @Nullable + protected List getTestDeviceIds() { + return testDeviceIds; + } + + FlutterRequestConfiguration build() { + return new FlutterRequestConfiguration( + maxAdContentRating, tagForChildDirectedTreatment, tagForUnderAgeOfConsent, testDeviceIds); + } + } + + public FlutterRequestConfiguration( + @Nullable String maxAdContentRating, + Integer tagForChildDirectedTreatment, + Integer tagForUnderAgeOfConsent, + @Nullable List testDeviceIds) { + this.maxAdContentRating = maxAdContentRating; + this.tagForChildDirectedTreatment = tagForChildDirectedTreatment; + this.tagForUnderAgeOfConsent = tagForUnderAgeOfConsent; + this.testDeviceIds = testDeviceIds; + } + + @Nullable + protected String getMaxAdContentRating() { + return maxAdContentRating; + } + + protected Integer getTagForChildDirectedTreatment() { + return tagForChildDirectedTreatment; + } + + protected Integer getTagForUnderAgeOfConsent() { + return tagForUnderAgeOfConsent; + } + + @Nullable + protected List getTestDeviceIds() { + return testDeviceIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof FlutterRequestConfiguration)) { + return false; + } + + FlutterRequestConfiguration config = (FlutterRequestConfiguration) o; + return Objects.equals(maxAdContentRating, config.maxAdContentRating) + && Objects.equals(tagForChildDirectedTreatment, config.tagForChildDirectedTreatment) + && Objects.equals(tagForUnderAgeOfConsent, config.tagForUnderAgeOfConsent) + && Objects.equals(testDeviceIds, config.testDeviceIds); + } + + @Override + public int hashCode() { + return Objects.hash( + maxAdContentRating, tagForChildDirectedTreatment, tagForUnderAgeOfConsent, testDeviceIds); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRewardedAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRewardedAd.java new file mode 100644 index 000000000..c15913f77 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRewardedAd.java @@ -0,0 +1,275 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.AdError; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.rewarded.OnUserEarnedRewardListener; +import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardItem; +import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardedAd; +import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardedAdEventCallback; +import java.lang.ref.WeakReference; + +/** A wrapper for {@link RewardedAd}. */ +class FlutterRewardedAd extends FlutterAd.FlutterOverlayAd { + private static final String TAG = "FlutterRewardedAd"; + + @NonNull private final AdInstanceManager manager; + @NonNull private final String adUnitId; + @NonNull private final FlutterAdLoader flutterAdLoader; + @Nullable private final FlutterAdRequest request; + @Nullable private final FlutterAdManagerAdRequest adManagerRequest; + @Nullable RewardedAd rewardedAd; + + /** A wrapper for {@link RewardItem}. */ + static class FlutterRewardItem { + @NonNull final Integer amount; + @NonNull final String type; + + FlutterRewardItem(@NonNull Integer amount, @NonNull String type) { + this.amount = amount; + this.type = type; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof FlutterRewardItem)) { + return false; + } + + final FlutterRewardItem that = (FlutterRewardItem) other; + if (!amount.equals(that.amount)) { + return false; + } + return type.equals(that.type); + } + + @Override + public int hashCode() { + int result = amount.hashCode(); + result = 31 * result + type.hashCode(); + return result; + } + } + + /** Constructor for AdMob Ad Request. */ + public FlutterRewardedAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdRequest request, + @NonNull FlutterAdLoader flutterAdLoader) { + super(adId); + this.manager = manager; + this.adUnitId = adUnitId; + this.request = request; + this.adManagerRequest = null; + this.flutterAdLoader = flutterAdLoader; + } + + /** Constructor for Ad Manager Ad request. */ + public FlutterRewardedAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdManagerAdRequest adManagerRequest, + @NonNull FlutterAdLoader flutterAdLoader) { + super(adId); + this.manager = manager; + this.adUnitId = adUnitId; + this.adManagerRequest = adManagerRequest; + this.request = null; + this.flutterAdLoader = flutterAdLoader; + } + + @Override + void load() { + final AdLoadCallback adLoadCallback = new DelegatingRewardedCallback(this); + if (request != null) { + flutterAdLoader.loadRewarded(request.asAdRequest(adUnitId), adLoadCallback); + } else if (adManagerRequest != null) { + flutterAdLoader.loadAdManagerRewarded( + adManagerRequest.asAdManagerAdRequest(adUnitId), adLoadCallback); + } else { + Log.e(TAG, "A null or invalid ad request was provided."); + } + } + + void onAdLoaded(@NonNull RewardedAd rewardedAd) { + FlutterRewardedAd.this.rewardedAd = rewardedAd; + manager.onAdLoaded(adId, rewardedAd.getResponseInfo()); + } + + void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + manager.onAdFailedToLoad(adId, new FlutterLoadAdError(loadAdError)); + } + + @Override + public void show() { + if (rewardedAd == null) { + Log.e(TAG, "Error showing rewarded - the rewarded ad wasn't loaded yet."); + return; + } + if (manager.getActivity() == null) { + Log.e(TAG, "Tried to show rewarded ad before activity was bound to the plugin."); + return; + } + rewardedAd.setAdEventCallback(new DelegatingRewardedCallback(this)); + rewardedAd.show(manager.getActivity(), new DelegatingRewardedCallback(this)); + } + + @Override + public void setImmersiveMode(boolean immersiveModeEnabled) { + if (rewardedAd == null) { + Log.e( + TAG, "Error setting immersive mode in rewarded ad - the rewarded ad wasn't loaded yet."); + return; + } + rewardedAd.setImmersiveMode(immersiveModeEnabled); + } + + void onAdMetadataChanged() { + manager.onAdMetadataChanged(adId); + } + + void onUserEarnedReward(@NonNull RewardItem rewardItem) { + manager.onRewardedAdUserEarnedReward( + adId, new FlutterRewardItem(rewardItem.getAmount(), rewardItem.getType())); + } + + @Override + void dispose() { + rewardedAd = null; + } + + public void setServerSideVerificationOptions(FlutterServerSideVerificationOptions options) { + if (rewardedAd != null) { + rewardedAd.setServerSideVerificationOptions(options.asServerSideVerificationOptions()); + } else { + Log.e(TAG, "RewardedAd is null in setServerSideVerificationOptions"); + } + } + + /** + * This class delegates various rewarded ad callbacks to FlutterRewardedAd. Maintains a weak + * reference to avoid memory leaks. + */ + private static final class DelegatingRewardedCallback + implements AdLoadCallback, + RewardedAdEventCallback, + OnUserEarnedRewardListener { + + private final WeakReference delegate; + + DelegatingRewardedCallback(FlutterRewardedAd delegate) { + this.delegate = new WeakReference<>(delegate); + } + + @Override + public void onAdLoaded(@NonNull RewardedAd rewardedAd) { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.onAdLoaded(rewardedAd); + } + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.onAdFailedToLoad(loadAdError); + } + } + + @Override + public void onUserEarnedReward(@NonNull RewardItem rewardItem) { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.onUserEarnedReward(rewardItem); + } + } + + @Override + public void onAdMetadataChanged() { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.onAdMetadataChanged(); + } + } + + @Override + public void onAdFailedToShowFullScreenContent(@NonNull FullScreenContentError adError) { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.manager.onFailedToShowFullScreenContent( + ad.adId, + new AdError( + adError.getCode().getValue(), + adError.getMessage(), + AdInstanceManager.NEXT_GEN_DOMAIN)); + } + } + + @Override + public void onAdShowedFullScreenContent() { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdShowedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdDismissedFullScreenContent() { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdDismissedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdImpression() { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdImpression(ad.adId); + } + } + + @Override + public void onAdClicked() { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdClicked(ad.adId); + } + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + FlutterRewardedAd ad = delegate.get(); + if (ad != null) { + ad.manager.onPaidEvent( + ad, + AdInstanceManager.comAdValueToFlutterAdValue(adValue)); + } + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRewardedInterstitialAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRewardedInterstitialAd.java new file mode 100644 index 000000000..39f5e04cb --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterRewardedInterstitialAd.java @@ -0,0 +1,248 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.AdError; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.rewarded.OnUserEarnedRewardListener; +import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardItem; +import com.google.android.libraries.ads.mobile.sdk.rewardedinterstitial.RewardedInterstitialAdEventCallback; +import com.google.android.libraries.ads.mobile.sdk.rewardedinterstitial.RewardedInterstitialAd; +import io.flutter.plugins.googlemobileads.FlutterRewardedAd.FlutterRewardItem; +import java.lang.ref.WeakReference; + +/** A wrapper for {@link RewardedInterstitialAd}. */ +class FlutterRewardedInterstitialAd extends FlutterAd.FlutterOverlayAd { + private static final String TAG = "FlutterRIAd"; + + @NonNull private final AdInstanceManager manager; + @NonNull private final String adUnitId; + @NonNull private final FlutterAdLoader flutterAdLoader; + @Nullable private final FlutterAdRequest request; + @Nullable private final FlutterAdManagerAdRequest adManagerRequest; + @Nullable RewardedInterstitialAd rewardedInterstitialAd; + + /** Constructor for AdMob Ad Request. */ + public FlutterRewardedInterstitialAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdRequest request, + @NonNull FlutterAdLoader flutterAdLoader) { + super(adId); + this.manager = manager; + this.adUnitId = adUnitId; + this.request = request; + this.adManagerRequest = null; + this.flutterAdLoader = flutterAdLoader; + } + + /** Constructor for Ad Manager Ad request. */ + public FlutterRewardedInterstitialAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdManagerAdRequest adManagerRequest, + @NonNull FlutterAdLoader flutterAdLoader) { + super(adId); + this.manager = manager; + this.adUnitId = adUnitId; + this.adManagerRequest = adManagerRequest; + this.request = null; + this.flutterAdLoader = flutterAdLoader; + } + + @Override + void load() { + final AdLoadCallback adLoadCallback = new DelegatingRewardedCallback(this); + if (request != null) { + flutterAdLoader.loadRewardedInterstitial(request.asAdRequest(adUnitId), adLoadCallback); + } else if (adManagerRequest != null) { + flutterAdLoader.loadAdManagerRewardedInterstitial( + adManagerRequest.asAdManagerAdRequest(adUnitId), adLoadCallback); + } else { + Log.e(TAG, "A null or invalid ad request was provided."); + } + } + + void onAdLoaded(@NonNull RewardedInterstitialAd rewardedInterstitialAd) { + FlutterRewardedInterstitialAd.this.rewardedInterstitialAd = rewardedInterstitialAd; + manager.onAdLoaded(adId, rewardedInterstitialAd.getResponseInfo()); + } + + void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + manager.onAdFailedToLoad(adId, new FlutterLoadAdError(loadAdError)); + } + + @Override + public void show() { + if (rewardedInterstitialAd == null) { + Log.e( + TAG, + "Error showing rewarded interstitial - the rewarded interstitial ad wasn't loaded yet."); + return; + } + if (manager.getActivity() == null) { + Log.e(TAG, "Tried to show rewarded interstitial ad before activity was bound to the plugin."); + return; + } + rewardedInterstitialAd.setAdEventCallback(new DelegatingRewardedCallback(this)); + rewardedInterstitialAd.show(manager.getActivity(), new DelegatingRewardedCallback(this)); + } + + @Override + public void setImmersiveMode(boolean immersiveModeEnabled) { + if (rewardedInterstitialAd == null) { + Log.e( + TAG, + "Error setting immersive mode in rewarded interstitial ad - the rewarded interstitial ad" + + " wasn't loaded yet."); + return; + } + rewardedInterstitialAd.setImmersiveMode(immersiveModeEnabled); + } + + void onAdMetadataChanged() { + manager.onAdMetadataChanged(adId); + } + + void onUserEarnedReward(@NonNull RewardItem rewardItem) { + manager.onRewardedAdUserEarnedReward( + adId, new FlutterRewardItem(rewardItem.getAmount(), rewardItem.getType())); + } + + @Override + void dispose() { + rewardedInterstitialAd = null; + } + + public void setServerSideVerificationOptions(FlutterServerSideVerificationOptions options) { + if (rewardedInterstitialAd != null) { + rewardedInterstitialAd.setServerSideVerificationOptions( + options.asServerSideVerificationOptions()); + } else { + Log.e(TAG, "RewardedInterstitialAd is null in setServerSideVerificationOptions"); + } + } + + /** + * This class delegates various rewarded interstitial ad callbacks to + * FlutterRewardedInterstitialAd. Maintains a weak reference to avoid memory leaks. + */ + private static final class DelegatingRewardedCallback + implements AdLoadCallback, + RewardedInterstitialAdEventCallback, + OnUserEarnedRewardListener { + + private final WeakReference delegate; + + DelegatingRewardedCallback(FlutterRewardedInterstitialAd delegate) { + this.delegate = new WeakReference<>(delegate); + } + + @Override + public void onAdLoaded(@NonNull RewardedInterstitialAd rewardedInterstitialAd) { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.onAdLoaded(rewardedInterstitialAd); + } + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.onAdFailedToLoad(loadAdError); + } + } + + @Override + public void onUserEarnedReward(@NonNull RewardItem rewardItem) { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.onUserEarnedReward(rewardItem); + } + } + + @Override + public void onAdMetadataChanged() { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.onAdMetadataChanged(); + } + } + + @Override + public void onAdFailedToShowFullScreenContent(@NonNull FullScreenContentError adError) { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onFailedToShowFullScreenContent( + ad.adId, + new AdError( + adError.getCode().getValue(), + adError.getMessage(), + AdInstanceManager.NEXT_GEN_DOMAIN)); + } + } + + @Override + public void onAdShowedFullScreenContent() { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdShowedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdDismissedFullScreenContent() { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdDismissedFullScreenContent(ad.adId); + } + } + + @Override + public void onAdImpression() { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdImpression(ad.adId); + } + } + + @Override + public void onAdClicked() { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onAdClicked(ad.adId); + } + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + FlutterRewardedInterstitialAd ad = delegate.get(); + if (ad != null) { + ad.manager.onPaidEvent( + ad, + AdInstanceManager.comAdValueToFlutterAdValue(adValue)); + } + } + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterServerSideVerificationOptions.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterServerSideVerificationOptions.java new file mode 100644 index 000000000..2545d2e1a --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterServerSideVerificationOptions.java @@ -0,0 +1,64 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.rewarded.ServerSideVerificationOptions; +import java.util.Objects; + +/** A wrapper for {@link ServerSideVerificationOptions}. */ +class FlutterServerSideVerificationOptions { + + private final String userId; + private final String customData; + + public FlutterServerSideVerificationOptions( + @Nullable String userId, @Nullable String customData) { + this.userId = TextUtils.isEmpty(userId) ? "" : userId; + this.customData = TextUtils.isEmpty(customData) ? "" : customData; + } + + public String getUserId() { + return userId; + } + + public String getCustomData() { + return customData; + } + + /** Gets an equivalent {@link ServerSideVerificationOptions}. */ + public ServerSideVerificationOptions asServerSideVerificationOptions() { + return new ServerSideVerificationOptions(userId, customData); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof FlutterServerSideVerificationOptions)) { + return false; + } + FlutterServerSideVerificationOptions other = (FlutterServerSideVerificationOptions) obj; + return Objects.equals(other.userId, userId) && Objects.equals(other.customData, customData); + } + + @Override + public int hashCode() { + return Objects.hash(userId, customData); + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/NativeAdFactory.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/NativeAdFactory.java new file mode 100644 index 000000000..dcdcbc80c --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/NativeAdFactory.java @@ -0,0 +1,40 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView; +import java.util.Map; + +/** + * Interface used to display a {@link NativeAd}. + * + *

Added to a {@link GoogleMobileAdsPlugin} and creates + * {@link NativeAdView}s from Native Ads created in Dart. + */ +public interface NativeAdFactory { + /** + * Creates a {@link NativeAdView} with a {@link + * NativeAd}. + * + * @param nativeAd Ad information used to create a {@link + * NativeAd} + * @param customOptions Used to pass additional custom options to create the {@link + * NativeAdView}. Nullable. + * @return a {@link NativeAdView} that is overlaid on top of + * the FlutterView + */ + NativeAdView createNativeAd(NativeAd nativeAd, Map customOptions); +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/res/layout/gnt_medium_template_view.xml b/packages/google_mobile_ads/android/src/adsNextGenSdk/res/layout/gnt_medium_template_view.xml new file mode 100644 index 000000000..31cbb7d99 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/res/layout/gnt_medium_template_view.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/res/layout/gnt_small_template_view.xml b/packages/google_mobile_ads/android/src/adsNextGenSdk/res/layout/gnt_small_template_view.xml new file mode 100644 index 000000000..196a90aed --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/res/layout/gnt_small_template_view.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java index 65f370cf4..77c5382a9 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java @@ -22,7 +22,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.ads.RequestConfiguration; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugins.googlemobileads.FlutterAd.FlutterAdError; import io.flutter.plugins.googlemobileads.FlutterAd.FlutterAdapterResponseInfo; @@ -32,7 +31,6 @@ import io.flutter.plugins.googlemobileads.nativetemplates.FlutterNativeTemplateStyle; import io.flutter.plugins.googlemobileads.nativetemplates.FlutterNativeTemplateTextStyle; import io.flutter.plugins.googlemobileads.nativetemplates.FlutterNativeTemplateType; -import java.lang.reflect.InvocationTargetException; import java.io.ByteArrayOutputStream; import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; @@ -217,9 +215,9 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { writeValue(stream, options.requestCustomMuteThisAd); writeValue(stream, options.shouldRequestMultipleImages); writeValue(stream, options.shouldReturnUrlsForImageAssets); - } else if (value instanceof RequestConfiguration) { + } else if (value instanceof FlutterRequestConfiguration) { stream.write(VALUE_REQUEST_CONFIGURATION_PARAMS); - RequestConfiguration params = (RequestConfiguration) value; + FlutterRequestConfiguration params = (FlutterRequestConfiguration) value; writeValue(stream, params.getMaxAdContentRating()); writeValue(stream, params.getTagForChildDirectedTreatment()); writeValue(stream, params.getTagForUnderAgeOfConsent()); @@ -280,7 +278,7 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { final String orientation = (String) readValueOfType(buffer.get(), buffer); final Integer width = (Integer) readValueOfType(buffer.get(), buffer); return new FlutterAdSize.AnchoredAdaptiveBannerAdSize( - context, adSizeFactory, orientation, width); + context, adSizeFactory, orientation, width, /* isLarge = */ false); case VALUE_SMART_BANNER_AD_SIZE: return new FlutterAdSize.SmartBannerAdSize(); case VALUE_AD_SIZE: @@ -413,7 +411,7 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { (Boolean) readValueOfType(buffer.get(), buffer), (Boolean) readValueOfType(buffer.get(), buffer)); case VALUE_REQUEST_CONFIGURATION_PARAMS: - RequestConfiguration.Builder rcb = new RequestConfiguration.Builder(); + FlutterRequestConfiguration.Builder rcb = new FlutterRequestConfiguration.Builder(); rcb.setMaxAdContentRating((String) readValueOfType(buffer.get(), buffer)); rcb.setTagForChildDirectedTreatment((Integer) readValueOfType(buffer.get(), buffer)); rcb.setTagForUnderAgeOfConsent((Integer) readValueOfType(buffer.get(), buffer)); diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AppStateNotifier.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AppStateNotifier.java index 3e6bffb9d..f4e527ec1 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AppStateNotifier.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AppStateNotifier.java @@ -64,9 +64,11 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { switch (call.method) { case "start": start(); + result.success(null); break; case "stop": stop(); + result.success(null); break; default: result.notImplemented(); diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java index cd12d7170..49a497b3f 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java @@ -17,7 +17,7 @@ /** Constants used in the plugin. */ public class Constants { /** Version request agent. Should be bumped alongside plugin versions. */ - public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-6.0.0"; + public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-9.0.0"; /** Prefix for news template */ public static final String REQUEST_AGENT_NEWS_TEMPLATE_PREFIX = "News"; diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterMediationExtras.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterMediationExtras.java index 21612988b..b27972a2c 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterMediationExtras.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterMediationExtras.java @@ -31,6 +31,7 @@ */ public abstract class FlutterMediationExtras { Map extras; + /** * Called when the {@link FlutterAdRequest} is parsed into an {@link * com.google.android.gms.ads.AdRequest}. diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index 809ed1ede..023e595a4 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java @@ -19,15 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.ads.AdInspectorError; import com.google.android.gms.ads.AdSize; -import com.google.android.gms.ads.MobileAds; -import com.google.android.gms.ads.OnAdInspectorClosedListener; -import com.google.android.gms.ads.RequestConfiguration; -import com.google.android.gms.ads.initialization.InitializationStatus; -import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; -import com.google.android.gms.ads.nativead.NativeAd; -import com.google.android.gms.ads.nativead.NativeAdView; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -40,8 +32,6 @@ import io.flutter.plugins.googlemobileads.FlutterAd.FlutterOverlayAd; import io.flutter.plugins.googlemobileads.nativetemplates.FlutterNativeTemplateStyle; import io.flutter.plugins.googlemobileads.usermessagingplatform.UserMessagingPlatformManager; -import java.lang.reflect.Method; - import java.io.PrintWriter; import java.io.StringWriter; import java.util.HashMap; @@ -77,6 +67,7 @@ private static T requireNonNull(T obj) { private MediationNetworkExtrasProvider mediationNetworkExtrasProvider; private final FlutterMobileAdsWrapper flutterMobileAds; + /** * Public constructor for the plugin. Dependency initialization is handled in lifecycle methods * below. @@ -102,36 +93,17 @@ protected GoogleMobileAdsPlugin(@NonNull AppStateNotifier appStateNotifier) { this.flutterMobileAds = new FlutterMobileAdsWrapper(); } - /** - * Interface used to display a {@link com.google.android.gms.ads.nativead.NativeAd}. - * - *

Added to a {@link io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin} and creates - * {@link com.google.android.gms.ads.nativead.NativeAdView}s from Native Ads created in Dart. - */ - public interface NativeAdFactory { - /** - * Creates a {@link com.google.android.gms.ads.nativead.NativeAdView} with a {@link - * com.google.android.gms.ads.nativead.NativeAd}. - * - * @param nativeAd Ad information used to create a {@link - * com.google.android.gms.ads.nativead.NativeAd} - * @param customOptions Used to pass additional custom options to create the {@link - * com.google.android.gms.ads.nativead.NativeAdView}. Nullable. - * @return a {@link com.google.android.gms.ads.nativead.NativeAdView} that is overlaid on top of - * the FlutterView - */ - NativeAdView createNativeAd(NativeAd nativeAd, Map customOptions); - } + /** - * Registers a {@link io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory} - * used to create {@link com.google.android.gms.ads.nativead.NativeAdView}s from a Native Ad + * Registers a {@link NativeAdFactory} + * used to create {@link NativeAdView}s from a Native Ad * created in Dart. * * @param engine maintains access to a GoogleMobileAdsPlugin instance * @param factoryId a unique identifier for the ad factory. The Native Ad created in Dart includes * a parameter that refers to this. - * @param nativeAdFactory creates {@link com.google.android.gms.ads.nativead.NativeAdView}s when + * @param nativeAdFactory creates {@link NativeAdView}s when * Flutter NativeAds are created * @return whether the factoryId is unique and the nativeAdFactory was successfully added */ @@ -203,15 +175,15 @@ private static boolean registerNativeAdFactory( } /** - * Unregisters a {@link io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory} - * used to create {@link com.google.android.gms.ads.nativead.NativeAdView}s from a Native Ad + * Unregisters a {@link NativeAdFactory} + * used to create {@link NativeAdView}s from a Native Ad * created in Dart. * * @param engine maintains access to a GoogleMobileAdsPlugin instance * @param factoryId a unique identifier for the ad factory. The Native ad created in Dart includes * a parameter that refers to this * @return the previous {@link - * io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory} associated with + * NativeAdFactory} associated with * this factoryId, or null if there was none for this factoryId */ @Nullable @@ -367,43 +339,13 @@ private void doCall(@NonNull MethodCall call, @NonNull final Result result) { flutterMobileAds.initialize(context, new FlutterInitializationListener(result)); break; case "MobileAds#openAdInspector": - flutterMobileAds.openAdInspector( - context, - new OnAdInspectorClosedListener() { - @Override - public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { - if (adInspectorError != null) { - String errorCode = Integer.toString(adInspectorError.getCode()); - result.error( - errorCode, adInspectorError.getMessage(), adInspectorError.getDomain()); - } else { - result.success(null); - } - } - }); + FlutterAdInspector.openAdInspector(context, flutterMobileAds, result); break; case "MobileAds#getRequestConfiguration": result.success(flutterMobileAds.getRequestConfiguration()); break; case "MobileAds#updateRequestConfiguration": - RequestConfiguration.Builder builder = MobileAds.getRequestConfiguration().toBuilder(); - String maxAdContentRating = call.argument("maxAdContentRating"); - Integer tagForChildDirectedTreatment = call.argument("tagForChildDirectedTreatment"); - Integer tagForUnderAgeOfConsent = call.argument("tagForUnderAgeOfConsent"); - List testDeviceIds = call.argument("testDeviceIds"); - if (maxAdContentRating != null) { - builder.setMaxAdContentRating(maxAdContentRating); - } - if (tagForChildDirectedTreatment != null) { - builder.setTagForChildDirectedTreatment(tagForChildDirectedTreatment); - } - if (tagForUnderAgeOfConsent != null) { - builder.setTagForUnderAgeOfConsent(tagForUnderAgeOfConsent); - } - if (testDeviceIds != null) { - builder.setTestDeviceIds(testDeviceIds); - } - MobileAds.setRequestConfiguration(builder.build()); + FlutterRequestConfiguration.updateRequestConfiguration(call); result.success(null); break; case "MobileAds#registerWebView": @@ -600,13 +542,28 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { context, new FlutterAdSize.AdSizeFactory(), call.argument("orientation"), - call.argument("width")); + call.argument("width"), + /* isLarge = */ false); if (AdSize.INVALID.equals(size.size)) { result.success(null); } else { result.success(size.height); } break; + case "AdSize#getLargeAnchoredAdaptiveBannerAdSize": + final FlutterAdSize.AnchoredAdaptiveBannerAdSize largeSize = + new FlutterAdSize.AnchoredAdaptiveBannerAdSize( + context, + new FlutterAdSize.AdSizeFactory(), + call.argument("orientation"), + call.argument("width"), + /* isLarge = */ true); + if (AdSize.INVALID.equals(largeSize.size)) { + result.success(null); + } else { + result.success(largeSize.height); + } + break; case "MobileAds#setAppMuted": flutterMobileAds.setAppMuted(call.argument("muted")); result.success(null); @@ -650,6 +607,23 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { } break; } + case "isCollapsible": + { + FlutterAd ad = instanceManager.adForId(call.argument("adId")); + if (ad == null) { + result.success(false); + } else if (ad instanceof FlutterBannerAd) { + result.success(((FlutterBannerAd) ad).isCollapsible()); + } else if (ad instanceof FlutterAdManagerBannerAd) { + result.success(((FlutterAdManagerBannerAd) ad).isCollapsible()); + } else { + result.error( + Constants.ERROR_CODE_UNEXPECTED_AD_TYPE, + "Unexpected ad type for isCollapsible: " + ad, + null); + } + break; + } case "setServerSideVerificationOptions": { FlutterAd ad = instanceManager.adForId(call.argument("adId")); @@ -677,34 +651,5 @@ BannerAdCreator getBannerAdCreator(@NonNull Context context) { return new BannerAdCreator(context); } - /** An {@link OnInitializationCompleteListener} that invokes result.success() at most once. */ - private static final class FlutterInitializationListener - implements OnInitializationCompleteListener { - - private final Result result; - private boolean isInitializationCompleted; - private FlutterInitializationListener(@NonNull final Result result) { - this.result = result; - isInitializationCompleted = false; - } - - @Override - public void onInitializationComplete(@NonNull InitializationStatus initializationStatus) { - // Make sure not to invoke this more than once, since Dart will throw an exception if success - // is invoked more than once. See b/193418432. - if (isInitializationCompleted) { - return; - } - try { - Class clazz = Class.forName("com.google.android.gms.ads.MobileAds"); - Method method = clazz.getDeclaredMethod("setPlugin", String.class); - method.setAccessible(true); - method.invoke(null, Constants.REQUEST_AGENT_PREFIX_VERSIONED); - } catch (Exception ignored) { - } - result.success(new FlutterInitializationStatus(initializationStatus)); - isInitializationCompleted = true; - } - } } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java index dde127149..3eca54af8 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java @@ -41,6 +41,7 @@ public class UserMessagingPlatformManager implements MethodCallHandler { private static final String METHOD_CHANNEL_NAME = "plugins.flutter.io/google_mobile_ads/ump"; + /** Use 0 for error code internal to the Flutter plugin. */ private static final String INTERNAL_ERROR_CODE = "0"; diff --git a/packages/google_mobile_ads/android/src/main/java/com/google/android/ads/nativetemplates/TemplateView.java b/packages/google_mobile_ads/android/src/playServices/java/com/google/android/ads/nativetemplates/TemplateView.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/com/google/android/ads/nativetemplates/TemplateView.java rename to packages/google_mobile_ads/android/src/playServices/java/com/google/android/ads/nativetemplates/TemplateView.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/BannerAdCreator.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/BannerAdCreator.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/BannerAdCreator.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/BannerAdCreator.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FluidAdManagerBannerAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FluidAdManagerBannerAd.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FluidAdManagerBannerAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FluidAdManagerBannerAd.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAd.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAd.java diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdInspector.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdInspector.java new file mode 100644 index 000000000..109185748 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdInspector.java @@ -0,0 +1,34 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.content.Context; +import io.flutter.plugin.common.MethodChannel.Result; + +public final class FlutterAdInspector { + public static void openAdInspector(Context context, FlutterMobileAdsWrapper flutterMobileAds, Result result) { + flutterMobileAds.openAdInspector( + context, + adInspectorError -> { + if (adInspectorError != null) { + String errorCode = Integer.toString(adInspectorError.getCode()); + result.error( + errorCode, adInspectorError.getMessage(), adInspectorError.getDomain()); + } else { + result.success(null); + } + }); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java similarity index 96% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java index ecf7892e5..c07168eb9 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerBannerAd.java @@ -120,4 +120,12 @@ FlutterAdSize getAdSize() { } return new FlutterAdSize(adView.getAdSize()); } + + boolean isCollapsible() { + if (adView == null) { + return false; + } + + return adView.isCollapsible(); + } } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerInterstitialAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerInterstitialAd.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerInterstitialAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerInterstitialAd.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java similarity index 79% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java index d1faf110b..d08ffa90a 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java @@ -27,18 +27,33 @@ class FlutterAdSize { /** Wrapper around static methods for {@link com.google.android.gms.ads.AdSize}. */ static class AdSizeFactory { + @SuppressWarnings("deprecation") AdSize getPortraitAnchoredAdaptiveBannerAdSize(Context context, int width) { return AdSize.getPortraitAnchoredAdaptiveBannerAdSize(context, width); } + @SuppressWarnings("deprecation") AdSize getLandscapeAnchoredAdaptiveBannerAdSize(Context context, int width) { return AdSize.getLandscapeAnchoredAdaptiveBannerAdSize(context, width); } + @SuppressWarnings("deprecation") AdSize getCurrentOrientationAnchoredAdaptiveBannerAdSize(Context context, int width) { return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, width); } + AdSize getLargePortraitAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLargePortraitAnchoredAdaptiveBannerAdSize(context, width); + } + + AdSize getLargeLandscapeAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLargeLandscapeAnchoredAdaptiveBannerAdSize(context, width); + } + + AdSize getLargeAnchoredAdaptiveBannerAdSize(Context context, int width) { + return AdSize.getLargeAnchoredAdaptiveBannerAdSize(context, width); + } + AdSize getCurrentOrientationInlineAdaptiveBannerAdSize(Context context, int width) { return AdSize.getCurrentOrientationInlineAdaptiveBannerAdSize(context, width); } @@ -64,26 +79,35 @@ private static AdSize getAdSize( @NonNull Context context, @NonNull AdSizeFactory factory, @Nullable String orientation, - int width) { - final AdSize adSize; + int width, + boolean isLarge) { if (orientation == null) { - adSize = factory.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, width); + if (isLarge) { + return factory.getLargeAnchoredAdaptiveBannerAdSize(context, width); + } + return factory.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, width); } else if (orientation.equals("portrait")) { - adSize = factory.getPortraitAnchoredAdaptiveBannerAdSize(context, width); + if (isLarge) { + return factory.getLargePortraitAnchoredAdaptiveBannerAdSize(context, width); + } + return factory.getPortraitAnchoredAdaptiveBannerAdSize(context, width); } else if (orientation.equals("landscape")) { - adSize = factory.getLandscapeAnchoredAdaptiveBannerAdSize(context, width); + if (isLarge) { + return factory.getLargeLandscapeAnchoredAdaptiveBannerAdSize(context, width); + } + return factory.getLandscapeAnchoredAdaptiveBannerAdSize(context, width); } else { throw new IllegalArgumentException("Unexpected value for orientation: " + orientation); } - return adSize; } AnchoredAdaptiveBannerAdSize( @NonNull Context context, @NonNull AdSizeFactory factory, @Nullable String orientation, - int width) { - super(getAdSize(context, factory, orientation, width)); + int width, + boolean isLarge) { + super(getAdSize(context, factory, orientation, width, isLarge)); this.orientation = orientation; } } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdapterStatus.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdapterStatus.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdapterStatus.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdapterStatus.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAppOpenAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAppOpenAd.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAppOpenAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAppOpenAd.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java similarity index 95% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java index 04b1ee3ad..ce4e18700 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java @@ -91,4 +91,12 @@ FlutterAdSize getAdSize() { } return new FlutterAdSize(adView.getAdSize()); } + + boolean isCollapsible() { + if (adView == null) { + return false; + } + + return adView.isCollapsible(); + } } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterFullScreenContentCallback.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterFullScreenContentCallback.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterFullScreenContentCallback.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterFullScreenContentCallback.java diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterInitializationListener.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterInitializationListener.java new file mode 100644 index 000000000..93c4ee28c --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterInitializationListener.java @@ -0,0 +1,52 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.android.gms.ads.initialization.InitializationStatus; +import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; +import io.flutter.plugin.common.MethodChannel.Result; +import java.lang.reflect.Method; + +/** An {@link OnInitializationCompleteListener} that invokes result.success() at most once. */ +public final class FlutterInitializationListener implements OnInitializationCompleteListener { + + private final Result result; + private boolean isInitializationCompleted; + + FlutterInitializationListener(@NonNull final Result result) { + this.result = result; + isInitializationCompleted = false; + } + + @Override + public void onInitializationComplete(@NonNull InitializationStatus initializationStatus) { + // Make sure not to invoke this more than once, since Dart will throw an exception if success + // is invoked more than once. See b/193418432. + if (isInitializationCompleted) { + return; + } + try { + Class clazz = Class.forName("com.google.android.gms.ads.MobileAds"); + Method method = clazz.getDeclaredMethod("setPlugin", String.class); + method.setAccessible(true); + method.invoke(null, Constants.REQUEST_AGENT_PREFIX_VERSIONED); + } catch (Exception ignored) { + } + result.success(new FlutterInitializationStatus(initializationStatus)); + isInitializationCompleted = true; + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterInitializationStatus.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterInitializationStatus.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterInitializationStatus.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterInitializationStatus.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterInterstitialAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterInterstitialAd.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterInterstitialAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterInterstitialAd.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java similarity index 99% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java index 83f0f783d..42ac82f97 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterMobileAdsWrapper.java @@ -15,7 +15,6 @@ package io.flutter.plugins.googlemobileads; import android.content.Context; -import android.os.Build; import android.util.Log; import android.webkit.WebView; import androidx.annotation.NonNull; diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java similarity index 99% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java index 8230b1ddc..fb22849bf 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java @@ -27,7 +27,6 @@ import com.google.android.gms.ads.nativead.NativeAdView; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.flutter.plugin.platform.PlatformView; -import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory; import io.flutter.plugins.googlemobileads.nativetemplates.FlutterNativeTemplateStyle; import java.util.Map; diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterPaidEventListener.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterPaidEventListener.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterPaidEventListener.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterPaidEventListener.java diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterRequestConfiguration.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterRequestConfiguration.java new file mode 100644 index 000000000..79d4180b0 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterRequestConfiguration.java @@ -0,0 +1,163 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import static com.google.android.gms.ads.RequestConfiguration.TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED; +import static com.google.android.gms.ads.RequestConfiguration.TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.MobileAds; +import com.google.android.gms.ads.RequestConfiguration; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.flutter.plugin.common.MethodCall; +import java.util.List; +import java.util.Objects; + +class FlutterRequestConfiguration { + + @Nullable private final String maxAdContentRating; + private final Integer tagForChildDirectedTreatment; + private final Integer tagForUnderAgeOfConsent; + @Nullable private final List testDeviceIds; + + public static void updateRequestConfiguration(@NonNull MethodCall call) { + RequestConfiguration.Builder builder = MobileAds.getRequestConfiguration().toBuilder(); + String maxAdContentRating = call.argument("maxAdContentRating"); + Integer tagForChildDirectedTreatment = call.argument("tagForChildDirectedTreatment"); + Integer tagForUnderAgeOfConsent = call.argument("tagForUnderAgeOfConsent"); + List testDeviceIds = call.argument("testDeviceIds"); + if (maxAdContentRating != null) { + builder.setMaxAdContentRating(maxAdContentRating); + } + if (tagForChildDirectedTreatment != null) { + builder.setTagForChildDirectedTreatment(tagForChildDirectedTreatment); + } + if (tagForUnderAgeOfConsent != null) { + builder.setTagForUnderAgeOfConsent(tagForUnderAgeOfConsent); + } + if (testDeviceIds != null) { + builder.setTestDeviceIds(testDeviceIds); + } + MobileAds.setRequestConfiguration(builder.build()); + } + + protected static class Builder { + @Nullable private String maxAdContentRating; + private Integer tagForChildDirectedTreatment = TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED; + private Integer tagForUnderAgeOfConsent = TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED; + @Nullable private List testDeviceIds; + + @CanIgnoreReturnValue + Builder setMaxAdContentRating(String maxAdContentRating) { + this.maxAdContentRating = maxAdContentRating; + return this; + } + + @CanIgnoreReturnValue + Builder setTagForChildDirectedTreatment(Integer tagForChildDirectedTreatment) { + this.tagForChildDirectedTreatment = tagForChildDirectedTreatment; + return this; + } + + @CanIgnoreReturnValue + Builder setTagForUnderAgeOfConsent(Integer tagForUnderAgeOfConsent) { + this.tagForUnderAgeOfConsent = tagForUnderAgeOfConsent; + return this; + } + + @CanIgnoreReturnValue + Builder setTestDeviceIds(List testDeviceIds) { + this.testDeviceIds = testDeviceIds; + return this; + } + + @Nullable + protected String getMaxAdContentRating() { + return maxAdContentRating; + } + + protected Integer getTagForChildDirectedTreatment() { + return tagForChildDirectedTreatment; + } + + protected Integer getTagForUnderAgeOfConsent() { + return tagForUnderAgeOfConsent; + } + + @Nullable + protected List getTestDeviceIds() { + return testDeviceIds; + } + + FlutterRequestConfiguration build() { + return new FlutterRequestConfiguration( + maxAdContentRating, tagForChildDirectedTreatment, tagForUnderAgeOfConsent, testDeviceIds); + } + } + + public FlutterRequestConfiguration( + @Nullable String maxAdContentRating, + Integer tagForChildDirectedTreatment, + Integer tagForUnderAgeOfConsent, + @Nullable List testDeviceIds) { + this.maxAdContentRating = maxAdContentRating; + this.tagForChildDirectedTreatment = tagForChildDirectedTreatment; + this.tagForUnderAgeOfConsent = tagForUnderAgeOfConsent; + this.testDeviceIds = testDeviceIds; + } + + @Nullable + protected String getMaxAdContentRating() { + return maxAdContentRating; + } + + protected Integer getTagForChildDirectedTreatment() { + return tagForChildDirectedTreatment; + } + + protected Integer getTagForUnderAgeOfConsent() { + return tagForUnderAgeOfConsent; + } + + @Nullable + protected List getTestDeviceIds() { + return testDeviceIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof FlutterRequestConfiguration)) { + return false; + } + + FlutterRequestConfiguration config = (FlutterRequestConfiguration) o; + return Objects.equals(maxAdContentRating, config.maxAdContentRating) + && Objects.equals(tagForChildDirectedTreatment, config.tagForChildDirectedTreatment) + && Objects.equals(tagForUnderAgeOfConsent, config.tagForUnderAgeOfConsent) + && Objects.equals(testDeviceIds, config.testDeviceIds); + } + + @Override + public int hashCode() { + return Objects.hash( + maxAdContentRating, + tagForChildDirectedTreatment, + tagForUnderAgeOfConsent, + testDeviceIds); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterRewardedAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterRewardedAd.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterRewardedAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterRewardedAd.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterRewardedInterstitialAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterRewardedInterstitialAd.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterRewardedInterstitialAd.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterRewardedInterstitialAd.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterServerSideVerificationOptions.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterServerSideVerificationOptions.java similarity index 100% rename from packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterServerSideVerificationOptions.java rename to packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterServerSideVerificationOptions.java diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/NativeAdFactory.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/NativeAdFactory.java new file mode 100644 index 000000000..c34db3837 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/NativeAdFactory.java @@ -0,0 +1,40 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileads; + +import com.google.android.gms.ads.nativead.NativeAd; +import com.google.android.gms.ads.nativead.NativeAdView; +import java.util.Map; + +/** + * Interface used to display a {@link NativeAd}. + * + *

Added to a {@link GoogleMobileAdsPlugin} and creates + * {@link NativeAdView}s from Native Ads created in Dart. + */ +public interface NativeAdFactory { + /** + * Creates a {@link NativeAdView} with a {@link + * NativeAd}. + * + * @param nativeAd Ad information used to create a {@link + * NativeAd} + * @param customOptions Used to pass additional custom options to create the {@link + * NativeAdView}. Nullable. + * @return a {@link NativeAdView} that is overlaid on top of + * the FlutterView + */ + NativeAdView createNativeAd(NativeAd nativeAd, Map customOptions); +} diff --git a/packages/google_mobile_ads/android/src/main/res/layout/gnt_medium_template_view.xml b/packages/google_mobile_ads/android/src/playServices/res/layout/gnt_medium_template_view.xml similarity index 97% rename from packages/google_mobile_ads/android/src/main/res/layout/gnt_medium_template_view.xml rename to packages/google_mobile_ads/android/src/playServices/res/layout/gnt_medium_template_view.xml index a9153d795..d4184b806 100644 --- a/packages/google_mobile_ads/android/src/main/res/layout/gnt_medium_template_view.xml +++ b/packages/google_mobile_ads/android/src/playServices/res/layout/gnt_medium_template_view.xml @@ -86,6 +86,7 @@ android:layout_width="match_parent" android:layout_height="@dimen/gnt_no_size" android:layout_weight="0.5" + android:maxLength="25" android:gravity="left" android:background="@android:color/transparent" app:layout_constraintBottom_toTopOf="@+id/row_two" @@ -197,17 +198,21 @@ android:paddingBottom="@dimen/gnt_default_margin" android:paddingLeft="@dimen/gnt_default_margin" android:id="@+id/body" + android:maxLength="90" + android:maxLines="2" + android:ellipsize="end" app:layout_constraintBottom_toTopOf="@+id/cta" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/middle" + app:layout_constraintHeight_max="50dp" android:paddingStart="@dimen/gnt_default_margin" android:layout_marginBottom="@dimen/gnt_small_margin" /> diff --git a/packages/google_mobile_ads/android/src/main/res/layout/gnt_small_template_view.xml b/packages/google_mobile_ads/android/src/playServices/res/layout/gnt_small_template_view.xml similarity index 96% rename from packages/google_mobile_ads/android/src/main/res/layout/gnt_small_template_view.xml rename to packages/google_mobile_ads/android/src/playServices/res/layout/gnt_small_template_view.xml index 02db5fda2..189dbef42 100644 --- a/packages/google_mobile_ads/android/src/main/res/layout/gnt_small_template_view.xml +++ b/packages/google_mobile_ads/android/src/playServices/res/layout/gnt_small_template_view.xml @@ -18,10 +18,10 @@ mockOptions = mock(Map.class); FlutterNativeAdOptions mockFlutterNativeAdOptions = mock(FlutterNativeAdOptions.class); @@ -339,7 +338,7 @@ public void nativeAdBuilderNullManager() { new FlutterNativeAd.Builder(ApplicationProvider.getApplicationContext()) .setManager(null) .setAdUnitId("testId") - .setAdFactory(mock(GoogleMobileAdsPlugin.NativeAdFactory.class)) + .setAdFactory(mock(NativeAdFactory.class)) .setRequest(request) .build(); } @@ -349,7 +348,7 @@ public void nativeAdBuilderNullAdUnitId() { new FlutterNativeAd.Builder(ApplicationProvider.getApplicationContext()) .setManager(testManager) .setAdUnitId(null) - .setAdFactory(mock(GoogleMobileAdsPlugin.NativeAdFactory.class)) + .setAdFactory(mock(NativeAdFactory.class)) .setRequest(request) .build(); } @@ -369,7 +368,7 @@ public void nativeAdBuilderNullRequest() { new FlutterNativeAd.Builder(ApplicationProvider.getApplicationContext()) .setManager(testManager) .setAdUnitId("testId") - .setAdFactory(mock(GoogleMobileAdsPlugin.NativeAdFactory.class)) + .setAdFactory(mock(NativeAdFactory.class)) .build(); } } diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java index 832d1d489..093552aa5 100644 --- a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java @@ -344,7 +344,7 @@ public void flutterAdListener_onNativeAdClicked() { .setAdUnitId("testId") .setRequest(request) .setAdFactory( - new GoogleMobileAdsPlugin.NativeAdFactory() { + new NativeAdFactory() { @Override public NativeAdView createNativeAd( NativeAd nativeAd, Map customOptions) { @@ -373,7 +373,7 @@ public void flutterAdListener_onNativeAdImpression() { .setAdUnitId("testId") .setRequest(request) .setAdFactory( - new GoogleMobileAdsPlugin.NativeAdFactory() { + new NativeAdFactory() { @Override public NativeAdView createNativeAd( NativeAd nativeAd, Map customOptions) { @@ -784,6 +784,13 @@ public void testGetAnchoredAdaptiveBannerAdSize() { verify(result).success(adSize.getHeight()); + adSize = AdSize.getLargePortraitAnchoredAdaptiveBannerAdSize(context, 23); + methodCall = new MethodCall("AdSize#getLargeAnchoredAdaptiveBannerAdSize", arguments); + result = mock(Result.class); + plugin.onMethodCall(methodCall, result); + + verify(result).success(adSize.getHeight()); + // Test for landscape Banner AdSize. arguments = new HashMap<>(); arguments.put("orientation", "landscape"); @@ -796,6 +803,13 @@ public void testGetAnchoredAdaptiveBannerAdSize() { verify(result).success(adSize.getHeight()); + adSize = AdSize.getLargeLandscapeAnchoredAdaptiveBannerAdSize(context, 23); + methodCall = new MethodCall("AdSize#getLargeAnchoredAdaptiveBannerAdSize", arguments); + result = mock(Result.class); + plugin.onMethodCall(methodCall, result); + + verify(result).success(adSize.getHeight()); + // Test for current orientation (inferred) Banner AdSize. arguments = new HashMap<>(); arguments.put("width", 23); @@ -806,6 +820,13 @@ public void testGetAnchoredAdaptiveBannerAdSize() { plugin.onMethodCall(methodCall, result); verify(result).success(adSize.getHeight()); + + adSize = AdSize.getLargeAnchoredAdaptiveBannerAdSize(context, 23); + methodCall = new MethodCall("AdSize#getLargeAnchoredAdaptiveBannerAdSize", arguments); + result = mock(Result.class); + plugin.onMethodCall(methodCall, result); + + verify(result).success(adSize.getHeight()); } public void testGetAdSize_bannerAd() { diff --git a/packages/google_mobile_ads/example/android/app/build.gradle b/packages/google_mobile_ads/example/android/app/build.gradle index c6fdf5d91..7edbf4ea6 100644 --- a/packages/google_mobile_ads/example/android/app/build.gradle +++ b/packages/google_mobile_ads/example/android/app/build.gradle @@ -22,8 +22,33 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } +def dartEnvironmentVariables = [] +if (project.hasProperty('dart-defines')) { + dartEnvironmentVariables = project.property('dart-defines') + .split(',') + .collectEntries { entry -> + def pair = new String(entry.decodeBase64(), 'UTF-8').split('=') + [(pair.first()): pair.last()] + } +} + android { - compileSdk 35 + def useNextGenSdk = dartEnvironmentVariables.USE_NEXT_GEN_SDK ? dartEnvironmentVariables.USE_NEXT_GEN_SDK : false + sourceSets.getByName("main") { + java.srcDirs("src/main/java") + if (useNextGenSdk) { + java.srcDirs("src/nextgengma/java") + } else { + java.srcDirs("src/playservices/java") + } + res.srcDirs("src/main/res") + if (useNextGenSdk) { + res.srcDirs("src/nextgengma/res") + } else { + res.srcDirs("src/playservices/res") + } + } + compileSdk 36 namespace 'io.flutter.plugins.googlemobileadsexample' @@ -32,9 +57,9 @@ android { } defaultConfig { - applicationId "io.flutter.plugins.googlemobileadsexample" - minSdkVersion 23 - targetSdkVersion 34 + applicationId 'io.flutter.plugins.googlemobileadsexample' + minSdk 24 + targetSdk 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/google_mobile_ads/example/android/app/src/main/java/io/flutter/plugins/googlemobileadsexample/MainActivity.java b/packages/google_mobile_ads/example/android/app/src/main/java/io/flutter/plugins/googlemobileadsexample/MainActivity.java index 59fc22db5..3cef3ad53 100644 --- a/packages/google_mobile_ads/example/android/app/src/main/java/io/flutter/plugins/googlemobileadsexample/MainActivity.java +++ b/packages/google_mobile_ads/example/android/app/src/main/java/io/flutter/plugins/googlemobileadsexample/MainActivity.java @@ -17,7 +17,7 @@ import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin; -import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory; +import io.flutter.plugins.googlemobileads.NativeAdFactory; public class MainActivity extends FlutterActivity { @Override diff --git a/packages/google_mobile_ads/example/android/app/src/nextgengma/java/io/flutter/plugins/googlemobileadsexample/NativeAdFactoryExample.java b/packages/google_mobile_ads/example/android/app/src/nextgengma/java/io/flutter/plugins/googlemobileadsexample/NativeAdFactoryExample.java new file mode 100644 index 000000000..43b5253f1 --- /dev/null +++ b/packages/google_mobile_ads/example/android/app/src/nextgengma/java/io/flutter/plugins/googlemobileadsexample/NativeAdFactoryExample.java @@ -0,0 +1,114 @@ +// Copyright 2026 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 +// +// https://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. + +package io.flutter.plugins.googlemobileadsexample; + +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.RatingBar; +import android.widget.TextView; +import com.google.android.libraries.ads.mobile.sdk.nativead.MediaView; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView; +import io.flutter.plugins.googlemobileads.NativeAdFactory; +import java.util.Map; + +class NativeAdFactoryExample implements NativeAdFactory { + private final LayoutInflater layoutInflater; + + NativeAdFactoryExample(LayoutInflater layoutInflater) { + this.layoutInflater = layoutInflater; + } + + @Override + public NativeAdView createNativeAd(NativeAd nativeAd, Map customOptions) { + final NativeAdView adView = (NativeAdView) layoutInflater.inflate(R.layout.my_native_ad, null); + + // Set the media view. + MediaView mediaView = (MediaView) adView.findViewById(R.id.ad_media); + + // Set other ad assets. + adView.setHeadlineView(adView.findViewById(R.id.ad_headline)); + adView.setBodyView(adView.findViewById(R.id.ad_body)); + adView.setCallToActionView(adView.findViewById(R.id.ad_call_to_action)); + adView.setIconView(adView.findViewById(R.id.ad_app_icon)); + adView.setPriceView(adView.findViewById(R.id.ad_price)); + adView.setStarRatingView(adView.findViewById(R.id.ad_stars)); + adView.setStoreView(adView.findViewById(R.id.ad_store)); + adView.setAdvertiserView(adView.findViewById(R.id.ad_advertiser)); + + // The headline and mediaContent are guaranteed to be in every NativeAd. + ((TextView) adView.getHeadlineView()).setText(nativeAd.getHeadline()); + mediaView.setMediaContent(nativeAd.getMediaContent()); + + // These assets aren't guaranteed to be in every NativeAd, so it's important to + // check before trying to display them. + if (nativeAd.getBody() == null) { + adView.getBodyView().setVisibility(View.INVISIBLE); + } else { + adView.getBodyView().setVisibility(View.VISIBLE); + ((TextView) adView.getBodyView()).setText(nativeAd.getBody()); + } + + if (nativeAd.getCallToAction() == null) { + adView.getCallToActionView().setVisibility(View.INVISIBLE); + } else { + adView.getCallToActionView().setVisibility(View.VISIBLE); + ((Button) adView.getCallToActionView()).setText(nativeAd.getCallToAction()); + } + + if (nativeAd.getIcon() == null) { + adView.getIconView().setVisibility(View.GONE); + } else { + ((ImageView) adView.getIconView()).setImageDrawable(nativeAd.getIcon().getDrawable()); + adView.getIconView().setVisibility(View.VISIBLE); + } + + if (nativeAd.getPrice() == null) { + adView.getPriceView().setVisibility(View.INVISIBLE); + } else { + adView.getPriceView().setVisibility(View.VISIBLE); + ((TextView) adView.getPriceView()).setText(nativeAd.getPrice()); + } + + if (nativeAd.getStore() == null) { + adView.getStoreView().setVisibility(View.INVISIBLE); + } else { + adView.getStoreView().setVisibility(View.VISIBLE); + ((TextView) adView.getStoreView()).setText(nativeAd.getStore()); + } + + if (nativeAd.getStarRating() == null) { + adView.getStarRatingView().setVisibility(View.INVISIBLE); + } else { + ((RatingBar) adView.getStarRatingView()).setRating(nativeAd.getStarRating().floatValue()); + adView.getStarRatingView().setVisibility(View.VISIBLE); + } + + if (nativeAd.getAdvertiser() == null) { + adView.getAdvertiserView().setVisibility(View.INVISIBLE); + } else { + adView.getAdvertiserView().setVisibility(View.VISIBLE); + ((TextView) adView.getAdvertiserView()).setText(nativeAd.getAdvertiser()); + } + + // This method tells the Google Mobile Ads SDK that you have finished populating your + // native ad view with this native ad. + adView.registerNativeAd(nativeAd, mediaView); + + return adView; + } +} diff --git a/packages/google_mobile_ads/example/android/app/src/nextgengma/res/layout/my_native_ad.xml b/packages/google_mobile_ads/example/android/app/src/nextgengma/res/layout/my_native_ad.xml new file mode 100644 index 000000000..8f64f532e --- /dev/null +++ b/packages/google_mobile_ads/example/android/app/src/nextgengma/res/layout/my_native_ad.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +