20190829のAndroidに関する記事は6件です。

Android Studioで最初のプロジェクト作成を行うときにやること

What's?

Android Studioで新規プロジェクトを作ろうとしたとき、実装上でやることになりそうなことをまとめていく。

AndroidManifestで発生するLintエラーを無効化する

ディープリンクを利用しない場合、AndroidManifestのApplicationタグ全体で表示されるGoogleAppIndexingWarning で表示されるワーニングは無効化してしまいたい。
以下を app/build.gradle に追記してやればOK

android {
    lintOptions {
        // 現状はディープリンクを利用しないので、無効化
        disable 'GoogleAppIndexingWarning'
    }

他の内容も随時追記予定

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

既存アプリ(apk)を Android App Bundle(aab) でアップロードできるようにする

概要

以前から

スクリーンショット 2019-08-29 15.34.29.png

こちらが出ていたのですが、新規アプリの aab は簡単だけど既存アプリはめんどくさい というのをどこかで見ていたのでなかなか取りかかれずにいましたが、
時間もできたのでようやく重い腰を上げて対応したのでそのときの手順を書いておきます。

Android App Bundle について公式は コチラ

ザッとみたところ、いい事しかないですよね!!

それにしても サイズ 43.9% 削減って・・・さっさとやれよ

「Google でアプリ署名鍵の管理、保護を行う」 を有効にする

※ 一度有効にすると無効にすることはできないそうなので注意してください

事前準備

アップロード公開鍵証明書を作成する

あとで使うので先に作っておきます。

アップロード鍵を生成する

公式にしっかり手順 書いてありますがいちおう以下に手順を書いておきます。

  • Android Studio - Build - Generate Signed Bundle / APK... から作成します。

スクリーンショット 2019-08-29 15.53.19.png

  • Android App Bundle を選択します。

スクリーンショット 2019-08-29 15.53.26.png

  • Create new... からアップロード鍵を生成します。

スクリーンショット 2019-08-29 15.53.36.png

  • 必要な情報を入力して OK します。

スクリーンショット 2019-08-29 16.02.34.png

  • OK を押すとアップロード鍵は生成はされていたのですがぼくは以下のエラーが表示されました。

スクリーンショット 2019-08-29 16.10.16.png

  • お薦めされているので言われるがままに表示されているコマンドを実行します。
$ 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 - アプリの署名 を表示します。

スクリーンショット 2019-08-29 17.54.02.png

  • 「Java Keystore から鍵をエクスポートしてアップロードする」を選択します。

スクリーンショット 2019-08-29 16.31.25.png

  • 「PEPK ツール」を押して pepk.jar をダウンロードします。

  • 画面に表示されているコマンドを太字の部分を修正して実行します。

$ java -jar pepk.jar --keystore=[使用している keystore の path] --alias=******** --output=encrypted_private_key_path --encryptionkey=****************
  • 「アプリ署名の秘密鍵」からアップロードします。

以上でアプリ署名の秘密鍵のアップロードは終了です。

アップロード公開鍵証明書をアップロードする

必須ではないとのことですが 推奨 なのでこちらもやっておいたほうが良さそうです。

  • アプリ署名の秘密鍵と同じ画面で 「(省略可)セキュリティを強化する〜」 を押します。

スクリーンショット 2019-08-29 15.46.57.png

  • 「アップロード公開鍵証明書」から アップロード公開鍵証明書を作成する で生成したアップロード公開鍵証明書(手順通りなら upload_certificate.pem) をアップロードします。

  • 「完了」を押します。

スクリーンショット 2019-08-29 18.57.57.png

  • こちらが表示されているか確認します。

以上で「Google でアプリ署名鍵の管理、保護を行う」 が有効になりました!?
おつかれさまでした!

アップロードできるようになったか確認する

念のためちゃんとアップロードできるか確認しておきます。
アップロードの方法は apk のときと同じです!

Google がアプリの署名をごにょごにょやってくれているので少しアップロードに時間かかるようになった気がします。(といっても微々たるものですが)

スクリーンショット 2019-08-29 16.36.43.png

無事にアップロードできました!

アプリのサイズも半分ぐらいに!

参考

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

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

  1. consoleにて内部テストするユーザーのメールアドレスを登録
  2. 現在GooglePlayに上がっているアプリ(v19)のバージョン+1(v20)のAPKを作成してconsoleにてリリースを作成する
  3. 1.で登録したアドレスでログインしたGooglePlayStore経由か、オプトインURLをユーザに共有して、実機にインストールする
  4. バージョンアップ通知をテストしたいので、2.のバージョン+1(v21)したAPKを作成し、consoleにてリリースを作成する
  5. 実機にインストールしたv20のアプリを起動すると、下記画面が出る

Immidiateの場合
immediate_flow.png

Flexibleの場合
flexible_flow.png

出ると嬉しい!

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

ローカルサーバーにLAN内の外部機器からアクセスする手順についての隠された真実

最初に

僕はある日突然、Laptopで立てたPythonの簡易サーバーにスマホからアクセスできないトラブルにぶつかりました。それがあまりにも複雑で長期に渡って僕を苦しめたので、こうして記事にまとめたという次第です。
隠された真実。(キリッ)(煽りタイトルやめろ)

面倒で語られない手順の真実

ぶっちゃけ、外部端末からのアクセスの実現は、巷で紹介されている程度を遥かに越えて大変に面倒なものでした。(しかし、あなたは以下の手順に従うだけで済みますね。素晴らしい!)

(ファイアウォールやアンチウィルスを切ってセキュリティホールを作る必要などどこにもなかったんや……(※効果には個人差があります))

前提

  • ローカルサーバの設定に問題はない
  • 機器とPCが同じLANに接続している
  • あなたはそのルーターの設定に必要なユーザー名とパスワードを知っている。
  •  Windows 10
  • この親切なサイトを見てもうまくいかなかった。

上記の内容についてはここでは説明しません。もしこれらの前提に引っかかるようなら、Google先生に聞いてみましょう。大半はいい手引が見つかるはずです。
そして、teratailに寄せられたこの回答は今回話す内容をざっくりと説明してくれています(これをもっと早く見つけていれば、あんなに苦しむ羽目にはなかったろうに……)。
当記事は、おおまかに言って、この回答の内容がよくわからない、またはおおよそわかったが解決には至らない、という人たちを対象にしています。以下は上記のteratailの回答をよく読んでから読み進めてください。

いますぐ試すべきこと

まず、接続しているLANが「プライベート」設定になっているか確認しましょう。
WindowsならタスクバーのWifiアイコンをクリックし、対象のLANのプロパティを開けば目前に当該の設定が現れます。

それでもうまくいかない?

  • ネットワークの共有オプションの、プライベートの設定はネットワーク探索を有効にするが選択されているか?
    1. タスクバーのWifiアイコン(右クリック)
    2. ネットワークとインターネットの設定を開く
    3. 共有オプション
    4. プライベート
  • Windows Defenderのすべての着信接続をブロックします。の選択が外されているか?
    1. タスクバーのWifiアイコン(右クリック)
    2. ネットワークとインターネットの設定を開く
    3. Windows ファイアウォール
    4. プライベート ネットワーク
  • このコンピューターのインターネット接続を通しての接続を許可するにチェックが入っているか?
    1. タスクバーのWifiアイコン(右クリック)
    2. ネットワークとインターネットの設定を開く
    3. アダプターのオプションを変更する
    4. Wi-Fi(右クリック) ※ただし、有線接続の場合はローカルエリア接続
    5. プロパティ
    6. 共有タブ

プライバシーセパレーター機能をオフにする

本題。僕には最後まで気づけなかった原因について。
プライバシーセパレーター機能ってなに?

この文献に目を通したら、お手持ちのルーターのセットアップをやりなおしましょう。
これはルーターによって設定方法がまちまちなので、各自でしらべてもらうしかありません。
ただし、以下のような傾向が考えられます。
1. ルーターと有線接続しないと設定できない
2. たいていは192.168.0.1で設定できる
3. SSIDに~GW, ~AWなどの、接尾辞らしきwがついている回線にはデフォルトでセパレート機能がオンになっていて、逆にそのwがない回線ではオフになっている場合がある。こうした場合、プリンターとPC両方をwのついていない回線につなぐことで設定変更しなくても解決する。

以上で僕がサーバー接続を実現した手順の紹介を終わります。
他にめぼしい原因があれば教えて下さい。ありがとうございました。

で、どこがどう「隠された真実」なの?

  1. プライバシーセパレーターの設定はググってもなかなか出てこない
  2. いわんや、ネットワーク設定からプライバシーセパレーターまで包括的に説明してくれる文献ももちろんなかった
  3. みなWindows ファイアウォールやアンチウィルスソフトウェアをオフにせよと口酸っぱく言うがそんな必要などなかったんや……
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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型は値の範囲等が複雑なため試していない。

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

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.java
public 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.kt
class 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#registerTypeAdapterDateMyDateTypeAdapter を関連づけます。

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.java
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);
  }
}

これと同様に、いくつかの parse 方法を用意しておいて、parse に失敗したら次の方法で parse するなどの策を取っておく必要があります。

まとめ

  • Gson で、そのまま Date を json 化すると、秒単位の精度に丸められる
  • 回避方法はあるが、導入や変更にタイミングには注意が必要
  • Gson に限らず、他のライブラリでも serialize / deserialize で意図しないデータの変化が発生しないか気をつけた方が良い
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む