From 2bd166736bc1b7316b2ce8b51b099ed2f54bec63 Mon Sep 17 00:00:00 2001 From: Anagha Sasikumar Date: Thu, 10 Oct 2024 12:09:50 +0000 Subject: [PATCH 1/3] Add InApp Banner Ad. --- .../java/com/example/client/MainActivity.kt | 18 ++--- .../example/implementation/SdkServiceImpl.kt | 6 +- .../inapp-mediatee-sdk-adapter/build.gradle | 9 ++- .../implementation/InAppMediateeSdkAdapter.kt | 5 +- .../implementation/SandboxedUiAdapterImpl.kt | 74 +++++++++++++++++++ .../com/inappmediatee/sdk/InAppMediateeSdk.kt | 19 +++++ 6 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/SandboxedUiAdapterImpl.kt diff --git a/PrivacySandboxKotlin/client-app/src/main/java/com/example/client/MainActivity.kt b/PrivacySandboxKotlin/client-app/src/main/java/com/example/client/MainActivity.kt index 5375fff..022a8ac 100644 --- a/PrivacySandboxKotlin/client-app/src/main/java/com/example/client/MainActivity.kt +++ b/PrivacySandboxKotlin/client-app/src/main/java/com/example/client/MainActivity.kt @@ -134,17 +134,13 @@ class MainActivity : AppCompatActivity() { // Mediated Banner Ad is shown when RUNTIME_MEDIATEE Mediation option is chosen. val mediationType = MediationOption.entries[mediationDropDownMenu.selectedItemId.toInt()].toString() - if (mediationType == MediationOption.INAPP_MEDIATEE.toString()) { - makeToast("RE_SDK<>InApp Mediated Banner Ad not yet implemented!") - } else { - bannerAd.loadAd( - this@MainActivity, - PACKAGE_NAME, - shouldStartActivityPredicate(), - loadWebView, - mediationType - ) - } + bannerAd.loadAd( + this@MainActivity, + PACKAGE_NAME, + shouldStartActivityPredicate(), + loadWebView, + mediationType + ) } private fun showFullscreenView() = lifecycleScope.launch { diff --git a/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt b/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt index d84e730..cd0c42c 100644 --- a/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt +++ b/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt @@ -69,11 +69,15 @@ class SdkServiceImpl(private val context: Context) : SdkService { bannerAdAdapter.addObserverFactory(SessionObserverFactoryImpl()) return bannerAdAdapter } + var adapter = mediateeAdapter + if (mediationType == context.getString(R.string.mediation_option_inapp_mediatee)) { + adapter = inAppMediateeAdapter + } return SdkSandboxedUiAdapterImpl( context, request, SandboxedUiAdapterFactory.createFromCoreLibInfo(checkNotNull( - mediateeAdapter?.getBannerAd( + adapter?.getBannerAd( request.appPackageName, request.activityLauncher, request.isWebViewBannerAd diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/build.gradle b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/build.gradle index 5fe51f8..14a4207 100644 --- a/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/build.gradle +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/build.gradle @@ -49,13 +49,16 @@ dependencies { debugImplementation project(':example-sdk-bundle') implementation project(':inapp-mediatee-sdk') - implementation 'androidx.privacysandbox.activity:activity-core:1.0.0-alpha01' - implementation 'androidx.privacysandbox.activity:activity-provider:1.0.0-alpha01' implementation 'androidx.activity:activity-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10' implementation "androidx.lifecycle:lifecycle-common:2.7.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" + + implementation "androidx.privacysandbox.ui:ui-core:$privacy_sandbox_ui_version" + implementation "androidx.privacysandbox.ui:ui-provider:$privacy_sandbox_ui_version" + + implementation "androidx.privacysandbox.activity:activity-core:$privacy_sandbox_activity_version" + implementation "androidx.privacysandbox.activity:activity-provider:$privacy_sandbox_activity_version" } diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/InAppMediateeSdkAdapter.kt b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/InAppMediateeSdkAdapter.kt index 0ad3b10..76b05c9 100644 --- a/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/InAppMediateeSdkAdapter.kt +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/InAppMediateeSdkAdapter.kt @@ -2,9 +2,11 @@ package com.inappmediateeadapter.implementation import android.content.Context import android.os.Bundle +import androidx.privacysandbox.ui.provider.toCoreLibInfo import androidx.privacysandbox.activity.core.SdkActivityLauncher import com.inappmediatee.sdk.InAppMediateeSdk import com.example.api.MediateeAdapterInterface +import java.com.inappmediateeadapter.implementation.SandboxedUiAdapterImpl /** * Adapter class that implements the interface declared by the Mediator. @@ -20,7 +22,8 @@ class InAppMediateeSdkAdapter(private val context: Context): MediateeAdapterInte activityLauncher: SdkActivityLauncher, isWebViewBannerAd: Boolean ): Bundle { - TODO("Not yet implemented") + return SandboxedUiAdapterImpl(inAppMediateeSdk.loadBannerAd(isWebViewBannerAd)) + .toCoreLibInfo(context) } override suspend fun loadFullscreenAd() { diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/SandboxedUiAdapterImpl.kt b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/SandboxedUiAdapterImpl.kt new file mode 100644 index 0000000..c1edc53 --- /dev/null +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/src/main/java/java/com/inappmediateeadapter/implementation/SandboxedUiAdapterImpl.kt @@ -0,0 +1,74 @@ +package java.com.inappmediateeadapter.implementation + +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.os.IBinder +import android.view.View +import androidx.privacysandbox.ui.core.SandboxedUiAdapter +import androidx.privacysandbox.ui.core.SessionObserverFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import java.util.concurrent.Executor + +class SandboxedUiAdapterImpl(private val mediateeAdView: View): SandboxedUiAdapter { + override fun openSession( + context: Context, + windowInputToken: IBinder, + initialWidth: Int, + initialHeight: Int, + isZOrderOnTop: Boolean, + clientExecutor: Executor, + client: SandboxedUiAdapter.SessionClient + ) { + val session = SdkUiSession(clientExecutor, mediateeAdView) + clientExecutor.execute { + client.onSessionOpened(session) + } + } + + override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) { + // Adds a [SessionObserverFactory] with a [SandboxedUiAdapter] for tracking UI presentation + // state across UI sessions. This has no effect on already open sessions. + } + + override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) { + // Removes a [SessionObserverFactory] from a [SandboxedUiAdapter], if it has been + // previously added with [addObserverFactory]. + } +} + +private class SdkUiSession(clientExecutor: Executor, mediateeAdView: View) : + SandboxedUiAdapter.Session { + + /** A scope for launching coroutines in the client executor. */ + private val scope = CoroutineScope(clientExecutor.asCoroutineDispatcher() + Job()) + + override val signalOptions: Set = setOf() + + override val view: View = mediateeAdView + + override fun close() { + // Notifies that the client has closed the session. It's a good opportunity to dispose + // any resources that were acquired to maintain the session. + scope.cancel() + } + + override fun notifyConfigurationChanged(configuration: Configuration) { + // Notifies that the device configuration has changed and affected the app. + } + + override fun notifyResized(width: Int, height: Int) { + // Notifies that the size of the presentation area in the app has changed. + } + + override fun notifyUiChanged(uiContainerInfo: Bundle) { + // Notify the session when the presentation state of its UI container has changed. + } + + override fun notifyZOrderChanged(isZOrderOnTop: Boolean) { + // Notifies that the Z order has changed for the UI associated by this session. + } +} \ No newline at end of file diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt index 0464c77..1724af8 100644 --- a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt @@ -2,9 +2,28 @@ package com.inappmediatee.sdk import android.content.Context import android.content.Intent +import android.graphics.Color +import android.view.View +import android.webkit.WebView +import android.widget.TextView class InAppMediateeSdk(private val context: Context) { + private val webViewUrl = "https://www.google.com" + private val bannerAdMsg = "Rendered from In-app Mediatee SDK" + + fun loadBannerAd(isWebViewBannerAd: Boolean) : View { + if (isWebViewBannerAd) { + val webview = WebView(context) + webview.loadUrl(webViewUrl) + return webview + } + val textView = TextView(context) + textView.text = bannerAdMsg + textView.setTextColor(Color.BLACK) + return textView + } + fun loadFullscreenAd() { // All the heavy logic to load fullscreen Ad that Mdiatee needs to perform goes here. } From 2e876ec14a40329521e8f62a265d8e93c5b9bad1 Mon Sep 17 00:00:00 2001 From: Anagha Sasikumar Date: Tue, 22 Oct 2024 12:15:59 +0000 Subject: [PATCH 2/3] No overlay for In-app mediatee + add README file. --- .../main/java/com/example/api/SdkService.kt | 3 ++- .../example/implementation/SdkServiceImpl.kt | 20 ++++++++++++------- .../main/java/com/existing/sdk/BannerAd.kt | 8 +++++++- .../inapp-mediatee-sdk-adapter/README.md | 9 +++++++++ .../com/inappmediatee/sdk/InAppMediateeSdk.kt | 6 +++--- 5 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/README.md diff --git a/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/api/SdkService.kt b/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/api/SdkService.kt index 6cd9f34..94873df 100644 --- a/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/api/SdkService.kt +++ b/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/api/SdkService.kt @@ -15,6 +15,7 @@ */ package com.example.api +import android.os.Bundle import androidx.privacysandbox.tools.PrivacySandboxService @PrivacySandboxService @@ -23,7 +24,7 @@ interface SdkService { suspend fun createFile(sizeInMb: Int): String - suspend fun getBanner(request: SdkBannerRequest, mediationType: String): SdkSandboxedUiAdapter? + suspend fun getBanner(request: SdkBannerRequest, mediationType: String): Bundle? suspend fun getFullscreenAd(mediationType: String): FullscreenAd diff --git a/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt b/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt index cd0c42c..ab53677 100644 --- a/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt +++ b/PrivacySandboxKotlin/example-sdk/src/main/java/com/example/implementation/SdkServiceImpl.kt @@ -33,8 +33,8 @@ import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo import androidx.privacysandbox.ui.core.SessionObserver import androidx.privacysandbox.ui.core.SessionObserverContext import androidx.privacysandbox.ui.core.SessionObserverFactory +import androidx.privacysandbox.ui.provider.toCoreLibInfo import com.example.api.MediateeAdapterInterface -import com.example.api.SdkSandboxedUiAdapter class SdkServiceImpl(private val context: Context) : SdkService { override suspend fun getMessage(): String = "Hello from Privacy Sandbox!" @@ -63,27 +63,33 @@ class SdkServiceImpl(private val context: Context) : SdkService { override suspend fun getBanner( request: SdkBannerRequest, mediationType: String - ): SdkSandboxedUiAdapter? { + ): Bundle? { if (mediationType == context.getString(R.string.mediation_option_none)) { val bannerAdAdapter = SdkSandboxedUiAdapterImpl(context, request, null) bannerAdAdapter.addObserverFactory(SessionObserverFactoryImpl()) - return bannerAdAdapter + return bannerAdAdapter.toCoreLibInfo(context) } - var adapter = mediateeAdapter + // For In-app mediatee, SandboxedUiAdapter returned by mediatee is not wrapped, it is + // directly returned to app. This is to avoid nested remote rendering. + // There is no overlay in this case for this reason. if (mediationType == context.getString(R.string.mediation_option_inapp_mediatee)) { - adapter = inAppMediateeAdapter + return inAppMediateeAdapter?.getBannerAd( + request.appPackageName, + request.activityLauncher, + request.isWebViewBannerAd + ) } return SdkSandboxedUiAdapterImpl( context, request, SandboxedUiAdapterFactory.createFromCoreLibInfo(checkNotNull( - adapter?.getBannerAd( + mediateeAdapter?.getBannerAd( request.appPackageName, request.activityLauncher, request.isWebViewBannerAd ) ) { "No banner Ad received from mediatee!" }) - ) + ).toCoreLibInfo(context) } override suspend fun getFullscreenAd(mediationType: String): FullscreenAd { diff --git a/PrivacySandboxKotlin/existing-sdk/src/main/java/com/existing/sdk/BannerAd.kt b/PrivacySandboxKotlin/existing-sdk/src/main/java/com/existing/sdk/BannerAd.kt index 3e735c9..a316cc0 100644 --- a/PrivacySandboxKotlin/existing-sdk/src/main/java/com/existing/sdk/BannerAd.kt +++ b/PrivacySandboxKotlin/existing-sdk/src/main/java/com/existing/sdk/BannerAd.kt @@ -22,6 +22,7 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.privacysandbox.activity.client.createSdkActivityLauncher +import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory import androidx.privacysandbox.ui.client.view.SandboxedSdkView import androidx.privacysandbox.ui.core.SandboxedUiAdapter import com.example.api.SdkBannerRequest @@ -69,7 +70,12 @@ class BannerAd(context: Context, attrs: AttributeSet) : LinearLayout(context, at val launcher = baseActivity.createSdkActivityLauncher(allowSdkActivityLaunch) val request = SdkBannerRequest(message, launcher, shouldLoadWebView) - return ExistingSdk.loadSdkIfNeeded(context)?.getBanner(request, mediationType) + return SandboxedUiAdapterFactory.createFromCoreLibInfo( + checkNotNull( + ExistingSdk.loadSdkIfNeeded( + context + )?.getBanner(request, mediationType) + ) { "No banner Ad received from ad SDK!" }) } private fun addViewToLayout(view: View) { diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/README.md b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/README.md new file mode 100644 index 0000000..e251b5d --- /dev/null +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk-adapter/README.md @@ -0,0 +1,9 @@ +# In App Mediatee Adapter SDK + +This SDK is Runtime Aware but runs in the App process. It facilitates interaction between mediator +and in-app mediatee. + +Implements MediateeAdapterInterface declared by mediator (example-sdk). + +This could be owned by the mediator sdk during transition, or optionally all the logic here could +also be a part of RA_SDK (existing-sdk). diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt index 1724af8..041b10b 100644 --- a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt @@ -10,7 +10,7 @@ import android.widget.TextView class InAppMediateeSdk(private val context: Context) { private val webViewUrl = "https://www.google.com" - private val bannerAdMsg = "Rendered from In-app Mediatee SDK" + private val bannerAdMsg = "Ad from In-app Mediatee SDK" fun loadBannerAd(isWebViewBannerAd: Boolean) : View { if (isWebViewBannerAd) { @@ -20,12 +20,12 @@ class InAppMediateeSdk(private val context: Context) { } val textView = TextView(context) textView.text = bannerAdMsg - textView.setTextColor(Color.BLACK) + textView.setTextColor(Color.WHITE) return textView } fun loadFullscreenAd() { - // All the heavy logic to load fullscreen Ad that Mdiatee needs to perform goes here. + // All the heavy logic to load fullscreen Ad that Mediatee needs to perform goes here. } fun showFullscreenAd() { From 3fbc36ed71513eac22cace3dc2ee451d185d65d4 Mon Sep 17 00:00:00 2001 From: Anagha Sasikumar Date: Tue, 22 Oct 2024 21:35:12 +0000 Subject: [PATCH 3/3] Add banner.xml in InAppMediateeSdk. --- .../com/inappmediatee/sdk/InAppMediateeSdk.kt | 9 +---- .../src/main/res/layout/banner.xml | 38 +++++++++++++++++++ .../src/main/res/values/strings.xml | 1 + 3 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/layout/banner.xml diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt index 041b10b..55df26d 100644 --- a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/java/com/inappmediatee/sdk/InAppMediateeSdk.kt @@ -2,15 +2,13 @@ package com.inappmediatee.sdk import android.content.Context import android.content.Intent -import android.graphics.Color import android.view.View import android.webkit.WebView -import android.widget.TextView +import com.inappmediatee.R class InAppMediateeSdk(private val context: Context) { private val webViewUrl = "https://www.google.com" - private val bannerAdMsg = "Ad from In-app Mediatee SDK" fun loadBannerAd(isWebViewBannerAd: Boolean) : View { if (isWebViewBannerAd) { @@ -18,10 +16,7 @@ class InAppMediateeSdk(private val context: Context) { webview.loadUrl(webViewUrl) return webview } - val textView = TextView(context) - textView.text = bannerAdMsg - textView.setTextColor(Color.WHITE) - return textView + return View.inflate(context, R.layout.banner, null) } fun loadFullscreenAd() { diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/layout/banner.xml b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/layout/banner.xml new file mode 100644 index 0000000..ee662a5 --- /dev/null +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/layout/banner.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/values/strings.xml b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/values/strings.xml index 6fa7ad7..8005e27 100644 --- a/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/values/strings.xml +++ b/PrivacySandboxKotlin/inapp-mediatee-sdk/src/main/res/values/strings.xml @@ -13,4 +13,5 @@ --> Full screen ad launched by in-app mediatee! + Ad from In-app Mediatee SDK (no overlay from mediator)