From 28a986e4dedd69229553b94077f6ab646092cdb2 Mon Sep 17 00:00:00 2001 From: Nicholas Ventimiglia Date: Wed, 15 May 2024 14:47:25 -0700 Subject: [PATCH] Added Jetpack Compose Native Compose Sample. PiperOrigin-RevId: 634082840 --- .../JetpackComposeDemo/app/build.gradle.kts | 59 +++ .../JetpackComposeDemo/app/proguard-rules.pro | 21 + .../app/src/main/AndroidManifest.xml | 41 ++ .../GoogleMobileAdsManager.kt | 153 ++++++++ .../jetpackcomposedemo/MainActivity.kt | 188 +++++++++ .../MobileAdsApplication.kt | 25 ++ .../jetpackcomposedemo/NativeActivity.kt | 152 ++++++++ .../NativeComposeActivity.kt | 213 +++++++++++ .../composables/NativeAd.kt | 161 ++++++++ .../composables/NativeAdState.kt | 48 +++ .../composables/NativeAdView.kt | 359 ++++++++++++++++++ .../composables/StatusText.kt | 40 ++ .../composables/TextButton.kt | 43 +++ .../jetpackcomposedemo/ui/StatusText.kt | 33 ++ .../jetpackcomposedemo/ui/theme/Color.kt | 31 ++ .../jetpackcomposedemo/ui/theme/Theme.kt | 66 ++++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ .../app/src/main/res/layout/nativead.xml | 141 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 14 + .../app/src/main/res/values/themes.xml | 5 + .../JetpackComposeDemo/build.gradle.kts | 7 + .../JetpackComposeDemo/gradle.properties | 23 ++ .../gradle/wrapper/gradle-wrapper.properties | 6 + kotlin/advanced/JetpackComposeDemo/gradlew | 160 ++++++++ .../advanced/JetpackComposeDemo/gradlew.bat | 90 +++++ .../JetpackComposeDemo/settings.gradle.kts | 19 + 40 files changed, 2320 insertions(+) create mode 100644 kotlin/advanced/JetpackComposeDemo/app/build.gradle.kts create mode 100644 kotlin/advanced/JetpackComposeDemo/app/proguard-rules.pro create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/AndroidManifest.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/GoogleMobileAdsManager.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MainActivity.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MobileAdsApplication.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeActivity.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeComposeActivity.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAd.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdState.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdView.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/StatusText.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/TextButton.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/StatusText.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Color.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Theme.kt create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/layout/nativead.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/values/colors.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/values/strings.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/app/src/main/res/values/themes.xml create mode 100644 kotlin/advanced/JetpackComposeDemo/build.gradle.kts create mode 100644 kotlin/advanced/JetpackComposeDemo/gradle.properties create mode 100644 kotlin/advanced/JetpackComposeDemo/gradle/wrapper/gradle-wrapper.properties create mode 100644 kotlin/advanced/JetpackComposeDemo/gradlew create mode 100644 kotlin/advanced/JetpackComposeDemo/gradlew.bat create mode 100644 kotlin/advanced/JetpackComposeDemo/settings.gradle.kts diff --git a/kotlin/advanced/JetpackComposeDemo/app/build.gradle.kts b/kotlin/advanced/JetpackComposeDemo/app/build.gradle.kts new file mode 100644 index 000000000..77fb08bd7 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.example.jetpackcomposedemo" + compileSdk = 34 + + defaultConfig { + applicationId = "com.google.android.gms.example.jetpackcomposedemo" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { useSupportLibrary = true } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = "1.8" } + buildFeatures { + compose = true + viewBinding = true + } + composeOptions { kotlinCompilerExtensionVersion = "1.5.1" } + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } +} + +dependencies { + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0") + implementation("androidx.activity:activity-compose:1.9.0") + implementation(platform("androidx.compose:compose-bom:2024.05.00")) + implementation("androidx.compose.ui:ui:1.7.0-beta03") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("com.google.android.gms:play-services-ads:23.1.0") + implementation("com.google.android.ump:user-messaging-platform:2.2.0") + implementation("com.google.android.gms:play-services-ads-lite:23.1.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2024.05.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/proguard-rules.pro b/kotlin/advanced/JetpackComposeDemo/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/AndroidManifest.xml b/kotlin/advanced/JetpackComposeDemo/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..dba349e8a --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/GoogleMobileAdsManager.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/GoogleMobileAdsManager.kt new file mode 100644 index 000000000..101c8fed5 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/GoogleMobileAdsManager.kt @@ -0,0 +1,153 @@ +package com.google.android.gms.example.jetpackcomposedemo + +import android.app.Activity +import android.content.Context +import android.util.Log +import androidx.annotation.IntDef +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import com.google.android.gms.ads.AdError +import com.google.android.gms.ads.MobileAds +import com.google.android.ump.* +import java.util.concurrent.atomic.AtomicBoolean + +/** This class manages the process of obtaining consent for and initializing Google Mobile Ads. */ +class GoogleMobileAdsManager { + + /** Represents initialization states for the Google Mobile Ads SDK. */ + object MobileAdsState { + /** Initial start state. */ + const val UNINITIALIZED = 0 + + /** User consent required but not yet obtained. */ + const val CONSENT_REQUIRED = 2 + + /** User consent obtained. Personalized vs non-personalized undefined. */ + const val CONSENT_OBTAINED = 3 + + /** Google Mobile Ads SDK initialized successfully. */ + const val INITIALIZED = 100 + + /** An error occurred when requesting consent. */ + const val CONSENT_REQUEST_ERROR = -1 + + /** An error occurred when showing the privacy options form. */ + const val CONSENT_FORM_ERROR = -2 + + @Target(AnnotationTarget.TYPE) + @IntDef( + UNINITIALIZED, + INITIALIZED, + CONSENT_REQUIRED, + CONSENT_OBTAINED, + CONSENT_REQUEST_ERROR, + CONSENT_FORM_ERROR, + ) + @Retention(AnnotationRetention.SOURCE) + annotation class State + } + + /** Represents current initialization states for the Google Mobile Ads SDK. */ + var mobileAdsState = mutableIntStateOf(MobileAdsState.UNINITIALIZED) + + /** Indicates whether the app has completed the steps for gathering updated user consent. */ + var canRequestAds = mutableStateOf(false) + + /** Helper variable to determine if the privacy options form is required. */ + var isPrivacyOptionsRequired = mutableStateOf(false) + + private var isMobileAdsInitializeCalled = AtomicBoolean(false) + + private lateinit var consentInformation: ConsentInformation + + /** + * Initiates the consent process and initializes the Google Mobile Ads SDK if the SDK is + * UNINITIALIZED. + * + * @param activity Activity responsible for initializing the Google Mobile Ads SDK. + * @param consentRequestParameters Parameters for the consent request form. + */ + fun initializeWithConsent( + activity: Activity, + consentRequestParameters: ConsentRequestParameters, + ) { + + if (isMobileAdsInitializeCalled.getAndSet(true)) { + return + } + + consentInformation = UserMessagingPlatform.getConsentInformation(activity) + + consentInformation.requestConsentInfoUpdate( + activity, + consentRequestParameters, + { + // Success callback. + showConsentFormIfRequired(activity) { error -> + if (error != null) { + Log.w(TAG, "Consent form error: ${error.errorCode} - ${error.message}") + mobileAdsState.intValue = MobileAdsState.CONSENT_FORM_ERROR + } else { + mobileAdsState.intValue = MobileAdsState.CONSENT_OBTAINED + } + canRequestAds.value = consentInformation.canRequestAds() + isPrivacyOptionsRequired.value = + consentInformation.privacyOptionsRequirementStatus == + ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED + if (consentInformation.canRequestAds()) { + initializeMobileAdsSdk(activity) + } + } + }, + { formError -> + // Failure callback. + Log.w(TAG, "Consent info update error: ${formError.errorCode} - ${formError.message}") + mobileAdsState.intValue = MobileAdsState.CONSENT_REQUEST_ERROR + }, + ) + + // This sample attempts to load ads using consent obtained from the previous session. + if (consentInformation.canRequestAds()) { + initializeMobileAdsSdk(activity) + } + } + + /** Shows the update consent form. */ + fun showPrivacyOptionsForm(activity: Activity) { + UserMessagingPlatform.showPrivacyOptionsForm(activity) { error -> + if (error != null) { + mobileAdsState.intValue = MobileAdsState.CONSENT_FORM_ERROR + } + } + } + + /** + * Initializes Mobile Ads SDK. + * + * @param activity Activity responsible for initializing the Google Mobile Ads SDK. + */ + fun initializeMobileAdsSdk(activity: Activity) { + MobileAds.initialize(activity) { + Log.d(TAG, "Mobile Ads SDK initialized") + mobileAdsState.intValue = MobileAdsState.INITIALIZED + } + } + + /** + * Opens the Ad Inspector UI. + * + * @param context The application or activity context required for launching the inspector. + * @param onAdInspectorResult A callback to handle the result of opening the Ad Inspector. + */ + fun openAdInspector(context: Context, onAdInspectorResult: (AdError?) -> Unit) { + MobileAds.openAdInspector(context) { error -> onAdInspectorResult(error) } + } + + private fun showConsentFormIfRequired(activity: Activity, onFormResult: (FormError?) -> Unit) { + UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity, onFormResult) + } + + companion object { + const val TAG = "GoogleMobileAdsSample" + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MainActivity.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MainActivity.kt new file mode 100644 index 000000000..dbc21a7c1 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MainActivity.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import com.example.jetpackcomposedemo.R +import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.RequestConfiguration +import com.google.android.gms.example.jetpackcomposedemo.composables.StatusText +import com.google.android.gms.example.jetpackcomposedemo.composables.TextButton +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateError +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateLoaded +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateUnloaded +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.JetpackComposeDemoTheme +import com.google.android.ump.ConsentDebugSettings +import com.google.android.ump.ConsentRequestParameters + +class MainActivity : ComponentActivity() { + + // This instance manages the process initializing Google Mobile Ads. + private val adsManager = GoogleMobileAdsManager() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Initialize the Google Mobile Ads SDK. + if (adsManager.mobileAdsState.intValue != GoogleMobileAdsManager.MobileAdsState.INITIALIZED) { + initMobileAds() + } + + setContent { + JetpackComposeDemoTheme { + Surface(modifier = Modifier.fillMaxHeight(), color = MaterialTheme.colorScheme.background) { + MainScreen() + } + } + } + } + + private fun initMobileAds() { + // Always use test ads: https://developers.google.com/admob/android/test-ads#kotlin + val testDeviceIds = listOf("33BE2250B43518CCDA7DE426D04EE231") + + // Configure RequestConfiguration. + val configuration = RequestConfiguration.Builder().setTestDeviceIds(testDeviceIds).build() + MobileAds.setRequestConfiguration(configuration) + + val debugSettings = ConsentDebugSettings.Builder(this) + // For testing purposes, you can force a DebugGeography of EEA or NOT_EEA. + // debugSettings.setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA); + + testDeviceIds.forEach { deviceId -> debugSettings.addTestDeviceHashedId(deviceId) } + val consentRequestParameters = + ConsentRequestParameters.Builder().setConsentDebugSettings(debugSettings.build()).build() + + adsManager.initializeWithConsent(this, consentRequestParameters) + } + + @Composable + @Preview + fun MainScreenPreview() { + JetpackComposeDemoTheme { + Surface(modifier = Modifier.fillMaxHeight(), color = MaterialTheme.colorScheme.background) { + MainScreen() + } + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun MainScreen() { + val context = LocalContext.current + val activity = this + + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + content = { + // Render title. + TopAppBar(title = { Text(resources.getString(R.string.main_title)) }) + // Render mobile ads status. + StatusText( + adsManager.mobileAdsState.intValue.messageColor(), + adsManager.mobileAdsState.intValue.messageText(), + ) + // Show Consent Form. + TextButton( + name = resources.getString(R.string.consent_show), + enabled = adsManager.isPrivacyOptionsRequired.value, + ) { + adsManager.showPrivacyOptionsForm(activity) + } + // Open Ad Inspector. + TextButton(name = resources.getString(R.string.adinspector)) { + adsManager.openAdInspector(context) { error -> + if (error != null) { + Toast.makeText( + context, + resources.getString(R.string.adinspector_error), + Toast.LENGTH_LONG, + ) + .show() + Log.e( + TAG, + String.format(resources.getString(R.string.adinspector_error), error.message), + ) + } + } + } + // Native Sample. + TextButton(name = "Native Layout", enabled = adsManager.canRequestAds.value) { + val intent = Intent(context, NativeActivity::class.java) + context.startActivity(intent) + } + // Native Compose Sample. + TextButton(name = "Native Compose", enabled = adsManager.canRequestAds.value) { + val intent = Intent(context, NativeComposeActivity::class.java) + context.startActivity(intent) + } + }, + ) + } + + // Extend MobileAdsState with message color. + private fun @GoogleMobileAdsManager.MobileAdsState.State Int.messageColor(): Color { + return when (this) { + GoogleMobileAdsManager.MobileAdsState.CONSENT_OBTAINED -> ColorStateUnloaded + GoogleMobileAdsManager.MobileAdsState.CONSENT_REQUEST_ERROR -> ColorStateError + GoogleMobileAdsManager.MobileAdsState.CONSENT_FORM_ERROR -> ColorStateError + GoogleMobileAdsManager.MobileAdsState.INITIALIZED -> ColorStateLoaded + else -> ColorStateUnloaded + } + } + + // Extend MobileAdsState with message text. + private fun @GoogleMobileAdsManager.MobileAdsState.State Int.messageText(): String { + return when (this) { + GoogleMobileAdsManager.MobileAdsState.UNINITIALIZED -> + resources.getString(R.string.mobileads_uninitialized) + GoogleMobileAdsManager.MobileAdsState.CONSENT_REQUIRED -> + resources.getString(R.string.mobileads_consentRequired) + GoogleMobileAdsManager.MobileAdsState.CONSENT_OBTAINED -> + resources.getString(R.string.mobileads_consentObtained) + GoogleMobileAdsManager.MobileAdsState.CONSENT_REQUEST_ERROR -> + resources.getString(R.string.mobileads_consentError) + GoogleMobileAdsManager.MobileAdsState.CONSENT_FORM_ERROR -> + resources.getString(R.string.mobileads_consentFormError) + GoogleMobileAdsManager.MobileAdsState.INITIALIZED -> + resources.getString(R.string.mobileads_initialized) + else -> resources.getString(R.string.mobileads_uninitialized) + } + } + + companion object { + const val TAG = "GoogleMobileAdsSample" + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MobileAdsApplication.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MobileAdsApplication.kt new file mode 100644 index 000000000..639511e7c --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MobileAdsApplication.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo + +import android.app.Application + +class MobileAdsApplication : Application() { + companion object { + const val TAG = "GoogleMobileAdsSample" + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeActivity.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeActivity.kt new file mode 100644 index 000000000..0f075abb4 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeActivity.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.jetpackcomposedemo.R +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAd +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdState +import com.google.android.gms.example.jetpackcomposedemo.composables.StatusText +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateError +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateLoaded +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateUnloaded +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.JetpackComposeDemoTheme + +class NativeActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + JetpackComposeDemoTheme { + // A surface container using the 'background' color from the theme + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + NativeLayoutScreen() + } + } + } + } + + @Preview + @Composable + fun NativeLayoutScreenPreview() { + JetpackComposeDemoTheme { + // A surface container using the 'background' color from the theme + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + NativeLayoutScreen() + } + } + } + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) + @Composable + fun NativeLayoutScreen() { + // Cache the mutable state for our notification bar. + val context = LocalContext.current + var messageText by remember { mutableStateOf("Native ad is not loaded.") } + var messageColor by remember { mutableStateOf(ColorStateUnloaded) } + + // Construct a banner state to configure the BannerComposable + val nativeState = + NativeAdState( + adUnitId = ADUNIT_ID, + adRequest = AdRequest.Builder().build(), + onAdLoaded = { + messageColor = ColorStateLoaded + messageText = "Native ad is loaded." + Log.i(TAG, messageText) + }, + onAdFailedToLoad = { error: LoadAdError -> + messageColor = ColorStateError + messageText = "Native ad failed to load with error: ${error.message}" + Log.e(TAG, messageText) + }, + onAdImpression = { Log.i(TAG, "Native ad had an impression.") }, + onAdClicked = { Log.i(TAG, "Native ad was clicked.") }, + onAdOpened = { Log.i(TAG, "Native ad was opened.") }, + onAdClosed = { Log.i(TAG, "Native ad was closed.") }, + ) + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + content = { + // Render title. + TopAppBar( + title = { Text(text = "Native") }, + navigationIcon = { + IconButton( + onClick = { + val intent = Intent(context, MainActivity::class.java) + context.startActivity(intent) + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + }, + ) + // Render status. + StatusText(messageColor, messageText, modifier = Modifier) + // Render NativeAd composable. + Box(modifier = Modifier.fillMaxWidth().background(Color.Gray).padding(8.dp)) { + NativeAd( + nativeAdState = nativeState, + R.layout.nativead, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + ) + } + + companion object { + const val TAG = "GoogleMobileAdsSample" + // Test AdUnitID for demonstrative purposes. + // https://developers.google.com/admob/android/test-ads + const val ADUNIT_ID = "ca-app-pub-3940256099942544/2247696110" + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeComposeActivity.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeComposeActivity.kt new file mode 100644 index 000000000..2f231cc20 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeComposeActivity.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.NativeAd +import com.google.android.gms.ads.nativead.NativeAdView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdBodyView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdCallToActionView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdChoicesView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdHeadlineView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdIconView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdMediaView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdPriceView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdStarRatingView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdState +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdStoreView +import com.google.android.gms.example.jetpackcomposedemo.composables.NativeAdView +import com.google.android.gms.example.jetpackcomposedemo.ui.StatusText +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateError +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateLoaded +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.ColorStateUnloaded +import com.google.android.gms.example.jetpackcomposedemo.ui.theme.JetpackComposeDemoTheme + +class NativeComposeActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + JetpackComposeDemoTheme { + // A surface container using the 'background' color from the theme + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + NativeLayoutScreen() + } + } + } + } + + @Preview + @Composable + fun NativeLayoutScreenPreview() { + JetpackComposeDemoTheme { + // A surface container using the 'background' color from the theme + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + NativeLayoutScreen() + } + } + } + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) + @Composable + fun NativeLayoutScreen() { + // Cache the mutable state. + val context = LocalContext.current + var messageText by remember { mutableStateOf("Native ad is not loaded.") } + var messageColor by remember { mutableStateOf(ColorStateUnloaded) } + var nativeAd by remember { mutableStateOf(null) } + + // Construct a native state to configure the NativeComposable. + val nativeAdState = remember { + NativeAdState( + adUnitId = ADUNIT_ID, + adRequest = AdRequest.Builder().build(), + onAdLoaded = { + messageColor = ColorStateLoaded + messageText = "Native ad is loaded." + Log.i(TAG, messageText) + }, + onAdFailedToLoad = { error: LoadAdError -> + messageColor = ColorStateError + messageText = "Native ad failed to load with error: ${error.message}" + Log.e(TAG, messageText) + }, + onAdImpression = { Log.i(TAG, "Native ad had an impression.") }, + onAdClicked = { Log.i(TAG, "Native ad was clicked.") }, + onAdOpened = { Log.i(TAG, "Native ad was opened.") }, + onAdClosed = { Log.i(TAG, "Native ad was closed.") }, + ) + } + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + // Render title. + TopAppBar( + title = { Text(text = "Native Compose") }, + navigationIcon = { + IconButton( + onClick = { + val intent = Intent(context, MainActivity::class.java) + context.startActivity(intent) + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + }, + ) + // Render ad status. + StatusText(messageColor, messageText, modifier = Modifier) + // Native ad composable. + SampleNativeTemplate(nativeAdState, nativeAd, nativeAdResult = { nativeAd = it }) + } + } + + @Composable + fun SampleNativeTemplate( + nativeAdState: NativeAdState, + nativeAd: NativeAd?, + nativeAdResult: (NativeAd) -> Unit, + ) { + Box(modifier = Modifier.padding(8.dp).wrapContentHeight(Alignment.Top)) { + NativeAdView(nativeAdState, nativeAdResult = { nativeAdResult(it) }) { + Column(Modifier.align(Alignment.TopStart).wrapContentHeight(Alignment.Top)) { + NativeAdChoicesView() + Row { + NativeAdIconView(Modifier.padding(5.dp)) { + nativeAd?.icon?.let { + nativeAd.icon?.drawable?.toBitmap()?.let { icon -> + Image(bitmap = icon.asImageBitmap(), "Icon") + } + } + } + Column { + NativeAdHeadlineView { + nativeAd?.headline?.let { + Text(text = it, style = MaterialTheme.typography.headlineLarge) + } + } + NativeAdStarRatingView { + nativeAd?.starRating?.let { + Text(text = "Rated $it", style = MaterialTheme.typography.labelMedium) + } + } + } + } + NativeAdBodyView() { nativeAd?.body?.let { Text(text = it) } } + NativeAdMediaView(Modifier.fillMaxWidth().height(500.dp).fillMaxHeight()) + Row(Modifier.align(Alignment.End).padding(5.dp)) { + NativeAdPriceView(Modifier.padding(5.dp).align(Alignment.CenterVertically)) { + nativeAd?.price?.let { Text(text = it) } + } + NativeAdStoreView(Modifier.padding(5.dp).align(Alignment.CenterVertically)) { + nativeAd?.store?.let { Text(text = it) } + } + NativeAdCallToActionView(Modifier.padding(5.dp)) { + nativeAd?.callToAction?.let { Button(onClick = {}) { Text(text = it) } } + } + } + } + } + } + } + + companion object { + const val TAG = "GoogleMobileAdsSample" + // Test AdUnitID for demonstrative purposes. + // https://developers.google.com/admob/android/test-ads + const val ADUNIT_ID = "ca-app-pub-3940256099942544/1044960115" + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAd.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAd.kt new file mode 100644 index 000000000..489d6b59b --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAd.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.composables + +import android.view.LayoutInflater +import android.view.View +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.viewinterop.AndroidView +import com.example.jetpackcomposedemo.databinding.NativeadBinding +import com.google.android.gms.ads.AdListener +import com.google.android.gms.ads.AdLoader +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.NativeAd +import com.google.android.gms.ads.nativead.NativeAdView + +/** + * A composable function to display a native ad with a native ad view layout defined in xml. + * + * @param nativeAdState The NativeAdState object containing ad configuration. + * @param layoutID The layout resource Id to use for the native ad view. + * @param modifier The modifier to apply to the banner ad. + */ +@Composable +fun NativeAd(nativeAdState: NativeAdState, layoutID: Int, modifier: Modifier) { + // Do not load the ad in preview mode + if (LocalInspectionMode.current) { + Box(modifier = Modifier.background(Color.Gray)) { + Text(text = "Native Ad Preview.", modifier.align(Alignment.Center)) + } + return + } + + var currentNativeAdView: NativeAdView? = null + var currentNativeAd: NativeAd? = null + + val adLoader = AdLoader.Builder(LocalContext.current, nativeAdState.adUnitId) + if (nativeAdState.nativeAdOptions != null) { + adLoader.withNativeAdOptions(nativeAdState.nativeAdOptions) + } + adLoader.withAdListener( + object : AdListener() { + override fun onAdFailedToLoad(error: LoadAdError) { + nativeAdState.onAdFailedToLoad?.invoke(error) + } + + override fun onAdLoaded() { + nativeAdState.onAdLoaded?.invoke() + } + + override fun onAdClicked() { + nativeAdState.onAdClicked?.invoke() + } + + override fun onAdClosed() { + nativeAdState.onAdClosed?.invoke() + } + + override fun onAdImpression() { + nativeAdState.onAdImpression?.invoke() + } + + override fun onAdOpened() { + nativeAdState.onAdOpened?.invoke() + } + + override fun onAdSwipeGestureClicked() { + nativeAdState.onAdSwipeGestureClicked?.invoke() + } + } + ) + adLoader.forNativeAd { nativeAd -> + + // Destroy old native ad assets to prevent memory leaks. + currentNativeAd?.destroy() + currentNativeAd = null + currentNativeAd = nativeAd + + if (currentNativeAdView == null) { + return@forNativeAd + } + + // Bind our native ad view with the native ad assets. + // This file is generated from /res/layouts/nativead + currentNativeAdView?.let { adView -> + val binding = NativeadBinding.bind(adView) + binding.adHeadline.text = nativeAd.headline + binding.adBody.text = nativeAd.body + binding.adCallToAction.text = nativeAd.callToAction + binding.adPrice.text = nativeAd.price + binding.adStore.text = nativeAd.store + binding.adStars.rating = nativeAd.starRating?.toFloat() ?: 0.toFloat() + binding.adAdvertiser.text = nativeAd.advertiser + binding.adAppIcon.setImageDrawable(nativeAd.icon?.drawable) + + // Hide unused native ad view elements. + binding.adBody.visibility = nativeAd.body?.let { View.VISIBLE } ?: View.GONE + binding.adCallToAction.visibility = nativeAd.callToAction?.let { View.VISIBLE } ?: View.GONE + binding.adPrice.visibility = nativeAd.price?.let { View.VISIBLE } ?: View.GONE + binding.adStore.visibility = nativeAd.store?.let { View.VISIBLE } ?: View.GONE + binding.adStars.visibility = nativeAd.starRating?.let { View.VISIBLE } ?: View.GONE + binding.adAdvertiser.visibility = nativeAd.advertiser?.let { View.VISIBLE } ?: View.GONE + binding.adAppIcon.visibility = nativeAd.icon?.let { View.VISIBLE } ?: View.GONE + binding.adMedia.visibility = nativeAd.mediaContent?.let { View.VISIBLE } ?: View.GONE + + // Set the mediaView just before calling setNativeAd. + adView.mediaView = binding.adMedia + + // This method tells the Google Mobile Ads SDK that you have finished populating your + // native ad view with this native ad. + adView.setNativeAd(nativeAd) + } + } + + AndroidView( + modifier = modifier, + factory = { context -> + LayoutInflater.from(context).inflate(layoutID, null, false) as NativeAdView + }, + ) { nativeAdView -> + currentNativeAdView = nativeAdView + return@AndroidView + } + + LaunchedEffect(Unit) { + // Load the native ad. + adLoader.build().loadAd(nativeAdState.adRequest) + } + + // Clean up the native ad view after use. + DisposableEffect(Unit) { + onDispose { + // Destroy old native ad assets to prevent memory leaks. + currentNativeAd?.destroy() + currentNativeAd = null + } + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdState.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdState.kt new file mode 100644 index 000000000..b31d49452 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdState.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.composables + +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.NativeAdOptions + +/** + * Represents the state of a banner advertisement, including its configuration. + * + * @param adUnitId The ID of the ad unit to load the banner into. + * @param adRequest The AdRequest object used to configure ad targeting and loading behavior. + * @param nativeAdOptions The native ad options used to configure ad behavior. + * @param onAdClicked Function invoked when the ad is clicked. + * @param onAdImpression Function invoked when an ad impression is recorded. + * @param onAdFailedToLoad Function invoked when the ad fails to load, includes the LoadAdError. + * @param onAdLoaded Function invoked when the ad is successfully loaded. + * @param onAdOpened Function invoked when the ad is opened (e.g., expands to a fullscreen). + * @param onAdClosed Function invoked when the ad is closed. + * @param onAdSwipeGestureClicked Function invoked when user performs a swipe gesture on the ad. + */ +data class NativeAdState( + val adUnitId: String, + val adRequest: AdRequest, + val nativeAdOptions: NativeAdOptions? = null, + val onAdClicked: (() -> Unit)? = null, + val onAdImpression: (() -> Unit)? = null, + val onAdFailedToLoad: ((LoadAdError) -> Unit)? = null, + val onAdLoaded: (() -> Unit)? = null, + val onAdOpened: (() -> Unit)? = null, + val onAdClosed: (() -> Unit)? = null, + val onAdSwipeGestureClicked: (() -> Unit)? = null, +) diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdView.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdView.kt new file mode 100644 index 000000000..2074933ed --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAdView.kt @@ -0,0 +1,359 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.composables + +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import com.google.android.gms.ads.AdListener +import com.google.android.gms.ads.AdLoader +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.AdChoicesView +import com.google.android.gms.ads.nativead.MediaView +import com.google.android.gms.ads.nativead.NativeAd +import com.google.android.gms.ads.nativead.NativeAdView + +/** + * A CompositionLocal that can provide a `NativeAdView` to ad attributes such as `NativeHeadline`. + */ +internal val LocalNativeAdView = staticCompositionLocalOf { null } + +/** + * This is the Compose wrapper for a NativeAdView. + * + * @param nativeAdState The NativeAdState object containing ad configuration. + * @param modifier The modifier to apply to the banner ad. + * @param modifier modify the native ad view container. + * @param nativeAdResult Unit which returns the loaded native ad. + * @param content A composable function that defines the rest of the native ad view's elements. + */ +@Composable +fun NativeAdView( + nativeAdState: NativeAdState, + modifier: Modifier = Modifier, + nativeAdResult: (NativeAd) -> Unit, + content: @Composable () -> Unit, +) { + val localContext = LocalContext.current + val nativeAdView = remember { NativeAdView(localContext).apply { id = View.generateViewId() } } + var lastNativeAd by remember { mutableStateOf(null) } + + AndroidView( + factory = { + val adLoader = AdLoader.Builder(localContext, nativeAdState.adUnitId) + if (nativeAdState.nativeAdOptions != null) { + adLoader.withNativeAdOptions(nativeAdState.nativeAdOptions) + } + adLoader.withAdListener( + object : AdListener() { + override fun onAdFailedToLoad(error: LoadAdError) { + nativeAdState.onAdFailedToLoad?.invoke(error) + } + + override fun onAdLoaded() { + nativeAdState.onAdLoaded?.invoke() + } + + override fun onAdClicked() { + nativeAdState.onAdClicked?.invoke() + } + + override fun onAdClosed() { + nativeAdState.onAdClosed?.invoke() + } + + override fun onAdImpression() { + nativeAdState.onAdImpression?.invoke() + } + + override fun onAdOpened() { + nativeAdState.onAdOpened?.invoke() + } + + override fun onAdSwipeGestureClicked() { + nativeAdState.onAdSwipeGestureClicked?.invoke() + } + } + ) + + adLoader.forNativeAd { nativeAd -> + // Destroy old native ad assets to prevent memory leaks. + lastNativeAd?.destroy() + lastNativeAd = nativeAd + + // Set the native ad on the native ad view. + nativeAdView.setNativeAd(nativeAd) + nativeAdResult(nativeAd) + } + adLoader.build().loadAd(nativeAdState.adRequest) + + nativeAdView.apply { + layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + addView( + ComposeView(context).apply { + layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setContent { + // Set `nativeAdView` as the current LocalNativeAdView so that + // `content` can access the `NativeAdView` via `LocalNativeAdView.current`. + // This would allow ad attributes (such as `NativeHeadline`) to attribute + // its contained View subclass via setter functions (e.g. nativeAdView.headlineView = + // view) + CompositionLocalProvider(LocalNativeAdView provides nativeAdView) { content.invoke() } + } + } + ) + } + }, + modifier = modifier, + ) + + DisposableEffect(Unit) { + onDispose { + // Destroy old native ad assets to prevent memory leaks. + lastNativeAd?.destroy() + lastNativeAd = null + } + } +} + +/** + * The ComposeWrapper container for a advertiserView inside a NativeAdView. This composable must be + * invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdAdvertiserView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + AndroidView( + factory = { context -> + ComposeView(context).apply { + id = View.generateViewId() + setContent(content) + nativeAdView.advertiserView = this + } + }, + modifier = modifier, + update = { view -> view.setContent(content) }, + ) +} + +/** + * The ComposeWrapper container for a bodyView inside a NativeAdView. This composable must be + * invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdBodyView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + val localComposeView = remember { ComposeView(localContext).apply { id = View.generateViewId() } } + AndroidView( + factory = { + nativeAdView.bodyView = localComposeView + localComposeView.apply { setContent(content) } + }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper container for a callToActionView inside a NativeAdView. This composable must + * be invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdCallToActionView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + val localComposeView = remember { ComposeView(localContext).apply { id = View.generateViewId() } } + AndroidView( + factory = { + nativeAdView.callToActionView = localComposeView + localComposeView.apply { setContent(content) } + }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper for a adChoicesView inside a NativeAdView. This composable must be invoked + * from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + */ +@Composable +fun NativeAdChoicesView(modifier: Modifier = Modifier) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + AndroidView( + factory = { + AdChoicesView(localContext).apply { + minimumWidth = 15 + minimumHeight = 15 + } + }, + update = { view -> nativeAdView.adChoicesView = view }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper container for a headlineView inside a NativeAdView. This composable must be + * invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdHeadlineView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + val localComposeView = remember { ComposeView(localContext).apply { id = View.generateViewId() } } + AndroidView( + factory = { + nativeAdView.headlineView = localComposeView + localComposeView.apply { setContent(content) } + }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper container for a iconView inside a NativeAdView. This composable must be + * invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdIconView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + val localComposeView = remember { ComposeView(localContext).apply { id = View.generateViewId() } } + AndroidView( + factory = { + nativeAdView.iconView = localComposeView + localComposeView.apply { setContent(content) } + }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper for a mediaView inside a NativeAdView. This composable must be invoked from + * within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + */ +@Composable +fun NativeAdMediaView(modifier: Modifier = Modifier) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + AndroidView( + factory = { MediaView(localContext) }, + update = { view -> nativeAdView.mediaView = view }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper container for a priceView inside a NativeAdView. This composable must be + * invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdPriceView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + val localComposeView = remember { ComposeView(localContext).apply { id = View.generateViewId() } } + AndroidView( + factory = { + nativeAdView.priceView = localComposeView + localComposeView.apply { setContent(content) } + }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper container for a starRatingView inside a NativeAdView. This composable must be + * invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdStarRatingView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + val localComposeView = remember { ComposeView(localContext).apply { id = View.generateViewId() } } + AndroidView( + factory = { + nativeAdView.starRatingView = localComposeView + localComposeView.apply { setContent(content) } + }, + modifier = modifier, + ) +} + +/** + * The ComposeWrapper container for a storeView inside a NativeAdView. This composable must be + * invoked from within a `NativeAdView`. + * + * @param modifier modify the native ad view element. + * @param content A composable function that defines the content of this native asset. + */ +@Composable +fun NativeAdStoreView(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val nativeAdView = LocalNativeAdView.current ?: throw IllegalStateException("NativeAdView null") + val localContext = LocalContext.current + val localComposeView = remember { ComposeView(localContext).apply { id = View.generateViewId() } } + AndroidView( + factory = { + nativeAdView.storeView = localComposeView + localComposeView.apply { setContent(content) } + }, + modifier = modifier, + ) +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/StatusText.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/StatusText.kt new file mode 100644 index 000000000..b6cb9b671 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/StatusText.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** + * A composable function to create a status text box. + * + * @param messageColor The background color of the box. + * @param messageText The text to be displayed in the box. + * @param modifier The Modifier to be applied to this button. + */ +@Composable +fun StatusText(messageColor: Color, messageText: String, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth().background(messageColor)) { + Text(text = messageText, style = MaterialTheme.typography.bodyLarge) + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/TextButton.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/TextButton.kt new file mode 100644 index 000000000..2f71f2aef --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/TextButton.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.composables + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * A composable function to create a standard button with text. + * + * @param name The text to be displayed on the button. + * @param enabled Controls whether the button is enabled or disabled (defaults to true). + * @param modifier The Modifier to be applied to this button. + * @param onClick The lambda function to be executed when the button is clicked. + */ +@Composable +fun TextButton( + name: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit, +) { + Button(onClick = { onClick() }, enabled = enabled, modifier = modifier.fillMaxWidth()) { + Text(name) + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/StatusText.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/StatusText.kt new file mode 100644 index 000000000..c671e655c --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/StatusText.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun StatusText(messageColor: Color, messageText: String, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth().background(messageColor)) { + Text(text = messageText, style = MaterialTheme.typography.bodyLarge) + } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Color.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Color.kt new file mode 100644 index 000000000..cd67390b2 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Color.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +val ColorStateLoaded = Color(0xFF009900) +val ColorStateUnloaded = Color(0xFFcc6600) +val ColorStateError = Color(0xFFcc0000) diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Theme.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Theme.kt new file mode 100644 index 000000000..0b35ba825 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/theme/Theme.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 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.gms.example.jetpackcomposedemo.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = + darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80) + +private val LightColorScheme = + lightColorScheme(primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40) + +@Composable +fun JetpackComposeDemoTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+. + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme(colorScheme = colorScheme, content = content) +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_background.xml b/kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..61bb79edb --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_foreground.xml b/kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..04d1a347b --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/res/layout/nativead.xml b/kotlin/advanced/JetpackComposeDemo/app/src/main/res/layout/nativead.xml new file mode 100644 index 000000000..c090d3886 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/res/layout/nativead.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +