diff --git a/app/build.gradle b/app/build.gradle index 8e9d382e9..f512bb2fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -179,6 +179,7 @@ dependencies { // implementation 'com.google.guava:listenablefuture:1.0' implementation 'androidx.paging:paging-runtime:3.2.0' implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation 'com.google.android.ump:user-messaging-platform:2.1.0' implementation 'androidx.activity:activity-ktx:1.7.2' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' diff --git a/app/src/main/java/com/pr0gramm/app/ApplicationClass.kt b/app/src/main/java/com/pr0gramm/app/ApplicationClass.kt index 453442eac..727454738 100644 --- a/app/src/main/java/com/pr0gramm/app/ApplicationClass.kt +++ b/app/src/main/java/com/pr0gramm/app/ApplicationClass.kt @@ -16,7 +16,6 @@ import com.pr0gramm.app.services.Track import com.pr0gramm.app.sync.SyncStatsWorker import com.pr0gramm.app.sync.SyncWorker import com.pr0gramm.app.ui.ActivityErrorHandler -import com.pr0gramm.app.ui.AdService import com.pr0gramm.app.ui.dialogs.ErrorDialogFragment.Companion.GlobalErrorDialogHandler import com.pr0gramm.app.util.AndroidUtility import com.pr0gramm.app.util.AndroidUtility.buildVersionCode @@ -25,7 +24,7 @@ import com.pr0gramm.app.util.debugOnly import com.pr0gramm.app.util.di.InjectorAware import com.pr0gramm.app.util.doInBackground import kotlinx.coroutines.runBlocking -import java.util.* +import java.util.WeakHashMap import java.util.concurrent.TimeUnit import java.util.logging.Level import java.util.logging.LogManager @@ -106,10 +105,6 @@ open class ApplicationClass : Application(), InjectorAware { log?.handlers?.forEach { it.level = Level.INFO } } - logger.time("Initializing MobileAds") { - AdService.initializeMobileAds(this) - } - // wait for firebase setup to finish runBlocking { firebaseJob.join() diff --git a/app/src/main/java/com/pr0gramm/app/services/Track.kt b/app/src/main/java/com/pr0gramm/app/services/Track.kt index ef8eca04b..c2df6d852 100644 --- a/app/src/main/java/com/pr0gramm/app/services/Track.kt +++ b/app/src/main/java/com/pr0gramm/app/services/Track.kt @@ -44,7 +44,7 @@ object Track : InjectorAware { fun loginSuccessful() { send("login") { - putBoolean("success", true) + putBoolean(FirebaseAnalytics.Param.SUCCESS, true) } Stats().increment("login.succeeded") @@ -52,8 +52,8 @@ object Track : InjectorAware { fun loginFailed(type: String) { send("login") { - putBoolean("success", false) - putString("type", type) + putBoolean(FirebaseAnalytics.Param.SUCCESS, false) + putString(FirebaseAnalytics.Param.VALUE, type) } Stats().increment("login.failed", "reason:$type") @@ -65,7 +65,7 @@ object Track : InjectorAware { fun writeComment(root: Boolean) { send("write_comment") { - putBoolean("root", root) + putBoolean(FirebaseAnalytics.Param.VALUE, root) } } @@ -79,22 +79,22 @@ object Track : InjectorAware { fun votePost(vote: Vote) { send("vote") { - putString("vote_type", vote.name) - putString("content_type", "post") + putString(FirebaseAnalytics.Param.VALUE, vote.name) + putString(FirebaseAnalytics.Param.CONTENT_TYPE, "post") } } fun voteTag(vote: Vote) { send("vote") { - putString("vote_type", vote.name) - putString("content_type", "tag") + putString(FirebaseAnalytics.Param.VALUE, vote.name) + putString(FirebaseAnalytics.Param.CONTENT_TYPE, "tag") } } fun voteComment(vote: Vote) { send("vote") { - putString("vote_type", vote.name) - putString("content_type", "comment") + putString(FirebaseAnalytics.Param.VALUE, vote.name) + putString(FirebaseAnalytics.Param.CONTENT_TYPE, "comment") } } @@ -104,7 +104,7 @@ object Track : InjectorAware { fun openBrowser(type: String) { send("open_browser") { - putString("type", type) + putString(FirebaseAnalytics.Param.VALUE, type) } } @@ -115,8 +115,8 @@ object Track : InjectorAware { val sizeCategory = "%d-%d kb".format(categoryStart, categoryStart + 512) send("upload") { - putLong("size", size) - putString("size_category", sizeCategory) + putLong(FirebaseAnalytics.Param.VALUE, size) + putString(FirebaseAnalytics.Param.ITEM_CATEGORY, sizeCategory) } } @@ -126,13 +126,13 @@ object Track : InjectorAware { fun inboxNotificationClosed(method: String) { send("inbox_notification_close") { - putString("method", method) + putString(FirebaseAnalytics.Param.METHOD, method) } } fun preloadCurrentFeed(size: Int) { send("preload_feed") { - putInt("item_count", size) + putInt(FirebaseAnalytics.Param.VALUE, size) } } @@ -162,7 +162,7 @@ object Track : InjectorAware { fun specialMenuActionClicked(uri: Uri) { send("special_menu_item") { - putString("uri", uri.toString()) + putString(FirebaseAnalytics.Param.VALUE, uri.toString()) } } @@ -178,15 +178,15 @@ object Track : InjectorAware { fun openFeed(filter: FeedFilter) { send("view_feed") { - filter.tags?.let { putBoolean("tags", true) } - filter.collection?.let { putBoolean("collection", true) } - filter.username?.let { putBoolean("username", true) } + filter.tags?.let { putBoolean(FirebaseAnalytics.Param.TERM, true) } + filter.collection?.let { putBoolean(FirebaseAnalytics.Param.GROUP_ID, true) } + filter.username?.let { putBoolean(FirebaseAnalytics.Param.AFFILIATION, true) } } } fun viewItem(itemId: Long) { send("view_item") { - putLong("id", itemId) + putLong(FirebaseAnalytics.Param.ITEM_ID, itemId) } } @@ -202,7 +202,7 @@ object Track : InjectorAware { fun openZoomView(itemId: Long) { send("zoom_view") { - putLong("id", itemId) + putLong(FirebaseAnalytics.Param.ITEM_ID, itemId) } } @@ -215,7 +215,7 @@ object Track : InjectorAware { installerTracked = true send("installer") { - putString("package", name.toString()) + putString(FirebaseAnalytics.Param.VALUE, name.toString()) } } } diff --git a/app/src/main/java/com/pr0gramm/app/services/config/ConfigService.kt b/app/src/main/java/com/pr0gramm/app/services/config/ConfigService.kt index c6d7aee96..69f96deb4 100644 --- a/app/src/main/java/com/pr0gramm/app/services/config/ConfigService.kt +++ b/app/src/main/java/com/pr0gramm/app/services/config/ConfigService.kt @@ -6,12 +6,19 @@ import android.content.Context import android.content.SharedPreferences import android.provider.Settings import androidx.core.content.edit -import com.pr0gramm.app.* +import com.pr0gramm.app.BuildConfig import com.pr0gramm.app.Duration.Companion.minutes +import com.pr0gramm.app.Instant +import com.pr0gramm.app.Logger +import com.pr0gramm.app.MoshiInstance import com.pr0gramm.app.api.pr0gramm.Api import com.pr0gramm.app.model.config.Config -import com.pr0gramm.app.util.* +import com.pr0gramm.app.util.AndroidUtility +import com.pr0gramm.app.util.debugOnly import com.pr0gramm.app.util.di.injector +import com.pr0gramm.app.util.doInBackground +import com.pr0gramm.app.util.getStringOrNull +import com.pr0gramm.app.util.runEvery import com.squareup.moshi.JsonDataException import com.squareup.moshi.adapter import kotlinx.coroutines.flow.Flow @@ -104,14 +111,17 @@ class ConfigService(context: Application, debugOnly { // update config for development. return configState.copy( - adTypesLoggedIn = listOf(Config.AdType.FEED), - adTypesLoggedOut = listOf(Config.AdType.FEED, Config.AdType.FEED_TO_POST_INTERSTITIAL), - interstitialAdIntervalInSeconds = 10, - specialMenuItems = configState.specialMenuItems.takeIf { it.isNotEmpty() } - ?: listOf(Config.MenuItem( - name = "Wichteln", - icon = "https://materialdesignicons.com/api/download/22D0C782-CD05-4FEB-845F-BBA7126C7326/000000/1/FFFFFF/0/48", - link = "https://pr0gramm.com/new/wichteln"))) + adTypesLoggedIn = listOf(Config.AdType.FEED), + adTypesLoggedOut = listOf(Config.AdType.FEED, Config.AdType.FEED_TO_POST_INTERSTITIAL), + interstitialAdIntervalInSeconds = 600, + specialMenuItems = configState.specialMenuItems.takeIf { it.isNotEmpty() } + ?: listOf( + Config.MenuItem( + name = "Wichteln", + icon = "https://materialdesignicons.com/api/download/22D0C782-CD05-4FEB-845F-BBA7126C7326/000000/1/FFFFFF/0/48", + link = "https://pr0gramm.com/new/wichteln" + ) + )) } return configState diff --git a/app/src/main/java/com/pr0gramm/app/ui/AdService.kt b/app/src/main/java/com/pr0gramm/app/ui/AdService.kt index f92cb06ec..6976763aa 100644 --- a/app/src/main/java/com/pr0gramm/app/ui/AdService.kt +++ b/app/src/main/java/com/pr0gramm/app/ui/AdService.kt @@ -20,6 +20,7 @@ import com.pr0gramm.app.model.config.Config import com.pr0gramm.app.services.Track import com.pr0gramm.app.services.UserService import com.pr0gramm.app.services.config.ConfigService +import com.pr0gramm.app.time import com.pr0gramm.app.util.AndroidUtility import com.pr0gramm.app.util.Holder import com.pr0gramm.app.util.ignoreAllExceptions @@ -43,7 +44,6 @@ class AdService( private val userService: UserService, ) { - private val logger = Logger("AdService") private var lastInterstitialAdShown: Instant? = null /** @@ -128,9 +128,6 @@ class AdService( } fun buildInterstitialAd(context: Context): Holder { - return Holder { null } - // currently not available - return if (enabledForTypeNow(Config.AdType.FEED_TO_POST_INTERSTITIAL)) { val value = CompletableDeferred() @@ -178,6 +175,8 @@ class AdService( } companion object { + private val logger = Logger("AdService") + private val interstitialUnitId: String = if (BuildConfig.DEBUG) { "ca-app-pub-3940256099942544/1033173712" } else { @@ -190,21 +189,32 @@ class AdService( "/61585078/pr0gramm.com_a_sticky-top" } + private var initialized: Boolean = false + fun initializeMobileAds(context: Context) { - // for some reason an internal getVersionString returns null, - // and the result is not checked. We ignore the error in that case - ignoreAllExceptions { - val listener = OnInitializationCompleteListener { } - MobileAds.initialize(context, listener) - - MobileAds.setAppVolume(0f) - MobileAds.setAppMuted(true) - - MobileAds.setRequestConfiguration( - RequestConfiguration.Builder() - .setTestDeviceIds(listOf("D5DDF82D7F630F71AB2E7699408B1429")) - .build() - ) + if (initialized) { + return + } + + // run initialization code only once + initialized = true + + logger.time("Initializing MobileAds") { + // for some reason an internal getVersionString returns null, + // and the result is not checked. We ignore the error in that case + ignoreAllExceptions { + val listener = OnInitializationCompleteListener { } + MobileAds.initialize(context, listener) + + MobileAds.setAppVolume(0f) + MobileAds.setAppMuted(true) + + MobileAds.setRequestConfiguration( + RequestConfiguration.Builder() + .setTestDeviceIds(listOf("D5DDF82D7F630F71AB2E7699408B1429")) + .build() + ) + } } } } diff --git a/app/src/main/java/com/pr0gramm/app/ui/MainActivity.kt b/app/src/main/java/com/pr0gramm/app/ui/MainActivity.kt index 63fc78d1b..521fd215e 100644 --- a/app/src/main/java/com/pr0gramm/app/ui/MainActivity.kt +++ b/app/src/main/java/com/pr0gramm/app/ui/MainActivity.kt @@ -22,6 +22,10 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.google.android.material.snackbar.Snackbar +import com.google.android.ump.ConsentInformation +import com.google.android.ump.ConsentRequestParameters +import com.google.android.ump.FormError +import com.google.android.ump.UserMessagingPlatform import com.pr0gramm.app.* import com.pr0gramm.app.Duration.Companion.seconds import com.pr0gramm.app.api.pr0gramm.MessageType @@ -62,6 +66,7 @@ class MainActivity : BaseAppCompatActivity("MainActivity"), PermissionHelperActivity, RecyclerViewPoolProvider by RecyclerViewPoolMap() { + private var consentInfo: ConsentInformation? = null private val handler = Handler(Looper.getMainLooper()) private var permissionHelper = PermissionHelperDelegate(this) @@ -170,6 +175,49 @@ class MainActivity : BaseAppCompatActivity("MainActivity"), invalidateRecyclerViewPool() } } + + askConsent() + } + + private fun askConsent() { + val params = ConsentRequestParameters.Builder() + .setTagForUnderAgeOfConsent(false) + .build() + + consentInfo = UserMessagingPlatform.getConsentInformation(this).also { consentInfo -> + consentInfo.requestConsentInfoUpdate( + this, + params, + this::onConsentInfoUpdateSuccess, + this::onConsentInfoUpdateFailure + ) + } + + // try to initialize mobile adds in parallel, we might already have + // consent from a previous form + initializeMobileAdsSdk() + } + + private fun onConsentInfoUpdateSuccess() { + UserMessagingPlatform.loadAndShowConsentFormIfRequired(this) { err -> + if (err != null) { + logger.warn { "Failed to get consent: $err" } + return@loadAndShowConsentFormIfRequired + } + + // we have consent, try to initialize mobile sdk + initializeMobileAdsSdk() + } + } + + private fun onConsentInfoUpdateFailure(err: FormError) { + logger.warn { "Failed to update consent form: $err" } + } + + private fun initializeMobileAdsSdk() { + launchWhenCreated { + AdService.initializeMobileAds(this@MainActivity) + } } private fun buildDrawerArrowDrawable(): DrawerArrowDrawable { diff --git a/build.gradle b/build.gradle index c67fd075a..be413b51d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' } }