AdMobを使っているAndroidアプリでEU一般データ保護規則(GDPR)対応する

# はじめに
前提として、私自身はAndroidアプリの不真面目な個人開発者です
GDPR対応に向けて全然準備しておらず、突貫で作業する羽目になったので、今後の対応も考えて備忘録的に内容を残している次第です
本記事の内容について責任は負えませんので、あくまでも参考程度にご利用ください
また、過不足や誤りなどありましたら、コメント等でご指摘頂けますと助かります
*(ここで記事を公開しているのも、自分の対応し忘れていることをだれかに指摘して頂けると嬉しいなという思惑を多く含んでいます)*


# EU一般データ保護規則(GDPR)とは
EU在住のユーザの情報を取得する場合に、ちゃんとユーザから同意を得ないといけないよ、というEUの法案
施行は2018年5月25日からです
※私は専門家ではないので、詳細は別途調べてください

*ちなみに、アプリ内の広告ユニットとかでもユーザ情報を取得していたりするので、おそらくほぼすべての開発者が対応しないといけないはず*
*アプリだけでなく個人ブログとかでも、GoogleAnalyticsや情報収集するタイプの広告を導入していたりすると対象だったりするし、EU圏からのアクセスを遮断してもEU居住の旅行者がEU圏外からアクセスする場合も対象らしいので、「すごく面倒なことになったな……」というのが個人的な感想*

# 前提条件
AndroidアプリをGooglePlayにて公開中
使用している広告ツールはAdMobのみ
広告メディエーションもデフォルトのAdMobのみ
公開中のアプリには、EU圏のユーザもいる
*(というのが現状の私の環境です)*

# GDPR対応(AdMobサイト編)
## 広告技術プロバイダの選択
AdMobにログイン
画面上部のメッセージの[詳細]から「ブロックの管理」へ遷移

![キャプチャ0 - コピー.JPG](https://qiita-image-store.s3.amazonaws.com/0/193827/e2e1b7ac-61ec-e90d-8def-bfca575551a5.jpeg)

広告技術プロバイダの選択で、「広告技術プロバイダのカスタム グループ」を選択

![キャプチャ1 - コピー - コピー.JPG](https://qiita-image-store.s3.amazonaws.com/0/193827/6bf0dcc8-8f44-6cc8-a220-e1f2b1541966.jpeg)

「プロバイダを選択」でGoogleだけが指定されていることを確認する (今回はAdMobしか使っていないため)
「変更を保存」を押す
**AdMob以外を使用している場合は、利用している広告配信会社がどこの技術を使っているか確認する必要があると思います**

「同意取得の設定」欄には以下の記載があるので、以降はガイドに従って作業する
今回は、ユーザーの同意取得に使用するダイアログに、Googleのものを使用する方針で進める
> Google Developers ガイド（Android、iOS）をご覧ください。同意に関するダイアログは、Google のものを使っても、独自に作成してもかまいません。ただし、メディエーションをご利用の場合は、独自のダイアログを作成してください。いずれの場合も、Google Developers ガイドの手順に従ってください。アプリコードの更新が終わったら、Google Play か App Store にアプリを再びアップロードします。パブリッシャー ID が必要になる場合があります。

上記の文中のAndroid向けGoogle Developers ガイドは[こちら](https://developers.google.com/admob/android/eu-consent)

ページの末尾に表示されている「サイト運営者 ID」は、後でコーディング時に埋め込むため控えておきます
(すでにアプリ内でstrings.xml等に埋め込んでいる場合は、それをそのまま使用する方針でも問題なし)


# GDPR対応(SDK準備編)
## Consent SDKの追加
[Consent SDK](https://developers.google.com/admob/android/eu-consent#consent_sdk)を使う
※Consent SDKはGoogleが公開しているGDPR対応用のユーティリティSDK

自分のAndroidプロジェクトのbuild.gradleを開いて、mavenリポジトリに`https://maven.google.com`を追加

```groovy:build.gradle(プロジェクト)
allprojects {
    repositories {
        jcenter()
        maven {
            url "https://maven.google.com"
        }
    }
}
```

続いて自分のアプリのbuild/gradleを開いて、dependenciesに`implementation 'com.google.android.ads.consent:consent-library:1.0.1'`を追加

```groovy:build.gradle(モジュール)
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    /*中略*/
    implementation 'com.google.android.ads.consent:consent-library:1.0.1'
}
```

追加が終わったら、AndroidStudio上部の黄色い帯に表示される「最後のプロジェクトの同期以降にGradleファイルが更新されています...」みたいなところで、「今すぐ同期」をクリック

ウェブ上での設定は以上
以降は実際にアプリのコードを修正する

### (おまけ) エラー表示 Gradle DSL method not found: 'implementation()'
(大部分の人には関係ない話だと思われるが) 私の環境では「今すぐ同期」の段階で、`Gradle DSL method not found: 'implementation()'`エラーが発生
Android Gradle Pluginのバージョンが古かったようなので、表示された選択肢`Upgrade plugin to version 3.0.1 and sync project`を押して再試行することで解決しました

# GDPR対応(アプリコード編)

*ここから先はおそらく`com.google.ads.consent`のインポートが必要だと思いますが、コードを書いていればAndroidStudioが補完してくれるはず*

## アプリ内でEU居住判定

今回はアプリ起動直後のOnCreate内でGDPR対応の要否を判定し、対応が必要なユーザの場合には同意を求めるフォームを表示します
2回目以降の起動などで同意取得済みの場合は、何もしません
*なお、今回は同意を求めるフォームが閉じられ同意取得できたときに、PERSONALIZEDかNON_PERSONALIZEDかをSharedPreferencesに保存する方針を取ることにします*

```java:StartActivity.java

    private ConsentForm consentForm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.start_activity);

        // EU一般データ保護規則(GDPR)対応
        ConsentInformation consentInformation = ConsentInformation.getInstance(this);
        // TODO AdMobのページに表示されていた、サイト運営者 IDをセットする
        String[] publisherIds = {"pub-0000000000000000"};

        consentInformation.requestConsentInfoUpdate(publisherIds, new ConsentInfoUpdateListener() {
            @Override
            public void onConsentInfoUpdated(ConsentStatus consentStatus) {
                // ユーザーのステータスを取得できた場合はユーザーの居住地をチェックする
                if(ConsentInformation.getInstance(getApplicationContext()).isRequestLocationInEeaOrUnknown()) {
                    // 居住地判定がtrue(=EU圏内もしくは不明)であれば、ConsentStatusをチェックする
                    // ※居住地判定がfalse(=EU圏外)であれば、今まで通りAdMobのSDKに広告のリクエストを送ってOKなので、既存処理のままとする
                    switch (consentStatus){
                        case PERSONALIZED:
                        case NON_PERSONALIZED:
                            // このまま同意情報を広告SDKに送信できるので、特に処理はない
                            // (別のActivityでも参照するので、同意フォームのクローズ時にSharedPreferencesに保存されている前提とする)
                            break;
                        case UNKNOWN:
                        default:
                            // 同意情報をユーザから取得する必要があるので、Google標準の同意書を表示する
                            consentForm = makeConsentForm(StartActivity.this); // 後述(同意取得のフォームを表示する)
                            consentForm.load();
                            break;
                    }
                }
            }
            @Override
            public void onFailedToUpdateConsentInfo(String errorDescription) {
                // ひとまずの対応として、ステータス取得に失敗した場合はアプリを終了することに
                endApp(); // アプリの終了処理を行うメソッド呼び出し
            }
        });
    }
```

## 同意取得のフォームを表示する

ガイドのサンプルコードをもとにメソッド化して、フォームが閉じられたときにSharedPreferencesに情報を保存するようにしました
別のActivity内でAdMobの動画表示をする際に、この保存情報を見てパーソナライズ広告と非パーソナライズ広告のどちらを表示するかを制御しています
どちらの同意も得られない場合は、アプリを終了します
privacyUrlには、そのアプリのプライバシーポリシーを記載しているウェブページのURLをセットします (このウェブページは、同意フォームからWebClientで表示できるようになっています)

表示されるテキストはGoogleの標準のものなので、**自身の要件に合わせて変更する必要がある場合は、Consent SDKに含まれるconsentform.htmlを修正する**必要があります

*(ちなみに、コード中に出てくるPersonalizedAdManagerはその場で適当に用意したSharedPreferencesを読み書きするためのラッパークラスなので、適当に読み替えてください)*

```java:StartActivity.java
    /**
     * GDPRの同意取得用フォーム作成
     * @param context コンテキスト
     * @return フォーム
     */
    private ConsentForm makeConsentForm(Context context){
        URL privacyUrl = null;
        try {
            // TODO 自分のアプリのプライバシーポリシー表示用URLをセットする
            privacyUrl = new URL("http://www.mywebsite.net/app/privacy-policy/");
        } catch (MalformedURLException e) {
            // TODO URLが異常だったらToastなどで異常を通知して問い合わせてもらうなど適当な処理を入れる
            e.printStackTrace();
        }
        ConsentForm form = new ConsentForm.Builder(context, privacyUrl)
            .withListener(new ConsentFormListener() {
                @Override
                public void onConsentFormLoaded() {
                    // ロードが完了したらフォームを表示
                    consentForm.show();
                }
                @Override
                public void onConsentFormOpened() {
                    // Consent form was displayed.
                }
                @Override
                public void onConsentFormClosed(ConsentStatus consentStatus, Boolean userPrefersAdFree) {
                    // ユーザがオプションを選択してフォームを閉じたときに発生、ここでconsentStatusをチェックする
                    PersonalizedAdManager adManager = new PersonalizedAdManager(getApplicationContext());
                    switch (consentStatus){
                        // 同意フォームのクローズ時にSharedPreferencesに情報を保存する
                        case PERSONALIZED:
                            adManager.updatePersonalized(); // SharedPreferencesに情報を保存
                            break;
                        case NON_PERSONALIZED:
                            adManager.updateNonPersonalized(); // SharedPreferencesに情報を保存
                            break;
                        case UNKNOWN:
                        default:
                            // 同意が得られなかったのでアプリを終了
                            endApp();
                            break;
                    }
                    // TODO userPrefersAdFreeがtrueの場合はユーザが有料版アプリのオプションでOKを選択したことになるので、そちらに誘導する (今回は有料版アプリがないので無し)
                }
                @Override
                public void onConsentFormError(String errorDescription) {
                    // エラーの場合もアプリを終了
                    endApp();
                }
            })
            .withPersonalizedAdsOption() // パーソナライズ広告オプション表示=ON
            .withNonPersonalizedAdsOption() // 非パーソナライズ広告オプション表示=ON
            //.withAdFreeOption() // 広告なし版アプリのオプション表示=OFF
            .build();
        return form;
    }
```


## フォームの結果に応じてAdMobの広告内容を変更する
ユーザ同意の取得関連処理が実装出来たら、実際にAdMobへの公告リクエストに対して反映させる

パーソナライズ広告の配信に同意された場合は、今まで通りのAdRequestをセットする処理でOK

```Java:AdRequestPersonalized.java
        AdView mAdView = viewFront.findViewById(R.id.adView);
        AdRequest adRequest = new AdRequest.Builder().build();
        mAdView.loadAd(adRequest);
```

ユーザーが非パーソナライズ広告の配信にのみ同意する場合は、以下のようにAdREquestにパラメータを追加して広告をリクエストする必要がある

```Java:AdRequestNonPersonalized.java
        AdView mAdView = viewFront.findViewById(R.id.adView);

        Bundle extras = new Bundle();
        extras.putString("npa", "1");

        AdRequest adRequest = new AdRequest
                .Builder()
                .addNetworkExtrasBundle(AdMobAdapter.class, extras)
                .build();
        mAdView.loadAd(adRequest);
```

## 作成済みの同意を変更できるようにする
Googleのガイド内には、下記の一文の記載があります
> Remember to provide users with the option to Change or revoke consent.

*(意訳) ユーザが同意を変更or取り消しできるようにするオプションを忘れないでね*

なので、アプリ内のどこかしらから同意フォームを再表示できるようにしておく必要があります


# テスト・動作確認

テストデバイス指定したうえで、`setDebugGeography`を呼び出すとEU圏内かどうかを切り替えて処理を走らせることができます

EU圏内にする場合の指定
`setDebugGeography(DebugGeography.DEBUG_GEOGRAPHY_EEA);`
EU圏外にする場合の指定
`setDebugGeography(DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA);`


呼び出し箇所は`ConsentInformation.getInstance(context)`の後になります

```java:StartActivity.java

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.start_activity);

        // 動作確認対応 テストデバイスを指定、logcatのホワイトリストにデバイスIDを登録
        // ※エミュレータの場合は自動でテストデバイス扱いになるので不要
        ConsentInformation consentInformation = ConsentInformation.getInstance(this).addTestDevice("33BE2250B43518CCDA7DE426D04EE231")

        // EU圏内にする場合
        ConsentInformation.getInstance(this).setDebugGeography(DebugGeography.DEBUG_GEOGRAPHY_EEA);

        // EU圏外にする場合(こちらを使う場合はコメントアウトを外す)
        //ConsentInformation.getInstance(this).setDebugGeography(DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA);

        String[] publisherIds = {"pub-0000000000000000"};
        consentInformation.requestConsentInfoUpdate(publisherIds, new ConsentInfoUpdateListener() {
            @Override
            public void onConsentInfoUpdated(ConsentStatus consentStatus) {
                // 省略
            }
            @Override
            public void onFailedToUpdateConsentInfo(String errorDescription) {
                // 省略
            }
        });
    }
```

EU圏内に設定してエミュレータを起動すると、アプリ起動時にこんなフォームが表示されるはずです

![キャプチャ7.JPG](https://qiita-image-store.s3.amazonaws.com/0/193827/5641fca2-16c0-04ee-f38d-8829a1c2710c.jpeg)

動作確認までできたら、apkをビルドしてGooglePlayにアップロードします

以上が、GDPR対応として必要なこととなります


# (おまけ) フォーム表示がうまくいかなかったら
onConsentFormErrorメソッド内で、errorDescription引数をログ出力してみるのが良さそうです

```java:StartActivity.java
    @Override
    public void onConsentFormError(String errorDescription) {
        Log.d("forGDPR", "-> onConsentFormError" + errorDescription);
        // エラーの場合もアプリを終了
        endApp();
    }
```

私の場合は最初、`form is not ready to be displayed.`として怒られました
`form.load();`の直後に`form.show();`を呼び出していたのが悪かったようで、ConsentForm.classを読んでようやくonConsentFormLoaded内で`form.show();`を呼び出す必要があることに気づきました

ちなみに、AdMobのサイトで「一般によく使用される広告技術プロバイダのグループ」を選んでいると、`Error: consent form can be used with custom provider selection only.`となってGoogleの標準フォームは使えなくなります
*(どうやら現状では、設定をサイト上で変更してから反映されるまでに時間がかかるようです。1～3時間程度のようなのでのんびり待ちましょう)* ※5/25追記
~~*(エラーメッセージ的にはそのはずなんですが、手元のエミュレータ環境では何故かカスタムプロバイダ選択中でもこのエラーから抜け出せずしばらく膠着状態でした。時間を置いたりbuild.gradleのバージョンコード変えて無理やり再ビルドかけてみたりしてたら、ソースコードを弄っていなくても出なくなりました)*~~

### その他のエラー

onConsentFormLoaded内での`consentForm.show();`後に以下のエラーが発生していた
`android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application`

悪さをしていたのは、思考停止で`getApplicationContext()`を渡していた、`makeConsentForm()`でした

`ConsentForm form = makeConsentForm(StartActivity.this);`とすることで解決したので、同じエラーになった場合はこの辺りを確認してみてください


# 参考情報
[Requesting Consent from European Users](https://developers.google.com/admob/android/eu-consent)

# その他

+ PersonalizedAdManagerのソースコード  
記事中にコードの無かった部分です  
(SharedPreferencesの読み書きとAdRequestの生成をするだけの単純なもの)  
アップロードしました → [PersonalizedAdManager/GitHub](https://github.com/tokisuzume/PersonalizedAdManager/tree/master)