- 投稿日:2019-08-29T23:15:26+09:00
Android Studioで最初のプロジェクト作成を行うときにやること
What's?
Android Studioで新規プロジェクトを作ろうとしたとき、実装上でやることになりそうなことをまとめていく。
AndroidManifestで発生するLintエラーを無効化する
ディープリンクを利用しない場合、AndroidManifestのApplicationタグ全体で表示されるGoogleAppIndexingWarning で表示されるワーニングは無効化してしまいたい。
以下をapp/build.gradle
に追記してやればOKandroid { lintOptions { // 現状はディープリンクを利用しないので、無効化 disable 'GoogleAppIndexingWarning' }他の内容も随時追記予定
- 投稿日:2019-08-29T19:14:30+09:00
既存アプリ(apk)を Android App Bundle(aab) でアップロードできるようにする
概要
以前から
こちらが出ていたのですが、
新規アプリの aab は簡単だけど既存アプリはめんどくさい
というのをどこかで見ていたのでなかなか取りかかれずにいましたが、
時間もできたのでようやく重い腰を上げて対応したのでそのときの手順を書いておきます。Android App Bundle について公式は コチラ。
ザッとみたところ、いい事しかないですよね!!
それにしても サイズ 43.9% 削減って・・・さっさとやれよ「Google でアプリ署名鍵の管理、保護を行う」 を有効にする
※ 一度有効にすると無効にすることはできないそうなので注意してください
事前準備
アップロード公開鍵証明書を作成する
あとで使うので先に作っておきます。
アップロード鍵を生成する
公式にしっかり手順 書いてありますがいちおう以下に手順を書いておきます。
- Android Studio - Build - Generate Signed Bundle / APK... から作成します。
- Android App Bundle を選択します。
- Create new... からアップロード鍵を生成します。
- 必要な情報を入力して OK します。
- OK を押すとアップロード鍵は生成はされていたのですがぼくは以下のエラーが表示されました。
- お薦めされているので言われるがままに表示されているコマンドを実行します。
$ keytool -importkeystore -srckeystore ./upload-keystore.jks -destkeystore ./upload-keystore.jks -deststoretype pkcs12 ソース・キーストアのパスワードを入力してください: 別名********のエントリのインポートに成功しました。 インポート・コマンドが完了しました: 1件のエントリのインポートが成功しました。0件のエントリのインポートが失敗したか取り消されました Warning: "./upload-keystore.jks"がNon JKS/JCEKSに移行されました。JKSキーストアは"./upload-keystore.jks.old"としてバックアップされます。どうやらうまく動いたようなので、これでアップロード鍵の生成は終了です。
アップロード鍵からアップロード公開鍵証明書を生成する
こちらはコマンドを実行するだけです。
$ keytool -export -rfc -keystore ./upload-keystore.jks -alias ******** -file upload_certificate.pem キーストアのパスワードを入力してください: 証明書がファイル<upload_certificate.pem>に保存されました以上で事前準備は終了です。
ちゃんと aab でアップロードできるか確認するのでココで作ったアップロード鍵で aab ファイルを生成しておきます。
(Export encrypted key for 〜 は チェックしなくて 大丈夫です。)アプリ署名の秘密鍵をアップロードする
- Google Play Console - アプリの署名 を表示します。
- 「Java Keystore から鍵をエクスポートしてアップロードする」を選択します。
「PEPK ツール」を押して
pepk.jar
をダウンロードします。画面に表示されているコマンドを太字の部分を修正して実行します。
$ java -jar pepk.jar --keystore=[使用している keystore の path] --alias=******** --output=encrypted_private_key_path --encryptionkey=****************
- 「アプリ署名の秘密鍵」からアップロードします。
以上でアプリ署名の秘密鍵のアップロードは終了です。
アップロード公開鍵証明書をアップロードする
必須ではないとのことですが
推奨
なのでこちらもやっておいたほうが良さそうです。
- アプリ署名の秘密鍵と同じ画面で 「(省略可)セキュリティを強化する〜」 を押します。
「アップロード公開鍵証明書」から アップロード公開鍵証明書を作成する で生成したアップロード公開鍵証明書(手順通りなら upload_certificate.pem) をアップロードします。
「完了」を押します。
- こちらが表示されているか確認します。
以上で「Google でアプリ署名鍵の管理、保護を行う」 が有効になりました!?
おつかれさまでした!アップロードできるようになったか確認する
念のためちゃんとアップロードできるか確認しておきます。
アップロードの方法は apk のときと同じです!Google がアプリの署名をごにょごにょやってくれているので少しアップロードに時間かかるようになった気がします。(といっても微々たるものですが)
無事にアップロードできました!
アプリのサイズも半分ぐらいに!
参考
- 投稿日:2019-08-29T11:40:07+09:00
Androidアプリでバージョンアップ通知を組み込んでテストする方法
背景
コーチング会社でニュースアプリをiPhone/Androidで作っています。
2年ほどアップデートしていなかったそうで、今後はバージョンアップをユーザに通知したいという要望がありました。AndroidではHockeyAppという通知お知らせSDKが入っていましたが、2019年の秋あたりに仕様が変わる?みたいな記述があったので、別のものを入れることにしました。
in-app updates
上記の代わりにGoogle純正の「in-app updates」というものをドキュメントを見て実装しました。
Support in-app updates はこちら必要環境
Android 5.0 (API level 21) 以上
Play Core library 1.5.0以上ドキュメントは英語だけど頑張って読んで、
ソースをそのままコピペ。更新のタイプ
ImmediateとFlexibleのタイプが選べて、
Immediateは強制アップデート(×ボタンがあるので強制とはいえない)
Flexibleはアップデートしないボタンが選べる重要!テストの仕方
GooglePlayに上がっているアプリのバージョンが19として、
バージョン18のAPKを作って実機に入れれば動くと思ったら動かない!!AndroidStudioでの署名付きAPKの作り方はこちら
GooglePlayConsole
https://developer.android.com/distribute/console?hl=JAにログインして、内部テストを実施しないといけませんでした!
GooglePlayConsoleでの内部テスト
https://support.google.com/googleplay/android-developer/answer/3131213?hl=ja
- consoleにて内部テストするユーザーのメールアドレスを登録
- 現在GooglePlayに上がっているアプリ(v19)のバージョン+1(v20)のAPKを作成してconsoleにてリリースを作成する
- 1.で登録したアドレスでログインしたGooglePlayStore経由か、オプトインURLをユーザに共有して、実機にインストールする
- バージョンアップ通知をテストしたいので、2.のバージョン+1(v21)したAPKを作成し、consoleにてリリースを作成する
- 実機にインストールしたv20のアプリを起動すると、下記画面が出る
出ると嬉しい!
- 投稿日:2019-08-29T09:36:44+09:00
ローカルサーバーにLAN内の外部機器からアクセスする手順についての隠された真実
最初に
僕はある日突然、Laptopで立てたPythonの簡易サーバーにスマホからアクセスできないトラブルにぶつかりました。それがあまりにも複雑で長期に渡って僕を苦しめたので、こうして記事にまとめたという次第です。
隠された真実。(キリッ)(煽りタイトルやめろ)面倒で語られない手順の真実
ぶっちゃけ、外部端末からのアクセスの実現は、巷で紹介されている程度を遥かに越えて大変に面倒なものでした。(しかし、あなたは以下の手順に従うだけで済みますね。素晴らしい!)
(ファイアウォールやアンチウィルスを切ってセキュリティホールを作る必要などどこにもなかったんや……(※効果には個人差があります))
前提
- ローカルサーバの設定に問題はない
- 機器とPCが同じLANに接続している
- あなたはそのルーターの設定に必要なユーザー名とパスワードを知っている。
- Windows 10
- この親切なサイトを見てもうまくいかなかった。
上記の内容についてはここでは説明しません。もしこれらの前提に引っかかるようなら、Google先生に聞いてみましょう。大半はいい手引が見つかるはずです。
そして、teratailに寄せられたこの回答は今回話す内容をざっくりと説明してくれています(これをもっと早く見つけていれば、あんなに苦しむ羽目にはなかったろうに……)。
当記事は、おおまかに言って、この回答の内容がよくわからない、またはおおよそわかったが解決には至らない、という人たちを対象にしています。以下は上記のteratailの回答をよく読んでから読み進めてください。いますぐ試すべきこと
まず、接続しているLANが「プライベート」設定になっているか確認しましょう。
WindowsならタスクバーのWifiアイコンをクリックし、対象のLANのプロパティ
を開けば目前に当該の設定が現れます。それでもうまくいかない?
- ネットワークの共有オプションの、プライベートの設定は
ネットワーク探索を有効にする
が選択されているか?
- タスクバーのWifiアイコン(右クリック)
ネットワークとインターネットの設定を開く
共有オプション
プライベート
- Windows Defenderの
すべての着信接続をブロックします。
の選択が外されているか?
- タスクバーのWifiアイコン(右クリック)
ネットワークとインターネットの設定を開く
Windows ファイアウォール
プライベート ネットワーク
このコンピューターのインターネット接続を通しての接続を許可する
にチェックが入っているか?
- タスクバーのWifiアイコン(右クリック)
ネットワークとインターネットの設定を開く
アダプターのオプションを変更する
Wi-Fi
(右クリック) ※ただし、有線接続の場合はローカルエリア接続
プロパティ
共有
タブプライバシーセパレーター機能をオフにする
本題。僕には最後まで気づけなかった原因について。
プライバシーセパレーター機能ってなに?この文献に目を通したら、お手持ちのルーターのセットアップをやりなおしましょう。
これはルーターによって設定方法がまちまちなので、各自でしらべてもらうしかありません。
ただし、以下のような傾向が考えられます。
1. ルーターと有線接続しないと設定できない
2. たいていは192.168.0.1で設定できる
3. SSIDに~GW, ~AWなどの、接尾辞らしきwがついている回線にはデフォルトでセパレート機能がオンになっていて、逆にそのwがない回線ではオフになっている場合がある。こうした場合、プリンターとPC両方をwのついていない回線につなぐことで設定変更しなくても解決する。以上で僕がサーバー接続を実現した手順の紹介を終わります。
他にめぼしい原因があれば教えて下さい。ありがとうございました。で、どこがどう「隠された真実」なの?
- プライバシーセパレーターの設定はググってもなかなか出てこない
- いわんや、ネットワーク設定からプライバシーセパレーターまで包括的に説明してくれる文献ももちろんなかった
- みなWindows ファイアウォールやアンチウィルスソフトウェアをオフにせよと口酸っぱく言うがそんな必要などなかったんや……
- 投稿日:2019-08-29T02:46:20+09:00
AudioTrackで正弦波の単音を再生させるためのデータ型選択
はじめに
AudioTrackを使って生成した正弦波をリアルタイムで再生するアプリを作成。再生できるようになったが再生したい周波数と違う周波数の音(雑音)も再生されていたため、修正。
正弦波生成
周波数を引数として正弦波を生成し、byte型の値の範囲-128~127で正規化。
getSound/** * 正弦波生成 * @param frequency 音の周波数 * @return 音声データ */ public byte[] getSound(double frequency) { frequency = frequency / 2; // byteバッファを作成 byte[] buffer = new byte[bufferSize]; double max = 0; double[] t = new double[buffer.length]; double hz = frequency / this.sampleRate; for(int i = 0; i < buffer.length; i++) { t[i] = Math.sin(i * 2 * Math.PI * hz); if(t[i] > max) { max = t[i]; } } double trans = 127 / max; for(int i = 0; i < buffer.length; i++) { buffer[i] = (byte)Math.round(t[i]*trans); } return buffer; }結果
20kHzの音を再生した。これ自体は非可聴音だが、可聴音も再生されてしまっている。この可聴音は雑音なので除去したい。
修正したこと
音声データをbyte型ではなくshort型にしただけ。short型の値の範囲-32768~32767で正規化することで正弦波の分解能が高くなった。
getSoundShort/** * 正弦波生成 * @param frequency 音の周波数 * @return 音声データ */ public short[] getSoundShort(double frequency) { double[] value = new double[sampleRate]; double max = 0.0; for(int i = 0; i < sampleRate; i++) { value[i] = Math.sin(2.0 * Math.PI * frequency * i / sampleRate); if(value[i] > max) { max = value[i]; } } short[] buffer = toShort(value, max); return buffer; } /** * double型をshort型に変換(-32768~32767で正規化) * @param val double型音声データ * @param max 最大値 * @return short型音声データ */ public short[] toShort(double[] val, double max) { double trans = 32767 / max; short[] buf = new short[sampleRate]; for(int i = 0; i < sampleRate; i++) { buf[i] = (short)Math.round(val[i] * trans); } return buf; }修正結果
同様に20kHzを再生したところ、可聴域の雑音がなくなり、20kHzの単音になった。
おそらく
詳しいことはわからないが、byte型の-128~127の範囲だと分解能が低く、誤差が生じて雑音が発生したと思われる。
AudioTrackの書込処理(writeメソッド)にはbyte型、short型、float型を用いることができるが、float型は値の範囲等が複雑なため試していない。
- 投稿日:2019-08-29T00:22:22+09:00
Gson で Date を json 化すると秒単位の精度に丸められる問題と対応
はじめに
Gson とは、Google 製のライブラリで、Java(Kotlin)のオブジェクトと json の相互変換が容易に可能になります。
しかし、Gson のデフォルトのままで、java.util.Date
を json 化すると、ミリ秒の情報が落ち、秒単位に丸められていました。Gson の使い所
本を表す Java(Kotlin) オブジェクトを
data class Book(val name: String, val price: Int)を
val gson = Gson() val book = Book("ABC", 123) val json = gson.toJson(book) // {"name":"ABC","price":123} val restoredBook = gson.fromJson(json, Book::class.java) // restoredBook == book -> trueの様に、json 化したり、json からオブジェクトに復元することが簡単に行えます。
ハマったところ
java.util.Date
を持つオブジェクトを Gson で json 化したら、秒単位に丸められました。先ほどの
Book
に出版日をDate
型で追加します。data class Book(val name: String, val price: Int, val publishDate: Date)先ほどと同様に json 化すると、
{"name":"ABC","price":123,"publishDate":"Aug 28, 2019 11:25:30 PM"}追加した
publishDate
も json のプロパティに出力されています。
しかしながら、 元の Date が持っていたミリ秒の情報が欠落して json に出力されています。具体的な値を記すと、
val gson = Gson() val book = Book("ABC", 123, Date()) // Book(name=ABC, price=123, publishDate=Wed Aug 28 23:25:30 GMT+09:00 2019) val time1 = book.time // 1567002330140 val json = gson.toJson(book) // {"name":"ABC","price":123,"publishDate":"Aug 28, 2019 11:25:30 PM"} val restoredBook = gson.fromJson(json, Book::class.java) // Book(name=ABC, price=123, publishDate=Wed Aug 28 23:25:30 GMT+09:00 2019) val time2 = restoredBook.time // 1567002330000 // restoredBook == book -> falseとなります。
肝となる差分は、time1 // 1567002330140 time2 // 1567002330000です。
そのため、assertEquals(book, restoredBook)のような Unit test は失敗します。
(Unit test を書いてて、この差分に気がつきました。)原因
Gson のデフォルトの
DateTypeAdapter
が、Date
を DateFormat した文字列で serialize / deserialize しているため。
Gson ライブラリ内のコードがこちら↓DateTypeAdapter.javapublic final class DateTypeAdapter extends TypeAdapter<Date> { ... private synchronized Date deserializeToDate(String json) { for (DateFormat dateFormat : dateFormats) { try { return dateFormat.parse(json); } catch (ParseException ignored) {} } try { return ISO8601Utils.parse(json, new ParsePosition(0)); } catch (ParseException e) { throw new JsonSyntaxException(json, e); } } @Override public synchronized void write(JsonWriter out, Date value) throws IOException { if (value == null) { out.nullValue(); return; } String dateFormatAsString = dateFormats.get(0).format(value); out.value(dateFormatAsString); } ... }対策
案1:Date を gson に渡さない
先ほどの
data class Book(val name: String, val price: Int, val publishDate: Date)を
Date
->Long
にして、data class Book(val name: String, val price: Int, val publishDate: Long)としてしまうのも1つの手です。
(String でも良いかもしれません。)ただ、Long にしても String にしても、どういう形式(ミリ秒単位や秒単位など)なのかをきちんと定める必要が出てきてしまいます。。。
案2:自作の DateTypeAdapter を使う
Gson には、
TypeAdapter
を登録してその型の serialize / deserialize 方法をカスタマイズすることができます。
Gson のデフォルトのDateTypeAdapter
が、Date
を DateFormat した文字列で serialize / deserialize いましたが、Date#getLong()
の値で serialize / deserialize するようにします。MyDateTypeAdapter.ktclass MyDateTypeAdapter : TypeAdapter<Date?>() { override fun write(out: JsonWriter?, value: Date?) { if (value == null) { out?.nullValue() return } out?.value(value.time) // Date#getLong() で serialize } override fun read(`in`: JsonReader?): Date? { if (`in`?.peek() == JsonToken.NULL) { `in`.nextNull() return null } return Date().apply { time = `in`?.nextString()?.toLong() ?: 0 } // deserialize } }を作り、
GsonBuilder#registerTypeAdapter
でDate
とMyDateTypeAdapter
を関連づけます。val gson = GsonBuilder() .registerTypeAdapter(Date::class.java, MyDateTypeAdapter()) .create()このようにすると、
Date
のままでも serialize / deserialize でミリ秒までの情報がきちんと保持されるようになります。注意
いずれの方法でも、 serialize / deserialize は問題なく行えるはずです。
しかしながら、既にAug 28, 2019 11:25:30 PM
の形式でどこか(端末ローカルなど)に serialize されている 従来形式のデータを deserialize することができなくなってしまいます。Gson 内の
DateTypeAdapter
では、いくつかのDateFormat
を持っています。DateTypeAdapter.javaprivate synchronized Date deserializeToDate(String json) { for (DateFormat dateFormat : dateFormats) { try { return dateFormat.parse(json); } catch (ParseException ignored) {} } try { return ISO8601Utils.parse(json, new ParsePosition(0)); } catch (ParseException e) { throw new JsonSyntaxException(json, e); } }これと同様に、いくつかの parse 方法を用意しておいて、parse に失敗したら次の方法で parse するなどの策を取っておく必要があります。
まとめ
- Gson で、そのまま Date を json 化すると、秒単位の精度に丸められる
- 回避方法はあるが、導入や変更にタイミングには注意が必要
- Gson に限らず、他のライブラリでも serialize / deserialize で意図しないデータの変化が発生しないか気をつけた方が良い