Skip to content

Commit f518858

Browse files
Nicholas Ventimigliacopybara-github
authored andcommitted
Added Jetpack Compose Sample Consent Manager.
PiperOrigin-RevId: 668061205
1 parent d63b73d commit f518858

9 files changed

Lines changed: 349 additions & 119 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.gms.example.jetpackcomposedemo
18+
19+
import android.app.Activity
20+
import android.content.Context
21+
import com.google.android.gms.example.jetpackcomposedemo.MainViewModel.Companion.TEST_DEVICE_HASHED_ID
22+
import com.google.android.ump.ConsentDebugSettings
23+
import com.google.android.ump.ConsentForm.OnConsentFormDismissedListener
24+
import com.google.android.ump.ConsentInformation
25+
import com.google.android.ump.ConsentRequestParameters
26+
import com.google.android.ump.FormError
27+
import com.google.android.ump.UserMessagingPlatform
28+
29+
/**
30+
* The Google Mobile Ads SDK provides the User Messaging Platform (Google's IAB Certified consent
31+
* management platform) as one solution to capture consent for users in GDPR impacted countries.
32+
* This is an example and you can choose another consent management platform to capture consent.
33+
*/
34+
class GoogleMobileAdsConsentManager private constructor(context: Context) {
35+
private val consentInformation = UserMessagingPlatform.getConsentInformation(context)
36+
37+
/** Interface definition for a callback to be invoked when consent gathering is complete. */
38+
fun interface OnConsentGatheringCompleteListener {
39+
fun consentGatheringComplete(error: FormError?)
40+
}
41+
42+
/** Determine if the app can request ads. */
43+
val canRequestAds: Boolean
44+
get() = consentInformation.canRequestAds()
45+
46+
/** Determine if the privacy options form is required. */
47+
val isPrivacyOptionsRequired: Boolean
48+
get() =
49+
consentInformation.privacyOptionsRequirementStatus ==
50+
ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
51+
52+
/**
53+
* Calls the UMP SDK methods to request consent information and load/show a consent form if
54+
* necessary.
55+
*/
56+
fun gatherConsent(
57+
activity: Activity,
58+
onConsentGatheringCompleteListener: OnConsentGatheringCompleteListener,
59+
) {
60+
// For testing purposes, you can force a DebugGeography of EEA or NOT_EEA.
61+
val debugSettings =
62+
ConsentDebugSettings.Builder(activity)
63+
// .setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA)
64+
.addTestDeviceHashedId(TEST_DEVICE_HASHED_ID)
65+
.build()
66+
67+
val params = ConsentRequestParameters.Builder().setConsentDebugSettings(debugSettings).build()
68+
69+
// Requesting an update to consent information should be called on every app launch.
70+
consentInformation.requestConsentInfoUpdate(
71+
activity,
72+
params,
73+
{
74+
UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity) { formError ->
75+
// Consent has been gathered.
76+
onConsentGatheringCompleteListener.consentGatheringComplete(formError)
77+
}
78+
},
79+
{ requestConsentError ->
80+
onConsentGatheringCompleteListener.consentGatheringComplete(requestConsentError)
81+
},
82+
)
83+
}
84+
85+
/** Calls the UMP SDK method to show the privacy options form. */
86+
fun showPrivacyOptionsForm(
87+
activity: Activity,
88+
onConsentFormDismissedListener: OnConsentFormDismissedListener,
89+
) {
90+
UserMessagingPlatform.showPrivacyOptionsForm(activity, onConsentFormDismissedListener)
91+
}
92+
93+
companion object {
94+
@Volatile private var instance: GoogleMobileAdsConsentManager? = null
95+
96+
fun getInstance(context: Context) =
97+
instance
98+
?: synchronized(this) {
99+
instance ?: GoogleMobileAdsConsentManager(context).also { instance = it }
100+
}
101+
}
102+
}

kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MainActivity.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,35 @@
1717
package com.google.android.gms.example.jetpackcomposedemo
1818

1919
import android.os.Bundle
20+
import android.util.Log
2021
import androidx.activity.ComponentActivity
2122
import androidx.activity.compose.setContent
2223
import androidx.activity.enableEdgeToEdge
24+
import androidx.lifecycle.lifecycleScope
25+
import com.example.jetpackcomposedemo.R
26+
import com.google.android.gms.ads.MobileAds
27+
import kotlinx.coroutines.launch
2328

2429
class MainActivity : ComponentActivity() {
2530

31+
private lateinit var mainViewModel: MainViewModel
32+
2633
override fun onCreate(savedInstanceState: Bundle?) {
2734
// Display content edge-to-edge.
2835
enableEdgeToEdge()
2936
super.onCreate(savedInstanceState)
3037

31-
setContent { MainScreen() }
38+
// Log the Mobile Ads SDK version.
39+
Log.d(
40+
GoogleMobileAdsApplication.TAG,
41+
getString(R.string.version_format, MobileAds.getVersion()),
42+
)
43+
44+
// Initialize the view model. This will gather consent and initialize Google Mobile Ads.
45+
mainViewModel = MainViewModel.getInstance()
46+
if (!mainViewModel.isInitCalled) {
47+
lifecycleScope.launch { mainViewModel.init(this@MainActivity) }
48+
}
49+
setContent { MainScreen(mainViewModel) }
3250
}
3351
}

kotlin/advanced/JetpackComposeDemo/app/src/main/java/com/google/android/gms/example/jetpackcomposedemo/MainScreen.kt

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.google.android.gms.example.jetpackcomposedemo
22

3+
import android.content.Context
4+
import android.content.ContextWrapper
5+
import androidx.activity.ComponentActivity
36
import androidx.compose.foundation.layout.Column
47
import androidx.compose.foundation.layout.Spacer
58
import androidx.compose.foundation.layout.WindowInsets
@@ -13,6 +16,7 @@ import androidx.compose.material.icons.Icons
1316
import androidx.compose.material.icons.automirrored.filled.ArrowBack
1417
import androidx.compose.material.icons.filled.MoreVert
1518
import androidx.compose.material3.DropdownMenu
19+
import androidx.compose.material3.DropdownMenuItem
1620
import androidx.compose.material3.ExperimentalMaterial3Api
1721
import androidx.compose.material3.Icon
1822
import androidx.compose.material3.IconButton
@@ -22,25 +26,51 @@ import androidx.compose.material3.Surface
2226
import androidx.compose.material3.Text
2327
import androidx.compose.material3.TopAppBar
2428
import androidx.compose.runtime.Composable
29+
import androidx.compose.runtime.LaunchedEffect
30+
import androidx.compose.runtime.collectAsState
2531
import androidx.compose.runtime.getValue
2632
import androidx.compose.runtime.mutableStateOf
2733
import androidx.compose.runtime.remember
2834
import androidx.compose.runtime.setValue
2935
import androidx.compose.ui.Modifier
3036
import androidx.compose.ui.platform.LocalContext
3137
import androidx.compose.ui.tooling.preview.Preview
32-
import androidx.navigation.NavHostController
3338
import androidx.navigation.compose.NavHost
3439
import androidx.navigation.compose.composable
3540
import androidx.navigation.compose.rememberNavController
3641
import com.example.jetpackcomposedemo.R
3742
import com.google.android.gms.example.jetpackcomposedemo.ui.theme.JetpackComposeDemoTheme
3843

3944
@Composable
40-
fun MainScreen() {
45+
fun MainScreen(googleMobileAdsViewModel: MainViewModel, modifier: Modifier = Modifier) {
46+
val context = LocalContext.current
47+
val activity = context.getActivity()
4148
val navController = rememberNavController()
49+
val uiState by googleMobileAdsViewModel.uiState.collectAsState()
50+
var showNavigationIcon by remember { mutableStateOf(false) }
51+
52+
LaunchedEffect(navController) {
53+
navController.addOnDestinationChangedListener { _, destination, _ ->
54+
showNavigationIcon = destination.route != NavDestinations.Home.name
55+
}
56+
}
57+
4258
Scaffold(
43-
topBar = { MainTopBar(navController = navController) },
59+
topBar = {
60+
MainTopBar(
61+
isMobileAdsInitialized = uiState.isMobileAdsInitialized,
62+
isPrivacyOptionsRequired = uiState.isPrivacyOptionsRequired,
63+
isNavigationEnabled = showNavigationIcon,
64+
navigateBack = { navController.popBackStack() },
65+
onOpenAdInspector = { googleMobileAdsViewModel.openAdInspector(context) {} },
66+
onShowPrivacyOptionsForm = {
67+
if (activity != null) {
68+
googleMobileAdsViewModel.showPrivacyOptionsForm(activity) {}
69+
}
70+
},
71+
modifier,
72+
)
73+
},
4474
contentWindowInsets =
4575
WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
4676
) { innerPadding ->
@@ -55,14 +85,24 @@ fun MainScreen() {
5585

5686
@OptIn(ExperimentalMaterial3Api::class)
5787
@Composable
58-
private fun MainTopBar(navController: NavHostController) {
88+
private fun MainTopBar(
89+
isMobileAdsInitialized: Boolean,
90+
isPrivacyOptionsRequired: Boolean,
91+
isNavigationEnabled: Boolean,
92+
navigateBack: () -> Unit,
93+
onOpenAdInspector: () -> Unit,
94+
onShowPrivacyOptionsForm: () -> Unit,
95+
modifier: Modifier = Modifier,
96+
) {
5997
val context = LocalContext.current
6098
var menuExpanded by remember { mutableStateOf(false) }
99+
61100
TopAppBar(
101+
modifier = modifier,
62102
title = { Text(context.getString(R.string.main_title)) },
63103
navigationIcon = {
64-
if (navController.currentBackStackEntry != null) {
65-
IconButton(onClick = { navController.popBackStack() }) {
104+
if (isNavigationEnabled) {
105+
IconButton(onClick = navigateBack) {
66106
Icon(
67107
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
68108
contentDescription = Icons.AutoMirrored.Filled.ArrowBack.name,
@@ -74,17 +114,36 @@ private fun MainTopBar(navController: NavHostController) {
74114
IconButton(onClick = { menuExpanded = true }) {
75115
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = Icons.Filled.MoreVert.name)
76116
}
77-
DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {}
117+
DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {
118+
DropdownMenuItem(
119+
text = { Text(context.getString(R.string.adinspector_open_button)) },
120+
enabled = isMobileAdsInitialized,
121+
onClick = onOpenAdInspector,
122+
)
123+
if (isPrivacyOptionsRequired) {
124+
DropdownMenuItem(
125+
text = { Text(context.getString(R.string.privacy_options_open_button)) },
126+
onClick = onShowPrivacyOptionsForm,
127+
)
128+
}
129+
}
78130
},
79131
)
80132
}
81133

134+
private fun Context.getActivity(): ComponentActivity? =
135+
when (this) {
136+
is ComponentActivity -> this
137+
is ContextWrapper -> baseContext.getActivity()
138+
else -> null
139+
}
140+
82141
@Preview
83142
@Composable
84143
private fun MainScreenPreview() {
85144
JetpackComposeDemoTheme {
86145
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
87-
MainScreen()
146+
MainScreen(MainViewModel.getInstance())
88147
}
89148
}
90149
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.google.android.gms.example.jetpackcomposedemo
2+
3+
/** UiState for the MainViewModel. */
4+
data class MainUiState(
5+
/** Represents current initialization states for the Google Mobile Ads SDK. */
6+
val isMobileAdsInitialized: Boolean = false,
7+
/** Indicates whether the app has completed the steps for gathering updated user consent. */
8+
val canRequestAds: Boolean = false,
9+
/** Indicates whether a privacy options form is required. */
10+
val isPrivacyOptionsRequired: Boolean = false,
11+
)

0 commit comments

Comments
 (0)