Celeste Engineer

Androidとか自転車とか

SMS が相手に届いたかどうか判定する

SMS や MMS と言ったメッセージのやりとりの仕組みには、Delivery Report という機能があります。SMS をおくると、キャリアが Delivery Report を送り主に返します。この Delivery Report を見ることで、SMS が相手に届いたかどうかが分かります*1

SMS や MMS をおくるには、SMSManager を使います。例えば、単純なテキストメッセージをおくるのであれば、sendTextMessage メソッドを呼び出してメッセージを送ります。 他にもいくつか用途に応じて異なるメソッドが用意されていますが、ほぼどのメソッドでも sentIntent と deliveryIntent というPendingIntent型の引数があります。

これらは、メッセージ送信中にエラーがあったかどうか、またメッセージが相手に届いたかどうかをブロードキャスト Intent としてコールバックするために使われます。 sentIntent はメッセージ送信が成功したかどうか、エラーならばどのようなエラーがあったかをブロードキャストします。電波がなかったり、PDU が空だったり、あるいはサービスに未登録だったり、様々な理由で SMS を送れない場合はこのブロードキャストが飛んできます。 一方で deliveryIntent は相手に届いたかどうかを、Delivery Report を元にブロードキャストします。

ここまでの話は公式のリファレンスにも記述してある内容ですが、実は deliveryIntent は必ずブロードキャストされるわけではありません。

これは、キャリアが Delivery Report をサポートしているかどうかによって変わります。例えば、T-Mobile や Verizon は Delivery Report をサポートしているようですが、AT&T はサポートしてないようです*2。また、場合によってはフェイクの Delivery Report を生成することもあるようで、届いていないのに届いているように見せかけるものもあるようです*3

単純に SMS を送信できたかどうかだけであれば、sentIntent のブロードキャストのみを使って判定できるので、届いたかどうかが必要なければ deliveryIntent の PendingIntent は null にしましょう。

RxJava 1.x -> 2.x: Observable.OnSubscribe<T> から ObservableOnSubscribe<T> へ移行する

RxJava 1.x には Observable の static なインナークラスとして Observable.OnSubscribe がいました。Javadoc にあるとおり、Observable を subscribe したときに呼ばれるメソッドを定義するためのフックになるクラスです。

RxJava 2.x ではインナークラスではなく ObservableOnSubscribe というクラスになっています。また、オーバライドすべきメソッドはcall(Subscriber<? super T>)ではなくsubscribe(ObservableEmitter<T>)になっています。

Observable.OnSubscribe<T>#call(Subscriber<? super T>)のなかで、unsubscribe したときにリソースの開放やリスナーの解除などをするフックを作っていた場合、RxAndroid のMainThreadSubscriptionでフックすることになります。そしてそのインスタンスSubscriber<? super T>#add(Subscription)に渡します。 これを RxJava 2.x に移行するとき、MainThreadSubscriptionMainThreadDisposableに、Subscriber<? super T>#add(Subscription)ObservableEmitter<T>#setDisposable(Disposable)に書き換えます。

ビーナスライン

最近は毎日30度超えの真夏日で、自宅から日帰りで行って帰ってこれる場所では暑すぎて熱中症待ったなしだと思ったので、電車輪行の練習も兼ねて長野県茅野市から松本市に至るビーナスラインを走ってきました。 電車での移動距離もそこそこあるので、特急に乗ってシュッと行ってシュッと帰ってきました。調べてみるとスーパーあずさは電車輪行しやすそうだったので、行き帰りともにスーパーあずさのチケットを取りました。

朝いちのスーパーあずさで立川から茅野まで行き、そこからまっすぐビーナスラインを走りました。

まずは茅野から早速ずいずい坂道を登って蓼科湖に。

f:id:KeithYokoma:20170716102041j:plain

蓼科湖で少し休憩と軽食を取って本格的な山登り。

f:id:KeithYokoma:20170716111704j:plain

途中の見晴らしのいい駐車場でハリボーをムシャムシャ。

f:id:KeithYokoma:20170716112240j:plain

白樺湖を超え、車山高原へ向かうところの駐車場で白樺湖をのぞみ休憩。

f:id:KeithYokoma:20170716115907j:plain

逆側も。

f:id:KeithYokoma:20170716120145j:plain

車山高原では鹿肉のジビエバーガーとソフトクリーム、コーヒーでお昼。

f:id:KeithYokoma:20170716122020j:plain

スッと登って途中の駐車場。

f:id:KeithYokoma:20170716130802j:plain

更に奥地へ。

f:id:KeithYokoma:20170716140949j:plain

360度大自然がのぞめるパノラマ!!

f:id:KeithYokoma:20170716141602j:plain

f:id:KeithYokoma:20170716141615j:plain

本当はさらに美ヶ原の北をぐるりとまわりたかったのですが、天候と体力と時間の関係で手前で松本に降りることに。 降りてきてからは松本城を見たり旧開智学校を見たり、大道芸を見たりしました。

f:id:KeithYokoma:20170716153310j:plain

f:id:KeithYokoma:20170716154034j:plain

無事に松本駅に到着。一瞬だけ市内で雨に振られましたが、それ以外は概ね涼しく心地よい天気でした。

f:id:KeithYokoma:20170716163402j:plain

初めての電車輪行でしたが、良い感じに一日を過ごすことが出来ました。そして一度走ってみたかったビーナスラインを自転車で走ることができてとても楽しかったです!また行きたい!!(次はもう少し身軽にして…)

<merge> タグをつかったレイアウトのプレビューを期待通りに表示する

Issue

Fragment を使わず View をベースにしてレイアウトを組むと、Fragment と同じような役割をもった CustomView を作ってレイアウトを組みます。 このとき CustomView は何かしらの ViewGroup を継承することになりますが、そのレイアウトファイルのルートを<merge>にしないと無駄な ViewGroup がひとつ挟まってしまいます。一方で、<merge>をそのまま使うと、レイアウトのプレビューでは親が何になるのかわからないため、期待通りの表示ができません。

Solution

tools 属性に<merge>の親が何になるかを指定するtools:parentTagが AndroidStudio 2.2 から増えています。

<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:parentTag="android.widget.LinearLayout">
    <!-- views -->
</merge>

tools:parentTagには親となる ViewGroup の名前を指定しますが、Android フレームワークの ViewGroup であっても FQCN をつかいます。またandroid:layout_widthandroid:layout_heightを設定しないとプレビューがうまく表示できないことに注意しましょう。

See Also

stackoverflow.com

Kotlin でかいた interface を Retrofit に食わせたときのエラーに対処する

TL; DR

Kotlin & @Body · Issue #1805 · square/retrofit · GitHub

問題

Retrofit で REST API のクライアント実装を生成する時、Kotlin で書いた interface を渡すとき、メソッドにジェネリクスを使った引数がいるとまれに次のようなエラーメッセージを吐き出してクラッシュします。

java.lang.IllegalArgumentException: Parameter type must not include a type variable or wildcard: java.util.List<? extends Something> (parameter #4)

たとえ Kotlin のコード上ではワイルドカードを使っていない場合でも、Java に変換する過程でワイルドカードに翻訳*1されることがあり、このような問題が発生します。

対処

@JvmSuppressWildcardsというアノテーションを使います。メソッドに対してこのアノテーションを付けることで、Java への変換時にワイルドカードを使わないようになります。

Robolectric 3.4 RC3 で dependencies の ArtifactId が変わります

Robolectric にはいくつかのサブプロジェクトがあり、それぞれに dependencies を追加する必要があります。Robolectric ではサブプロジェクト名が ArtifactId になっています。 そして 3.4 RC3 から ArtifactId が変更になったものがあります。主には、robolectric-というプレフィクスが取れたり、shadows-というプレフィクスが取れるなどの変更ですが、shadows-coreframeworkというようにガラリと変わったものもあります。

3.3.2 まで

dependencies {
  testCompile "org.robolectric:robolectric-annotations:3.3.2"
  testCompile "org.robolectric:robolectric-junit:3.3.2"
  testCompile "org.robolectric:robolectric-processor:3.3.2"
  testCompile "org.robolectric:robolectric-resources:3.3.2"
  testCompile "org.robolectric:robolectric-sandbox:3.3.2"
  testCompile "org.robolectric:robolectric-utils:3.3.2"
  testCompile "org.robolectric:shadows-core:3.3.2"
  testCompile "org.robolectric:shadows-httpclient:3.3.2"
  testCompile "org.robolectric:shadows-maps:3.3.2"
  testCompile "org.robolectric:shadows-play-services:3.3.2"
  testCompile "org.robolectric:shadows-multidex:3.3.2"
  testCompile "org.robolectric:shadows-support-v4:3.3.2"
}

3.4 RC3 から

dependencies {
  testCompile "org.robolectric:annotations:3.4-rc3"
  testCompile "org.robolectric:junit:3.4-rc3"
  testCompile "org.robolectric:processor:3.4-rc3"
  testCompile "org.robolectric:resources:3.4-rc3"
  testCompile "org.robolectric:sandbox:3.4-rc3"
  testCompile "org.robolectric:utils:3.4-rc3"
  testCompile "org.robolectric:framework:3.4-rc3"
  testCompile "org.robolectric:httpclient:3.4-rc3"
  testCompile "org.robolectric:maps:3.4-rc3"
  testCompile "org.robolectric:playservices:3.4-rc3"
  testCompile "org.robolectric:multidex:3.4-rc3"
  testCompile "org.robolectric:supportv4:3.4-rc3"
}

Wercker での Android アプリの CI を速くするポイント

CI の速さは開発のプロセスを高速化する上で重要です。Android アプリのビルドはどうしても時間のかかる部分が多く数分から十数分の時間を要してしまいますが、工夫次第では数十秒から数分の短縮が可能です。 プロジェクトの規模や構成にもよるので一概にすべて効果があるとは言いにくい部分もありますが、この記事で取り上げる幾つかのポイントを抑えておくと、CI の高速化に役立つと思います。今回は Wercker を使用した場合の高速化の方法を書きます。

1.ビルドキャッシュ

Wercker の場合、WERCKER_CACHE_DIRという環境変数にキャッシュのためのディレクトリ情報が入っています。ここにビルドキャッシュを放り込むことで、複数ステップで gradlew コマンドを叩いたときにあとで実行するコマンドが速くなります。また、dependencies の artifact をキャッシュすることで、次回以降のビルドで依存解決が高速になります。

ビルドキャッシュをWERCKER_CACHE_DIRに放り込む場合は、gradlew コマンドのオプションに--project-cache-dir=$WERCKER_CACHE_DIRを指定します。

dependencies の artifact をWERCKER_CACHE_DIRに放り込む場合は、ビルド後のステップで~/.m2~/.gradleWERCKER_CACHE_DIRに cp するか、ビルド前に~/.m2~/.gradleWERCKER_CACHE_DIR配下のディレクトリを指すようシンボリックリンクを作るかのいずれかの方法があります。

依存するライブラリが多くなればなるほど効果が出ます。最近は JCenter が不安定なのか dependencies がダウンロードできなくてビルドがコケるという事象が日に何度か起こっていて、その失敗を解消することにも役立ちます。

2.マルチモジュール化

巨大なモジュールをモリモリビルドするのではなく、小さなモジュールを並列でビルドするようにします。最近の Gradle プラグインはマルチモジュールのビルドについて改善が入っているので、並列ビルドで起こりがちな問題もある程度はうまくやってくれます。キャッシュが効くとなお速いので、ローカルマシンでのビルド時間のほうが改善するかもしれません。

3.メモリ割り当て

Androidアプリのビルドはとにかくメモリが重要です。何はなくともメモリだけは広く確保する必要があります。MacBook Pro など多くのラップトップマシンでは 16GB が上限となりますが、Wercker では(2017年6月現在のところ)コンテナごとにメモリの上限は設定していないようですので、思い切って 32GB 割り当てるなどという富豪的な使い方ができます。特に build.gradle で指定するdexOptionsdexInProcessを有効にしたとき、javaMaxHeapSizeが大きくないと時間がかかってしまいます。CIという環境変数trueのときは32gなど大きな数字を割り当て、そうでないローカルマシン等では8gなどになるような柔軟性があるとよいです。

dexOptions {
  dexInProcess true
  javaMaxHeapSize "true".equals(System.getenv("CI")) ? "32g" : "8g"
}

大抵の CI as a Service ではCI環境変数が用意されているはずです。サービスによっては環境変数の値が真偽値の場合とCIサービス名の場合とがあるので注意が必要です。 また、dexOptionsjavaMaxHeapSizeを指定しても、gradle.propertiesorg.gradle.jvmArgsに何も指定しないと「もっと大きなヒープを使わないと意味が無いぞ」という警告が出ます。併せて、次のようにJVMが使える領域を大きくします。

org.gradle.jvmargs=-Xmx33280M

ただし、この記述をそのままリポジトリに放り込むと、そんなにたくさんメモリのないマシンで困ることになります。できれば CI でだけ大きな領域を確保したいので、リポジトリに入れる gradle.propertis にはローカルマシンで確保可能な数字にしておき、CI ではビルドステップの前に次のようなコマンドを実行するようにして大きな領域を確保するようにします。

- script:
  name: set up environment
  code: |
    echo -e "org.gradle.jvmargs=-Xmx33280M\nandroid.enableBuildCache=true\norg.gradle.parallel=true\norg.gradle.caching=true\norg.gradle.configureondemand=true\n" > gradle.properties

4.テストの並列実行

アプリの機能が増えればその分テストも増え、テストを実行する時間も長くなります。testOptionsでテストを並列実行するための設定項目があるのでこれを使います。ただし、並列実行すると壊れるテストも中にはあるかもしれませんので注意してください。

testOptions {
  unitTests.all {
    maxParallelForks = 2
    forkEvery = 150
  }
}

maxParallelForksが何並列で動かすかを決めるパラメータで、forkEveryJVM を再起動するタイミングをいつにするかを決めるパラメータです。この設定の場合、2並列で150個のテストケースを実行するごとに JVM を再起動します。

併せて、テスト実行時のメモリについても設定しましょう。次の例では Java 8 のランタイムでテストを動かすことを想定しています。

testOptions {
  unitTests.all {
    maxHeapSize = '8192m'
    jvmArgs '-XX:MaxMetaspaceSize=8192M', '-noverify', '-Xmx8192M'
  }
}

5.ビルドステップの整理

大抵の CI as a Service では、ビルドそのものや個々のビルドステップごとにタイムアウトが設定されていて、一定時間を過ぎてもコマンドが終わらないと失敗とみなされます。このため、時間のかかるステップを小分けにしてタイムアウトを回避するような対策をとることがあります。一方で、ステップを小分けにすると、毎回 gradlew を叩いてプロセスを起こすので、少し無駄な時間がどうしてもできてしまいます。先述のビルドキャッシュである程度は改善できますが、これまでの高速化ポイントで速くなったステップは、タイムアウト以内に納まるのであればマージしましょう。自分の場合、assemble と test でそれぞれ別のステップを作っていましたが、それぞれタイムアウト以内に十分納まる時間で終わるようになったのでマージしました。