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で確認出来る通り、ツイート出来ていることがわかる。