Jenkins + MavenでAndroid projectをbuild

割といたるところで実践されているネタだとおもうのでアレなのですが、まぁ色々とハマったところもあり、せっかくなのでメモっておこうかと。
Jenkinsを動かすremote serverは以下のとおり。

$ cat /etc/redhat-release 
CentOS release 6.2 (Final)
$ uname -a
Linux xxxxxx.com 2.6.32-220.7.1.el6.x86_64 #1 SMP Wed Mar 7 00:52:02 GMT 2012 x86_64 x86_64 x86_64 GNU/Linux
  • まずはMaven対応のAndroid projectを作成 (local PC)

最近は Eclipse上で、New -> Project -> Maven Project -> ArchType (https://github.com/akquinet/android-archetypes) が楽かなぁと思っています。

参考: http://d.hatena.ne.jp/komamitsu/20110923/1316787031

  • このProjectをGit管理下に (local PC)

ここでは仮に、git.yourgitrepo.com に git init --bare した remote repository があるものとします。あと都合上、余計なことをせず即効でpushする構えで。

$ git init 
Initialized empty Git repository in /home/komamitsu/workspace/hogedroid/.git/
$ cat <<EOF > .gitignore
> bin
> gen
> target
> EOF
$ git add .
$ git commit -m 'first import'
$ git remote add origin ssh://git.yourgitrepo.com/git/hogedroid.git
  • Jenkinsさんインストールと起動 (remote server)

http://pkg.jenkins-ci.org/redhat/ にあるとおり以下の手順で。JDKが入ってなければ、(Open|Oracle)JDKを適当に入れておく必要あり。

sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
sudo yum install jenkins
sudo service jenkins start

起動させるとデフォルト設定で8080(多分)で起動する。今回は前にnginxを置いてreverse proxyさせたけれども、記事の内容的にあまり関係ないのでその辺は省略。

  • Android SDK のインストール (remote server)

http://developer.android.com/sdk/index.html からLinux (i386)版のtarballをwgetして展開。これを/usr/local/android-sdkとして置いておく。

$ wget http://dl.google.com/android/android-sdk_r18-linux.tgz
$ tar zxvf android-sdk_r18-linux.tgz
$ sudo mv android-sdk-linux /usr/local/android-sdk
$ cd /usr/local/android-sdk
$ sudo chgrp -R jenkins .  (これをやっておかないと後にbuild errorに...)

このままだと中身がスカスカなので、SDK Readme.txtに従って以下。

$ cd /usr/local/android-sdk
$ tools/android update sdk --no-ui

     : (結構時間かかるし、3rd partyっぽいコンポーネントについてinteractiveに聞かれるのでenter key連打が必要)

で、インストール後、試しにadbを叩いてみると失敗する。

$ /usr/local/android-sdk/platform-tools/adb
-bash: /usr/local/android-sdk-linux/platform-tools/adb: /lib/ld-linux.so.2: bad ELF interpreter: No such file or directory

なので下記のように適当にi686版のものをインストールしておく(怒られる度にインストールすればOK)。

$ sudo yum install glibc.i686
$ sudo yum install ncurses-libs.i686
$ sudo yum install libstdc++.i686
$ sudo yum install libz.i686

2012-05-29 追記:debian系なら

apt-get install ia32-libs
  • Jenkinsさんの設定 (remote server via web browser)

Manage Jenkins -> Configure System にいって

辺りを設定。

  • JenkinsにJobを追加 (remote server via web browser)

New Jobから、適当にProject名を入力し、Build a maven2/3 projectを選択。
次の設定画面では、Source Code Management = Git、Git Repository = /git/hogedroid.git (今回同じサーバーだったのでこんな感じで)、を必須で設定。
その他はお好みで。
注意事項としては、filesystemのpermissionなどで、git repositoryにアクセスできないとBuild errorになるので、jenkinsユーザーでもアクセス可能にしておくこと。

  • JenkinsのJobを試しにBuild (remote server via web browser)

上記で追加したプロジェクトを選択するとBuild Nowというリンクがあるので、押すと非同期でBuildが行われます。失敗したらbuild logを眺めつつ対応することになるんだけども、今回ハマっていたのが、以下のエラー。

cause : Execution default-generate-sources of goal com.jayway.maven.plugins.android.generation2:android-maven-plugin:3.2.0:generate-sources failed: Path "/usr/local/android-sdk/platforms" is not a directory. Please provide a proper Android SDK directory path as configuration parameter <sdk><path>...</path></sdk> in the plugin <configuration/>. As an alternative, you may add the parameter to commandline: -Dandroid.sdk.path=... or set environment variable ANDROID_HOME.

/usr/local/android-sdk/platformsは存在しているのに何でかなぁ、と思っていたけれど、filesystemのpermission的に、jenkins userでは/usr/local/android-sdkにアクセスできない状態になっていたため、chgrpして対応しました(前述)。


ということで、Jenkinsでbuildできるところまでつらつら書きましたが、テストやらデプロイやらがまだなので、後々付け足すかもです。

Jenkins + Maven で Android test project を build & test

Test project も Jenkins上で build & test できたので、忘れないうちにメモ。とはいえ、力つきそうなのでザックリと。

  • Android test projectの作成(local PC)

やりかたは色々あると思うけど、今回は

あと、どのdevice上でテストするかを指定するため以下の設定をpom.xmlに書いておいた。詳しくは上記のURL参照のこと。

<project><properties>
    <android.device>emulator</android.device>
  </properties></project>
  • Unit test作成 & 実行(local PC)

Android test project なので、src/main/java の下にテストコードを書く。

import junit.framework.TestCase;

public class ZuttomoTest extends TestCase {
  public void testZuttomoDayo() {

Eclipse上でRun -> Android Unit testを実行して上手くいくことを確認しておく。

次に、mvnコマンドからのテストが成功するかも確認。この際, 適切なAndroid emulatorが起動していること。ちなみに、mvn install でテストが実施される。

$ mvn install
  • Git摘要(local PC)

昨日のエントリと同様にtest projectをGit管理下において、commit, push originしておく。

  • AVD作成(remote server)と起動

Jenkins上でAndroid testが実行されるためには、Emulatorが起動している必要がある。その前準備。

例えばAPI level 8で、sdcard size 20M, デバイス名をG22の場合、emulatorの作成と起動は以下。

$ android create avd -c 20M -n G22 -t 8
$ emulator -no-window -avd G22

今回用いたremote serverはGUIが無いので、emulator -no-window にしています。

で、Jenkinsのテストが行われる間はemulatorを起動している必要あり。この辺は自動化したいところ。

  • JenkinsにNew Job作成してテスト(remote server via web browser)

こちらも昨日のエントリ参照のこと。

Build NowするとBuild後、Testが実施されるが、今回は "Found 0 devices connected with the Android Debug Bridge (snip) No online devices attached." と怒られ続けた。結局、emulatorが起動していなかったのが原因なので、CIの際にはEmulatorを起動しておく必要がある(どうやら pluginで解決できそうな気もするけど)。

AdMarvelという広告系のSDKが位置情報まで送っているみたいな件

どうもインストールしていたShazam絡みで動いているみたいなんだけども、logcat見ているとたまに以下のようなエラーを吐いている(UNIQUE_ID以降とGEOLOCATION以降は一部値を変えてます)。

E/admarvel( 1299): postString: &site_id=14488&partner_id=ef8a30b841b36346&timeout=5000&version=1.5&
language=java&format=android&sdk_version=2.1.9.1&
sdk_version_date=2011-11-30&sdk_supported=_admob&device_model=IS06&device_name=FRG83&
device_systemversion=2.2.1&retrynum=0&excluded_banners=&device_orientation=landscape&device_connectivity=wifi&resolution_width=800&max_image_width=800&resolution_height=480&max_image_height=480&
device_os=Android&adtype=banner&target_params=UNIQUE_ID%3D%3Ef3cb6270eb3eb6ac%7C%7Cappv%3D%3ES%7C%7C
GEOLOCATION%3D%3E35.97797994444445%252C139.99682984444444%7C%7Cco%3D%3EJP%7C%7C
screenorient%3D%3El%7C%7Cappvn%3D%3E3.8.2%7C%7Cosv%3D%3E2.2.1%7C%7Cla%3D%3Een%7C%7C
RESPONSE_TYPE%3D%3Exml

明らかにRequestに含めているパラメータっぽいんだけど、このうち以下の部分が位置情報っぽい。

GEOLOCATION%3D%3E35.97797994444445%252C139.99682984444444%7C%7Cco%3D%3EJP%7C%7C

デコードすると、

irb(main):004:0> require 'cgi'; CGI::unescape('GEOLOCATION%3D%3E35.97797994444445%252C139.99682984444444%7C%7Cco%3D%3EJP%7C%7C')
=> "GEOLOCATION=>35.97797994444445%2C139.99682984444444||co=>JP||"

となり%2Cが ',' なので北緯と東経に分けられているみたい。一部の数字を変更するまえの値をGoogle mapでみるとピンポイントで自宅でした。

ちなみにUNIQUE_IDのほうはANDROID_IDだったのだけども、これは端末で一意(まぁ別の問題で一意になりきれていないのが現状だけどそれに近い)なので、まぁそれと合わせて現在位置情報が都度通知されると、いろいろとまずい感じですねぇ。

とりあえず、AdMarvelがひどすぎるのでShazamは消しておこうかと。

Android WidgetアプリをMarketに登録してみたの巻

NAVER Topic Widget - Google Play の Android アプリ

NAVER検索(http://www.naver.jp/)のトップに流れるトピックワードを、AndroidWidget上で順番に表示します。

情報の更新は20分間隔、トピックの切り替えは15秒間隔でおこないます。スリープ中は処理を停止していますので、電池消費の恐れはありません。

トピックをクリックすると、Webブラウザで検索結果画面を表示し、詳細な情報を見ることができます。

一年位前にAndroid Widgetアプリの練習として個人的に作ってみた小品で、当時会社にプロトタイプとして寄贈しようかと相談してみたのですが諸事情あり「個人でご自由に...」という方向になったという経緯があります...

で、私もすっかり存在自体忘れてたのですが、

  • 自分の端末に入ったままになっていて最近動かしてみたら多少の暇つぶしになるなぁ
  • 仕事ではAndroidアプリの開発に携わって来たけれど個人的にリリースしてない... やってみたい!!
  • 弊社サービスへの流入が少し増えるかも...

ということで、試しにアプリを登録してみることにしました。まぁそんなに頑張って使うものでは無さそうです。


以下、登録してみた後の感想

  • イコン画像作りが面倒い...
    • GIMPで適当に作ってみましたが、作っては捨ての連続で結構大変... で出来上がりもショボい...
    • デザイナーさんってすごいなぁ
  • アプリ登録時にはスクリーンショット&大きいアイコン画像が必須なので焦る
    • apkファイルをアップロードしてはじめて必要なことが分かるので焦る
    • しかもスクリーンショット画像がエラー扱いになっちゃってリカバリ不能に陥るという状況になっちゃって、画面リロードしたら入力情報がクリアされてしまうという事態に... 誰だ!!こんなシステムを作ったやt

ちなみに実装的な話ですが、トピック情報取得用のWeb APIとか無いみたい(良く知らない)ので、HTMLを解析して取得していたような気がします。

一応ソースコードはこちらで公開しています(LICENSE置いてないですがMIT licenseです。後で置いておきます...)。
https://github.com/komamitsu/android-naver-topic-widget

Android上でWeb serverを動かしてみた

Wifi環境であればPCからAndroidにアクセスできるはずで、すなわちAndroid上にWeb server立ててPCからアクセスできるんじゃないかなぁ、と思ったのと、NanoHTTPD has moved to github という酷くかわいいhttpdJava source file一枚!)を見つけてしまったので何か作ろうかと思い立ったので試してみました。

XML的なところは適当に...

今回はアクセス元のHTTP-Request headersを画面に表示してみてます。


で、Activityはこちら(同じpackageにNanoHTTPD.javaを置いてます)

起動させたところ

PCからブラウザでアクセスしたところ(このときブラウザには "Hello, World" 的なh1が表示)

ということで、めちゃめちゃ簡単にAndroidがWeb serverになりました。何だか色々と面白いことができそうなので暇な時に何かしてみようかと思います。あとNanoHTTPDが小さくてかわいい。

そもそもAndroidOS2.2以前では別ThreadからのSocket#close()が効かない件

http://d.hatena.ne.jp/komamitsu/20111103/1320338412 の続きで少し見てみたら、HttpRequestBase#abort() -> AbstractClientConnAdapter#abortConnection() -> SocketHttpClientConnection#shutdown() -> Socket#close() と来ているので、やっぱりこの辺かなぁと。

今度はこんなのを作って確かめてみました。

public class SocketAbortAndroidActivity extends Activity {
  private static final String TAG = SocketAbortAndroidActivity.class.getSimpleName();
  private final String HOSTNAME = "10.0.2.2";
  private final int PORT = 8080;
  private Socket socket;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    try {
      socket = new Socket(HOSTNAME, PORT);
    } catch (UnknownHostException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }

    ExecutorService es = Executors.newSingleThreadExecutor();
    es.execute(new Runnable() {
      @Override
      public void run() {
        try {
          OutputStream output = socket.getOutputStream();
          output.write("Hello, World!".getBytes());
          InputStream input = socket.getInputStream();
          byte[] buf = new byte[256];
          input.read(buf);
          Log.d(TAG, "received => " + buf);
        } catch (UnknownHostException e) {
          e.printStackTrace();
        } catch (IOException e) {
          e.printStackTrace();
        } finally {
          Log.d(TAG, "Finish");
        }
      }
    });

    try {
      Thread.sleep(5 * 1000);
    } catch (InterruptedException e) {}

    try {
      socket.close();
    } catch (IOException e) {
      e.printStackTrace();
    }

    Log.i(TAG, "Close");

    try {
      Thread.sleep(5 * 1000);
    } catch (InterruptedException e) {}

    es.shutdown();
  }
}

OS2.3.3のEmulatorではclose()されると前回同様以下のようにExceptionが発生するのですが、

I/SocketAbortAndroidActivity(  357): Close
W/System.err(  357): java.net.SocketException: Socket closed
W/System.err(  357):    at org.apache.harmony.luni.platform.OSNetworkSystem.read(Native Method)
W/System.err(  357):    at dalvik.system.BlockGuard$WrappedNetworkSystem.read(BlockGuard.java:273)
W/System.err(  357):    at org.apache.harmony.luni.net.PlainSocketImpl.read(PlainSocketImpl.java:458)
W/System.err(  357):    at org.apache.harmony.luni.net.SocketInputStream.read(SocketInputStream.java:85)
W/System.err(  357):    at org.apache.harmony.luni.net.SocketInputStream.read(SocketInputStream.java:65)
W/System.err(  357):    at com.komamitsu.SocketAbortAndroidActivity$1.run(SocketAbortAndroidActivity.java:43)
W/System.err(  357):    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1088)
W/System.err(  357):    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:581)
W/System.err(  357):    at java.lang.Thread.run(Thread.java:1019)
D/SocketAbortAndroidActivity(  357): Finish

OS2.2ではException発生せず... というかTCPレベルでFINが飛んでないす。

# tcpdump -n tcp port 8080
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes

00:36:00.607484 IP 10.0.2.15.51333 > 10.0.2.2.8080: S 3496404316:3496404316(0) win 5840 <mss 1460,sackOK,timestamp 128072 0,nop,wscale 1>
00:36:00.608027 IP 10.0.2.2.8080 > 10.0.2.15.51333: S 203136001:203136001(0) ack 3496404317 win 8192 <mss 1460>
00:36:00.608117 IP 10.0.2.15.51333 > 10.0.2.2.8080: . ack 1 win 5840
00:36:00.637904 IP 10.0.2.15.51333 > 10.0.2.2.8080: P 1:14(13) ack 1 win 5840
00:36:00.638026 IP 10.0.2.2.8080 > 10.0.2.15.51333: . ack 14 win 8760
(ずっとこのまま...)

明日(というか今日か...)、子供の遊び相手の隙をついてちゃんとAndroidソースコードを見たいところ...


追記 2011-11-06 22:54

まぁ、結構どうでも良いのだけども、2.2のばあい、
org.apache.harmony.luni.net.PlainSocketImpl#connect() -> org.apache.harmony.luni.platform.OSNetworkSystem#connectWithTimeoutSocketImpl() -> (native) osNetworkSystem_connectWithTimeoutSocketImpl() -> (native) sockConnectWithTimeout() -> (native) doConnect() -> connect()
とconnect(2)を呼んでいるんだけども、その直前で

        /* set the socket to non-blocking */
        int block = JNI_TRUE;
        rc = ioctl(handle, FIONBIO, &block);

とsocketをnon-blockingにしている。

2.3のほうは、この辺り (libcore/luni/src/main/native) のコードの構成がガラリと変わっているんだけども結局は

    if (!blocking) {
        flags |= O_NONBLOCK;
    } else {
        flags &= ~O_NONBLOCK;
    }
    int rc = fcntl(fd, F_SETFL, flags);

とかやってる(non-blockingにしている)。