自分用のサンプルコードとして、Permissionの取得、AdMobの表示、EUユーザ向けのGDPR対応の機能を持った単一アクティビティのひな形を作りました。
個別には大体理解してるけど、「じゃあ全部くっつけたらどうなるの?」という実装案。ベストプラクティスとかじゃ全くないです。
サンプルコードはご自由にお持ち帰りください。
ただし、お腹を壊しても責任はとれませんのであしからず。
Activityのソースはだいぶ見切れてるのでAndroid Studioに貼り付けてから見た方がいいかも
追記:リリースビルドに最新バージョンのAdMobライブラリを使用しないこと(体験談)neet-rookie.hatenablog.com
前提条件
- Android Studio 3.4
- minSdkVersion 16 Android4.1 Jelly Bean
- targetSdkVersion 28 Android9.0 Pie
- Gradle5.1.1、Android Gradle Plugin3.4.0
- サポートライブラリAndroidX
- AdMob SDK 17.2.1
- Consent SDK 1.0.7
- AdMobアカウントを持っていること(パブリシャーID必須)
- AdMob広告プロパイダの選択→「一般によく使用される広告技術プロバイダのグループ」執筆時点で199プロパイダ
- AdMobメディエーション未使用※執筆時点でConsentSDK未対応
機能
サンプルコード
build.gradle(app)※趣旨と関係ない自動生成コードを含む
apply plugin: 'com.android.application' android { compileSdkVersion 28 defaultConfig { applicationId "jp.your.pack.sample" minSdkVersion 16 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' implementation 'com.google.android.gms:play-services-ads:17.2.1' implementation 'com.google.android.ads.consent:consent-library:1.0.7' }
AndroidManifest.xml※meta-data必須 ※例としてカメラとストレージ権限
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.your.pack.sample"> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="jp.your.pack.sample.SampleGDPRActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="@string/admob_app_id"/> </application> </manifest>
string.xml
<resources> <string name="app_name">SampleGDPR</string> <string name="app_privacy_policy">自分で用意したwebページ</string> <string name="admob_publisher_id">自分のパブリシャーID</string> <string name="admob_app_id">ca-app-pub-3940256099942544~3347511713</string><--Manifestからも参照--> <string name="admob_unit_id_banner">自分の広告ユニットIDバナー</string> </resources>
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/linearLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".SampleGDPRActivity"> <LinearLayout android:id="@+id/bottomAdContainer" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> <Button android:id="@+id/btnShowConsentForm" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="showConsentForm" android:text="同意ステータスの更新" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
SampleGDPRActivity.java
package jp.your.pack.sample; import android.Manifest; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.google.ads.consent.ConsentForm; import com.google.ads.consent.ConsentFormListener; import com.google.ads.consent.ConsentInfoUpdateListener; import com.google.ads.consent.ConsentInformation; import com.google.ads.consent.ConsentStatus; import com.google.ads.consent.DebugGeography; import com.google.ads.mediation.admob.AdMobAdapter; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.AdSize; import com.google.android.gms.ads.AdView; import com.google.android.gms.ads.MobileAds; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; public class SampleGDPRActivity extends AppCompatActivity { /** 改行コード */ private static final String BR = System.getProperty("line.separator"); /** ログ出力時のタグ */ private static final String LOGTAG = "SampleGDPRActivity"; /** onRequestPermissionsResultをオーバーライドする際の識別子 */ private static final int REQUEST_CODE_PERMISSION = 1000; //数字はてきとー、Activity内で一意 /** 画面UI参照 */ private UI ui; private class UI { /** 画面下部ドッキングのAdViewコンテナ、SmartBanner想定 */ private final @NonNull LinearLayout bottomAdContainer; /** 同意ステータス更新ボタン */ private final @NonNull Button btnShowConsentForm; /** ここをしっかり作ればぬるぽにならない */ private UI(@NonNull LinearLayout bottomAdContainer, @NonNull Button btnShowConsentForm){ this.bottomAdContainer = bottomAdContainer; this.btnShowConsentForm = btnShowConsentForm; } //TODO:画面項目の一括非表示とかUI制御を書く } /** 広告表示とGDPR対応 */ private ADUnit ad = new ADUnit(); private class ADUnit { /** GDPR対応:欧州ユーザ同意確認フォーム */ private ConsentForm consentForm; /** GDPR対応:同意ステータス取得処理 排他制御 */ private boolean mutexGDPR_checking = false; /** GDPR対応:同意フォーム表示 排他制御 */ private boolean mutexGDPR_consent = false; /** AdViewの参照 */ private AdView adView = null; /** アプリケーションのPrivacy PolicyのURL、Webページを用意する、Google Siteが便利 */ private String getPrivacyPolicyURL(){ return "http://www.google.co.jp/"; //ダミーURL //return getString(R.string.app_privacy_policy); //TODO:Stringリソースから読み込む } /** AdMobのパブリシャーID、GDPR同意取得時に使用、広告プロバイダリストの取得に使ってるっぽい */ private String getPublisherID(){ //SampleIDは無い、実在の物を使う return getString(R.string.admob_publisher_id); //Stringリソースから読み込む } /** AdMobのアプリケーションID、AdMob初期化に使用 */ private String getAppID(){ return "ca-app-pub-3940256099942544~3347511713"; //Sample AdMob app ID //return getString(R.string.admob_app_id); //TODO:Stringリソースから読み込む } /** AdMobの広告ユニットID、AdView初期化に使用 */ private String getAdUnitID_Banner(){ return "ca-app-pub-3940256099942544/6300978111"; //Sample AdMob banner unit ID //return getString(R.string.admob_unit_id_banner); //TODO:Stringリソースから読み込む } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sample_gdpr); Log.d(LOGTAG, "onCreate()"); //UI参照、レイアウトで定義した画面項目の参照を全部取得しておく ui = new UI( (LinearLayout)findViewById(R.id.bottomAdContainer), //AdViewコンテナ、SmartBanner想定 (Button)findViewById(R.id.btnShowConsentForm) //同意ステータス更新ボタン、同意するのと同じ程度の操作で同意ステータスを変更できなければいけない ); //パーミッションチェック、ほんとはGDPR同意ステータスを更新してからがいいけど、ネットワークない時とかfail判定までおよそ3分かかる //パーミッション取得フォームとGDPR取得フォームが同時に表示されると、パーミッション取得が前面にモーダル表示される。 checkPermissionAndRequest(); //以降GDPR関係 ad.mutexGDPR_checking = true; //GDPR排他制御、同意ステータス更新ボタン制御のため //同意ステータス更新ボタンを無効にする ui.btnShowConsentForm.setEnabled(false); //同意ステータス取得のfail判定に時間がかかるためデフォルトで無効 //ConsentInformation初期化 ConsentInformation consentInformation = ConsentInformation.getInstance(this); //consentInformation.addTestDevice("ほげほげほげーーーー"); エミュレータ以外で実行するとLogcatにテストデバイスIDが出力されるのでそれを埋める //consentInformation.setDebugGeography(DebugGeography.DEBUG_GEOGRAPHY_EEA); //テスト用:EUユーザ //consentInformation.setDebugGeography(DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA); //テスト用:EU圏外 //パブリシャーIDをセットする、配列ということは・・・ String[] publisherIds = {ad.getPublisherID()}; //【https://support.google.com/admob/answer/2784578】パブリシャーIDを確認する //同意ステータスチェック、なぜUpdateというメソッド名なのか consentInformation.requestConsentInfoUpdate(publisherIds, new ConsentInfoUpdateListener() { @Override public void onConsentInfoUpdated(ConsentStatus consentStatus) { Log.d(LOGTAG, "onConsentInfoUpdated()"); //居住地チェック if(ConsentInformation.getInstance(getApplicationContext()).isRequestLocationInEeaOrUnknown()) { //EUユーザのため、同意ステータスを確認する Log.d(LOGTAG, "EUユーザー、同意ステータスの確認と同意ステータス更新ボタンの有効化"); //同意状態変更ボタンを有効にする ui.btnShowConsentForm.setEnabled(true); //同意ステータスは? switch (consentStatus){ case PERSONALIZED: Log.d(LOGTAG, "パーソナライズ広告の同意済み"); initAdMob(true); //AdMob初期化 break; case NON_PERSONALIZED: Log.d(LOGTAG, "非パーソナライズ広告の同意済み"); initAdMob(false); //AdMob初期化 break; case UNKNOWN: Log.d(LOGTAG, "consentStatus=UNKNOWN"); default: Log.d(LOGTAG, "同意情報がない、同意情報の取得が必要"); showConsentForm(null); //Google提供の同意フォームを表示する break; } }else{ //居住地がEU以外、広告出し放題は最高だぜ Log.d(LOGTAG, "非EUユーザ"); initAdMob(true); //AdMob初期化 } ad.mutexGDPR_checking = false; //排他制御解除 } @Override public void onFailedToUpdateConsentInfo(String errorDescription) { Log.d(LOGTAG, "同意情報の取得に失敗。特別に広告無しで使用させる。ネットワークエラーとか?"); ad.mutexGDPR_checking = false; //排他制御解除 } }); } /** * パーミッションチェックと取得 * @return 権限をすでに取得していたかどうか */ private boolean checkPermissionAndRequest(){ Log.d(LOGTAG, "checkPermissionAndRequest()"); //パーミッションチェック、TODO:アプリによって必要な権限を取得する、Manifestにも書く if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ){ Log.d(LOGTAG, "ActivityCompat.requestPermissions()"); //権限取得 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_PERMISSION); return false; }else{ return true; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Log.d(LOGTAG, "onRequestPermissionsResult()"); Log.d(LOGTAG, "permissions=" + Arrays.toString(permissions)); Log.d(LOGTAG, "grantResults=" + Arrays.toString(grantResults)); if (requestCode == REQUEST_CODE_PERMISSION) { boolean allGreen = true; if(0 < grantResults.length) { //中断された場合に0個の配列が来る、通常は要求した権限と同数 //全て許可されたか確認 for(int grantResult:grantResults){ if(grantResult!=PackageManager.PERMISSION_GRANTED allGreen = false; break; } } }else{ allGreen = false; } if (allGreen) { Log.d(LOGTAG, "権限取得"); //TODO:アプリの再初期化処理を行う。機能の有効化とか? } else { Log.d(LOGTAG, "拒否られた"); } // [2019/7/2修正]先頭しかGRANTED確認してなかった(冷汗 // if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Log.d(LOGTAG, "権限取得"); // //TODO:アプリの再初期化処理を行う。機能の有効化とか? // } else { // Log.d(LOGTAG, "拒否られた"); // } }else{ //別なリクエストコードを指定した時、Switch文でもいい } } /** * consentForm.load()、同意ステータス更新ボタンを実現するためにサブルーチン化 * @param v 特に使用しないためnull指定でよい、同意ステータス更新ボタンのクリックイベントとして設定できるようにするため必要 */ public void showConsentForm(@Nullable View v){ Log.d(LOGTAG, "showConsentForm()"); //排他制御確認 if((v!=null && ad.mutexGDPR_checking) || ad.mutexGDPR_consent){ Log.d(LOGTAG, "同意ステータス確認中か、すでに同意フォーム起動処理中のため中断"); //同意ステータス確認中(確認後必要に応じてフォーム自動起動) //または、すでに同意情報取得フォームの起動処理が行われまだ終了していないため中断 return; } //以降でGoogle提供の同意フォームの表示と結果の処理 ad.mutexGDPR_consent = true; //同意フォーム排他制御 //URLオブジェクト生成 URL privacyUrl = null; try { privacyUrl = new URL(ad.getPrivacyPolicyURL()); } catch (MalformedURLException e) { Log.e(LOGTAG, "不正なプライバシーポリシーURL", e); } //同意フォームを構築 ad.consentForm = new ConsentForm.Builder(SampleGDPRActivity.this, privacyUrl) //privacyUrl==nullだと例外発生 .withListener(new ConsentFormListener() { @Override public void onConsentFormLoaded() { Log.d(LOGTAG, "ConsentFormListener.onConsentFormLoaded()"); ad.consentForm.show(); //ロードが完了したらフォームを表示 } @Override public void onConsentFormOpened() { Log.d(LOGTAG, "ConsentFormListener.onConsentFormOpened()"); //フォームが表示された } /** * @param consentStatus 同意ステータス * @param userPrefersAdFree ユーザが有料版を希望したかどうか、trueならPlayストアとかに飛ばす */ @Override public void onConsentFormClosed(ConsentStatus consentStatus, Boolean userPrefersAdFree) { Log.d(LOGTAG, "ConsentFormListener.onConsentFormClosed()"); // ユーザがオプションを選択してフォームを閉じたときに発生、ここでconsentStatusをチェックする switch (consentStatus){ case PERSONALIZED: Log.d(LOGTAG, "パーソナライズ広告の同意"); initAdMob(true); //AdMob初期化 break; case NON_PERSONALIZED: Log.d(LOGTAG, "非パーソナライズ広告の同意"); initAdMob(false); //AdMob初期化 break; case UNKNOWN: default: Log.d(LOGTAG, "同意が得られなかった"); exitApplication(); //アプリ終了 break; } ad.mutexGDPR_consent = false; //同意フォーム排他制御解除 } /** 同意情報のフォームで何らかのエラー */ @Override public void onConsentFormError(String reason) { Log.d(LOGTAG, "ConsentFormListener.onConsentFormError"+BR+reason); ad.mutexGDPR_consent = false; //同意フォーム排他制御解除 Log.d(LOGTAG, "アプリを強制終了します"); exitApplication(); //強制終了する、TODO:アプリによって好きな処理を } }) .withPersonalizedAdsOption() //パーソナライズ広告ボタン .withNonPersonalizedAdsOption() //非パーソナライズ広告ボタン //.withAdFreeOption() //広告無し有料版ボタン //単純なキャンセルボタンの設定ができない・・・ .build(); //同意フォームをロード→onConsentFormLoadedへ ad.consentForm.load(); } /** * AdMobの初期化、GDPR対応後に呼ぶ * @param personalized パーソナライズ広告かどうか */ private void initAdMob(boolean personalized){ Log.d(LOGTAG, "initAdMob()"); //現在表示しているadViewがあるか if(ad.adView==null) { Log.d(LOGTAG, "AdMob初期処理:MobileAds.initialize()"); MobileAds.initialize(SampleGDPRActivity.this, ad.getAppID()); //AdMob初期化、アプリケーションの起動時に一度だけ呼び出す【https://developers.google.com/android/reference/com/google/android/gms/ads/MobileAds.html#initialize(android.content.Context,%20java.lang.String)】 }else{ //2回目以降、同意ステータスの変更を想定 Log.d(LOGTAG, "AdMob初期処理-2回目以降:表示している広告ユニットの破棄"); ui.bottomAdContainer.removeView(ad.adView); //追加済みのadViewをいったん削除 ad.adView = null; } //広告ユニットの生成 ad.adView = new AdView(SampleGDPRActivity.this); //AdView初期化、広告IDなどをレイアウトに埋めるとアプリIDと分離され美しくない、がしかしプログラムに埋める場合レイアウトにAdViewを配置するとエラーになる ad.adView.setAdSize(AdSize.SMART_BANNER); //みんな大好きスマートバナー ad.adView.setAdUnitId(ad.getAdUnitID_Banner()); //バナー広告ユニットのID、広告ユニットの種類で変わる ad.adView.setMinimumHeight(100); //広告がロードされるまで高さが決定されないと、他のUIの位置が定まらない、SMART_BANNERは32dp or 50dp or 90dpのパターンがある ui.bottomAdContainer.addView(ad.adView); //AdView配置 //広告リクエスト AdRequest adRequest; if(personalized) { //パーソナライズ広告 adRequest = new AdRequest.Builder().build(); //adRequest = new AdRequest.Builder().addTestDevice("ほげほげほげーーーー").build(); //テストデバイス、エミュレータ以外で実行するとLogcatにテストデバイスIDが出力されるのでそれを埋める }else{ //非パーソナライズ広告、Google Mobile Ads SDK に同意を転送 Bundle extras = new Bundle(); //この実装方法は変更される可能性がある。 extras.putString("npa", "1"); //【https://developers.google.com/admob/android/eu-consent】の「Google Mobile Ads SDK への同意の転送」参照 adRequest = new AdRequest .Builder() //.addTestDevice("ほげほげほげーーーー") //テストデバイス、エミュレータ以外で実行するとLogcatにテストデバイスIDが出力されるのでそれを埋める .addNetworkExtrasBundle(AdMobAdapter.class, extras) .build(); } ad.adView.loadAd(adRequest); //広告リクエスト } @Override protected void onResume(){ Log.d(LOGTAG, "onResume()"); super.onResume(); } @Override protected void onPause(){ Log.d(LOGTAG, "onPause()"); super.onPause(); } @Override protected void onStop(){ Log.d(LOGTAG, "onStop()"); super.onStop(); } @Override protected void onDestroy() { Log.d(LOGTAG, "onDestroy()"); super.onDestroy(); } /** アプリを終了する */ private void exitApplication(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask(); //完全に終了 }else{ finish(); //プロセス残骸が残る、システムがそのうち削除する } } }
Android6.0実行結果スクショ
Android4.4実行結果スクショ問題あり
Android4.1実行結果スクショ問題あり
参考にしたページ
developers.google.comdevelopers.google.com
developers.google.com
qiita.com