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に記述すれば一律で設定可能。

2011年12月11日日曜日

jetty8でport80起動

さくらVPSにUbuntu 10.04 LTSをインストールしたので、Jettyをインストールしてみた。
Jettyはサイズも小さくWebサーバの機能も十分あるのでJettyのみで運用することにした。
Jettyはデフォルトだとポートは8080なので80に変更して起動することにする。

Jetty起動ユーザー:jetty
インストールディレクトリ:/usr/local/jetty

jettyユーザー作成

$ sudo useradd -m jetty

jetty8インストール

$ cd <workディレクトリ>
$ wget http://dist.codehaus.org/jetty/jetty-hightide-8.1.0/jetty-hightide-8.1.0.RC1.zip
$ unzip jetty-hightide-8.1.0.RC1.zip
$ sudo mv jetty-hightide-8.1.0.RC1 /usr/local/jetty
$ sudo chown -R jetty:jetty /usr/local/jetty

jettyの設定


/usr/local/jetty/etc/jetty.xmlを修正
<Call name="addConnector">
      <Arg>
          <New class="org.eclipse.jetty.server.nio.SelectChannelConnector">
            <Set name="host"><Property name="jetty.host" /></Set>
            <Set name="port"><Property name="jetty.port" default="8080"/></Set>
↓
<Call name="addConnector">
      <Arg>
          <New class="org.eclipse.jetty.server.nio.SelectChannelConnector">
            <Set name="host"><Property name="jetty.host" /></Set>
            <Set name="port"><Property name="jetty.port" default="80"/></Set>

/usr/local/jetty/start.iniを修正
--execのコメントを外す
-Djava.library.path=lib/setuidを追加する
# --exec
# -Dorg.apache.jasper.compiler.disablejsr199=true
↓
--exec
-Djava.library.path=lib/setuid
# -Dorg.apache.jasper.compiler.disablejsr199=true

OPTIONSにsetuidを追加
OPTIONS=Server,jsp,jmx,resources,websocket,ext,plus,annotations,jta,jdbc
↓
OPTIONS=Server,jsp,jmx,resources,websocket,ext,plus,annotations,jta,jdbc,setuid

etc/jetty-setuid.xmlを追加
#===========================================================
# Configuration files.
# For a full list of available configuration files do
#   java -jar start.jar --help
#-----------------------------------------------------------
etc/jetty-jmx.xml
etc/jetty.xml
↓
#===========================================================
# Configuration files.
# For a full list of available configuration files do
#   java -jar start.jar --help
#-----------------------------------------------------------
etc/jetty-setuid.xml
etc/jetty-jmx.xml
etc/jetty.xml

jettyの起動

$ cd /usr/local/jetty/
$ sudo java -jar start.jar
これで目的は達成。
ちなみにJetty起動時に停止用のパラメーターを指定しておけば停止用のコマンドで停止可能。

起動
$ sudo java -DSTOP.PORT=停止ポート -DSTOP.KEY=停止パスワード -jar start.jar
e.g.
$ sudo java -DSTOP.PORT=8079 -DSTOP.KEY=jetty -jar start.jar
停止
$ sudo java -DSTOP.PORT=停止ポート -DSTOP.KEY=停止パスワード -jar start.jar --stop
e.g.
$ sudo java -DSTOP.PORT=8079 -DSTOP.KEY=jetty -jar start.jar --stop

※注意
起動ユーザーがjettyじゃない場合はjetty-setuid.xmlの下記の箇所に修正が必要。
<Configure id="Server" class="org.mortbay.setuid.SetUIDServer">
  <Set name="startServerAsPrivileged">false</Set>
  <Set name="umask">2</Set>
  <Set name="username">jetty</Set>
  <Set name="groupname">jetty</Set>

2011年12月8日木曜日

Gerrit起動時に「** ERROR: GERRIT_SITE not set」

Gerrit起動時や停止時に使用するgerrit.shを実行する際、以下のようなエラーが出る場合がある。
** ERROR: GERRIT_SITE not set
下記のようにシェルの配置先まで移動して実行すると発生する。
cd <Gerritインストールディレクトリ>/bin
./gerrit.sh start
↓こんな感じでエラーが解消されるはず。
cd <Gerritインストールディレクトリ>
./bin/gerrit.sh start
Gerrit使おうと思ってる人はたいていシェルの中身見たりデバッグしたりして
すぐ解消するんでしょうけど。。

理由が知りたい人はシェル内のこのあたり見たりデバッグしてみたりすればわかると思う。
##################################################
# Try to determine GERRIT_SITE if not set
##################################################
if test -z "$GERRIT_SITE" ; then
  GERRIT_SITE_1=`dirname "$0"`
  GERRIT_SITE_1=`dirname "$GERRIT_SITE_1"`
  if test -f "${GERRIT_SITE_1}/${GERRIT_INSTALL_TRACE_FILE}" ; then 
    GERRIT_SITE=${GERRIT_SITE_1} 
  fi
fi

2011年12月4日日曜日

Gitで中央リポジトリにプッシュしたらJenkinsビルドを実行させる

Jenkinsにはコミットを検出して更新があったときにビルドを実行する機能を持っているが
これは定期的にポーリングしているためJenkinsのビルドが始まるまでにタイムラグが発生する。
プッシュされたタイミングで即時にJenkinsビルドを実行させるにはGitフックを使用する。

(Git Pluginのインストールは省略)
Gitフックを使用するにはhooksディレクトリに適切なファイル名で配置する。
ここでは中央リポジトリにプッシュしたタイミングでJenkinsビルドを実行させるという想定なので
post-updateファイルを作成し、中央リポジトリのhooksディレクトリ直下に配置する。
#!/bin/sh
wget -q "http://[Jenkinsトップ画面のアドレス]/job/[ジョブ名]/build?delay=0"

if [ "$?" -eq 0 ]; then
  echo "Jenkins build run."
else
  echo "Jenkins build failed."
fi
作成したら実行権限をつけ忘れずに。
chmod +x post-update
上記スクリプトを見るとわかる通りwgetでジョブ実行のURLを叩くだけ。
ちなみにJenkinsのセキュリティが有効になっている場合は認証トークンの設定が必要。
上記のURLに下記を追加。
&token=[認証トークン]
もちろんシェルスクリプトじゃなくてもpythonでもrubyでもお好きなのでどうぞ。

2011年11月28日月曜日

lime-mvcを使ってみた

久々にWebアプリを作る機会があったのでlime-mvcを使ってみた。
今のところ(2011/11/28現在)あまり知名度がなく、日本語の記事など一切存在していないので
今後の参考にメモ。
lime-mvc - http://code.google.com/p/lime-mvc/

lime-mvcとは

Google Guiceを拡張して作られたMVCパターンのフレームワーク。

セットアップ

lime-mvcはMavenリポジトリから利用可能になっているのでpom.xmlに下記を追加するだけ。
<dependency>
  <groupId>org.zdevra</groupId>
  <artifactId>lime-velocity</artifactId>
  <version>0.2.0.rc2</version>
</dependency>
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.0.1</version>
  <scope>provided</scope>
</dependency>
<!-- ViewとしてJSPを使う場合は必要 -->
<!-- Velocity等を使用する場合は不要 -->
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>jstl</artifactId>
  <version>1.2</version>
  <scope>provided</scope>
</dependency>

lime-mvcを使用する場合、MvcModuleのconfigureControllersをオーバーライドして
URLパターンとコントローラーのマッピングを行う。
これはアプリケーション起動時に呼ぶ必要があるため、GuiceServletContextListenerを継承して
デプロイメントディスクリプタ(web.xml)にリスナーとして登録する必要がある。
public class WebAppContextListener extends GuiceServletContextListener {

    @Override
    protected Injector getInjector() {
        return Guice.createInjector(new MvcModule() {
            @Override
            protected void configureControllers() {
                // URLパターンとコントローラーのマッピングを行う
                control("/samples/*").withController(WebAppController.class);
            }
        });
    }
}

Guiceは上記リスナーでマッピングしたデータをGuiceFilterでキャッチしてDispatchするので
ここではすべてのリクエストに対してGuiceFilterを通す設定をweb.xmlに記述する。
<filter>
  <filter-name>guiceFilter</filter-name>
  <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>guiceFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
  <listener-class>jp.u1aryz.products.samples.lime.listener.WebAppContextListener
  </listener-class>
</listener>

/helloworld/(.*)というパスに対してリクエストがあった場合にdoActionメソッドを呼び出し
main.jspに転送する場合のコントローラーは下記の通りとなる。
メソッドの戻り値をデータとしてビューに渡すことが出来る。
@Controller
public class WebAppController {

    @Path("/helloworld/(.*)")
    @ModelName("msg")
    @ToView("main.jsp")
    public String doAction(@UriParameter(1) String name) {
        return "Hello World " + name + "!";
    }
}

coreタグライブラリでModelNameアノテーションより指定された変数名で出力することにより、
JSPで表示することが出来る。
<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %>
<html>
<head>
</head>
<body>
<c:out value="${msg}" />
</body>
</html>

コンテキストパスが"/"の場合はhttp://localhost:8080/samples/helloworld/hogeでアクセスすれば
下記のように出力されるはず。
Hello World hoge!

ほかにもlime-mvcはビューとしてJSilverやVelocity、Freemarkerを使えるので
Velocity使いとしてはありがたや〜。

2011年11月20日日曜日

いつからかEclipse上でAndroidプロジェクトのMavenビルドが出来なくなったので…

久々にMavenを使用してAndroidアプリを作成しようと思ったらなぜかビルドが出来ない。。
ADTやらSDKを更新したタイミングで使えなくなったのだろうと思い、いろいろ思考錯誤してみた。
そしてようやくビルドが出来る環境を作成する手順が確立出来たので忘れずメモ。

ちなみに私の環境は以下の通り。
Mac OS X 10.6.8
ADT 15 & SDK15
Maven 3.0.3
Eclipse Indigo

m2eのインストール

[Help] -> [Install New Software...]
プルダウンからIndigoのアップデートサイトを選択しCollaborationを展開する。
「Maven Integration for Eclipse」と「slf4j over logback loggind」にチェックし、-> [Next]
あとは指示通り進んでインストール完了後Eclipseを再起動。

m2e-androidのインストール

[Help] -> [Eclipse Marketplace...]
「m2e-android」と検索すると「Android Configuration for M2E」がヒットするので
Installボタンをクリックし指示通り進んでインストール完了後Eclipseを再起動。

Archetypeの追加

[New] -> [Project...] -> [Maven Project]
Workspace locationに任意の場所を指定 -> [Next] -> [Add Archetype...]
下記のような画面になるので各項目を入力 -> [OK]
Archetype Group Id: de.akquinet.android.archetypes
Archetype Artifact Id : android-quickstart
Archetype Version: 1.0.6
Repository URL: 空白
上記で追加したandroid-quickstartを選択しAndroidプロジェクトを作成する。

m2e connectorのインストール

Androidプロジェクトを作成すると、下記画面のようにpom.xmlにエラーマークがつくので
POMエディターで開く。
pom.xmlを開くと上記に赤くメッセージが表示されるのでその箇所をクリック。
下記のような黄色いウィンドウが表示されるので「Discover new m2e connectors」をクリック。
m2e Marketplaceなるものが開くので「Application」と「Maven」にチェックをし、
「Lifecycle Mappings」と「embedded maven runtimes」をインストールしEclipseを再起動。

pom.xmlの修正

<artifactId>maven-android-plugin</artifactId>
<version>2.8.4</version>
↓
<artifactId>android-maven-plugin</artifactId>
<version>3.0.0-alpha-14</version>
以上でEclipse上でAndroidプロジェクトのMavenビルドが出来るようになるはず。

ちなみにpom.xmlの全体はこんな感じ。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>jp.u1aryz.products.hoge</groupId>
  <artifactId>HogeSample</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>apk</packaging>
  <name>HogeSample</name>

  <dependencies>
    <dependency>
      <groupId>com.google.android</groupId>
      <artifactId>android</artifactId>
      <version>2.1.2</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <finalName>${project.artifactId}</finalName>
    <sourceDirectory>src/main/java</sourceDirectory>
    <plugins>
      <plugin>
        <groupId>com.jayway.maven.plugins.android.generation2</groupId>
        <artifactId>android-maven-plugin</artifactId>
        <version>3.0.0-alpha-14</version>
        <configuration>
          <sdk>
            <!-- SDKのパスはsettings.xmlで定義していれば不要 -->
            <path>"your ANDROID_HOME"</path>
            <platform>7</platform>
          </sdk>
          <manifest>
            <debuggable>true</debuggable>
          </manifest>
        </configuration>
        <extensions>true</extensions>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

2011年11月18日金曜日

RoboGuiceでAndroidアプリのDI開発

今更ながらRoboGuiceを使ってみたので忘れずメモ。
RoboGuiceについて簡単に触れておくとGoogle GuiceベースのAndroid用のDIコンテナである。
roboguice - Project Hosting on Google Code

細かいViewの制御は抜きにして以下のようなアプリを例としてRoboGuiceを使用して作成してみる。
  • トップ画面より入力された名前を次の画面で文字列を付け足して表示する。




  • まず前エントリーを参考にMavenプロジェクトからAndroidプロジェクトを作成。
    pom.xmlを追記
    <dependency>
      <groupId>org.roboguice</groupId>
      <artifactId>roboguice</artifactId>
      <version>1.1.2</version>
    </dependency>
    

    TopActivity
    public class TopActivity extends RoboActivity {
    
        @InjectResource(R.string.message)
        String message;
    
        @InjectView(R.id.txt_msg)
        TextView mTextView;
    
        @InjectView(R.id.edt_name)
        EditText mEditText;
    
        @InjectView(R.id.btn_decision)
        Button mButton;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
    //        mTextView.setText(message); // ここでViewにアクセスするとエラー
            setContentView(R.layout.top); // ここでViewがInjectされる
            mTextView.setText(message);
            mButton.setOnClickListener(new OnClickListener() {
    
                public void onClick(View v) {
                    Intent intent = new Intent(TopActivity.this, HelloActivity.class);
                    intent.putExtra("name", mEditText.getText().toString());
                    startActivity(intent);
                }
            });
        }
    
    }
    
    
    RoboActivityを継承してアクティビティを作成する。※1.1からGuiceActivityからRoboActivityに変更された
    ListActivityの場合はRoboListActivity、TabActivityの場合はRoboTabActivityなどそれぞれ対応したもの
    が存在する。
    ソースを見るとわかる通り、@InjectView(resouceId)でViewをInjectしてくれる。
    リソースを参照したい場合は@InjectResource(resourceId)を使用すればいい。
    他にもこんなことが可能である。
    Drawable icon = getResources().getDrawable(R.drawable.icon);
    ↓
    @InjectResource(R.drawable.icon)
    Drawable icon;
    
    LocationManager loc = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);
    ↓
    @Inject
    LocationManager loc;
    

    HelloActivityを実装する前にインターフェースと実装クラスを使用して
    少しDIっぽい処理を入れてみる。
    RoboSampleService
    public interface RoboSampleService {
    
        public String hello(String name);
    }
    
    

    RoboSampleServiceImpl
    public class RoboSampleServiceImpl implements RoboSampleService {
    
        public String hello(String name) {
            return "Hello " + name;
        }
    
    }
    
    

    続いて上記サービスをバインド
    MyApplication
    public class MyApplication extends RoboApplication {
    
        @Override
        protected void addApplicationModules(List<Module> modules) {
            modules.add(new MyModule());
        }
    
        static class MyModule extends AbstractAndroidModule {
    
            @Override
            protected void configure() {
                // RoboSampleServiceのInject要求に対してRoboSampleServiceImplを返すようバインドする
                bind(RoboSampleService.class).to(RoboSampleServiceImpl.class);
            }
    
        }
    }
    
    ※こちらもGuiceApplicationからRoboApplicationに変更されているので注意

    アプリケーションのエントリポイントとして上記クラスを呼び出す必要があるので
    AndroidManifest.xmlに以下を追加
    <application...
        android:name=".MyApplication"
        ...>
    

    最後にHelloActivityの実装
    HelloActivity
    public class HelloActivity extends RoboActivity {
    
        @InjectExtra("name")
        String name;
    
        @InjectView(R.id.hello_msg)
        TextView textView;
    
        @Inject
        RoboSampleService service;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.hello);
            textView.setText(service.hello(name));
        }
    
    }
    

    @InjectExtra("name")でTopActivityでputExtraした値が入る。
    また、RoboSampleServiceに@Injectアノテーションをつけることで上記でバインドした
    RoboSampleServiceImplをInjectしてくれるのでインスタンスを生成せず使用することが出来る。

    続いてDIの長所を活かしてHelloActivityのテストを書いてみる。
    テストプロジェクトを作成する前にテスト対象のプロジェクトのクラスパスをエクスポート。

    通常通りAndroidテストプロジェクトを作成する。
    今回作成したサンプルプログラムのhelloメソッドは静的な文字列を付加するだけの
    シンプルな作りであるが、実際は動的であったり、バグが混入していたり、
    決まっていなかったりするので依存するクラスの振る舞いを固定化するモックを作成する。

    MockServiceImpl
    public class MockServiceImpl implements RoboSampleService {
    
        @Override
        public String hello(String name) {
            return "test";
        }
    
    }
    

    続いてテストコードの実装
    HelloActivityTest
    public class HelloActivityTest extends RoboActivityUnitTestCase<HelloActivity> {
    
        private Context mContext;
    
        public HelloActivityTest() {
            super(HelloActivity.class);
        }
    
        class MockApplication extends RoboApplication {
    
            public MockApplication(Context context) {
                super();
                attachBaseContext(context);
            }
    
            @Override
            protected void addApplicationModules(List<Module> modules) {
                modules.add(new MockModule());
            }
    
        }
    
        class MockModule extends AbstractAndroidModule {
    
            @Override
            protected void configure() {
                // RoboSampleServiceのInject要求に対してMockServiceImplを返すようバインドする
                bind(RoboSampleService.class).to(MockServiceImpl.class);
            }
    
        }
    
        @Override
        protected void setUp() throws Exception {
            super.setUp();
            mContext = getInstrumentation().getContext();
            setApplication(new MockApplication(mContext));
        }
    
        @MediumTest
        public void testShouldBeHelloMsg() {
            Intent intent = new Intent(mContext, HelloActivity.class);
            intent.putExtra("name", "name");
            HelloActivity a = startActivity(intent, null, null);
            assertNotNull(a);
    
            // Mockで返す文字列を"test"にしているため
            assertEquals(((TextView)a.findViewById(R.id.hello_msg)).getText(), "test");
        }
    }
    

    RoboActivityUnitTestCase<対象アクティビティ>を継承してテストコードを作成する。
    RoboSampleServiceのInject要求に対してモックを返すようにバインドさせ、
    setApplicationでセットすることで依存したオブジェクトの振る舞いを固定化させることが出来る。

    Run As -> Android JUnit Testで...
    これで一通り完了。案外使いやすいかも?

    2011年9月25日日曜日

    Androidでオブジェクト/XMLマッピング

    TwitterクライアントなどXMLやJSONをWeb上から取得してアプリに表示することは少なくない。
    しかし、XMLやJSONをパースするような単調な処理は出来るだけ無くしたいので、
    Spring AndroidとSimple(xmlシリアルフレームワーク)を使用してXMLデータを取得し、
    アプリに表示させてみた。
    今回はイベント開催支援ツールのATNDのAPIを使用してAndroid系イベントを取得して
    イベントタイトルとイベント日時をアプリにリスト表示してみる。
    実際に使用するAPI→http://api.atnd.org/events/?keyword=android&format=xml
    サンプルプロジェクトのダウンロードリンクは記事の最後。

    まず前エントリーを参考にMavenプロジェクトからAndroidプロジェクトを作成。

    pom.xmlを追記
    <dependency>
      <groupId>org.simpleframework</groupId>
      <artifactId>simple-xml</artifactId>
      <version>2.6.1</version>
      <exclusions>
        <exclusion>
          <artifactId>stax</artifactId>
          <groupId>stax</groupId>
          </exclusion>
          <exclusion>
            <artifactId>stax-api</artifactId>
            <groupId>stax</groupId>
          </exclusion>
          <exclusion>
            <artifactId>xpp3</artifactId>
            <groupId>xpp3</groupId>
          </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.android</groupId>
      <artifactId>spring-android-rest-template</artifactId>
      <version>1.0.0.M4</version>
    </dependency>
    
    <repositories>
      <repository>
        <id>org.springframework.maven.milestone</id>
        <name>Spring Maven Milestone Repository</name>
        <url>http://maven.springframework.org/milestone</url>
        <snapshots><enabled>false</enabled></snapshots>
      </repository>
    </repositories>
    

    次にXMLデータをマッピングさせるオブジェクトを作成する。
    要素には@Element、属性には@Attributeをつける。
    @Element、@Attributeのnameは要素名(属性名)と変数名が同じであれば省略可能。
    XMLのすべての要素をオブジェクトにマッピングしない場合は@Rootアノテーションにstrict = false
    をつける必要がある。
    Event.java
    package jp.u1aryz.products.atndsearch;
    
    import org.simpleframework.xml.Element;
    import org.simpleframework.xml.Root;
    
    @Root(name = "event", strict = false)
    public class Event {
    
        @Element
        private String title;
    
        @Element(name = "started_at")
        private String eventDate;
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setEventDate(String eventDate) {
            this.eventDate = eventDate;
        }
    
        public String getEventDate() {
            return eventDate;
        }
    }
    

    リストの場合は@ElementListアノテーションを使う。
    EventList.java
    package jp.u1aryz.products.atndsearch;
    
    import java.util.List;
    
    import org.simpleframework.xml.ElementList;
    import org.simpleframework.xml.Root;
    
    @Root(name = "events", strict = false)
    public class EventList {
    
        @ElementList
        private List<Event> events;
    
        public void setEvents(List<Event> events) {
            this.events = events;
        }
    
        public List<Event> getEvents() {
            return events;
        }
    }
    

    そしてXMLデータからオブジェクトにマッピングさせる処理を作成する。
    HTTP通信は通常、UIスレッドとは別のスレッドで行うため、今回のポイントとなる処理も
    doInBackground内に入れる。
    GetAndroidEventActivity.java
    package jp.u1aryz.products.atndsearch;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.client.RestTemplate;
    
    import android.app.ListActivity;
    import android.app.ProgressDialog;
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.util.Log;
    
    public class GetAndroidEventActivity extends ListActivity {
    
        private static String TAG = GetAndroidEventActivity.class.getSimpleName();
        private ProgressDialog progressDialog;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            // ATND APIの呼び出しを非同期で行う
            new GetEventTask().execute();
        }
    
        private void refreshEvents(List<Event> events) {
            if (events != null) {
                EventListAdapter adapter = new EventListAdapter(this, events);
                setListAdapter(adapter);
            }
        }
    
        public void showProgressDialog() {
            if (progressDialog == null) {
                progressDialog = new ProgressDialog(this);
                progressDialog.setIndeterminate(true);
            }
    
            progressDialog.setMessage("Loading. Please wait...");
            progressDialog.show();
        }
    
        public void dismissProgressDialog() {
            if (progressDialog != null) {
                progressDialog.dismiss();
            }
        }
    
        private class GetEventTask extends AsyncTask<Void, Void, List<Event>> {
    
            @Override
            protected void onPreExecute() {
                // プログレスバーを表示
                showProgressDialog();
            }
    
            @Override
            protected List<Event> doInBackground(Void... params) {
                try {
                    // Android関連のイベントを検索するURL
                    String url =
                        "http://api.atnd.org/events/?keyword=android&format=xml";
                    // Acceptヘッダに"application/xml"をセット
                    HttpHeaders requestHeaders = new HttpHeaders();
                    List<MediaType> acceptableMediaTypes =
                        new ArrayList<MediaType>();
                    acceptableMediaTypes.add(MediaType.APPLICATION_XML);
                    requestHeaders.setAccept(acceptableMediaTypes);
    
                    HttpEntity<?> requestEntity =
                        new HttpEntity<Object>(requestHeaders);
                    RestTemplate restTemplate = new RestTemplate();
    
                    // HTTPのGETリクエストを実行(マッピングさせるクラスを指定する)
                    ResponseEntity<EventList> responseEntity =
                        restTemplate.exchange(url,
                                            HttpMethod.GET,
                                            requestEntity,
                                            EventList.class);
    
                    // イベントリストを取得
                    EventList eventList = responseEntity.getBody();
    
                    return eventList.getEvents();
                } catch (Exception e) {
                    Log.e(TAG, e.getMessage(), e);
                }
    
                return null;
            }
    
            @Override
            protected void onPostExecute(List<Event> result) {
                // プログレスバーを非表示
                dismissProgressDialog();
    
                // 結果を返す
                refreshEvents(result);
            }
        }
    }
    

    最後にViewに取得したデータをセットするAdapterの作成。
    特別な処理はしていないので、通常のAndroidの知識で理解出来ると思う。
    EventListAdapter.java
    package jp.u1aryz.products.atndsearch;
    
    import java.util.List;
    
    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.TextView;
    
    public class EventListAdapter extends ArrayAdapter<Event> {
    
        private LayoutInflater mInflater;
    
        public EventListAdapter(Context context, List<Event> objects) {
            super(context, 0, objects);
            mInflater = LayoutInflater.from(context);
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
    
            if (convertView == null) {
                convertView =
                    mInflater.inflate(android.R.layout.simple_list_item_2, null);
                holder = new ViewHolder();
                holder.title =
                    (TextView) convertView.findViewById(android.R.id.text1);
                holder.eventDate =
                    (TextView) convertView.findViewById(android.R.id.text2);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
    
            Event event = getItem(position);
            holder.title.setText(event.getTitle());
            holder.eventDate.setText(event.getEventDate());
    
            return convertView;
        }
    
    }
    class ViewHolder {
        TextView title;
        TextView eventDate;
    }
    

    <uses-permission android:name="android.permission.INTERNET" />を忘れなければ
    下記のようにイベントを取得出来る。
    日時のフォーマットが見づらいけど。。。
    これで単調なパース処理とおさらば〜
    時間があればJSONバージョンもやるかも!?

    ダウンロードはこちらから

    2011年9月23日金曜日

    MavenでAndroidアプリケーションの構成管理をはじめる手順

    最近ではサードパーティー製のAndroidフレームワークやライブラリが増加してきており、
    構成管理にMavenを使用すると便利である。
    Maven初心者ながらAndroidプロジェクトの構成管理を始めてみたのでその時の手順をメモ。
    2011年11月20日に更新しました。

    <事前環境>
    Apache Maven 3.0.3
    Eclipse 3.7 + ADT v12
    ※環境のインストール手順については割愛
    1. M2_REPOの設定
      EclipseからMavenを利用するために下記コマンドを実行
      mvn -Declipse.workspace=<path-to-eclipse-workspace> eclipse:add-maven-repo
    2. Eclipseプラグインのインストール
      Update siteから下記の2つのプラグインを追加
      ( [Help] -> [Install New Software...] -> [Add...] )
      • Maven Integration for Eclipse
        http://m2eclipse.sonatype.org/sites/m2e
      • Maven Integration for Android Development Tools
        https://svn.codespot.com/a/eclipselabs.org/m2eclipse-android-integration/updates/m2eclipse-android-integration/
    3. Archetypeの追加
      現在ではGitHub等に多数Archetypeが公開されているので、今回はこちらを利用する。
      [New] -> [Project...] -> [Maven Project]
      Workspace locationに任意の場所を指定 -> [Next] -> [Add Archetype...]
      下記のような画面になるので各項目を入力 -> [OK]


      Archetype Group Id: de.akquinet.android.archetypes
      Archetype Artifact Id : android-quickstart
      Archetype Version: 1.0.5
      Repository URL: 空白
    4. Androidプロジェクトの作成
      上記で追加したandroid-quickstartを選択 -> [Next]
      下記のような画面になったら各項目に任意の値を入力 -> [Finish]

    以上でAndroidアプリもMavenで構成管理が出来るようになるはず。

    Androidへのデプロイはプロジェクト配下で下記コマンドを実行する。
    mvn clean install android:deploy

    2011年9月20日火曜日

    Ubuntu 11.04にRedmineをインストールする

    最近ではfluxflexでボタンを数回クリックすればRedmineの環境は用意出来てしまう程便利になった。
    試しにUbuntuにインストールしてみたら結構めんどうだったのでインストールログをメモ。

    環境はUbuntu 11.04(Ubuntu標準アプリ以外は空)
    最終目標はRedmine 1.0.5をインストールし、
    http://localhost/redmineでRedmineにアクセス出来るようにする

    ※Redmineの各バージョンで必要となるRailsのバージョンはこちらを参照

    まずはRubyをインストール
    $ sudo apt-get install ruby
    $ ruby -v
    ruby 1.8.7 (2010-08-16 patchlevel 302) [x86_64-linux]
    

    RubyGems(Ruby用のパッケージ管理システム)をインストール
    $ cd /home/u1aryz/work
    $ sudo wget http://rubyforge.org/frs/download.php/70696/rubygems-1.3.7.tgz
    $ sudo tar zxvf rubygems-1.3.7.tgz
    $ cd rubygems-1.3.7
    $ sudo ruby setup.rb
    $ gem1.8 -v
    1.3.7
    
    $ sudo apt-get install rubygems1.8
    $ gem -v
    1.3.7
    

    Ruby on Railsをインストール
    $ sudo gem install rails -v=2.3.5
    $ rails -v
    Rails 2.3.5
    

    Rackをインストール
    $ sudo gem install rack -v=1.0.1
    

    SQLite及び開発用ライブラリをインストール
    $ sudo apt-get install sqlite libsqlite3-dev
    

    i18nをインストール(多言語対応するため?)
    $ sudo gem install -v=0.4.2 i18n
    

    sqlite3-ruby(SQLiteのRuby用アダプタ)をインストール
    $ sudo gem install sqlite3-ruby
    

    Redmineをインストール
    $ cd /home/u1aryz/work
    $ sudo wget http://rubyforge.org/frs/download.php/73692/redmine-1.0.5.tar.gz
    $ sudo tar zxvf redmine-1.0.5.tar.gz
    $ sudo mv redmine-1.0.5 /usr/local/.
    

    Redmineの設定
    $ cd /usr/local/redmine-1.0.5/config
    $ cp -p database.yml.example database.yml
    $ vi database.yml
    

    database.ymlの中を一部修正(+が追加、-が削除)
    production:
    +   adapter: sqlite3
    +   dbfile: db/redmine.db
    +   timeout: 5000
    -   adapter: mysql
    -   database: redmine
    -   host: localhost
    -   username: root
    -   password:
    -   encoding: utf8
    

    セッションストア秘密鍵を生成する
    $ rake config/initializers/session_store.rb
    

    テーブルを作成する
    $ rake db:migrate RAILS_ENV=production
    

    確認のためWEBrickで起動し、ブラウザからアクセスする。
    $ cd /usr/local/redmine-1.0.5/
    $ ruby script/server webrick -e production
    
    http://localhost:3000でアクセス出来れば次へ

    Apache及び開発用ライブラリをインストール
    $ sudo apt-get install apache2 apache2-prefork-dev
    

    OpenSSLの開発ライブラリをインストール
    $ sudo apt-get install libcurl4-openssl-dev libcurl4-openssl-dev
    

    Phusion Passengerをインストール
    $ sudo gem install passenger
    $ sudo passenger-install-apache2-module
    

    Apacheの設定
    $ cd /etc/apache2/sites-available
    $ sudo vi default
    

    defaultの中の中を一部修正(+が追加、-が削除)
    + LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-3.0.9/ext/apache2/mod_passenger.so
    + PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-3.0.9
    + PassengerRuby /usr/bin/ruby1.8
    
      <VirtualHost *:80>
          ServerAdmin webmaster@localhost
    +     RailsBaseURI /redmine
          DocumentRoot /var/www
          <Directory />
              Options FollowSymLinks
              AllowOverride None
          </Directory>
          <Directory /var/www/>
              Options Indexes FollowSymLinks MultiViews
              AllowOverride None
              Order allow,deny
              allow from all
          </Directory>
    

    リンクを貼る
    $ cd /var/www/
    $ sudo ln -s /usr/local/redmine-1.0.5/public redmine
    

    Apacheの再起動
    $ sudo /etc/init.d/apache2 restart
    

    http://localhost/redmineでブラウザからアクセス出来れば終了~

    2011年7月14日木曜日

    AndroidのXMLで2次元配列を定義してソースから呼び出す

    Androidでは文字列などのリソースをXMLで定義すると何かと便利なので、
    2次元配列をXMLで定義してソースからアクセスする方法を忘れずにメモ。

    XML
    <array name="array_parent">
        <item >@array/array_sub1</item>
        <item >@array/array_sub2</item>
    </array>
    
    <string-array name="array_sub1" >
        <item >data1</item>
        <item >data2</item>
        <item >data3</item>
    </string-array>
    
    <string-array name="array_sub2" >
        <item >data4</item>
        <item >data5</item>
        <item >data6</item>
    </string-array>
    

    Java
    TypedArray typedArray = getResources().obtainTypedArray(R.array.array_parent);
    // 配列の要素数(ここではarray_parentの子要素の数)
    int length = typedArray.length();
    
    // 子要素の配列のリソースIDを取得(ここではarray_sub1のリソースID)
    int resourceId = typedArray.getResourceId(0, 0);
    
    // 配列の値を取得(ここではarray_sub1の各値)
    String[] array = getResources().getStringArray(resourceId);
    


    これに多少の応用を利かせればDrawable配列などの多次元配列も可能となる。

    2011年5月29日日曜日

    Androidでスクリーンロック解除しなくても操作可能な画面をつくる&呼び出す


    音楽アプリなどで音楽再生中にスクリーンロックがかかった場合、
    再度音楽を操作するのにいちいちスクリーンロックを解除するのはかなりの手間である。

    Androidマーケットで常に上位のPowerAMPはこのように操作可能になっている。
    このようなアプリではユーザビリティを考慮する上でも必要な機能となる。

    そこで今回はスクリーンロック解除しなくても操作可能な画面&呼び出しを
    ミニマム構成で作成してみた。
    サンプルプロジェクトのダウンロードリンクは記事の最後。

    構成は以下の通り。
    1. 画面の電源ONの通知を受け取るレシーバーの登録/登録解除を行うサービス
      ScreenStateService
    2. スクリーンロック解除画面より手前に表示させる画面
      ScreenLockEnabledActivity
    3. "2"の画面を表示させるか否かを設定する画面
      SetActivity
    まず、"画面の電源ONの通知を受け取るレシーバーの登録/登録解除を行うサービス"を作成する。
    画面の電源が入るとACTION_SCREEN_ONが通知される。
    ACTION_SCREEN_ONを受け取るには明示的にregisterReceiverする必要がある。
    ScreenStateService.java
    package jp.u1aryz.products.screenlockenable;
    
    import android.app.Service;
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.os.IBinder;
    
    public class ScreenStateService extends Service {
    
        private BroadcastReceiver mScreenOnListener = new BroadcastReceiver() {
    
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
    
                // 画面の電源が入ったらActivityを起動
                if (action.equals(Intent.ACTION_SCREEN_ON)) {
                    Intent i = new Intent(context, ScreenLockEnabledActivity.class);
                    i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    context.startActivity(i);
                }
            }
        };
    
        @Override
        public void onStart(Intent intent, int startId) {
            super.onStart(intent, startId);
            // ACTION_SCREEN_ONを受け取るBroadcastReceiverを登録
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_SCREEN_ON);
            registerReceiver(mScreenOnListener, filter);
        }
    
        @Override
        public void onDestroy() {
            // BroadcastReceiverを登録解除
            unregisterReceiver(mScreenOnListener);
    
            super.onDestroy();
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }
    
    サービスが起動されたらレシーバーの登録、サービスが停止されたらレシーバーの登録解除を行う。
    かなり簡略化してシンプルな作りにしている。

    そして今回のポイントとなる"Lock解除画面より手前に表示させる画面"を作成する。
    ScreenLockEnabledActivity.java
    package jp.u1aryz.products.screenlockenable;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.Window;
    import android.view.WindowManager;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    
    public class ScreenLockEnabledActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.lock);
    
            // Lock解除画面より手前に表示させる
            final Window win = getWindow();
            win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                    | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
    
            Button btnRelease = (Button) findViewById(R.id.btn_release);
            btnRelease.setOnClickListener(new OnClickListener() {
    
                @Override
                public void onClick(View v) {
                    // Activityを終了することでLock解除画面に移る
                    finish();
                }
            });
        }
    }
    
    こちらもかなりミニマムであるが、このActivityに各々操作できるウィジェット(View)等を
    配置するといいと思う。

    続いて"上記の画面を表示させるか否かを設定する画面"を作成する。
    通常はPreferenceActivityなどで実装すると良い。
    SetActivity.java
    package jp.u1aryz.products.screenlockenable;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.content.SharedPreferences;
    import android.os.Bundle;
    import android.widget.CheckBox;
    import android.widget.CompoundButton;
    import android.widget.CompoundButton.OnCheckedChangeListener;
    
    public class SetActivity extends Activity {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            // チェックボックスの値を保存するために使用
            final SharedPreferences pref = getSharedPreferences("pref", MODE_PRIVATE);
            final Intent intent = new Intent(SetActivity.this, ScreenStateService.class);
    
            CheckBox chbEnable = (CheckBox) findViewById(R.id.chb_enable);
            chbEnable.setChecked(pref.getBoolean("is_lockEnable", false));
            chbEnable.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    // チェックされたらサービスを起動
                    if (isChecked) {
                        pref.edit().putBoolean("is_lockEnable", isChecked).commit();
                        startService(intent);
                    // チェックが外されたらサービスを停止
                    } else {
                        pref.edit().putBoolean("is_lockEnable", isChecked).commit();
                        stopService(intent);
                    }
                }
            });
    
        }
    }
    
    端末の再起動のパターンやその他もろもろ対応しきれてないけどあしからず〜

    ダウンロードはこちらから

    2011年3月27日日曜日

    ListViewにグループタイトルをつける


    上記のようにアルファベットなどでグルーピングされたリストに
    グループタイトルをつける方法を考えてみた。
    (PreferenceCategoryのタイトルのようなものです)

    まず、ListViewの1行分のレイアウトを用意する。
    その際、グループタイトル+通常要素(アルバム)となるようにする。
    上記のようなアルバム一覧の場合、下記のようなレイアウトを用意する。


    具体的なレイアウトのXMLは下記のようになる。
    list_item.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        >
        <!-- グループタイトル -->
        <TextView
            android:id="@+id/groupTitle"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:background="#280030"
            android:textColor="#c62b00"
            android:textStyle="bold"
            android:paddingLeft="5dp"
            />
        <!-- アルバム名 -->
        <TextView
            android:id="@+id/albumName"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="24sp"
            android:paddingLeft="5dp"
            />
        <!-- アーティスト名 -->
        <TextView
            android:id="@+id/artistName"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="5dp"
            />
    </LinearLayout>
    

    続いてArrayAdapterを継承したクラスを実装する。
    MyAdapter.java
    package jp.u1aryz.products.grouptitle;
    
    import java.util.List;
    
    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.TextView;
    
    class BindData {
        String groupTitle;
        String albumName;
        String artistName;
    
        public BindData(String groupTitle, String albumName, String artistName) {
            this.groupTitle = groupTitle;
            this.albumName = albumName;
            this.artistName = artistName;
        }
    }
    
    class ViewHolder {
        TextView groupTitle;
        TextView albumName;
        TextView artistName;
    }
    
    public class MyAdapter extends ArrayAdapter<BindData> {
    
        private LayoutInflater inflater;
    
        public MyAdapter(Context context, List<BindData> objects) {
            super(context, 0, objects);
            this.inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
    
        @Override
        public boolean isEnabled(int position) {
            // BindDataのgroupTitleが!nullの場合、選択不可にする
            return getItem(position).groupTitle == null;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
    
            if (convertView == null) {
                convertView = inflater.inflate(R.layout.list_item, parent, false);
                holder = new ViewHolder();
                holder.groupTitle = (TextView) convertView.findViewById(R.id.groupTitle);
                holder.albumName = (TextView) convertView.findViewById(R.id.albumName);
                holder.artistName = (TextView) convertView.findViewById(R.id.artistName);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
    
            BindData data = getItem(position);
            // グループタイトル
            if (!isEnabled(position)) {
                holder.groupTitle.setVisibility(View.VISIBLE);
                holder.groupTitle.setText(data.groupTitle);
                holder.albumName.setVisibility(View.GONE);
                holder.artistName.setVisibility(View.GONE);
            // アルバム
            } else {
                holder.groupTitle.setVisibility(View.GONE);
                holder.albumName.setVisibility(View.VISIBLE);
                holder.albumName.setText(data.albumName);
                holder.artistName.setVisibility(View.VISIBLE);
                holder.artistName.setText(data.artistName);
            }
            return convertView;
        }
    }
    

    ここでは選択不可の項目を設定する際、BindDataのgroupTileが!nullだった場合と
    ルール決めをしている。
    ポイントは63行目〜75行目になり、View#setVisibility(int visibility)を使用し、
    各項目を表示、非表示と切り替える。

    最後にActivityから以下のようにしてadapterを設定する。
    package jp.u1aryz.products.grouptitle;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import android.app.ListActivity;
    import android.os.Bundle;
    
    public class MyActivity extends ListActivity {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            setTitle("アルバム一覧");
    
            List<BindData> list = new ArrayList<BindData>();
            // ここにlistに項目を追加する処理が入る
    
            MyAdapter adapter = new MyAdapter(this, list);
            setListAdapter(adapter);
        }
    }
    

    正直もっといい方法がありそうなので、より良い方法が見つかったら更新するかも。。。

    2011年3月6日日曜日

    ListViewのスクロール位置のあれこれ


    AndroidMarketアプリやYouTubeアプリ、Gmailアプリなどでは
    ListViewを最後までスクロールすると自動的に次の数件を取得し表示される。

    android.widget.AbsListView.OnScrollListener#onScrollを利用すると
    表示されている先頭のインデックス(firstVisibleItem)、
    表示されているリストの数(visibleItemCount)、リストのトータル数(totalItemCount)
    が引数として渡ってくるので、firstVisibleItem + visibleItemCount = totalItemCount
    になる時、最後までスクロールされたと判定出来る。


    上図で赤枠部分が画面に表示されている部分になる。
    最後までスクロールしている右図でfirstVisibleItem + visibleItemCount = totalItemCount
    になっていることがわかる。

    下記がソースの一部。
    ListView listView = (ListView) findViewById(android.R.id.list);
    listView.setOnScrollListener(new OnScrollListener() {
    
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
        }
    
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {
            // 最後までスクロールされたかどうかの判定
            if (totalItemCount == firstVisibleItem + visibleItemCount) {
                // ここに次の数件を取得して表示する処理を書けばいい
            }
        }
    });
    

    リストの追加などでListViewを更新する際、スクロール位置が毎回戻ってしまっては
    ユーザービリティの悪いものになってしまう。
    ListViewのスクロール位置の取得と設定は下記のようにして実現出来る。

    取得
    int position = listView.getFirstVisiblePosition();
    int y = listView.getChildAt(0).getTop();
    

    設定
    listView.setSelectionFromTop(position, y);
    

    インターネット上からデータを取得する際に使用する頻度が多いかも。