phicdy devlog

Androidアプリ開発やその他技術系の記事をたまに書きます

AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

DockerでAndroidの環境を構築する

Androidでテストを同時並行に実行したいなーと思っていたので作ってみた。

github.com


Javaのインストール

github.com

こちらからコードを拝借。ただ、add-aptがデフォルトではUbuntuに入っていないのでatp-getでインストールする

# For add-apt-repositor in order to install Java
RUN apt-get install -y software-properties-common

# Install Java
RUN \
  echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
  add-apt-repository -y ppa:webupd8team/java && \
  apt-get update && \
  apt-get install -y oracle-java8-installer && \
  rm -rf /var/lib/apt/lists/* && \
  rm -rf /var/cache/oracle-jdk8-installer

# Define commonly used JAVA_HOME variable
ENV JAVA_HOME /usr/lib/jvm/java-8-oracle

Android SDKのインストール

Android SDKGoogleのページにあるので、ダウンロードしてくればよい。問題はtoolsなどのインストールで、インストールの最中度々承認を求められる。今回はexpcetコマンドを使って、特定の文が来たらそれに対してキーを送るようにしてスキップしている。

あとうまくいかなかった点としては、32bit互換性のためにライブラリをインストールすることと、toolsのアップデート中にtmpフォルダへのコピーが失敗してインストールが失敗するということがあった。

前者はapt-getでlib32stdc++6をインストールする。 後者は、toolsを一旦別フォルダに退避し、退避したtoolsのandroidコマンドを使ってtoolsのアップデートを行う。

ADD expect-android-update.sh .
RUN chmod +x expect-android-update.sh
RUN apt-get install -y wget
RUN apt-get update
RUN apt-get -y install expect
RUN apt-get -y install lib32stdc++6
RUN wget http://dl.google.com/android/android-sdk_r24.2-linux.tgz && \
    mv android-sdk_r24.2-linux.tgz /opt && \
    cd /opt && \
    tar zxvf android-sdk_r24.2-linux.tgz && \
    rm android-sdk_r24.2-linux.tgz && \
    cd android-sdk-linux && \
    cp -a tools copy-tools && \
    /expect-android-update.sh platform-tools && \
    ./platform-tools/adb kill-server && \
    rm -rf temp/ && \
    /expect-android-update.sh tools && \
    /expect-android-update.sh build-tools-22.0.1,android-18,android-19,android-21,android-22,sys-img-armeabi-v7a-android-22,sys-img-armeabi-v7a-android-21,sys-img-armeabi-v7a-android-19,sys-img-armeabi-v7a-android-18
         
ENV ANDROID_SDK_HOME /opt/android-sdk-linux
ENV ANDROID_HOME /opt/android-sdk-linux
ENV PATH $PATH:/opt/android-sdk-linux/platform-tools:/opt/android-sdk-linux/tools

adbで端末のモデル名を取得する

adb shell cat /system/build.prop | grep ro\.product\.model | awk -F"=" 'NR==1 {print $2}' | tr -d ' ' | awk -v RS='\r\n' '{print $1}'


詳細はこちらから

d.hatena.ne.jp


ほとんどの機種は上記記事のコマンドで問題ないのだが、Nexus5のような端末だとro.product.modelが「Nexus 5」のようにスペースが入っている。

そのため、tr -d ' ' を追加した。

Alfredからビルドする

最近、なるべくマウスから手を離したくないなーと思ってなんでもかんでもターミナルでコマンドなりシェルスクリプトでやろうとしている。

ビルドに関してもオプションを受け取れるようにして、シェルスクリプトで切り替えているのだけども、実行するには一回ターミナルを開いて実行して終わったら元の画面に戻って・・・ということになるので、なるべく開きたくない。

これをAlfredでなんとかならないかなと試行錯誤してできるようになったのでメモ。

環境

Alfredとは

AlfredはMac用のランチャーアプリ。バックグラウンドで常駐し、option+spaceでいつでもどこからでも起動できる。

www.alfredapp.com

AlfredのUI自体はただの入力欄で、起動するとフォアグラウンドに出てきて、アプリの頭文字を入力することで素早くアプリを起動できる。

f:id:phicdy:20150407235051p:plain

Alfredのいいところはいつでもどこからでも起動できるというところで、どんな作業をしていようとアプリを起動できる。

またアプリ起動だけでなく、Google検索やファイル検索といった検索機能もカスタマイズして利用できる。

今回はビルドを行うアプリケーションを作成して、それをAlfredから起動することで、いつでもどこからでもビルドができるようにした。

そのビルドを行うアプリケーションを簡単に作れるのがMacに標準で入っているAutomatorである。

Automatorの設定

Automatorでは開始の通知の表示とビルドスクリプトの実行、 そして終了の通知の表示を行うだけである。

まず、Automatorを開き通知を右の欄にドラッグする。

f:id:phicdy:20150418170039j:plain

タイトル等は適当に。

次にシェルスクリプトの実行をドラッグする。 ここでの注意点として、Automatorからシェルスクリプトを実行する際、bash_profileが読み込まれないので、明示的に読み込む必要がある。

f:id:phicdy:20150418170100j:plain

最後にもう一度通知を追加して保存する。

f:id:phicdy:20150418170125j:plain

保存先は、Alfredの検索対象(/Applicationsなど)となっているフォルダにする。

Alfredから起動できるようになる。

f:id:phicdy:20150418194536j:plain

通知は右上に出る。

f:id:phicdy:20150418194609j:plain

通知がすぐ消えるのを変えたい場合は、設定から通知を通知パネルに変更すれば、自分で消すまでは残り続ける

f:id:phicdy:20150418194623j:plain

おわりに

Alfredからビルドできるようになったけど、ビルド対象やビルドオプションが増えるたびにAutomatorで設定する必要があるのどうにかならないかな。。。

JenkinsのJUnitでNoClassDefFoundError

JUnitの結果のXMLファイルがなぜかNoClassDefFoundErrorで読めなかったが、解決したのでメモ。

環境

エラーと原因

エラーは以下の通り。

Process leaked file descriptors. See http://wiki.jenkins-ci.org/display/JENKINS/Spawning+processes+from+build for more information
Recording test results
ERROR: Publisher hudson.tasks.junit.JUnitResultArchiver aborted due to exception
java.lang.NoClassDefFoundError: hudson/tasks/junit/JUnitParser$ParseResultCallable
     at hudson.tasks.junit.JUnitParser.parseResult(JUnitParser.java:90)
     at hudson.tasks.junit.JUnitResultArchiver.parse(JUnitResultArchiver.java:120)
     at hudson.tasks.junit.JUnitResultArchiver.perform(JUnitResultArchiver.java:137)
     at hudson.tasks.BuildStepCompatibilityLayer.perform(BuildStepCompatibilityLayer.java:74)
     at hudson.tasks.BuildStepMonitor$1.perform(BuildStepMonitor.java:20)
     at hudson.model.AbstractBuild$AbstractBuildExecution.perform(AbstractBuild.java:770)
     at hudson.model.AbstractBuild$AbstractBuildExecution.performAllBuildSteps(AbstractBuild.java:734)
     at hudson.model.Build$BuildExecution.post2(Build.java:183)
     at hudson.model.AbstractBuild$AbstractBuildExecution.post(AbstractBuild.java:683)
     at hudson.model.Run.execute(Run.java:1770)
     at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43)
     at hudson.model.ResourceController.execute(ResourceController.java:89)
     at hudson.model.Executor.run(Executor.java:240)

 

調べた結果以下のページで解決した。 

[#JENKINS-26060] Using Jenkins 1.583. Today updated plugins, junit publisher now crashes. - Jenkins JIRA

The problem is that JUnit 1.3 is actually incompatible with Jenkins before 1.587 except 1.580.1 LTS. Unfortunately this cannot be expressed through dependencies, and to make it available to LTS users, it'll also be made available to users on weeklies >1.580.


どうもJenkinsのバージョンが1.587より古く、JUnitプラグインのバージョンが1.3だと不具合が出てしまうらしい。 

私の環境ではJUnitプラグインのバージョンは1.5だったが、同様の問題が発生したので、1.3以降なら起きるのかもしれない(未検証)。


解決方法

Workarounds:
・Downgrade JUnit to 1.2
・Downgrade/cross-grade Jenkins to 1.580.1 LTS
・Upgrade Jenkins to 1.587 or higher
As this only affects older weekly releases between 1.580 and 1.586, it's not worth the effort to fix. The general expectation is that users keep their instance fairly close to the current release when using the weeklies.  


JUnitのバージョンを下げるか、Jenkinsのバージョンを1.580.1にするか1.587以上にすればいいとのことなので、Jenkinsのバージョンを最新版にして解決。

# 一応バックアップ
$sudo mv /usr/share/jenkins/jenkins.war /usr/share/jenkins/jenkins.war.1.581
# Jenkinsの設定画面からダウンロード
$sudo cp /home/hoge/Download/jenkins.war /usr/share/jenkins/
# 再起動
$sudo /etc/init.d/jenkins restart


というかJenkinsの管理画面でアップデートしろとずっと出ていたのを無視していたのがよくなかったな。。。  

mitmproxyで端末の通信内容を見る

mitmproxyとは

mitmproxyはコンソール型の通信監視ツールである。

mitmproxyは、プロキシを通じて通信サーバと端末の間に入り、その通信内容を表示する。

mitmproxy - home

f:id:phicdy:20150530154256p:plain


例えばモバイルのアプリケーションを開発している際に、端末から実際に行っている通信内容を確認したいときがある。

ログなどに出力すれば見ることはできるが、細かい情報を見ようとするならば実際に通信内容を見るしかない。

mitmproxyを使えば、リアルタイムに通信内容を確認できる。

今回はVirtualBox上のUbuntuにインストールして、Androidの通信を見てみた。

環境

 

インストール

pipを使ってインストールできる。

RUN apt-get -y update

# Install pip
sudo apt-get -y install python-dev
sudo apt-get -y install libxml2-dev
sudo apt-get -y install libxslt1-dev
sudo apt-get -y install zlib1g-dev
sudo apt-get -y install libffi-dev
sudo apt-get -y install libssl-dev
sudo apt-get -y install python-pip

# Install mitmproxy
sudo pip install mitmproxy

 

準備

端末

Androidの場合、Wi-Fi設定にてプロキシを設定できるのは3.2 以上のみである。

まず端末とmitmproxyの入ったUbuntuを同ネットワーク上に繋ぐ。

次に端末のWi-Fi設定から接続しているSSIDを長押しする。

そして「ネットワークを変更」から「詳細オプションを表示」にチェックを入れてプロキシを設定する。

プロキシのホストはUbuntuIPアドレス、ポートは自由に設定できる。

 

証明書のインストール

mitmproxyを使うと、mitmproxyを通じて通信を行うことになるので、端末に証明書をインストールする必要がある。

まずmitmproxyを起動する。デフォルトのポートは8080なので、プロキシのポートを8080以外に設定した場合は、-pオプションで変更する。

mitmproxy -p 8001

証明書をインストールする方法は2つある。

  • http://mitm.it にアクセスする。
  • adb pushで証明書を端末に直接入れてインストールする。

http://mitm.it にアクセスする際はmitmproxyを起動しておかないと接続できない。

証明書はmitmproxyを1度実行後、~/.mitmproxy/mitmproxy-ca-cert.cer に置かれるのでこれをadb pushで端末に入れる。

証明書を端末に入れた後は、設定 -> セキュリティ -> ストレージからインストール でインストールできる。

 

使い方

mitmproxyはコンソールで操作するツールになっている。

左にカーソルがあり、見たい通信のところでEnterキーを押すことで詳しい中身が見れる。

下の画像はAmazonにアクセスしたときのリクエスト内容である。

f:id:phicdy:20150530154314p:plain


tabキーを押すことでサーバからのレスポンスを確認できる。

f:id:phicdy:20150530154323p:plain

 

その他にも通信内容を変更して送り返したり、通信のフィルタリングを行ったりといろいろな機能がある。

下記記事に詳しい使い方が書いてある。

モバイルアプリ開発者のための mitmproxy 入門 - Qiita

【Android】API16のuiautomatorでNoSuchMethodError


uiautomatorでNoSuchMethodErrorが発生して困っていた問題が解決したのでメモ。

発生までの流れ

http://www.atmarkit.co.jp/ait/articles/1410/07/news029.html

に従ってテストプロジェクトを作り、ターゲットは、API16(4.1, 4.1.1)としていた。 そして(Android SDK)/platforms/android-16のuiautomator.jarを使っていた。

このテストプロジェクトでテストを作成し、API 17(4.2)の端末で実行したところ、NoSuchMethodErrorが発生してしまっていた。


原因

UiScrollableのsetAsVerticalList()の定義がAPI16とAPI17で変わっていた。

//4.1.1
public void setAsVerticalList()

//4.2
public UIScrollable setAsVerticalList()

参考:http://stackoverflow.com/questions/15204154/uiautomator-failing-on-4-1-2-device



今回はScrollViewの中身のViewを取得するために、UiScrollableのインスタンスを作り、setAsVerticalList()を設定していた。

UiScrollable scrollableView = new UiScrollable(new UiSelector().className(ScrollView.class.getName()).scrollable(true));
if (!scrollableView.exists()) {
    fail("scrollview does not exist");
}
scrollableView.setAsVerticalList();


メソッド名が同じで返り値を使っていたわけではないので、ビルドは普通に通る。 だがAPI17で実行しようとするとビルド時の定義と実際に呼ばれるメソッドが異なるためNoSuchMethodErrorが出ていたのかと思われる。

おわりに

悩んだ理由として、Android Developerのページには特にAPI Levelが書いていなくて困った。

変更したことを書いていて欲しい。

http://developer.android.com/tools/help/uiautomator/UiScrollable.html#setAsVerticalList%28%29

【Android】ActionBarから検索して別Activityで結果を表示する

検索してもなぜか、同じActivityで受け取る例ばかり出てくるので、自分のメモ用に。

検索を行うActivityをDoSearchActivity、検索結果を受け取るActivityをSearchResultActivityとする。

AndroidManifest

検索を行う側(DoSearchActivity)にandroid.app.default_searchableという名前のmeta-dataを追加する。
そしてvalueに検索結果を受け取るActvityを指定する

検索結果を表示するActivityも同様にmeta-dataを追加するが、こちらはandroid.app.searchableという名前にしてXMLのリソースを指定する。
そして、ActionBarから検索が行われたときにintentを受け取れるようにandroid.intent.action.SEARCHのアクションをintent-filterに追加する

<activity
	android:name="xxx.xxx.xxx.DoSearchActivity"
	android:configChanges="orientation|keyboardHidden|screenSize"
	android:label="@string/app_name" >
	<meta-data
    		android:name="android.app.default_searchable"
     		android:value="xxx.xxx.xxx..SearchResultActivity" />
</activity>
<activity
	android:name="xxx.xxx.xxx.SearchResultActivity"
	android:configChanges="orientation|keyboardHidden|screenSize"
	android:label="@string/app_name" >
	<meta-data
    		android:name="android.app.searchable"
		android:resource="@xml/searchable" />
    	<intent-filter>
		<action android:name="android.intent.action.SEARCH" />
     	</intent-filter>
</activity>

検索結果を表示するActivityのmeta-dataにて指定したXMLは以下のようにする。

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
        android:label="@string/app_name"
        android:hint="@string/search" />


android:hintを指定することで検索バーをタップした時に表示する文字を指定できる。


http://developer.android.com/images/ui/actionbar-searchview@2x.png

Action Bar | Android Developers


DoSearchActivityのActionBarに検索バーを設置する

DoSearchActivityのメニューファイルに検索バーを追加することでActionBarに検索バーを表示できる。
Android SDKにSearchViewというクラスが用意されているのでそれを利用する。

<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:id="@+id/search"
        android:actionViewClass="android.widget.SearchView"
        android:icon="@drawable/ic_action_search"
        android:showAsAction="collapseActionView|ifRoom"
        android:title="@string/search"/>
</menu>

よく見る検索のアイコンはGoogleが公式で配布しているので、ダウンロードして利用する。

Iconography | Android Developers


DoSearchActivityの設定

DoSearchActivityでは、メニューの設定でSearchViewを初期化する。
デフォルトの設定では、キーボードを閉じても検索バーが閉じてくれない。

そのため、OnFocusChangeListenerをセットしてフォーカスが他に移ったら検索バーを閉じるという処理を入れている。

参考:
android - Auto Collapse ActionBar SearchView on Soft Keyboard close - Stack Overflow

@Override
public boolean onCreateOptionsMenu(Menu menu) {
	MenuInflater inflater = getMenuInflater();
	inflater.inflate(R.menu.menu_do_search, menu);

	// SearchViewの初期化
	SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
	final MenuItem searchMenuView = menu.findItem(R.id.search);
	searchView = (SearchView)searchMenuView.getActionView();
	searchView.setSearchableInfo(searchManager
			.getSearchableInfo(getComponentName()));

	// 検索バーからフォーカスが移ったら、SearchViewを終了して空の文字列で検索する
	searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
        	@Override
        	public void onFocusChange(View view, boolean queryTextFocused) {
          		if(!queryTextFocused) {
            			searchMenuView.collapseActionView();
                		searchView.setQuery("", false);
            		}
        	}
    	});
	
	return true;
}



SearchResultActivityで検索結果を表示する

ユーザがキーワードを入れて検索をすると、Intentにそのキーワードが入る。

キーはSearchManager.QUERYなので、Intent#getStringExtra()にそのキーを指定することでユーザが入力したキーワードが取得できる。

あとはそのキーワードから検索を行い結果を表示すればよい。

Intent intent = getIntent();
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
	String keyword = intent.getStringExtra(SearchManager.QUERY);

	// keyword を使ってデータベースから検索などをして結果を表示
}