365連休

にわかのandroidとかの開発メモ。

Android Studio 3.4 Permission & AdMob & GDPRに対応したActivityのサンプル(EU の e プライバシー指令と一般データ保護規則)

自分用のサンプルコードとして、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未対応

 

機能

  • 単一アクティビティ
  • パーミッションの確認と取得(例としてカメラとストレージ)
  • AdMobスマートバナー
  • EUユーザ同意取得と同意更新
  • Viewと広告関係をまとめた内部クラス

サンプルコード

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>

 
activity_sample_gdpr.xml※下部ドッキングLinearLayoutと同意更新ボタンのみ

<?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実行結果スクショ

f:id:neet_rookie:20190525212303p:plain:h200,left
f:id:neet_rookie:20190525212354p:plain:h200,left
f:id:neet_rookie:20190525212514p:plain:h200,left
f:id:neet_rookie:20190525212551p:plain:h200,left
f:id:neet_rookie:20190525212630p:plain:h200,left
f:id:neet_rookie:20190525212714p:plain:h200,left
f:id:neet_rookie:20190525212749p:plain:h200,left
f:id:neet_rookie:20190525213144p:plain:h200,left

Android4.4実行結果スクショ問題あり

f:id:neet_rookie:20190525212910p:plain:h200

Android4.1実行結果スクショ問題あり

f:id:neet_rookie:20190525213012p:plain:h200
 
 

参考にしたページ

developers.google.com
developers.google.com
developers.google.com
qiita.com