2013年8月25日日曜日

mvn eclipse:eclipseでソースコードとJavadocをダウンロードしてくれるsettings.xml

mvn eclipse:eclipse -DdownloadSources=true -DdownloadJavadocs=true
ってやるとEclipseプロジェクトファイルを作ってくれるのと同時に依存ライブラリのソースコードとJavadocをダウンロードしてくれるんだけど、-DdownloadSources=true -DdownloadJavadocs=trueって毎回書くのはめんどい。
コマンドのエイリアス作ったり、pom.xmlに書いたりすれば解決するけど、なんとなくどちらも気持ち悪かったので、 settings.xmlに記載したった。


これで以下のコマンドを打つだけでソースコードとJavadocをダウンロードしてくれるはず!
mvn eclipse:eclipse

2013年8月11日日曜日

ListAdapterのgetViewが呼び出される階層を調べてみた

ListViewを継承して独自のカスタムViewを作ろうとしてた時に必要になったので、 ListAdapterのgetViewが呼び出される階層を調べてみた。
android.widget.ListAdapter#getView
↑
android.widget.AbsListView#obtainView
↑
android.widget.ListView#makeAndAddView
↑
android.widget.ListView#fillDown
↑
android.widget.ListView#fillGap
↑
android.widget.AbsListView#trackMotionScroll
↑
android.widget.AbsListView#scrollIfNeeded
↑
android.widget.AbsListView#onTouchEvent
まだ続いてるけどここまでわかればOKでしょ。 ちなみにこれ4.2.2の挙動ね。

2013年6月8日土曜日

githubをMavenリポジトリとしてAndroidライブラリプロジェクト(aar)をデプロイして使用する

序章

Android StudioとGradleのビルドシステム(Plugin)が正式にリリースされ、個人的に気になっていたAndroidライブラリプロジェクトの扱いについて調べてみた。新しいGradleのビルドシステムではライブラリプロジェクトはaar(Android archive)という1つのアーカイブで配布することが可能になった。aarファイルは拡張子によって判別され、フォーマットはzip形式である。(aarファイルについての詳細はまた今度。)従来はライブラリプロジェクトを使用するためにプロジェクト一式が必要だったけど、1つのアーカイブを参照するだけで使用出来るようになるので再利用しやすくなったってこと。

※ちなみにこれまでもandroid-maven-pluginというサードパーティ製のプラグインを使用することで解決されてきたのだけど、個人的には正式にサポートされたというのが大きい。

現時点ではビルドシステムのソースコードを見る限り、aarを参照する方法は"External module dependencies"タイプしかサポートされていない。これはどこかのリポジトリに配置されたモジュールを参照するということ。Gradleでは様々なリポジトリ形式に対応しているので、それらの形式(MavenとかIvyとか)でaarをデプロイする必要がある。

注意したいのはただ単にローカルファイルシステム上に配置されたaarを参照出来ないという点である。以下はNGパターン。

dependencies {
    compile files('path/to/some/a.aar', 'path/to/some/b.aar')
    compile fileTree(dir: 'path/to/some', include: '*.aar')
}

GradleでもMavenリポジトリ形式が一般的っぽいのでとりあえずaarをMavenリポジトリ形式でgithubにアップロードして使用出来るか試してみた。結果何事もなく出来たので手順を公開しておく。

環境

  • Android Studio 0.1.1
  • Mac OS X 10.8.3

デプロイ編

まずはAndroid Studioを使用して新規プロジェクトを作成する。こんな感じ。Eclipseユーザーにはわかりづらいかもしれないけど、Android Studioで新規プロジェクトを作成する場合はプロジェクト=アプリではなく、モジュール=アプリとなる。つまり、1つのプロジェクト内にライブラリモジュールや本体アプリモジュール、テストアプリモジュールを配置していくことになる。慣れればこっちのがわかりやいかもね。ちなみにEclipseでbuild.gradleファイルをエクスポートしてAndroid Studioでインポートする場合は上記の通りではない。

ナビ通り進めていくと以下のようなbuild.gradleを含んだプロジェクトが作成される。

現時点でのAndroid Studioは「Mark this project as a library」にチェックを入れてもライブラリプロジェクトとして扱ってくれないらしい。。なので手動で修正。(ソースコードを見る限りそのうち直ると思う。)

apply plugin: 'android'

の箇所を以下に変更。

apply plugin: 'android-library'

試しにaarが作成されるか、ターミナルを起動してビルドしてみる。aarはデバッグ版はassembleDebug、リリース版はassembleReleaseのGradleタスクで作成される。プロジェクト配下で以下のコマンドを実行。

$ cd <project_home>
$ ./gradlew assembleRelease

以下ファイルが生成されていればOK。

$ find . -name '*.aar'
./library/build/libs/library.aar

続いて、Mavenプラグインを使用してMavenリポジトリ形式でデプロイするためlibrary/build.gradleに追記する。Mavenプラグインについてはここを参照。

// プロジェクトルート配下のrepositoryディレクトリにデプロイする
def deployTo = new File(rootDir, "repository")

// Mavenプラグインの利用を宣言
apply plugin: 'maven'

uploadArchives {
    repositories {
        mavenDeployer {
            repository url: "file://${repos.absolutePath}" // デプロイ先
            pom.version = '0.0.1' // ライブラリバージョン
            pom.groupId = 'com.u1aryz' // グループ名
            pom.artifactId = 'sample-lib' // アーティファクト名
        }
    }
}

ターミナルに移ってプロジェクト配下で以下のコマンドを実行。

$ ./gradlew uploadArchives

プロジェクト配下のrepositoryディレクトリに以下のようにファイル群が出来ていれば成功。

$ ls -laR repository
total 0
drwxr-xr-x   3 u1aryz  staff  102  6  1 02:01 .
drwxr-xr-x  18 u1aryz  staff  612  6  1 02:01 ..
drwxr-xr-x   3 u1aryz  staff  102  6  1 02:01 com

repository/com:
total 0
drwxr-xr-x  3 u1aryz  staff  102  6  1 02:01 .
drwxr-xr-x  3 u1aryz  staff  102  6  1 02:01 ..
drwxr-xr-x  3 u1aryz  staff  102  6  1 02:01 u1aryz

repository/com/u1aryz:
total 0
drwxr-xr-x  3 u1aryz  staff  102  6  1 02:01 .
drwxr-xr-x  3 u1aryz  staff  102  6  1 02:01 ..
drwxr-xr-x  6 u1aryz  staff  204  6  1 02:01 sample-lib

repository/com/u1aryz/sample-lib:
total 24
drwxr-xr-x  6 u1aryz  staff  204  6  1 02:01 .
drwxr-xr-x  3 u1aryz  staff  102  6  1 02:01 ..
drwxr-xr-x  8 u1aryz  staff  272  6  1 02:01 0.0.1
-rw-r--r--  1 u1aryz  staff  298  6  1 02:09 maven-metadata.xml
-rw-r--r--  1 u1aryz  staff   32  6  1 02:09 maven-metadata.xml.md5
-rw-r--r--  1 u1aryz  staff   40  6  1 02:09 maven-metadata.xml.sha1

repository/com/u1aryz/sample-lib/0.0.1:
total 936
drwxr-xr-x  8 u1aryz  staff     272  6  1 02:01 .
drwxr-xr-x  6 u1aryz  staff     204  6  1 02:01 ..
-rw-r--r--  1 u1aryz  staff  457944  6  1 02:09 sample-lib-0.0.1.aar
-rw-r--r--  1 u1aryz  staff      32  6  1 02:09 sample-lib-0.0.1.aar.md5
-rw-r--r--  1 u1aryz  staff      40  6  1 02:09 sample-lib-0.0.1.aar.sha1
-rw-r--r--  1 u1aryz  staff     422  6  1 02:09 sample-lib-0.0.1.pom
-rw-r--r--  1 u1aryz  staff      32  6  1 02:09 sample-lib-0.0.1.pom.md5
-rw-r--r--  1 u1aryz  staff      40  6  1 02:09 sample-lib-0.0.1.pom.sha1

このファイル群を構成そのままでまるまるgh-pagesブランチにコミットしてgithubにプッシュする。以下のようになっていればOK。

ここまで手順通りに進んでいれば以下の記述をbuild.gradleに追記するだけでaarを参照することが出来る。

repositories {
    maven { url 'http://<githubユーザー名>.github.com/<githubリポジトリ名>/repository' }
}

dependencies {
    compile 'com.u1aryz:sample-lib:0.0.1'
}

※Mavenリポジトリと言ってもただのファイル群なので、プロジェクト配下でなくてもいいけど、その場合はmavenのurlは変わる。

ビルド編

今度はgithubにデプロイしたライブラリプロジェクトを参照してビルドしてみる。自分でデプロイしたものを参照したい場合は適宜読み替えて。ここでは俺がデプロイしたやつを使用するでよ。

新規プロジェクトを作成してもいいけど、めんどいのでさっき作成したSampleLibraryプロジェクトにAndroid Applicationモジュールを追加する。

  • Android StudioでFile -> New Module…を選択
  • Android Applicationを選択してNext

ナビ通り進めて作成されたmainモジュールのbuild.gradleにrepositoriesとdependenciesを追記する。

buildscript {
    repositories {
        maven { url 'http://repo1.maven.org/maven2' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.4'
    }
}

// 追記
repositories {
    maven { url 'http://u1aryz.github.com/Android-NewPopupMenu/repository/release' }
}

apply plugin: 'android'

dependencies {
    compile files('libs/android-support-v4.jar')
    compile 'com.u1aryz:android-newpopupmenu-lib:1.0.1' // 追記
}

android {
    compileSdkVersion 17
    buildToolsVersion "17.0.0"

    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 16
    }
}

プロジェクトを開き直すとAndroid Studioがbuild.gradleを読みなおして以下のようにExternal Librariesに追加されているはずや。(これはちょっとめんどい)

あとは今まで通りライブラリプロジェクトを使用出来る。

終章

自分で手を入れる必要のないライブラリプロジェクトを使用したい場合は、この方法のがスマートだね。例えば、ActionBarSherlockとかを使用したい場合とか。ちなみにActionBarSherlockは4.3.2でGradle対応するのでmavenCentralにアップされるのが待ち遠しいね〜。

2013年5月25日土曜日

Android StudioでRecent Projectのリストをクリアする

Android Studioでいろいろ触りまくっているとRecent Projectがいっぱいになってすごい邪魔になる。IntelliJだとRecent Projectをクリアするメニューがあるんだけど、Android Studioだと見つからなかった。なので、無理やりRecent Projectをクリアする方法をメモ。基本的にはIntelliJで無理やりやる方法と一緒。ちなみにMacね。

以下のファイルを開く
/Users/<username>/Library/Preferences/AndroidStudioPreview/options/other.xml

んで以下のあたりのrecentPathsのlistの中身を消す。
<component name="RecentProjectsManager">
  <option name="recentPaths">
    <list>
      <option value="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" />
    </list>
  </option>
  <option name="names">
    <map>
      <entry key="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" value="" />
      <entry key="$USER_HOME$/dev/github/ActionBarSherlock/actionbarsherlock-samples/fragments" value="" />
    </map>
  </option>
  <option name="lastPath" value="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" />
</component>

以下が修正後。
<component name="RecentProjectsManager">
  <option name="recentPaths">
    <list>
    </list>
  </option>
  <option name="names">
    <map>
      <entry key="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" value="" />
      <entry key="$USER_HOME$/dev/github/ActionBarSherlock/actionbarsherlock-samples/fragments" value="" />
    </map>
  </option>
  <option name="lastPath" value="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" />
</component>

2013年1月3日木曜日

Androidで「Unknown error merging manifest」

AndroidでライブラリプロジェクトのAndroidManifest.xmlをメインプロジェクトに自動でマージさせるにはproject.propertiesにmanifestmerger.enabled=trueを加えるだけで良い。 しかし、メインプロジェクトのandroid:targetSdkVersionがライブラリプロジェクトより低いと以下のようなエラーメッセージが表示される。
Unknown error merging manifest
もっとわかりやすいメッセージを表示してほしいよね。

2013年1月2日水曜日

GradleでSlim3 + JPPのビルドスクリプトを書いてみた

Android(アプリ)も新しいビルドシステムとしてGradleを採用したこともあって、流行りに流行りまくっているGradleを試してみた。とりあえず会社でよく使うSlim3 + JsonPullParserのビルドスクリプトを書いてみた。

Gradleでaptを処理する場合はAntタスクを使うらしい。そんでもってSlim3はJava1.5のaptでJPPはJava1.6のaptを使う必要があるらしいので、これまためんどい。

実はこれ、javacのバグによってエラーメッセージが誤って報告されるので気持ち悪い。
もっと良い方法があったら誰か教えて(´・ω・`)

参考資料)

2012年6月3日日曜日

さくらVPSのUbuntu10.04(64bit)にgitolite(g3)をインストール

Ubuntu11ならaptitudeで簡単にインストール出来るみたいだけど、
Ubuntu10.04の場合、アレコレちょっとめんどかったのでインストールログを残しておくことにした。
(それとg3のインストール方法を紹介している日本語のサイトがなかったという理由も)

git-coreのインストール


$ sudo apt-get install git-core

セットアップ準備


1人目のgitolite管理者ユーザーの鍵を予め作成しておく。
$ ssh-keygen -t rsa

gitoliteユーザーを作成する。
$ sudo adduser gitolite

gitoliteユーザーから参照出来る場所に公開鍵を配置する。(あとで削除してね)
$ cp -p .ssh/id_rsa.pub /tmp/ubuntu.pub

gitoliteのインストール / セットアップ


gitoliteユーザーで行う。
$ sudo su - gitolite

githubからクローン
$ git clone git://github.com/sitaramc/gitolite

gitoliteインストール
$ gitolite/install

gitoliteのセットアップを行う。(WARNING出るけど気にしない)
$ export PATH=$HOME/gitolite/src:$PATH
$ gitolite setup -pk /tmp/ubuntu.pub
$ exit

管理用リポジトリをクローン


セットアップ時に指定した公開鍵のユーザーで管理用リポジトリをクローンする。
 (ここでは同サーバなのでlocalhost)
$ git clone gitolite@localhost:gitolite-admin.git

リポジトリを追加する場合はgitolite-admin/conf/gitolite.conf にrepoを追加してプッシュするだけ。 
ユーザーを追加する場合はgitolite-admin/keydir配下に公開鍵を 追加してプッシュするだけ。 
これでリポジトリを追加する作業がgitosisより楽になったー

2012年5月18日金曜日

さくらVPSのUbuntu10.04(64bit)にgit + maven + Jenkinsな環境構築をしてAndroidのCIが出来るまでのメモ

Androidに限らずgit + Jenkins + mavenな環境はよく使うと思うけど、初回のビルドはだいたいこける。
環境が整備出来ていないという理由でね。
ということでAndroidプロジェクトがCI出来るまでにやったことをメモしておく。

Ubuntuインストール・設定


さくらVPSにカスタムOSのUbuntu 10.04をインストール+セキュリティ設定などをした時のメモ
を参考にインストール・設定を行う。
(ここではsshのport変更はやってない)

Jenkinsインストール


Jenkinsをインストールするとjenkinsユーザーが自動で作成されるけどなんとなく先に作っておいた。
$ sudo adduser jenkins
インストールはここを参考に行う。

ビルド環境構築


openjdk-6-jdkとmavenをインストールする。
$ sudo apt-get install openjdk-6-jdk
mavenインストールはここを参考に行う。

Android SDKインストール


まず、32ビット版のソフトを動かすためにia32-libsをインストールする。(ハマりポイント)
$ sudo apt-get install ia32-libs

jenkinsユーザーから実行出来るようにする必要があるのでjenkinsユーザーで行う。
$ mkdir -p /var/lib/jenkins/tools
$ cd /var/lib/jenkins/tools
$ wget http://dl.google.com/android/android-sdk_r18-linux.tgz // バージョンは適宜変更
$ tar zxvf android-sdk_r18-linux.tgz
$ ./android-sdk-linux/tools/android update sdk -u

gitosisインストール・設定


まず、gitosisをインストールする。今までapt-getで今回はaptitudeだけど気にしないw
$ sudo aptitude install gitosis

gitosisの初期化の際に一人目の管理者の公開鍵が必要なのでgitosisユーザーから見える箇所に配置する。
(ここでは同サーバー内のubuntuユーザーを一人目の管理者とする)
$ cp -p /home/ubuntu/.ssh/id_rsa.pub /tmp/.
$ sudo su - gitosis
$ gitosis-init < /tmp/id_rsa.pub

gitリポジトリ作成


gitosis管理者ユーザーで管理情報リポジトリをクローンする。(クローン済みならpullする)
$ git clone gitosis@ホスト名:gitosis-admin.git

gitosis.confを修正する。(XXXXは任意ユーザー)
[gitosis]
loglevel=DEBUG

[group gitosis-admin]
writable = gitosis-admin
members = XXXX

[group リポジトリ名]
writable = リポジトリ名
members = XXXX jenkins

修正が終わったらjenkinsユーザーのssh公開鍵をkeydir配下に配置する。
(ssh公開鍵はパスフレーズなしで作成)
コミットしてプッシュしたら実際にリポジトリを作成する。
$ cd /srv/gitosis/repositories
$ sudo mkdir リポジトリ名.git
$ cd リポジトリ名.git
$ sudo git init --bare --shared=true
$ sudo chown -R gitosis:gitosis ./

jenkinsユーザーまたはmembersに記載した任意のユーザーから以下コマンドを実行してクローン出来ることを確認する。
$ git clone gitosis@ホスト名:リポジトリ名.git

Jenkinsセットアップ


jenkinsユーザーがGitリポジトリと通信するためknown_hostsに追加する必要がある。(重要)
(ここではGitリポジトリとJenkinsが同サーバ)
$ sudo -u jenkins ssh gitosis@localhost // yesを答える

続いてクローン時に必要になるgitconfigの設定を行う。
$ sudo su - jenkins
$ git config --global user.email "jenkins@jenkins-server"
$ git config --global user.name "jenkins"

http://your-jenkins-server:8080にアクセスする。
Jenkinsの管理 -> プラグインの管理から以下の3つを選択してインストールする。
  • Git plugin
  • Android Emulator Plugin
  • Android Lint Plugin
続いてJenkinsの管理 -> システムの設定から以下を設定する。
  • Android
    • Android SDK root : /var/lib/jenkins/tools/android-sdk-linux
  • Maven
    • Name : Default(任意)
    • MAVEN_HOME : /usr/local/maven

ビルドしてみる


上記で作成したリポジトリにプロジェクトを追加してプッシュする。
android-archetypesのandroid-with-testを使用してプロジェクトを作成する。
正常に作成出来たらリポジトリにプッシュする。(Jenkinsからクローン出来るように)
mvn archetype:generate \
  -DarchetypeArtifactId=android-with-test \
  -DarchetypeGroupId=de.akquinet.android.archetypes \
  -DarchetypeVersion=1.0.8 \
  -DgroupId=your.group \
  -DartifactId=your-project-name \
  -Dpackage=your.package

http://your-jenkins-server:8080にアクセスし新規ジョブ作成よりMaven2/3プロジェクトのビルドのジョブを作成する。

ソースコード管理システムでGitを選択し、Repository URLにgitosis@ホスト名:リポジトリ名.gitを入力する。

ビルドのルートPOMにリポジトリから見たルートのpom.xmlのパスを入力する。
ゴールとオプションにinstallを入力する。
ビルド環境の"Run an Android emulator during build"にチェックを入れ、エミュレーターの設定項目を入力する。
"Show emulator window"のチェックを外す。(重要)
ビルド後の処理の追加をクリックして成果物を保存を選択する。
保存するファイルにメインプロジェクトのapkを指定すればいい。
保存、ビルド実行して"Finished: SUCCESS"がコンソールに出力されればOK。
最後に成果物としてapkが存在していれば完了〜

2012年3月6日火曜日

android-maven-pluginのライフサイクル

android-maven-pluginのcomponents.xmlで定義されているライフサイクルを表にまとめてみた。

packagingがapkの場合(通常のAndroidアプリケーション)


phase goal
generate-sources android:generate-sources
process-resources resources:resources
compile compiler:compile
process-classes android:proguard
process-test-resources resources:testResources
test-compile compiler:testCompile
test surefire:test
prepare-package android:dex
package jar:jar, android:apk
install install:install
pre-integration-test android:internal-pre-integration-test
integration-test android:internal-integration-test
deploy deploy:deploy

packagingがapklibの場合(Androidライブラリ)


phase goal
generate-sources android:generate-sources
process-resources resources:resources
compile compiler:compile
process-classes android:proguard
process-test-resources resources:testResources
test-compile compiler:testCompile
test surefire:test
package jar:jar, android:apklib
install install:install
deploy deploy:deploy

components.xml見るとprepare-packageフェイズでandroid:emmaを呼び出しているように見えるけど
android:emmaゴールなんて見つからんぞー!?

2012年3月4日日曜日

MacOS XにIntelliJとmavenで開発環境構築しようと思ったら...

Mavenとの相性が良いというのを聞きつけてIntelliJを使い始めてそっこーエラーに遭遇。
Maven Moduleからarchetypeを選択してプロジェクトを作成しようとするとコンソールらしきところに
[FATAL_ERROR] Cannot start Maven: No valid Maven installation found. Either set the home directory in the configuration dialog or set the M2_HOME environment variable on your system.

環境変数のM2_HOMEが設定されてないよ的なメッセージが表示される。
.bash_profileに設定して読み込んでIntelliJを再起動してみても変わらず。
色々調べた結果、Mac OSのアプリケーションはbashで設定してる環境変数は読めないということがわかった。

解決方法


/etc/launchd.confを作成
sudo vi /etc/launchd.conf

内容
※ /usr/share/mavenの箇所は適宜読み替えて
setenv M2_HOME /usr/share/maven

Mac再起動
〜終わり〜

2012年2月19日日曜日

AndroidのSpinner内のテキストってselector効かなくね?


上記のように条件によってSpinnerを選択不可にしたいパターンはよくあると思う。
ついでにSpinner内のテキストの色も選択 / 選択不可に応じて色を変えたい場合もあるだろう。
通常の例に沿って下記のようなセレクタとスタイルを用意してテキストカラー変更を試みてみた。
color/spinner_text.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_enabled="false" android:color="@android:color/darker_gray" />
    <item android:color="@android:color/black" />
</selector>
values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme" parent="@android:style/Theme">
        <item name="android:spinnerItemStyle">@style/SpinnerItem</item>
    </style>

    <style name="SpinnerItem" parent="@android:style/Widget.TextView.SpinnerItem">
        <item name="android:textColor">@color/spinner_text</item>
    </style>
</resources>

だけど、なぜか選択不可状態の色が反映されず...
ということで代替案で対応させてみたのが下記。
SpinnerActivity.java
public class SpinnerActivity extends Activity implements
        OnCheckedChangeListener {

    private Spinner mSpinner;
    private RadioGroup mRadioGroup;

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

        mSpinner = (Spinner) findViewById(R.id.spinner);
        String[] brands =
                new String[] { "LARK", "Seven Stars", "MILD SEVEN", "etc" };
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                android.R.layout.simple_spinner_item, brands);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        mSpinner.setAdapter(adapter);

        mRadioGroup = (RadioGroup) findViewById(R.id.group);
        mRadioGroup.setOnCheckedChangeListener(this);
    }

    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        RadioButton radioButton = (RadioButton) findViewById(checkedId);
        if (radioButton.isChecked()) {
            spinnerControl(checkedId);
        }
    }

    /**
     * Spinnerの選択状態を制御する
     * @param checkedId
     */
    private void spinnerControl(int checkedId) {

        int color = Color.BLACK;
        switch (checkedId) {
        case R.id.yes:
            mSpinner.setEnabled(true);
            break;

        case R.id.no:
            mSpinner.setEnabled(false);
            color = Color.GRAY;
            break;
        }
        // SpinnerからTextViewを取り出してテキストカラーを設定
        TextView textView = (TextView) mSpinner.getChildAt(0);
        textView.setTextColor(color);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        int checkedId = mRadioGroup.getCheckedRadioButtonId();
        spinnerControl(checkedId);
    }
}

はい完成。ポイントは50行目〜51行目。
誰かセレクタで出来た人いたらやり方おせ〜て〜。

2012年2月18日土曜日

パッケージが更新された(バージョンが上がった)時のBroadcastについて

パッケージが更新された場合、PACKAGE_REMOVED→PACKAGE_ADDED→PACKAGE_REPLACED
の順番でBroadcastが投げられる。
個人的には違和感ありまくりなんだけど、内部的には一旦削除してから追加していて
このような挙動になっているんだと思う。(確かめてはないけど...)

しかし、パッケージ更新時のみPACKAGE_REMOVEDやPACKAGE_ADDEDの処理を
スキップしたいことはあると思う。そんな時は以下のようにインテントからデータを
抜き出して判定してやればいい。
public class PackageMonitor extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        String action = intent.getAction();

        // パッケージ更新の場合はスキップ
        if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
            return;
        }

        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
            // パッケージが追加された時にしたい処理
        } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
            // パッケージが削除された時にしたい処理
        }
    }

}

2012年2月10日金曜日

RenamingDelegatingContextを使ってみた

昨年(2011年)の8月に開催されたAndroidテスト祭りで@ussy00さんの発表を聞いてRenamingDelegatingContextの存在を
知ってから結構経ってしまったけど、ようやく仕事でDBを使う機会が出来たので使ってみた。

なにが出来るの?


RenamingDelegatingContextを使うことでテスト用のプレフィックスのついたSQLiteファイルが用意され、
毎回クリーンなデータベース環境が手に入る。

使ってみる


一応テスト対象のクラス等も紹介しておく。

Employee.java(DTO的な何か。必要に応じてシリアライズ可能に。)
※ ゲッター/セッター、import省略
public class Employee {

    public static class EmployeeColumns {

        public static final String ID = "_id"; // 社員ID

        public static final String NAME = "name"; // 社員名

        public static final String DEPARTMENT = "department"; // 部署
    }

    public static final String TABLE_NAME = "employee";

    private int id;
    private String name;
    private String department;
}

テーブル構成も簡単にしてみた。
DatabaseHelper.java(ヘルパー)
※ import省略
public class DatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "company.db";
    private static final int DATABASE_VERSION = 1;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE " + Employee.TABLE_NAME + "(" +
                Employee.EmployeeColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                Employee.EmployeeColumns.NAME + " TEXT NOT NULL," +
                Employee.EmployeeColumns.DEPARTMENT + " TEXT NOT NULL" +
                ")");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

EmployeeDao.java(データベースアクセスクラス。今回のテスト対象クラス。)
※ import省略
public class EmployeeDao {

    private DatabaseHelper mHelper;

    public EmployeeDao(Context context) {
        mHelper = new DatabaseHelper(context);
    }

    /**
     * 部署を条件に社員一覧を取得する。
     * 順不同
     * @param department
     * @return 社員一覧 0件の場合は空のリスト
     */
    public List<Employee> getEmployeeByDepartment(String department) {
        List<Employee> employees = new ArrayList<Employee>();
        SQLiteDatabase db = mHelper.getReadableDatabase();

        try {
            Cursor c = db.query(Employee.TABLE_NAME,
                    new String[]{ EmployeeColumns.ID,
                            EmployeeColumns.NAME,
                            EmployeeColumns.DEPARTMENT },
                    EmployeeColumns.DEPARTMENT + " = ?",
                    new String[]{ department }, null, null, null);
            c.moveToFirst();
            while (!c.isAfterLast()) {
                Employee employee = new Employee();
                employee.setId(c.getInt(c.getColumnIndex(EmployeeColumns.ID)));
                employee.setName(c.getString(c.getColumnIndex(EmployeeColumns.NAME)));
                employee.setDepartment(c.getString(c.getColumnIndex(EmployeeColumns.DEPARTMENT)));
                employees.add(employee);
                c.moveToNext();
            }
            c.close();
        } finally {
            db.close();
        }
        return employees;
    }
}

そして今回のポイントとなるテストクラス。
EmployeeDaoTest.java
※ import省略
public class EmployeeDaoTest extends AndroidTestCase {

    private static final String TEST_PREFIX = "test_";
    private DatabaseHelper mHelper;
    private RenamingDelegatingContext mContext;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
        // テストメソッド毎に空のテスト用DBを用意
        mHelper = new DatabaseHelper(mContext);
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        mHelper.close();
    }

    /**
     * getEmployeeByDepartmentのテスト
     */
    public void testGetEmployeeByDepartment() {
        // RenamingDelegatingContextを渡してテスト用DBを使用する
        EmployeeDao dao = new EmployeeDao(mContext);
        List<Employee> employees = dao.getEmployeeByDepartment("人事部");
        assertNotNull(employees);
        // これ以降のテストは省略
    }
}

ソースを見ればわかると思うけど手順は簡単で
  • Context と文字列を渡してRenamingDelegatingContextを生成
  • 生成したRenamingDelegatingContextを渡してヘルパーを生成
以上。

これを実行すると。。。
テスト用のDBがちゃんとあるね。

2012年1月26日木曜日

AndroidでDBの存在確認

AndroidでDBの存在確認の仕方は他にもあるけど、
データベースの保存場所がストレージであれば「/data/data/パッケージ名/databases/DB名」
に保存されるのでファイルの存在確認するのが手っ取り早い。
String DB_NAME = "test.db";

File file = new File(context.getDatabasePath(DB_NAME).getPath());
boolean dbExists = file.exists();

2012年1月11日水曜日

Androidでステータスバー(通知バー)を起動する

ステータスバーとか通知バーとかいろんな言い方あるけど、とにかくコレのこと↓

これを起動するのにちょいと手間がかかる。かかるといってもたいしたことはないけど。
まず、AndroidManifest.xmlに下記パーミッションを追加。
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />

あとはこんな感じのソースを書いて呼べばOK。
private void showNotifications() {
    try {
        Object service = getSystemService("statusbar");
        if (service != null) {
            Method expand = service.getClass().getMethod("expand");
            expand.invoke(service);
        }
    } catch (Exception e) {
    }
}

ちなみにHideなAPI使ってるのでいきなり使えなくなるかも?

2012年1月5日木曜日

twitter4jを使用してツイートするサンプルをHeroku上で動かす

RailsでHeroku上にTwitterサービスを作り始める場合「1時間でツイッターサービスを作ろう!」の記事が参考になる。
Railsもいいけど最近Androidでも活躍しているtwitter4jを使ってみたかったのでtwitter4jを使用して
ツイートをポストするWebアプリをHeroku上にデプロイしてみた。
twitter4jの作者である@yusukeさんのサンプルソースを流用するのでだいたい30分くらいで出来る。

事前準備

  • Gitインストール
  • Maven 3インストール
  • Herokuのアカウント作成
  • herokuコマンドインストール

Herokuアプリケーションの作成

$ heroku create --stack cedar
Creating blazing-mist-9962... done, stack is cedar
http://blazing-mist-9962.herokuapp.com/ | git@heroku.com:blazing-mist-9962.git

Twitterサービスの登録


https://dev.twitter.com/apps/newにアクセスして今回作成するサービスの情報を入力し登録する。
Callback URLはHerokuアプリケーションの作成時に表示されたURL + callback

Name(アプリケーション名):u1aryzの備忘録とか(任意)
Description(アプリケーションの説明):Herokuからtwitter4jを使用してツイートをポストするサンプル。(任意)
WebSite(ウェブサイトURL):http://u1aryz.blogspot.com/(任意)
Callback URL(コールバックURL):http://blazing-mist-9962.herokuapp.com/callback

登録し終わったらSettingsからApplication TypeをRead and Writeに変更。
Consumer keyとConsumer secretは後で使用するのでメモ。


twitter4jのサンプルソースをダウンロード


GitHubで公開されているtwitter4jのサンプルソースをダウンロードする。
yusuke / sign-in-with-twitter


Heroku(Jetty)用に修正&追加


pom.xml(修正)
<packaging>war</packaging>
↓変更
<packaging>jar</packaging>

<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>servlet-api</artifactId>
  <version>2.4</version>
  <scope>provided</scope>
</dependency>
↓変更
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>servlet-api</artifactId>
  <version>2.5</version>
</dependency>

pom.xml(追記)
dependenciesの子ノードに下記を追加
<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-webapp</artifactId>
  <version>7.4.5.v20110725</version>
</dependency>
<dependency>
  <groupId>taglibs</groupId>
  <artifactId>standard</artifactId>
  <version>1.1.2</version>
</dependency>
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>jstl</artifactId>
  <version>1.2</version>
</dependency>
<dependency>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>jsp-2.1-glassfish</artifactId>
  <version>2.1.v20100127</version>
</dependency>

build,pluginsの子ノードに下記を追加
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>appassembler-maven-plugin</artifactId>
  <version>1.1.1</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>assemble</goal>
      </goals>
      <configuration>
        <assembleDirectory>target</assembleDirectory>
        <programs>
          <program>
            <mainClass>twitter4j.examples.signin.StartServer</mainClass>
            <name>webapp</name>
          </program>
        </programs>
      </configuration>
    </execution>
  </executions>
</plugin>

StartServer.java(新規)
package twitter4j.examples.signin;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class StartServer {

    public static void main(String[] args) throws Exception {

        Server server = new Server(Integer.valueOf(System.getenv("PORT")));
        WebAppContext context = new WebAppContext("src/main/webapp", "/");
        context.setDescriptor("WEB-INF/web.xml");
        server.setHandler(context);

        server.start();
        server.join();
    }

}


twitter4j.properties(修正)
twitter4j.oauth.consumerKeyとtwitter4j.oauth.consumerSecretにそれぞれ上記で登録した内容を入れる。
入力したらファイル自体をプロジェクト直下に移動する。

Procfile(新規)
下記内容でプロジェクト直下に作成する。
web: sh target/bin/webapp

Herokuにデプロイ


デプロイといってもソースをプッシュするだけ。
$ cd プロジェクトディレクトリ
$ git init
$ git add .
$ git commit -m "Generate twitter app."
$ git remote add heroku git@heroku.com:blazing-mist-9962.git
$ git push heroku master

ツイートしてみる

$ heroku open

ログイン後、コールバックURLが呼ばれ下記のようなページが表示される。

公式Twitterで確認出来る通り、ツイート出来ていることがわかる。

2011年12月26日月曜日

Androidでファイルの入出力

汎用的なユーティリティー系の処理はその都度書いていては時間の無駄なので
ファイルの入出力の処理をコピペ出来るようにここに貼付けておく。
ちなみにファイルの入出力先は「/data/data/パッケージ名/files/」

FileUtils.java
package yourpackage;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;

import android.content.Context;

public class FileUtils {

    /**
     * ファイルへ文字列を書き込み
     * @param context
     * @param str ファイル出力文字列
     * @param fileName ファイル名
     */
    public static void writeFile(Context context, String str, String fileName) {
        writeBinaryFile(context, str.getBytes(), fileName);
    }

    /**
     * ファイルへバイナリデータを書き込み
     * @param context
     * @param data バイトデータ
     * @param fileName ファイル名
     */
    public static void writeBinaryFile(Context context, byte[] data, String fileName) {
        OutputStream out = null;
        try {
            out = context.openFileOutput(fileName, Context.MODE_PRIVATE);
            out.write(data, 0, data.length);
        } catch (Exception e) {
            // 必要に応じて
//            throw e;
        } finally {
            try {
                if (out != null) out.close();
            } catch (Exception e) {
            }
        }
    }

    /**
     * ファイルから文字列を読み込む
     * @param context
     * @param fileName ファイル名
     * @return 文字列 ファイルがない場合はnull
     */
    public static String readFile(Context context, String fileName) {
        String str = null;
        byte[] data = readBinaryFile(context, fileName);
        if (data != null) {
            str = new String(data);
        }
        return str;
    }

    /**
     * ファイルからバイナリデータを読み込む
     * @param context
     * @param fileName
     * @return バイトデータ ファイルがない場合はnull
     */
    public static byte[] readBinaryFile(Context context, String fileName) {
        // ファイルの存在チェック
        if (!(new File(context.getFilesDir().getPath() + "/" + fileName).exists())) {
            return null;
        }

        int size;
        byte[] data = new byte[1024];
        InputStream in = null;
        ByteArrayOutputStream out = null;

        try {
            in = context.openFileInput(fileName);
            out = new ByteArrayOutputStream();
            while ((size = in.read(data)) != -1) {
                out.write(data, 0, size);
            }
            return out.toByteArray();
        } catch (Exception e) {
            // エラーの場合もnullを返すのでここでは何もしない
        } finally {
            try {
                if (in != null) in.close();
                if (out != null) out.close();
            } catch (Exception e) {
            }
        }

        return null;
    }
}

2011年12月16日金曜日

RubyでHerokuにデプロイするまでのメモ for Mac OS X (Snow Leopard)

Ruby使ったことないけどHerokuを使いたいがためにRailsを勉強することになった。
ちなみにHerokuは現在Node.js, Clojure, Java, Python, Scalaもサポートしているので
Railsじゃなくてもいいだろってツッコミがありそうだけど、もともとRubyのPaaSなので。
ということで環境構築からHerokuでデプロイするまでの流れをメモ。

事前準備

  • Xcodeインストール
  • MacPortsインストール
  • Gitインストール
  • Herokuのアカウント作成

MacPortsアップデート

$ export PATH=$PATH:/opt/local/bin/ # MacPortsのパスを通す
$ sudo port -d selfupdate
$ sudo port -d sync

RVM(Ruby Version Manager)インストール

$ bash < <(curl -sk https://rvm.beginrescueend.com/install/rvm)
$ echo '[[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm"' >> ~/.bash_profile
$ source .bash_profile
$ rvm --version

rvm 1.10.0 by Wayne E. Seguin (wayneeseguin@gmail.com) [https://rvm.beginrescueend.com/]

Rubyインストール


Herokuでのサポートされている1.9.2をインストール。
Macにもともと入っているRubyは1.8.7なので別途インストールする必要がある。
$ rvm pkg install readline
$ rvm install 1.9.2 --with-readline-dir=$HOME/.rvm/usr
$ rvm use 1.9.2 --default
$ ruby -v
ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin10.8.0]

必要なパッケージのインストール

$ gem install rails
$ gem install sqlite3
$ gem install heroku

プロジェクト作成

$ mkdir project
$ cd project
$ rails new herokuapp

ひな形を作成してマイグレーションを実行


どうせならHello World的なやつではなくscaffoldを使ってみる。
$ cd herokuapp
$ rails g scaffold Product title:string description:text image_url:string price:decimal
$ rake db:migrate

ローカルで実行


ローカルでアクセスしてみる。
$ rake routes
    products GET    /products(.:format)          {:action=>"index", :controller=>"products"}
             POST   /products(.:format)          {:action=>"create", :controller=>"products"}
 new_product GET    /products/new(.:format)      {:action=>"new", :controller=>"products"}
edit_product GET    /products/:id/edit(.:format) {:action=>"edit", :controller=>"products"}
     product GET    /products/:id(.:format)      {:action=>"show", :controller=>"products"}
             PUT    /products/:id(.:format)      {:action=>"update", :controller=>"products"}
             DELETE /products/:id(.:format)      {:action=>"destroy", :controller=>"products"}

$ rails s
ブラウザでhttp://localhost:3000/productsにアクセス。
正常にアクセス出来たら次へ。

Heroku用に修正&追加


Herokuは無料の範囲だとPostgreSQLを使うことになるのでGemfileの内容を下記のように書き換える。
gem 'sqlite3'
↓
gem 'pg'

必要なパッケージをインストールしてGitのローカルリポジトリへコミット。
$ sudo port install postgresql84
$ sudo ln -s /opt/local/bin/psql84 /usr/bin/psql
$ gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
$ bundle install
$ RAILS_ENV=production bundle exec rake assets:precompile
$ git init
$ git add .
$ git commit -m "Generate rails app."

Herokuへデプロイ


一番最初にHerokuアプリを作成する場合、emailとパスワードの入力を求められるので入力する。
$ heroku create
Enter your Heroku credentials.
Email: u1aryz.d@gmail.com
Password: 
Creating fierce-autumn-9435... done, stack is bamboo-mri-1.9.2
http://fierce-autumn-9435.heroku.com/ | git@heroku.com:fierce-autumn-9435.git
Git remote heroku added

$ git push heroku master
$ heroku rake db:migrate

デプロイされたWebアプリへアクセス

$ heroku open
これでブラウザが立ち上がるはずなので、urlにルートのパス(/products)を追加してアクセス。
最初はだいぶ手こずったけど次からは楽ちん。
便利な時代になったなー。

2011年12月14日水曜日

Redmine1.3.0をJettyで動作出来たときの各バージョン

RedmineをJetty上で動作させる手順は「RedmineをJettyで動かす覚え書き。」に詳しく書かれている。
しかし、gemとかRails、Rackなどその他いろいろなバージョンを合わせないと動作しないので
Jetty上でRedmine1.3.0を動作出来たときの各バージョンを記録しておく。

JRubyのバージョン

$ jruby -v
jruby 1.6.5 (ruby-1.8.7-p330) (2011-10-25 9dcd388) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_26) [linux-amd64-java]

gemのバージョン

$ jruby -S gem -v
1.3.7

その他のバージョン

$ jruby -S gem list

*** LOCAL GEMS ***

actionmailer (2.3.14)
actionpack (2.3.14)
activerecord (2.3.14)
activerecord-jdbc-adapter (1.2.1)
activerecord-jdbcsqlite3-adapter (1.2.1)
activeresource (2.3.14)
activesupport (2.3.14)
bouncy-castle-java (1.5.0146.1)
i18n (0.4.2)
jdbc-sqlite3 (3.7.2)
jruby-jars (1.6.5)
jruby-openssl (0.7.4)
jruby-rack (0.9.8)
rack (1.1.2)
rails (2.3.14)
rake (0.8.7)
rubygems-update (1.3.7)
rubyzip (0.9.5)
sources (0.0.1)
warbler (1.1.0)

ちなみにgem、Rails、warbler、jruby-rackを上記のバージョンを指定してインストールしたら動作出来た。
Rubyわからんと苦労するわー。

2011年12月12日月曜日

jetty8でBASIC認証

jettyでBASIC認証する必要があったので忘れずメモ。

jetty側の設定


まず認証レルムの設定ファイルを用意する。
用意すると言ってもデフォルトで$JETTY_HOME/etc/realm.propertiesが存在するのでこちらを利用する。
下記のフォーマットで記述する。
username: password[,rolename ...]
暗号化する場合は種類に応じてOBF:、MD5:、CRYPT:のプレフィックスを付与する。
暗号化する場合は下記のようにして生成する。
$ java -cp lib/jetty-xxx.jar:lib/jetty-util-xxx.jar org.mortbay.jetty.security.Password ユーザー名 パスワード
しかし、なぜか$JETTY_HOME/lib内にorg.mortbay.jetty.security.Passwordクラスが見つからなかったので
下記2つをダウンロードして生成する。
$ cd <workディレクトリ>
$ wget http://www.java2s.com/Code/JarDownload/jetty-core-6.1.14.jar.zip
$ wget http://www.java2s.com/Code/JarDownload/jetty-util-6.1.18.jar.zip
$ unzip jetty-core-6.1.14.jar.zip
$ unzip jetty-util-6.1.18.jar.zip
$ java -cp jetty-core-6.1.14.jar:jetty-util-6.1.18.jar org.mortbay.jetty.security.Password jetty password
password
OBF:1v2j1uum1xtv1zej1zer1xtn1uvk1v1v
MD5:5f4dcc3b5aa765d61d8327deb882cf99
CRYPT:je5/ATIGzeDQw

続いてjetty.xmlに認証レルムを使用する設定を追記する。
<Configure id="Server" class="org.eclipse.jetty.server.Server">
…
    <Call name="addBean">
      <Arg>
        <New class="org.eclipse.jetty.security.HashLoginService">
          <Set name="name">User Realm</Set>
          <Set name="config">
            <!-- 上記で用意した認証レルムの設定ファイルを指定 -->
            <SystemProperty name="jetty.home" default="."/>/etc/realm.properties
          </Set>
          <Set name="refreshInterval">0</Set>
        </New>
      </Arg>
    </Call>
…
</Configure>
Webアプリケーション毎に認証レルムを指定したい場合は下記のようにする。
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
  <Set name="contextPath">/jetty</Set>
  <Set name="war"><SystemProperty name="jetty.home" default="."/>/webapps/jetty</Set>
…
  <Get name="securityHandler">
    <Set name="loginService">
      <New class="org.eclipse.jetty.security.HashLoginService">
            <Set name="name">User Realm</Set>
            <Set name="config">
              <SystemProperty name="jetty.home" default="."/>/etc/realm.properties
            </Set>
      </New>
    </Set>
  </Get>
…
</Configure>

Webアプリケーション側の設定


web.xmlにBASIC認証用の設定を追記する。
<web-app…
…
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>Authentication of BASIC</web-resource-name>
      <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>admin</role-name>
    </auth-constraint>
  </security-constraint>
  <login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>User Realm</realm-name>
  </login-config>
  <security-role>
    <role-name>admin</role-name>
  </security-role>
…
</web-app>
ちなみに$JETTY_HOME/etc/webdefault.xmlに記述すれば一律で設定可能。