KotlinでConsentSDKを使いAdMobのGDPR対応(EU一般データ保護規則対応)をしてみた

Twitter アプリ「Txiicha」にて、KotlinでConsentSDKを使いAdMobのGDPR対応(EU一般データ保護規則対応)を行ってみました。

ConsentSDK に関する Googleさんのドキュメント「Requesting Consent from European Users」はこちらです。

間違い等ございましたらご指摘頂けましたら嬉しいです。

 

プライバシーポリシーを準備

GDPR対応についてを記述した英文のプライバシーポリシーをWebサイトに掲載しました。プライバシーポリシージェネレーターで作成したものを一部調整しました。(利用したジェネレーターのリンクはプライバシーポリシー内にあります)

 

AdMob設定変更

  1. AdMob管理画面にて「プロック管理>EUユーザーの同意」を開きました。
  2. 広告技術プロバイダの選択で「広告技術プロバイダのカスタム グループ」を選択しました。
  3. 「プロバイダを選択」をクリックし「Google」のみが選択されていることを確認しました。
  4. 「変更を保存」ボタンを押しました。
  5. 「サイト運営者Id(パブリッシャーID)」をメモしました。

 

ソースコードの前提条件

  • ソースコードに出てくる「SinLog」は Log を独自拡張したものですで、Log として読み替えてください。
  • ConsentForm 表示時に「Error: consent form can be used with custom provider selection only.」が表示される事があるよです。その場合は「広告技術プロバイダの選択」で「広告技術プロバイダのカスタム グループ」が選択されていることを確認し、1-3時間待ってみると良いようです。
  • ユーザが同意を変更できるようにする必要がありますが今回の記事からは割愛しました。

 

Content SDK をインポート

プロジェクトレベルの build.gradle を開き、「allprojects/repositories」に「maven  { url “https://maven.google.com” } 」を追加しました。

allprojects {
	repositories {
		...
		maven {
			url 'https://maven.google.com'
		}
	}
}

 

アプリレベルの build.gradle を開き、「dependencies」に「implementation ‘com.google.android.ads.consent:consent-library:1.0.3’」を追加しました。
「com.android.support:*」のバージョンが古かったようで、 ‘26.0.2’ から ‘26.1.0’ に変更しました。

dependencies {
	...
	implementation 'com.android.support:appcompat-v7:26.1.0'
	implementation 'com.android.support:customtabs:26.1.0'
	...
	implementation 'com.google.android.ads.consent:consent-library:1.0.3'
}

 

AdMob Publisher ID

AdMobのPublisher ID を values/strings_translatable_false.xml に記述しました。

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<string name="admob_publisher_id" translatable="false">pub-4064604490XXX558</string>

</resources>

 

文字列リソース

ネットワークに繋がっていないときに Consent SDK の ConsentInformation#requestConsentInfoUpdate() が Failed になるので、そのときに表示するダイアログの文字列リソース(en, ja)を準備しました。

values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<string name="please_start_this_app_online">Please start this app online.</string>
	<string name="restart">Restart</string>
	<string name="exit">Exit</string>

</resources>

values-ja/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<string name="please_start_this_app_online">このアプリはオンライン状態で起動してください。</string>
	<string name="restart">再起動する</string>
	<string name="exit">終了する</string>

</resources>

 

SharedPreferencesUtils

Txiichaではアプリ設定は全部 Realm に保存しているのですが、GDPR 対応は SharedPreferences に設定情報を保存することにし、Utils を作りました。

object SharedPreferencesUtils {

	fun getDefault(context: Context): SharedPreferences {
		return PreferenceManager.getDefaultSharedPreferences(context)
	}

	fun getEditor(context: Context): SharedPreferences.Editor {
		return getDefault(context).edit()
	}

	fun put(context: Context, key: String, value: Boolean) {
		getEditor(context).putBoolean(key, value).apply()
	}

	fun get(context: Context, key: String, default: Boolean): Boolean {
		return getDefault(context).getBoolean(key, default)
	}

}

 

アプリ用定数

テスト用のデバイスIdと、SharedPreferences に状態を保存するためのキー、プライバシーポリシーURLを準備しました。(デバイスIdの調べ方は「テスト」で説明)

object AppKey {

	val TEST_DEVICE_ID		= "975F2AD984C46974AD90B1D793XXXAA6"
	val ADMOB_NON_PERSONALIZED	= "ADMOB_NON_PERSONALIZED"
}
object SinproWebUtils {

	val PRIVACY_POLICY_EN_URL	= "https://sinproject.net/en/privacy-policy/"

}

 

Consent SDK を利用して、EEAエリア判定し、同意確認ダイアログを表示

EEAエリアで利用しているかを ConsentInformation を使って判定するようにしました。EEAエリアにて同意未確認の場合は ConsentForm を表示するようにしました。

object AdMobConsentUtils {

   fun requestConsent(activity: Activity, onValidCallback: () -> Unit, onInvalidCallback: () -> Unit) {
      val consentInformation	= ConsentInformation.getInstance(activity)
      val publisherIds		= arrayOf(activity.getString(R.string.admob_publisher_id))

      // FOR TEST ONLY
//    consentInformation.reset()
      // If you comment out addTestDevice and execute it, you can find DeviceId in logcat like this:
      // I/ConsentInformation: Use ConsentInformation.getInstance(context).addTestDevice("975F2AD984C46974AD90B1D793XXXAA6") to get test ads on this device.
//    consentInformation.addTestDevice(AppKey.TEST_DEVICE_ID)
//    consentInformation.debugGeography = DebugGeography.DEBUG_GEOGRAPHY_EEA
//    consentInformation.debugGeography = DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA
      // ---

      consentInformation.requestConsentInfoUpdate(publisherIds, object: ConsentInfoUpdateListener {
         override fun onFailedToUpdateConsentInfo(errorDescription: String?) {
            SinLog.e(errorDescription)
            AlertDialog.Builder(activity)
                  .setCancelable(false)
                  .setMessage(R.string.please_start_this_app_online)
                  .setPositiveButton(R.string.restart) { _, _ ->
                     onInvalidCallback()
                     activity.startActivity(activity.intent)
                  }
                  .setNegativeButton(R.string.exit) { _, _ -> onInvalidCallback() }
                  .show()
         }

         override fun onConsentInfoUpdated(consentStatus: ConsentStatus?) {
            if (ConsentInformation.getInstance(activity).isRequestLocationInEeaOrUnknown) {
               when (consentStatus) {
                  ConsentStatus.NON_PERSONALIZED, ConsentStatus.PERSONALIZED -> {}
                  else -> {
                     makeConsentForm(activity, onValidCallback, onInvalidCallback).load()
                     return
                  }
               }
            }

            onValidCallback()
         }
      })
   }

   fun makeConsentForm(context: Context, onValidCallback: () -> Unit, onInvalidCallback: () -> Unit): ConsentForm {
      val privacyPolicyUrl		= URL(SinproWebUtils.PRIVACY_POLICY_EN_URL)
      var consentForm: ConsentForm?	= null

      consentForm = ConsentForm.Builder(context, privacyPolicyUrl)
            .withListener(object: ConsentFormListener() {
               override fun onConsentFormLoaded() {
                  consentForm?.show()
               }

               override fun onConsentFormOpened() {}

               override fun onConsentFormClosed(consentStatus: ConsentStatus?, userPrefersAdFree: Boolean?) {
                  if (userPrefersAdFree!!) {
                     AppEdition.openPro(context)
                     onInvalidCallback()
                  }

                  when (consentStatus) {
                     ConsentStatus.PERSONALIZED -> {
                        SharedPreferencesUtils.put(context, AppKey.ADMOB_NON_PERSONALIZED, false)
                        onValidCallback()
                     }
                     ConsentStatus.NON_PERSONALIZED -> {
                        SharedPreferencesUtils.put(context, AppKey.ADMOB_NON_PERSONALIZED, true)
                        onValidCallback()
                     }
                     else -> {
                        onInvalidCallback()
                     }
                  }
               }

               override fun onConsentFormError(reason: String?) {
                  SinLog.e(reason)
                  onInvalidCallback()
               }
            })
            .withPersonalizedAdsOption()
            .withNonPersonalizedAdsOption()
            .withAdFreeOption()
            .build()

      return consentForm
   }
}

 

PERSONALIZED OR NON_PERSONALIZED

同意結果により、PERSONALIZED広告を表示するか、NON_PERSONALIZED 表示するかを、広告表示時に指定しました。

object AppAdMobAdsUtils {

	fun loadAd(context: Context, adView: AdView) {
		val adRequestBuilder = AdRequest.Builder()

//		adRequestBuilder.addTestDevice(AppKey.TEST_DEVICE_ID)

		if (SharedPreferencesUtils.get(context, AppKey.ADMOB_NON_PERSONALIZED, false)) {
			val extras = Bundle()

			extras.putString("npa", "1")
			adRequestBuilder.addNetworkExtrasBundle(AdMobAdapter::class.java, extras)
		}

		val adRequest = adRequestBuilder.build()

		adView.loadAd(adRequest)
		SinLog.d("loadAd finished.")
	}

 

初期表示Activityに判定処理を追加

初期表示Activityの onCreate に記述していた処理を fun onCreate2() に移動しました。その後、Txiicha Lite のみ AdMobConsentUtils.requestConsent() を呼ぶようにしました。

※AppAdMobAdsUtils.loadAd() をコールする前に AdMobConsentUtils.requestConsent() を呼ぶようにします。

class SplashActivity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)

		if (AppEdition.isPro(this)) {
			onCreate2(savedInstanceState)
		}
		else {
			AdMobConsentUtils.requestConsent(this, { onCreate2(savedInstanceState) }, { finish() })
		}
	}

	fun onCreate2(savedInstanceState: Bundle?) {
		setTheme(R.style.AppTheme_Splash)
		setContentView(R.layout.activity_splash)

		...
	}
}

 

テスト

テスト前に以下の準備をしました。

  1. Android実機にて、ConsentInformation に DeviceId を指定せずに実行し、Logcat で「I/ConsentInformation: Use ConsentInformation.getInstance(context).addTestDevice」を探し、DeviceIdを確認しました。

 

以下をテストしました。

  1. ConsentInformation#reset() を有効化し、ConsentInformation に DeviceId と DebugGeography.DEBUG_GEOGRAPHY_EEA をセット後、起動時にダイアログが表示されること
  2. 同意ダイアログ
    1. Linkをタップすると、プライバシーポリシーが表示されること
    2. 「Pay …」を押下すると「Txiicha Pro」のストア画面が表示されること
    3. 「Yes, …」を押下すると、パーソナライズド広告が表示されること
    4. 「No, …」を押下すると、ノンパーソナライズド広告が表示されること
  3. Wi-Fiとデータ通信をOFFにし、起動時にオンライン起動を促すダイアログが表示されること
    1. 「終了する」でアプリが終了すること
    2. 「再起動する」でもう一度ダイアログが表示されること
    3. ダイアログ表示後、「Wi-Fi」をONにし、「再起動する」で同意確認ダイアログが表示されること
    4. ダイアログ表示後、「データ通信」をONにし、「再起動する」で同意確認ダイアログが表示されること
  4. ConsentInformation#reset() をコメントアウトして実行し、同意ダイアログで「Yes」を選択後、再度アプリを起動したときに同意確認無しでパーソナライズド広告が表示されること。
  5. ConsentInformation#reset() を一旦有効化してアプリを実行し同意ダイアログ表示を確認。その後 ConsentInformation#reset() をコメントアウトして実行し、同意ダイアログで「No」を選択後、再度アプリを起動したときに同意確認無しでノンパーソナライズド広告が表示されること。
  6. ConsentInformation#reset() を一旦有効化してアプリを実行し同意ダイアログ表示を確認。ConsentInformation#reset() をコメントアウトし ConsentInformation に DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA を指定し、再度アプリを起動したときに同意ダイアログが表示されないこと。

 

あとがき

短くシンプルにまとめようと思ったのに長くなってしまいました。

この記事が参考になったら、下の「いいね」ボタンを押してもらえると嬉しいです。

同じ悩みを持つ、世の中の知らないどこかの誰かに、少しでも役に立ちますように。

 

参考サイト

以下のサイトを参考にさせていただきました。情報公開ありがとうございます。

  1. Requesting Consent from European Users  |  Google Mobile Ads SDK for Android  |  Google Developers
  2. AdMobを使っているAndroidアプリでEU一般データ保護規則(GDPR)対応する
  3. KotlinでAdMobをGDPR対応させる(Consent SDKの簡単な説明)

Leave a comment

メールアドレスが公開されることはありません。

*

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)