0

CircleCIでのAndroidビルドのメモリ使用量制限、SDKのアップデート

前:CircleCIでAndroidのプロジェクトをビルドする

色々やってプロジェクトが複雑化してきたら、2回ビルドできなくなった。けど解決した。

circle.ymlはこうなっている

machine:
java:
version: oraclejdk8
environment:
JAVA_OPTS: "-Xms518m -Xmx2048m"
dependencies:
pre:
- echo y | android update sdk --no-ui --all --filter "extra-android-m2repository,extra-android-support,extra-google-google_play_services,extra-google-m2repository"
test:
pre:
- cd mobile/src/main/java/org/shokai/voicetweet/ && cp TwitterConfig.java.sample TwitterConfig.java
override:
- ./gradlew assembleDebug

メモリが足りない


メモリを4GB以上使うとテストがこける。

Warning: You build has exceeded the memory limit of 4G on 1 container.


CircleCiでAndroid開発を爆速にする | Covelline Developer Blog
を参考に、machineのJAVA_OPTSでメモリ使用量を設定した。
AndroidのVMを起動してのテストせず、ビルドだけなので2GBに制限した。見ているとgradleからたまにjavaのプロセスがforkしているように見えたのでとりあえず4GBの半分使うようにしておいた。

CircleCIでの実行時は環境変数CIRCLECIが設定されるのでbuild.gradleも設定した。

android {
(略)
dexOptions {
if(System.getenv()["CIRCLECI"] as boolean) {
javaMaxHeapSize "2048M"
preDexLibraries false
}
}
}
あとCircleCIではdex(Dalvic VMの実行ファイルだが、次回コンパイルを高速にするらしい?)を生成しないようにした。


Android SDKのアップデート


play-service-wearableを6.5.87から8.3.0にアップデートしたら、CircleCIに8.3.0が無くてこういうエラーが出る

Could not find com.google.android.gms:play-services-wearable:8.3.0.
Searched in the following locations:
https://jcenter.bintray.com/com/google/android/gms/play-services-wearable/8.3.0/play-services-wearable-8.3.0.pom
https://jcenter.bintray.com/com/google/android/gms/play-services-wearable/8.3.0/play-services-wearable-8.3.0.jar
file:/usr/local/android-sdk-linux/extras/android/m2repository/com/google/android/gms/play-services-wearable/8.3.0/play-services-wearable-8.3.0.pom
file:/usr/local/android-sdk-linux/extras/android/m2repository/com/google/android/gms/play-services-wearable/8.3.0/play-services-wearable-8.3.0.jar
file:/usr/local/android-sdk-linux/extras/google/m2repository/com/google/android/gms/play-services-wearable/8.3.0/play-services-wearable-8.3.0.pom
file:/usr/local/android-sdk-linux/extras/google/m2repository/com/google/android/gms/play-services-wearable/8.3.0/play-services-wearable-8.3.0.jar


CircleCIのドキュメントによるとdependenciesのpreでandroidコマンドを実行してupdateできるとの事だったので、circle.ymlに設定した。

dependencies:
pre:
- echo y | android update sdk --no-ui --all --filter "extra-android-m2repository,extra-android-support,extra-google-google_play_services,extra-google-m2repository"
–filterオプションに渡せるパッケージ名は
% android list sdk --all --extended

で確認できる。

0

AndroidAnnotationsで非同期処理を1つずつ実行する

AndroidAnnotationsを使い始めた。
findViewByIdを@ViewById(R.id.button)とか書けたり、@Click(R.id.button)の後に関数を定義でクリックリスナーとして登録されたりとかで便利だった。

1つずつ実行したい


@Backgroundを関数の頭に付けるとその関数は非同期実行されるようになる。AsyncTaskなどを自分で書かなくてすむ。
でも複数の非同期な関数を順番に呼び出したい時があって(例えばWebAPIとか)、一気に実行されたら困る。
順番に実行するには1つ目の関数のコールバックに2つ目の関数を登録するか、Promiseみたいなものを返すとかしなければならなくなる。

なので非同期処理を順番に実行したい時はRxJavaとかjdeferredとか使わないといけないのかな・・とか思ってたけどAndroidAnnotationでも綺麗に書けた。

@Background(serial = “id”)


WorkingWithThreads · excilys/androidannotations Wiki

@Backgroundは何も指定しないと全て並列に動作するのだが、同じserialを指定されたBackgroundは1つずつ実行される。Threadを管理しているBackgroundExecutorがqueueを持っているらしい。

おかげで例えば
(Background) Twitterのscreen name取得
(UiThread) screen nameを画面に表示
(Background) screen nameを使ってアイコン画像取得
(UiThread) アイコンを表示
のような処理を簡単に書ける。

@Backgroundや@UiThreadを付けた関数は値を返す事ができないので、classのメンバ変数を値渡しに使うと

    String mSreenName;

@Click(R.id.buttonShowTwitterInfo)
void showTwitterInfo(){
getTwitterScreenNameAsync(); // 1つずつ実行される
getTwitterProfileImageAsync();
}

@Background(serial = "twitter")
void getTwitterScreenNameAsync(){
mScreenName = mTwitter.getScreenName();
displayTwitterScreenName(mScreenName);
}

@UiThread
void displayTwitterScreenName(String name){
mTextViewScreenName.setText("@" + name);
}

@Background(serial = "twitter")
void getTwitterProfileImageAsync() {
User user = mTwitter.showUser(mScreenName);
URL imageUrl = new URL(user.getBiggerProfileImageURL());
Bitmap profileImage = BitmapFactory.decodeStream(imageUrl.openConnection().getInputStream());
displayTwitterProfileImage(profileImage);
}

@UiThread
void displayTwitterProfileImage(Bitmap image){
mImageViewProfile.setImageBitmap(image);
}

まあなんとか我慢できる程度に読みやすいコードになる。やっぱりPromiseみたいに値をつないで渡していけた方がいいけど

0

CircleCIでAndroidのプロジェクトをビルドする

Androidのtestまだよくわからないのでとりあえずビルドだけする。
Travis CIでは色々細かく設定しなければならないみたいなので、適当にSDKバージョンとか見つけてくれるCircle CIを使う事にした。

JavaとXMLの世界なので、コンパイルが通るかをプルリク・マージの時に自動的にCircleCIがやってくれて結果がGitHub表示されているだけでも安心感がある(という事をテストがあるとプルリクしやすいにも書いた)


手順


手元でビルド

自分のプロジェクトが手元で
./gradlew assembleDebug
でデバッグビルドできるのを確認しておく


Github → Circle CIの連携設定

GitHubの各リポジトリ内の設定から、Webhooks&ServicesでCircle CIを追加する。
githubにpushする毎に自動的にCircle CIにhookが飛ぶ。

Add projects – CircleCIでもプロジェクトを追加する。


circle.yml

リポジトリルートにcircle.ymlを置く。中身はこれだけでいい
test:
override:
- ./gradlew assembleDebug


テストする

githubにpushしたら https://circleci.com/gh/(ユーザ名)/(リポジトリ名) でテストが走ってるのが見える


Status Badge

https://circleci.com/gh/(ユーザ名)/(リポジトリ名).png に画像が生成されるので、README.mdに埋め込んでおく


circle.ymlの書き方

Configuring CircleCI – CircleCIに設定方法が書いてある

VoiceTweetではTwitterのconsumer keyをリポジトリにコミットしたくなかったので、testのpreでサンプルファイルから事前にコピーしてからコンパイルするようにした。
test:
pre:
- cd mobile/src/main/java/org/shokai/voicetweet/ && cp TwitterConfig.java.sample TwitterConfig.java
override:
- ./gradlew assembleDebug

0

Android WearのエミュレータをAndroid実機本体に接続する

Moto360へのapk書き込みが遅すぎるので、Wearはエミュレータで、本体はNexus5でやってみる事にした。


参考

Creating and Running a Wearable App | Android Developers
のSet up an Android Wear Virtual Device


Wearエミュレータの設定

Android Studioの[Tools]→[Android]→[AVD Manager]でWearのエミュレータを作る。ARMよりx86のイメージの方が速いらしい。

「About」からビルド番号を7回タップして開発者オプションを有効にし、ADB Debuggingを有効にする


この時点ではWearは親機とペアリングして無いので、Disconnectedになっている


Android実機の設定

Android実機がエミュレータと接続できるようにしておく。
AndroidStudioのTerminalで
% adb -d forward tcp:5601 tcp:5601

そして接続する。
Android実機でGoogle製のAndroid Wearアプリを起動し、普段使ってるMoto360へのペアリングを解除して、右上から「エミュレータをペア設定」で接続。


音声入力

マイクが無いのでキーボードで入力する。タイミングがすごい微妙で、
音声入力画面になる→2秒ぐらい待つ→「Didn’t catch that」と表示される→表示が消える→ここで素早く入力→enterとか押さずにしばらく待つ→するとなぜか入力できてる。

0

Android Wearからスマホ本体側にsendMessageでメッセージを送る

参考

Sending and Receiving Messages | Android Developers
これ読んであとは勘で書いたらWearable.MessageApi.sendMessageがなんとなく動いた。


Wearable.MessageApi.sendMessageの用途

Wearにはスマホ側の通知が全部表示されてそこからアクションも全部実行できるんだけど、スマホ通知からWearのActivityを起動したりはできない。スマホからWearのActivityとか呼びたい時は、sendMessageでWear内のWearableListenerServiceを実装したサービスと通信して、そこからWear内でIntent発行してもらってActivityを起こす。

Wear上のActivityからスマホに通信して何か処理してもらう時もsendMessageを使う。
WearにはWebブラウザが無いので、twitterクライアント作る時とか少なくとも認証はスマホにやってもらう必要がある。

sendMessageとは別にDataItemというWear/Handheld間の共有オブジェクトもあって、これは変更通知もくるのでこっちでやった方がいい用途もある


WearからHandheld(スマホ)へ送る場合の手順

  1. WearでGoogle API Client接続
  2. Wearable.NodeApiでHandheldのNodeIDを探す
  3. sendMessageにpath(と呼ばれてるけどタイトルみたいなもの)とdataを送信
  4. Handheld側でWearableListenerServiceを作っておけば受信できる


Wear側でGoogle API Clientを作って、接続しておく

onStartで接続してonStopで切るとかは、AndroidStudioで新規プロジェクト作成時のMobileのMainActivityをGoogle Play Service Activityにすると生成されるテンプレを参考にした。
メッセージ受診するだけのHandheld側にはGoogle API Clientを作る必要はない。


Node ID

Wearable.NodeApi.getConnectedNodesでHandheldのnodeIDを取得する。Nodeを判別できる要素はgetDisplayNameしかなくて親機を正確に判別できる気がしないし、ふつうHandheldとWearしか存在してないはずなので全員に送信するようにしてしまった。


WearからsendMessage

UIスレッドから呼べないのでAsyncTaskの中でやるとかする。

    public void sendTweetAsync(String tweet){
if(tweet == null) return;
Log.i(TAG, "send \""+tweet+"\" to handheld");
new AsyncTask<String, Void, String>() {
@Override
protected String doInBackground(String... params) {
String tweet = params[0];
byte[] bytes;
try {
bytes = tweet.getBytes("UTF-8");
}
catch(Exception ex){
Log.e(TAG, ex.getMessage());
return null;
}
for (Node node : Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await().getNodes()){
Log.v(TAG, "sending to node:" + node.getId());
Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), "/post/tweet", bytes)
.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
@Override
public void onResult(MessageApi.SendMessageResult sendMessageResult) {
Log.v(TAG, sendMessageResult.toString());
}
});
}
return null;
}
}.execute(tweet);
}


HandheldにWearableListenerServiceを作って受信


受信側はService作ってintent filterにwearable.BIND_LISTENERを追加するとライフサイクルは適当に管理してくれる。自分でGoogle API Clientの接続管理する必要もない。
        <service android:name=".TweetService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
</service>

public class TweetService extends WearableListenerService {

@Override
public void onMessageReceived(MessageEvent messageEvent) {
Log.v(TAG, "onMessageReceived");
if (messageEvent.getPath().equals(MESSAGE_PATH_TWEET)) {
String msg;
try {
msg = new String(messageEvent.getData(), "UTF-8");
}
catch (Exception ex){
Log.e(TAG, ex.getMessage());
return;
}
Log.i(TAG, "receive: "+ msg);
// ここで適当に処理
}
}


Activityで受信する場合はMessageListenerを実装すればいいらしい。