20191130のAndroidに関する記事は7件です。

【Android】Viewのスクリーンショットを撮影してギャラリーアプリで見れるようにする

背景

画面内に表示されている特定の情報を画像として保存し、後から見返せるようにしてユーザー利便性を向上させたいという要件がありました。(例えば、ゲームアプリのユーザー情報だったり、乗換案内アプリの検索結果だったり。)

この記事ではこの要件の実現方法について、下記のポイントを解説いたします。

  • スクリーンショットの撮影方法
  • ギャラリーへの反映方法

コード解説

全てのソースコードはGithubで公開しています。
クローンして手元で動かしてみると処理の流れがイメージしやすいと思います。
https://github.com/umechanhika/playground/tree/master/android/AddScreenshotToGallery

今回のポイントとなるコードは、スクリーンショットの撮影から保存までを行なっているScreenshotUtil.ktです。

ScreenshotUtil.kt
package com.example.addscreenshottogallery.ui.util

import android.content.ContentResolver
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.view.View
import androidx.core.view.drawToBitmap
import java.io.File
import java.io.FileOutputStream

object ScreenshotUtil {
  private const val DIRECTORY_NAME = "AddScreenshotToGallery"
  private const val FILE_EXTENSION = ".jpg"

  fun take(
    contentResolver: ContentResolver,
    targetView: View,
    fileName: String,
    completion: (Bitmap, Uri) -> Unit
  ) {
    // 対象のViewをBitmapに書き出します、スクリーンショットの撮影処理はこれだけです
    val bitmap = targetView.drawToBitmap()
    targetView.draw(Canvas(bitmap))

    // ギャラリーへの反映時に必要な情報を格納している値です
    // AndroidQ以降とそれより前で設定可能なKeyが異なるため、
    // ここでは共通部分のみ生成してif文内でそれぞれに必要な値を追加しています
    val contentValues = createContentValues(fileName)
    val uri: Uri

    if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) {
      contentValues.apply {
        // 画像の保存先を指定します(AndroidQより前と指定方法が異なります)
        put(
          MediaStore.Images.Media.RELATIVE_PATH,
          "${Environment.DIRECTORY_PICTURES}/$DIRECTORY_NAME"
        )
        put(MediaStore.MediaColumns.IS_PENDING, 1)
      }

      contentResolver.run {
        uri = insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
          ?: return

        // AndroidQ以降では画像の書き出し前にギャラリーへの登録(正確に言うとMediaStoreへの登録)を
        // 済ませてから該当Uriに画像を書き出します(処理の順番がAndroidQより前と異なります)
        openOutputStream(uri).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) }

        contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
        update(uri, contentValues, null, null)
      }
    } else {
      // AndroidQより前の処理では一部メソッドがDeprecatedになっていますが、
      // Q以降の処理に互換性がないためSDK_INTによって処理を分岐させています

      // スクリーンショット画像書き出し用のディレクトリ・ファイルを準備
      val directory = File(
        // ファイルの書き出し先はいくつか候補がありますが、
        // アプリを削除してもファイルが消えない、外部アプリからアクセス可能という理由から、
        // getExternalStoragePublicDirectoryを選択しています
        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
        DIRECTORY_NAME
      )
      if (!directory.exists()) directory.mkdirs()
      val file = File(directory, "$fileName$FILE_EXTENSION")

      // Bitmapをファイルに書き出します
      FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) }

      // 書き出した画像をギャラリーに反映させています
      contentResolver.run {
        contentValues.put(MediaStore.Images.Media.DATA, file.absolutePath)
        uri = insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?: return
      }
    }

    completion(bitmap, uri)
  }

  /**
   * [Build.VERSION_CODES.Q]以上とそれより前で設定できる値が違うため、ここでは共通部分のみ設定
   */
  private fun createContentValues(name: String) = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "$name$FILE_EXTENSION")
    put(MediaStore.Images.Media.TITLE, name)
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1_000)
  }
}

GIF

screen_record.gif

参考リンク

AndroidQからのストレージアクセスに関する変更点や、MediaStoreの保存処理の変更点についての情報を記載してくださっています
https://medium.com/@star_zero/android-q%E3%81%AEscoped-storage%E3%81%AB%E3%82%88%E3%82%8B%E5%A4%89%E6%9B%B4-afe41cde9f35
https://medium.com/@mag0716/scoped-storage-%E8%AA%BF%E6%9F%BB%E3%83%A1%E3%83%A2-8a13c70d3ec8

Androidのストレージに関する公式ドキュメントです(日本語ページでは一部情報が足りていない場合があるため、言語設定は英語で閲覧することをおすすめします)
https://developer.android.com/training/data-storage

AndroidQ以降とそれより前のバージョンでの実装の違いについて記載されています
https://stackoverflow.com/questions/56904485/how-to-save-an-image-in-android-q-using-mediastore

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidStudioがおかしいとき、ビルドができないときに試す手順 2019年完全版

はじめに

この記事はAndroid 初心者向け Advent Calendar 2019の19日目の記事です。
https://qiita.com/advent-calendar/2019/android_beginners

こんばんは。
みなさんはAndroidStudioで開発しているときに、文法エラーを起こしているつもりがないけれど、
エラーが出てビルドできない状態になったことはありませんか?私はあります。
そのときに試してきたことをこの記事ではまとめてみたいと思います。

なお、記事中のAndroidStudioのバージョンは3.5.2(執筆時の安定最新版)とします。

Rebuild Project

基本中の基本。
Screen Shot 2019-11-25 at 20.03.52.png

Clean Project

中間ビルドやキャッシュ ビルドファイルを消すのみ。
ただ、これだけだとデータバインディング等で作成されるファイルも消えたままなのでRebuild ProjectやMake Moduleの実行を行うこと。

Screen Shot 2019-11-25 at 20.09.01.png

Rebuild Project と Clean Projectの違い

公式リファレンスによると、
Clean Projectはすべての中間物を消す、Rebuild Projectは対象のビルド バリアントに対してClean Projectしたあと、APKを作成するらしい。

公式リファレンス
https://developer.android.com/studio/run/index.html?hl=ja#reference
データバインディングやアノテーションプロフェッサーでのクラス自動生成のみを行い、
apkまでは不要という場合、Make Moduleで十分。

BuildConfig等、一部のクラスはClean Projectでも消えないので
そういうのまで消したいときは gradleのcleanを使用する。
AndroidStudioの右端のGradleからcleanをダブルクリック
Screen Shot 2019-11-25 at 20.14.15.png

Android Gradle Pluginのビルドキャッシュ

AndroidのGradle Pluginもキャッシュを使うようです。
自分はなったことありませんが、Gradle Pluginのバージョンアップを行うと
エラーになったという場合は有効なんじゃないでしょうか。
AndroidStudioの右端のGradleからcleanBuildCacheをダブルクリック
Screen Shot 2019-11-30 at 17.14.49.png

参考
ビルド キャッシュによるクリーンビルドの高速化
https://developer.android.com/studio/build/build-cache?hl=ja

AnddroidStudioのキャッシュ

Invalidate Cashes / Restart
Screen Shot 2019-11-25 at 20.15.30.png
FileからInvalidate Cashes / Restartを選択後、

Screen Shot 2019-11-25 at 20.17.12.png
Invalidate And Restart
AndroidStudio自体がなんかおかしいときにも有効です。

以下、AndroidStudioを一旦終了すること。※以下操作注意

(AndroidStudioがキャッシュをつかんだままだと削除できない、または削除されない恐れがあるため)
また、以下から手動でファイルを消すことになるので操作には要注意。

Gradleのキャッシュ

rm -rf ~/.gradle

.gradleの中にキャッシュ用のディレクトリがあったりするんだけど、再ダウンロードされるのでまるごと削除でもOK

なお、プロジェクト直下にもあるのでここでも削除

(PROJECT_DIRに移動したあと)
rm -rf .gradle/

再作成されるのでまるごと削除でもOK。

IDEのキャッシュ

AndroidStudioのベースとなったIDEが
PROJECT_DIR/.idea
にキャッシュを使っているので、そこでも削除。
※ workspace.xml、usage.statistics.xml、tasks.xmlは必要なのでそれらのファイルは消さないこと。
※ プロジェクトによってはあえて共有しているものもあります。注意

コマンド一発で消すなら以下

(PROJECT_DIRに移動したあと)
find .idea -type f | grep -v "(workspace|usage.statistics|tasks).xml" | xargs rm

プロジェクト全体の設定ファイル

*.iml
samuraism様のサポート情報によるとGradleプロジェクトなら不要とのこと

https://support.samuraism.com/hc/ja/articles/360017715953-%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%81%AB%E4%BD%9C%E3%82%89%E3%82%8C%E3%82%8B-idea%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%81%AF%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86%E3%81%97%E3%81%A6%E5%85%B1%E6%9C%89%E3%81%97%E3%81%A6%E8%89%AF%E3%81%84%E3%81%A7%E3%81%99%E3%81%8B-

(PROJECT_DIRに移動したあと)
find . -type f | grep iml$ | xargs rm

参考

https://qiita.com/takahirom/items/af6b83edd0dc3294046f
https://qiita.com/kirimin/items/706523d637fd09152122

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

詳解AdMob

広告に関する実装をしてきて、やっと納得のいく実装ができた気がしたので備忘録的にまとめようと思います。
今後モバイルアプリに広告の導入を検討されている方の手助けになれば幸いです。
ユーザー影響大きいのでご利用は計画的に...

下記のような実装は経験できたので関連でもし質問などあれば回答できることもあるかもしれません。

  • バナー広告
    • メディエーションする
    • ヘッダービディングをラップする
    • RecyclerViewの中で使う
    • RemoteConfigと連携する
  • ネイティブ広告
    • メディエーションする
    • RecyclerViewの中で使う
    • Firebase A/B Testingと連携する
  • インタースティシャル広告
    • メディエーションする
    • RemoteConfigと連携する

AdMobについて(3行)

  • Firebase機能群の1つで、アプリ内に広告を表示することができる機能
  • 複数の広告フォーマットに対応しており、他社の広告をAdMobの中で表示するといったことも可能
  • Firebase(Google開発)提供なのでAndroidアプリではAdMobをまずは検討する(はず)

この記事で触れないもの

  • 公式ドキュメントを見てもらえば簡単にわかってしまうこと
  • AdMob以外の広告SDK仕様
  • アドテク用語
    • 全部知っておかないと開発できないわけではないと思います。
    • 本記事の用語集としてリンク先に一読はおすすめします
  • 収益に関わるパフォーマンスチューニングのTips
    • 業種やアプリの規模、採用しているアドネットワークによって全く異なるので言及しません。
  • AdMob管理画面の使い方
    • ちゃんと触ってません!
  • マルチモジュールを使った広告管理
  • CoroutineFlowを使った広告管理
    • こういうのやってみたい....
  • 実装時の開発以外でのつらみ
  • 広告SDKの内部実装について
    • 広告のSDKは大半が実装部分は難読化されていて読み解くことが難しいです。なので以下でつらつら書いていることも動作確認した時の動きだったりおそらくこういうことだろう、という考察で説明しているところも多いので実際は間違っているということもあるかもしれませんのでご理解ください。

基本編

おすすめの実装、ハマりそうなポイントなど

広告の表示

AdMobは機能自体はそこまで多くないので公式ドキュメントを一周してもらえればある程度の使い方を理解するのは簡単だと思います。テスト用の枠IDは公式で用意されているのでアプリを識別するためのAppIDが発行できればすぐ表示まで確認できると思います。

公式ドキュメント
https://developers.google.com/admob?hl=ja

実装メモ
  • AdView に使う Context は必ず Activity である必要があります
  • TestMode(広告を読み込んでも収益が発生しない状態)にするには端末ごとに異なるキーを AdRequest.Builder.addTestDevice() に設定して AdView をロードする必要があります
  • AppIDはAndroidManifest.xmlに参照がないとアプリがクラッシュします:construction_worker_tone4:
  • バナー広告のサイズ一覧(https://developers.google.com/admob/android/banner?hl=ja#banner_sizes)
Size in dp (WxH) Description Availability AdSize constant
320x50 Banner Phones and Tablets BANNER
320x100 Large Banner Phones and Tablets LARGE_BANNER
300x250 IAB Medium Rectangle Phones and Tablets MEDIUM_RECTANGLE
468x60 IAB Full-Size Banner Tablets FULL_BANNER
728x90 IAB Leaderboard Tablets LEADERBOARD
Provided width x Adaptive height Adaptive banner Phones and Tablets N/A
Screen width x 32 50 90 Smart banner
  • テスト用の枠IDで広告を読み込んで以下のように表示されたら成功です!
    • Bannerの場合

Screenshot_20191128-204011のコピー.png

  • Rectangleの場合

Screenshot_20191128-204011.png

広告はAACのViewModelで管理しない方が良い?

上でも書いたのですが、広告をインスタンス化する時はActivityContextが必要になります。
AndroidViewModelから取れるApplicationクラスのContextを使うことはできません。
また、外からActivityContextを入れてもViewModelはActivity/Fragmentよりも長生きするのでメモリーリークの原因になり得ます。

ViewModelLifecycleObserver を付けてON_DESTROYのイベントを受け取った時に広告インスタンスを破棄したりすればまあいけるのかもしれないですね。
こういう時ってActivityのインスタンスをDIでViewModelにInjectして使っても問題にならないのでしょうか?(詳しい方教えてください:joy:)

ただ、バナー広告は基本的にキャッシュされることを想定されていないので、Fragmentをまたいで広告インスタンスを使い回すのはあまりオススメしません(広告SDKが計測しているimpの定義ともずれてしまいがち)。なのでViewModelを使うメリットがあまり活かせないのかなと思っています。

広告のライフサイクルを管理する

AdViewクラスが継承しているBaseAdViewクラスにはresume() pause() destroy()が用意されていて、広告を表示しているUIのライフサイクルに応じてそれぞれのメソッドを叩く必要があります。
この3つのメソッドはAdMobに関わらず他社の大半の広告SDKでも同じように設計されている印象です。
各メソッドの役割は例えば以下のようなことがあると言えます(これらは各社SDKの仕様に左右されます。ただ、叩いておいた方がいいことには変わりないという認識は持っておいた方が良いでしょう)。

  • resume()
    • 広告のオートリフレッシュ間隔の計測を再開する
    • Impression(収益に関わる)の計測を再開する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する
  • pause()
    • 広告のオートリフレッシュ間隔の計測を中断する
    • Impression(収益に関わる)の計測を中断する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する
  • destroy()
    • 広告のインスタンスを破棄する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する

これらは記述漏れが発生しやすいので注意が必要です。
Androidでは広告の管理クラスにLifecycleObserverをセットしてライフサイクルの処理も管理クラス内部で処理しておくとイマドキな気がします。

実装メモ
  • 広告で収益を発生させるにはざっくりクリックされる多く見られるの2種類がある
    • クリックされたこと画面に見えているというのは広告のView内で計測しているので外からはいじれない
    • AdMobでは広告が見えていない時にオートリフレッシュのタイミングを迎えるとリフレッシュはスキップされるような動きをする
    • destroy()を呼んだ広告インスタンスはリロードできなくなるので作り直すしかありません

メディエーションはどう動くか

メディエーションというのは、AdMob以外の広告SDKから表示される広告コンテンツをAdMobのAdViewの中で表示できる機能です。例えば、TwitterFacebookAmazonなど広告SDKを提供している企業が抱える広告コンテンツもAdMobの中で表示できるようになります。
もちろんAdMob自身が持っている広告コンテンツも様々な広告配信ネットワークから価値があるものが優先して表示されるのですが、ただこれだとAdMobの配信システムに大きく依存しているので、アドネットワーク内に競合を増やして収益獲得の効率化を目指そうよというものです(ヘッダービディングも思想は同じようなものだと認識してますが違うのかな?)。

具体的なやり方を説明する前に、なぜこういうことができるのかというと、簡単に言ってしまえば、

バナー広告はそれぞれViewGroupを継承しているのでaddView(view: View)みたいなことをすれば中身を上書きできるよ

というだけです。
(ちなみにAdMobのネイティブ広告はViewGroupではなくて、UnifiedNativeAdというデータクラスを使っていて、パーツごとに情報をセットするコンバート処理を挟むことでメディエーションを実現していると言えます)

それでは実装を解説していきましょう。

1. メディエーション実装クラスを作る

CustomEventBannerを継承していることで他社の広告SDKのメディエーションをしたいんだなとAdMob側に認識してもらえる準備になります。

package sample.android.ad

class SampleBannerAdCustomEvent : CustomEventBanner {

    /**
     * @param customEventBannerListener 各社SDKのAdViewを読み込んで必要なタイミングで対象のイベントをコールする
     * @param serverParameter AdMob管理画面で設定できる文字列が返る(枠IDを入れておくことが多いです)
     * @param adSize AdMob側で設定した広告のサイズを受け取り、メディエーション時の読み込む広告のサイズと合わせるために使う
     * @param mediationAdRequest AdRequest.Builder()内で設定したKeywordやBirthday情報を取れる(使ったことないです)
     * @param customEventExtras AdRequest.Builder()内で設定したBundleの値を渡せる(枠IDを入れておくことが多いです)
     */
    override fun requestBannerAd(context: Context, customEventBannerListener: CustomEventBannerListener,
                                 serverParameter: String, adSize: AdSize, mediationAdRequest: MediationAdRequest,
                                 customEventExtras: Bundle?) {
        // 取り扱いたい広告SDKのAdViewを読み込む
    }

    override fun onResume() {
        // 各社SDKの`Resume`時の処理を書く
    }

    override fun onPause() {
        // 各社SDKの`Pause`時の処理を書く
    }

    override fun onDestroy() {
        // 各社SDKの`Destroy`時の処理を書く
    }
}

1つ注意なのですが、Kotlinで実装する場合、customEventExtrasはリクエスト時に設定されていない場合はnullで返ってくるのでNullableで定義しておきましょう:raising_hand_tone1:

ちなみにCustomEventBannerListener は以下のようなメソッドを持っています。

public interface CustomEventBannerListener extends CustomEventListener {
    void onAdLoaded(View adView);
}

public interface CustomEventListener {
    void onAdFailedToLoad(int errorCode);

    void onAdOpened();

    void onAdClicked();

    void onAdClosed();

    void onAdLeftApplication();
}

requestBannerAd内で行なった他社SDKの読み込み処理やイベントに応じていずれかのメソッドを叩く必要があります。
例えば、広告を読み込んで成功した時に広告のインスタンスをonAdLoaded(View adView)に突っ込んであげるとAdMobのAdViewaddView(view: View)されるようなイメージです。
逆に読み込み失敗時にonAdFailedToLoad(int errorCode)が呼ばれるとAdMobが違うメディエーション先へ広告を取りに行くというわけです。

実装メモ
  • Logcatを見る感じ、広告の読み込み時に枠IDごとに設定されている管理画面上のClassNameの文字列とメディエーション実装クラスの絶対パスが一致しているものがあるかをチェックしている挙動も見て取れました。
    • Adsでフィルターすると良い感じです

2. AdMob管理画面でClassNameとParameterを設定する

1で作ったメディエーション実装クラスのrequestBannerAdが呼ばれるためには、枠IDごとにどんなメディエーションが設定されているかを管理画面上で定義しておく必要があります。それを紐付けるのがClassNameParameterという概念です。例えば1のようなクラスを作った場合は対象の枠IDに対して以下のように設定することになります。

設定項目 ClassName Parameter(オプショナル)
Android sample.android.ad.SampleBannerAdCustomEvent 文字列を1つだけ設定可能
iOS SampleBannerAdCustomEvent 文字列を1つだけ設定可能

iOSは実装クラスのクラス名だけでいけるみたいですね:ok_woman:

AdMobの管理画面で設定してみてください!
スクリーンショット 2019-12-02 14.44.09.png

上記2点で最低限のメディエーションの実装は完了です!
あとは管理画面で複数のメディエーションに対してどういう順序でメディエーションするとより収益化が見込めるかという視点で運用しながらチューニングをしていくと良いでしょう。

ただ、最初の頃はこの実装だけでなぜ動くのか不思議なはずです。先ほど作ったSampleBannerAdCustomEventFind Usageしてもどこからも使われていません。
なんで参照されていないクラスのrequestBannerAdが呼ばれるねん!!!
と突っ込みたくなりますが、実装自体は合っています。
内部実装が読み取れないので具体的な解説は難しいのですが、リフレクションとかでうまいことやったりしてるんでしょう(適当)

補足ですが、requestBannerAdのそれぞれの引数と実装者が外から設定した場合の値の紐付き方を考察混じりになりますが書いてみます。

  • context : AdViewをイニシャライズした時に入れたContext
  • customEventBannerListener : SDKの内部実装で勝手に紐付く(これだけが謎紐付き)
  • serverParameter : AdMob管理画面で設定したParameter
  • adSize : AdViewをイニシャライズした時にセットしたAdSize
  • mediationAdRequest : AdViewのload時にセットしたAdRequestの一部のパラメーター(LocationやKeywords)
  • customEventExtras : AdViewのload時にセットしたAdRequestの中のaddCustomEventExtrasBundleで対象のクラスにセットしたBundle

実装メモ
  • カスタムイベントとメディエーションはだいたい同じ意味です
  • Parameterにはメディエーション先で使いたい何かしらの文字列を管理画面から送りたい要件があるときに使うと良いです
    • 画面ごとにAdMobの枠IDを発行して収益性を比較したいのでメディエーション先の枠IDも同様に発行して柔軟に変更させたい。その場合はParameterにそれぞれの枠IDを設定して管理画面側で動的に読み込む枠IDを変更できる
    • バナー広告とレクタングル広告で同じ広告SDKに対してメディエーションしたい。その場合は同じ実装クラスを参照しつつ、指定フォーマットは引数のadSizeから取り、Parameterに対象の枠IDをセットすることでメディエーション実装を使い回しできる
  • メディエーション実装クラスをリリース後にリファクタリングの途中でパッケージやクラス名を変えてしまうと動かなくなる可能性があるので注意
    • JavaからKotlinへのコンバートは問題ないです

3. メディエーション実装クラスに外から値を受け渡ししたい

実際に運用していて困るのが、リリース後に管理画面の設定やメディエーション内の挙動を変えたい場合です。
枠IDやメディエーションは一度リリースしてしまうと古いバージョンのアプリの動きを担保したまま運用していくのが難しくなります。これはアプリのソースコード上でも管理画面上でも同様です。
どれだけ意識して開発していても広告SDK側の破壊的なアップデートや先方との契約の都合など変更せざるを得ないケースはいくつか挙げられるでしょう。
ではメディエーションの実装に関してはどのような柔軟性を持たせておけばいいのか。方法が2つあります。

  • 管理画面で設定したParameterを受け取れるserverParameterを使うようにする
  • RemoteConfigを使って必要なパラメーターをアプリ側でcustomEventExtrasに詰めて渡す

customEventExtrasを使った値の受け渡しは以下のように実装します。

val adRequest = AdRequest.Builder()
            .addCustomEventExtrasBundle(SampleBannerAdCustomEvent::class.java, Bundle().apply {
                // Bundle型にできればなんでも可
                putString("key", value)
            }).build()

adView.loadAd(adRequest)

絶対にやらないほうがいいのは決め打ちで固定の枠IDや特定の文字列をメディエーション実装クラス内で使ってしまうということで、上の2つのやり方に関しては実装しやすい方を選択すると良いでしょう。

ただ、個人的には、serverParameterを使う方法は初めて実装を見た人がどうやって動いているのか理解しづらいと思うので、RemoteConfigを使った処理に寄せてしまった方が良いと考えています。
customEventExtrasに詰める箇所はJavaもしくはKotlinで書くことになるので実装が追いやすいし、SampleBannerAdCustomEventのような実行クラスへの参照を作ることができます。

実装メモ
  • 管理画面もしくはRemoteConfigから渡す文字列はJson形式にしておくと何かと安心です
    • 例えば、古いバージョンに影響を与えずに、新しいバージョンで管理画面もしくはRemoteConfigの値を参照したいような要件に対応できます(特に管理画面では1つしか文字列を設定できないので)
  • 一部のメディエーション設定を消したい場合、古いアプリにメディエーション実装クラスが残っていても、ためらわず消してもらっても大丈夫です
    • メディエーション時に内部でエラーが起きた時の動きと同様に他のメディエーション先にリクエストが優先されます
    • 実装クラスのパスと管理画面のクラスネームが間違っていても同じ動きになります
  • 逆にメディエーション設定を増やしたい場合も新しいバージョンでしか認識されないので古いバージョンを使っているユーザーでクラッシュや意図しない挙動が起きるケースは少ないはずです
  • メディエーションしようとしたのにメディエーション先でエラーとなり、他に表示できる広告がない場合は白板になるのを防ぐためデフォルト設定ではAdMobの広告が表示されるようになります(フィラーの設定で変更可能)
  • メディエーション先の広告SDKのオートリフレッシュ機能はOFFにしておいてAdMobのオートリフレッシュに委ねておくのがいいです
    • ちらつきの原因になり得るのと、無駄なimpが意図せず増えてしまう要因になり得ます

応用編

実務でつまづいたこと、運用してみて良かったこと、分かったことなど

メディエーション実装には大きく2種類あった

どういうことかというと、メディエーションするにあたり必要な実装は1つしかないのですが、自前で実装する場合すでに実装されたjar/aarファイルやGradleで取り込めるライブラリが提供されている場合があります。比較してみると以下のような違いがあります。

実装方式 自前実装 提供品利用
使い方 CustomEventBannerを継承したクラスを作る Gradleで参照する
クラスネーム設定 変動(作成クラスの絶対パス) 常に固定
手軽さ ?‍♂️ ?‍♂️
拡張性 ?‍♂️ ?‍♂️
バグの原因の発見容易性 ?‍♂️ ?‍♂️

提供品は圧倒的に手軽で、ライブラリを入れるだけでアプリ側の作業が完了したりするのですが、内部実装をいじれないので要件によってはうまく適合しないケースもあります。
ですので、すでに提供品がある広告SDKであれば一旦そちらを使ってみて、うまくいかなかった時に自前で実装するというアプローチが良いように思います(最近はもうメディエーションを自前実装する機会は多くないかなと思います)。
最近はGoogleがAdMobに組み込むための各社SDK用のアダプターを公開してくれているので実装がとても楽になってきていると言えます。

AdMobのメディエーションアダプター一覧
https://developers.google.com/admob/android/mediate?hl=ja

これまでに自前実装をしたケース
  • メディエーションしたい広告SDKがアダプターを提供していなかった
  • メディエーション先で特定のキーワードを設定して広告をロードしたかったが、提供されているアダプターが対応していなかった
  • ファーストリクエストの広告がNO_AD(在庫がない)を返してきたらフォーマットの異なる広告を読み込みたい要件があった
    • 広告フォーマットが違うのでメディエーションで対応できないケース

実装メモ
  • アダプターを利用する場合は管理画面に設定するClassNameが他社のパッケージのパスになり違和感がありますが間違ってはないのでご安心ください

広告テストがしづらいので枠IDの管理を考える

これは運用してみると出てくる問題の一部で、例えば以下のような問題がありました。対策と合わせて書いてみます。

1. TestModeを使用せずにDev環境やQAで広告をテストしすぎると垢BAN食らって2週間ほどアカウント停止されがち問題

AdMobでTestModeを使うには、一度ビルドしてログから一意なKeyを取得してきて都度設定する必要があります。
QAのために社内全ての端末のIDを管理するのは大規模なプロジェクトほど骨が折れます。
また、判定の仕組みは全く不明ですが、StoreにリリースされているApplicationIdと異なるビルドやDebugビルドからTestModeを設定せずに広告を読み込みすぎる(おそらくクリックすると更に良くないです)とAdMobのアカウントが停止されて問答無用で広告も読み込めなくなるし管理画面もいじれなくなります。
本番にリリースされている状態で垢BANされると相当クリティカルな問題ですので、広告の挙動テストではせめてReleaseビルドを使う不正に収益を得ることがまずいのでクリックするテストは避けるは心がけておいたほうが良いでしょう。Debugビルドでは広告を表示しない仕組みがあると安心ですね。

実装メモ
  • 他社のSDKではTestModeはTrue/Falseだけで切り替えられるSDKも多い
  • AdMobに理由を添えて問い合わせしても例外なく2週間程度停止されるようです
  • 今はもう起きてないかもですが、TestModeをONにしているとメディエーションのテストはできなかったです

2. 広告機能のリリース後に広告に関する機能追加をしようとした時、本番の枠IDの設定をテストのためにいじりづらい問題

これはもう解決策は明確で、枠IDにもProduction、Staging、Developの概念を取り入れると良いです。
例えば自社では下記のような思想で運用していました。

  • Production枠ID : 基本ProductionReleaseのようなVariantでしか参照しないようにする
  • Staging枠ID : ここで機能開発を行う。eCPMも自由に調整してメディエーション配信の優先順位も操作できるようにする
  • Development枠ID : 公式のテスト用IDを使うか、抽象化した広告インターフェースにテスト用のダミーViewを実装したようなクラスを差し替えれるようにしてAdMobを実行クラスで使わないようにする(AdMobのAdViewの代わりにImageViewを表示しておく的な)

3. メディエーション先の広告が表示されているのかわかりづらい問題

2の案でStaging枠でメディエーション設定は自由に弄れるようになるのですが、実際にメディエーションが成功しているかを目視で判断するのは難しいケースもあります。広告出稿側がAdMob以外にも同じ広告を提供しているケースがあるためです。そんな時は以下のような判断方法を検討してみてください。

  • AdView.getMediationAdapterClassName()を使う
    • メディエーション先にリクエストが渡る時はLogcatでも確認はできます
  • クリックした時にブラウザが開く時のURLをチェックする
    • 広告のスクショと一緒に広告担当者に確認すれば出稿している広告かどうか判断してもらえるケースが多いです
    • Production以外は垢BANにドキドキしながらクリックすることになりますが、、、

実装メモ
  • TwitterやAmazonは見た目に特徴があるので判断しやすい
  • TwitterやAmazonなどは公式アプリが入っていないとメディエーションが反応しないケースもあります

4. 意図したメディエーションを試すのに手間がかかる問題

2で解説したStaging枠を更に拡張させるやり方の提案です。
管理画面で設定を都度変更するのは反映ラグ(1~2時間程度)もあり、複数人からの一斉の要望に答えづらい問題があります。
QAを置いている企業だと連携コストがかかってしまうので、弊社ではStaging枠IDをメディエーション分だけ複数発行するというやり方を取っています。
1つの枠IDに対して例えば3つのメディエーションが紐づいていると目視での確認がしんどくなってくるので、
1つの枠IDに対して1つのメディエーションというセットを複数作って参照する枠IDに応じて表示される広告が切り替わるような仕組みにしています。
アプリのデバッグ設定などで動的に枠IDを切り替えられるようにしておくと即反映できるし便利ですね!

実装メモ
  • 設定してすぐの時はある程度広告リクエストを送らないとメディエーションが反応しないので辛抱強くリクエスト送ってみましょう

白板を防ぐ

広告コンテンツが読み込めない、在庫がない、広告の読み込み中などの状態でユーザーに広告のViewが見えてしまうと真っ白に見えるようになっています。これは白板(しろいた/はくばん)と呼ばれていて、これが見えている状態というのは、ユーザー側にも収益的に見てもベネフィットが限りなく小さいです。
SDK側の仕様に大きく左右されるということもあり、この白板をゼロにすることは難しいので、なるだけ起きない方法を考えていきたいと思います。

1. 広告の読み込み中もしくはエラーになった場合は広告表示領域を隠す

読み込み中かどうかはAdMobでは、AdView.isLoadingというメソッドがあり、状態を取得することが可能です。またエラー状態もAdListenerのonAdFailedToLoadで受け取れるので状態管理の仕組みを作れば簡単に判定できます。広告表示領域はデフォルトGoneにしておいて、読み込みに成功したらVisibleに切り替えるのが良さそうです。

private fun loadAd() {
    binding.adContainer.isGone = true
    ad?.also {
        it.setCallback(object : Ad.Callback {
            override fun onSuccess(view: View) {
                bindAdView(binding.adContainer, view)
                binding.adContainer.isVisible = true
            }

            override fun onFailure(errorCode: Int) {
                binding.adContainer.isGone = true
            }
        })
        if (it.isLoading()) {
            return
        }
        it.load()
    }
}

ちなみにこの辺の実装は各広告SDKの仕様によって書き方が異なるので、状態管理を内部で行うことも含めて下記のようなInterfaceを切っておくと便利です。

interface Ad {

    fun isLoading(): Boolean

    fun isOnError(): Boolean

    fun getView(): LiveData<View>?

    // 必要に応じて
    fun getNativeAd(): LiveData<UnifiedNativeAd>?

    fun resume()

    fun pause()

    fun destroy()

    fun setCallback(callback: Callback?)

    fun load()

    interface Callback {

        fun onSuccess(view: View?)

        fun onFailure(errorCode: Int)
    }
}

実装メモ
  • オートリフレッシュが有効な場合、AdListenerをセットしていた場合はオートリフレッシュ間隔でコールバックが返ってきます。成功->失敗のようなケースに広告が白板になることもあるので考慮できると良いです。
  • Adの実行クラスはDaggerなどでManagerクラスに注入できるようにすればテストが簡単にできそうです。

2. プリフェッチしておく

バナー広告でキャッシュがNGと書いてあるものの、見えることがほぼ確実な広告を先に読み込んでおいてもバチは当たらないと思います。プリフェッチを実装した公式サンプルもあります。
ただこのサンプルおそらく問題があるケースがあって、オートリフレッシュが有効な広告の場合だとプリフェッチがループしてしまいます。

private fun loadAdForInitialize(adLoadCount: Int) {
    if (adLoadCount >= adList.size) {
        return
    }
    adList?.also {
        if (it.isEmpty() || !isInitialized) {
            // 読み込み中に画面が破棄されていたら止めたい
            return
        }
        val ad = it[adLoadCount]
        val callback = object : Ad.Callback {
            override fun onSuccess(view: View) {
                ad.setCallback(null)
                loadAdForInitialize(adLoadCount + 1)
            }

            override fun onFailure(errorCode: Int) {
                ad.setCallback(null)
                loadAdForInitialize(adLoadCount + 1)
            }
        }
        ad.setCallback(callback)
        ad.load()
    }
}

サンプルコードのように広告実装を抽象化している場合はCallbackを、AdMobを使っている場合はAdListenerをnullにしてあげましょう。また、非同期処理がループしているので画面のライフサイクルに応じて処理を止めてあげる仕組みがあるとさらに良いかなと思います。

3. (任意)リスト内で広告を取り扱う場合の白板防止策

リスト内で広告を扱う実装方法はまだ確立したベストプラクティスがないので実装方法によりますが、プリフェッチを採用していて、かつList型で広告を管理している場合、以下のようなUtilメソッドを作っておくと便利です。

fun getIncrementedIndex(targetIndex: Int, adListSize: Int): Int {
    var nextIndex = targetIndex + 1
    if (nextIndex >= adListSize) {
        // 配列のサイズを超えたら0に戻す
        nextIndex = 0
    }
    return nextIndex
}

fun getValidAd(adList: List<Ad>, targetIndex: Int): Ad? {
    val targetAd = adList[targetIndex]
    if (!targetAd.isOnError()) {
        // 白板でなければそのまま利用する
        return targetAd
    }
    var nextAd: Ad? = null
    var incrementedIndex = getIncrementedIndex(targetIndex, adList.size)
    // adListを一周して有効な広告がないか探してくる
    while (incrementedIndex != targetIndex) {
        val incrementedAd = adList[incrementedIndex]
        val isLoadedAd = !incrementedAd.isLoading() && !incrementedAd.isOnError()
        if (isLoadedAd) {
            nextAd = incrementedAd
            break
        }
        incrementedIndex = getIncrementedIndex(incrementedIndex, adList.size)
    }
    return nextAd
}

これで白板が見えてしまう可能性は大きく下がるはずです!
これでもAdnullで返ってきてしまう場合は広告表示領域をGONEにすると良いでしょう。

開発Tips/小ネタなど

  • Firebase A/B Testingを使えば広告のパフォーマンス比較ができる
    • 例えばバナー広告とネイティブ広告
    • AdMobを使っていると収益性やインプレッション数もFirebase上でわかるので比較しやすい
    • TargetingにPredictionsを活用できて非常にイケている
  • 広告の詳細な実装はInterfaceで隠してテスタブル設計にしておく
    • 最近のdex.fmでも話題に上がってたので少しテンション上がりました:relaxed:
  • AdViewはリロード時に再利用できるか、作り直すべきか
    • AdViewはdestroy()を呼ばない限りはリロードが可能みたいです
    • リングバッファ管理できますね!
    • 広告SDKによっては作り直さないとダメなやつもあるっぽいです
  • 広告は比較的プレミアム機能と絡むことが多いので、destroy時とcreate時の処理を外から叩けるようにしておくと良いと思います
    • たまーーーーーに行儀の悪い広告コンテンツだと、画面に見えていなくても、広告の読み込みが成功していると画面を奪ったり、動画広告であれば裏で音声を流し始めたりするものもあるので、プリフェッチの取り扱いには気をつけるのと、例えばプレミアム機能が有効なユーザーはそもそも広告を読み込まない、という設計にするべきです。

まとめ

  • 実装設計するにあたり、LifecyclesとLiveDataはAdMobと相性がいい気がした
  • ViewModelで広告インスタンスを管理するのは玄人でなければやめておく
  • RemoteConfigどんどん使っていきましょう
  • A/B TestingとPredictionsの連携がアツい
  • できるだけ公式の実装から外れないようにする
  • メディエーションは各社SDKがアダプターを提供してくれているかをまず探す
  • バナー広告はキャッシュしすぎないようにする
  • 自前で実装するメディエーション用のクラスはパッケージを安易に移動させないように気をつける
  • メディエーションをたくさん付けるよりもパフォーマンスチューニングをうまくやるほうが有益
  • RequestクラスとViewクラスをそれぞれ使いまわしてもいいかはSDKごとに確認する
  • ユーザーに悪影響がある分、良い実装をして出来る限りストレスは軽減してあげる努力をしていきましょう

最後に

長らく読んでいただきありがとうございました!
最初は取っつきづらい領域なんですが、実装方法を理解してしまえば拡張もしやすく、色々な要件にも柔軟に対応できて収益の最大化に貢献できるはずなのでネット上にもっと情報が増えてくれると良いなぁと思っています。

広告のSDKはAndroidXに対応されてなかったりJavaのままだったりSDKによって挙動が大きく違ったりと最新のトレンドと比べると遅れているなぁと感じますが、技術としてはとても面白いと思うのでどんどん盛り上がってくれるといいなぁと切に願っています!

MoPubはAndroidX対応をすでに諦めているっぽい:ghost:
https://github.com/mopub/mopub-android-sdk/issues/311

そして、リストの中で広告を表示させたい要件ってあると思うんですが、公式のBannerRecyclerViewSampleはリストのデータをObject型で管理してたりと苦しい感じだし、
ViewHolderにAdViewを設定してonBindのタイミングで読み込みを開始するだけだと読み込みのレイテンシで白板の状態が見えてしまって見栄えが良くないです。

ということで良い感じにリスト内でも動くサンプルを作ってみました。

https://github.com/FujiKinaga/AdMobSample

まだこれがベストプラクティスな実装ではないと思っているのでアドバイスなども頂けると大変嬉しく思います!
個人的にはRecyclerViewPoolをうまく扱えるともっと良い感じになるのかなと思ったりします。

Qiita楽しい:santa:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]ExoPlayer でハードウェアのデコーダーを利用しないようにする

はじめに

ExoPlayer ではメディアにあわせて一番良いデコーダーを再生時に自動的に選択してくれるので、
ExoPlayer を利用する時は、デコーダー周りは特に何も気にしないで良いことになっています。

そのはずだったのですが 端末に搭載されるハードウェアのデコーダーの不具合?なのか
再生時に動画が途切れたり、上手くループ再生できなくなるなどの現象に遭遇しました。

その時に ExoPlayer に対してはハードウェアのデコーダーを利用しないように
設定を施したのでその方法を共有したいと思います。

動作を確認するためのサンプルアプリを作る

本記事では次のような構成のアプリを作成していると仮定して設定を進めていきます。

Nov-29-2019 23-20-18.gif

ExoPlayer をインストールする

build.gradle(app)
android {
    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'com.google.android.exoplayer:exoplayer:2.10.8'
        
}

インターネットにアクセスするのでパーミッションを許可しておく

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="kaleidot725.exoplayersample">
    <uses-permission android:name="android.permission.INTERNET"/></manifest>

ExoPlayer の PlayerView をレイアウトする

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/player_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:show_buffering="when_playing"
        app:show_shuffle_button="true"/>

</FrameLayout>

ExoPlayer をセットアップする

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val view = findViewById<PlayerView>(R.id.player_view)
        val player = ExoPlayerFactory.newSimpleInstance(this).apply {
            val uri = Uri.parse("https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4")
            val dataSource = DefaultDataSourceFactory(this@MainActivity, Util.getUserAgent(this@MainActivity, "ExoPlayerSample"))
            val videoSource = ProgressiveMediaSource.Factory(dataSource).createMediaSource(uri)
            prepare(videoSource)
        }
        view.player = player
    }
}

デバイスに搭載されているハードウェアのデコーダーを探す

次のコードでデバイスに搭載されているエンコーダーとデコーダーを確認します。

    MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos.forEach { codecInfo ->
        Log.d("CODEC_INFO", "media ${codecInfo.name} ")
    }

次のような感じでメディアごとのデコーダーとエンコーダーが出力されます。
これは Pixel3 XL で実行したものですが c2.android.*OMX.google.* と2種類のエンコーダーとデコーダーがあります。

2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media c2.android.avc.decoder 
2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media OMX.google.h264.decoder 
2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media c2.android.avc.encoder 
2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media OMX.google.h264.encoder

調べたところc2.android.* が Pixel3 に搭載されたハードウェアのエンコーダーとデコーダー、
OMX.google.* が Android OS で実装されたソフトウェアのエンコーダーとデコーダーを指しているようです。
今回はハードウェアのデコーダーを利用したくないので、c2.android.avc.decoder が対象になります。

ハードウェアのデコーダーを利用しないようにする

ExoPlayer では MediaCodecSelector クラスを通して、どんなデコーダーが ExoPlayer で利用できるかを取得してます。
通常は MediaCodecSelector.DEFAULT の実装を利用しており、端末に搭載される全てのデコーダーを返すようになっています。

MediaCodecSelector.java
  MediaCodecSelector DEFAULT =
      new MediaCodecSelector() {
        @Override
        public List<MediaCodecInfo> getDecoderInfos(
            String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder)
            throws DecoderQueryException {
          return MediaCodecUtil.getDecoderInfos(
              mimeType, requiresSecureDecoder, requiresTunnelingDecoder);
        }

        @Override
        public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
          return MediaCodecUtil.getPassthroughDecoderInfo();
        }
      };

今回はこの MediaCodecSelector クラスをカスタマイズすることで
ハードウェアのデコーダーを ExoPlayer で利用しないようにします。

次のように getDecoderInfo で利用できるデコーダーを取得する処理に
namec2.android.*で始まるものを取り除く処理を追加します。

        val customCodecSelector = object : MediaCodecSelector {
            override fun getDecoderInfos(mimeType: String?, requiresSecureDecoder: Boolean, requiresTunnelingDecoder: Boolean): MutableList<MediaCodecInfo> {
                return MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder, requiresTunnelingDecoder).toMutableList().filter {
                    !it.name.startsWith("c2.android.")
                }.toMutableList()
            }

            override fun getPassthroughDecoderInfo(): MediaCodecInfo? {
                return MediaCodecUtil.getPassthroughDecoderInfo()
            }
        }

そして ExoPlayer を生成するときにこのクラスのインスタンスを設定すれば、
ExoPlayer では c2.android.* 以外のデコーダーを利用して動画を再生するようになります。

        val view = findViewById<PlayerView>(R.id.player_view)
        val trackSelector = DefaultTrackSelector()
        val renderersFactory = DefaultRenderersFactory(this).apply {
            setMediaCodecSelector(customCodecSelector)
        }
        val player = ExoPlayerFactory.newSimpleInstance(this, renderersFactory, trackSelector).apply {
            val uri = Uri.parse("https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4")
            val dataSource = DefaultDataSourceFactory(this@MainActivity, Util.getUserAgent(this@MainActivity, "ExoPlayerSample"))
            val videoSource = ProgressiveMediaSource.Factory(dataSource).createMediaSource(uri)
            prepare(videoSource)
        }
        view.player = player

うまく設定できていれば指定したハードウェアを利用せずに動画を再生していることがわかるような次のログが出でてきます。

2019-11-30 11:58:49.588 3862-3941/kaleidot725.exoplayersample I/MediaCodec: [OMX.google.h264.decoder] setting surface generation to 3954689

おわりに

ハードウェアのデコーダーを利用しないようにしましたが、
ソフトウェアのデコーダーを利用するとCPU使用率が上昇するので注意が必要だったりします。
そのため本来であれは ExoPlayer にデコーダーの選択など全てを任せるのが安心できます。

ですがデバイスだとうまくデコードできないという場面に直面した場合は本実装が有効なので、
デバイス依存の問題には直面してしまったときはこういった実装でしのげるかなと思います。

参考記事

本記事は Can we tweak hardware/software decoding in Exoplayer? を参考にしました、
詳しくはこっちで議論されているので確認してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android で OpenCV のサンプル mixedprocessing を試す

AndroidStudio で OpenCV をインポートする
の続きです。

概要

Android 用 OpenCV には、
8個のサンプルが同封されている。

そのうち mixedprocessing は、
Native API を使うサンプルサンプルです。
前回の記事で作った Android のライブラリをインポートする手法は使えないので、
アプリごとに Android 用のOpenCV をインポートしてビルドする。

tutorial-2-mixedprocessing

OpenCVの Java API と Native API の2つの方法で、
カメラからの画像を変換して、表示するサンプルです。

メニューは下記の4つ
- Preview RGBA
カメラ画像をカラーで描画する
- Preview Gray   
カメラ画像をグレースケールで描画する
- Canny 
キャニー ( Canny) 法によりエッジを検出して描画する
- Find feature   
Native API により特徴点を検出して、小さな赤い丸を描画する

アプリを作成する

まず、下記の記事をお読みください。

(1) 新規のプロジェクトを作成する
File -> New -> New Project -> Empty Activity

(2) OpenCV の Android SDK をインポートする
File -> New -> Import Module

(3) 依存関係を設定する
File -> Project Structure

(4) opencv/build.gradle を修正する

(5) AndroidManifest にカメラのアクセス許可を追加する。

(6) レイアウトファイルに、JavaCamera2View を配置する。

(7) MainActivity.java を作成する。
samples/tutorial-2-mixedprocessing/src/org/opencv/samples/tutorial2/Tutorial2Activity.javaを
app/src/main にコピーして、
MainActivity.java にリネームする。

MainActivity.java
public class MainActivity extends CameraActivity implements CvCameraViewListener2 {

    public native void FindFeatures(long matAddrGr, long matAddrRgba);

    public void onCreate(Bundle savedInstanceState) {
        // 省略
    }


    public void onResume() {
        // 省略
    }



    public boolean onCreateOptionsMenu(Menu menu) {
        // 省略
    }

    public boolean onOptionsItemSelected(MenuItem item) {
        // 省略
    }


    public void onCameraViewStarted(int width, int height) {
        // 省略
    }


    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        // 省略
    }


    private BaseLoaderCallback  mLoaderCallback = new BaseLoaderCallback(this) {
        public void onManagerConnected(int status) {
            // 省略
    }
};

(8) native コードを作成する。
samples/tutorial-2-mixedprocessing/jni ディレクトリを
app/src/main にコピーして、
cpp にリネームする。
プロジェクトのファイル構成に合わせてする。

jni_part.cpp
/**
 * org_opencv_samples_tutorial2_Tutorial2Activity を
 * jp_ohwada_android_opencv48_MainActivity に変更する
 */

JNIEXPORT void JNICALL Java_org_opencv_samples_tutorial2_Tutorial2Activity_FindFeatu

(9) app/build.gradleを変更する

app/build.gradle
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a'
        }
        externalNativeBuild {
            cmake {
                arguments "-DOpenCV_DIR=" + project(':opencv').projectDir + "/native/jni",
"-DANDROID_TOOLCHAIN=clang",
"-DANDROID_STL=c++_shared"
                targets "mixed_sample"
            }

    buildTypes {
    externalNativeBuild {
        cmake {
            path 'src/main/cpp/CMakeLists.txt'
        }
    }

(10) cmake を修正する
Make するとエラーが出るので、
cmake を修正する

(11) Makeする
Build -> Make Projec

cmake を修正する

Makeすると、
エラーが出るので、
プロジェクトのファイル構成に合わせて
下記の2つの cmake を修正する

  • opencv/native/jni/abi-armeabi-v7a/OpenCVConfig.cmake
  • opencv/native/jni/abi-armeabi-v7a/OpenCVModules-release.cmake

CMake Warning at /Users/ohwada/AndroidStudioProjects/Opencv48/opencv/native/jni/abi-armeabi-v7a/OpenCVConfig.cmake:129
OpenCV: Include directory doesn't exist:
'/Users/ohwada/AndroidStudioProjects/Opencv48/sdk/native/jni/include'.

OpenCVConfig.cmake
# /sdk/native/ を /opencv/native/ に変更する

set(__OpenCV_INCLUDE_DIRS "${OpenCV_INSTALL_PATH}/sdk/native/jni/include")

CMake Error at /Users/ohwada/AndroidStudioProjects/Opencv48/opencv/native/jni/abi-armeabi-v7a/OpenCVModules.cmake:245 (message):
The imported target "libcpufeatures" references the file
"/Users/ohwada/AndroidStudioProjects/Opencv48/sdk/native/3rdparty/libs/armeabi-v7a/libcpufeatures.a"
but this file does not exist.

OpenCVModules-release.cmake
# /sdk/native/ を /opencv/native/ に変更する

set_target_properties(libcpufeatures PROPERTIES
  IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "C"
  IMPORTED_LOCATION_RELEASE "${_IMPORT_PREFIX}/sdk/native/3rdparty/libs/armeabi-v7a/libcpufeatures.a"
  )

list(APPEND _IMPORT_CHECK_FILES_FOR_libcpufeatures "${_IMPORT_PREFIX}/sdk/native/3rdparty/libs/armeabi-v7a/libcpufeatures.a" )

アプリを実行する

下記の3つが同封されていることが確認できる。
- libopencv_java4.so
- libc++_shared.so
- libmixed_sample.so
opencv48_analyze_apk.png

アプリが起動すると、
カメラのアクセス許可を要求する。
opencv48_permission.png

スクリーンショット

Preview RGBA (カラー)
opencv48_rgba.png

Preview Gray (グレースケール)
opencv48_gray.png

Canny(エッジを検出)
opencv48_canny.png

Find feature(特徴点検出)
opencv48_find._featurepng.png

  

サンプルコードをgithub に公開した。
https://github.com/ohwada/Android_Samples/tree/master/Opencv48

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidStudioでFileInputStreamを使うときの文字化けについて

実機デバッグ時に文字列をメールアプリの本文に渡したら、文章の間に文字化け記号が挟まった。

AndroidStudioの基本的な設定

ここを見る。

http://k-hiura.cocolog-nifty.com/blog/2015/07/androidstudio-b.html

FileInputStreamで文字コードをUTF-8指定にする

文字化けするコード

public String readFile(String fileName){
        try {
            FileInputStream stream = openFileInput(fileName);
            byte[] buf = new byte[1024];
            stream.read(buf);
            return new String(buf);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return "readFile() is wrong";
        } catch (IOException e){
            e.printStackTrace();
            return "readFile() is wrong";
        }
    }

文字化け解消コード

public String readFile(String fileName){
    try {
        FileInputStream stream = openFileInput(fileName);
        InputStreamReader streamReader = new InputStreamReader(stream, "UTF-8");
        BufferedReader bufferedReader = new BufferedReader(streamReader);
        String s;
        String col = "";

        while ((s = bufferedReader.readLine()) != null){
            byte[] b = s.getBytes();
            s = new String(b, "UTF-8");
            col += s;

        }
        return col;

    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return "readFile() is wrong";
    } catch (IOException e){
        e.printStackTrace();
        return "readFile() is wrong";
    }
}

FileInputStreamから文字列を取得するときに文字コードがUTF-8以外になって余計なものが付いてきたっぽい。

参考
https://www.sejuku.net/blog/20746
https://qiita.com/aaaanwz/items/1554afadc44cb5cf3a80

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlutterでAndroidアプリをリリースした時に解決したつまづきポイントメモ。

検証日時

2019年11月28日

全体的なリリースまでの流れと、本記事の概要

公式チュートリアルです。
https://flutter.dev/docs/deployment/android

英文がよくわからないという場合は、Chromeで日本語翻訳にかければほぼほぼ内容わかります。対応している単語がわからないときは翻訳を切って見比べると解決します。

このチュートリアルを進める時にエラーが出た部分にフォーカスして、その解決法(自分のケースでうまくいった操作)などを共有します。(iOS 版も編集中です。完成したら公開予定です。)

署名構成の部分でコピペ忘れでデバッグ版ビルドに署名していたのを解決

チュートリアル「Configure signing in gradle」の「With the signing configuration info:」の部分について

signingConfig signingConfigs.releaseは初期設定だとdebugになっててコピペ忘れてGoogle Play Consoleアップ時に「ビルドの署名がデバッグバージョンにされてます」という趣旨のエラー出してしまいました。

content_copy
   signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile file(keystoreProperties['storeFile'])
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release // ここを変更!

       }
   }

「Enabling Proguard」の「Step 2 - Enable obfuscation and/or minification」でのエラー

チュートリアルのそのままだとエラーでした。
the file you created in step 1:

android {

    ...

    buildTypes {

        release {

            signingConfig signingConfigs.release

            minifyEnabled true
            useProguard true

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }
    }
}

下記の参考記事に沿って変更しました。そしたらうまくいきました。
the file you created in step 1:

android {

    ...

    buildTypes {

        release {

            signingConfig signingConfigs.release

            minifyEnabled false // ここを変更!
            useProguard true

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }
    }
}

参考記事
https://qiita.com/chooyan_eng/items/022721c09ddfc758476e

$flutter build appbundle

でアプリバンドル作成(.aabファイル)、アップ
上記のポイントでエラー回避されていれば無事にリリースビルドが得られると思います。

Google App Console でのポイント

アプリの署名について。

3つ選択肢がありましたが、
自分は、Google Play アプリ署名を選びました。

(pepk.jarを使う暗号化も一度試しましたが、

なぜか下記のエラー報告のようにうまくいかなかったのですが、
https://stackoverflow.com/questions/52269806/error-running-pepk-app-signing-tool-at-command-line-java-jar-pepk-jar

上記Google Play アプリ署名を選択したら不要になったので結果オーライです。)

なお、証明書ダウンロードなど色々出てきますが、Flutterプロジェクトに新たに追加操作するなどの必要はないです。当初に行った鍵生成→署名つきリリースビルドで署名に関する操作は完了しています。

Consoleでチェックマークがある項目は全部埋めないとリリースできない。

「アプリの署名」や、「ストアの掲載情報」、「コンテンツのレーティング」などチェックマークが薄く灰色で示されている部分は入力必須項目なので埋めます。

下記が参考になりました。
https://www.cotegg.com/blog/?p=1807

コードアップからリリースまでかかった時間計測

テスト版app.bundleをアップしてから審査完了(問題なしストレート)で4日間
その後同じバンドルを製品版公開してから、1日後に審査完了でGooglePlay一般公開完了

という感じでした。

なおリリースしてすぐの現在GooglePlayでの検索には引っかからない状態です。ので、直リンクでTwitterやブログなどの広報記事から誘導する必要があるようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む