- 投稿日:2019-11-30T17:22:14+09:00
【Android】Viewのスクリーンショットを撮影してギャラリーアプリで見れるようにする
背景
画面内に表示されている特定の情報を画像として保存し、後から見返せるようにしてユーザー利便性を向上させたいという要件がありました。(例えば、ゲームアプリのユーザー情報だったり、乗換案内アプリの検索結果だったり。)
この記事ではこの要件の実現方法について、下記のポイントを解説いたします。
- スクリーンショットの撮影方法
- ギャラリーへの反映方法
コード解説
全てのソースコードはGithubで公開しています。
クローンして手元で動かしてみると処理の流れがイメージしやすいと思います。
https://github.com/umechanhika/playground/tree/master/android/AddScreenshotToGallery今回のポイントとなるコードは、スクリーンショットの撮影から保存までを行なっているScreenshotUtil.ktです。
ScreenshotUtil.ktpackage 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
参考リンク
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-8a13c70d3ec8Androidのストレージに関する公式ドキュメントです(日本語ページでは一部情報が足りていない場合があるため、言語設定は英語で閲覧することをおすすめします)
https://developer.android.com/training/data-storageAndroidQ以降とそれより前のバージョンでの実装の違いについて記載されています
https://stackoverflow.com/questions/56904485/how-to-save-an-image-in-android-q-using-mediastore
- 投稿日:2019-11-30T17:20:02+09:00
AndroidStudioがおかしいとき、ビルドができないときに試す手順 2019年完全版
はじめに
この記事はAndroid 初心者向け Advent Calendar 2019の19日目の記事です。
https://qiita.com/advent-calendar/2019/android_beginnersこんばんは。
みなさんはAndroidStudioで開発しているときに、文法エラーを起こしているつもりがないけれど、
エラーが出てビルドできない状態になったことはありませんか?私はあります。
そのときに試してきたことをこの記事ではまとめてみたいと思います。なお、記事中のAndroidStudioのバージョンは3.5.2(執筆時の安定最新版)とします。
Rebuild Project
Clean Project
中間ビルドやキャッシュ ビルドファイルを消すのみ。
ただ、これだけだとデータバインディング等で作成されるファイルも消えたままなのでRebuild ProjectやMake Moduleの実行を行うこと。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をダブルクリック
Android Gradle Pluginのビルドキャッシュ
AndroidのGradle Pluginもキャッシュを使うようです。
自分はなったことありませんが、Gradle Pluginのバージョンアップを行うと
エラーになったという場合は有効なんじゃないでしょうか。
AndroidStudioの右端のGradleからcleanBuildCacheをダブルクリック
参考
ビルド キャッシュによるクリーンビルドの高速化
https://developer.android.com/studio/build/build-cache?hl=jaAnddroidStudioのキャッシュ
Invalidate Cashes / Restart
FileからInvalidate Cashes / Restartを選択後、
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プロジェクトなら不要とのこと(PROJECT_DIRに移動したあと) find . -type f | grep iml$ | xargs rm参考
https://qiita.com/takahirom/items/af6b83edd0dc3294046f
https://qiita.com/kirimin/items/706523d637fd09152122
- 投稿日:2019-11-30T16:34:36+09:00
詳解AdMob
広告に関する実装をしてきて、やっと納得のいく実装ができた気がしたので備忘録的にまとめようと思います。
今後モバイルアプリに広告の導入を検討されている方の手助けになれば幸いです。
ユーザー影響大きいのでご利用は計画的に...下記のような実装は経験できたので関連でもし質問などあれば回答できることもあるかもしれません。
- バナー広告
- メディエーションする
- ヘッダービディングをラップする
- RecyclerViewの中で使う
- RemoteConfigと連携する
- ネイティブ広告
- メディエーションする
- RecyclerViewの中で使う
- Firebase A/B Testingと連携する
- インタースティシャル広告
- メディエーションする
- RemoteConfigと連携する
AdMobについて(3行)
- Firebase機能群の1つで、アプリ内に広告を表示することができる機能
- 複数の広告フォーマットに対応しており、他社の広告をAdMobの中で表示するといったことも可能
- Firebase(Google開発)提供なのでAndroidアプリではAdMobをまずは検討する(はず)
この記事で触れないもの
- 公式ドキュメントを見てもらえば簡単にわかってしまうこと
- AdMob以外の広告SDK仕様
- アドテク用語
- 全部知っておかないと開発できないわけではないと思います。
- 本記事の用語集としてリンク先に一読はおすすめします
- 収益に関わるパフォーマンスチューニングのTips
- 業種やアプリの規模、採用しているアドネットワークによって全く異なるので言及しません。
- AdMob管理画面の使い方
- ちゃんと触ってません!
- マルチモジュールを使った広告管理
- やりたいことに近いことを実現されている記事がありました
- こういうのやってみたい....
- CoroutineFlowを使った広告管理
- こういうのやってみたい....
- 実装時の開発以外でのつらみ
- やっていて色々思うことはありましたが割愛します。ただ、とても共感したスライドがあったので置いておきますね。
- https://speakerdeck.com/ryokosuge/guang-gao-shi-zhuang-falseku-simi
- 広告SDKの内部実装について
- 広告のSDKは大半が実装部分は難読化されていて読み解くことが難しいです。なので以下でつらつら書いていることも動作確認した時の動きだったりおそらくこういうことだろう、という考察で説明しているところも多いので実際は間違っているということもあるかもしれませんのでご理解ください。
基本編
おすすめの実装、ハマりそうなポイントなど
広告の表示
AdMobは機能自体はそこまで多くないので公式ドキュメントを一周してもらえればある程度の使い方を理解するのは簡単だと思います。テスト用の枠IDは公式で用意されているのでアプリを識別するためのAppIDが発行できればすぐ表示まで確認できると思います。
公式ドキュメント
https://developers.google.com/admob?hl=ja
実装メモ
AdView
に使うContext
は必ずActivity
である必要があります- TestMode(広告を読み込んでも収益が発生しない状態)にするには端末ごとに異なるキーを
AdRequest.Builder.addTestDevice()
に設定してAdView
をロードする必要があります- AppIDは
AndroidManifest.xml
に参照がないとアプリがクラッシュします- バナー広告のサイズ一覧(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の場合
- Rectangleの場合
広告はAACのViewModelで管理しない方が良い?
上でも書いたのですが、広告をインスタンス化する時は
ActivityContext
が必要になります。
AndroidViewModel
から取れるApplication
クラスのContext
を使うことはできません。
また、外からActivityContext
を入れてもViewModelはActivity/Fragmentよりも長生きするのでメモリーリークの原因になり得ます。
ViewModel
にLifecycleObserver
を付けてON_DESTROY
のイベントを受け取った時に広告インスタンスを破棄したりすればまあいけるのかもしれないですね。
こういう時ってActivityのインスタンスをDIでViewModelにInjectして使っても問題にならないのでしょうか?(詳しい方教えてください)ただ、バナー広告は基本的にキャッシュされることを想定されていないので、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
の中で表示できる機能です。例えば、Amazon
など広告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
で定義しておきましょうちなみに
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のAdView
にaddView(view: View)
されるようなイメージです。
逆に読み込み失敗時にonAdFailedToLoad(int errorCode)
が呼ばれるとAdMobが違うメディエーション先へ広告を取りに行くというわけです。
実装メモ
- Logcatを見る感じ、広告の読み込み時に枠IDごとに設定されている管理画面上のClassNameの文字列とメディエーション実装クラスの絶対パスが一致しているものがあるかをチェックしている挙動も見て取れました。
Ads
でフィルターすると良い感じです2. AdMob管理画面でClassNameとParameterを設定する
1
で作ったメディエーション実装クラスのrequestBannerAd
が呼ばれるためには、枠IDごとにどんなメディエーションが設定されているかを管理画面上で定義しておく必要があります。それを紐付けるのがClassName
とParameter
という概念です。例えば1
のようなクラスを作った場合は対象の枠IDに対して以下のように設定することになります。
設定項目 ClassName Parameter(オプショナル) Android sample.android.ad.SampleBannerAdCustomEvent 文字列を1つだけ設定可能 iOS SampleBannerAdCustomEvent 文字列を1つだけ設定可能 iOSは実装クラスのクラス名だけでいけるみたいですね
上記2点で最低限のメディエーションの実装は完了です!
あとは管理画面で複数のメディエーションに対してどういう順序でメディエーションするとより収益化が見込めるかという視点で運用しながらチューニングをしていくと良いでしょう。ただ、最初の頃はこの実装だけでなぜ動くのか不思議なはずです。先ほど作った
SampleBannerAdCustomEvent
はFind Usage
してもどこからも使われていません。
「なんで参照されていないクラスのrequestBannerAd
が呼ばれるねん!!!」
と突っ込みたくなりますが、実装自体は合っています。
内部実装が読み取れないので具体的な解説は難しいのですが、リフレクションとかでうまいことやったりしてるんでしょう(適当)補足ですが、
requestBannerAd
のそれぞれの引数と実装者が外から設定した場合の値の紐付き方を考察混じりになりますが書いてみます。
context
: AdViewをイニシャライズした時に入れたContextcustomEventBannerListener
: SDKの内部実装で勝手に紐付く(これだけが謎紐付き)serverParameter
: AdMob管理画面で設定したParameteradSize
: AdViewをイニシャライズした時にセットしたAdSizemediationAdRequest
: 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 }これで白板が見えてしまう可能性は大きく下がるはずです!
これでもAd
がnull
で返ってきてしまう場合は広告表示領域をGONE
にすると良いでしょう。開発Tips/小ネタなど
- Firebase A/B Testingを使えば広告のパフォーマンス比較ができる
- 例えばバナー広告とネイティブ広告
- AdMobを使っていると収益性やインプレッション数もFirebase上でわかるので比較しやすい
- Targetingに
Predictions
を活用できて非常にイケている- 広告の詳細な実装はInterfaceで隠してテスタブル設計にしておく
- 最近のdex.fmでも話題に上がってたので少しテンション上がりました
- 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対応をすでに諦めているっぽい
https://github.com/mopub/mopub-android-sdk/issues/311そして、リストの中で広告を表示させたい要件ってあると思うんですが、公式のBannerRecyclerViewSampleはリストのデータを
Object型
で管理してたりと苦しい感じだし、
ViewHolderにAdViewを設定してonBind
のタイミングで読み込みを開始するだけだと読み込みのレイテンシで白板の状態が見えてしまって見栄えが良くないです。ということで良い感じにリスト内でも動くサンプルを作ってみました。
https://github.com/FujiKinaga/AdMobSample
まだこれがベストプラクティスな実装ではないと思っているのでアドバイスなども頂けると大変嬉しく思います!
個人的にはRecyclerViewPool
をうまく扱えるともっと良い感じになるのかなと思ったりします。Qiita楽しい
- 投稿日:2019-11-30T12:14:09+09:00
[Android]ExoPlayer でハードウェアのデコーダーを利用しないようにする
はじめに
ExoPlayer ではメディアにあわせて一番良いデコーダーを再生時に自動的に選択してくれるので、
ExoPlayer を利用する時は、デコーダー周りは特に何も気にしないで良いことになっています。そのはずだったのですが 端末に搭載されるハードウェアのデコーダーの不具合?なのか
再生時に動画が途切れたり、上手くループ再生できなくなるなどの現象に遭遇しました。その時に ExoPlayer に対してはハードウェアのデコーダーを利用しないように
設定を施したのでその方法を共有したいと思います。動作を確認するためのサンプルアプリを作る
本記事では次のような構成のアプリを作成していると仮定して設定を進めていきます。
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.ktclass 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.javaMediaCodecSelector 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
で利用できるデコーダーを取得する処理に
name
がc2.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? を参考にしました、
詳しくはこっちで議論されているので確認してみてください。
- 投稿日:2019-11-30T11:21:43+09:00
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.javapublic 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.gradledefaultConfig { 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 Projeccmake を修正する
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
スクリーンショット
サンプルコードをgithub に公開した。
https://github.com/ohwada/Android_Samples/tree/master/Opencv48
- 投稿日:2019-11-30T02:02:24+09:00
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
- 投稿日:2019-11-30T00:28:12+09:00
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やブログなどの広報記事から誘導する必要があるようです。