Android nendの本番広告設定にハマる

nendの広告枠の承認を頂いたので、Androidアプリに設定を行っていた。

SDKの組み込み自体はマニュアルを見ながら簡単に行え、テストAdの表示まで順調に完了できた。

ところが、本番用のapikeyとspot idに差し替えてみたところ、広告表示が行われない...

で、バナー型広告_実装手順 · fan-ADN/nendSDK-Android Wiki · GitHub このページでイベントリスナーからエラーがキャッチできるようなので、

class MainrActivity : AppCompatActivity(), NendAdListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //layoutに配置したnend view
        nendAdView.setListener(this)
    }

    override fun onFailedToReceiveAd(nendAdView: NendAdView) {
        val nendError = nendAdView.nendError
        when (nendError) {
            NendAdView.NendError.INVALID_RESPONSE_TYPE -> {
               // 不明な広告ビュータイプ
            }
            NendAdView.NendError.FAILED_AD_DOWNLOAD -> {
                // 広告画像の取得失敗
            }
            NendAdView.NendError.FAILED_AD_REQUEST -> {
                // 広告取得失敗
            }
            NendAdView.NendError.AD_SIZE_TOO_LARGE -> {
                // 広告サイズがディスプレイサイズよりも大きい
            }
            NendAdView.NendError.AD_SIZE_DIFFERENCES -> {
                // リクエストしたサイズと取得したサイズが異なる
            }
            else -> {
            }
        }
    }
    /** 受信成功通知  */
    override fun onReceiveAd(nendAdView: NendAdView) {
        Toast.makeText(applicationContext, "onReceiveAd", Toast.LENGTH_LONG).show()
    }

    /** クリック通知  */
    override fun onClick(nendAdView: NendAdView) {
        Toast.makeText(applicationContext, "onClick", Toast.LENGTH_LONG).show()
    }

    /** 復帰通知  */
    override fun onDismissScreen(arg0: NendAdView) {
        Toast.makeText(applicationContext, "onDismissScreen", Toast.LENGTH_LONG).show()
    }
}

として、デバッグしたところSDKからFAILED_AD_REQUESTが返されてきてるみたい。

承認時のメールに、「数時間後に配信が開始されます」的なことを書いてあったので、まだ配信開始されてないのかなー。なんて思ってたんだけど待てど暮らせど表示される気配がない...

で、問い合わせてみようと思って問い合わせフォームに入力中...

●検証端末またはシュミレータ―の言語設定が日本語以外になっていないか※日本語以外になっている場合、日本語に設定してお試しください

なんで記載があった。





日本語にすると... 表示された!

テストAdだと日本語以外でも表示されるので、ちょっとした嵌りどころだった。という話し。


Android WebViewの広告表示を制限する

概要

WebViewでロードしたページの広告表示を制限する

Hacking up an ad blocker for Android | Ha Duy Trung’s Blog こちらのページを参考に実装を行った。


ミュートする広告配信ホスト一覧の取得

https://sites.google.com/site/hosts2ch/ja より日本国内向けの広告配信事業者のホスト一覧が取得できる。

「ja」というファイル名になるので、hosts.txtというようにリネームを行っておく。

リネームを行ったhosts.txtファイルをAndroidプロジェクトのassetsフォルダに配置する。


okhttpの導入

後述するAdBlockerクラスにokhttpに梱包されているライブラリ(okio)を使用するのでokhttpを導入しておく。

app/build.gradle

implementation("com.squareup.okhttp3:okhttp:4.2.0")


AdBlockerの実装

ほぼ、上記の参考サイト通りの実装で良い。Java -> Kotlin書き直すのが面倒なので、Javaのままで取り込んだ。

public class AdBlocker {
    private static final String AD_HOSTS_FILE = "hosts.txt";
    private static final Set<String> AD_HOSTS = new HashSet<>();

    public static void init(final Context context) {
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                try {
                    loadFromAssets(context);
                } catch (IOException e) {
                    // noop
                }
                return null;
            }
        }.execute();
    }

    @WorkerThread
    private static void loadFromAssets(Context context) throws IOException {
        InputStream stream = context.getAssets().open(AD_HOSTS_FILE);
        BufferedSource buffer = Okio.buffer(Okio.source(stream));
        String line;
        while ((line = buffer.readUtf8Line()) != null) {
            AD_HOSTS.add(line.replace("127.0.0.1 ", ""));
        }
        buffer.close();
        stream.close();
    }

    public static boolean isAd(String url) {
        HttpUrl httpUrl = HttpUrl.parse(url);
        return isAdHost(httpUrl != null ? httpUrl.host() : "");
    }

    private static boolean isAdHost(String host) {
        if (TextUtils.isEmpty(host)) {
            return false;
        }
        int index = host.indexOf(".");
        return index >= 0 && (AD_HOSTS.contains(host) ||
                index + 1 < host.length() && isAdHost(host.substring(index + 1)));
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public static WebResourceResponse createEmptyResource() {
        return new WebResourceResponse("text/plain", "utf-8", new ByteArrayInputStream("".getBytes()));
    }
}

参考サイトとの違いは

AD_HOSTS.add(line.replace("127.0.0.1 ", ""));

この部分のみ。

hosts.txtにミュートするドメインの前にローカルループバックアドレスが記載されているので、ループバックアドレスを除外したホスト名のみを広告配信ホストの一覧として取り扱う。

ざっと説明すると、init()でhosts.txtを読み込み、AD_HOSTSに広告配信を行うホスト一覧を展開する。

isAd()で与えられたurlが広告を配信するホストであるかを判定する。


AdBlockerを初期化する

多分、WebViewを使用する各Activityでも大丈夫だと思うんだけど、参考サイトにしたがってApplicationクラスで行うようにした。

Applicationを継承した`MyApplicationクラスを作成する。すでにApplicationを継承したクラスが存在する場合は、onCreateメソッドに追記を行う。

class MyApplication: Application(){

    override fun onCreate() {
        super.onCreate()
        AdBlocker.init(this)
    }
}

AndroidManifest.xml

<application
    android:name="MyApplication"
    ....


WebViewのセットアップ

loadUrlしたページがページ上でリクエストしたURLをキャプチャし、そのURLが広告配信ホストであるかを行う判定を行う。

ここは参考サイトのコードを少し改変した。

webView.apply {
    webViewClient = object: WebViewClient(){
        override fun shouldInterceptRequest( view: WebView?, request: WebResourceRequest? ): WebResourceResponse? {
            var ad: Boolean = false
            request?.let {
                ad = AdBlocker.isAd(it.url.toString())
            }
            return if (ad) {
                AdBlocker.createEmptyResource()
            }else {
                super.shouldInterceptRequest(view, request)
            }
        }
}

先ず、参考サイト上だと名前解決できないmloadedUrlsという配列が現れるが、これは多分loadedUrlsのタイポだと思う。

オーバライドしたshouldInterceptRequestは参考サイト上だとshouldInterceptRequest( view: WebView?, url: String? )にしていたが、shouldInterceptRequest( view: WebView?, request: WebResourceRequest? )にてWebViewからのリクエストをキャプチャするようにした。

loadedUrlsはそのWebView上で一度、広告配信ホストと判断したホストをキャッシングする配列のようだが、そもそもAdBlocker.initで広告配信ホスト一覧をメモリに展開しているので不要だと判断した。一覧のレコード数は1,300件ほどあるが、パフォーマンスに影響を与える件数ではないと判断した。


このセットアップを行ったWebViewでloadUrlを行うとhosts.txt内に記載されたホストからの広告をブロックできる。

その他参考にしたページ

[Android]WebViewにAdblockを実装する - Qiita


Google Playチームから虚偽の振る舞いに関する回答が来た

質問をしてから半日ほどですごく丁寧な回答が来ました。

こういった実装をしていた

で、どういった内容の違反だったかと言うと、僕のアプリの場合、ActionBarに敢えてテキストを非表示にする処理を施していたんだけど、ご存知の通り、ActionBarに表示するデフォルトのテキストは、string.xmlapp_nameに定義されているわけ。

で、ここの値をemptyにしてたわけさ。

<string name="app_name"></string>

そうするともちろん、ActionBarのテキストの表示は消えるわけなんだけど、app_nameは端末にインストールされたランチャーアイコン下のアプリ表示名にも使用されているわけなのさ。

そうすると端末にインストールされたアプリ名表記がemptyになってしまい、アイコンのみの表示なる。

ポリシー違反の原因

ここのアプリ名表記をemptyにすることはポリシー上、許可されていなくて、どんなアプリの振る舞いかを記載できない時点で「虚偽の振る舞い」をするのではないかという指摘だと思う。

これはポリシーのドキュメントを読んでも見つけられなかったので問い合わせて良かった。といったところ。

ActionBarのテキストを非表示

ちなみにActionBarのテキストだけ消すのであればActivity側で以下のようにすれば影響ない。

supportActionBar?.title = ""
Admobについて

ポリシー違反が見つかったアプリはAdmobの広告配信も自動で停止されるらしく、これはポリシー違反を修正すれば1週間程度で再配信されるようになるらしい。

今度は別のアプリがまたGoogle Playから削除された

昨日のアプリ削除対応から束の間、またGoogle Playより別のアプリ削除のメールが届いた。

今度は虚偽の振る舞いに関するポリシーに違反していると判断されたため、Google Play での公開が停止され、削除されました。とのこと。

で、今回はアプリ停止という状態になっているらしくて、先日の違反より重いペナルティを課せられている... アカウント自体も良好なものじゃないかもしれないと書いている... ひええ。

自分としては、悪意があるつもりは全くないのですが...

で、今回は正直思い当たる節がなくて、ユーザの承諾なしにデバイスの機能にアクセスしたり、重要な情報をフィッシングするような機能ももちろんない。

警告のメールにはポリシー違反の詳細と例について、虚偽の振る舞いに関するポリシーを確認。アプリの機能が正しく伝わるようにタイトルや説明を変更とあったので、アプリのタイトルがまた適切ではなかった?ことなのか... それにしてはすごく重いペナルティのような気がするのだが。

警告は受け入れるし、意義申立てする気はないんだけど、意義申立フォームに「何が違反したか詳しい情報が欲しい」というリクエストがあったので、そちらをリクエストしてみた。

2日間ほどで返信があると書いてあるのだが果たして...

Google Playからポリシー違反でアプリが削除された話し

突然、こーんなメールがGoogleより届いた

After review, FF14タイムズ, packagename, has been removed from Google Play because your app title violates our impersonation policy. This app won’t be available to users until you submit a compliant update.

Here’s how you can submit your app for another review:

Sign in to your Play Console and change your app title so that it doesn’t imply an official relationship with an existing product or service. In this case, app title includes the name of another app or brand: FF14 (Final Fantasy XIV)
Read through the Impersonation article for more details. For example, you can note relevance at the end of your app title.
Incorrect: “Ingress Guide”
Correct: “Guide for Ingress”
Review your app to make sure it’s compliant with the Impersonation and Intellectual Property policy and all other policies listed in the Developer Program Policies. Remember that additional enforcement could occur if there are further policy issues with your apps.
Submit your app.

ええと、アプリのタイトルにFF14というブランド名が記載されていて、スクエアエニックスの公式アプリになりすましたように見えるからGoogle Playから削除したよ。ということらしい。

なりすまし、知的財産権に対するポリシーを確認して、修正したアプリを再送してね。だって。

Googleからの凡例

こういう風にすればよいらしい。

ダメなケース:イングレス ガイド

OKなケース:ガイド for イングレス

以下にあるようにアプリ名の最後に関連してるものを記載するのはOKらしい。

Read through the Impersonation article for more details. For example, you can note relevance at the end of your app title.
Incorrect: “Ingress Guide”
Correct: “Guide for Ingress”

んー。Google Playに上がってる他のFF14個人開発アプリもアウトっぽいの沢山あるんだけどねえ... 誰かに通報されたのかな...

対応と結果

で、アプリ名とランチャーアイコンとストア掲載用のスクショを何枚か差し替えて再アップロード。

22:30くらいにアップして24:00にはまだストア公開されていなくて、朝起きて確認すると新しいアプリ名でストア公開されていた。

先にメールを確認したんだけど、特に何の連絡もなく再公開されてたみたい。新規公開の時より再審査は時間がかかるみたいだね。

あと、新しいアプリでなぜかAdmobの広告が表示されなくなっている。エミュレータ上ではtest adが表示されているので、時間が経つと表示されるようになると思うが果たして...

追記

アップデート公開から一夜明けて、admob広告の表示が始まっているのを確認した。24時間くらいは平気で掲載が遅れるみたいね。

Android ホームランチャーアイコンの作成

最近のOSでは丸型のランチャーアイコンなんかにも対応しているらしく、全サイズのランチャーアイコンを作成するのは、Image Assetを利用するのが便利。

f:id:letitride:20190702143758p:plain

利用の仕方は簡単で、ランチャーアイコンとする背景透過のPNGを用意する。

Foreground Layerを選択してAsset Type:Imageとし、Pathに用意したPNGを指定すればよい。このPATHはAndroid Studio内のDrawable内でなくてもよいので、PC上の好きなところに保存した画像ファイルを指定することができる。また、このパスはデフォルトはADK側のDrawableを指していて、Android Studio内のDrawableではないので注意。最初気づかずに、Android Studio内のDrawableにリソースファイルを配置したが一向にリソースが参照できずハマってしまいました。

f:id:letitride:20190702144341p:plain

指定することで右側にランチャーアイコンのプレビューが表示される。余白を多く取りたい場合などは、透過PNGのファイルを適宜調節すればよい。

f:id:letitride:20190702144356p:plain

次にBackground Layerを指定し、背景色または背景画像を指定できる。

f:id:letitride:20190702145003p:plain

プレビューを確認後、Nextボタンで先に進むことで、すべてのサイズ及び丸型アイコンのic_launcher画像が作成される。特別なことをしない限り、画像名はデフォルトのic_launcherで作成されるので、自動でランチャーアイコンが差し代わることになる。

また、512 *512のアイコンが画像がres/ic_launcher-web.pngに作成されるのでPlay Store用の画像として使用できる。

Android WebViewにアメブロブログの画像を表示する

AndroidのWebViewでアメブロブログの画像が出ない問題。

どうやらアメブロはブラウザのDomストレージにアクセスしているようで、WebViewにストレージアクセスを設定しておく必要がある。

エラーログ

Uncaught "TypeError: Cannot read property 'getItem' of null", source

setDomStorageEnabled(true)で許可できる。

webView.settings.setDomStorageEnabled(true)