diff --git a/kotlin/advanced/JetpackComposeDemo/app/build.gradle.kts b/kotlin/advanced/JetpackComposeDemo/app/build.gradle.kts new file mode 100644 index 000000000..7c699d4b4 --- /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") + 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..fd78fadba --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/BannerActivity.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/BannerActivity.kt new file mode 100644 index 000000000..01ac783b0 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/BannerActivity.kt @@ -0,0 +1,144 @@ +/* + * 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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +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.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.AdSize +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.example.jetpackcomposedemo.composables.BannerAd +import com.google.android.gms.example.jetpackcomposedemo.composables.BannerAdState +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 BannerActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + JetpackComposeDemoTheme { + Surface(modifier = Modifier.fillMaxHeight(), color = MaterialTheme.colorScheme.background) { + BannerScreen() + } + } + } + } + + @Preview + @Composable + fun BannerScreenPreview() { + JetpackComposeDemoTheme { + Surface(modifier = Modifier.fillMaxHeight(), color = MaterialTheme.colorScheme.background) { + BannerScreen() + } + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun BannerScreen() { + // Cache the mutable state for our notification bar. + val context = LocalContext.current + var messageText by remember { mutableStateOf("Banner ad is not loaded.") } + var messageColor by remember { mutableStateOf(ColorStateUnloaded) } + + // Construct a banner ad state which configures our BannerAd composable. + val bannerState = + BannerAdState( + adUnitId = ADUNIT_ID, + adSize = AdSize.BANNER, + adRequest = AdRequest.Builder().build(), + onAdLoaded = { + messageColor = ColorStateLoaded + messageText = "Banner ad is loaded." + Log.i(TAG, messageText) + }, + onAdFailedToLoad = { error: LoadAdError -> + messageColor = ColorStateError + messageText = "Banner ad failed to load with error: ${error.message}" + Log.e(TAG, messageText) + }, + onAdImpression = { Log.i(TAG, "Banner ad impression.") }, + onAdClicked = { Log.i(TAG, "Banner ad clicked.") }, + onAdOpened = { Log.i(TAG, "Banner ad opened.") }, + onAdClosed = { Log.i(TAG, "Banner ad closed.") }, + ) + + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + content = { + // Render title. + TopAppBar( + title = { Text(text = "Banner ad") }, + navigationIcon = { + IconButton( + onClick = { + val intent = Intent(context, MainActivity::class.java) + context.startActivity(intent) + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + }, + ) + // Render status. + Box(modifier = Modifier.fillMaxSize().background(messageColor)) { + Text(text = messageText, style = MaterialTheme.typography.bodyLarge) + } + // Render the BannerAd composable. + BannerAd(bannerState, modifier = Modifier) + }, + ) + } + + 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/6300978111" + } +} 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..1e21cfba0 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MainActivity.kt @@ -0,0 +1,195 @@ +/* + * 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.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +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.ui.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. + Box( + modifier = + Modifier.fillMaxSize().background(adsManager.mobileAdsState.intValue.messageColor()) + ) { + Text( + text = adsManager.mobileAdsState.intValue.messageText(), + style = MaterialTheme.typography.bodyLarge, + ) + } + // 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), + ) + } + } + } + // Banner Sample. + TextButton(name = "Banner", enabled = adsManager.canRequestAds.value) { + val intent = Intent(context, BannerActivity::class.java) + context.startActivity(intent) + } + // Native Sample. + TextButton(name = "Native", enabled = adsManager.canRequestAds.value) { + val intent = Intent(context, NativeActivity::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..c30f4059f --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/NativeActivity.kt @@ -0,0 +1,153 @@ +/* + * 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.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 impression") }, + onAdClicked = { Log.i(TAG, "Native ad clicked") }, + onAdOpened = { Log.i(TAG, "Native ad opened") }, + onAdClosed = { Log.i(TAG, "Native ad 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. + Box(modifier = Modifier.fillMaxSize().background(messageColor)) { + Text(text = messageText, style = MaterialTheme.typography.bodyLarge) + } + // 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/composables/BannerAd.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/BannerAd.kt new file mode 100644 index 000000000..46855f176 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/BannerAd.kt @@ -0,0 +1,111 @@ +package com.google.android.gms.example.jetpackcomposedemo.composables + +/* + * 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. + */ +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.google.android.gms.ads.AdListener +import com.google.android.gms.ads.AdView +import com.google.android.gms.ads.LoadAdError + +/** + * A composable function to display a banner advertisement. + * + * @param bannerAdState The BannerState object containing ad configuration. + * @param modifier The modifier to apply to the banner ad. + */ +@Composable +fun BannerAd(bannerAdState: BannerAdState, modifier: Modifier) { + // Remember the adView so we can dispose of it later. + var adView by remember { mutableStateOf(null) } + + if (LocalInspectionMode.current) { + Box( + modifier = + Modifier.background(Color.Gray) + .width(bannerAdState.adSize.width.dp) + .height(bannerAdState.adSize.height.dp) + ) { + Text(text = "Google Mobile Ads preview banner.", modifier.align(Alignment.Center)) + } + return + } + + AndroidView( + modifier = modifier.fillMaxWidth(), + factory = { context -> + AdView(context).apply { + // Make sure we only run this code block once and in non-preview mode. + if (adView != null) { + return@apply + } + + adView = this + this.adUnitId = bannerAdState.adUnitId + this.setAdSize(bannerAdState.adSize) + this.adListener = + object : AdListener() { + override fun onAdLoaded() { + bannerAdState.onAdLoaded?.invoke() + } + + override fun onAdFailedToLoad(error: LoadAdError) { + bannerAdState.onAdFailedToLoad?.invoke(error) + } + + override fun onAdImpression() { + bannerAdState.onAdImpression?.invoke() + } + + override fun onAdClosed() { + bannerAdState.onAdClosed?.invoke() + } + + override fun onAdClicked() { + bannerAdState.onAdClicked?.invoke() + } + + override fun onAdOpened() { + bannerAdState.onAdClicked?.invoke() + } + + override fun onAdSwipeGestureClicked() { + bannerAdState.onAdSwipeGestureClicked?.invoke() + } + } + this.loadAd(bannerAdState.adRequest) + } + }, + ) + // Clean up the AdView after use. + DisposableEffect(Unit) { onDispose { adView?.destroy() } } +} diff --git a/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/BannerAdState.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/BannerAdState.kt new file mode 100644 index 000000000..4bfe05d66 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/BannerAdState.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.AdSize +import com.google.android.gms.ads.LoadAdError + +/** + * Represents the configuration of a banner advertisement. + * + * @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 adSize The desired size of the banner ad (default is AdSize.BANNER). + * @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 BannerAdState( + val adUnitId: String, + val adRequest: AdRequest, + val adSize: AdSize = AdSize.BANNER, + 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/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..458de7c54 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/composables/NativeAd.kt @@ -0,0 +1,164 @@ +/* + * 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) + + // TODO: Remove after androidx.compose.ui:ui:1.7.0-beta04 + adView.viewTreeObserver?.dispatchOnGlobalLayout() + } + } + + 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/ui/TextButton.kt b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/TextButton.kt new file mode 100644 index 000000000..821359c38 --- /dev/null +++ b/kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/ui/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.ui + +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/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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +